Кавычки в конструкциях типа ssh $ host $ FOO и ssh $ host «sudo su user -c $ FOO»

30

Я часто заканчиваю тем, что выполняю сложные команды через ssh; Эти команды включают в себя конвейерную передачу в awk или perl на одну строку, и в результате содержат одинарные кавычки и $. Я не смог придумать жесткое и быстрое правило для правильного цитирования, и не нашел для этого хороших ссылок. Например, рассмотрим следующее:

# what I'd run locally:
CMD='pgrep -fl java | grep -i datanode | awk '{print $1}'
# this works with ssh $host "$CMD":
CMD='pgrep -fl java | grep -i datanode | awk '"'"'{print $1}'"'"

(Обратите внимание на дополнительные кавычки в выражении awk.)

Но как мне заставить это работать, например ssh $host "sudo su user -c '$CMD'"? Есть ли общий рецепт для управления кавычками в таких сценариях? ..

Лев Алексеев
источник

Ответы:

35

Работа с несколькими уровнями цитирования (на самом деле, несколькими уровнями синтаксического анализа / интерпретации) может стать сложной. Это помогает запомнить несколько вещей:

  • Каждый «уровень цитирования» может включать другой язык.
  • Правила цитирования зависят от языка.
  • При работе с более чем одним или двумя вложенными уровнями, как правило, легче всего работать «снизу вверх» (т. Е. От самого внутреннего до самого внешнего).

Уровни цитирования

Давайте посмотрим на ваши примеры команд.

pgrep -fl java | grep -i datanode | awk '{print $1}'

Ваша первая примерная команда (выше) использует четыре языка: ваша оболочка, регулярное выражение в pgrep , регулярное выражение в grep (которое может отличаться от языка регулярных выражений в pgrep ) и awk . Для каждой из задействованных команд существует два уровня интерпретации: оболочка и один уровень после оболочки. Существует только один явный уровень цитирования (цитирование оболочки в awk ).

ssh host 

Далее вы добавили уровень ssh сверху. Это фактически другой уровень оболочки: ssh не интерпретирует саму команду, он передает ее оболочке на удаленном конце (через (например) sh -c …), и эта оболочка интерпретирует строку.

ssh host "sudo su user -c …"

Затем вы спросили о добавлении еще одного уровня оболочки в середине с помощью su (через sudo , который не интерпретирует аргументы своей команды, поэтому мы можем его игнорировать). На данный момент у вас есть три уровня вложенности ( awk → shell, shell → shell ( ssh ), shell → shell ( su user -c ), поэтому я советую использовать подход «снизу вверх». Я предполагаю, что ваши оболочки совместимы с Bourne (например, sh , ash , dash , ksh , bash , zsh и т. д.). Некоторые другие виды оболочек ( fish , rcи т. д.) может потребоваться другой синтаксис, но метод все еще применяется.

Вверх дном

  1. Сформулируйте строку, которую вы хотите представить на самом внутреннем уровне.
  2. Выберите механизм цитирования из репертуара цитирования следующего по приоритетности языка.
  3. Цитируйте желаемую строку в соответствии с выбранным вами механизмом цитирования.
    • Часто существует много вариантов того, как применять какой механизм цитирования. Делать это вручную - это вопрос практики и опыта. Делая это программно, обычно лучше выбрать самый легкий, чтобы получить право (обычно «самый буквальный» (наименьшее количество побегов)).
  4. При желании можно использовать результирующую строку в кавычках с дополнительным кодом.
  5. Если вы еще не достигли желаемого уровня цитирования / интерпретации, возьмите получившуюся строку в кавычках (плюс любой добавленный код) и используйте ее в качестве начальной строки в шаге 2.

Цитирую семантику варя

Здесь следует иметь в виду, что каждый язык (уровень цитирования) может давать несколько разную семантику (или даже существенно различную семантику) одному и тому же символу цитирования.

Большинство языков имеют «буквальный» механизм цитирования, но они различаются по точности. Одиночная кавычка оболочек типа Борна на самом деле буквальная (что означает, что вы не можете использовать ее для кавычек самого символа одинарной кавычки). Другие языки (Perl, Ruby) менее буквальны в том смысле, что они интерпретируют некоторые последовательности обратной косой черты внутри областей в одинарных кавычках не буквально (в частности, \\и \'приводят к \и ', но другие последовательности обратной косой черты на самом деле буквальны).

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

Ваш пример

Самым внутренним уровнем вашего примера является программа awk .

{print $1}

Вы собираетесь встроить это в командную строку оболочки:

pgrep -fl java | grep -i datanode | awk 

Нам нужно защитить (как минимум) пространство и $в программе awk . Очевидный выбор - использовать одинарные кавычки в оболочке вокруг всей программы.

  • '{print $1}'

Есть и другие варианты:

  • {print\ \$1} прямо покинуть пространство и $
  • {print' $'1} одиночная кавычка только пробел и $
  • "{print \$1}" двойная цитата целого и избежать $
  • {print" $"1}двойная кавычка только пробел, и $
    это может немного согнуть правила (неэкранированный $в конце строки в двойных кавычках является буквальным), но, похоже, это работает в большинстве оболочек.

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

Мы выбираем '{print $1}'и встраиваем его в остальную часть «кода» оболочки:

pgrep -fl java | grep -i datanode | awk '{print $1}'

Далее вы хотели запустить это через su и sudo .

sudo su user -c 

su user -c …точно так же some-shell -c …(за исключением запуска под другим UID), поэтому su просто добавляет еще один уровень оболочки. sudo не интерпретирует свои аргументы, поэтому не добавляет никаких уровней цитирования.

Нам нужен еще один уровень оболочки для нашей командной строки. Мы можем снова выбрать одинарные кавычки, но мы должны уделить особое внимание существующим одинарным кавычкам. Обычный способ выглядит так:

'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Здесь есть четыре строки, которые оболочка будет интерпретировать и объединять: первая строка в одинарных кавычках ( pgrep … awk), экранированная одинарная кавычка, программа awk с одинарными кавычками , другая экранированная одинарная кавычка.

Есть, конечно, много альтернатив:

  • pgrep\ -fl\ java\ \|\ grep\ -i\ datanode\ \|\ awk\ \'{print\ \$1} избежать всего важного
  • pgrep\ -fl\ java\|grep\ -i\ datanode\|awk\ \'{print\$1}то же самое, но без лишних пробелов (даже в программе awk !)
  • "pgrep -fl java | grep -i datanode | awk '{print \$1}'" двойная кавычка, избегай $
  • 'pgrep -fl java | grep -i datanode | awk '"'"'{print \$1}'"'"ваш вариант; немного длиннее обычного способа из-за использования двойных кавычек (два символа) вместо escape-символов (один символ)

Использование разных цитат на первом уровне допускает другие варианты на этом уровне:

  • 'pgrep -fl java | grep -i datanode | awk "{print \$1}"'
  • 'pgrep -fl java | grep -i datanode | awk {print\ \$1}'

Встраивание первого варианта в командную строку sudo / * su * дает следующее:

sudo su user -c 'pgrep -fl java | grep -i datanode | awk '\''{print $1}'\'

Вы можете использовать эту же строку в любых других контекстах уровня оболочки (например ssh host …).

Далее вы добавили уровень ssh сверху. Это фактически другой уровень оболочки: ssh не интерпретирует саму команду, но передает ее оболочке на удаленном конце (через (например) sh -c …), и эта оболочка интерпретирует строку.

ssh host 

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

Используя одинарные кавычки снова:

'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

Теперь есть одиннадцать строк, которые интерпретируются и объединяются:, 'sudo su user -c 'экранированная одинарная кавычка, 'pgrep … awk 'экранированная одинарная кавычка, экранированная обратная косая черта, две экранированные одинарные кавычки, программа awk с одинарными кавычками, экранированная одинарная кавычка, экранированная обратная косая черта и финальная экранированная одинарная кавычка ,

Окончательная форма выглядит так:

ssh host 'sudo su user -c '\''pgrep -fl java | grep -i datanode | awk '\'\\\'\''{print $1}'\'\\\'

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

#!/bin/sh

sq() { # single quote for Bourne shell evaluation
    # Change ' to '\'' and wrap in single quotes.
    # If original starts/ends with a single quote, creates useless
    # (but harmless) '' at beginning/end of result.
    printf '%s\n' "$*" | sed -e "s/'/'\\\\''/g" -e 1s/^/\'/ -e \$s/\$/\'/
}

# Some shells (ksh, bash, zsh) can do something similar with %q, but
# the result may not be compatible with other shells (ksh uses $'...',
# but dash does not recognize it).
#
# sq() { printf %q "$*"; }

ap='{print $1}'
s1="pgrep -fl java | grep -i datanode | awk $(sq "$ap")"
s2="sudo su user -c $(sq "$s1")"

ssh host "$(sq "$s2")"
Крис Джонсен
источник
5
Отличное объяснение!
Жиль "ТАК - перестань быть злым"
7

См . Ответ Криса Джонсена для ясного, подробного объяснения с общим решением. Я собираюсь дать несколько дополнительных советов, которые помогут в некоторых общих обстоятельствах.

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

su -c "grep '$pattern' /root/file"  # assuming there is no ' in $pattern

Если ваша локальная оболочка - ksh93 или zsh, вы можете справиться с одинарными кавычками в переменной, переписав их в '\''. (Хотя bash также имеет ${foo//pattern/replacement}конструкцию, его обработка одинарных кавычек не имеет смысла для меня.)

su -c "grep '${pattern//'/'\''}' /root/file"  # if the outer shell is zsh
su -c "grep '${pattern//\'/\'\\\'\'}' /root/file"  # if the outer shell is ksh93

Еще один совет, чтобы избежать необходимости иметь дело с вложенными кавычками, - это как можно больше передавать строки через переменные окружения. Ssh и sudo имеют тенденцию отбрасывать большинство переменных среды, но они часто настроены на пропуск LC_*, потому что они обычно очень важны для удобства использования (они содержат информацию о локали) и редко считаются чувствительными к безопасности.

LC_CMD='what you would use locally' ssh $host 'sudo su user -c "$LC_CMD"'

Здесь, поскольку он LC_CMDсодержит фрагмент оболочки, он должен быть предоставлен буквально самой внутренней оболочке. Поэтому переменная раскрывается оболочкой непосредственно выше. Внутренняя оболочка "$LC_CMD", кроме одной, видит , а внутренняя оболочка видит команды.

Подобный метод полезен для передачи данных в утилиту обработки текста. Если вы используете интерполяцию оболочки, утилита будет обрабатывать значение переменной как команду, например sed "s/$pattern/$replacement/", не будет работать, если переменные содержат /. Поэтому используйте awk (не sed), а также его -vпараметр или ENVIRONмассив для передачи данных из оболочки (если вы проходите ENVIRON, не забудьте экспортировать переменные).

awk -vpattern="$pattern" replacement="$replacement" '{gsub(pattern,replacement); print}'
Жиль "ТАК - прекрати быть злым"
источник
2

Как очень хорошо описывает Крис Джонсон , у вас есть несколько уровней цитируемой косвенности; вы указываете своему местному shellжителю проинструктировать пульт дистанционного управления shellчерез sshто, что он должен sudoпроинструктировать suего проинструктировать пульт дистанционного управления shellзапустить ваш конвейерpgrep -fl java | grep -i datanode | awk '{print $1}' как user. Такая команда требует много утомительного \'"quote quoting"\'.

Если вы примете мой совет, вы откажетесь от всей этой чепухи и сделаете:

% ssh $host <<REM=LOC_EXPANSION <<'REMOTE_CMD' |
> localhost_data='$(commands run on localhost at runtime)' #quotes don't affect expansion
> more_localhost_data="$(values set at heredoc expansion)" #remote shell will receive m_h_d="result"
> REM=LOC_EXPANSION
> commands typed exactly as if located at 
> the remote terminal including variable 
> "${{more_,}localhost_data}" operations
> 'quotes and' \all possibly even 
> a\wk <<'REMOTELY_INVOKED_HEREDOC' |
> {as is often useful with $awk
> so long as the terminator for}
> REMOTELY_INVOKED_HEREDOC
> differs from that of REM=LOC_EXPANSION and
> REMOTE_CMD
> and here you can | pipeline operate on |\
> any output | received from | ssh as |\
> run above | in your local | terminal |\
> however | tee > ./you_wish.result
<desired output>

ДЛЯ БОЛЬШЕГО:

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

-Майк

Микесерв
источник
Это выглядит интересно, но я не могу заставить его работать. Не могли бы вы опубликовать минимальный рабочий пример?
Джон Лоуренс
Я считаю, что этот пример требует Zsh, так как он использует несколько перенаправлений на стандартный ввод. В других оболочках, похожих на Bourne, вторая <<просто заменяет первую. Должен ли он где-то сказать «только zsh», или я что-то пропустил? (Умный трюк, хотя, чтобы иметь heredoc, который частично зависит от локального расширения)
Вот Баш совместимая версия: unix.stackexchange.com/questions/422489/...
dabest1
0

Как насчет использования более двойных кавычек?

Тогда вы ssh $host $CMDдолжны прекрасно работать с этим:

CMD="pgrep -fl java | grep -i datanode | awk '{print $1}'"

Теперь к более сложному ssh $host "sudo su user -c \"$CMD\"". Я думаю , все , что вам нужно сделать , это избежать чувствительных персонажей CMD: $, \и ". Так что я бы попробовал посмотреть, сработает ли это echo $CMD | sed -e 's/[$\\"]/\\\1/g'.

Если все выглядит хорошо, оберните echo + sed в функцию оболочки, и с вами все в порядке ssh $host "sudo su user -c \"$(escape_my_var $CMD)\"".

Алекс
источник