Почему (выход 1) не выходит из сценария?

48

У меня есть скрипт, который не выходит, когда я этого хочу.

Пример сценария с той же ошибкой:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Я хотел бы увидеть вывод:

:~$ ./test.sh
1
:~$

Но я на самом деле вижу:

:~$ ./test.sh
1
2
:~$

Создает ли ()цепочка команд какую-либо область видимости? Из чего exitвыходит, если не сценарий?

Minix
источник
5
Для этого нужно ответить одним словом: subshell
Joshua
Связанный: unix.stackexchange.com/q/247187/135943
Wildcard

Ответы:

87

()запускает команды в подоболочке, поэтому exitвы выходите из подоболочки и возвращаетесь в родительскую оболочку. Используйте фигурные скобки, {}если вы хотите запускать команды в текущей оболочке.

Из руководства Bash:

(список) список выполняется в среде подоболочки. Переменные и встроенные команды, которые влияют на среду оболочки, не остаются в силе после ее завершения. Статус возврата - это статус выхода из списка.

{список; } список просто выполняется в текущей среде оболочки. список должен заканчиваться символом новой строки или точкой с запятой. Это называется групповой командой. Статус возврата - это статус выхода из списка. Обратите внимание, что в отличие от метасимволов (и), {и} являются зарезервированными словами и должны встречаться там, где зарезервированное слово разрешено распознавать. Поскольку они не вызывают разрыв слова, они должны быть отделены от списка пробелом или другим метасимволом оболочки.

Стоит отметить, что синтаксис оболочки является вполне согласованным, и подоболочка участвует также в других ()конструкциях, таких как подстановка команд (также с `..`синтаксисом старого стиля ) или подстановка процессов, поэтому следующее не выйдет из текущей оболочки:

echo $(exit)
cat <(exit)

Хотя может быть очевидным, что подоболочки участвуют, когда команды размещаются внутри явно (), менее заметным является тот факт, что они также создаются в этих других структурах:

  • команда началась в фоновом режиме

    exit &

    не выходит из текущей оболочки, потому что (после man bash)

    Если команда завершается оператором управления &, оболочка выполняет команду в фоновом режиме в подоболочке. Оболочка не ожидает завершения команды, и статус возврата равен 0.

  • трубопровод

    exit | echo foo

    по-прежнему выходит только из подоболочки.

    Однако разные оболочки ведут себя по-разному в этом отношении. Например, bashвсе компоненты конвейера помещаются в отдельные подоболочки (если только вы не используете lastpipeопцию в вызовах, где управление заданиями не включено), но выполняйте AT & T kshи zshзапускайте последнюю часть внутри текущей оболочки (оба поведения разрешены POSIX). таким образом

    exit | exit | exit

    в bash ничего не делает, но выходит из zsh из-за последнего exit .

  • coproc exitтакже работает exitв подоболочке.

jimmij
источник
5
Ах. Теперь нужно найти все места, где мой предшественник использовал неправильные брекеты. Спасибо за понимание.
Minix
10
Обратите особое внимание на расстояние в странице человека: {и }не синтаксис, они являются зарезервированными словами и должны быть окружены пробелами, и список должен заканчиваться командой терминатора (точка с запятой, символ новой строки, амперсанд)
Glenn Джекман
Из интереса, имеет ли это тенденцию быть другим процессом или просто отдельной средой во внутреннем стеке? Я часто использую () для выделения chdir, и, должно быть, мне повезло с использованием $$ и т. Д., Если первое.
Дэн Шеппард
5
@DanSheppard Это другой процесс, но (echo $$)печатает идентификатор родительской оболочки, потому что $$он раскрывается еще до создания подоболочки. На самом деле печать подоболочку идентификатор процесса может быть сложно, см stackoverflow.com/questions/9119885/...
jimmij
@jimmij, как это может быть $$развернуто перед созданием подоболочки и $BASHPIDпокажет правильное значение для подоболочки?
Подстановочный
13

Выполнение exitв подоболочке является одной ловушкой:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

Сценарий печатает 42, выходит из подоболочки с кодом возврата 1и продолжает выполнение сценария. Даже замена вызова на echo $(CALC) || exit 1не помогает, потому что код возврата echoравен 0 независимо от кода возврата calc. И calcвыполняется до echo.

Еще большее недоумение сводит на нет эффект exitего оборачивания во localвстроенный, как в следующем скрипте. Я наткнулся на проблему, когда написал функцию для проверки входного значения. Пример:

Я хочу создать файл с именем "year month day.log", т.е. 20141211.logна сегодня. Дата вводится пользователем, который не может предоставить разумное значение. Поэтому в моей функции fnameя проверяю возвращаемое значение, dateчтобы проверить правильность ввода пользователя:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Выглядит неплохо. Пусть сценарий будет назван s.sh. Если пользователь вызывает скрипт с помощью ./s.sh "Thu Dec 11 20:45:49 CET 2014", файл 20141211.logсоздается. Однако, если пользователь вводит ./s.sh "Thu hec 11 20:45:49 CET 2014", скрипт выводит:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

Строка fname…говорит, что в подоболочке были обнаружены неверные входные данные. Но exit 1в конце local …строки никогда не срабатывает, потому что localдиректива всегда возвращается 0. Это потому, что localвыполняется после $(fname) и, следовательно, перезаписывает свой код возврата. И из-за этого скрипт продолжается и запускается touchс пустым параметром. Этот пример прост, но поведение bash может привести к путанице в реальном приложении. Я знаю, что настоящие программисты не используют местных жителей.

Чтобы было понятно: без local, сценарий прерывается, как и ожидалось, когда вводится недопустимая дата.

Исправление состоит в том, чтобы разделить линию как

local FNAME
FNAME=$(fname "$1") || exit 1

Странное поведение соответствует документации localна странице руководства bash: «Статус возврата равен 0, если local не используется вне функции, указано неверное имя или name является переменной только для чтения».

Хотя это и не ошибка, но я чувствую, что поведение bash нелогично. Я в курсе последовательности выполнения, тем localне менее не следует маскировать нарушенное назначение.

Мой первоначальный ответ содержал некоторые неточности. После откровенного и глубокого обсуждения с mikeserv (спасибо за это) я пошел на их исправление.

hermannk
источник
@mikeserv: я добавил пример, чтобы показать актуальность.
hermannk
@mikeserv: Да, ты прав. Еще терже. Но ловушка все еще там.
hermannk
@mikeserv: Извините, мой пример был сломан. Я забыл тест в doit().
hermannk
2

Актуальное решение:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

Группировка ошибок будет выполняться только в том случае, если blaвозвращается состояние ошибки, а exitне в подоболочке, поэтому весь сценарий останавливается.

WALF
источник
1

Квадратные скобки запускают подоболочку, а выход - только из этой скорлупы.

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

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
rubo77
источник