Как отловить ошибку в скрипте linux bash?

13

Я сделал следующий скрипт:

# !/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"

Сценарий работает, но помимо моего эха, есть также вывод, когда

cd $1

не удается выполнить.

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

Можно ли это уловить?

Томас де Уайлд
источник
Просто к вашему сведению, вы также можете сделать это намного проще; test -d /path/to/directory(или [[ -d /path/to/directory ]]в bash) скажет вам, является ли данная цель каталогом или нет, и сделает это тихо.
Патрик
@ Патрик, он просто проверяет, является ли это каталогом, а не, если вы можете cdв него.
Стефан Шазелас
@ StephaneChazelas да. Имя функции есть directoryExists.
Патрик
Смотрите подробный ответ здесь: Возникла ошибка в скрипте Bash .
Codeforester

Ответы:

8

Ваш скрипт меняет каталоги во время работы, что означает, что он не будет работать с рядом относительных путей. Позже вы прокомментировали, что вам нужно только проверять наличие каталога, а не умение использовать cd, поэтому ответы вообще не нужно использовать cd. Перераб. Использование tput и цвета из 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
}

(Отредактировано, чтобы использовать более неуязвимое printfвместо проблемного, echoкоторое могло бы воздействовать на escape-последовательности в тексте.)

Ян Д. Аллен
источник
Это также исправляет (если не включен xpg_echo) проблемы, когда имена файлов содержат символы обратной косой черты.
Стефан Шазелас
12

Используется set -eдля установки режима выхода при ошибке: если простая команда возвращает ненулевой статус (указывает на сбой), оболочка завершается.

Остерегайтесь, set -eэто не всегда срабатывает. Команды в тестовых позициях допускаются к сбою (например if failing_command, failing_command || fallback). Команды в субоболочке только приводят к выходу подоболочки, а не родителю: set -e; (false); echo fooдисплеи foo.

В качестве альтернативы или в дополнение, в bash (и ksh и zsh, но не в простом sh), вы можете указать команду, которая выполняется в случае, если команда возвращает ненулевой статус, с ERRловушкой, например trap 'err=$?; echo >&2 "Exiting on error $err"; exit $err' ERR. Обратите внимание, что в подобных случаях (false); …прерывание ERR выполняется в подоболочке, поэтому оно не может привести к выходу родителя.

Жиль "ТАК - перестань быть злым"
источник
Недавно я немного поэкспериментировал и обнаружил удобный способ исправления ||поведения, который позволяет легко выполнять правильную обработку ошибок без использования ловушек. Смотри мой ответ . Что вы думаете об этом методе?
скозин
@ sam.kozin У меня нет времени, чтобы подробно рассмотреть ваш ответ, он выглядит хорошо в принципе. Помимо переносимости, каковы преимущества перед ERR-ловушкой ksh / bash / zsh?
Жиль "ТАК - перестань быть злым"
Вероятно, единственным преимуществом является возможность компоновки, так как вы не рискуете перезаписать другую ловушку, которая была установлена ​​до запуска функции. Это полезная функция, когда вы пишете какую-то обычную функцию, которую потом будете использовать и использовать в других скриптах. Другим преимуществом может быть полная совместимость с POSIX, хотя это не так важно, поскольку ERRпсевдосигнал поддерживается во всех основных оболочках. Спасибо за обзор! =)
скозин
@ sam.kozin Я забыл написать в своем предыдущем комментарии: вы можете добавить это в Code Review и разместить ссылку в чате .
Жиль "ТАК - перестань быть злым"
Спасибо за предложение, я постараюсь следовать ему. Не знал о проверке кода.
скозин
6

Чтобы расширить ответ @Gilles :

Действительно, set -eне работает внутри команд, если вы используете ||оператор после них, даже если вы запускаете их в подоболочке; например, это не сработает:

#!/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

Но ||оператор необходим для предотвращения возврата из внешней функции перед очисткой.

Чтобы исправить это, можно использовать небольшую хитрость: запустите внутреннюю команду в фоновом режиме, а затем сразу дождитесь ее. waitВстроенный возвращают код завершения внутренней команды, и теперь вы используете ||после того wait, а не внутренней функции, поэтому set -eработает должным образом внутри последний:

#!/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

Вот общая функция, основанная на этой идее. Он должен работать во всех POSIX-совместимых оболочках, если вы удаляете localключевые слова, т.е. заменяете все local x=yпросто 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
}

Пример использования:

#!/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 "$@"

Выполнение примера:

$ ./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

Единственное, что вам необходимо знать при использовании этого метода, это то, что все изменения переменных оболочки, сделанные из команды, которую вы передаете run, не будут распространяться на вызывающую функцию, потому что команда выполняется в подоболочке.

skozin
источник
2

Вы не говорите, что именно вы подразумеваете под catch--- сообщайте и продолжайте; прервать дальнейшую обработку?

Поскольку cdпри сбое возвращает ненулевое состояние, вы можете сделать:

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

Вы можете просто выйти при неудаче:

cd -- "$1" || exit 1

Или введите свое собственное сообщение и выйдите:

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

И / или подавить ошибку, возникшую при cdсбое:

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

По стандартам команды должны помещать сообщения об ошибках в STDERR (дескриптор файла 2). Таким образом, 2>/dev/nullговорится, перенаправить STDERR к «битовой корзине», известной /dev/null.

(не забудьте процитировать ваши переменные и отметить конец параметров для cd).

JRFerguson
источник
@ Стефан Chazelas точки цитирования и сигнализации о конце опционов хорошо приняты. Спасибо за редактирование.
JRFerguson
1

На самом деле для вашего случая я бы сказал, что логика может быть улучшена.

Вместо cd и затем проверьте, существует ли он, проверьте, существует ли он, затем перейдите в каталог.

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

Но если ваша цель состоит в том, чтобы заставить замолчать возможные ошибки, то cd -- "$1" 2>/dev/nullэто сделает вашу отладку в будущем более сложной. Вы можете проверить флаги тестирования if на: Bash, если документация :

BitsOfNix
источник
Этот ответ не может заключить в кавычки $1переменную и потерпит неудачу, если эта переменная содержит пробелы или другие метасимволы оболочки. Он также не может проверить, есть ли у пользователя разрешение cdна это.
Ян Д. Аллен
Я на самом деле пытался проверить, существует ли определенная директория, не обязательно к ней. Но так как я не знал лучше, я думал, что попытка cd к нему вызовет ошибку, если ее не существует, так почему бы не поймать это? Я не знал, что если [-d $ 1] это именно то, что мне нужно. Итак, большое спасибо! (Я привык программировать Java и проверять каталог в операторе if, который не совсем обычен в Java)
Томас Де Уайлд