Padrões de projeto e boas práticas para scripts de shell [fechado]
-
09-06-2019 - |
Pergunta
Alguém sabe de todos os recursos que falar sobre as melhores práticas e padrões de projeto para scripts de shell (sh, bash, etc.)?
Solução
Eu escrevi bastante complexo, shell scripts, e a minha primeira sugestão é "não".A razão é que é bastante fácil de fazer um pequeno erro que impeça seu script, ou até mesmo tornar-se perigoso.
O que disse, eu não tem outros recursos para passar a você, mas a minha experiência pessoal.Aqui está o que eu faço normalmente, o que é um exagero, mas tende a ser sólida, embora muito detalhado.
Invocação
fazer o script aceita curtas e longas de opções.tome cuidado, pois existem dois comandos para analisar opções, getopt e getopts.Use getopt como você enfrenta 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ída com êxito, não-zero se algo saiu errado.
Chamadas de função
Você pode chamar funções em bash, apenas lembre-se de defini-los antes de a chamada.Funções são, como scripts, eles só podem retornar valores numéricos.Isso significa que você tem que inventar uma estratégia diferente para retornar valores de seqüência de caracteres.A minha estratégia é usar uma variável chamada RESULTADO para armazenar o resultado, e retornando 0 se a função foi concluída corretamente.Além disso, você pode gerar exceções, se você estiver retornando um valor diferente de zero e, em seguida, definir dois "exceção de variáveis" (o meu:EXCEÇÃO e EXCEPTION_MSG), o primeiro contendo o tipo da exceção e o segundo um formato legível por humanos mensagem.
Quando você chamar uma função, os parâmetros da função são atribuídos aos vars $0, $1, etc.Eu sugiro que você colocá-los em nomes mais significativos.declare as variáveis dentro da função como local:
function foo {
local bar="$0"
}
Propenso a erro situações
No bash, a menos que você declare o contrário, uma unset variável é usada como uma cadeia de caracteres vazia.Isso é muito perigoso em caso de erro de digitação, como o mal variável digitada não será divulgado, e será avaliada como vazio.utilização
set -o nounset
para evitar que isso aconteça.Porém, tome cuidado, porque se você fizer isso, o programa irá anular a cada vez que você avaliar uma variável indefinida.Por esta razão, 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 conseguir "python como" modularização se você usar o código a seguir:
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
}
em seguida, você pode importar arquivos com a extensão .shinc com a seguinte sintaxe
importar "AModule/ModuleFile"
O que será pesquisado em SHELL_LIBRARY_PATH.Como sempre importar o espaço de nomes global, lembre-se de prefixo de todas as suas funções e variáveis com prefixo correcto, caso contrário, você corre o risco de nome de confrontos.Eu uso duplo sublinhado como o python ponto.
Também, colocar isso como a primeira coisa na sua módulo
# avoid double inclusion
if test "${BashInclude__imported+defined}" == "defined"
then
return 0
fi
BashInclude__imported=1
Programação orientada a objeto
No bash, você não pode fazer programação orientada a objeto, a menos que você construir uma bastante complexo sistema de alocação de objetos (eu pensei sobre isso.é viável, mas insano).Na prática, no entanto, você pode fazer "Singleton programação orientada":você tem uma instância de cada objeto, e somente um.
O que eu faço é:eu defina um objeto em um módulo (consulte a modularização de entrada).Então eu definir vazio vars (análogos às variáveis de membro) uma função init (construtor) e funções de membro, como neste exemplo de código
# 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
}
Interceptação e tratamento de sinais
Eu encontrei este ú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 reorganizar o código.A ordem é importante e nem sempre é intuitivo.
não mesmo considerar a possibilidade de trabalhar com o bash.ele não oferece suporte a funções, e isso é horrível em geral.
Espero que ele ajuda, mas, por favor, note.Se você tiver que usar o tipo de coisas que eu escrevi aqui, isso significa que o problema é muito complexo para ser resolvido com a shell.usar outro idioma.Eu tive que usá-lo devido a fatores humanos e legado.
Outras dicas
Dê uma olhada no Avançado Bash-Scripting Guide para muita sabedoria em shell scripts - não apenas Bash, qualquer um.
Não ouvir as pessoas dizendo que você olhe para o outro, sem dúvida mais complexo idiomas.Se o shell script atenda às suas necessidades, use-o.Você quer funcionalidade, não fanciness.Novas linguagens e valiosa de novas habilidades para o seu currículo, mas que não ajuda se você tiver um trabalho que precisa ser feito e você já sabe o shell.
Como dito, não há um monte de "melhores práticas" ou "padrões de projeto" para a criação de shell scripts.Diferentes usos têm orientações diferentes e preconceito - como qualquer outra linguagem de programação.
shell script é uma linguagem projetada para manipular arquivos e processos.Enquanto ele é ótimo para isso, não é uma linguagem de propósito geral, então, tente sempre cola lógica de utilitários existentes, ao invés de incluir a recriação de uma nova lógica em shell script.
Que princípio geral eu coletei alguns comum de script do shell de erros.
Houve uma grande sessão na OSCON este ano (2008), apenas sobre este tema: http://assets.en.oreilly.com/1/event/12/Shell%20Scripting%20Craftsmanship%20Presentation%201.pdf
Fácil:usar python em vez de scripts shell.Você chegar perto de 100 vezes mais que em readablility, sem ter para complicar qualquer coisa que você não precisa, e a preservação da capacidade para evoluir partes do seu script em funções, objetos, objetos persistentes (zodb), objetos distribuídos (pyro) quase sem qualquer código extra.
usar definir -e para que você não lavrar em frente depois de erros.Tente torná-lo compatível com sh, sem depender de bash se você deseja executá-lo em não-linux.
Sabe quando usá-lo. Para uma rápida e suja a colagem de comandos, juntos, está tudo bem.Se você precisar fazer mais do que poucos não-trivial de decisões, loops, qualquer coisa, vá para a linguagem Python, Perl, e modularizar.
O maior problema com a shell é, muitas vezes, que o resultado final só se parece com uma grande bola de lama, 4000 linhas de bash e crescendo...e você não pode se livrar dela, porque agora todo o seu projeto depende dele.Claro, tudo começou em 40 linhas de belo bash.
Para encontrar alguns "melhores práticas", olha como distro Linux (por exemplo,Debian) escrever seus init scripts (geralmente encontrado em /etc/init.d)
A maioria deles são sem "bash-ismos" e ter uma boa separação de definições de configuração, biblioteca, arquivos e formatação de origem.
Meu estilo pessoal é escrever um mestre-de-script de shell que define algumas variáveis predefinidas e, em seguida, tenta carregar ("fonte") um arquivo de configuração, o qual pode conter novos valores.
Eu tento evitar funções, já que tendem a fazer com que o script mais complicado.(Perl foi criado para esse propósito.)
Para certificar-se de que o script é portátil, o teste não só com #!/bin/sh, mas também usar #!/bin/ash, #!/bin/dash, etc.Você avistará o Bash código específico em breve.
Ou a mais antiga citação semelhante ao que João disse:
"Utilizar a linguagem perl.Você vai querer saber o bash, mas não usá-lo."
Infelizmente eu me esqueci de quem disse o que.
E sim, esses dias eu recomendaria python sobre o perl.