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

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

Eu estava me perguntando por que as seguintes maneiras de executar o comando acima falhar ou ter sucesso? Obrigado.

$ 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
    
por Tim 20.05.2018 / 14:46

3 respostas

28

Isso foi discutido em várias questões sobre o unix.SE, e tentarei coletar todos os problemas que surgirem 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 que aspas expandidas de variáveis não são efetivadas.

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 cotações presentes):

$ $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, por isso é mantida como uma única palavra. O shell tenta encontrar um programa chamado 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, apenas a primeira palavra ou $abc é considerada como o 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 de shell, que faz as cotações funcionarem, mas também faz com que todas as expansões de shell sejam processadas novamente , portanto há um risco de acidentalmente executando uma expansão de comando a partir de dados fornecidos pelo usuário, a menos que você seja muito cuidadoso com citações.

Melhores maneiras de fazer isso

As duas melhores maneiras de armazenar um comando são: a) use uma função; b) use uma variável de matriz.

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ó são processadas 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:

Os arrays permitem a criação de variáveis com várias palavras, em que as palavras individuais contêm espaços em branco. Aqui, as palavras individuais são armazenadas como elementos de matriz distintos e a expansão "${array[@]}" expande cada elemento como palavras de 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ê 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 constante de linha de comando e use o array preencha apenas uma parte dele, 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 suportam. Bash, ksh e zsh, no entanto.

Tenha 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 nenhuma aspas simples:

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

Mas se eles derem a entrada '$(uname)'.txt , seu script executará a substituição do comando.

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

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

Referências

por 20.05.2018 / 14:58
4

A maneira mais segura de executar um comando (não trivial) é eval . Então você pode escrever o comando como você faria na linha de comando e ele é executado exatamente como se você tivesse acabado de entrar nele. 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"
    
por 20.05.2018 / 15:20
2

O segundo sinal de citaçã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á erro algum

Não há como consertar isso no momento (para mim), mas você pode evitar o erro por não ter 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 : (

    
por 20.05.2018 / 15:02