Как устранить ошибку "bash:! D ': событие не найдено" в подстановке команд Bash

Я пытаюсь проанализировать вывод события запуска VNC-сервера и столкнулся с проблемой синтаксического анализа с использованием sed в подстановке команд. В частности, удаленный VNC-сервер запускается следующим образом:

address1="user1@lxplus.cern.ch"
VNCServerResponse="$(ssh "${address1}" 'vncserver' 2>&1)"

Затем выводится стандартная ошибка, полученная в этом событии запуска, для извлечения сервера и отображения информации. На данный момент содержимое переменной VNCServerResponse это что-то вроде следующего:

New 'lxplus0186.cern.ch:1 (user1)' desktop is lxplus0186.cern.ch:1

Starting applications specified in /afs/cern.ch/user/u/user1/.vnc/xstartup
Log file is /afs/cern.ch/user/u/user1/.vnc/lxplus0186.cern.ch:1.log

Эти выходные данные могут быть проанализированы следующим образом для извлечения сервера и отображения информации:

echo "${VNCServerResponse}" | sed '/New.*desktop.*is/!d' \
    | awk -F" desktop is " '{print $2}'

В результате получается что-то вроде следующего:

lxplus0186.cern.ch:1

То, что я хочу сделать, это использовать этот анализ в подстановке команд что-то вроде следующего:

VNCServerAndDisplayNumber="$(echo "${VNCServerResponse}" \
    | sed '/New.*desktop.*is/!d' | awk -F" desktop is " '{print $2}')"

При попытке сделать это, я получаю следующую ошибку:

bash: !d': event not found

Я не уверен, как решить эту проблему. Кажется, проблема в том, как sed используется в подстановке команд. Буду признателен за руководство.

5 ответов

Решение

Проблема в том, что в двойных кавычках Bash пытается расширить !d перед тем, как передать его в подоболочку. Вы можете обойти эту проблему, удалив двойные кавычки, но я бы также предложил упрощение вашего сценария:

VNCServerAndDisplayNumber=$(echo "$VNCServerResponse" | awk '/desktop/ {print $NF}')

Это просто печатает последнее поле в строке, содержащей слово "рабочий стол".

В новой версии bash вы можете использовать herestring вместо echo:

VNCServerAndDisplayNumber=$(awk '/desktop/ {print $NF}' <<<"$VNCServerResponse")

Расширение истории Bash - очень странный угол в анализаторе командной строки bash, и вы явно сталкиваетесь с неожиданным расширением истории, которое объясняется ниже. Однако любое расширение истории в сценарии является неожиданным, потому что обычно расширение истории не разрешено в сценариях; даже сценарии не работают с source (или же .) встроенный.

Как расширение истории включено (или отключено)

Существует две опции оболочки, которые управляют расширением истории:

  • set -o historyТребуется для записи истории.

  • set -H (или же set -o histexpand): Дополнительно требуется для включения расширения истории.

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

Согласно руководству по bash, эти параметры не установлены для неинтерактивных оболочек, поэтому, если вы хотите включить раскрытие истории в сценарии (и я не могу представить причину, по которой вы хотели бы этого), вам необходимо установить оба из них:

set -o history -o histexpand

Ситуация для скриптов запускается с source более сложный (и то, что я собираюсь сказать, относится только к bash v4, и, поскольку он недокументирован, может измениться в будущем). [Заметка 3]

Запись истории (и, следовательно, расширение) отключена в sourceСценарии, но через внутренний флаг, который, насколько я знаю, не делается видимым. Это, конечно, не появляется в $SHELLOPTS, Так как sourceСкрипт d запускается в текущем контексте bash, он разделяет текущую среду выполнения, включая параметры оболочки. Так что в исполнении sourced сценарий инициирован из интерактивного сеанса, вы увидите оба history а также histexpand в $SHELLOPTS, но расширение истории не произойдет. Чтобы включить его, вам необходимо:

set -o history

который не запрещен, потому что имеет побочный эффект сброса внутреннего флага, который подавляет запись истории. Настройка histexpand Опция оболочки не имеет этого побочного эффекта.

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

Как анализируется расширение истории

Реализация расширения bash предназначена для работы с readline, так что это может быть выполнено во время ввода команды. (По умолчанию эта функция привязана к Meta- ^; обычно Meta - это ESC, но вы также можете настроить это.) Однако, она также выполняется сразу после ввода каждой строки, прежде чем будет выполнен любой анализ bash.

По умолчанию символ расширения истории - это ! и - как в основном задокументировано - это вызовет расширение истории за исключением:

  1. когда за ним следует пробел или =

  2. если опция оболочки extglob установлен, и за ним следует ( [Примечание 1]

  3. если он появляется в строке в одинарных кавычках

  4. если ему предшествует \ [Примечание 2 и см. ниже]

  5. если ему предшествует $ или ${ [Примечание 1]

  6. если ему предшествует [ [Примечание 1]

  7. (Начиная с bash v4.3), если это последний символ в строке в двойных кавычках.

Непосредственная проблема здесь - точная интерпретация третьего случая ! появляется внутри строки в одинарных кавычках. Обычно, bash запускает новый контекст цитирования для подстановки команды ($(...) или устаревшая обратная нотация). Например:

$ s=SUBSTITUTED
$ # The interior single quotes are just characters
$ echo "'Echoing $s'"
'Echoing SUBSTITUTED'
$ # The interior single quotes are single quotes
$ echo "$(echo 'Echoing $s')"
Echoing $s

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

# A no-op to indicated history expansion
$ HIST() { :; }
# Single-quoted strings inhibit history expansion
$ HIST
$ echo '!!'
!!
# Double-quoted strings allow history expansion
$ HIST
$ echo "'!!'"
echo "'HIST'"
'HIST'
# ... and it applies also to interior command substitution.
$ HIST
$ echo "$(echo '!!')"
echo "$(echo 'HIST')"
HIST

Так что если у вас есть совершенно нормальная команда, как sed '/foo/!d' fileгде вы ожидаете, что одинарные кавычки защитят вас от раскрытия истории, и вы поместите его в подстановку команд в двойных кавычках:

result="$(sed '/foo/!d' file)"

Вы вдруг обнаружите, что ! это персонаж расширения истории. Хуже того, вы не можете исправить это с помощью обратной косой черты после восклицательного знака, потому что хотя "\!" запрещает расширение истории, не удаляет обратную косую черту:

$ echo "\!"
\!

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

# Undesired history expansion
printf "The answer is '%s'\n" "$(sed '/foo/!d' file)"

# Undesired word splitting
printf "The answer is '%s'\n" $(sed '/foo/!d' file)

В этом случае лучшее решение, вероятно, состоит в том, чтобы поместить аргумент sed в переменную

# Works
sed_prog='/foo/!d'
printf "The answer is '%s'\n" "$(sed "$sed_prog" file)"

(Кавычки вокруг $sed_prog в этом случае не были необходимы, но обычно они были бы, и они не причиняют вреда.)


Заметки:

  1. Запрещение раскрытия истории, когда следующий символ представляет собой некоторую форму открытых скобок, работает только в том случае, если в остальной части строки есть соответствующие закрывающие скобки. Тем не менее, он не обязательно должен соответствовать открытым скобкам. Например:

    # No matching close parenthesis
    $ echo "!("
    bash: !: event not found
    # The matching close parenthesis has nothing to do with the open
    $ echo "!(" ")"
    !( )
    # An actual extended glob: files whose names don't start with a
    $ echo "!(a*)"
    b
    
  2. Как указано в bash Вручную, символ расширения истории рассматривается как обычный символ, если непосредственно перед ним стоит обратная косая черта. Это буквально правда; не имеет значения, будет ли обратный слеш позже рассматриваться как символ экранирования или нет:

    $ echo \!
    !
    $ echo \\!
    \!
    $ echo \\\!
    \!
    

    \ также запрещает расширение истории внутри двойных кавычек, но \! не является допустимой escape-последовательностью внутри строки в двойных кавычках, поэтому обратный слеш не удаляется:

    $ echo "\!"
    \!
    $ echo "\\!"
    \!
    $ echo "\\\!"
    \\!
    
  3. Я имею в виду исходный код для bash v4.2, когда я пишу это, поэтому любое недокументированное поведение может полностью отличаться от v4.3.

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

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

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

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

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

  1. Если вы выполняете свой скрипт с . foo или же source fooиспользовать ./foo вместо.

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

  3. Если ваш скрипт (или BASH_ENV) явно делает set -Hнет

Цитировать это с '' или же \ или отключить расширение истории с помощью set +H или же shopt -u -o histexpand, Смотрите расширение истории.

Другие вопросы по тегам