Padrões de design ou práticas recomendadas para scripts de shell [fechados]

167

Alguém conhece algum recurso que fale sobre práticas recomendadas ou padrões de design para scripts de shell (sh, bash etc.)?

user14437
fonte
2
Acabei de escrever um pequeno artigo sobre padrão de modelo no BASH ontem à noite. Veja o que você pensa.
usar o seguinte código

Respostas:

222

Escrevi scripts shell bastante complexos e minha primeira sugestão é "não". O motivo é que é bastante fácil cometer um pequeno erro que atrapalha o seu script ou até mesmo o torna perigoso.

Dito isto, não tenho outros recursos para passar, mas minha experiência pessoal. Aqui está o que eu normalmente faço, que é um exagero, mas tende a ser sólido, embora muito detalhado.

Invocação

faça seu script aceitar opções longas e curtas. tenha cuidado porque existem dois comandos para analisar opções, getopt e getopts. Use getopt ao enfrentar menos problemas.

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

Outro ponto importante é que um programa sempre deve retornar zero se for concluído com êxito, diferente de zero se algo der errado.

Chamadas de função

Você pode chamar funções no bash, lembre-se de defini-las antes da chamada. As funções são como scripts, elas podem retornar apenas valores numéricos. Isso significa que você precisa inventar uma estratégia diferente para retornar valores de sequência. Minha estratégia é usar uma variável chamada RESULT para armazenar o resultado e retornar 0 se a função for concluída corretamente. Além disso, você pode gerar exceções se estiver retornando um valor diferente de zero e definir duas "variáveis ​​de exceção" (mina: EXCEPTION e EXCEPTION_MSG), a primeira contendo o tipo de exceção e a segunda uma mensagem legível por humanos.

Quando você chama uma função, os parâmetros da função são atribuídos aos vars especiais $ 0, $ 1 etc. Sugiro que você os coloque em nomes mais significativos. declare as variáveis ​​dentro da função como local:

function foo {
   local bar="$0"
}

Situações propensas a erros

No bash, a menos que você declare o contrário, uma variável não definida é usada como uma sequência vazia. Isso é muito perigoso no caso de erros de digitação, pois a variável mal digitada não será relatada e será avaliada como vazia. usar

set -o nounset

para impedir que isso aconteça. Porém, tenha cuidado, pois se você fizer isso, o programa será interrompido toda vez que você avaliar uma variável indefinida. Por esse motivo, a única maneira de verificar se uma variável não está definida é a seguinte:

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

Você pode declarar variáveis ​​como somente leitura:

readonly readonly_var="foo"

Modularização

Você pode obter a modularização "semelhante a python" se usar o seguinte código:

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
} 

você pode importar arquivos com a extensão .shinc com a seguinte sintaxe

importar "AModule / ModuleFile"

O qual será pesquisado em SHELL_LIBRARY_PATH. Como você sempre importa no espaço para nome global, lembre-se de prefixar todas as suas funções e variáveis ​​com um prefixo adequado; caso contrário, você corre o risco de conflitos de nome. Eu uso o sublinhado duplo como o ponto python.

Além disso, coloque isso como a primeira coisa no seu módulo

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

Programação orientada a objetos

No bash, você não pode fazer programação orientada a objetos, a menos que construa um sistema bastante complexo de alocação de objetos (pensei nisso. É viável, mas insano). Na prática, você pode, no entanto, fazer "programação orientada a Singleton": você tem uma instância de cada objeto e apenas uma.

O que faço é: defino um objeto em um módulo (consulte a entrada de modularização). Em seguida, defino vars vazios (análogos às variáveis ​​de membro), uma função init (construtor) e funções de membro, como neste código de exemplo

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

Capturar e manipular sinais

Achei isso útil para capturar e manipular exceções.

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

Dicas e sugestões

Se algo não funcionar por algum motivo, tente reordenar o código. A ordem é importante e nem sempre intuitiva.

nem considere trabalhar com o tcsh. não suporta funções e é horrível em geral.

Espero que ajude, embora observe. Se você precisar usar o tipo de coisa que escrevi aqui, significa que seu problema é muito complexo para ser resolvido com o shell. use outro idioma. Eu tive que usá-lo devido a fatores humanos e legado.

Stefano Borini
fonte
7
Uau, e eu pensei que estava indo para um exagero no bash ... Eu costumo usar funções isoladas e abusar de subcascas (portanto sofro quando a velocidade é de alguma forma relevante). Nunca há variáveis ​​globais, nem dentro nem fora (para preservar restos de sanidade). Tudo retorna através de saída padrão ou de arquivo. set -u / set -e (muito ruim set -e se torna inútil assim que primeiro, e a maior parte do meu código geralmente está lá). Argumentos de função obtidos com [local something = "$ 1"; shift] (permite reordenar facilmente ao refatorar). Depois de um 3000 linhas bater roteiro que tendem a escrever até mesmo menores scripts neste moda ...
Eugene
pequenas correções para modularização: 1 você precisa de um retorno depois. "$ script_absolute_dir / $ module.shinc" para evitar aviso em falta. 2, você deve definir IFS = "$ saved_IFS" antes do retorno ao encontrar o módulo em $ SHELL_LIBRARY_PATH
Duff
"fatores humanos" são os piores. Máquinas não brigam com você quando você lhes oferece algo melhor.
jeremyjjbrown
1
Por que getoptvs getopts? getoptsé mais portátil e funciona em qualquer shell POSIX. Especialmente porque a questão são práticas recomendadas de shell, em vez de práticas recomendadas especificamente para bash, eu daria suporte à conformidade com POSIX para dar suporte a vários shells quando possível.
Wimateeka
1
obrigado por oferecer todos os conselhos sobre scripts de shell, mesmo sendo sincero: "Espero que ajude, embora observe. Se você precisar usar o tipo de coisa que escrevi aqui, isso significa que seu problema é complexo demais para ser resolvido com" shell. use outro idioma. Eu tive que usá-lo devido a fatores humanos e legado ".
dieHellste
25

Dê uma olhada no Advanced Bash-Scripting Guide para obter mais informações sobre scripts de shell - e não apenas sobre o Bash.

Não ouça as pessoas dizendo para você procurar outros idiomas, sem dúvida mais complexos. Se o script de shell atender às suas necessidades, use-o. Você quer funcionalidade, não fantasia. Novos idiomas fornecem novas habilidades valiosas para o seu currículo, mas isso não ajuda se você tiver um trabalho que precisa ser feito e já conhece o shell.

Como afirmado, não existem muitas "práticas recomendadas" ou "padrões de design" para scripts de shell. Usos diferentes têm diretrizes e tendências diferentes - como qualquer outra linguagem de programação.

jtimberman
fonte
9
Observe que, para scripts de complexidade ainda menor, essa NÃO é uma prática recomendada. Codificação não é apenas conseguir que algo funcione. Trata-se de construí-lo de maneira rápida, fácil e confiável, reutilizável e fácil de ler e manter (especialmente para outros). Os scripts de shell não se adaptam bem a nenhum nível. Linguagens mais robustas são muito mais simples para projetos com qualquer lógica.
drifter
20

O shell script é uma linguagem projetada para manipular arquivos e processos. Embora seja ótimo para isso, não é uma linguagem de uso geral; portanto, tente sempre colar a lógica dos utilitários existentes, em vez de recriar a nova lógica no shell script.

Fora esse princípio geral, eu coletei alguns erros comuns de script de shell .

pixelbeat
fonte
11

Saiba quando usá-lo. Para comandos de colagem rápida e suja, tudo bem. Se você precisar tomar mais do que poucas decisões não triviais, loops, qualquer coisa, escolha Python, Perl e modularize .

O maior problema com o shell geralmente é que o resultado final se parece com uma grande bola de lama, 4000 linhas de bash e crescendo ... e você não pode se livrar dele, porque agora todo o seu projeto depende disso. Claro, começou em 40 linhas de festança bonita.

Paweł Hajdan
fonte
9

Fácil: use python em vez de scripts de shell. Você obtém um aumento de quase 100 vezes na legibilidade, sem ter que complicar tudo o que não precisa e preservando a capacidade de evoluir partes do seu script em funções, objetos, objetos persistentes (zodb), objetos distribuídos (pyro) quase sem nenhum código extra.


fonte
7
você se contradiz dizendo "sem ter que complicar" e listando as várias complexidades que você acha que agregam valor, enquanto na maioria dos casos são abusadas por monstros feios, em vez de usadas para simplificar problemas e implementação.
Evgeny
3
isso implica uma grande desvantagem, os scripts não serão portáteis em sistemas onde python não está presente
astropanic
1
Sei que isso foi respondido em 08 (agora é dois dias antes de 12); no entanto, para aqueles que olham isso mais tarde, eu recomendaria a todos que não voltassem as costas para linguagens como Python ou Ruby, pois é mais provável que ele esteja disponível e, caso contrário, é um comando (ou alguns cliques) para não ser instalado . Se você precisar de mais portabilidade, pense em escrever seu programa em Java, pois será difícil encontrar uma máquina que não tenha uma JVM disponível.
Wil Moore III
@astropanic praticamente todas as portas Linux com Python hoje em dia
Pithikos
@Pithikos, claro, e mexa com o incômodo de python2 vs python3. Atualmente, escrevo todas as minhas ferramentas e não posso estar mais feliz.
astropanic 7/03/2017
9

use set -e para não avançar após erros. Tente torná-lo compatível, sem depender do bash, se você deseja que ele seja executado no não-linux.

user10392
fonte
7

Para encontrar algumas "melhores práticas", veja como as distribuições Linux (por exemplo, Debian) escrevem seus scripts de inicialização (geralmente encontrados em /etc/init.d)

A maioria deles não possui "bash-isms" e possui uma boa separação de definições de configuração, arquivos de biblioteca e formatação de origem.

Meu estilo pessoal é escrever um shellscript mestre que defina algumas variáveis ​​padrão e tente carregar ("fonte") um arquivo de configuração que possa conter novos valores.

Eu tento evitar funções, pois elas tendem a tornar o script mais complicado. (Perl foi criado para esse fim.)

Para garantir que o script seja portátil, teste não apenas com #! / Bin / sh, mas também use #! / Bin / ash, #! / Bin / dash, etc. Você verá o código específico do Bash em breve.

Willem
fonte
-1

Ou a citação mais antiga, semelhante ao que João disse:

"Use perl. Você vai querer conhecer o bash, mas não usá-lo."

Infelizmente eu esqueci quem disse isso.

E sim, hoje em dia eu recomendaria python sobre perl.

Sarien
fonte