Как сохранить вывод команды в переменной без создания подоболочки [Bash ‹v4]

ksh имеет действительно интересную конструкцию для этого, подробно описанную в этом ответе: https://stackoverflow.com/ а/11172617/636849

Начиная с Bash 4.0, существует встроенная команда mapfile, которая должна решить эту проблему: http://www.gnu.org/software/bash/manual/html_node/Bash-Builtins..html

Но, как ни странно, это не работает с заменой процесса:

foo () { echo ${BASH_SUBSHELL}; }
mapfile -t foo_output <(foo) # FAIL: hang forever here
subshell_depth=${foo_output[0]} # should be 0

Но как это сделать в Bash v3.2?


person Lucas Cimon    schedule 07.02.2014    source источник
comment
Из любопытства, как бы вы использовали mapfile? Насколько мне известно, эквивалента bash нет ни в одной версии.   -  person chepner    schedule 07.02.2014
comment
mapfile -t foo_output <(foo) - это замена процесса - foo запускается в совершенно новом процессе, что, я думаю, вам не нужно. Смотрите дополнение к моему ответу.   -  person Digital Trauma    schedule 07.02.2014
comment
Я не знаком с mapfile, но поскольку он читает стандартный ввод, а не файл, вам, возможно, придется писать < <(cmd), а не просто <(cmd), поскольку <(cmd) в конечном итоге заменяется чем-то вроде /dev/fd/63 (попробуйте echo <(echo), чтобы проверить это). По крайней мере, кажется, что так не висит.   -  person Alice M.    schedule 11.09.2018


Ответы (4)


Вот еще один способ сделать это, который отличается настолько, что требует отдельного ответа. Я думаю, что этот метод не содержит подоболочек и не содержит подпроцессов bash:

ubuntu@ubuntu:~$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
ubuntu@ubuntu:~$ bar
0 8215
ubuntu@ubuntu:~$ mkfifo /tmp/myfifo
ubuntu@ubuntu:~$ exec 3<> /tmp/myfifo
ubuntu@ubuntu:~$ unlink /tmp/myfifo
ubuntu@ubuntu:~$ bar 1>&3
ubuntu@ubuntu:~$ read -u3 a
ubuntu@ubuntu:~$ echo $a
0 8215
ubuntu@ubuntu:~$ exec 3>&-
ubuntu@ubuntu:~$

Хитрость здесь заключается в том, чтобы использовать exec для открытия FIFO в режиме чтения-записи с FD, который, по-видимому, имеет побочный эффект, делая FIFO неблокирующим. Затем вы можете перенаправить свою команду на FD без блокировки, а затем прочитать FD.

Обратите внимание, что FIFO будет буфером ограниченного размера, вероятно, около 4 КБ, поэтому, если ваша команда выдаст больше вывода, чем это, она снова заблокируется.

person Digital Trauma    schedule 07.02.2014
comment
Ууу, фиктивный трюк с FD/FIFO потрясающий! Да, похоже, это делает трюк. Вы даже можете определить функцию назначения: assign () { local var=$1; shift; "$@" > /tmp/myfifo; read ${var} < /tmp/myfifo; }. Проверил, работает foo () { local b; assign b bar; echo $b; } - person Lucas Cimon; 08.02.2014
comment
@LucasCimon Обратите внимание, что он будет работать только для однострочного вывода, и хотя он, вероятно, лучше, чем подоболочка, если /tmp смонтирован в памяти, определенно хуже, если это не так, поскольку он создает доступ к диску. - person EvgenKo423; 18.02.2021

Вот что я мог придумать - это немного запутанно, но foo запускается в контексте оболочки верхнего уровня, и его вывод предоставляется в переменной a в контексте оболочки верхнего уровня:

#!/bin/bash

foo () { echo ${BASH_SUBSHELL}; }

mkfifo /tmp/fifo{1,2}
{
    # block, then read everything in fifo1 into the buffer array
    i=0
    while IFS='' read -r ln; do
        buf[$((i++))]="$ln"
    done < /tmp/fifo1
    # then write everything in the buffer array to fifo2
    for i in ${!buf[@]}; do
        printf "%s\n" "${buf[$i]}"
    done > /tmp/fifo2
} &

foo > /tmp/fifo1
read a < /tmp/fifo2
echo $a

rm /tmp/fifo{1,2}

Это, конечно, предполагает две вещи:

  • FIFO разрешены
  • Группу команд, выполняющую буферизацию, можно перевести в фоновый режим.

Я проверил, как это работает в следующих версиях bash:

  • 3.00.15(1)-выпуск (x86_64-redhat-linux-gnu)
  • 3.2.48(1)-выпуск (x86_64-apple-darwin12)
  • 4.2.25(1)-выпуск (x86_64-pc-linux-gnu)

Приложение

Я не уверен, что подход mapfile в bash 4.x делает то, что вы хотите, поскольку замена процесса <() создает совершенно новый процесс bash (хотя и не подоболочку bash внутри этого процесса bash):

$ bar () { echo "$BASH_SUBSHELL $BASHPID"; }
$ bar
0 2636
$ mapfile -t bar_output < <(bar)
$ echo ${bar_output[0]}
0 60780
$ 

Таким образом, хотя $BASH_SUBSHELL здесь равен 0, это потому, что он находится на верхнем уровне нового процесса оболочки 60780 в замене процесса.

person Digital Trauma    schedule 07.02.2014
comment
Действительно интересный ответ, спасибо! Я думал об использовании /dev/shm , но именованные каналы — действительно хорошая идея. Однако есть несколько вопросов: 1) откуда берется buf ? 2) Зачем увеличивать i в первом цикле? Кроме того, ваше объяснение mapfile идеально! - person Lucas Cimon; 07.02.2014
comment
@LucasCimon - buf - это просто имя массива bash, который я использую для буферизации потока между fifo1 и fifo2. - person Digital Trauma; 07.02.2014
comment
С точки зрения оболочки <(cmd) выглядит как файл. Поэтому вам нужен дополнительный < для перенаправления на mapfile. Вот почему mapfile блокируется - на самом деле он блокируется на стандартном вводе. Введите ^D, и он разблокируется. - person Digital Trauma; 07.02.2014
comment
О, я не знал, что массивы не нужно объявлять. Тем не менее, вам нужен еще один непрерывно работающий фоновый процесс. Как насчет отсутствия процесса/подоболочки ВООБЩЕ? - person Lucas Cimon; 07.02.2014
comment
Индексированные массивы не нужно объявлять явно. Ассоциативные массивы делают declare -A array. Отсутствие fork определенно станет для меня более сложной задачей. - person Digital Trauma; 07.02.2014
comment
@LucasCimon i - это просто индекс в буфере. Он увеличивается, чтобы мы могли записать следующую строку в следующий элемент массива в буфере. Я думаю, что вместо этого я мог бы просто добавить строки к длинной строке, но подход с массивом казался более чистым. - person Digital Trauma; 07.02.2014

Самый простой способ — удалить функцию и напрямую передать переменную, например:

declare -a foo_output
mapfile -t foo_output <<<${BASH_SUBSHELL}
subshell_depth=${foo_output[0]} # Should be zero.

В противном случае заданы два элемента в функции:

foo () { echo "$BASH_SUBSHELL $BASHPID"; }

вы можете использовать read (изменив IFS по мере необходимости) как одну из следующих команд:

cat < <(foo) | read subshell_depth pid # Two variables.
read -r subshell_depth pid < <(foo) # Two separate variables.
read -a -r foo_arr < <(foo) # One array.

или используя readarray/mapfile (Bash > 4):

mapfile -t foo_output < <(foo)
readarray -t foo_output < <(foo)

затем преобразуйте вывод обратно в массив:

foo_arr=($foo_output)
subshell_depth=${foo_arr[0]} # should be 0
person kenorb    schedule 27.02.2016
comment
Очень хороший ответ. Я не понимаю, почему это не было принято. Это решение работает только для версии 4? Спасибо, @kenorb! - person SomeStupid; 24.12.2020

Этот вопрос возникает очень часто при поиске того, как просто записать вывод любой команды «печати» в переменную. Так что для тех, кто ищет, это возможно (начиная с bash v3.1.0) с помощью:

printf -v VARIABLE_NAME "whatever you need here: %s" $ID

Если вы настроите свои скрипты на скорость, вы можете использовать шаблон установки некоторой глобальной переменной в конце функций вместо того, чтобы просто "выводить" ее - используйте это с осторожностью, иногда это критикуют за то, что это приводит к сложному сопровождению кода.

person yatsek    schedule 16.03.2018
comment
действительно, не ответ на вопрос, но полезно знать. Обратите внимание, что это относится к встроенной функции bash. GNU coreutils printf (у меня 8.3) не предлагает флаг -v. type -a printf, чтобы узнать, какие printf у вас есть в наличии. builtin printf -v VARTOSET "put me in the var" для явного вызова встроенного. - person Jack Wasey; 22.01.2021