Должны ли переменные быть заключены в кавычки при выполнении?

18

Общее правило в сценариях оболочки заключается в том, что переменные всегда должны заключаться в кавычки, если нет веской причины не делать этого. Для получения более подробной информации, чем вы, вероятно, хотели бы узнать, взгляните на эти замечательные вопросы и ответы: последствия для безопасности того, что вы забыли заключить переменную в оболочки bash / POSIX .

Рассмотрим, однако, функцию, подобную следующей:

run_this(){
    $@
}

Должны ли они $@быть указаны там или нет? Я немного поиграл с ним и не смог найти ни одного случая, когда бы нехватка кавычек вызвала проблему. С другой стороны, использование кавычек приводит к разрыву при передаче команды, содержащей пробелы в качестве переменной в кавычках:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

Запуск приведенного выше сценария возвращает:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Я могу обойти это, если использую run_that $commвместо run_that "$comm", но, поскольку run_this(без кавычек) функция работает с обоими, это кажется более безопасной ставкой.

Таким образом, в конкретном случае использования $@в функции, работа которой заключается в выполнении $@в качестве команды, следует $@заключить в кавычки? Пожалуйста, объясните, почему он не должен / не должен быть в кавычках, и приведите пример данных, которые могут его сломать.

Тердон
источник
6
run_thatповедение определенно то, что я ожидал (что если в пути к команде есть пробел?). Если вы хотите другое поведение, конечно , вы бы его конец цитаты на вызова -сайт , где вы знаете , что данные? Я ожидал бы вызвать эту функцию как run_that ls -l, которая работает одинаково в любой версии. Есть ли случай, который заставил вас ожидать по-другому?
Майкл Гомер
@MichaelHomer Полагаю, что мои правки здесь вызвали следующее: unix.stackexchange.com/a/250985/70524
Muru
@MichaelHomer по какой-то причине (возможно, потому, что у меня еще не было моей второй чашки кофе), я не рассматривал пробелы в аргументах или пути команды, а только в самой команде (опции). Как это часто бывает, в ретроспективе это кажется очень очевидным.
Тердон
Существует причина, по которой оболочки по-прежнему поддерживают функции, а не просто вставляют команды в массив и выполняют его с помощью ${mycmd[@]}.
chepner

Ответы:

20

Проблема заключается в том, как команда передается функции:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"следует использовать в общем случае, когда ваша run_thisфункция имеет префикс к обычно написанной команде. run_thisприводит к цитированию ада:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Я не уверен, как я должен передать имя файла с пробелами run_this.

Мур
источник
1
Это было действительно ваше редактирование, которое вызвало это. По какой-то причине мне просто не пришло в голову проверять имя файла с пробелами. Я понятия не имею, почему бы и нет, но вы идете. Вы совершенно правы, конечно, я не вижу способа сделать это правильно с run_thisлюбым из них.
Тердон
Цитирование @terdon стало настолько привычкой, что я предположил, что вы $@случайно оставили без кавычек. Я должен был оставить пример. : D
Муру
2
Нет, это действительно такая большая привычка, что я проверил это (ошибочно) и пришел к выводу, что "да, может, этот не нуждается в кавычках". Процедура, обычно известная как мозговое перо.
Тердон
1
Вы не можете передать имя файла с пробелами в run_this. По сути, это та же проблема, с которой вы сталкиваетесь при вставке сложных команд в строки, как обсуждалось в Bash FAQ 050 .
Этан Рейснер
9

Это либо:

interpret_this_shell_code() {
  eval "$1"
}

Или:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

или:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Но:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Не имеет особого смысла.

Если вы хотите выполнить ls -lкоманду (а не lsкоманду с аргументами lsи в -lкачестве аргументов), вы должны сделать:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Но если (более вероятно), это lsкоманда с аргументами lsи в -lкачестве аргумента, вы должны выполнить:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Теперь, если вы хотите выполнить не просто простую команду, а если вы хотите делать переменные, перенаправления, каналы ..., только interpret_this_shell_codeсделайте:

interpret_this_shell_code 'ls -l 2> /dev/null'

хотя, конечно, вы всегда можете сделать:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Стефан Шазелас
источник
5

Глядя на него из Баша / КШ / ЗШ точки зрения, $*и $@являются частным случаем общего расширения массива. Расширения массива не похожи на обычные расширения переменных:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

С помощью расширений $*/ ${a[*]}вы объединяете массив с первым значением IFS(по умолчанию это пробел) в одну гигантскую строку. Если вы не заключите его в кавычки, он будет разбит как обычная строка.

С расширениями $@/ ${a[@]}поведение зависит от того, цитируется ли расширение $@/ ${a[@]}или нет:

  1. если он указан ( "$@"или "${a[@]}"), вы получите эквивалент "$1" "$2" "$3" #... или"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. если он не указан ( $@или ${a[@]}), вы получите эквивалент $1 $2 $3 #... или${a[1]} ${a[2]} ${a[3]} # ...

Для переноса команд вам определенно нужны заключенные в кавычки @ расширения (1.).


Более подробная информация о массивах bash (и bash-like): https://lukeshu.com/blog/bash-arrays.html

PSkocik
источник
1
Просто понял, что я имею в виду ссылку, начинающуюся с Люка, ношу маску Вейдера. Сила сильна с этим постом.
PSkocik
4

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

Как вы могли запустить команду с именем *? Вы не можете сделать это с run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

И вы видите, даже когда произошла ошибка, run_thatвы получили более значимое сообщение.

Единственный способ расширить $@до отдельных слов это двойные кавычки. Если вы хотите запустить его как команду, вы должны передать команду и параметры в виде отдельных слов. То, что вы сделали на стороне вызывающего абонента, а не в вашей функции.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

это лучший выбор. Или если ваша оболочка поддерживает массивы:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Даже когда оболочка вообще не поддерживает массив, вы все равно можете поиграть с ним, используя"$@" .

cuonglm
источник
3

Выполнение переменных в bashметоде подвержено сбоям. Просто невозможно написать run_thisфункцию, которая правильно обрабатывает все крайние случаи, например:

  • трубопроводы (например ls | grep filename)
  • перенаправление ввода / вывода (например ls > /dev/null)
  • операторы оболочки, как if whileи т.д.

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

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Ты должен написать

command() {
    ls -l
}
...
command

Если команды доступны только во время выполнения, вы должны использовать eval, который специально разработан для обработки всех причуд, которые могут привести к run_thisсбою:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Обратите внимание, что evalэто известно проблемами безопасности, но если вы передадите переменные из ненадежных источников run_this, вы также столкнетесь с выполнением произвольного кода.

Дмитрий Григорьев
источник
1

Выбор ваш. Если вы не цитируете $@какое-либо из его значений, подвергните дополнительному расширению и интерпретации. Если вы заключите это в кавычки, все переданные аргументы будут воспроизведены в ее расширении дословно. Вы никогда не сможете надежно обрабатывать синтаксические токены оболочки, такие как &>|и т. Д., Так или иначе, не разбирая аргументы самостоятельно, так что у вас останется более разумный выбор - передать свою функцию одному из:

  1. Именно слова, используемые при выполнении одной простой команды с "$@".

...или...

  1. Еще одна расширенная и интерпретированная версия ваших аргументов, которые только затем применяются вместе как простая команда $@.

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

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... это не бесполезно , просто редко вероятно, будут иметь много использования . И в bashоболочке, потому bashчто по умолчанию не привязывает определение переменной к своей среде, даже когда указанное определение добавляется перед командной строкой специальной встроенной функции или функции, глобальное значение для $IFSне затрагивается, и его объявление является локальным только на run_this()звонок.

Так же:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... Глобализация также настраивается. Цитаты служат цели - они не зря. Без них расширение оболочки подвергается дополнительной интерпретации - настраиваемой интерпретации. Раньше - с некоторыми очень старыми снарядами - что $IFSбыло во всем мире применяются для всех входных данных, а не только расширение. На самом деле, указанные оболочки вели себя очень похоже run_this()на то, что разбили все входные слова на значение $IFS. Итак, если вам нужно очень старое поведение оболочки, то вам следует использовать run_this().

Я не ищу его, и в данный момент мне трудно найти полезный пример для этого. Я обычно предпочитаю, чтобы команды, которые запускает моя оболочка, были теми, которые я набираю на ней. И так, учитывая выбор, я бы почти всегда run_that(). Кроме этого...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Почти все может быть процитировано. Команды будут выполняться в кавычках. Это работает, потому что к тому моменту, когда команда фактически выполняется, все входные слова уже подверглись удалению кавычек, что является последним этапом процесса интерпретации ввода оболочки. Таким образом, разница между 'ls'и lsможет иметь значение только во время интерпретации оболочки - и поэтому цитирование lsгарантирует, что любой названный псевдонимls не заменит мое lsкомандное слово в кавычках . Кроме этого, единственное, на что влияют кавычки, - это разграничение слов (как и почему работает цитирование переменных / ввода-пробела) и интерпретация метасимволов и зарезервированных слов.

Так:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Вы никогда не будете в состоянии сделать это с любым из run_this()или run_that().

Но имена функций, или $PATHкоманды d, или встроенные команды будут выполняться просто в кавычках или без кавычек, и это именно то, как run_this()и run_that()работает в первую очередь. Вы не сможете сделать ничего полезного с $<>|&(){}любым из них. Если не считать eval, есть.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

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

(run_that(){ "$@";}; echo hey | run_that cat)

hey

Я мог бы так же легко <перенаправить ввод или >вывод туда, как открыл канал.

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

mikeserv
источник