Почему awk останавливается и ждет, если имя файла содержит = и как обойти это?

25
awk 'processing_script_here' my=file.txt

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

don_crissti
источник
связанные: unix.stackexchange.com/a/475013/308316
мосви

Ответы:

19

Как говорит Крис , аргументы формы variablename=anythingобрабатываются как присваивание переменных (которые выполняются в то время, когда аргументы обрабатываются в отличие от (более новых) -v var=value, которые выполняются до BEGINоператоров) вместо имен входных файлов.

Это может быть полезно в таких вещах, как:

awk '{print $1}' FS=/ RS='\n' file1 FS='\n' RS= file2

Где вы можете указать другой FS/ RSдля файла. Это также обычно используется в:

awk '!file1_processed{a[$0]; next}; {...}' file1 file1_processed=1 file2

Какая версия безопаснее:

awk 'NR==FNR{a[$0]; next}; {...}' file1 file2

(который не работает, если file1пуст)

Но это мешает, когда у вас есть файлы, имя которых содержит =символы.

Теперь проблема только в том, что от первого =остается правильное awkимя переменной.

То, что составляет допустимое имя переменной в, awkявляется более строгим, чем в sh.

POSIX требует, чтобы это было что-то вроде:

[_a-zA-Z][_a-zA-Z0-9]*

Только с символами переносимого набора символов. Однако /usr/xpg4/bin/awkSolaris 11, по крайней мере, не соответствует этим требованиям и допускает любые алфавитные символы в локали в именах переменных, а не только a-zA-Z.

Таким образом, аргумент, подобный x+y=fooили =barили ./foo=bar, все еще рассматривается как имя входного файла, а не как присваивание, поскольку то, что осталось от первого =, не является допустимым именем переменной. Аргумент типа Stéphane=Chazelas.txtможет или не может, в зависимости от awkреализации и локали.

Вот почему с awk рекомендуется использовать:

awk '...' ./*.txt

вместо того

awk '...' *.txt

например, чтобы избежать проблемы, если вы не можете гарантировать, что имя txtфайла не будет содержать =символов.

Также помните, что аргумент, подобный аргументу, -vfoo=bar.txtможет рассматриваться как опция, если вы используете:

awk -f file.awk -vfoo=bar.txt

(также относится и к awk '{code}' -vfoo=bar.txtс awkот версии BusyBox до 1.28.0, см соответствующего сообщения об ошибке ).

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

Вот почему

#! /usr/bin/awk -f

шебанги на самом деле не работают. В то время как var=valueте , можно обойти путем фиксации на ARGVзначения (добавить ./префикс) в BEGINзаявлении:

#! /usr/bin/awk -f
BEGIN {
  for (i = 1; i < ARGC; i++)
    if (ARGV[i] ~ /^[_[:alpha:]][_[:alnum:]]*=/)
      ARGV[i] = "./" ARGV[i]
}
# rest of awk script

Это не поможет с опциональными, так как они видны самим сценарием, awkа не самим awkсценарием.

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

Реализация GNU awkисправляет все эти проблемы с помощью -Eопции.

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

Он специально разработан для:

#! /usr/bin/gawk -E

shebangs, где список аргументов всегда является входными файлами (обратите внимание, что вы все еще можете редактировать этот ARGVсписок в BEGINвыражении).

Вы также можете использовать его как:

gawk -e '...awk code here...' -E /dev/null *.txt

Мы используем -Eс пустым script ( /dev/null) просто для того, чтобы *.txtвпоследствии убедиться, что эти потом всегда обрабатываются как входные файлы, даже если они содержат =символы.

Стефан Шазелас
источник
Я не вижу, как явный путь, заканчивающийся в FILENAME, является проблемой. Либо сценарий awk является общим, и в этом случае он должен обрабатывать все виды путей, заканчивающихся в FILENAME (включая, но не ограничиваясь ../foo, /path/to/fooи пути, которые находятся в другой кодировке) - в этом случае substr(FILENAME,3)будет недостаточно, или это сценарий с одним выстрелом, где пользователь в основном знает, что такое имена файлов - в этом случае он / она, вероятно, не должен беспокоиться ни о одном из них, содержащем =либо ;-)
mosvy
2
@mosvy Я не думаю, что в нем так много говорится о том, что ./это проблема, но это может быть нежелательно при определенных условиях, таких как случаи, когда имя файла должно быть включено в вывод, и в этом случае ./должно быть избыточным и ненужным, так что вы Нужно как-то от этого избавиться. Вот хотя бы один пример . Что касается пользователя, который знает, что такое имена файлов - хорошо, в этом случае мы также знаем, что такое имя файла, но =все равно мешает правильной обработке. Так может помешать ведущему -.
Сергей Колодяжный
@mosvy, да, идея в том, что вы хотите использовать ./префикс, чтобы обойти эту awk(неправильную) функцию, но затем вы получите то, что ./на выходе, которое вы можете удалить. Посмотрите, как проверить, содержит ли первая строка файла определенную строку? В качестве примера.
Стефан Шазелас
Является не только локальным (относительно этого каталога), ./но и глобальным (абсолютный путь), /который заставляет awk интерпретировать аргумент как файл.
Исаак
21

В большинстве версий awk аргументы после выполняемой программы:

  1. Файл
  2. Назначение формы x=y

Поскольку ваше имя файла интерпретируется как случай №2, awk все еще ожидает что-то, что можно прочитать на stdin (так как он не видит, что какое-либо имя файла было передано).

Это поведение задокументировано в POSIX :

Любой из следующих двух типов аргументов может быть смешан:

  • file: путь к файлу, который содержит входные данные для чтения, которые сопоставляются с набором шаблонов в программе. Если файловые операнды не указаны, или если файловый операнд имеет значение «-», должен использоваться стандартный ввод.
  • назначение: операнд, который начинается с символа подчеркивания или алфавита из переносимого набора символов (см. таблицу в томе Базовых определений стандарта IEEE Std 1003.1-2001, раздел 6.1, переносимый набор символов), за которым следует последовательность подчеркиваний, цифр, и алфавит из переносимого набора символов, за которым следует символ «=», должен определять присвоение переменной, а не имя пути.

Таким образом, переносимо, у вас есть несколько вариантов (# 1, вероятно, наименее навязчивый):

  1. Использование awk ... ./my=file, которое обходит это, поскольку .не является "подчеркиванием или буквенным символом из переносимого набора символов".
  2. Поместите файл на стандартный ввод awk ... < my=file. Тем не менее, это не работает хорошо с несколькими файлами.
  3. Сделайте временную жесткую ссылку на файл и используйте это. Вы можете сделать что-то вроде ln my=file my_file, а затем использовать my_fileкак обычно. Копирование не будет выполняться, и оба файла будут поддерживаться одними и теми же данными и метаданными inode. После его использования можно безопасно удалить созданную ссылку, поскольку количество ссылок на индекс будет по-прежнему больше 0.
Крис Даун
источник
6
Не ./my=file работает? % awk 'processing_script_here' ./my=file.txt awk: fatal: cannot open file ./my=file.txt' for reading (No such file or directory). Это должно быть переносимым, потому что ./myне является допустимым именем переменной, поэтому не должно быть проанализировано таким образом.
Стивен Харрис
2
Как говорится в этом тексте POSIX, проблема заключается только в том, что перед первым =следует знак подчеркивания или алфавитный символ из переносимого набора символов (см. Таблицу в разделе «Базовые определения» стандарта IEEE Std 1003.1-2001, раздел 6.1, Переносимый набор символов), затем следует последовательность символов подчеркивания, цифр и алфавита из переносимого набора символов . Таким образом, путь к файлу, как ++foo=bar.txtили =fooили ./foo=barвсе в порядке, .или +не является [_a-zA-Z].
Стефан Шазелас
1
@SergiyKolodyazhnyy awk является внешним по отношению к оболочке, поэтому не имеет значения, какой вы используете. ./my=fileбудет передано через дословно.
Крис Даун
1
@SergiyKolodyazhnyy, то же самое для awk '{print $1,$2}' /etc/passwd. Дело в том, что наличие оболочки, открывающей файл, в отличие от awk, не имеет никакого значения, делает ли его доступным для поиска или нет. На самом деле, awk '{exit}' < /etc/passwdвы можете ожидать awkвозврата к концу первой записи, exitчтобы убедиться, что он оставит там позицию в stdin. POSIX требует этого. /usr/xpg4/bin/awkделает это на Solaris, но, похоже, gawkни mawkна GNU / Linux.
Стефан Шазелас
3
@mosvy, см. раздел « ВХОДНЫЕ ФАЙЛЫ » на pubs.opengroup.org/onlinepubs/9699919799/utilities/… Это полезно для ряда шаблонов использования, которые имеют смысл только для обычных файлов, например, когда вы хотите обрезать файл или записать в него данные в позиция, идентифицированная awkтаким образом.
Стефан Шазелас
3

Чтобы процитировать документацию gawk (примечание выделено):

Любые дополнительные аргументы в командной строке обычно обрабатываются как входные файлы для обработки в указанном порядке. Однако аргумент, который имеет форму var = value, присваивает значение value переменной var - он вообще не указывает файл.

Почему команда останавливается и ждет? Потому что в форме awk 'processing_script_here' my=file.txt нет файла, указанного в приведенном выше определении - my=file.txtинтерпретируется как присвоение переменной, и, если файл не определен, awkбудет читать stdin (также видно, из straceчего видно, что awk в такой команде ожидает read(0,'...)syscall.

Это также задокументировано в спецификации POSIX awk , см. Раздел ОПЕРАНДЫ и часть назначений )

Назначение переменной очевидно в awk '{print foo}' foo=bar /etc/passwdтом смысле, что значение fooвыводится для каждой строки в / etc / passwd. Однако указание ./foo=barили полный путь работает.

Обратите внимание , что работает straceна awk '1' foo=bar, а также проверки с cat foo=barпоказывает , что это AWK-специфическая проблема, и execve делает шоу имени файла в качестве аргумента передается, поэтому снаряды не имеют ничего общего с переменным окр заданий в этом случае.

Кроме того, обратите внимание, что awk '...script...' foo=barэто не приведет к созданию переменной окружения с помощью оболочки, так как назначение переменных окружения должно предшествовать команде для вступления в силу. См. Правила грамматики оболочки POSIX , пункт № 7. Кроме того, это можно проверить с помощьюawk '{print ENVIRON["foo"]}' foo=bar /etc/passwd

Сергей Колодяжный
источник