Странности перенаправления ввода в сценарии оболочки

Кто-нибудь может объяснить такое поведение? Бег:

#!/bin/sh
echo "hello world" | read var1 var2
echo $var1
echo $var2

ничего не выводит, в то время как:

#!/bin/sh
echo "hello world" > test.file
read var1 var2 < test.file
echo $var1
echo $var2

производит ожидаемый результат:

hello
world

Разве канал не должен делать за один шаг то же, что перенаправление на test.file во втором примере? Я пробовал один и тот же код с оболочками dash и bash и получил одинаковое поведение от них обоих.

Ответов (9)

Решение

Недавнее дополнение bash - это lastpipe опция, которая позволяет последней команде в конвейере выполняться в текущей оболочке, а не в подоболочке, когда управление заданиями деактивировано.

#!/bin/bash
set +m      # Deactiveate job control
shopt -s lastpipe
echo "hello world" | read var1 var2
echo $var1
echo $var2

действительно будет выводить

hello
world

Это связано с тем, что версия конвейера создает подоболочку, которая считывает переменную в свое локальное пространство, которое затем уничтожается при выходе из подоболочки.

Выполните эту команду

$ echo $$;cat | read a
10637

и используйте pstree -p для просмотра запущенных процессов, вы увидите дополнительную оболочку, свисающую с вашей основной оболочки.

    |                       |-bash(10637)-+-bash(10786)
    |                       |             `-cat(10785)

Хорошо, я разобрался!

Эту ошибку сложно поймать, но она возникает из-за того, как оболочка обрабатывает каналы. Каждый элемент конвейера выполняется в отдельном процессе. Когда команда чтения устанавливает переменные var1 и var2, она устанавливает их собственную подоболочку, а не родительскую оболочку. Поэтому, когда подоболочка закрывается, значения var1 и var2 теряются. Однако вы можете попробовать сделать

var1=$(echo "Hello")
echo var1

который возвращает ожидаемый ответ. К сожалению, это работает только для отдельных переменных, вы не можете установить их сразу несколько. Чтобы установить несколько переменных одновременно, вы должны либо прочитать одну переменную и разделить ее на несколько переменных, либо использовать что-то вроде этого:

set -- $(echo "Hello World")
var1="$1" var2="$2"
echo $var1
echo $var2

Хотя я признаю, что это не так элегантно, как использование трубы, это работает. Конечно, вы должны иметь в виду, что чтение предназначалось для чтения из файлов в переменные, поэтому чтение из стандартного ввода должно быть немного сложнее.

На этот вопрос уже был дан правильный ответ, но решение еще не заявлено. Используйте ksh, а не bash. Сравнивать:

$ echo 'echo "hello world" | read var1 var2
echo $var1
echo $var2' | bash -s

К:

$ echo 'echo "hello world" | read var1 var2
echo $var1
echo $var2' | ksh -s
hello
world

ksh - превосходная программная оболочка из-за таких мелких нюансов. (На мой взгляд, bash - лучшая интерактивная оболочка.)

На сообщение был дан правильный ответ, но я хотел бы предложить альтернативный лайнер, который, возможно, может быть полезен.

Для присвоения значений, разделенных пробелом, из echo (или stdout, если на то пошло) переменным оболочки, вы можете рассмотреть возможность использования массивов оболочки:

$ var=( $( echo 'hello world' ) )
$ echo ${var[0]}
hello
$ echo ${var[1]}
world

В этом примере var - это массив, и к его содержимому можно получить доступ с помощью конструкции $ {var [index]}, где index - это индекс массива (начинается с 0).

Таким образом, у вас может быть столько параметров, сколько вы хотите присвоить соответствующему индексу массива.

read var1 var2 < <(echo "hello world")

Пытаться:

echo "hello world" | (read var1 var2 ; echo $var1 ; echo $var2 )

The problem, as multiple people have stated, is that var1 and var2 are created in a subshell environment that is destroyed when that subshell exits. The above avoids destroying the subshell until the result has been echo'd. Another solution is:

result=`echo "hello world"`
read var1 var2 <<EOF
$result
EOF
echo $var1
echo $var2
#!/bin/sh
echo "hello world" | read var1 var2
echo $var1
echo $var2

не производит никакого вывода, потому что конвейеры запускают каждый из своих компонентов внутри подоболочки. Субоболочки наследуют копии переменных родительской оболочки, а не разделяют их. Попробуй это:

#!/bin/sh
foo="contents of shell variable foo"
echo $foo
(
    echo $foo
    foo="foo contents modified"
    echo $foo
)
echo $foo

Скобки определяют область кода, которая запускается в подоболочке, а $ foo сохраняет свое исходное значение после изменения внутри них.

А теперь попробуйте это:

#!/bin/sh
foo="contents of shell variable foo"
echo $foo
{
    echo $foo
    foo="foo contents modified"
    echo $foo
}
echo $foo

Фигурные скобки предназначены исключительно для группировки, подоболочка не создается, и $ foo, измененный внутри фигурных скобок, является тем же самым $ foo, измененным вне них.

А теперь попробуйте это:

#!/bin/sh
echo "hello world" | {
    read var1 var2
    echo $var1
    echo $var2
}
echo $var1
echo $var2

Внутри фигурных скобок встроенная функция чтения правильно создает $ var1 и $ var2, и вы можете видеть, что они отображаются эхом. Вне брекетов их больше нет. Весь код в фигурных скобках выполняется в подоболочке, поскольку это один из компонентов конвейера .

Вы можете поместить произвольное количество кода между фигурными скобками, так что вы можете использовать эту конструкцию piping-into-a-block всякий раз, когда вам нужно запустить блок сценария оболочки, который анализирует вывод чего-то еще.

Мой взгляд на эту проблему (с использованием Bash):

read var1 var2 <<< "hello world"
echo $var1 $var2