Чем перенаправление файлов bash на стандартный вход отличается от перенаправления файлов оболочки (`sh`) в Linux?

Чем перенаправление файлов bash на стандартный вход отличается от перенаправления файлов оболочки (`sh`) в Linux?

Я написал скрипт, который переключает пользователей во время работы, и выполнил его, используя перенаправление файлов на стандартный вход. Так user-switch.shже...

#!/bin/bash

whoami
sudo su -l root
whoami

И запуск этого bashдает мне поведение, которое я ожидаю

$ bash < user-switch.sh
vagrant
root

Однако, если я запущу скрипт с помощью sh, я получу другой вывод

$ sh < user-switch.sh 
vagrant
vagrant

Почему bash < user-switch.shвыводится другой результат, чем sh < user-switch.sh?

Примечания:

  • происходит на двух разных компьютерах под управлением Debian Jessie

решение1

Похожий скрипт, без sudo, но с похожими результатами:

$ cat script.sh
#!/bin/bash
sed -e 's/^/--/'
whoami

$ bash < script.sh
--whoami

$ dash < script.sh
itvirta

При использовании bashостальная часть скрипта передается в качестве входных данных для sed, при использовании dashоболочка интерпретирует их.

Запуск straceна них: dashсчитывает блок скрипта (здесь восемь кБ, более чем достаточно для хранения всего скрипта), а затем создает sed:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 8192) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Это означает, что дескриптор файла находится в конце файла и sedне увидит никаких входных данных. Оставшаяся часть буферизуется в dash. (Если бы скрипт был длиннее размера блока 8 кБ, оставшаяся часть была бы прочитана sed.)

Bash, с другой стороны, возвращается к концу последней команды:

read(0, "#!/bin/bash\nsed -e 's/^/--/'\nwho"..., 36) = 36
stat("/bin/sed", {st_mode=S_IFREG|0755, st_size=73416, ...}) = 0
...
lseek(0, -7, SEEK_CUR)                  = 29
clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|...

Если входные данные поступают из канала, как здесь:

$ cat script.sh | bash

перемотка невозможна, так как каналы и сокеты не могут быть найдены. В этом случае Bash возвращается к чтению вводаодин символ за разчтобы избежать перечитывания. (fd_to_buffered_stream()вinput.c) Выполнение полного системного вызова для каждого байта в принципе не очень эффективно. На практике я не думаю, что чтение будет большой накладной расходой по сравнению, например, с тем фактом, что большинство действий оболочки включают в себя порождение совершенно новых процессов.

Похожая ситуация:

echo -e 'foo\nbar\ndoo' | bash -c 'read a; head -1'

Подоболочка должна убедиться, что readсчитывает только первую новую строку, чтобы headувидеть следующую строку. (Это dashтоже работает с .)


Другими словами, Bash делает дополнительные шаги для поддержки чтения одного и того же источника для самого скрипта и для команд, выполняемых из него. dashне делает. zsh, и ksh93упакованные в Debian идут с Bash в этом плане.

решение2

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

Оболочки работают так: они считывают фрагмент исходного кода, анализируют его и, если находят полную команду, запускают ее, а затем продолжают работу с оставшейся частью фрагмента и оставшейся частью файла. Если фрагмент не содержит полной команды (с завершающим символом в конце — я думаю, что все оболочки считывают до конца строки), оболочка считывает другой фрагмент и так далее.

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

Bash ищет конец исходного кода команды в скрипте перед выполнением команды. На это нельзя рассчитывать, не только потому, что другие оболочки этого не делают, но и потому, что это работает только в том случае, если оболочка читает из обычного файла. Если оболочка читает из канала (например, ssh remote-host.example.com <local-script-file.sh), данные, которые были прочитаны, считываются и не могут быть отменены.

Если вы хотите передать входные данные команде в скрипте, вам нужно сделать это явно, обычно с помощьюздесь документ. (Документ here обычно наиболее удобен для многострочного ввода, но подойдет любой метод.) Написанный вами код работает только в нескольких оболочках, только если скрипт передается в качестве входных данных в оболочку из обычного файла; если вы ожидали, что второй файл whoamiбудет передан в качестве входных данных в sudo …, подумайте еще раз, имея в виду, что большую часть времени скрипт не передается в стандартный ввод оболочки.

#!/bin/bash
whoami
sudo su -l root <<'EOF'
whoami
EOF

Обратите внимание, что в этом десятилетии вы можете использовать sudo -i root. Бег sudo su— это хак из прошлого.

Связанный контент