Pola desain atau praktik terbaik untuk skrip shell [ditutup]

167

Apakah ada yang tahu sumber daya apa pun yang berbicara tentang praktik terbaik atau pola desain untuk skrip shell (sh, bash dll)?

pengguna14437
sumber
2
Saya baru saja menulis sebuah artikel kecil tentang pola template di BASH tadi malam. Lihat apa yang Anda pikirkan.
quickshiftin

Jawaban:

222

Saya menulis skrip shell yang cukup kompleks dan saran pertama saya adalah "jangan". Alasannya adalah cukup mudah untuk membuat kesalahan kecil yang menghalangi skrip Anda, atau bahkan membuatnya berbahaya.

Yang mengatakan, saya tidak punya sumber daya lain untuk melewati Anda tetapi pengalaman pribadi saya. Inilah yang biasanya saya lakukan, yang berlebihan, tetapi cenderung solid, meskipun sangat bertele-tele.

Doa

buat skrip Anda menerima opsi panjang dan pendek. hati-hati karena ada dua perintah untuk mengurai opsi, getopt dan getopts. Gunakan getopt karena Anda menghadapi lebih sedikit masalah.

CommandLineOptions__config_file=""
CommandLineOptions__debug_level=""

getopt_results=`getopt -s bash -o c:d:: --long config_file:,debug_level:: -- "$@"`

if test $? != 0
then
    echo "unrecognized option"
    exit 1
fi

eval set -- "$getopt_results"

while true
do
    case "$1" in
        --config_file)
            CommandLineOptions__config_file="$2";
            shift 2;
            ;;
        --debug_level)
            CommandLineOptions__debug_level="$2";
            shift 2;
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "$0: unparseable option $1"
            EXCEPTION=$Main__ParameterException
            EXCEPTION_MSG="unparseable option $1"
            exit 1
            ;;
    esac
done

if test "x$CommandLineOptions__config_file" == "x"
then
    echo "$0: missing config_file parameter"
    EXCEPTION=$Main__ParameterException
    EXCEPTION_MSG="missing config_file parameter"
    exit 1
fi

Poin penting lainnya adalah bahwa suatu program harus selalu mengembalikan nol jika selesai dengan sukses, bukan nol jika terjadi kesalahan.

Panggilan fungsi

Anda dapat memanggil fungsi dalam bash, hanya ingat untuk mendefinisikannya sebelum panggilan. Fungsinya seperti skrip, mereka hanya bisa mengembalikan nilai numerik. Ini berarti Anda harus menemukan strategi berbeda untuk mengembalikan nilai string. Strategi saya adalah menggunakan variabel yang disebut HASIL untuk menyimpan hasilnya, dan mengembalikan 0 jika fungsi selesai dengan bersih. Selain itu, Anda dapat meningkatkan pengecualian jika Anda mengembalikan nilai yang berbeda dari nol, dan kemudian menetapkan dua "variabel pengecualian" (milik saya: EXCEPTION dan EXCEPTION_MSG), yang pertama berisi jenis pengecualian dan yang kedua adalah pesan yang dapat dibaca manusia.

Ketika Anda memanggil suatu fungsi, parameter fungsi ditugaskan ke vars khusus $ 0, $ 1 dll. Saya sarankan Anda untuk memasukkannya ke nama yang lebih bermakna. mendeklarasikan variabel di dalam fungsi sebagai lokal:

function foo {
   local bar="$0"
}

Situasi rawan kesalahan

Di bash, kecuali Anda menyatakan sebaliknya, variabel yang tidak disetel digunakan sebagai string kosong. Ini sangat berbahaya jika terjadi kesalahan ketik, karena variabel yang diketik dengan buruk tidak akan dilaporkan, dan akan dievaluasi sebagai kosong. menggunakan

set -o nounset

untuk mencegah hal ini terjadi. Berhati-hatilah, karena jika Anda melakukan ini, program akan dibatalkan setiap kali Anda mengevaluasi variabel yang tidak ditentukan. Untuk alasan ini, satu-satunya cara untuk memeriksa apakah suatu variabel tidak didefinisikan adalah sebagai berikut:

if test "x${foo:-notset}" == "xnotset"
then
    echo "foo not set"
fi

Anda dapat mendeklarasikan variabel sebagai hanya baca:

readonly readonly_var="foo"

Modularisasi

Anda dapat mencapai modularisasi "python like" jika Anda menggunakan kode berikut:

set -o nounset
function getScriptAbsoluteDir {
    # @description used to get the script path
    # @param $1 the script $0 parameter
    local script_invoke_path="$1"
    local cwd=`pwd`

    # absolute path ? if so, the first character is a /
    if test "x${script_invoke_path:0:1}" = 'x/'
    then
        RESULT=`dirname "$script_invoke_path"`
    else
        RESULT=`dirname "$cwd/$script_invoke_path"`
    fi
}

script_invoke_path="$0"
script_name=`basename "$0"`
getScriptAbsoluteDir "$script_invoke_path"
script_absolute_dir=$RESULT

function import() { 
    # @description importer routine to get external functionality.
    # @description the first location searched is the script directory.
    # @description if not found, search the module in the paths contained in $SHELL_LIBRARY_PATH environment variable
    # @param $1 the .shinc file to import, without .shinc extension
    module=$1

    if test "x$module" == "x"
    then
        echo "$script_name : Unable to import unspecified module. Dying."
        exit 1
    fi

    if test "x${script_absolute_dir:-notset}" == "xnotset"
    then
        echo "$script_name : Undefined script absolute dir. Did you remove getScriptAbsoluteDir? Dying."
        exit 1
    fi

    if test "x$script_absolute_dir" == "x"
    then
        echo "$script_name : empty script path. Dying."
        exit 1
    fi

    if test -e "$script_absolute_dir/$module.shinc"
    then
        # import from script directory
        . "$script_absolute_dir/$module.shinc"
    elif test "x${SHELL_LIBRARY_PATH:-notset}" != "xnotset"
    then
        # import from the shell script library path
        # save the separator and use the ':' instead
        local saved_IFS="$IFS"
        IFS=':'
        for path in $SHELL_LIBRARY_PATH
        do
            if test -e "$path/$module.shinc"
            then
                . "$path/$module.shinc"
                return
            fi
        done
        # restore the standard separator
        IFS="$saved_IFS"
    fi
    echo "$script_name : Unable to find module $module."
    exit 1
} 

Anda kemudian dapat mengimpor file dengan ekstensi .shinc dengan sintaks berikut

impor "AModule / ModuleFile"

Yang akan dicari di SHELL_LIBRARY_PATH. Saat Anda selalu mengimpor di namespace global, ingatlah untuk awalan semua fungsi dan variabel Anda dengan awalan yang tepat, jika tidak, Anda berisiko bentrok nama. Saya menggunakan garis bawah ganda sebagai titik python.

Juga, letakkan ini sebagai hal pertama dalam modul Anda

# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
    return 0
fi
BashInclude__imported=1

Pemrograman berorientasi objek

Dalam bash, Anda tidak dapat melakukan pemrograman berorientasi objek, kecuali jika Anda membangun sistem alokasi objek yang cukup kompleks (saya memikirkannya. Ini layak, tetapi gila). Namun dalam praktiknya, Anda dapat melakukan "Pemrograman Berorientasi Singleton": Anda memiliki satu instance dari setiap objek, dan hanya satu.

Apa yang saya lakukan adalah: saya mendefinisikan objek ke dalam modul (lihat entri modularisasi). Kemudian saya mendefinisikan vars kosong (analog dengan variabel anggota) fungsi init (konstruktor) dan fungsi anggota, seperti dalam kode contoh ini

# avoid double inclusion
if test "${Table__imported+defined}" == "defined"
then
    return 0
fi
Table__imported=1

readonly Table__NoException=""
readonly Table__ParameterException="Table__ParameterException"
readonly Table__MySqlException="Table__MySqlException"
readonly Table__NotInitializedException="Table__NotInitializedException"
readonly Table__AlreadyInitializedException="Table__AlreadyInitializedException"

# an example for module enum constants, used in the mysql table, in this case
readonly Table__GENDER_MALE="GENDER_MALE"
readonly Table__GENDER_FEMALE="GENDER_FEMALE"

# private: prefixed with p_ (a bash variable cannot start with _)
p_Table__mysql_exec="" # will contain the executed mysql command 

p_Table__initialized=0

function Table__init {
    # @description init the module with the database parameters
    # @param $1 the mysql config file
    # @exception Table__NoException, Table__ParameterException

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -ne 0
    then
        EXCEPTION=$Table__AlreadyInitializedException   
        EXCEPTION_MSG="module already initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi


    local config_file="$1"

      # yes, I am aware that I could put default parameters and other niceties, but I am lazy today
      if test "x$config_file" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter config file"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi


    p_Table__mysql_exec="mysql --defaults-file=$config_file --silent --skip-column-names -e "

    # mark the module as initialized
    p_Table__initialized=1

    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0

}

function Table__getName() {
    # @description gets the name of the person 
    # @param $1 the row identifier
    # @result the name

    EXCEPTION=""
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    RESULT=""

    if test $p_Table__initialized -eq 0
    then
        EXCEPTION=$Table__NotInitializedException
        EXCEPTION_MSG="module not initialized"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
    fi

    id=$1

      if test "x$id" = "x"; then
          EXCEPTION=$Table__ParameterException
          EXCEPTION_MSG="missing parameter identifier"
          EXCEPTION_FUNC="$FUNCNAME"
          return 1
      fi

    local name=`$p_Table__mysql_exec "SELECT name FROM table WHERE id = '$id'"`
      if test $? != 0 ; then
        EXCEPTION=$Table__MySqlException
        EXCEPTION_MSG="unable to perform select"
        EXCEPTION_FUNC="$FUNCNAME"
        return 1
      fi

    RESULT=$name
    EXCEPTION=$Table__NoException
    EXCEPTION_MSG=""
    EXCEPTION_FUNC=""
    return 0
}

Menjebak dan menangani sinyal

Saya menemukan ini berguna untuk menangkap dan menangani pengecualian.

function Main__interruptHandler() {
    # @description signal handler for SIGINT
    echo "SIGINT caught"
    exit
} 
function Main__terminationHandler() { 
    # @description signal handler for SIGTERM
    echo "SIGTERM caught"
    exit
} 
function Main__exitHandler() { 
    # @description signal handler for end of the program (clean or unclean). 
    # probably redundant call, we already call the cleanup in main.
    exit
} 

trap Main__interruptHandler INT
trap Main__terminationHandler TERM
trap Main__exitHandler EXIT

function Main__main() {
    # body
}

# catch signals and exit
trap exit INT TERM EXIT

Main__main "$@"

Petunjuk dan kiat

Jika sesuatu tidak berhasil karena suatu alasan, cobalah untuk menyusun ulang kode. Ketertiban itu penting dan tidak selalu intuitif.

bahkan tidak mempertimbangkan untuk bekerja dengan tcsh. itu tidak mendukung fungsi, dan itu mengerikan secara umum.

Semoga ini bisa membantu, walaupun harap diperhatikan. Jika Anda harus menggunakan hal-hal yang saya tulis di sini, itu berarti masalah Anda terlalu rumit untuk diselesaikan dengan shell. gunakan bahasa lain. Saya harus menggunakannya karena faktor manusia dan warisan.

Stefano Borini
sumber
7
Wow, dan saya pikir saya akan melakukan terlalu banyak dalam bash ... Saya cenderung menggunakan fungsi yang terisolasi dan penyalahgunaan subkulit (jadi saya menderita ketika kecepatan dengan cara apa pun relevan). Tidak ada variabel global yang pernah ada, baik dalam maupun luar (untuk mempertahankan sisa kewarasan). Semua kembali melalui stdout atau output file. set -u / set -e (terlalu buruk set -e menjadi tidak berguna begitu pertama jika, dan sebagian besar kode saya sering ada di sana). Argumen fungsi diambil dengan [local something = "$ 1"; shift] (memungkinkan untuk pemesanan ulang dengan mudah saat refactoring). Setelah satu 3000 jalur menampar naskah saya cenderung untuk menulis bahkan skrip terkecil dalam mode ini ...
Eugene
koreksi kecil untuk modularisasi: 1 Anda perlu pengembalian setelah. "$ script_absolute_dir / $ module.shinc" untuk menghindari peringatan yang hilang. 2 Anda harus menetapkan IFS = "$ disimpan_IFS" sebelum pengembalian Anda menemukan modul di $ SHELL_LIBRARY_PATH
Duff
"faktor manusia" adalah yang terburuk. Mesin tidak melawan Anda ketika Anda memberi mereka sesuatu yang lebih baik.
jeremyjjbrown
1
Mengapa getoptvs getopts? getoptslebih portabel dan berfungsi di semua shell POSIX. Terutama karena pertanyaannya adalah praktik terbaik shell, bukan khusus praktik terbaik bash, saya akan mendukung kepatuhan POSIX untuk mendukung beberapa shell jika memungkinkan.
Wimateeka
1
terima kasih telah menawarkan semua saran untuk skrip shell meskipun Anda jujur: "Semoga ini membantu, meskipun harap dicatat. Jika Anda harus menggunakan hal-hal yang saya tulis di sini, itu berarti bahwa masalah Anda terlalu rumit untuk diselesaikan dengan shell. gunakan bahasa lain. Saya harus menggunakannya karena faktor manusia dan warisan. "
dieHellste
25

Lihatlah Panduan Bash-Scripting Lanjutan untuk banyak kebijaksanaan tentang skrip shell - bukan hanya Bash, baik.

Jangan dengarkan orang yang memberi tahu Anda untuk melihat bahasa lain yang bisa dibilang lebih rumit. Jika skrip shell memenuhi kebutuhan Anda, gunakan itu. Anda menginginkan fungsionalitas, bukan fantasi. Bahasa baru memberikan keterampilan baru yang berharga untuk resume Anda, tetapi itu tidak membantu jika Anda memiliki pekerjaan yang perlu dilakukan dan Anda sudah tahu shell.

Seperti yang dinyatakan, tidak ada banyak "praktik terbaik" atau "pola desain" untuk skrip shell. Penggunaan yang berbeda memiliki pedoman dan bias yang berbeda - seperti bahasa pemrograman lainnya.

jtimberman
sumber
9
Perhatikan bahwa untuk skrip dengan kompleksitas yang bahkan sedikit, itu BUKAN praktik terbaik. Pengkodean bukan hanya tentang mendapatkan sesuatu untuk bekerja. Ini adalah tentang membangunnya dengan cepat, mudah, dan menjadi dapat diandalkan, dapat digunakan kembali, dan mudah dibaca dan dipelihara (terutama untuk orang lain). Skrip shell tidak skala dengan baik ke level apa pun. Bahasa yang lebih kuat jauh lebih sederhana untuk proyek dengan logika apa pun.
drifter
20

skrip shell adalah bahasa yang dirancang untuk memanipulasi file dan proses. Meskipun bagus untuk itu, itu bukan bahasa tujuan umum, jadi selalu mencoba untuk merekatkan logika dari utilitas yang ada daripada menciptakan kembali logika baru dalam skrip shell.

Selain itu prinsip umum saya telah mengumpulkan beberapa kesalahan skrip shell umum .

pixelbeat
sumber
11

Tahu kapan harus menggunakannya. Untuk perintah perekatan yang cepat dan kotor tidak masalah. Jika Anda perlu membuat lebih dari beberapa keputusan non-sepele, loop, apa pun, gunakan Python, Perl, dan modularisasi .

Masalah terbesar dengan shell seringkali adalah hasil akhirnya hanya tampak seperti bola lumpur besar, 4000 baris bash dan terus bertambah ... dan Anda tidak dapat menyingkirkannya karena sekarang seluruh proyek Anda bergantung padanya. Tentu saja, itu dimulai pada 40 baris pesta indah.

Paweł Hajdan
sumber
9

Mudah: gunakan python bukan skrip shell. Anda mendapatkan peningkatan keterbacaan hampir 100 kali lipat, tanpa harus menyulitkan apa pun yang tidak Anda butuhkan, dan mempertahankan kemampuan untuk mengembangkan bagian skrip Anda menjadi fungsi, objek, objek persisten (zodb), objek terdistribusi (piro) hampir tanpa ada kode tambahan.


sumber
7
Anda membantah diri sendiri dengan mengatakan "tanpa harus menyulitkan" dan kemudian mendaftar berbagai kompleksitas yang Anda pikir menambah nilai, sementara dalam banyak kasus disalahgunakan menjadi monster jelek alih-alih digunakan untuk menyederhanakan masalah dan implementasi.
Evgeny
3
ini menyiratkan kelemahan besar, skrip Anda tidak akan portabel pada sistem di mana python tidak ada
astropanic
1
Saya menyadari ini dijawab pada '08 (sekarang dua hari sebelum '12); namun, bagi mereka yang melihatnya bertahun-tahun kemudian, saya akan memperingatkan siapa pun untuk tidak menggunakan bahasa seperti Python atau Ruby karena lebih mungkin tersedia dan jika tidak, ini adalah perintah (atau beberapa klik) dari pemasangan . Jika Anda membutuhkan portabilitas lebih lanjut, pikirkan tentang menulis program Anda di Java karena Anda akan kesulitan menemukan mesin yang tidak memiliki JVM.
Wil Moore III
@astropanic hampir semua port Linux dengan Python saat ini
Pithikos
@Pithikos, tentu saja, dan mengutak-atik kerumitan python2 vs python3. Saat ini saya merapikan semua alat saya dengan pergi, dan tidak bisa lebih bahagia.
astropan
9

gunakan set -e agar Anda tidak terus maju setelah kesalahan. Cobalah membuatnya kompatibel tanpa mengandalkan bash jika Anda ingin dijalankan di bukan-linux.

pengguna10392
sumber
7

Untuk menemukan "praktik terbaik", lihat bagaimana distro Linux (misalnya Debian) menulis skrip init mereka (biasanya ditemukan di /etc/init.d)

Sebagian besar dari mereka tanpa "bash-isme" dan memiliki pemisahan yang baik dari pengaturan konfigurasi, file perpustakaan dan pemformatan sumber.

Gaya pribadi saya adalah menulis naskah-master yang mendefinisikan beberapa variabel default, dan kemudian mencoba memuat ("sumber") file konfigurasi yang mungkin berisi nilai-nilai baru.

Saya mencoba untuk menghindari fungsi karena cenderung membuat skrip lebih rumit. (Perl diciptakan untuk tujuan itu.)

Untuk memastikan skripnya portabel, uji tidak hanya dengan #! / Bin / sh, tetapi juga gunakan #! / Bin / ash, #! / Bin / dash, dll. Anda akan segera menemukan kode spesifik Bash.

Willem
sumber
-1

Atau kutipan yang lebih lama mirip dengan apa yang dikatakan Joao:

"Gunakan perl. Kamu ingin tahu bash tetapi tidak menggunakannya."

Sayangnya saya lupa siapa yang mengatakan itu.

Dan ya hari ini saya akan merekomendasikan python over perl.

Sarien
sumber