company_banner

В чем именно был смысл [ “x$var” = “xval” ]?

Автор оригинала: Vidar
  • Перевод


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

При написании консольных скриптов мы иногда сталкиваемся со сравнениями, в которых каждое значение имеет префикс “x”. Вот примеры с GitHub:

if [ "x${JAVA}" = "x" ]; then
if [ "x${server_ip}" = "xlocalhost" ]; then
if test x$1 = 'x--help' ; then

Назову этот прием x-hack.

Для любой POSIX-совместимой оболочки значение x-hack будет равно нулю, то есть сравнение в 100% случаев сработает и без x. В чем же тогда его суть?

Ресурсы вроде StackOverflow Q&A размыто поясняют, что это альтернатива цитированию вне контекста, указывающему на проблемы с «некоторыми версиями» конкретных оболочек или в целом предостерегающему о загадочном поведении, в особенности древних UNIX-систем. Примерами же эти пояснения не подкрепляются.

Чтобы определить, должна ли ShellCheck об этом предупреждать, и если да, то на каком логическом обосновании, я решил обратиться к истории Unix, а именно к архивам Unix Heritage Society. К сожалению, мне не удалось заглянуть в тщательно охраняемый мир подобий HP-UX и AIX, так что пастухам динозавров рекомендую сохранять бдительность.

Вот найденные мной кейсы, которые могут провалиться.

Левая сторона представлена унарным оператором


Оболочка AT&T Unix v6 от 1973 года, по крайней мере согласно данным из PWB/UNIX от 1977 года, проваливала выполнение тестовых команд, где левая сторона была представлена унарным оператором. Это мог заметить любой, кто пытался выполнить проверку параметров командной строки:

% arg="-f"
% test "$arg" = "-f"
syntax error: -f
% test "x$arg" = "x-f"
(true)

Ошибка была исправлена в оболочке Борна ОС Unix v7, выпущенной в 1979 году. Тем не менее test и [ были также доступны как отдельные исполняемые файлы, и сохранили вариант ошибочного поведения:

$ arg="-f"
$ [ "$arg" = "-f" ]
(false)
$ [ "x$arg" = "x-f" ]
(true)

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

«Современное» поведение оболочки Борна в 1988 было скопировано общедоступной KornShell и стало частью POSIX.2 в 1992 году. В GNU Bash 1.14 то же самое было сделано для встроенной инструкции [, при этом пакет GNU shellutils, предоставлявший внешние исполняемые файлы test/[, последовал уже за POSIX. В результате ранние дистрибутивы GNU/Linux вроде SLS этим багом затронуты не были также, как и FreeBSD 1.0.

X-hack в данном случае эффективен по той причине, что ни один унарный оператор не может начинаться с x.

Одна из сторон представлена оператором длины строки -l


Похожая проблема, просуществовавшая дольше предыдущей, была связана с оператором длины строки -l. В отличие от стандартных унарных предикатов этот считывался только как часть операнда для бинарных предикатов:

var="helloworld"
[ -l "$var" -gt 8 ] && echo "String is longer than 8 chars"

Согласно приведенному выше обоснованию, он не перешел в POSIX, так как: «не был задокументирован в большинстве реализаций, был удален из некоторых реализаций (включая System V), и эта функциональность предоставляется оболочкой». В пример приводится [ ${#var} -gt 8 ].

Это не было проблемой в UNIX v7, где приоритет отдавался =, но в Bash 1.14 от 1996 года данный оператор считывался наперед:

$ var="-l"
$ [ "$var" = "-l" ]
test: -l: binary operator expected
$ [ "x$var" = "x-l" ]
(true)

Та же проблема касалась и правой стороны, но только во вложенных выражениях. Проверка на -l гарантировала наличие второго аргумента, следовательно, требовалось дополнительное выражение или скобки для его активации:

$ [ "$1" = "-l" -o 1 -eq 1 ]
[: too many arguments
$ [ "x$1" = "x-l" -o 1 -eq 1 ]
(true)

Позже в том же году этот оператор был удален из Bash 2.0, и проблема ушла вместе с ним.

Левая сторона представлена "!"


Еще одно затруднение в ранних оболочках возникало, когда левая сторона сравнения была представлена оператором отрицания !:

$ var="!"
$ [ "$var" = "!" ]
test: argument expected            (UNIX v7, 1979)
test: =: unary operator expected   (bash 1.14, 1996)
(false)                            (pd-ksh88, 1988)
$ [ "x$var" = "x!" ]
(true)

Опять же, x-hack решал проблему, не позволяя распознать ! как оператор отрицания.

Ksh рассматривала его как [ ! "=" ]и игнорировала остальные аргументы. В итоге просто возвращался false, так как = не является нулевой строкой. При этом в ksh завершающие аргументы игнорируются и по сей день:

$ [ -e / random words/ops here ]
(true)                              (ksh93, 2021)
bash: [: too many arguments         (bash5, 2021)

В Bash 2.0 и ksh93 эта проблема в соответствии с POSIX была решена за счет предоставления оператору = приоритета в случае с тремя аргументами.

Левая сторона представлена "("


Это, безусловно, моя любимая.

Встроенная в UNIX v7 оболочка давала сбой, когда левая сторона была представлена левой скобкой:

$ left="(" right="("
$ [ "$left" = "$right" ]
test: argument expected
$ [ "x$left" = "x$right" ]
(true)

Это происходило из-за того, что ( получал приоритет над = и становился недопустимой группой скобок.

Но почему моя любимая? Вот Dash 0.5.4 вплоть до 2009:

$ left="(" right="("
$ [ "$left" = "$right" ]
[: 1: closing paren expected
$ [ "x$left" = "x$right" ]
(true)

На момент публикации темы в StakOverflow Q&A данный баг продолжал существовать.

Но это еще не все!

Вот Zsh конца 2015 года перед самым выходом версии 5.3:

% left="(" right=")"
% [ "$left" = "$right" ]
(true)
% [ "x$left" = "x$right" ]
(false)

Удивительно, что x-hack продолжали использовать для обхода ряда багов аж до 2015 года, семь лет после того, как на StackOverflow этот прием списали как архаичный пережиток прошлого.

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

В запоздавших можно также записать Solaris, чья /bin/sh оставалась устаревшей оболочкой Борна даже в Solaris 10 2009 года. Однако причиной такой задержки определенно стала совместимость, а не оценка разработчиками этой оболочки как оптимальной. «Совместимая со стандартами» оболочка оставалась опцией достаточно долго, пока Solaris 11 не перетащил ее в 21 век – или как минимум в 90-е – переключившись на ksh93 по умолчанию в 2011 году.

X-hack выручает во всех подобных случаях, не давая распознать операнды как скобки.

Заключение


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

Тем не менее его ценность уже к концу 90-х практически полностью сошла на нет, и лишь несколько остававшихся проблем были подчищены только к 2010 году – поразительно поздно, но все же больше десяти лет назад.

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

Думаю, что настало время отказаться от этой идиомы, и даже ShellCheck теперь по умолчанию предлагает соответствующие рекомендации по стилю.

Эпилог


Проблема с [ "(" = ")" ] в Dash была впервые отмечена в 2008 году и проявлялась как в Bash 3.2.48, так и в Dash 0.5.4. В bash на macOS ее можно встретить до сих пор:

$ str="-e"
$ [ \( ! "$str" \) ]
[: 1: closing paren expected     # dash
bash: [: `)' expected, found ]   # bash

POSIX исправляет все эти неоднозначности в командах, содержащих вплоть до 4-х параметров, гарантируя, что условные конструкции оболочки будут работать одинаково везде и всегда.

Мейнтейнер Dash, Герберт Сюй, по этому поводу оставил в исправлении такой комментарий:

/*

 * Регламент POSIX: написавший это заслуживает Нобелевской премии мира*

 */


RUVDS.com
VDS/VPS-хостинг. Скидка 10% по коду HABR10

Комментарии 12

    +4
    Я всегда воспринимал этот х как (cast String), чтобы неизвесто что гарантировано было строкой.
      +3
      А я всегда (ещё со времён .bat файлов DOS, с далёких 80-х годов) воспринимал это именно как экранирующий символ для всяких спецсимволов, с которых могла начинаться переменная и всегда так экранировал переменные в своих .bat файлах. И мне странно что такое очевидное на мой взгляд предназначение 'x' в начале переменной может быть таким спорным и неоднозначным.
        +2
        Отчасти так и есть, в примере с длиной строки показывается, как оболочка ухитряется интерпретировать "-l" в переменной как опцию [.
        0

        Жду статью про [ -z ${var+x} ]

          +10
          Подозреваю, что частично это растет еще вот отсюда

          bytamine@bytamine-pc:~$ [ $var == test ] && echo «yes» || echo «no»
          bash: [: ==: unary operator expected
          no
          bytamine@bytamine-pc:~$ [ x$var == xtest ] && echo «yes» || echo «no»
          no
          bytamine@bytamine-pc:~$ var=test
          bytamine@bytamine-pc:~$ [ x$var == xtest ] && echo «yes» || echo «no»
          yes
            0
            Да, но конкретно эта проблема всегда решалась кавыками. Это же решение прекрасно работает и на современном баше, и на условном ksh из 95-ого (честно, специально проверял как-то) :)

            → [ $var == test ] && echo "yes" || echo "no"
            no
            → [ "$var" == test ] && echo "yes" || echo "no"
            no
            → var=test
            → [ "$var" == test ] && echo "yes" || echo "no"
            yes
              0
              У вас равно больше чем нужно :)
              0
              > оболочке Борна
              Давайте все-таки Bourne shell писать, у нас же не Идентификация Борна тут
                +5
                «Идентификация Борна» это первая часть относительно современной (с 2000-х годов) пенталогии. А «Оболочка Борна» это приквел, вышедший ещё в 1979 году!
                  0
                  А что делать с «Борн против Оболочки»?
                +2

                Это они с юникодными строками просто не работали, когда из одного приложения прилетает пустая строка с BOM, и ее надо сравнить с другой пустой строкой без BOM. При конкатенации BOM любезно вырезается башем (главное, чтобы он был собран с поддержкой unicode). И тогда "x${foo}"="x".

                  +1
                  Автор перебрал все ошибки различных шелов и сделал абсолютно неверный вывод что техника устарела и не должна применяться. И забыл про то, что ошибки бывают и у скриптописателей. Это техника защищает от случаев когда в бинарном операторе используется переменная, которая не была определена. Особенно когда переменная сорсится из «конфигурационного файла» и может быть просто по ошибке удалена.

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое