Сохранить вывод команды, которая изменяет среду в переменную

8

Как сохранить выходные данные команды, которая изменяет среду в переменную?

Я использую оболочку bash.

Предположим, что у меня есть:

function f () { a=3; b=4 ; echo "`date`: $a $b"; }

И теперь я могу использовать команды для запуска f:

$ a=0; b=0; f; echo $a; echo $b; echo $c
Sat Jun 28 21:27:08 CEST 2014: 3 4
3
4

но я хотел бы сохранить вывод fпеременной c, поэтому я попытался:

a=0; b=0; c=""; c=$(f); echo $a; echo $b; echo $c

но, к сожалению, у меня есть:

0
0
Sat Jun 28 21:28:03 CEST 2014: 3 4

поэтому у меня нет никаких изменений окружающей среды здесь.

Как сохранить выходные данные команды (не только функции) в переменную и сохранить изменения среды?

Я знаю, что $(...)открывается новый подоболочек и в этом проблема, но можно ли сделать какой-то обходной путь?

Фарамир
источник
Правильно, проблема в том, что $aи $bявляются локальными переменными в вашей fфункции. Вы могли бы exportих, но это кажется отрывочным.
HalosGhost
1
@HalosGhost: Нет, я так не думаю. Посмотрите на первый пример: …; f; echo $a; …результаты 3отображаются, fкак и изменение переменной оболочки (а не только ее локальной переменной).
Скотт

Ответы:

2

Если вы используете Bash 4 или более позднюю версию, вы можете использовать сопроцессы :

function f () { a=3; b=4 ; echo "`date`: $a $b"; }
coproc cat
f >&${COPROC[1]}
exec {COPROC[1]}>&-
read c <&${COPROC[0]}
echo a $a
echo b $b
echo c $c

будет выводить

a 3
b 4
c Sun Jun 29 10:08:15 NZST 2014: 3 4

coprocсоздает новый процесс, выполняющий данную команду (здесь, cat). Он сохраняет PID COPROC_PIDи стандартные дескрипторы выходных / входных файлов в массиве COPROC(как pipe(2), например , или смотрите здесь или здесь ).

Здесь мы запускаем функцию со стандартным выводом, указывающим на запущенный нами сопроцесс cat, а затем readиз него. Так как catпросто выплевывает свой ввод обратно, мы получаем вывод функции в нашу переменную. exec {COPROC[1]}>&-просто закрывает файловый дескриптор, чтобы catне ждать вечно.


Обратите внимание, что readзанимает только одну строку за раз. Вы можете использовать, mapfileчтобы получить массив строк, или просто использовать дескриптор файла, однако вы хотите использовать его по-другому.

exec {COPROC[1]}>&-работает в текущих версиях Bash, но более ранние версии 4- й серии требуют , чтобы сохранить дескриптор файла в простой переменной первой: fd=${COPROC[1]}; exec {fd}>&-. Если ваша переменная не установлена, она закроет стандартный вывод.


Если вы используете версию Bash для 3-х серий, вы можете получить тот же эффект mkfifo, но это не намного лучше, чем использовать настоящий файл в тот момент.

Майкл Гомер
источник
Я могу использовать только Bash 3.1. Трубы сложны в использовании. Есть ли у mktemp и это единственный вариант? Разве здесь нет специального синтаксиса или опции для запуска команды и получения вывода, но с изменениями среды?
Фарамир
Вы можете использовать mkfifo для создания именованного канала в качестве замены для coproc, который не включает в себя фактическую запись данных на диск. При этом вы перенаправляете вывод в fifo и вводите его вместо сопроцесса. В противном случае нет.
Майкл Гомер
+1 за адаптацию моего ответа не использовать файл. Но в моих тестах exec {COPROC[1]}>&-команда (по крайней мере, иногда) немедленно завершает сопроцесс , поэтому ее выходные данные больше не доступны для чтения. coproc { cat; sleep 1; }кажется, работает лучше. (Возможно, вам придется увеличить, 1если вы читаете более одной строки.)
Скотт
1
Здесь используется общепринятый, хорошо понятный смысл «фактического файла», который вы умышленно игнорируете.
Майкл Гомер
1
«Фактический файл» означал файл на диске , как понимают все, кроме вас, якобы. Никто никогда не подразумевал и не позволял разумному читателю сделать вывод, что любой из них совпадает с аргументами или переменными среды. Я не заинтересован в продолжении этой дискуссии.
Майкл Гомер
2

Если вы уже рассчитываете на то, что тело будет fвыполнено в той же оболочке, что и вызывающая программа, и, таким образом, сможете изменять переменные, такие как aи b, почему бы не сделать функцию просто установленной c? Другими словами:

$ function f () { a=3; b=4 ; c=$(echo "$(date): $a $b"); }
$ a=0; b=0; f; echo $a; echo $b; echo $c
3
4
Mon Jun 30 14:32:00 EDT 2014: 3 4

Одна из возможных причин может заключаться в том, что выходная переменная должна иметь разные имена (c1, c2 и т. Д.) При вызове в разных местах, но вы можете решить эту проблему, установив функцию c_TEMP и сделав вызывающую функцию и c1=$c_TEMPт. Д.

godlygeek
источник
1

Это клуге, но попробуй

f > c.file
c=$(cat c.file)

(и, необязательно, rm c.file).

Скотт
источник
Спасибо. Это простой ответ, и он работает, но имеет некоторые недостатки. Я знаю, что могу использовать mktemp, но я не хотел бы создавать дополнительные файлы.
Фарамир
1

Просто назначьте все переменные и запишите результат одновременно.

f() { c= ; echo "${c:=$(date): $((a=3)) $((b=4))}" ; }

Теперь, если вы делаете:

f ; echo "$a $b $c"

Ваш вывод:

Tue Jul  1 04:58:17 PDT 2014: 3 4
3 4 Tue Jul  1 04:58:17 PDT 2014: 3 4

Обратите внимание, что это полностью переносимый код POSIX. Я первоначально установлен c=на ''строку с нулевым символом , поскольку расширение параметр ограничивает одновременное задание переменной + оценку либо только числовые (например $((var=num))) или с нулевыми или несуществующих значений - или, другими словами, вы не можете одновременно установить и оценить переменную для произвольной строки если этой переменной уже присвоено значение. Поэтому я просто проверяю, что он пуст, прежде чем пытаться. Если бы я не опустел cдо того, как попытаться назначить его, расширение вернуло бы только старое значение.

Просто чтобы продемонстрировать:

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2

newvalэто не назначается $cрядный , потому что oldvalрасширяется в ${word}, в то время как рядный $((арифметическое =задание ))всегда происходит. Но если $c не имеет oldval и пусто или не установлено ...

sh -c '
    c=oldval a=1
    echo ${c:=newval} $((a=a+a)) 
    c= a=$((a+a))
    echo ${c:=newval} $((a=a+a))
'
###OUTPUT###
oldval 2
newval 8

... тогда newvalсразу назначается и расширяется в $c.

Все другие способы сделать это включают некоторую форму вторичной оценки. Например, скажем, я хотел назначить вывод f()переменной, названной nameв одной точке и varв другой. Как написано в настоящее время, это не будет работать без установки переменной var в области действия вызывающего. Другой способ может выглядеть так:

f(){ fout_name= fout= set -- "${1##[0-9]*}" "${1%%*[![:alnum:]]*}"
    (: ${2:?invalid or unspecified param - name set to fout}) || set --
    export "${fout_name:=${1:-fout}}=${fout:=$(date): $((a=${a:-50}+1)) $((b=${b:-100}-4))}"
    printf %s\\n "$fout"
}

f &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$a" "$b"

Ниже приведен лучший форматированный пример, но, как указано выше, вывод выглядит так:

sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
fout
Wed Jul  2 02:27:07 PDT 2014: 51 96
51
96

Или с разными $ENVаргументами:

b=9 f myvar &&
    printf %s\\n \
        "$fout_name" \
        "$fout" \
        "$myvar" \
        "$a" "$b"

###OUTPUT###

Tue Jul  1 19:56:42 PDT 2014: 52 5
myvar
Tue Jul  1 19:56:42 PDT 2014: 52 5
Tue Jul  1 19:56:42 PDT 2014: 52 5
52
5

Вероятно, самая сложная вещь, которую нужно сделать правильно, когда дело доходит до двукратной оценки, - убедиться, что переменные не разбивают кавычки и не выполняют случайный код. Чем больше раз оценивается переменная, тем сложнее она становится. Расширение параметров здесь очень помогает, и использование exportв отличие от evalнего намного безопаснее.

В приведенном выше примере f()первый назначает $foutна ''строку с нулевым символом , а затем устанавливает позиционные Params испытанию для допустимых имен переменных. Если оба теста не пройдены, отправляется сообщение stderrи foutназначается значение по умолчанию $fout_name. Однако независимо от тестов $fout_nameвсегда присваивается либо foutуказанное вами имя, либо, $foutи, опционально, указанному имени всегда присваивается выходное значение функции. Чтобы продемонстрировать это, я написал этот маленький forцикл:

for v in var '' "wr;\' ong"
    do sleep 10 &&
        a=${a:+$((a*2))} f "$v" || break
    echo "${v:-'' #null}" 
    printf '#\t"$%s" = '"'%s'\n" \
        a "$a" b "$b" \
        fout_name "$fout_name" \
        fout "$fout" \
        '(eval '\''echo "$'\''"$fout_name"\")' \
            "$(eval 'echo "$'"$fout_name"\")"
 done

Он играет с именами переменных и расширениями параметров. Если у вас есть вопрос, просто спросите. Это только запускает те же несколько строк в функции, уже представленной здесь. Стоит отметить , по крайней мере , хотя , что $aи $bпеременные ведут себя по- разному в зависимости от того, являются ли они определены при вызове, или установить уже. Тем forне менее, они почти ничего не делают, кроме как форматируют набор данных и предоставляют f(). Посмотри:

###OUTPUT###

Wed Jul  2 02:50:17 PDT 2014: 51 96
var
#       "$a" = '51'
#       "$b" = '96'
#       "$fout_name" = 'var'
#       "$fout" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:17 PDT 2014: 51 96'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:27 PDT 2014: 103 92
'' #null
#       "$a" = '103'
#       "$b" = '92'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:27 PDT 2014: 103 92'
sh: line 2: 2: invalid or unspecified param - name set to fout
Wed Jul  2 02:50:37 PDT 2014: 207 88
wr;\' ong
#       "$a" = '207'
#       "$b" = '88'
#       "$fout_name" = 'fout'
#       "$fout" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
#       "$(eval 'echo "$'"$fout_name"\")" = 'Wed Jul  2 02:50:37 PDT 2014: 207 88'
mikeserv
источник