Чи слід цитувати змінні при виконанні?


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
вчора

@MichaelHomer чомусь (можливо, тому, що я ще не мав своєї другої чашки кави) я не розглядав пробіли в аргументах команди або шляху, а лише в самій команді (параметри). Як це часто буває, це здається дуже очевидним в ретроспективі.
тердон

Є причина, чому оболонки все ще підтримують функції замість того, щоб просто заповнювати команди в масив і виконувати їх ${mycmd[@]}.
чепнер

Відповіді:


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]} # ...

Для загортання команд ви, безумовно, хочете, щоб цитували @ expansions (1.).


Більше корисної інформації про bash (і bash-подібні) масиви: https://lukeshu.com/blog/bash-arrays.html


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

Навіть коли оболонка зовсім не підтримує масив, ви все одно можете грати з нею за допомогою"$@" .


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

Я міг би так само легко < перенаправляти вхід або >вихід там, як і відкривав трубу.

У будь-якому випадку, в усьому шляху, тут немає правильного чи неправильного способу - кожен спосіб має своє використання. Просто ви повинні написати це так, як маєте намір використовувати, і ви повинні знати, що ви маєте намір робити. Упущення цитат може мати мету - інакше цитати взагалі не буде, але якщо ви їх опустите з причин, не важливих для вашої мети, ви просто пишете неправильний код. Робіть те, що маєте на увазі; Я намагаюся все одно.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.