Як ми можемо запустити команду, що зберігається у змінній?


35
$ ls -l /tmp/test/my\ dir/
total 0

Мені було цікаво, чому наступні способи виконання вищевказаної команди не вдаються чи не вдаються?

$ abc='ls -l "/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

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

$ bash -c $abc
'my dir'

$ bash -c "$abc"
total 0

$ eval $abc
total 0

$ eval "$abc"
total 0


Відповіді:


54

Це обговорювалося в ряді питань на 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[@]}"

Список літератури


2
ви можете обійти цитую річ цитуванням, зробивши це cmd="ls -l $(printf "%q" "$filename")". не дуже, але якщо користувач мертвий налаштований на використання eval, це допомагає. Це також дуже корисно для відправки команди , хоча подібні речі, наприклад ssh foohost "ls -l $(printf "%q" "$filename")", або в Sprit цього питання: ssh foohost "$cmd".
Патрік

Не пов’язані безпосередньо, але ви жорстко зашифрували каталог? У такому випадку ви можете подивитися псевдонім. Щось на кшталт: $ alias abc='ls -l "/tmp/test/my dir"'
Скачущий Зайчик

4

Найбезпечніший спосіб виконання (нетривіальної) команди - це eval. Тоді ви можете написати команду так, як ви це робите в командному рядку, і вона виконується точно так, як ніби ви її щойно ввели. Але ви повинні все цитувати.

Простий випадок:

abc='ls -l "/tmp/test/my dir"'
eval "$abc"

не такий простий випадок:

# command: awk '! a[$0]++ { print "foo: " $0; }' inputfile
abc='awk '\''! a[$0]++ { print "foo: " $0; }'\'' inputfile'
eval "$abc"

3

Другий знак лапки порушує команду.

Коли я бігаю:

abc="ls -l '/home/wattana/Desktop'"
$abc

Це дало мені помилку.

Але коли я біжу

abc="ls -l /home/wattana/Desktop"
$abc

Помилки взагалі немає

Немає можливості це виправити вчасно (для мене), але ви можете уникнути помилки, не маючи місця в імені каталогу.

У цій відповіді сказано, що команду eval можна використовувати, щоб виправити це, але це не працює для мене :(


1
Так, це працює до тих пір, поки немає необхідності, наприклад, назви файлів із вбудованими пробілами (або ті, що містять глобальні символи).
ilkkachu

0

Якщо це не спрацьовує '', слід скористатися ``:

abc=`ls -l /tmp/test/my\ dir/`

Оновити кращий спосіб:

abc=$(ls -l /tmp/test/my\ dir/)

Це зберігає результат команди у змінній. ОП (як не дивно) хоче зберегти саму команду в змінній. О, і вам слід по-справжньому почати використовувати $( command... )замість задніх посилань.
roaima

Дуже дякую за роз’яснення та поради!
балон

0

Як щодо python3 однолінійного?

bash-4.4# pwd
/home
bash-4.4# CUSTOM="ls -altr"
bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)"
total 8
drwxr-xr-x    2 root     root          4096 Mar  4  2019 .
drwxr-xr-x    1 root     root          4096 Nov 27 16:42 ..
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.