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

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

Eu estava me perguntando 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 

Comentários

Resposta

Isso foi discutido em uma série de perguntas no unix.SE, vou tentar coletar todos que posso encontrar aqui. Referências no final.


Por que falha

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

O casos apresentados na pergunta:

A atribuição aqui atribui a única string ls -l "/tmp/test/my dir" para abc:

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

Abaixo, $abc é dividido em espaços em branco e ls obtém os três argumentos -l, "/tmp/test/my e dir" (com uma aspa na frente do segundo e outra atrás do terceiro). A opção funciona, mas o caminho é processado incorretamente:

$ $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 para encontrar um programa chamado literalmente ls -l "/tmp/test/my dir", espaços e aspas incluídos.

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

E aqui, $abc é dividido e apenas a primeira palavra resultante é considerada o argumento para -c, então o Bash apenas executa ls no diretório atual. As outras palavras são argumentos para bash e são usadas para preencher $0, $1, etc.

$ bash -c $abc "my dir" 

Com bash -c "$abc" e eval "$abc", há um adicional etapa de processamento de shell, que faz as aspas funcionarem, mas também faz com que todas as expansões de shell sejam processadas novamente , portanto, há “um risco de execução acidental, por exemplo, uma substituição de comando de dados fornecidos pelo usuário, a menos que você” são muito cuidadosos muito sobre citações.


Melhores maneiras de fazer isso

As duas melhores maneiras de armazenar um comando são a) usar uma função em vez disso, 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. Expansões em 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 branco espaço. Aqui, as palavras individuais são armazenadas como elementos distintos da matriz e a expansão "${array[@]}" 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 os arrays também permitem que você crie 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 para preencher apenas uma parte dela, como 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, shells POSIX simples (como dash, o padrão /bin/sh no Debian / Ubuntu) não os oferece suporte (mas veja abaixo). Bash, ksh e zsh têm, no entanto, “é 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.

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

Primeiro, basta armazenar um comando com argumentos em "$@" e executá-lo:

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

Definindo condicionalmente partes das opções de linha de comando para um comando:

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

Use apenas "$@" 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, então você ” terei que salvá-los em algum lugar antes de voltar a propor "$@".)


Tenha cuidado com eval !

Visto que eval introduz 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 enquanto o usuário não digita aspas simples:

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

Mas se eles derem a entrada "$(uname)".txt, seu script ficará feliz executa a substituição do comando.

Uma versão com arrays é imune ao como as palavras são mantidas separadas o tempo todo, não há citação ou outro processamento para o conteúdo de filename.

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

Referências

Comentários

  • você pode contornar a coisa de cotação de avaliação fazendo cmd="ls -l $(printf "%q" "$filename")". não é bonito, mas se o usuário estiver decidido a usar um eval, isso ajuda. Ele ‘ também é muito útil para enviar o comando por meio de coisas semelhantes, como ssh foohost "ls -l $(printf "%q" "$filename")", ou no espírito desta questão: ssh foohost "$cmd".
  • Não diretamente relacionado, mas você codificou o diretório? Nesse caso, você pode querer olhar para o alias. Algo como: $ alias abc='ls -l "/tmp/test/my dir"'

Resposta

A maneira mais segura de executar um comando (não trivial) é eval. Então você pode escrever o comando como faria na linha de comando e ele é executado exatamente como se você o tivesse inserido. Mas você precisa 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" 

Comentários

  • Olá @HaukeLaging, o que é \ ‘ ‘?
  • @jumping_monkey Você não pode ter uma ' dentro de uma ' string citada . Assim, você tem que (1) terminar / interromper a citação, (2) escapar do próximo ' para obtê-lo literalmente, (3) digitar a citação literal e (4) caso de interrupção, retome a string citada: (1)'(2)\(3)'(4)'
  • Olá @HaukeLaging, interessante, e por que não apenas abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile' ?
  • @jumping_monkey Sem falar que acabei de explicar por que isso não funciona: Não ‘ faria sentido testar o código antes de postá-lo?

Resposta

O segundo sinal de aspas interrompe o comando.

Quando eu executo:

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

Ocorreu um erro.

Mas quando executo

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 consertar isso, mas não funciona para mim 🙁

Comentários

  • Sim, isso funciona, desde que ‘ não haja necessidade de, por exemplo, nomes de arquivos com espaços incorporados (ou que contenham caracteres glob).

Resposta

Se isso não funcionar com "", então você deve usar ``:

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

Atualizar melhor forma:

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

Comentários

  • Isso armazena o resultado do comando em uma variável. O OP está (estranhamente) querendo salvar o próprio comando em uma variável. Ah, e você realmente deve começar a usar $( command... ) em vez de crases.
  • Muito obrigado pelos esclarecimentos e dicas!

Resposta

Que tal um one-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 .. 

Comentários

  • A questão é sobre BASH, não python.

Resposta

Outro truque simples para executar qualquer comando (trivial / não trivial) armazenado em abc a variável é:

$ history -s $abc 

e pressione UpArrow ou Ctrl-p para colocá-lo na linha de comando. Ao contrário de qualquer outro método, você pode editá-lo antes da execução, se necessário.

Este comando anexará o conteúdo da variável como uma nova entrada ao histórico do Bash e você pode chamá-lo por UpArrow.

Deixe uma resposta

O seu endereço de email não será publicado. Campos obrigatórios marcados com *