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
- Divisão de palavras em BashGuide
- BashFAQ / 050 ou "Estou tentando colocar um comando em uma variável, mas os casos complexos sempre falham!"
- A pergunta Por que meu script de shell sufoca em espaço em branco ou outros caracteres especiais? , que discute várias questões relacionadas à cotação e espaço em branco, incluindo o armazenamento de comandos.