Я пытаюсь понять, как именно Bash обрабатывает следующую строку:

$(< "$FILE")

Согласно справочной странице Bash, это эквивалентно:

$(cat "$FILE")

И я могу проследить ход рассуждений для этой второй строки. Bash выполняет преобразование переменной в $FILE, вводит подстановку команд, передает значение $FILE в cat, cat выводит содержимое $FILE в стандартный вывод, подстановка команд завершается заменой вся строка со стандартным выводом, полученным из команды внутри, и Bash пытается выполнить ее как простую команду.

Однако для первой строки, о которой я упоминал выше, я понимаю это так: Bash выполняет подстановку переменных на $FILE, Bash открывает $FILE для чтения на стандартном вводе, каким-то образом копируется стандартный ввод в стандартный вывод, подстановка команд завершается, и Bash пытается выполнить результирующий стандартный вывод.

Может кто-нибудь объяснить мне, как содержимое $FILE переходит из стандартного ввода в стандартный вывод?

15
Scott 3 Сен 2017 в 08:43

4 ответа

Лучший ответ

< не является прямым аспектом подстановки команд bash . Это оператор перенаправления (например, канал), который некоторые оболочки разрешают без команды (POSIX не определяет это поведение).

Возможно, было бы понятнее с большим количеством пробелов:

echo $( < $FILE )

Это эффективно* то же самое, что и более POSIX-безопасный

echo $( cat $FILE )

... что также эффективно*

echo $( cat < $FILE )

Начнем с последней версии. Это запускает cat без аргументов, что означает, что он будет считываться со стандартного ввода. $FILE перенаправляется на стандартный ввод из-за <, поэтому cat помещает его содержимое в стандартный вывод. Затем подстановка $(command) помещает вывод cat в аргументы для echo.

В bash (но не в стандарте POSIX) вы можете использовать < без команды. bash (а также zsh и ksh, но не dash) будет интерпретировать это как cat <, хотя и без вызова нового подпроцесса. Поскольку это встроено в оболочку, это быстрее, чем буквальное выполнение внешней команды cat. *Вот почему я говорю "фактически так же, как".

-4
Adam Katz 25 Авг 2017 в 18:04
Итак, в последнем абзаце, когда вы говорите «bash будет интерпретировать это как cat filename», вы имеете в виду, что это поведение характерно для подстановки команд? Потому что, если я запускаю < filename сам по себе, bash его не выдает. Он ничего не выведет и вернет меня к подсказке.
 – 
Stanley Yu
12 Мар 2015 в 23:06
1
@StanleyYu, $(< file) работает быстрее, потому что не создает новый процесс (cat). < является встроенным.
 – 
Adam Katz
12 Мар 2015 в 23:18
1
Канал — это тип файла. Оператор оболочки | создает канал между двумя подпроцессами (или, в некоторых оболочках, от подпроцесса к стандартному вводу оболочки). Оператор оболочки $(…) создает канал от подпроцесса к самой оболочке (не к ее стандартному вводу). Оператор оболочки < не использует канал, он только открывает файл и перемещает дескриптор файла на стандартный ввод.
 – 
Gilles 'SO- stop being evil'
13 Мар 2015 в 02:15
6
< file не совпадает с cat < file (за исключением zsh, где это похоже на $READNULLCMD < file). < file полностью соответствует POSIX и просто открывает file для чтения, а затем ничего не делает (поэтому file сразу закрывается). Это $(< file) или `< file`, который является специальным оператором ksh, zsh и bash (и поведение не указано в POSIX). Подробнее см. мой ответ.
 – 
Stéphane Chazelas
23 Авг 2017 в 18:41
3
Чтобы представить комментарий @StéphaneChazelas в другом свете: в первом приближении $(cmd1) $(cmd2) обычно совпадает с $(cmd1; cmd2). Но посмотрите на случай, когда cmd2 равно < file. Если мы говорим $(cmd1; < file), файл не читается, но с $(cmd1) $(< file) он читается. Так что неверно говорить, что $(< file) — это обычный случай $(command) с командой < file. $(< …) – это частный случай подстановки команд, а не обычное использование перенаправления.
 – 
Scott
3 Сен 2017 в 08:44

$(<file) (также работает с `<file`) — это специальный оператор оболочки Korn, скопированный zsh и bash. Это очень похоже на подстановку команд, но на самом деле это не так.

В оболочках POSIX простая команда:

< file var1=value1 > file2 cmd 2> file3 args 3> file4

Все части являются необязательными, у вас могут быть только перенаправления, только команды, только назначения или комбинации.

Если есть перенаправления, но нет команды, перенаправления выполняются (таким образом, > file будет открываться и усекаться file), но тогда ничего не происходит. Так

< file

Открывает file для чтения, но дальше ничего не происходит, так как нет команды. Таким образом, file закрывается, и все. Если бы $(< file) была простой подстановкой команд, то она бы ничего не расширила.

В спецификации POSIX в $(script) , если script состоит только из перенаправлений, которые выдают неуказанные результаты. Это сделано для того, чтобы обеспечить особое поведение оболочки Korn.

В ksh (здесь протестировано с ksh93u+), если сценарий состоит из одной и только одной простой команды (хотя комментарии разрешены до и после), которая состоит только из перенаправлений (без команды, без назначение) и если первое перенаправление является только вводом stdin (fd 0) (<, << или <<<), то:

  • $(< file)
  • $(0< file)
  • $(<&3) (также $(0>&3) на самом деле, так как это один и тот же оператор)
  • $(< file > foo 2> $(whatever))

Но нет:

  • $(> foo < file)
  • ни $(0<> file)
  • ни $(< file; sleep 1)
  • ни $(< file; < file2)

Тогда

  • все перенаправления, кроме первого, игнорируются (разбираются)
  • и он расширяется до содержимого файла/heredoc/herestring (или того, что можно прочитать из дескриптора файла при использовании таких вещей, как <&3) за вычетом завершающих символов новой строки.

Как при использовании $(cat < file), за исключением того, что

  • чтение выполняется внутри оболочки, а не cat
  • не задействован ни канал, ни дополнительный процесс
  • как следствие вышесказанного, поскольку код внутри не запускается в подоболочке, любые модификации остаются после этого (как в $(<${file=foo.txt}) или $(<file$((++n))))
  • ошибки чтения (но не ошибки при открытии файлов или дублировании файловых дескрипторов) молча игнорируются.

В zsh это то же самое, за исключением того, что это специальное поведение запускается только при наличии только одного перенаправления ввода файла (<file или 0< file, без <&3, <<<here , < a < b...)

Однако, за исключением случаев эмуляции других оболочек, в:

< file
<&3
<<< here...

То есть когда есть только перенаправления ввода без команд, вне подстановки команд, zsh запускает $READNULLCMD (пейджер по умолчанию), а когда есть перенаправления ввода и вывода, $NULLCMD (cat по умолчанию), поэтому, даже если $(<&3) не распознается как этот специальный оператор, он все равно будет работать, как в ksh, хотя для этого вызывается пейджер (этот пейджер, действующий например cat, так как его стандартный вывод будет каналом).

Однако в то время как $(< a < b) ksh будет расширяться до содержимого a, в zsh оно расширяется до содержимого a и b (или просто b, если опция multios отключена), $(< a > b) скопирует a в b и ничего не расширит и т. д.

bash имеет аналогичный оператор, но с некоторыми отличиями:

  • комментарии разрешены до, но не после:

      echo "$(
         # getting the content of file
         < file)"
    

Работает, но:

    echo "$(< file
       # getting the content of file
    )"

Расширяется до нуля.

  • как и в zsh, только одно перенаправление файла stdin, хотя нет возврата к $READNULLCMD, поэтому $(<&3), $(< a < b) выполняют перенаправления, но не расширяются.
  • по какой-то причине, хотя bash не вызывает cat, он по-прежнему разветвляет процесс, который передает содержимое файла через канал, что делает его гораздо менее оптимизированным, чем в других оболочках. По сути, это похоже на $(cat < file), где cat будет встроенным cat.
  • как следствие вышеизложенного, любые изменения, сделанные внутри, впоследствии теряются (в упомянутом выше $(<${file=foo.txt}), например, это назначение $file впоследствии теряется).

В bash IFS= read -rd '' var < file (также работает в zsh) является более эффективным способом чтения содержимого текстового файла в переменную. Это также имеет преимущество сохранения завершающих символов новой строки. См. также $mapfile[file] в zsh (в модуле zsh/mapfile и только для обычных файлов), который также работает с двоичными файлами.

Обратите внимание, что варианты ksh на основе pdksh имеют несколько отличий от ksh93. Интересно, что в mksh (одна из тех оболочек, производных от pdksh), в

var=$(<<'EOF'
That's multi-line
test with *all* sorts of "special"
characters
EOF
)

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

Чтобы быть переносимым на все версии ksh, zsh и bash, лучше всего ограничиться только $(<file), избегая комментариев и учитывая, что изменения переменных, сделанные внутри, могут или может не сохраниться.

20
Stéphane Chazelas 29 Сен 2021 в 19:40
Верно ли, что $(<) является оператором имен файлов? Является ли < в $(<) оператором перенаправления или не является оператором сам по себе и должен быть частью всего оператора $(<)?
 – 
Tim
24 Авг 2017 в 00:32
1
@ Тим, не имеет большого значения, как ты хочешь их называть. $(<file) предназначен для расширения содержимого file аналогично $(cat < file). Как это делается, зависит от оболочки к оболочке, которая подробно описана в ответе. Если хотите, вы можете сказать, что это специальный оператор, который запускается, когда то, что выглядит как подстановка команды (синтаксически), содержит то, что выглядит как одно перенаправление стандартного ввода (синтаксически), но опять же с оговорками и вариациями в зависимости от оболочки, как указано здесь .
 – 
Stéphane Chazelas
24 Авг 2017 в 11:21
@StéphaneChazelas: Как обычно, увлекательно; Я добавил это в закладки. Итак, n<&m и n>&m делают одно и то же? Я этого не знал, но думаю, это не слишком удивительно.
 – 
Scott
3 Сен 2017 в 08:45
@ Скотт, да, они оба делают dup(m, n). Я вижу некоторые доказательства того, что ksh86 использует stdio и некоторые fdopen(fd, "r" or "w"), так что тогда это могло иметь значение. Но использование stdio в оболочке не имеет большого смысла, поэтому я не ожидаю, что вы найдете какую-либо современную оболочку, где это будет иметь значение. Одно отличие состоит в том, что >&n – это dup(n, 1) (сокращение от 1>&n), а <&n – это dup(n, 0) (сокращение от 0<&n).
 – 
Stéphane Chazelas
3 Сен 2017 в 12:08
Правильно. За исключением, конечно, двухаргументной формы вызова дублирования дескриптора файла, которая называется dup2(); dup() принимает только один аргумент и, подобно open(), использует наименьший доступный файловый дескриптор. (Сегодня я узнал, что есть функция dup3().)
 – 
Scott
3 Сен 2017 в 18:52

Поскольку bash делает это внутри вас, расширяет имя файла и выводит файл на стандартный вывод, как если бы вы делали $(cat < filename). Это функция bash, возможно, вам нужно заглянуть в исходный код bash, чтобы точно знать, как это работает.

Вот функция для обработки этой функции (из исходного кода bash, файл builtins/evalstring.c):

/* Handle a $( < file ) command substitution.  This expands the filename,
   returning errors as appropriate, then just cats the file to the standard
   output. */
static int
cat_file (r)
     REDIRECT *r;
{
  char *fn;
  int fd, rval;

  if (r->instruction != r_input_direction)
    return -1;

  /* Get the filename. */
  if (posixly_correct && !interactive_shell)
    disallow_filename_globbing++;
  fn = redirection_expand (r->redirectee.filename);
  if (posixly_correct && !interactive_shell)
    disallow_filename_globbing--;

  if (fn == 0)
    {
      redirection_error (r, AMBIGUOUS_REDIRECT);
      return -1;
    }

  fd = open(fn, O_RDONLY);
  if (fd < 0)
    {
      file_error (fn);
      free (fn);
      return -1;
    }

  rval = zcatfd (fd, 1, fn);

  free (fn);
  close (fd);

  return (rval);
}

Обратите внимание, что $(<filename) не совсем эквивалентен $(cat filename); последний завершится ошибкой, если имя файла начинается с дефиса -.

$(<filename) изначально был из ksh и был добавлен в bash из Bash-2.02.

9
cuonglm 13 Мар 2015 в 09:55
1
cat filename завершится ошибкой, если имя файла начинается с тире, потому что cat принимает параметры. Вы можете обойти это в большинстве современных систем с помощью cat -- filename.
 – 
Adam Katz
6 Июл 2017 в 04:20

Вот фрагмент кода bash 3.2, который показывает разницу, объясненную:

  • Используйте strace для отслеживания процессов и отображения вызовов execve strace -f -e trace=execve
  • Запуск команд чтения bash из строки bash -c - с /bin/cat и без
  • Разделите выходные данные в режиме «бок о бок» на 80 столбцов, чтобы поместиться здесь diff -y -W 80

Вы можете увидеть дополнительные execve(/bin/cat...) в правой части разницы:

$ echo $BASH_VERSION
3.2.25(1)-release
$ echo "hi" >/tmp/f
$ strace -f -e trace=execve /bin/bash -c 'echo $(</tmp/f)'          >/tmp/no_cat 2>&1
$ strace -f -e trace=execve /bin/bash -c 'echo $(/bin/cat </tmp/f)' >/tmp/wi_cat 2>&1
$ diff -y -W 80 /tmp/no_cat /tmp/wi_cat
execve("/bin/bash", ["/bin/bash", "-c | execve("/bin/bash", ["/bin/bash", "-c
Process 24253 attached (waiting for p | Process 24256 attached (waiting for p
Process 24253 resumed (parent 24252 r | Process 24256 resumed (parent 24255 r
Process 24253 detached                | Process 24257 attached (waiting for p
                                      > Process 24257 resumed (parent 24256 r
                                      > Process 24256 suspended
                                      > [pid 24257] execve("/bin/cat", ["/bin
                                      > Process 24256 resumed
                                      > Process 24257 detached
                                      > [pid 24256] --- SIGCHLD (Child exited
                                      > Process 24256 detached
--- SIGCHLD (Child exited) @ 0 (0) --   --- SIGCHLD (Child exited) @ 0 (0) --
hi                                      hi
0
qneill 16 Сен 2021 в 00:52