it-swarm-id.com

Desain pola atau praktik terbaik untuk skrip Shell

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

161
user14437

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:: -- "[email protected]"`

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 bahwa Anda harus menciptakan strategi yang 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 menyarankan Anda untuk memasukkannya ke dalam 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 python dot.

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 "[email protected]"

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, meskipun 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.

215
Stefano Borini

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 scripting 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.

24
jtimberman

Script 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 .

20
pixelbeat
13
Fhoxh

Mudah: gunakan python daripada skrip Shell. Anda mendapatkan peningkatan keterbacaan hampir 100 kali lipat, tanpa harus menyulitkan apa pun yang tidak Anda butuhkan, dan menjaga kemampuan untuk mengubah bagian skrip Anda menjadi fungsi, objek, objek persisten (zodb), objek terdistribusi (pyro) hampir tanpa kode tambahan.

9
Joao S O Bueno

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

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

9
Paweł Hajdan

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

8
user10392

Untuk menemukan beberapa "praktik terbaik", lihat bagaimana distro Linux (mis. 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 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.

7
Willem