Це обговорювалося в ряді питань на unix.SE, я спробую зібрати всі питання, з якими я можу зійти тут. Список літератури в кінці.
Чому це не вдається
Причина, з якою ви стикаєтеся з цими проблемами, - це розділення слів і той факт, що цитати, розширені зі змінних, не виступають цитатами, а є лише звичайними символами.
Випадки, представлені у запитанні:
$ abc='ls -l "/tmp/test/my dir"'
Тут $abc
розбивається і ls
отримує два аргументи "/tmp/test/my
і dir"
(з цитатами в передній частині та задній частині другого):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Тут цитується розширення, тому воно зберігається як одне слово. Оболонка намагається знайти програму під назвою ls -l "/tmp/test/my dir"
, пробіли та лапки включені.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
І тут $abc
як аргумент береться лише перше слово або -c
, тому Bash просто працює ls
в поточному каталозі. Решта слова аргументи Баш, і використовуються для заповнення $0
, $1
і т.д.
$ bash -c $abc
'my dir'
З bash -c "$abc"
, і eval "$abc"
, є додатковий крок обробки оболонки, який змушує котирування працювати, але також змушує повторно обробляти всі розширення оболонок , тому існує ризик випадкового запуску розширення команди з наданих користувачем даних, якщо ви не дуже обережно щодо цитування.
Кращі способи це зробити
Два кращих способу збереження команди: а) використання замість неї функції; б) використання змінної масиву (або позиційних параметрів).
Використання функції:
Просто оголосіть функцію з командою всередині і запустіть функцію так, ніби це була команда. Розширення в командах в межах функції обробляються лише тоді, коли команда працює, а не тоді, коли вона визначена, і вам не потрібно цитувати окремі команди.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Використання масиву:
Масиви дозволяють створювати багатослівні змінні, де окремі слова містять пробіл. Тут окремі слова зберігаються як окремі елементи масиву, а "${array[@]}"
розширення розширює кожен елемент як окремі слова оболонки:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
Синтаксис трохи жахливий, але масиви також дозволяють будувати командний рядок по частинах. Наприклад:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
або підтримуйте постійні частини командного рядка та використовуйте масив, заповнюючи лише його частину, параметри або назви файлів:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Недоліком масивів є те, що вони не є стандартною функцією, тому звичайні оболонки POSIX (наприклад dash
, за замовчуванням /bin/sh
у Debian / Ubuntu) не підтримують їх (див. Нижче). Bash, ksh і zsh, однак, так що, швидше за все, у вашій системі є якась оболонка, яка підтримує масиви.
Використання "$@"
У оболонках без підтримки названих масивів все ще можна використовувати позиційні параметри (псевдомасив "$@"
) для утримання аргументів команди.
Далі повинні бути портативні біти скриптів, які виконують еквівалент бітів коду в попередньому розділі. Масив замінюється "$@"
списком позиційних параметрів. Налаштування "$@"
проводиться за допомогою set
, а подвійні цитати навколо "$@"
є важливими (вони спричиняють, що елементи списку мають бути окремо цитовані).
По-перше, просто зберігати команду з аргументами в "$@"
і запускати її:
set -- ls -l "/tmp/test/my dir"
"$@"
Умовно встановлення частин параметрів командного рядка для команди:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Тільки використовуючи "$@"
параметри та операнди:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Звичайно, "$@"
зазвичай заповнюється аргументами самого сценарію, тому вам доведеться зберегти їх десь перед повторним призначенням "$@"
.)
Будьте обережні eval
!
Оскільки eval
вводиться додатковий рівень обробки котирування та розширення, вам потрібно бути обережними з введенням користувача. Наприклад, це працює до тих пір, поки користувач не набере жодної лапки:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Але якщо вони дають вхід '$(uname)'.txt
, ваш сценарій із задоволенням виконує заміну команд.
Версія з масивами не захищена, оскільки слова зберігаються окремо весь час, немає жодної цитати та іншої обробки вмісту filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Список літератури