Wzorce projektowe lub najlepsze praktyki dotyczące skryptów powłoki [zamknięte]

167

Czy ktoś zna jakieś zasoby, które mówią o najlepszych praktykach lub wzorcach projektowych dla skryptów powłoki (sh, bash itp.)?

user14437
źródło
2
Właśnie napisałem wczoraj wieczorem mały artykuł o wzorach szablonów w BASH . Zobacz, co myślisz.
quickshiftin

Odpowiedzi:

222

Napisałem dość złożone skrypty powłoki i moja pierwsza sugestia brzmi „nie rób tego”. Powodem jest to, że dość łatwo jest popełnić mały błąd, który utrudnia skrypt, a nawet czyni go niebezpiecznym.

To powiedziawszy, nie mam innych zasobów, aby ci przekazać, ale moje osobiste doświadczenie. Oto, co zwykle robię, co jest przesadą, ale zwykle jest solidne, chociaż bardzo rozwlekłe.

Wezwanie

spraw, aby twój skrypt akceptował długie i krótkie opcje. bądź ostrożny, ponieważ istnieją dwa polecenia do analizowania opcji, getopt i getopts. Użyj getopt, gdy będziesz mieć mniej problemów.

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

Inną ważną kwestią jest to, że program powinien zawsze zwracać zero, jeśli zakończy się pomyślnie, niezerowe, jeśli coś poszło nie tak.

Wywołania funkcji

Możesz wywoływać funkcje w bashu, pamiętaj tylko, aby zdefiniować je przed wywołaniem. Funkcje są jak skrypty, mogą zwracać tylko wartości liczbowe. Oznacza to, że musisz wymyślić inną strategię zwracania wartości ciągów. Moja strategia polega na użyciu zmiennej o nazwie RESULT do przechowywania wyniku i zwracaniu 0, jeśli funkcja zakończyła się poprawnie. Możesz także zgłosić wyjątki, jeśli zwracasz wartość różną od zera, a następnie ustawić dwie „zmienne wyjątków” (moje: EXCEPTION i EXCEPTION_MSG), pierwsza zawiera typ wyjątku, a druga czytelną dla człowieka wiadomość.

Kiedy wywołujesz funkcję, parametry funkcji są przypisywane do specjalnych zmiennych $ 0, $ 1 itd. Proponuję nadać im bardziej znaczące nazwy. zadeklaruj zmienne wewnątrz funkcji jako lokalne:

function foo {
   local bar="$0"
}

Sytuacje podatne na błędy

W bash, chyba że zadeklarujesz inaczej, nieustawiona zmienna jest używana jako pusty łańcuch. Jest to bardzo niebezpieczne w przypadku literówki, ponieważ źle wpisana zmienna nie zostanie zgłoszona i zostanie oceniona jako pusta. posługiwać się

set -o nounset

aby temu zapobiec. Uważaj jednak, ponieważ jeśli to zrobisz, program przerwie działanie za każdym razem, gdy oceniasz niezdefiniowaną zmienną. Z tego powodu jedynym sposobem sprawdzenia, czy zmienna nie jest zdefiniowana, jest:

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

Możesz zadeklarować zmienne jako tylko do odczytu:

readonly readonly_var="foo"

Modularyzacja

Możesz osiągnąć modularyzację "podobną do Pythona", używając następującego kodu:

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
} 

możesz następnie importować pliki z rozszerzeniem .shinc z następującą składnią

import „AModule / ModuleFile”

Które będą przeszukiwane w SHELL_LIBRARY_PATH. Ponieważ zawsze importujesz w globalnej przestrzeni nazw, pamiętaj, aby poprzedzać wszystkie funkcje i zmienne odpowiednim prefiksem, w przeciwnym razie ryzykujesz konfliktami nazw. Używam podwójnego podkreślenia jako kropki Pythona.

Ponadto umieść to jako pierwszą rzecz w swoim module

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

Programowanie obiektowe

W bashu nie możesz robić programowania obiektowego, chyba że zbudujesz dość złożony system alokacji obiektów (myślałem o tym. Jest to wykonalne, ale szalone). W praktyce możesz jednak wykonać „programowanie zorientowane na singleton”: masz jedną instancję każdego obiektu i tylko jedną.

Co robię, to: definiuję obiekt w module (patrz wpis modularyzacji). Następnie definiuję puste zmienne (analogiczne do zmiennych składowych), funkcję init (konstruktor) i funkcje składowe, jak w tym przykładowym kodzie

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

Wychwytywanie i obsługa sygnałów

Uważam, że jest to przydatne do wychwytywania i obsługi wyjątków.

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

Porady i wskazówki

Jeśli z jakiegoś powodu coś nie działa, spróbuj ponownie zamówić kod. Porządek jest ważny i nie zawsze intuicyjny.

nawet nie myśl o pracy z tcsh. nie obsługuje funkcji i ogólnie jest okropny.

Mam nadzieję, że to pomoże, chociaż pamiętaj. Jeśli musisz użyć rzeczy, które tutaj napisałem, oznacza to, że twój problem jest zbyt złożony, aby można go było rozwiązać za pomocą powłoki. używać innego języka. Musiałem go użyć ze względu na czynniki ludzkie i dziedzictwo.

Stefano Borini
źródło
7
Wow, i pomyślałem, że idę na przesadę w bashu ... Zwykle używam izolowanych funkcji i nadużywam podpowłok (dlatego cierpię, gdy prędkość jest w jakikolwiek sposób istotna). Żadnych zmiennych globalnych nigdy, ani w, ani na zewnątrz (aby zachować resztki zdrowia psychicznego). Wszystko wraca przez standardowe wyjście lub plik wyjściowy. set -u / set -e (szkoda, że ​​set -e staje się bezużyteczne, gdy tylko pierwsze if, a większość mojego kodu często tam jest). Argumenty funkcji pobrane z [lokalne coś = "$ 1"; shift] (pozwala na łatwą zmianę kolejności podczas refaktoryzacji). Po jednym 3000 linijek skryptu bash mam tendencję do pisania nawet najmniejszych skryptów w ten sposób ...
Eugene
małe poprawki do modularyzacji: 1, po którym potrzebujesz zwrotu. „$ script_absolute_dir / $ module.shinc”, aby uniknąć pominięcia ostrzeżenia. 2 musisz ustawić IFS = "$ zapisany_IFS" przed powrotem do znajdowania modułu w $ SHELL_LIBRARY_PATH
Duff
Najgorsze są „czynniki ludzkie”. Maszyny nie walczą z tobą, gdy dajesz im coś lepszego.
jeremyjjbrown
1
Dlaczego getoptvs getopts? getoptsjest bardziej przenośny i działa w każdej powłoce POSIX. Zwłaszcza, że ​​pytanie dotyczy najlepszych praktyk powłoki, a nie tylko najlepszych praktyk basha, poparłbym zgodność z POSIX, aby obsługiwać wiele powłok, jeśli to możliwe.
Wimateeka,
1
dziękuję za udzielenie wszystkich rad dotyczących skryptów powłoki, mimo że jesteś szczery: „Mam nadzieję, że to pomoże, chociaż pamiętaj. Jeśli musisz skorzystać z tego, co tutaj napisałem, oznacza to, że Twój problem jest zbyt złożony, aby go rozwiązać powłoki. użyj innego języka. Musiałem go używać ze względu na czynniki ludzkie i dziedzictwo ”.
dieHellste
25

Zapoznaj się z Przewodnikiem po zaawansowanych skryptach Bash aby uzyskać wiele informacji na temat skryptów powłoki - nie tylko Bash.

Nie słuchaj, jak ludzie mówią ci, żebyś spojrzał na inne, prawdopodobnie bardziej złożone języki. Jeśli skrypt powłoki spełnia Twoje potrzeby, użyj tego. Chcesz funkcjonalności, a nie elegancji. Nowe języki zapewniają cenne nowe umiejętności w Twoim CV, ale to nie pomaga, jeśli masz pracę do wykonania i znasz już powłokę Shell.

Jak już wspomniano, nie ma wielu „najlepszych praktyk” ani „wzorców projektowych” dotyczących skryptów powłoki. Różne zastosowania mają różne wytyczne i odchylenia - jak każdy inny język programowania.

jtimberman
źródło
9
Zauważ, że w przypadku skryptów o nawet niewielkiej złożoności NIE jest to najlepsza praktyka. W kodowaniu nie chodzi tylko o to, żeby coś zadziałało. Chodzi o szybkie i łatwe zbudowanie, niezawodność, możliwość wielokrotnego użytku oraz łatwość czytania i konserwacji (zwłaszcza dla innych). Skrypty powłoki nie skalują się dobrze do żadnego poziomu. Solidniejsze języki są znacznie prostsze w przypadku projektów z dowolną logiką.
włóczęga
20

skrypt powłoki to język przeznaczony do manipulowania plikami i procesami. Chociaż jest do tego świetny, nie jest to język ogólnego przeznaczenia, więc zawsze staraj się sklejać logikę z istniejących narzędzi, zamiast odtwarzać nową logikę w skrypcie powłoki.

Oprócz tej ogólnej zasady zebrałem kilka typowych błędów w skryptach powłoki .

pixelbeat
źródło
11

Wiedz, kiedy go używać. W przypadku szybkich i brudnych poleceń sklejania jest w porządku. Jeśli potrzebujesz więcej niż kilka nietrywialnych decyzji, pętli, czegokolwiek, wybierz Python, Perl i modularyzację .

Największym problemem związanym z powłoką jest często to, że efekt końcowy wygląda jak wielka kula błota, 4000 linii uderzenia i rośnie ... i nie możesz się jej pozbyć, ponieważ teraz cały twój projekt od tego zależy. Oczywiście zaczęło się od 40 linii pięknego basha.

Paweł Hajdan
źródło
9

Łatwe: używaj Pythona zamiast skryptów powłoki. Otrzymujesz prawie 100-krotny wzrost czytelności, bez konieczności komplikowania czegokolwiek, czego nie potrzebujesz, i zachowując możliwość przekształcania części twojego skryptu w funkcje, obiekty, trwałe obiekty (zodb), rozproszone obiekty (pyro) prawie bez żadnych dodatkowy kod.


źródło
7
zaprzeczasz sobie, mówiąc „bez komplikowania”, a następnie wymieniając różne zawiłości, które Twoim zdaniem dodają wartości, podczas gdy w większości przypadków są wykorzystywane do tworzenia brzydkich potworów, a nie do upraszczania problemów i wdrażania.
Evgeny
3
oznacza to wielką wadę, twoje skrypty nie będą przenośne w systemach, w których nie ma Pythona
astropaniczny
1
Zdaję sobie sprawę, że odpowiedź na to pytanie pojawiła się w '08 (teraz są dwa dni przed '12); Jednak dla tych, którzy patrzą na to lata później, ostrzegłbym każdego przed odwracaniem się od języków takich jak Python lub Ruby, ponieważ jest bardziej prawdopodobne, że jest dostępny, a jeśli nie, jest to polecenie (lub kilka kliknięć) od zainstalowania . Jeśli potrzebujesz większej przenośności, pomyśl o napisaniu programu w Javie, ponieważ trudno będzie znaleźć maszynę, na której nie ma dostępnej maszyny JVM.
Wil Moore III
@astropanic obecnie prawie wszystkie porty Linuksa z Pythonem
Pithikos
@Pithikos, pewnie, i baw się przy kłopotach z python2 i python3. Obecnie wszystkie narzędzia piszę bez trudu i nie mogę być szczęśliwszy.
astropaniczny
9

użyj set -e, aby nie pchać do przodu po błędach. Spróbuj uczynić go kompatybilnym bez polegania na bash, jeśli chcesz, aby działał na innym systemie niż Linux.

user10392
źródło
7

Aby znaleźć "najlepsze praktyki", spójrz, jak dystrybucje Linuksa (np. Debian) piszą swoje skrypty inicjujące (zwykle znajdują się w /etc/init.d)

Większość z nich nie zawiera "bash-izmów" i ma dobrą separację ustawień konfiguracyjnych, plików bibliotek i formatowania źródła.

Moim osobistym stylem jest napisanie głównego skryptu powłoki, który definiuje niektóre domyślne zmienne, a następnie próbuje załadować („źródło”) plik konfiguracyjny, który może zawierać nowe wartości.

Staram się unikać funkcji, ponieważ powodują one, że skrypt jest bardziej skomplikowany. (Perl został stworzony w tym celu.)

Aby upewnić się, że skrypt jest przenośny, przetestuj nie tylko z #! / Bin / sh, ale także użyj #! / Bin / ash, #! / Bin / dash itp. Wkrótce zauważysz specyficzny kod Bash.

Willem
źródło
-1

Lub starszy cytat podobny do tego, co powiedział Joao:

„Użyj perla. Będziesz chciał znać bash, ale go nie używać”.

Niestety zapomniałem, kto to powiedział.

I tak, obecnie polecałbym Pythona zamiast Perla.

Sarien
źródło