As variáveis devem ser citadas quando executadas?

18

A regra geral no shell script é que as variáveis devem sempre ser citadas, a menos que exista uma razão convincente para não fazê-lo. Para mais detalhes do que você provavelmente gostaria de saber, dê uma olhada neste excelente Q & A: Implicações de segurança de esquecer de citar uma variável em shells bash / POSIX .

Considere, no entanto, uma função como a seguinte:

run_this(){
    $@
}

O $@ deve estar lá ou não? Eu brinquei com isso um pouco e não consegui encontrar nenhum caso em que a falta de citações causasse um problema. Por outro lado, usar as aspas faz com que seja quebrado ao passar um comando contendo espaços como uma variável citada:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

A execução do script acima retorna:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Eu posso contornar isso se eu usar run_that $comm em vez de run_that "$comm" , mas como a função run_this (sem aspas) funciona com ambos, parece a aposta mais segura.

Portanto, no caso específico de usar $@ em uma função cujo trabalho é executar $@ como um comando, $@ deve ser citado? Por favor, explique por que deveria / não deveria ser citado e dar um exemplo de dados que podem quebrá-lo.

    
por terdon 23.12.2015 / 10:17

6 respostas

19

O problema está em como o comando é passado para a função:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@" deve ser usado no caso geral em que sua função run_this é prefixada para um comando normalmente escrito. run_this leva a citar o inferno:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Não sei como devo passar um nome de arquivo com espaços para run_this .

    
por 23.12.2015 / 10:22
9

Ou é:

interpret_this_shell_code() {
  eval "$1"
}

Ou:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

ou:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Mas:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Não faz muito sentido.

Se você deseja executar o comando ls -l (não o comando ls com ls e -l como argumentos), você faria:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Mas se (mais provavelmente), é o comando ls com ls e -l como argumentos, você executaria:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Agora, se for mais do que um simples comando que você deseja executar, se você quiser fazer atribuições de variáveis, redirecionamentos, canais ..., somente interpret_this_shell_code será:

interpret_this_shell_code 'ls -l 2> /dev/null'

embora, claro, você sempre possa fazer:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
    
por 24.12.2015 / 00:58
5

Olhando para a perspectiva do bash / ksh / zsh, $* e $@ são um caso especial de expansão geral de matriz. Expansões de matriz não são como expansões de variáveis normais:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Com as expansões $* / ${a[*]} , você obtém a matriz associada ao primeiro valor de IFS (que é espaço por padrão) em uma cadeia gigante. Se você não citar, ele será dividido como uma string normal.

Com as expansões $@ / ${a[@]} , o comportamento depende se a expansão $@ / ${a[@]} é citada ou não:

  1. se for citado ( "$@" ou "${a[@]}" ), você recebe o equivalente a %código% ou "$1" "$2" "$3" #...
  2. se não estiver entre aspas ( "${a[1]}" "${a[2]}" "${a[3]}" # ... ou $@ ), você recebe o equivalente a %código% ou ${a[@]}

Para agrupar comandos, você definitivamente quer as expansões citadas @ (1.).

Mais informações interessantes sobre matrizes bash (e bash): link

    
por 23.12.2015 / 12:05
4

Desde quando você não dá aspas duplas em $@ , você deixou todos os problemas de globbing em o link você deu a sua função.

Como você pode executar um comando chamado * ? Você não pode fazer isso com run_this :

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

E, mesmo quando o erro ocorreu, run_that deu a você uma mensagem mais significativa.

A única maneira de expandir $@ para palavras individuais é aspas duplas. Se você quiser executá-lo como um comando, você deve passar o comando e os parâmetros como palavras separadas. Aquilo que você fez no lado do interlocutor, não dentro da sua função.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

é uma escolha melhor. Ou se suas matrizes de suporte ao shell:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Mesmo quando o shell não suporta array, você ainda pode jogar com ele usando "$@" .

    
por 23.12.2015 / 11:42
3

A execução de variáveis em bash é uma técnica propensa a falhas. É simplesmente impossível escrever uma função run_this que lida corretamente com todos os casos de borda, como:

    Pipelines
  • (por exemplo, ls | grep filename )
  • redirecionamentos de entrada / saída (por exemplo, ls > /dev/null )
  • instruções de shell como if while etc.

Se tudo o que você quer é evitar a repetição de código, é melhor usar funções. Por exemplo, em vez de:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Você deve escrever

command() {
    ls -l
}
...
command

Se os comandos estiverem disponíveis somente em tempo de execução, você deve usar eval , que é projetado especificamente para lidar com todas as peculiaridades que farão run_this falhar:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Observe que eval é conhecido por questões de segurança, mas se você passar variáveis de fontes não confiáveis para run_this , você também enfrentará a execução arbitrária de códigos.

    
por 23.12.2015 / 12:58
1

A escolha é sua. Se você não citar $@ , qualquer um dos seus valores sofrerá expansão e interpretação adicionais. Se você citar todos os argumentos passados, a função será reproduzida em sua expansão na íntegra. Você nunca será capaz de lidar de forma confiável com tokens de sintaxe shell como &>| e etc de qualquer maneira sem analisar os argumentos você mesmo - e então você fica com as opções mais razoáveis de entregar sua função:

  1. Exatamente as palavras usadas na execução de um único comando simples com "$@" .

... ou ...

  1. Uma versão expandida e interpretada dos seus argumentos, que só então são aplicados juntos como um simples comando com $@ .

Nenhuma maneira está errada se for intencional e se os efeitos do que você escolher forem bem compreendidos. Ambas as maneiras têm vantagens uma sobre a outra, embora as vantagens da segunda raramente sejam particularmente úteis. Ainda assim ...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')
drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... não é inútil , raramente é provável que seja de muito uso . E em um bash shell, porque bash não por padrão cola uma definição de variável ao seu ambiente mesmo quando a dita definição é anexada à linha de comando de um especial embutido ou a uma função , o valor global para $IFS não é afetado e sua declaração é local apenas para a chamada run_this() .

Da mesma forma:

(run_this(){ $@; }; set -f; run_this ls -l \*)
ls: cannot access *: No such file or directory

... o globbing também é configurável. Citações servem a um propósito - elas não são para nada. Sem eles, a expansão de shell passa por uma interpretação extra - interpretação configurável . Costumava ser - com alguns shells muito antigos - que $IFS era globalmente aplicado a todos entrada, e não apenas expansões. De fato, as referidas shells se comportaram muito como run_this() em que elas quebraram todas as palavras de entrada no valor de $IFS . E assim, se o que você está procurando é um comportamento muito antigo do shell, você deve usar run_this() .

Eu não estou procurando por isso, e estou bastante pressionado no momento para encontrar um exemplo útil para isso. Eu geralmente prefiro os comandos que meu shell executa para serem aqueles que eu digito nele. E assim, dada a escolha, eu quase sempre run_that() . Exceto que ...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')
drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Apenas sobre qualquer coisa pode ser citada. Comandos serão executados entre aspas. Ele funciona porque, quando o comando é executado, todas as palavras de entrada já foram submetidas a quote-removal - que é o último estágio do processo de interpretação de entrada do shell. Portanto, a diferença entre 'ls' e ls só pode importar enquanto o shell estiver sendo interpretado - e é por isso que citar ls garante que qualquer alias chamado ls não seja substituído por meu citado ls palavra de comando. Além disso, as únicas coisas que as citações afetam são a delimitação das palavras (que é como e por que a variável / input-espaço em branco) , e a interpretação de metacaracteres e palavras reservadas.

Então:

'for' f in ...
 do   :
 done
bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Você nunca poderá fazer isso com run_this() ou run_that() .

Mas os nomes das funções, ou $PATH 'd comandos, ou built-in serão executados com a melhor cotação ou sem aspas, e é exatamente assim que run_this() e run_that() funcionam. Você não poderá fazer nada útil com $<>|&(){} desses. Curto de eval , é.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')
eval
printf
"%s\n"
"$@"

Mas sem isso, você está limitado aos limites de um comando simples em virtude das aspas que usa (mesmo quando não o faz porque o $@ age como uma citação no início do processo quando o comando é analisado por metacaracteres) . A mesma restrição é válida para as atribuições e redirecionamentos da linha de comando, que são limitados à linha de comando da função. Mas isso não é grande coisa:

(run_that(){ "$@";}; echo hey | run_that cat)
hey

Eu poderia ter tão facilmente < entrada redirecionada ou > saída como abri o pipe.

De qualquer forma, de uma maneira circular, não há maneira certa ou errada aqui - cada maneira tem seus usos. É só que você deve escrever como você pretende usá-lo, e você deve saber o que você pretende fazer. Omitir citações pode ter um propósito - caso contrário, não haveria citações -, mas se você as omitir por razões que não são relevantes para o seu propósito, você está apenas escrevendo um código incorreto. Faça o que você quer dizer; Eu tento de qualquer maneira.

    
por 29.12.2015 / 09:24