Но тот же результат.
Ответ 2
Это требует 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 "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }
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="$("[email protected]")"
}
Теперь вместо того, чтобы писать
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
не только избыточен, он подвержен ошибкам и всегда повторяется в правильном контексте. Что если вы используете его 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
который не работает в oder common 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 "[email protected]")"; }
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 ad; echo
Затем _xcapture ad; echo
выводит x
или a
first соответственно.
-
_passback x
не должен быть частью _xcapture
, потому что это затрудняет повторное использование этого рецепта. - Также у нас есть несколько ненужных форков (
$(cat)
), но так как это решение !DO NOT USE!
Я выбрал кратчайший путь.
Тем не менее, это показывает, что мы можем сделать это без модификации d()
(и без local -n
)!
Обратите внимание, что нам необязательно нужен _xcapture
, так как мы могли бы написать все правильно в eval
.
Однако делать это обычно не очень читабельно. И если вы вернетесь к своему сценарию через несколько лет, вы, вероятно, захотите прочитать его снова без особых проблем.
Исправить гонку
Теперь давайте исправим состояние гонки.
Трюк может заключаться в том, чтобы подождать, пока printf
закроет его STDOUT, а затем вывести x
.
Есть много способов архивировать это:
- Вы не можете использовать трубы оболочки, потому что трубы работают в разных процессах.
- Можно использовать временные файлы,
- или что-то вроде файла блокировки или FIFO. Это позволяет ждать блокировки или FIFO,
- или другие каналы, чтобы вывести информацию, а затем собрать вывод в некоторой правильной последовательности.
Следующий путь может выглядеть следующим образом (обратите внимание, что он выполняет 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 "[email protected]")"; }
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 "[email protected]")"; }
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 "[email protected]" "$?"; }
_capture() { { out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)"; }
capture() { eval "$(_capture "[email protected]")"; }
# 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 passback() { set -- "[email protected]" "$?"; while [ 1 -lt $# ]; do printf '%q=%q;' "$1" "${!1}"; shift; done; return $1; }
passback() { set -- "[email protected]" "$?"; 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)")"; }
capture() { eval "$({ out="$("${@:2}" 3<&-; "$2_" >&3)"; ret=$?; printf "%q=%q;" "$1" "$out"; } 3>&1; echo "(exit $ret)")"; }
-
Решение загрязняет файловый дескриптор (здесь 3), используя его для внутреннего использования. Вы должны иметь это в виду, если вам случится пройти FDs.
Обратите внимание, что в bash
4.1 и выше есть {fd}
для использования неиспользованного FD.
(Возможно, я добавлю решение здесь, когда приду.)
Обратите внимание, что именно поэтому я использую его в отдельных функциях, таких как _capture
, потому что это возможно в одной строке, но все труднее читать и понимать
-
Возможно, вы хотите захватить STDERR вызываемой функции тоже. Или вы хотите даже передать и вывести более одного файлового дескриптора из и в переменные.
У меня пока нет решения, однако здесь есть способ перехватить более одного FD, поэтому мы, вероятно, можем также передать переменные таким же образом.
И не забудь
Это должно вызывать функцию оболочки, а не внешнюю команду.
Нет простого способа передать переменные окружения из внешних команд. (С LD_PRELOAD=
это должно быть возможно, хотя!) Но тогда это нечто совершенно другое.
Последние слова
Это не единственно возможное решение. Это один из примеров решения.
Как всегда у вас есть много способов выразить вещи в оболочке. Так что не стесняйтесь улучшать и находить что-то лучшее.
Представленное здесь решение довольно далеко от идеального:
- Это был почти не тест, так что, пожалуйста, прости опечатки.
- Существует много возможностей для улучшения, см. Выше.
- Он использует много функций из современного
bash
, поэтому, вероятно, его сложно портировать на другие оболочки. - И могут быть некоторые причуды, о которых я не думал.
Однако я думаю, что это довольно легко использовать:
- Добавьте всего 4 строчки "библиотеки".
- Добавьте только 1 строку "аннотации" для вашей функции оболочки.
- Временно жертвует только одним файловым дескриптором.
- И каждый шаг должен быть понятен даже спустя годы.