Чому (вихід 1) не виходить із сценарію?


48

У мене є сценарій, який не виходить, коли я цього хочу.

Приклад сценарію з тією ж помилкою:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

Я б припустив побачити вихід:

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

Але я насправді бачу:

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

Чи ()створює команда ланцюжок якось створює область? Що exitвиходить із сценарію, якщо не сценарій?


5
Це вимагає відповіді на одне слово: subshell
Джошуа

Відповіді:


87

()запускає команди в нижній exitчастині корпусу , тому ви виходите з нижньої частини і повертаєтесь до батьківської оболонки. Використовуйте дужки, {}якщо ви хочете запускати команди в поточній оболонці.

З баш-посібника:

(list) список виконується в середовищі підзарядки. Змінні призначення та вбудовані команди, які впливають на середовище оболонки, не залишаються в силі після завершення команди. Статус повернення - це статус виходу зі списку.

{список; } список просто виконується в поточному середовищі оболонки. список повинен бути завершений новим рядком або крапкою з комою. Це відомо як команда групи. Статус повернення - це статус виходу зі списку. Зауважте, що на відміну від метахарактерів (і), {і} є зарезервованими словами і повинні виникати там, де дозволено розпізнавати зарезервоване слово. Оскільки вони не викликають розрив слова, їх потрібно відокремити від списку пробілом або іншим метахарактером оболонки.

Варто відзначити, що синтаксис оболонки є досить послідовним, і підрозділ бере участь також і в інших ()конструкціях, таких як підміна команд (також із `..`синтаксисом старого стилю ) або підміна процесу, тому наступні оболонки також не виходять з поточної оболонки:

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в нижній частині.


5
Ага. Тепер, щоб знайти всі місця, де мій попередник використовував неправильні брекети. Дякуємо за розуміння.
Мінікс

10
Уважно зверніть увагу на пробіли на сторінці man: {і }це не синтаксис, вони є зарезервованими словами і повинні бути оточені пробілами, а список повинен закінчуватися командним термінатором (крапка з комою, новий рядок, амперсанд)
glenn jackman

Чи не представляє інтерес це, власне, інший процес чи просто окреме середовище у внутрішній стеці? Я використовую () багато для ізоляції акордів, і, мабуть, пощастило з моїм використанням $$ тощо, якщо перший.
Ден Шеппард

5
@DanSheppard Це ще один процес, але (echo $$)друкує ідентифікатор батьківської оболонки, оскільки $$він розгортається ще до того, як буде створена нижня оболонка . Насправді друк подоболочкі ідентифікатор процесу може бути складно, см stackoverflow.com/questions/9119885 / ...
jimmij

@jimmij, Як це можна $$розширити до того, як буде створена нижня оболонка, і все-таки $BASHPIDпоказано правильне значення для нижньої оболонки ?
Wildcard

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в розділі manh на сторінці bash: "Стан повернення 0, якщо не використовується локальна поза функцією, недійсне ім'я або ім'я - змінна, що читається".

Хоча я не є клопотом, але я вважаю, що поведінка баш протикоректно. Я знаю послідовність виконання, все localж не слід маскувати порушене завдання.

Моя початкова відповідь містила деякі неточності. Після виявлення та поглибленої дискусії з mikeserv (дякую за це) я пішов їх виправити.


@mikeserv: Я додав приклад, щоб показати актуальність.
германнк

@mikeserv: Так, ти маєш рацію. Ще коротше. Але підводний камінь все-таки є.
германнк

@mikeserv: Вибачте, мій приклад був порушений. Я забув тест в doit().
германнк


2

Фактичне рішення:

#!/bin/bash

function bla() {
    return 1
}

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

echo '2'

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


1

В дужках починається нижня оболонка, і вихід виходить лише з цієї підклітини.

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

#!/bin/bash

function bla() {
    return 1
}

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

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

echo '2'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.