Как мы можем запустить команду, хранящуюся в переменной?

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в качестве аргумента берется только первое слово or -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[@]}"

Ссылки

ilkkachu
источник
2
вы можете обойти eval цитата, делая 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 можно исправить, но у меня она не работает :(

Wattana Gaming
источник
1
Да, это работает, если нет необходимости, например, в именах файлов со встроенными пробелами (или в тех, которые содержат символы глобуса).
ilkkachu
0

Если это не работает '', то вы должны использовать ``:

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

Обнови лучше:

abc=$(ls -l /tmp/test/my\ dir/)
Балон
источник
Это сохраняет результат команды в переменной. ОП (как ни странно) хочет сохранить саму команду в переменной. О, и вы действительно должны начать использовать $( command... )вместо кавычек.
Ройма
Большое спасибо за разъяснения и советы!
бал
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 ..
Мариус Митрофан
источник