Isso já foi discutido em várias questões no unix.SE, tentarei coletar todos os problemas que pudermos apresentar 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 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 na parte de trás 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 -c
, então o Bash é executado ls
no diretório atual. As outras palavras são argumentos para bash, e são usados 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 com que as cotações funcionem, mas também faz com que todas as expansões de shell sejam processadas novamente , portanto, existe o risco de executar acidentalmente uma expansão de comando a partir de dados fornecidos pelo usuário, a menos que você 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 "${array[@]}"
expansão 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 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 a matriz 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 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.
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 "$@"
, a lista de parâmetros posicionais. A configuração "$@"
é feita com set
, e as aspas duplas "$@"
são importantes (isso faz com que os elementos da lista sejam citados individualmente).
Primeiro, simplesmente armazene um comando com argumentos "$@"
e execute-o:
set -- ls -l "/tmp/test/my dir"
"$@"
Configurando condicionalmente partes das opções da linha de comandos para um comando:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Somente usando "$@"
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, portanto, você precisará salvá-los em algum lugar antes de redistribuir "$@"
.)
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 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 de 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