Для этого нужен bash 4.1, если вы используете {fd}
илиlocal -n
.
Остальное, надеюсь, будет работать в bash 3.x. Я не совсем уверен, потому что printf %q
это может быть особенность bash 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
, потому что это затрудняет повторное использование этого рецепта.
- Также у нас есть ненужная вилка (the
$(cat)
), но, поскольку это решение, !DO NOT USE!
я выбрал кратчайший путь.
Однако это показывает, что мы можем сделать это без изменений d()
(и без local -n
)!
Обратите внимание, что нам это не обязательно _xcapture
, так как мы могли бы написать все прямо в eval
.
Однако делать это обычно не очень удобно. И если вы вернетесь к своему сценарию через несколько лет, вы, вероятно, захотите без особых проблем прочитать его снова.
Исправить гонку
Теперь исправим состояние гонки.
Уловка может заключаться в том, чтобы дождаться, пока printf
не закроется его STDOUT, а затем вывести 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 вызываемой функции. Или вы хотите даже передавать и передавать более одного дескриптора файла из переменных и в них.
У меня пока нет решения, но вот способ поймать более одного FD , так что мы, вероятно, тоже можем передать обратно переменные.
Также не забывайте:
Это должно вызывать функцию оболочки, а не внешнюю команду.
Нет простого способа передать переменные среды из внешних команд. ( LD_PRELOAD=
Хотя это должно быть возможно!) Но тогда это совсем другое.
Последние слова
Это не единственно возможное решение. Это один из примеров решения.
Как всегда, у вас есть много способов выразить вещи в оболочке. Так что не стесняйтесь улучшать и находить что-то лучше.
Представленное здесь решение далеко от совершенства:
- Это почти не тестировалось, так что простите за опечатки.
- Есть много возможностей для улучшения, см. Выше.
- Он использует многие функции из современных
bash
, поэтому, вероятно, его сложно перенести на другие оболочки.
- И могут быть некоторые причуды, о которых я не задумывался.
Однако я думаю, что это довольно просто использовать:
- Добавьте всего 4 строчки «библиотеки».
- Добавьте только одну строку «аннотации» для вашей функции оболочки.
- Временно жертвует только одним файловым дескриптором.
- И каждый шаг должен быть понятен даже спустя годы.