Como podemos executar um comando armazenado em uma variável?


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

Eu queria saber por que as seguintes maneiras de executar o comando acima falham ou são bem-sucedidas?

$ 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


Respostas:


54

Isso já foi discutido em várias questões no unix.SE, tentarei coletar todos os problemas que pudermos apresentar aqui. Referências no final.


Por que falha

A razão pela qual você enfrenta esses problemas é a divisão de palavras e o fato de as aspas expandidas das variáveis ​​não agirem como aspas, mas são apenas caracteres comuns.

Os casos apresentados na pergunta:

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

Aqui, $abcé dividido e lsobtém os dois argumentos "/tmp/test/mye dir"(com as aspas na frente do primeiro e na parte de trás do segundo):

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Aqui, a expansão é citada, portanto, é mantida como uma única palavra. O shell tenta encontrar um programa chamado ls -l "/tmp/test/my dir", incluindo espaços e aspas.

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

E aqui, apenas a primeira palavra ou $abcé usada como argumento -c, então o Bash é executado lsno diretório atual. As outras palavras são argumentos para bash, e são usados para preencher $0, $1etc.

$ bash -c $abc
'my dir'

Com bash -c "$abc", e eval "$abc", há uma etapa adicional de processamento de shell, que faz com que as cotações funcionem, mas também faz com que todas as expansões de shell sejam processadas novamente , portanto, existe o risco de executar acidentalmente uma expansão de comando a partir de dados fornecidos pelo usuário, a menos que você cuidado ao citar.


Melhores maneiras de fazer isso

As duas melhores maneiras de armazenar um comando são: a) usar uma função; b) usar uma variável de matriz (ou os parâmetros posicionais).

Usando uma função:

Simplesmente declare uma função com o comando dentro e execute a função como se fosse um comando. As expansões nos comandos dentro da função são processadas apenas quando o comando é executado, não quando definido, e você não precisa citar os comandos individuais.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Usando uma matriz:

As matrizes permitem a criação de variáveis ​​com várias palavras, onde as palavras individuais contêm espaço em branco. Aqui, as palavras individuais são armazenadas como elementos distintos da matriz e a "${array[@]}"expansão expande cada elemento como palavras shell separadas:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

A sintaxe é um pouco horrível, mas as matrizes também permitem que você construa a linha de comando peça por peça. Por exemplo:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

ou mantenha partes da linha de comando constantes e use a matriz preenchendo apenas uma parte, opções ou nomes de arquivos:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

A desvantagem dos arrays é que eles não são um recurso padrão; portanto, os shells POSIX comuns (como dasho padrão /bin/shno Debian / Ubuntu) não os suportam (mas veja abaixo). Bash, ksh e zsh, no entanto, portanto, é provável que seu sistema tenha algum shell que suporte matrizes.

Usando "$@"

Em shells sem suporte para matrizes nomeadas, ainda é possível usar os parâmetros posicionais (a pseudo-matriz "$@") para conter os argumentos de um comando.

A seguir, devem ser apresentados os bits de script portáteis que equivalem aos bits de código na seção anterior. A matriz é substituída por "$@", a lista de parâmetros posicionais. A configuração "$@"é feita com set, e as aspas duplas "$@"são importantes (isso faz com que os elementos da lista sejam citados individualmente).

Primeiro, simplesmente armazene um comando com argumentos "$@"e execute-o:

set -- ls -l "/tmp/test/my dir"
"$@"

Configurando condicionalmente partes das opções da linha de comandos para um comando:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "$@" -l
fi
set -- "$@" "$targetdir"

"$@"

Somente usando "$@"para opções e operandos:

set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir

transmutate "$@"

(É claro, "$@"geralmente é preenchido com os argumentos do próprio script, portanto, você precisará salvá-los em algum lugar antes de redistribuir "$@".)


Tenha cuidado com eval!

Como evalintroduz um nível adicional de processamento de cotação e expansão, você precisa ter cuidado com a entrada do usuário. Por exemplo, isso funciona desde que o usuário não digite aspas simples:

read -r filename
cmd="ls -l '$filename'"
eval "$cmd";

Mas se eles fornecerem a entrada '$(uname)'.txt, seu script executará a substituição de comando com prazer.

Uma versão com matrizes é imune a isso, já que as palavras são mantidas separadas o tempo todo, não há aspas ou outro processamento para o conteúdo de filename.

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

Referências


2
você pode contornar a coisa da avaliação eval fazendo cmd="ls -l $(printf "%q" "$filename")". não é bonito, mas se o usuário estiver decidido a usar um eval, isso ajuda. É também muito útil para enviar o comando que as coisas semelhantes, como ssh foohost "ls -l $(printf "%q" "$filename")", ou no espírito desta questão: ssh foohost "$cmd".
22318 Patrick

Não está diretamente relacionado, mas você codificou o diretório? Nesse caso, convém procurar o alias. Algo como: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny

4

A maneira mais segura de executar um comando (não trivial) é eval. Em seguida, você pode escrever o comando como faria na linha de comando e ele é executado exatamente como se você tivesse acabado de inseri-lo. Mas você tem que citar tudo.

Caso simples:

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

caso não tão simples:

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

3

O segundo sinal de cotação quebra o comando.

Quando eu corro:

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

Isso me deu um erro.

Mas quando eu corro

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

Não há nenhum erro

Não há como corrigir isso no momento (para mim), mas você pode evitar o erro não tendo espaço no nome do diretório.

Esta resposta disse que o comando eval pode ser usado para corrigir isso, mas não funciona para mim :(


11
Sim, isso funciona, desde que não seja necessário, por exemplo, nomes de arquivos com espaços incorporados (ou que contenham caracteres glob).
Ilkkachu

0

Se isso não funcionar '', você deve usar ``:

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

Atualize melhor maneira:

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

Isso armazena o resultado do comando em uma variável. O OP (estranhamente) deseja salvar o próprio comando em uma variável. Ah, e você realmente deve começar a usar, em $( command... )vez de backticks.
roaima 23/09

Muito obrigado pelos esclarecimentos e dicas!
balon 23/09

0

Que tal um liner 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 ..
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.