В AWK почему несуществующее поле типа $ (NF + 1) не равно нулю?

При использовании AWK я пытаюсь понять, почему несуществующее поле (поле после $NF) не сравнивается с числовым нулем.

В приведенном ниже примере строка ввода имеет два поля, поэтому согласно спецификации $3 должно быть "неинициализированное значение" и сравнивать равным 0. Другими словами, $3 == 0 должно возвращать true, но, как вы можете видеть ниже, возвращает false:

$ echo '1 2' | awk '{ print($3 == 0 ? "t" : "f") }'
f

Оба "One True AWK" (версия 20121220) и GNU AWK (версия 4.2.1) ведут себя одинаково. Здесь вывод GNU AWK:

$ echo '1 2' | gawk '{ print($3 == 0 ? "t" : "f") }'
f

Согласно спецификации POSIX AWK, несуществующие поля, такие как $3 должны быть неинициализированными значениями:

Ссылки на несуществующие поля (то есть поля после $ NF) должны оцениваться неинициализированным значением.

Кроме того, сравнения типа == должны выполняться численно, если один операнд является числовым, а другой - неинициализированным значением:

Сравнения (с операторами "<", "<=", "! =", "==", "> и"> = "должны выполняться численно, если оба операнда являются числовыми, если они являются числовыми и другой имеет строковое значение, которое является числовой строкой, или если оно числовое, а другое имеет неинициализированное значение. В противном случае операнды должны быть преобразованы в строки по мере необходимости...

И, наконец, неинициализированное значение "числовое значение" должно быть равно нулю:

Неинициализированное значение должно иметь как числовое значение нуля, так и строковое значение пустой строки.

Сравните это с неинициализированной переменной, которая сравнивается с равным нулю:

$ awk 'BEGIN { print(x == 0 ? "t" : "f") }'
t

Итак, в нашем первом примере $3 должно быть неинициализированным значением, == должно сравнить его численно, а его числовое значение должно быть равно нулю. Следовательно, мне кажется, что $3 == 0? "t": "f" $3 == 0? "t": "f" должен выводить t вместо f.

Может ли кто-нибудь помочь мне понять, почему это не так, или помочь мне понять, как я неправильно читаю спецификацию?

Ответ 1

Есть интересный отрывок на языке программирования AWK Альфреда В. Ахо, Брайана У. Кернигана и Питера Дж. Вайнбергера (1988) (книга здесь):

Неинициализированные переменные создаются с числовым значением 0 и строковым значением "". Необязательные поля и поля, которые явно имеют нуль, имеют только строковое значение "" ; они не являются числовыми, но при принуждении к номерам они получают числовое значение 0.

источник: язык программирования AWK, раздел 2.2, стр. 45

Более того:

Неинициализированные переменные имеют числовое значение 0 и строковое значение "". Соответственно, если x неинициализируется,

if (x) ...

является ложным и

if (!x) ...
if (x == 0) ...
if (x == "") ...

все верно. Но учтите, что

if (x == "0") ...

false.

Тип поля определяется по возможности, когда это возможно; например, $1++ подразумевает, что при необходимости необходимо принудительно $1++ $1, а $1 = $1 "," $2 означает, что при необходимости будут принудительно привязаны к строкам $1 и $2.

В контексте, где типы не могут быть надежно определены, например,

if {$1 == $2) ...

тип каждого поля определяется на входе. Все поля являются строками; кроме того, каждое поле, содержащее только число, также считается числовым. Поля, которые явно имеют нулевое значение, имеют строковое значение ""; они не являются числовыми. Аналогичным образом обрабатываются несуществующие поля (т.е. Поля за пределами NF) и $0 для пустых строк.

Как и для полей, так это для элементов массива, созданных split.

источник: язык программирования AWK, приложение A, инициализация, сравнение и принуждение типа, стр. 192

На мой взгляд, эти строки хорошо объясняют наблюдаемое поведение, и, похоже, большинство программ следуют этому.


Кроме того, в добавление к сообщению rici:

При исследовании исходного кода GNU Awk 4.2.1 я обнаружил, что:

  • Неинициализированные переменные присваиваются Node именем Nnull_string который имеет флаги:

    main.c: Nnull_string->flags = (MALLOC|STRCUR|STRING|NUMCUR|NUMBER);
    
  • Необязательным полям присваивается Node с именем Null_field который является переопределенным Nnull_string как:

    field.c: *Null_field = *Nnull_string;
    field.c: Null_field->valref = 1;
    field.c: Null_field->flags = (STRCUR|STRING|NULL_FIELD); /* do not set MALLOC */
    

Если поля имеют значения (от awk.h):

#       define  STRING  0x0002       /* assigned as string */
#       define  STRCUR  0x0004       /* string value is current */
#       define  NUMCUR  0x0008       /* numeric value is current */
#       define  NUMBER  0x0010       /* assigned as number */
#       define  NULL_FIELD 0x2000    /* this is the null field */

Функция сравнения int cmp_nodes(NODE *t1, NODE *t2, bool use_strcmp) определенная в eval.c, просто проверяет, установлен ли флаг NUMBER как в t1 и в t2:

if ((t1->flags & NUMBER) != 0 && (t2->flags & NUMBER) != 0)
    return cmp_numbers(t1, t2);

Поскольку Null_field не имеет числового поля, он просто предположит, что он представляет строку. Все это похоже на то, что цитирует книга!

Кроме того, из awk.h:

* STRING and NUMBER are mutually exclusive, except for the special
* case of an uninitialized value, represented internally by
* Nnull_string. They represent the type of a value as assigned.
* Nnull_string has both STRING and NUMBER attributes, but all other
* scalar values should have precisely one of these bits set.
*
* STRCUR and NUMCUR are not mutually exclusive. They represent that
* the particular type of value is up to date.  For example,
*
*   a = 5       # NUMBER | NUMCUR
*   b = a ""    # Adds STRCUR to a, since a string value
*               # is now available. But the type hasn't changed!
*
*   a = "42"    # STRING | STRCUR
*   b = a + 0   # Adds NUMCUR to a, since numeric value
*               # is now available. But the type hasn't changed!

Ответ 2

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

$ seq 1 4 | nawk '{
>     a = "+2"
>     b = 2
>     if (NR % 2)
>         c = a + b
>     if (a == b)
>         print "numeric comparison"
>     else
>         print "string comparison"
> }
> '
numeric comparison
string comparison
numeric comparison
string comparison

Точная обработка пустых и неопределенных значений полей является одной из отличий между спецификацией Posix и языком awk, определяемой языком программирования Awk. Поэтому, в конце концов, вам придется решить, какую спецификацию вы считаете окончательной.

Как вы заметили, Posix четко говорит, что: (переменные и специальные значения)

Ссылки на несуществующие поля (то есть поля после $ NF) должны оцениваться неинициализированным значением....

На самом деле это не просто недействительные поля, которые получают это лечение. Хотя пустые строки не являются "числовыми строками", как определено Posix [Примечание 1], исключение делается для пустых полей (которые возможны, если вы явно устанавливаете разделитель полей):

Каждая переменная поля должна иметь строковое значение или неинициализированное значение при создании. Переменные поля должны иметь неинициализированное значение при создании из $0 с использованием FS, и переменная не содержит никаких символов.

Операторы сравнения являются числовыми, если один аргумент является числом, а другой - числом, "числовой строкой" или неинициализированным значением: (Выражения в awk, добавленный курсором):

Сравнения (с операторами '<', "<=", "!=", "==", '>' и ">=" должны выполняться численно, если оба операнда являются числовыми, если они являются числовыми и другой имеет строковое значение, которое является числовой строкой, или если оно числовое, а другое имеет неинициализированное значение. В противном случае операнды должны быть преобразованы в строки по мере необходимости, и должно быть выполнено сравнение строк...

Однако это не реализация Gnu awk, и, по-видимому, это не реализация многих других awks. Общие реализации:

  • Обрабатывать пустые и недопустимые поля как пустую строку (которая не является числовой строкой), а не унифицированное значение; а также

  • Сравните две "числовые строки", используя числовое сравнение, а не сравнение строк.

Я не могу найти архив списка рассылки awk, который достаточно далеко уходит вовремя, а исходная история в Savannah восходит только к 2006 году или около того, но Changelog включает следующую запись с 1997 года:

Sun Jan 19 23:37:03 1997 Арнольд Д. Роббинс

* field.c (get_field): Add new var that is like Nnull_string but
  does not have numeric attributes, so that new fields are strings.

И код все еще отражает это решение. (Nnull_string - неинициализированное значение gawk. Указанная переменная теперь является глобальным Null_field.)

Интересно, что в правиле BEGIN gawk (правильно) рассматривает $0 как неинициализированный, а не пустой:

$ gawk 'BEGIN{print $0 == 0, $1 == 0}'
1 0

Заметки

  1. "Числовая строка" представляет собой строку, исходящую от пользовательского ввода, форма которого представляет собой номер. Это не включает цитированные литералы в awk-программе; "1" - это строка, а не числовая строка. Возможные происхождение числовой строки перечислены в разделе "Выражения в awk", упомянутом выше; они включают поля, переменные среды и параметры командной строки, а атрибут сохраняется при назначении.

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

    • Используйте эквивалент strtod с дополнительным ограничением на то, что число обработанных чисел должно состоять по крайней мере из одного символа и что все конечные символы являются пробелами;

    • Используйте лексическое определение NUMBER из грамматики awk.

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

Ответ 3

Стандарт POSIX кажется более запутанным, чем необходимо при обсуждении этого вопроса, но посмотрите на это утверждение в таблице в разделе "Выражения в awk" стандарта POSIX:

Syntax |      Name       | Type of Result | Associativity
$expr  | Field reference |    String      |     N/A

поэтому тип $<whatever> по умолчанию - String. Теперь давайте посмотрим, что говорится в этом разделе о том, как он может стать Numeric-String:

A string value shall be considered a numeric string if it comes from one of the following:

    Field variables

    <other N/A stuff - Ed.>

and an implementation-dependent condition corresponding to either case (a) or (b) below is met.

    a) After the equivalent of the following calls to functions defined by the ISO C standard, string_value_end would differ from string_value, and any characters before the terminating null character in string_value_end would be <blank> characters:

    char *string_value_end;
    setlocale(LC_NUMERIC, "");
    numeric_value = strtod (string_value, &string_value_end);

При передаче строки NULL strtod() возвращает 0, но string_value_end не будет отличаться от string_value, поэтому вышеупомянутый тест не будет распознавать NULL в виде числовой строки.

    b) After all the following conversions have been applied, the resulting string would lexically be recognized as a NUMBER token as described by the lexical conventions in Grammar :

        All leading and trailing <blank> characters are discarded.

        If the first non- <blank> is '+' or '-', it is discarded.

        Each occurrence of the decimal point character from the current locale is changed to a <period>.

NULL НЕ будет распознаваться как токен NUMBER для вышеупомянутого анализа.

Итак, в соответствии с вышеизложенным, поле ввода считается только числовым-строковым, если входное значение "похоже" на число, которое, конечно, NULL не является таким образом, что неопытный $<whatever> является просто строкой со значением NULL и любым сравнением с использованием String - сравнение String (см. таблицу в https://www.gnu.org/software/gawk/manual/gawk.html#Variable-Typing для IMHO - самое четкое изображение типов сравнения), поэтому оно никогда не будет быть равно любому числу, включая 0, так как $X == 0 фактически рассматривается как $X == "0" который совпадает с "" == "0" когда $X равно NULL.