Для цього потрібно bash 4.1, якщо ви використовуєте {fd}
або local -n
.
Решта повинні працювати в баш 3.x, сподіваюся. Я не зовсім впевнений через те, що printf %q
це може бути функцією 4.
Підсумок
Ваш приклад можна змінити так, щоб архівувати потрібний ефект:
# Add following 4 lines:
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
e=2
# Add following line, called "Annotation"
function test1_() { passback e; }
function test1() {
e=4
echo "hello"
}
# Change following line to:
capture ret test1
echo "$ret"
echo "$e"
друкується за бажанням:
hello
4
Зауважте, що це рішення:
- Працює і для
e=1000
.
- Консервує,
$?
якщо вам потрібно$?
Єдиними поганими побічними ефектами є:
- Це потрібно сучасне
bash
.
- Він роздрібнюється досить часто.
- Потрібна примітка (названа по вашій функції, із додаванням
_
)
- Він жертвує дескриптором файлів 3.
- Ви можете змінити його на інший FD, якщо вам це потрібно.
- У
_capture
просто замінити всі місця де 3
з іншого (вище) числом.
Наступне (що досить довго, вибачте за це), сподіваємось, пояснює, як віднести цей рецепт до інших сценаріїв.
Проблема
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
d1=$(d)
d2=$(d)
d3=$(d)
d4=$(d)
echo $x $d1 $d2 $d3 $d4
виходи
0 20171129-123521 20171129-123521 20171129-123521 20171129-123521
поки потрібний вихід
4 20171129-123521 20171129-123521 20171129-123521 20171129-123521
Причина проблеми
Змінні оболонки (або взагалі кажучи, оточення) передаються від батьківських процесів до дочірніх процесів, але не навпаки.
Якщо ви займаєтеся захопленням виводу, зазвичай це запускається в підпакеті, тому передавати змінні назад складно.
Деякі навіть вам кажуть, що це неможливо виправити. Це неправильно, але давно відому важко вирішити проблему.
Існує кілька способів, як це найкраще вирішити, це залежить від ваших потреб.
Ось покрокове керівництво про те, як це зробити.
Передача змінних в батьківську оболонку
Існує спосіб повернення змінних до батьківської оболонки. Однак це небезпечний шлях, оскільки він використовує eval
. Якщо зробити це неправильно, ви ризикуєте багатьма злими справами. Але якщо зробити все правильно, це абсолютно безпечно за умови, що немає помилок bash
.
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; d=$(date +%Y%m%d-%H%M%S); _passback x d; }
x=0
eval `d`
d1=$d
eval `d`
d2=$d
eval `d`
d3=$d
eval `d`
d4=$d
echo $x $d1 $d2 $d3 $d4
відбитки
4 20171129-124945 20171129-124945 20171129-124945 20171129-124945
Зауважте, що це працює і для небезпечних речей:
danger() { danger="$*"; passback danger; }
eval `danger '; /bin/echo *'`
echo "$danger"
відбитки
; /bin/echo *
Це пов'язано з тим printf '%q'
, що цитує все таке, що ви можете повторно використовувати його в контексті оболонки.
Але це біль у ..
Це не тільки виглядає некрасиво, воно також багато набирає, тому воно схильне до помилок. Лише одна єдина помилка, і ви приречені, правда?
Ну, ми на рівні оболонки, тож ви можете вдосконалити її. Подумайте лише про інтерфейс, який ви хочете бачити, і тоді ви зможете його реалізувати.
Додайте, як оболонка обробляє речі
Повернемось крок назад і подумаємо про якийсь API, який дозволяє нам легко висловити, що ми хочемо зробити.
Ну, що ми хочемо робити з d()
функцією?
Ми хочемо захопити результат у змінну. Гаразд, тоді давайте реалізуємо API саме для цього:
# This needs a modern bash 4.3 (see "help declare" if "-n" is present,
# we get rid of it below anyway).
: capture VARIABLE command args..
capture()
{
local -n output="$1"
shift
output="$("$@")"
}
Тепер замість того, щоб писати
d1=$(d)
ми можемо писати
capture d1 d
Ну, це виглядає так, що ми не сильно змінилися, оскільки, знову ж таки, змінні не передаються назад d
у батьківську оболонку, і нам потрібно набрати ще трохи.
Однак тепер ми можемо кинути на нього всю потужність оболонки, оскільки вона гарно укутана у функцію.
Подумайте про легкий для повторного використання інтерфейс
Друга річ - це те, що ми хочемо бути СУХОМИ (не повторюйте себе). Тож ми остаточно не хочемо набирати щось подібне
x=0
capture1 x d1 d
capture1 x d2 d
capture1 x d3 d
capture1 x d4 d
echo $x $d1 $d2 $d3 $d4
x
Тут не тільки зайвим, це значною кількістю помилок завжди repeate в правильному контексті. Що робити, якщо ви використовуєте його 1000 разів у сценарії, а потім додаєте змінну? Ви, безумовно, не хочете змінювати всі 1000 місць, в d
яких бере участь виклик .
Тож залишайте x
подалі, щоб ми могли написати:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
d() { let x++; output=$(date +%Y%m%d-%H%M%S); _passback output x; }
xcapture() { local -n output="$1"; eval "$("${@:2}")"; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
виходи
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Це вже виглядає дуже добре. (Але все ж є те, local -n
що не працює в звичайному bash
3.x)
Уникайте змін d()
Останнє рішення має деякі великі вади:
d()
потребує змін
xcapture
Для передачі результату потрібно використовувати деякі внутрішні деталі .
- Зауважте, що ця тінь (спалює) одну змінну з назвою
output
, тому ми її ніколи не можемо повернути назад.
- З цим потрібно співпрацювати
_passback
Чи можемо ми також позбутися цього?
Звичайно, ми можемо! Ми перебуваємо в оболонці, тому є все необхідне для цього.
Якщо ви придивитесь трохи ближче до дзвінка, eval
то можете побачити, що ми маємо 100% контроль у цьому місці. "Всередині" eval
ми перебуваємо в нижній частині, тому ми можемо робити все, що хочемо, не побоюючись зробити щось погане батьківській оболонці.
Так, добре, так що давайте додамо ще одну обгортку, зараз прямо всередині eval
:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
# !DO NOT USE!
_xcapture() { "${@:2}" > >(printf "%q=%q;" "$1" "$(cat)"); _passback x; } # !DO NOT USE!
# !DO NOT USE!
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
відбитки
4 20171129-132414 20171129-132414 20171129-132414 20171129-132414
Однак це, знову ж таки, має певний вагомий недолік:
- У
!DO NOT USE!
фломастери є, тому що є дуже поганий стан гонки в цьому, що ви не можете легко бачити:
- Це
>(printf ..)
фонове завдання. Тож воно все ще може виконуватися під час _passback x
запуску.
- Ви можете переконатись у цьому самі, якщо додати
sleep 1;
до printf
або _passback
.
_xcapture a d; echo
потім виходи x
або a
спочатку відповідно.
- Це
_passback x
не повинно бути частиною _xcapture
, оскільки це ускладнює повторне використання цього рецепту.
- Також у нас є декілька безпілотних вилок (але
$(cat)
), але, як це рішення, !DO NOT USE!
я взяв найкоротший маршрут.
Однак це показує, що ми можемо це зробити, не змінюючи d()
(і без local -n
)!
Зверніть увагу, що нам це необов'язково потрібно _xcapture
, як це можна було б написати в кожному правильному eval
.
Однак зазвичай це не дуже читабельно. Якщо ви повернетесь до свого сценарію через кілька років, ви, мабуть, захочете знову прочитати його без особливих проблем.
Зафіксуйте гонку
Тепер давайте виправимо умову гонки.
Хитрість може полягати в тому, щоб зачекати, поки він printf
не закриється, і потім вивести його x
.
Існує багато способів архівувати це:
- Не можна використовувати оболонки, оскільки труби працюють в різних процесах.
- Можна використовувати тимчасові файли,
- або щось на кшталт блокувального файлу або фіфо. Це дозволяє чекати на замок або фіфо,
- або різних каналів, щоб вивести інформацію, а потім зібрати вихід у певній правильній послідовності.
Наступний шлях може виглядати таким чином (зауважте, що він працює printf
останнім, оскільки це працює тут краще):
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_xcapture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; _passback x >&3)"; } 3>&1; }
xcapture() { eval "$(_xcapture "$@")"; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
xcapture d1 d
xcapture d2 d
xcapture d3 d
xcapture d4 d
echo $x $d1 $d2 $d3 $d4
виходи
4 20171129-144845 20171129-144845 20171129-144845 20171129-144845
Чому це правильно?
_passback x
безпосередньо спілкується з STDOUT.
- Однак, оскільки STDOUT потрібно зафіксувати у внутрішній команді, ми спочатку "збережемо" його в FD3 (можна використовувати, звичайно, інші) за допомогою "3> & 1", а потім повторно використовуємо
>&3
.
- В
$("${@:2}" 3<&-; _passback x >&3)
обробки після того , як _passback
, коли подоболочка закриває STDOUT.
- Тож
printf
не може статися раніше _passback
, незалежно від того, скільки часу _passback
займає
- Зауважте, що
printf
команда не виконується до монтажу повного командного рядка, тому ми не можемо побачити артефакти з printf
, незалежно від того , як printf
реалізовано.
Отже спочатку _passback
виконується, а потім printf
.
Це вирішує гонку, жертвуючи одним дескриптором фіксованого файлу 3. Ви, звичайно, можете вибрати інший дескриптор файлу у випадку, якщо FD3 не є вільним у вашому оболонковому сценарії.
Будь ласка, зверніть увагу, 3<&-
що захищає FD3 для передачі функції.
Зробіть це більш загальним
_capture
містить частини, які належать d()
, що погано, з точки зору повторного використання. Як це вирішити?
Ну, зробіть це відчайдушним шляхом, ввівши ще одну річ, додаткову функцію, яка повинна повертати правильні речі, яка названа на честь оригінальної функції із _
доданою.
Ця функція викликається після реальної функції і може доповнювати речі. Таким чином, це можна прочитати як деяку анотацію, тож вона легко читається:
_passback() { while [ 0 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; }
_capture() { { printf "%q=%q;" "$1" "$("${@:2}" 3<&-; "$2_" >&3)"; } 3>&1; }
capture() { eval "$(_capture "$@")"; }
d_() { _passback x; }
d() { let x++; date +%Y%m%d-%H%M%S; }
x=0
capture d1 d
capture d2 d
capture d3 d
capture d4 d
echo $x $d1 $d2 $d3 $d4
досі друкує
4 20171129-151954 20171129-151954 20171129-151954 20171129-151954
Дозволити доступ до коду повернення
Є лише дефіс:
v=$(fn)
встановлює $?
те, що fn
повернулося. Тож ви, мабуть, і цього хочете. Однак це потребує більшого налаштування:
# This is all the interface you need.
# Remember, that this burns FD=3!
_passback() { while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { _passback "$@" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "$@")"; }
# Here is your function, annotated with which sideffects it has.
fails_() { passback x y; }
fails() { x=$1; y=69; echo FAIL; return 23; }
# And now the code which uses it all
x=0
y=0
capture wtf fails 42
echo $? $x $y $wtf
відбитки
23 42 69 FAIL
Є ще багато можливостей для вдосконалення
_passback()
можна елімінувати с passback() { set -- "$@" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
_capture()
можна усунути за допомогою capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
Розчин забруднює дескриптор файлу (тут 3), використовуючи його внутрішньо. Вам потрібно пам’ятати про це, якщо вам трапляється передавати FD.
Зауважте, що bash
4.1 і вище {fd}
повинні використовувати деякі невикористані FD.
(Можливо, я додам рішення тут, коли я обійдуся.)
Зауважте, що саме тому я використовую для того, щоб розмістити його в окремих функціях, як-от _capture
, оскільки введення цього все в один рядок можливо, але це робить все важче для читання та розуміння
Можливо, ви також хочете захопити STDERR викликаної функції. Або ви хочете навіть передавати та передавати більше ніж один fileescriptor із змінних та до них.
У мене поки що немає рішення, однак тут є спосіб залучити більше ніж один FD , тому ми, ймовірно, можемо також повернути змінні таким чином.
Також не забувайте:
Це повинно викликати функцію оболонки, а не зовнішню команду.
Немає простого способу передачі змінних середовища із зовнішніх команд. ( LD_PRELOAD=
Хоча з цим має бути можливо!) Але це тоді щось зовсім інше.
Останні слова
Це не єдине можливе рішення. Це один із прикладів рішення.
Як завжди, у вас є багато способів виразити речі в оболонці. Тож сміливо вдосконалюйтесь та знайдіть щось краще.
Представлене тут рішення є далеко не ідеальним:
- Це майже не було тестуванням, тому, будь ласка, пробачте друкарські помилки.
- Існує багато можливостей для вдосконалення, див. Вище.
- Він використовує багато функцій від сучасних
bash
, тому, ймовірно, важко перенести на інші корпуси.
- І можуть бути якісь химерності, про які я не думав.
Однак я думаю, що це досить просто у використанні:
- Додайте лише 4 рядки "бібліотеки".
- Додайте лише 1 рядок "анотації" для функції оболонки.
- Жертви лише один дескриптор файлів тимчасово.
- І кожен крок повинен бути легко зрозуміти навіть через роки.