Kabuk komut dosyaları için tasarım kalıpları veya en iyi uygulamalar [kapalı]


167

Herkes kabuk komut dosyaları (sh, bash vb.) İçin en iyi uygulamalar veya tasarım kalıpları hakkında konuşan herhangi bir kaynak biliyor mu?


2
Dün gece BASH'deki şablon deseniyle ilgili küçük bir makale yazdım . Ne düşündüğünüzü görün.
quickshiftin

Yanıtlar:


222

Oldukça karmaşık kabuk betikleri yazdım ve ilk önerim "yapma" dır. Bunun nedeni, senaryonuzu engelleyen küçük bir hata yapmak ve hatta tehlikeli hale getirmek oldukça kolaydır.

Bununla birlikte, sizi geçirecek başka kaynağım yok, kişisel deneyimim var. İşte normalde yaptığım şey, aşırı kilolu, ancak çok ayrıntılı olmasına rağmen katı olma eğilimindedir .

yakarma

betiğinizin uzun ve kısa seçenekleri kabul etmesini sağlayın. dikkatli olun çünkü seçenekleri ayrıştırmak için iki komut vardır, getopt ve getopts. Daha az sorunla karşılaştığınızda getopt kullanın.

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

Bir başka önemli nokta, bir programın başarılı bir şekilde tamamlanması durumunda daima sıfır, bir şeyler ters gittiğinde sıfırdan farklı olması gerektiğidir.

İşlev çağrıları

Bash işlevlerini çağırabilirsiniz, çağrıdan önce tanımlamayı unutmayın. İşlevler komut dosyaları gibidir, yalnızca sayısal değerler döndürebilirler. Bu, dize değerlerini döndürmek için farklı bir strateji icat etmeniz gerektiği anlamına gelir. Stratejim, sonucu saklamak için RESULT adlı bir değişken kullanmak ve işlev temiz şekilde tamamlandıysa 0 döndürmektir. Ayrıca, sıfırdan farklı bir değer döndürüyorsanız istisnaları yükseltebilir ve sonra birincisi istisna türünü ve ikincisi okunabilir mesajı içeren iki "istisna değişkeni" (mine: EXCEPTION ve EXCEPTION_MSG) ayarlayabilirsiniz.

Bir işlevi çağırdığınızda, işlevin parametreleri $ 0, $ 1 vb. Özel değişkenlere atanır. Onları daha anlamlı adlara koymanızı öneririm. işlevin içindeki değişkenleri yerel olarak bildirir:

function foo {
   local bar="$0"
}

Hata eğilimli durumlar

Bash'da, aksi belirtilmedikçe, boş bir dize olarak ayarlanmamış bir değişken kullanılır. Yazım hatası durumunda bu çok tehlikelidir, çünkü kötü yazılan değişken rapor edilmeyecektir ve boş olarak değerlendirilecektir. kullanım

set -o nounset

bunun olmasını önlemek için. Ancak dikkatli olun, çünkü bunu yaparsanız tanımsız bir değişkeni her değerlendirdiğinizde program iptal edilir. Bu nedenle, bir değişkenin tanımlanıp tanımlanmadığını kontrol etmenin tek yolu şudur:

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

Değişkenleri salt okunur olarak bildirebilirsiniz:

readonly readonly_var="foo"

modularization

Aşağıdaki kodu kullanırsanız "python benzeri" modülerleştirme elde edebilirsiniz:

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
} 

Daha sonra .shinc uzantılı dosyaları aşağıdaki sözdizimiyle içe aktarabilirsiniz

"AModule / ModuleFile" dosyasını içe aktar

Hangisi SHELL_LIBRARY_PATH içinde aranacak. Genel ad alanında her zaman içe aktardığınız için, tüm işlevlerinize ve değişkenlerinize uygun bir önek önekini eklemeyi unutmayın, aksi takdirde isim çakışması riskiyle karşı karşıya kalırsınız. Python dot olarak çift alt çizgi kullanıyorum.

Ayrıca, bunu modülünüzde ilk şey olarak koyun

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

Nesne yönelimli programlama

Bash'da, nesnelerin tahsis edilmesi için oldukça karmaşık bir sistem oluşturmadıkça, nesne yönelimli programlama yapamazsınız (bunu düşündüm. Bu mümkün, ancak çılgınca). Ancak pratikte "Singleton odaklı programlama" yapabilirsiniz: her nesnenin bir örneği ve sadece bir tane vardır.

Yaptığım şey: Modüle bir nesne tanımlamak (modülerleştirme girişine bakın). Sonra boş vars (üye değişkenlere benzer) bir init fonksiyonu (yapıcı) ve üye fonksiyonlarını tanımlarım, bu örnek kodda olduğu gibi

# 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
}

Yakalama ve işleme sinyalleri

Bunu istisnaları yakalamak ve ele almak için yararlı buldum.

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 "$@"

İpuçları ve püf noktaları

Bir şey herhangi bir nedenle çalışmazsa kodu yeniden sıralamayı deneyin. Düzen önemlidir ve her zaman sezgisel değildir.

tcsh ile çalışmayı bile düşünmeyin. işlevleri desteklemez ve genel olarak korkunçtur.

Umarım yardımcı olur, lütfen unutmayın. Burada yazdığım şeyleri kullanmak zorunda kalırsanız, bu sorununuzun kabukla çözülemeyecek kadar karmaşık olduğu anlamına gelir. başka bir dil kullanın. İnsan faktörleri ve mirası nedeniyle kullanmak zorunda kaldım.


7
Vay be, ve bash'da aşırıya kaçacağımı sanıyordum ... İzole işlevleri ve alt kabukları kötüye kullanma eğilimindeyim (böylece hız herhangi bir şekilde alakalı olduğunda acı çekerim). Ne içeride ne de dışarıda (akıl sağlığını korumak için) hiçbir küresel değişken yoktur. Tümü stdout veya dosya çıktısıyla döner. set -u / set -e (çok kötü set -e en kısa sürede işe yaramaz hale gelir ve kodumun çoğu genellikle oradaysa). [Local something = "$ 1" ile alınan işlev bağımsız değişkenleri; shift] (yeniden düzenleme sırasında kolay yeniden sıralama sağlar). Bir 3000 satır bash betiğinden sonra bu tarzda en küçük komut dosyalarını bile yazma eğilimindeyim ...
Eugene

modülerleştirme için küçük düzeltmeler: 1 sonra bir dönüşe ihtiyacınız var. "$ script_absolute_dir / $ module.shinc" uyarısını kaçırmamak için. 2 modül bulma getirinizden önce $ SHELL_LIBRARY_PATH
Duff

"insan faktörleri" en kötüleridir. Makineler onlara daha iyi bir şey verdiğinizde savaşmaz.
jeremyjjbrown

1
Neden getoptvs getopts? getoptsdaha taşınabilir ve herhangi bir POSIX kabuğunda çalışır. Özellikle soru, özellikle bash en iyi uygulamaları yerine mermi en iyi uygulamaları olduğundan, mümkün olduğunda çoklu mermileri desteklemek için POSIX uyumluluğunu destekleyeceğim.
Wimateeka

1
dürüst olmanıza rağmen kabuk komut dosyası oluşturma için tüm tavsiyeleri sunduğunuz için teşekkür ederiz: "Umarım yardımcı olur, lütfen unutmayın. Burada yazdığım şeyleri kullanmak zorunda kalırsanız, probleminiz çözülemeyecek kadar karmaşık demektir Başka bir dil kullan. İnsan faktörleri ve miras nedeniyle kullanmak zorunda kaldım. "
dieHellste

25

Sadece Bash için değil, kabuk komut dosyalarında çok fazla bilgelik için Gelişmiş Bash-Scripting Rehberine bir göz atın .

Başka, tartışmasız daha karmaşık dillere bakmanızı söyleyen insanları dinlemeyin. Kabuk komut dosyası oluşturma gereksinimlerinizi karşılıyorsa, bunu kullanın. Fantezi değil işlevsellik istiyorsun. Yeni diller özgeçmişiniz için değerli yeni beceriler sağlar, ancak yapılması gereken bir işiniz varsa ve zaten kabuk biliyorsanız, bu yardımcı olmaz.

Belirtildiği gibi, kabuk komut dosyası oluşturma için pek çok "en iyi uygulama" veya "tasarım deseni" yoktur. Farklı kullanımlar, diğer programlama dilleri gibi farklı yönergelere ve önyargılara sahiptir.


9
Hafif karmaşıklığa sahip komut dosyaları için bu en iyi uygulama değildir. Kodlama sadece işe yarayacak bir şey değil. Hızlı, kolay bir şekilde inşa etmekle ilgilidir ve güvenilir, tekrar kullanılabilir ve okunması ve bakımı kolay (özellikle diğerleri için). Kabuk betikleri hiçbir düzeyde iyi ölçeklenmez. Daha sağlam diller, herhangi bir mantığı olan projeler için çok daha basittir.
drifter

20

kabuk betiği dosyaları ve işlemleri yönetmek için tasarlanmış bir dildir. Bunun için harika olsa da, genel amaçlı bir dil değildir, bu nedenle kabuk komut dosyasında yeni mantık yeniden oluşturmak yerine her zaman mevcut yardımcı programlardan mantığı yapıştırmaya çalışın.

Bu genel prensip dışında bazı yaygın kabuk senaryo hataları topladım .



11

Ne zaman kullanacağınızı bilin. Hızlı ve kirli yapıştırma komutları için sorun değil. Önemsiz birkaç karardan daha fazlasına ihtiyacınız varsa, döngüler, herhangi bir şey, Python, Perl için gidin ve modülerleştirin .

Kabuk ile ilgili en büyük sorun, genellikle nihai sonucun sadece büyük bir çamur topu, 4000 satır bash ve büyüyen gibi görünmesidir ... ve bundan kurtulamazsınız çünkü şimdi tüm projeniz buna bağlı. Tabii ki, 40 satır güzel bash başladı.


9

Kolay: kabuk komut dosyaları yerine python kullanın. Gereksinim duymadığınız herhangi bir şeyi karmaşıklaştırmak zorunda kalmadan ve komut dosyanızın parçalarını neredeyse hiç olmadan işlevlere, nesnelere, kalıcı nesnelere (zodb), dağıtılmış nesnelere (pyro) evrimleşme yeteneğini koruyarak, okunabilirlikte yaklaşık 100 kat artış elde edersiniz ekstra kod.


7
"karmaşıklaştırmak zorunda kalmadan" diyerek ve sonra değer katacağınızı düşündüğünüz çeşitli karmaşıklıkları listeleyerek kendinizle çelişirken, çoğu durumda sorunları ve uygulamayı basitleştirmek için değil, çirkin canavarlara taciz edilirsiniz.
Evgeny

3
Bu büyük bir dezavantaj anlamına gelir, komut dosyalarınız
python'un

1
Bunun '08'de yanıtlandığını fark ediyorum (şimdi '12'den iki gün önce); Ancak, bu yıllara bakanlar için, Python veya Ruby gibi dillere geri dönmelerine karşı herkesi uyarıyorum çünkü mevcut olması daha muhtemeldir ve eğer değilse, kurulmasından uzak bir komut (veya çift tıklama) . Daha fazla taşınabilirliğe ihtiyacınız varsa, JVM bulunmayan bir makine bulmak için zorlanacağınız için programınızı Java'da yazmayı düşünün.
Wil Moore III

@astropanic bugünlerde Python ile tüm Linux portları
Pithikos

@Pithikos, emin ve python2 vs python3 güçlük ile keman. Bugünlerde tüm araçlarımı go ile yazıyorum ve daha mutlu olamıyorum.
astropanik

9

hatalardan sonra ileri sürülmemek için set -e komutunu kullanın. Linux'ta çalışmasını istiyorsanız bash'a güvenmeden sh uyumlu hale getirmeyi deneyin.


7

Bazı "en iyi uygulamaları" bulmak için Linux dağıtım şirketlerinin (örneğin Debian) init komut dosyalarını nasıl yazdıklarına bakın (genellikle /etc/init.d dosyasında bulunur)

Çoğu "bash-isms" içermez ve yapılandırma ayarları, kütüphane dosyaları ve kaynak biçimlendirmesi arasında iyi bir ayrım vardır.

Kişisel stilim, bazı varsayılan değişkenleri tanımlayan ve daha sonra yeni değerler içerebilecek bir yapılandırma dosyasını yüklemeye ("kaynak") yüklemeye çalışan bir master-shellscript yazmaktır.

Komut dosyasını daha karmaşık hale getirme eğilimindeyken işlevlerden kaçınmaya çalışıyorum. (Perl bu amaçla yaratıldı.)

Betiğin taşınabilir olduğundan emin olmak için, sadece #! / Bin / sh ile değil, aynı zamanda #! / Bin / ash, #! / Bin / dash, vb. İle de test edin. Bash'e özgü kodu yakında bulabilirsiniz.


-1

Veya Joao'nun söylediklerine benzer eski alıntı:

"Perl kullan. Bash'i bilmek isteyeceksiniz, ama kullanmıyorsunuz."

Ne yazık ki bunu kimin söylediğini unuttum.

Ve evet bu günlerde perl üzerinde python öneriyoruz.

Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.