Como capturar um erro em um script linh bash?

13

Eu fiz o seguinte script:

# !/bin/bash

# OUTPUT-COLORING
red='\e[0;31m'
green='\e[0;32m'
NC='\e[0m' # No Color

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    cd $1
    if [ $? = 0 ]
            then
                    echo -e "${green}$1${NC}"
            else
                    echo -e "${red}$1${NC}"
    fi
}

# EXE
directoryExists "~/foobar"
directoryExists "/www/html/drupal"

O script funciona, mas além dos meus ecos, há também a saída quando

cd $1

falha na execução.

testscripts//test_labo3: line 11: cd: ~/foobar: No such file or directory

É possível pegar isso?

Thomas De Wilde
fonte
Apenas um FYI, você também pode fazer isso muito mais simples; test -d /path/to/directory(ou [[ -d /path/to/directory ]]no bash) informará se um determinado destino é um diretório ou não e o fará silenciosamente.
22413 Patrick
@ Patrick, que apenas testa se é um diretório, não se você puder cd.
Stéphane Chazelas
@StephaneChazelas yes. O nome da função é directoryExists.
Patrick
Veja uma resposta detalhada aqui: Gerar erro em um script Bash .
Codeforester 26/08/18

Respostas:

8

Seu script altera os diretórios à medida que é executado, o que significa que não funcionará com uma série de nomes de caminho relativos. Você comentou mais tarde que só queria verificar a existência do diretório, não a capacidade de usar cd, para que as respostas não precisem ser usadas cd. Revisado. Usando tput e cores de man terminfo:

#!/bin/bash -u
# OUTPUT-COLORING
red=$( tput setaf 1 )
green=$( tput setaf 2 )
NC=$( tput setaf 0 )      # or perhaps: tput sgr0

# FUNCTIONS
# directoryExists - Does the directory exist?
function directoryExists {
    # was: do the cd in a sub-shell so it doesn't change our own PWD
    # was: if errmsg=$( cd -- "$1" 2>&1 ) ; then
    if [ -d "$1" ] ; then
        # was: echo "${green}$1${NC}"
        printf "%s\n" "${green}$1${NC}"
    else
        # was: echo "${red}$1${NC}"
        printf "%s\n" "${red}$1${NC}"
        # was: optional: printf "%s\n" "${red}$1 -- $errmsg${NC}"
    fi
}

(Editado para usar o mais invulnerável, em printfvez do problemático, echoque pode atuar nas seqüências de escape no texto.)

Ian D. Allen
fonte
Isso também corrige (a menos que xpg_echo esteja ativado) os problemas quando os nomes de arquivos contêm caracteres de barra invertida.
Stéphane Chazelas
12

Use set -epara definir o modo de saída com erro: se um comando simples retornar um status diferente de zero (indicando falha), o shell será encerrado.

Cuidado que set -enem sempre entra em ação. Os comandos nas posições de teste podem falhar (por exemplo,if failing_command , failing_command || fallback). Os comandos no subshell apenas levam à saída do subshell, não o pai: é set -e; (false); echo fooexibido foo.

Como alternativa, ou além disso, no bash (e ksh e zsh, mas não sh), você pode especificar um comando que será executado caso um comando retorne um status diferente de zero, com a ERRinterceptação, por exemplo trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Observe que, em casos como (false); …, a interceptação de ERR é executada no subshell, portanto, não pode causar a saída do pai.

Gilles 'SO- parar de ser mau'
fonte
Recentemente, experimentei um pouco e descobri uma maneira conveniente de corrigir o ||comportamento, que permite facilmente manipular erros sem usar armadilhas. Veja minha resposta . O que você acha desse método?
Skokin
@ sam.kozin Não tenho tempo para revisar sua resposta em detalhes, ela fica bem em princípio. Além da portabilidade, quais são os benefícios da armadilha de ERR do ksh / bash / zsh?
Gilles 'SO- stop be evil' (
Provavelmente, o único benefício é a composição, pois você não corre o risco de sobrescrever outra armadilha que foi definida antes da execução da função. O que é um recurso útil quando você está escrevendo alguma função comum que posteriormente será usada e fonte de outros scripts. Outro benefício pode ser a compatibilidade total com POSIX, embora não seja tão importante quanto o ERRpseudo-sinal seja suportado em todos os principais shells. Obrigado pela revisão! =)
skozin 11/01
@ sam.kozin Esqueci de escrever no meu comentário anterior: você pode postar isso na Revisão de Código e postar um link na sala de chat .
Gilles 'SO- stop be evil' ''
Obrigado pela sugestão, vou tentar segui-la. Não sabia sobre a Revisão de Código.
Skokin
5

Para expandir a resposta do @Gilles :

De fato, set -enão funciona dentro de comandos se você usar o ||operador depois deles, mesmo se você executá-los em um subshell; 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 impedir o retorno da função externa antes da limpeza.

Há um pequeno truque que pode ser usado para corrigir isso: execute o comando interno em segundo plano e aguarde imediatamente por ele. O waitbuiltin retornará o código de saída do comando interno e agora você está usando ||depois wait, não a função interna, portanto, set -efunciona 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 função genérica que se baseia nessa idéia. Ele deve funcionar em todos os shells compatíveis com POSIX se você remover localpalavras-chave, ou seja, substitua todos local x=ypor 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 esse método é que todas as modificações das variáveis ​​do Shell feitas a partir do comando para o qual você passa runnão serão propagadas para a função de chamada, porque o comando é executado em um subshell.

skozin
fonte
2

Você não diz exatamente o que quer dizer com catch--- denuncie e continue; abortar processamento adicional?

Como cdretorna um status diferente de zero em caso de falha, você pode:

cd -- "$1" && echo OK || echo NOT_OK

Você pode simplesmente sair com falha:

cd -- "$1" || exit 1

Ou ecoar sua própria mensagem e sair:

cd -- "$1" || { echo NOT_OK; exit 1; }

E / ou suprima o erro fornecido por cdna falha:

cd -- "$1" 2>/dev/null || exit 1

Por padrão, os comandos devem colocar mensagens de erro no STDERR (descritor de arquivo 2). Assim 2>/dev/nulldiz redirecionar STDERR para o "bit-bucket" conhecido por /dev/null.

(não esqueça de citar suas variáveis ​​e marcar o final das opções para cd ).

JRFerguson
fonte
@Stephane Chazelas ponto de citação e sinalização de fim de opções bem tomadas. Obrigado pela edição.
JRFerguson
1

Na verdade, no seu caso, eu diria que a lógica pode ser melhorada.

Em vez de cd e verifique se existe, verifique se existe e entre no diretório.

if [ -d "$1" ]
then
     printf "${green}${NC}\\n" "$1"
     cd -- "$1"
else 
     printf "${red}${NC}\\n" "$1"
fi  

Mas se seu objetivo é silenciar os possíveis erros cd -- "$1" 2>/dev/null, isso fará com que você depure mais no futuro. Você pode verificar os sinalizadores de teste se em: Bash if documentation :

BitsOfNix
fonte
Esta resposta falha ao citar a $1variável e falhará se essa variável contiver espaços em branco ou outros metacaracteres do shell. Ele também falha ao verificar se o usuário tem permissão para cdentrar nele.
Ian D. Allen
Na verdade, eu estava tentando verificar se um determinado diretório existia, não necessariamente um CD para ele. Mas como eu não conhecia melhor, pensei que tentar fazer um cd causaria um erro se não existisse, por que não capturá-lo? Eu não sabia sobre se [-d $ 1] era exatamente o que eu precisava. Então, muito obrigado! (Eu estou acostumado a proram Java, e verificação de um diretório em uma instrução if não é exatamente comum em Java)
Thomas De Wilde