Erro ao manipular no Bash

Qual é o seu método favorito para lidar com erros no Bash? O melhor exemplo de erros de manuseio que encontrei na web foi escrito por William Shotts, Jr em http://www.linuxcommand.org .

Ele sugere o uso da seguinte function para manipulação de erros no Bash:

#!/bin/bash # A slicker error handling routine # I put a variable in my scripts named PROGNAME which # holds the name of the program being run. You can get this # value from the first item on the command line ($0). # Reference: This was copied from  PROGNAME=$(basename $0) function error_exit { # ---------------------------------------------------------------- # Function for exit due to fatal program error # Accepts 1 argument: # string containing descriptive error message # ---------------------------------------------------------------- echo "${PROGNAME}: ${1:-"Unknown Error"}" 1>&2 exit 1 } # Example call of the error_exit function. Note the inclusion # of the LINENO environment variable. It contains the current # line number. echo "Example of error with line number and message" error_exit "$LINENO: An error has occurred." 

Você tem uma rotina de tratamento de erros melhor que você usa em scripts Bash?

   

Use uma armadilha!

 tempfiles=( ) cleanup() { rm -f "${tempfiles[@]}" } trap cleanup 0 error() { local parent_lineno="$1" local message="$2" local code="${3:-1}" if [[ -n "$message" ]] ; then echo "Error on or near line ${parent_lineno}: ${message}; exiting with status ${code}" else echo "Error on or near line ${parent_lineno}; exiting with status ${code}" fi exit "${code}" } trap 'error ${LINENO}' ERR 

… então, sempre que você criar um arquivo temporário:

 temp_foo="$(mktemp -t foobar.XXXXXX)" tempfiles+=( "$temp_foo" ) 

e $temp_foo será excluído na saída e o número da linha atual será impresso. (O set -e também fornecerá um comportamento de saída por erro, embora ele possua graves limitações e enfraqueça a previsibilidade e a portabilidade do código).

Você pode deixar o error chamada de interceptação para você (nesse caso, ele usa o código de saída padrão de 1 e nenhuma mensagem) ou chamá-lo por conta própria e fornecer valores explícitos; por exemplo:

 error ${LINENO} "the foobar failed" 2 

sairá com o status 2 e fornecerá uma mensagem explícita.

Essa é uma ótima solução. Eu só queria adicionar

 set -e 

como um mecanismo de erro rudimentar. Parará imediatamente seu script se um comando simples falhar. Eu acho que este deveria ter sido o comportamento padrão: como esses erros quase sempre significam algo inesperado, não é realmente ‘sadio’ continuar executando os seguintes comandos.

Ler todas as respostas nesta página me inspirou muito.

Então, aqui está minha dica:

conteúdo do arquivo: lib.trap.sh

 lib_name='trap' lib_version=20121026 stderr_log="/dev/shm/stderr.log" # # TO BE SOURCED ONLY ONCE: # ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## if test "${g_libs[$lib_name]+_}"; then return 0 else if test ${#g_libs[@]} == 0; then declare -A g_libs fi g_libs[$lib_name]=$lib_version fi # # MAIN CODE: # ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## set -o pipefail # trace ERR through pipes set -o errtrace # trace ERR through 'time command' and other functions set -o nounset ## set -u : exit the script if you try to use an uninitialised variable set -o errexit ## set -e : exit the script if any statement returns a non-true return value exec 2>"$stderr_log" ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## # # FUNCTION: EXIT_HANDLER # ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## function exit_handler () { local error_code="$?" test $error_code == 0 && return; # # LOCAL VARIABLES: # ------------------------------------------------------------------ # local i=0 local regex='' local mem='' local error_file='' local error_lineno='' local error_message='unknown' local lineno='' # # PRINT THE HEADER: # ------------------------------------------------------------------ # # Color the output if it's an interactive terminal test -t 1 && tput bold; tput setf 4 ## red bold echo -e "\n(!) EXIT HANDLER:\n" # # GETTING LAST ERROR OCCURRED: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # # # Read last file from the error log # ------------------------------------------------------------------ # if test -f "$stderr_log" then stderr=$( tail -n 1 "$stderr_log" ) rm "$stderr_log" fi # # Managing the line to extract information: # ------------------------------------------------------------------ # if test -n "$stderr" then # Exploding stderr on : mem="$IFS" local shrunk_stderr=$( echo "$stderr" | sed 's/\: /\:/g' ) IFS=':' local stderr_parts=( $shrunk_stderr ) IFS="$mem" # Storing information on the error error_file="${stderr_parts[0]}" error_lineno="${stderr_parts[1]}" error_message="" for (( i = 3; i < = ${#stderr_parts[@]}; i++ )) do error_message="$error_message "${stderr_parts[$i-1]}": " done # Removing last ':' (colon character) error_message="${error_message%:*}" # Trim error_message="$( echo "$error_message" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )" fi # # GETTING BACKTRACE: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # _backtrace=$( backtrace 2 ) # # MANAGING THE OUTPUT: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # local lineno="" regex='^([az]{1,}) ([0-9]{1,})$' if [[ $error_lineno =~ $regex ]] # The error line was found on the log # (eg type 'ff' without quotes wherever) # -------------------------------------------------------------- then local row="${BASH_REMATCH[1]}" lineno="${BASH_REMATCH[2]}" echo -e "FILE:\t\t${error_file}" echo -e "${row^^}:\t\t${lineno}\n" echo -e "ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow echo -e "ERROR MESSAGE:\n$error_message" else regex="^${error_file}\$|^${error_file}\s+|\s+${error_file}\s+|\s+${error_file}\$" if [[ "$_backtrace" =~ $regex ]] # The file was found on the log but not the error line # (could not reproduce this case so far) # ------------------------------------------------------ then echo -e "FILE:\t\t$error_file" echo -e "ROW:\t\tunknown\n" echo -e "ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow echo -e "ERROR MESSAGE:\n${stderr}" # Neither the error line nor the error file was found on the log # (eg type 'cp ffd fdf' without quotes wherever) # ------------------------------------------------------ else # # The error file is the first on backtrace list: # Exploding backtrace on newlines mem=$IFS IFS=' ' # # Substring: I keep only the carriage return # (others needed only for tabbing purpose) IFS=${IFS:0:1} local lines=( $_backtrace ) IFS=$mem error_file="" if test -n "${lines[1]}" then array=( ${lines[1]} ) for (( i=2; i<${#array[@]}; i++ )) do error_file="$error_file ${array[$i]}" done # Trim error_file="$( echo "$error_file" | sed -e 's/^[ \t]*//' | sed -e 's/[ \t]*$//' )" fi echo -e "FILE:\t\t$error_file" echo -e "ROW:\t\tunknown\n" echo -e "ERROR CODE:\t${error_code}" test -t 1 && tput setf 6 ## white yellow if test -n "${stderr}" then echo -e "ERROR MESSAGE:\n${stderr}" else echo -e "ERROR MESSAGE:\n${error_message}" fi fi fi # # PRINTING THE BACKTRACE: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # test -t 1 && tput setf 7 ## white bold echo -e "\n$_backtrace\n" # # EXITING: # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # test -t 1 && tput setf 4 ## red bold echo "Exiting!" test -t 1 && tput sgr0 # Reset terminal exit "$error_code" } trap exit_handler EXIT # ! ! ! TRAP EXIT ! ! ! trap exit ERR # ! ! ! TRAP ERR ! ! ! ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## # # FUNCTION: BACKTRACE # ###~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~## function backtrace { local _start_from_=0 local params=( "$@" ) if (( "${#params[@]}" >= "1" )) then _start_from_="$1" fi local i=0 local first=false while caller $i > /dev/null do if test -n "$_start_from_" && (( "$i" + 1 >= "$_start_from_" )) then if test "$first" == false then echo "BACKTRACE IS:" first=true fi caller $i fi let "i=i+1" done } return 0 

Exemplo de uso:
conteúdo do arquivo: trap-test.sh

 #!/bin/bash source 'lib.trap.sh' echo "doing something wrong now .." echo "$foo" exit 0 

Corrida:

 bash trap-test.sh 

Saída:

 doing something wrong now .. (!) EXIT HANDLER: FILE: trap-test.sh LINE: 6 ERROR CODE: 1 ERROR MESSAGE: foo: unassigned variable BACKTRACE IS: 1 main trap-test.sh Exiting! 

Como você pode ver na captura de canvas abaixo, a saída é colorida e a mensagem de erro vem no idioma usado.

insira a descrição da imagem aqui

Uma alternativa equivalente a “set -e” é

 set -o errexit 

Isso torna o significado da bandeira um pouco mais claro do que apenas “-e”.

Adição aleatória: para desativar temporariamente o sinalizador e retornar ao padrão (de execução contínua, independentemente dos códigos de saída), use apenas

 set +e echo "commands run here returning non-zero exit codes will not cause the entire script to fail" echo "false returns 1 as an exit code" false set -e 

Isso impede o tratamento adequado de erros mencionado em outras respostas, mas é rápido e eficaz (assim como o bash).

Inspirado pelas idéias apresentadas aqui, desenvolvi uma maneira legível e conveniente de lidar com erros em scripts bash no meu projeto bash boilerplate .

Ao simplesmente acessar a biblioteca, você obtém o seguinte fora da checkbox (isto é, interrompe a execução de qualquer erro, como se estivesse usando set -e graças a um trap em ERR e algum bash-fu ):

manipulação de erros bash-oo-framework

Existem alguns resources extras que ajudam a lidar com erros, como try and catch , ou a palavra-chave throw , que permite interromper a execução em um ponto para ver o backtrace. Além disso, se o terminal o suportar, ele emite powerlines, partes de colors da saída para uma ótima legibilidade e sublinha o método que causou a exceção no contexto da linha de código.

A desvantagem é – não é portátil – o código funciona no bash, provavelmente> = 4 apenas (mas eu imagino que poderia ser portado com algum esforço para bater 3).

O código é separado em vários arquivos para melhor manuseio, mas fui inspirado pela ideia de backtrace da resposta acima por Luca Borrione .

Para ler mais ou dar uma olhada na fonte, veja o GitHub:

https://github.com/niieani/bash-oo-framework#error-handling-with-exceptions-and-throw

Eu prefiro algo realmente fácil de ligar. Então eu uso algo que parece um pouco complicado, mas é fácil de usar. Eu costumo apenas copiar e colar o código abaixo em meus scripts. Uma explicação segue o código.

 #This function is used to cleanly exit any script. It does this displaying a # given error message, and exiting with an error code. function error_exit { echo echo "$@" exit 1 } #Trap the killer signals so that we can exit with a good message. trap "error_exit 'Received signal SIGHUP'" SIGHUP trap "error_exit 'Received signal SIGINT'" SIGINT trap "error_exit 'Received signal SIGTERM'" SIGTERM #Alias the function so that it will print a message with the following format: #prog-name(@line#): message #We have to explicitly allow aliases, we do this because they make calling the #function much easier (see example). shopt -s expand_aliases alias die='error_exit "Error ${0}(@`echo $(( $LINENO - 1 ))`):"' 

Eu costumo colocar uma chamada para a function de limpeza no lado da function error_exit, mas isso varia de script para script, então eu deixei de fora. As armadilhas captam os sinais terminais comuns e garantem que tudo seja limpo. O alias é o que faz a mágica real. Eu gosto de verificar tudo por falha. Então, em geral, eu chamo programas em um “se!” declaração de tipo. Ao subtrair 1 do número da linha, o alias me dirá onde ocorreu a falha. Também é muito simples ligar, e praticamente à prova de idiotas. Abaixo está um exemplo (apenas substitua / bin / false com o que você vai chamar).

 #This is an example useage, it will print out #Error prog-name (@1): Who knew false is false. if ! /bin/false ; then die "Who knew false is false." fi 

Outra consideração é o código de saída para retornar. Apenas ” 1 ” é bastante padrão, embora haja um punhado de códigos de saída reservados que o bash usa , e essa mesma página argumenta que os códigos definidos pelo usuário devem estar no intervalo 64-113 para estar em conformidade com os padrões C / C ++.

Você também pode considerar a abordagem de vetor de bits que a mount usa para seus códigos de saída:

  0 success 1 incorrect invocation or permissions 2 system error (out of memory, cannot fork, no more loop devices) 4 internal mount bug or missing nfs support in mount 8 user interrupt 16 problems writing or locking /etc/mtab 32 mount failure 64 some mount succeeded 

OR os códigos juntos permitem que seu script sinalize vários erros simultâneos.

Eu usei

 die() { echo $1 kill $$ } 

antes; Eu acho que porque “sair” estava falhando por mim por algum motivo. Os padrões acima parecem ser uma boa ideia, no entanto.

Isso me serviu bem por um tempo agora. Imprime mensagens de erro ou aviso em vermelho, uma linha por parâmetro e permite um código de saída opcional.

 # Custom errors EX_UNKNOWN=1 warning() { # Output warning messages # Color the output red if it's an interactive terminal # @param $1...: Messages test -t 1 && tput setf 4 printf '%s\n' "$@" >&2 test -t 1 && tput sgr0 # Reset terminal true } error() { # Output error messages with optional exit code # @param $1...: Messages # @param $N: Exit code (optional) messages=( "$@" ) # If the last parameter is a number, it's not part of the messages last_parameter="${messages[@]: -1}" if [[ "$last_parameter" =~ ^[0-9]*$ ]] then exit_code=$last_parameter unset messages[$((${#messages[@]} - 1))] fi warning "${messages[@]}" exit ${exit_code:-$EX_UNKNOWN} } 

Eu uso o seguinte código de armadilha, ele também permite que os erros sejam rastreados através de pipes e comandos ‘time’

 #!/bin/bash set -o pipefail # trace ERR through pipes set -o errtrace # trace ERR through 'time command' and other functions function error() { JOB="$0" # job name LASTLINE="$1" # line of error occurrence LASTERR="$2" # error code echo "ERROR in ${JOB} : line ${LASTLINE} with exit code ${LASTERR}" exit 1 } trap 'error ${LINENO} ${?}' ERR 

Não tenho certeza se isso será útil para você, mas eu modifiquei algumas das funções sugeridas aqui para include a verificação do erro (código de saída do comando anterior) dentro dele. Em cada “cheque” eu também passo como parâmetro a “mensagem” do que é o erro para fins de registro.

 #!/bin/bash error_exit() { if [ "$?" != "0" ]; then log.sh "$1" exit 1 fi } 

Agora, para chamá-lo dentro do mesmo script (ou em outro caso eu use export -f error_exit ) eu simplesmente escrevo o nome da function e passo uma mensagem como parâmetro, assim:

 #!/bin/bash cd /home/myuser/afolder error_exit "Unable to switch to folder" rm * error_exit "Unable to delete all files" 

Usando isso eu consegui criar um arquivo bash realmente robusto para algum processo automatizado e ele irá parar em caso de erros e notificar-me ( log.sh fará isso)

Esta function tem me servido muito bem recentemente:

 action () { # Test if the first parameter is non-zero # and return straight away if so if test $1 -ne 0 then return $1 fi # Discard the control parameter # and execute the rest shift 1 "$@" local status=$? # Test the exit status of the command run # and display an error message on failure if test ${status} -ne 0 then echo Command \""$@"\" failed >&2 fi return ${status} } 

Você o chama adicionando 0 ou o último valor de retorno ao nome do comando a ser executado, para poder encadear comandos sem precisar verificar os valores de erro. Com isto, este bloco de declaração:

 command1 param1 param2 param3... command2 param1 param2 param3... command3 param1 param2 param3... command4 param1 param2 param3... command5 param1 param2 param3... command6 param1 param2 param3... 

Torna-se isso:

 action 0 command1 param1 param2 param3... action $? command2 param1 param2 param3... action $? command3 param1 param2 param3... action $? command4 param1 param2 param3... action $? command5 param1 param2 param3... action $? command6 param1 param2 param3... < <>> 

Se algum dos comandos falhar, o código de erro é simplesmente passado para o final do bloco. Eu acho que é útil quando você não quer executar comandos subseqüentes se um anterior falhou, mas você também não quer que o script saia imediatamente (por exemplo, dentro de um loop).

Esse truque é útil para perder comandos ou funções. O nome da function ausente (ou executável) será passado em $ _

 function handle_error { status=$? last_call=$1 # 127 is 'command not found' (( status != 127 )) && return echo "you tried to call $last_call" return } # Trap errors. trap 'handle_error "$_"' ERR 

Usar armadilha nem sempre é uma opção. Por exemplo, se você está escrevendo algum tipo de function reutilizável que precisa de tratamento de erros e que pode ser chamada de qualquer script (depois de obter o arquivo com funções auxiliares), essa function não pode assumir nada sobre o tempo de saída do script externo. o que torna o uso de armadilhas muito difícil. Outra desvantagem do uso de armadilhas é a má composibilidade, já que você corre o risco de sobrescrever a armadilha anterior que pode ser configurada anteriormente na cadeia de chamadas.

Há um pequeno truque que pode ser usado para fazer o tratamento adequado de erros sem armadilhas. Como você já deve saber de outras respostas, set -e não funciona dentro de comandos se você usar || operador depois deles, mesmo que você os execute em uma subcamada; por exemplo, isso não funcionaria:

 #!/bin/sh # prints: # # --> outer # --> inner # ./so_1.sh: line 16: some_failed_command: command not found # < -- inner # <-- outer set -e outer() { echo '--> outer' (inner) || { exit_code=$? echo '--> cleanup' return $exit_code } echo '< -- outer' } inner() { set -e echo '--> inner' some_failed_command echo '< -- inner' } outer 

Mas || operador é necessário para evitar o retorno da function externa antes da limpeza. O truque é executar o comando interno em segundo plano e esperar imediatamente por ele. A wait embutida retornará o código de saída do comando interno, e agora você está usando || depois de wait , não a function interna, então set -e funciona corretamente dentro do último:

 #!/bin/sh # prints: # # --> outer # --> inner # ./so_2.sh: line 27: some_failed_command: command not found # --> cleanup set -e outer() { echo '--> outer' inner & wait $! || { exit_code=$? echo '--> cleanup' return $exit_code } echo '< -- outer' } inner() { set -e echo '--> inner' some_failed_command echo '< -- inner' } outer 

Aqui está a function genérica que se baseia nessa ideia. Ele deve funcionar em todos os shells compatíveis com POSIX se você remover local palavras-chave local , ou seja, replace todos local x=y por apenas x=y :

 # [CLEANUP=cleanup_cmd] run cmd [args...] # # `cmd` and `args...` A command to run and its arguments. # # `cleanup_cmd` A command that is called after cmd has exited, # and gets passed the same arguments as cmd. Additionally, the # following environment variables are available to that command: # # - `RUN_CMD` contains the `cmd` that was passed to `run`; # - `RUN_EXIT_CODE` contains the exit code of the command. # # If `cleanup_cmd` is set, `run` will return the exit code of that # command. Otherwise, it will return the exit code of `cmd`. # run() { local cmd="$1"; shift local exit_code=0 local e_was_set=1; if ! is_shell_attribute_set e; then set -e e_was_set=0 fi "$cmd" "$@" & wait $! || { exit_code=$? } if [ "$e_was_set" = 0 ] && is_shell_attribute_set e; then set +e fi if [ -n "$CLEANUP" ]; then RUN_CMD="$cmd" RUN_EXIT_CODE="$exit_code" "$CLEANUP" "$@" return $? fi return $exit_code } is_shell_attribute_set() { # attribute, like "x" case "$-" in *"$1"*) return 0 ;; *) return 1 ;; esac } 

Exemplo de uso:

 #!/bin/sh set -e # Source the file with the definition of `run` (previous code snippet). # Alternatively, you may paste that code directly here and comment the next line. . ./utils.sh main() { echo "--> main: $@" CLEANUP=cleanup run inner "$@" echo "< -- main" } inner() { echo "--> inner: $@" sleep 0.5; if [ "$1" = 'fail' ]; then oh_my_god_look_at_this fi echo "< -- inner" } cleanup() { echo "--> cleanup: $@" echo " RUN_CMD = '$RUN_CMD'" echo " RUN_EXIT_CODE = $RUN_EXIT_CODE" sleep 0.3 echo '< -- cleanup' return $RUN_EXIT_CODE } main "$@" 

Executando o exemplo:

 $ ./so_3 fail; echo "exit code: $?" --> main: fail --> inner: fail ./so_3: line 15: oh_my_god_look_at_this: command not found --> cleanup: fail RUN_CMD = 'inner' RUN_EXIT_CODE = 127 < -- cleanup exit code: 127 $ ./so_3 pass; echo "exit code: $?" --> main: pass --> inner: pass < -- inner --> cleanup: pass RUN_CMD = 'inner' RUN_EXIT_CODE = 0 < -- cleanup <-- main exit code: 0 

A única coisa que você precisa estar ciente ao usar este método é que todas as modificações de variables ​​do shell feitas do comando que você passa para run não se propagarão à function de chamada, porque o comando é executado em um subshell.