desenv-web-rp.com

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

$ 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
53
Tim

Isso já foi discutido em várias questões no unix.SE, tentarei coletar todos os problemas que puder 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 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 ls obtém os dois argumentos "/tmp/test/my E dir" (Com as aspas na frente do primeiro e no verso 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 para -c; Portanto, 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á uma etapa adicional de processamento do Shell, que faz as cotações funcionarem, mas também faz com que todas as expansões do Shell sejam processadas novamente, portanto, há o risco de executar acidentalmente uma expansão de comando a partir de dados fornecidos pelo usuário, a menos que você tenha muito 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 expansão "${array[@]}" Expande cada elemento como palavras separadas do Shell:

# 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 o array 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 dash, o padrão /bin/sh No Debian/Ubuntu) não os suportam (mas veja abaixo) . Bash, ksh e zsh, no entanto, portanto, é provável que seu sistema possua algum Shell que suporte matrizes.

Usando "[email protected]"

Em shells sem suporte para matrizes nomeadas, ainda é possível usar os parâmetros posicionais (o pseudo-array "[email protected]") 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 "[email protected]", A lista de parâmetros posicionais. A configuração "[email protected]" É feita com set, e as aspas duplas em torno de "[email protected]" São importantes (isso faz com que os elementos da lista sejam citados individualmente).

Primeiro, basta armazenar um comando com argumentos em "[email protected]" E executá-lo:

set -- ls -l "/tmp/test/my dir"
"[email protected]"

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

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

"[email protected]"

Apenas usando "[email protected]" Para opções e operandos:

set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir

transmutate "[email protected]"

(É claro que "[email protected]" Geralmente é preenchido com os argumentos do próprio script, portanto você precisará salvá-los em algum lugar antes de redistribuir "[email protected]".)


Cuidado com eval!

Como 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 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 do 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

72
ilkkachu

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"
8
Hauke Laging

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 :(

3
Wattana Gaming

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

$ history -s $abc

e pressione UpArrow ou Ctrl-p para trazê-lo na linha de comando. Diferentemente 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 nova entrada ao histórico do Bash e você poderá recuperá-lo por UpArrow.

0
bloody

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

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

Atualize melhor maneira:

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

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 ..
0
Marius Mitrofan