$ 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
Isso foi discutido em várias perguntas no unix.SE, tentarei coletar todos os problemas que puder surgir aqui. Abaixo está uma descrição de por que e como as várias tentativas falham, uma maneira de fazê-lo corretamente com uma função (para um comando fixo), ou com arrays de shell (Bash/ksh/zsh) ou o
$@
pseudo-array (POSIX sh), e notas sobre como usareval
para fazer isso. Algumas referências no final.Para os propósitos aqui, não importa se são apenas os argumentos do comando ou também o nome do comando que deve ser armazenado em uma variável. Eles são processados de forma semelhante até o ponto em que o comando é iniciado, ponto em que o shell apenas recebe a primeira palavra como o nome do comando a ser executado.
Por que falha
A razão pela qual você enfrenta esses problemas é o fato de que a divisão de palavras é bastante simples e não se presta a casos complexos, e o fato de que aspas expandidas de variáveis não agem como aspas, mas são apenas caracteres comuns.
(Observe que a parte sobre aspas é semelhante a qualquer outra linguagem de programação: por exemplo
char *s = "foo()"; printf("%s\n", s)
, não chama a funçãofoo()
em C, mas apenas imprime a stringfoo()
. O shell é uma linguagem de programação, não um processador de macro.)Lembre-se de que é o shell que processa aspas e expansões de variáveis na linha de comando, transformando-a de uma única string na lista de strings que eventualmente são passadas para o comando iniciado. O programa em si não vê as aspas. Por exemplo, se for dado o comando
ls -l "foo bar"
, o shell transforma isso nas três stringsls
e (removendo as aspas)-l
efoo bar
as passa parals
. (Até o nome do comando é passado, embora nem todos os programas o usem.)Os casos apresentados na pergunta:
A atribuição aqui atribui a string única
ls -l "/tmp/test/my dir"
aabc
:Abaixo,
$abc
é dividido em espaço em branco, els
recebe os três argumentos-l
,"/tmp/test/my
edir"
(com uma aspa na frente do segundo e outra no verso do terceiro). A opção funciona, mas o caminho é processado incorretamente:Aqui, a expansão é citada, então é mantida como uma única palavra. O shell tenta encontrar um programa literalmente chamado
ls -l "/tmp/test/my dir"
, incluindo espaços e aspas.E aqui,
$abc
é dividido, e apenas a primeira palavra resultante é tomada como argumento para-c
, então o Bash é executadols
no diretório atual. As outras palavras são argumentos para bash e são usadas para preencher$0
,$1
, etc.Com
bash -c "$abc"
, eeval "$abc"
, há uma etapa adicional 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á o risco de executar acidentalmente, por exemplo, uma substituição de comando de dados fornecidos pelo usuário, a menos que você esteja muito cuidado ao citar.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.
Usando uma matriz:
Arrays permitem criar variáveis de várias palavras onde as palavras individuais contêm espaço em branco. Aqui, as palavras individuais são armazenadas como elementos de matriz distintos e a
"${array[@]}"
expansão expande cada elemento como palavras de shell separadas: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:
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:
A desvantagem dos arrays é que eles não são um recurso padrão, então shells POSIX simples (como
dash
, o padrão/bin/sh
no Debian/Ubuntu) não os suportam (mas veja abaixo). Bash, ksh e zsh sim, então é provável que seu sistema tenha algum shell que suporte arrays.Usando
"$@"
Em shells sem suporte para arrays nomeados, ainda é possível usar os parâmetros posicionais (o pseudo-array
"$@"
) para armazenar os argumentos de um comando.A seguir, devem ser bits de script portáteis que fazem o equivalente aos bits de código na seção anterior. A matriz é substituída por
"$@"
, a lista de parâmetros posicionais. A configuração"$@"
é feita comset
, e as aspas duplas ao redor"$@"
são importantes (elas fazem com que os elementos da lista sejam citados individualmente).Primeiro, simplesmente armazenando um comando com argumentos
"$@"
e executando-o:Configurando condicionalmente partes das opções de linha de comando para um comando:
Usando apenas
"$@"
para opções e operandos:(Claro,
"$@"
geralmente é preenchido com os argumentos do próprio script, então você terá que salvá-los em algum lugar antes de reaproveitar"$@"
.)Usando
eval
(cuidado aqui!)eval
pega uma string e a executa como um comando, como se fosse digitado na linha de comando do shell. Isso inclui todo o processamento de cotação e expansão, que é útil e perigoso.No caso simples, permite fazer exatamente o que queremos:
Com
eval
, as aspas são processadas, entãols
eventualmente vê apenas os dois argumentos-l
e/tmp/test/my dir
, como queremos.eval
também é inteligente o suficiente para concatenar quaisquer argumentos que obtenha, portanto,eval $cmd
também pode funcionar em alguns casos, mas, por exemplo, todas as execuções de espaços em branco seriam alteradas para espaços únicos. Ainda é melhor citar a variável lá, pois isso garantirá que ela não seja modificada paraeval
.No entanto, é perigoso incluir a entrada do usuário na string de comando para
eval
. Por exemplo, isso parece funcionar:Mas se o usuário fornecer uma entrada que contenha aspas simples, ele poderá sair das aspas e executar comandos arbitrários! Por exemplo, com o input
'$(whatever)'.txt
, seu script executa alegremente a substituição do comando. Que poderia ter sidorm -rf
(ou pior) em vez disso.O problema é que o valor de
$filename
foi incorporado na linha de comando queeval
é executada. Foi expandido anteseval
, que viu, por exemplo, o comandols -l ''$(whatever)'.txt'
. Você precisaria pré-processar a entrada para ser seguro.Se fizermos de outra forma, mantendo o nome do arquivo na variável e deixando o
eval
comando expandi-lo, é mais seguro novamente:Observe que as aspas externas agora são aspas simples, portanto, as expansões internas não acontecem. Portanto,
eval
vê o comandols -l "$filename"
e expande o nome do arquivo com segurança.Mas isso não é muito diferente de apenas armazenar o comando em uma função ou array. Com funções ou matrizes, não há esse problema, pois as palavras são mantidas separadas o tempo todo e não há citação ou outro processamento para o conteúdo de
filename
.Praticamente a única razão para usar
eval
é aquela em que a parte variável envolve elementos de sintaxe de shell que não podem ser trazidos por meio de variáveis (pipelines, redirecionamentos, etc.). No entanto, você precisará citar/escapar tudo o mais na linha de comando que precisa de proteção da etapa de análise adicional (consulte o link abaixo). De qualquer forma, é melhor evitar incorporar a entrada do usuário noeval
comando!Referências
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ê tivesse acabado de digitá-lo. Mas você tem que citar tudo.Caso simples:
caso não tão simples:
Se precisar de um array para ser executado, transforme-o em um array!
a primeira linha converte a string em uma matriz. A segunda linha executa o comando.
Isso não parece funcionar com comandos encadeados, por exemplo, usando
&&
ou;
.O segundo sinal de aspas interrompe o comando.
Quando eu corro:
Deu-me um erro.
Mas quando eu corro
Não há nenhum erro em tudo
Não há como corrigir 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 :(
Outro truque para executar qualquer comando (trivial/não trivial) armazenado na
abc
variável é:e pressione
UpArrow
ouCtrl-p
para trazê-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 no histórico do Bash e você poderá recuperá-lo por
UpArrow
.