As variáveis ​​devem ser citadas quando executadas?

18

A regra geral no script de shell é que as variáveis ​​sempre devem ser citadas, a menos que haja uma razão convincente para não fazê-lo. Para obter mais detalhes do que você provavelmente deseja saber, dê uma olhada nesta grande seção de perguntas e respostas: Implicações de segurança de esquecer de citar uma variável nos shells bash / POSIX .

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

run_this(){
    $@
}

Deve $@ser citado lá ou não? Brinquei um pouco e não consegui encontrar nenhum caso em que a falta de aspas causasse um problema. Por outro lado, o uso de aspas faz com que seja interrompido ao passar um comando contendo espaços como uma variável entre aspas:

#!/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 usar em run_that $commvez de run_that "$comm", mas como a run_thisfunção (sem aspas) funciona com ambos, parece a aposta mais segura.

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

terdon
fonte
6
run_thatO comportamento de definitivamente é o que eu esperaria (e se houver um espaço no caminho para o comando?). Se você quisesse o outro comportamento, certamente o citaria no local da chamada onde sabe quais são os dados? Eu esperaria chamar essa função como run_that ls -l, que funciona da mesma maneira em qualquer versão. Existe um caso que fez você esperar diferente?
Michael Homer
@MichaelHomer Acho que minha edição aqui levou isso: unix.stackexchange.com/a/250985/70524 #
muru
@MichaelHomer, por algum motivo (provavelmente porque ainda não tomei minha segunda xícara de café), não havia considerado espaços nos argumentos ou no caminho do comando, mas apenas no próprio comando (opções). Como é frequentemente o caso, isso parece muito óbvio em retrospecto.
terdon
Há uma razão pela qual os shells ainda suportam funções, em vez de simplesmente colocar comandos em uma matriz e executá-los ${mycmd[@]}.
chepner

Respostas:

20

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 run_thisfunção é prefixada para um comando normalmente escrito. run_thisleva 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 tenho certeza de como devo passar um nome de arquivo com espaços para run_this.

muru
fonte
11
Foi de fato sua edição que levou a isso. Por alguma razão, simplesmente não me ocorreu testar com um nome de arquivo com espaços. Não tenho absolutamente nenhuma idéia do por que não, mas lá está você. Você está certo, claro, também não vejo uma maneira de fazer isso corretamente run_this.
terdon
As citações de @terdon tornaram-se um hábito que eu assumi que você tinha deixado sem $@aspas acidentalmente. Eu deveria ter deixado um exemplo. : D
muru 23/12/2015
2
Nah, é de fato um hábito que eu testei (erradamente) e concluí que "huh, talvez este não precise de aspas". Um procedimento conhecido como peido cerebral.
terdon
11
Você não pode passar um nome de arquivo com espaços para run_this. Esse é basicamente o mesmo problema com o preenchimento de comandos complexos em cadeias de caracteres, conforme discutido na Bash FAQ 050 .
Etan Reisner
9

Também é:

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 ls -lcomando (não o lscomando com lse -lcomo argumentos), faça:

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

Mas se (mais provável), é o lscomando com lse -lcomo argumentos, você deve executar:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Agora, se é mais do que um simples comando que você deseja executar, se você deseja fazer atribuições de variáveis, redirecionamentos, pipes ..., apenas interpret_this_shell_codefará:

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

embora você possa sempre fazer:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
fonte
5

Olhando para ele da perspectiva bash / ksh / zsh, $*e $@são um caso especial de expansão geral da matriz. As expansões de matriz não são como expansões 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 $*/ ${a[*]}expansions, você junta a matriz com o primeiro valor de IFS- que é o espaço por padrão - em uma sequência gigante. Se você não citar, ele será dividido como uma string normal.

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

  1. se estiver entre aspas ( "$@"ou "${a[@]}"), você obtém o equivalente "$1" "$2" "$3" #... ou"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. se não estiver entre aspas ( $@ou ${a[@]}), você obtém o equivalente $1 $2 $3 #... ou${a[1]} ${a[2]} ${a[3]} # ...

Para comandos de quebra automática, você definitivamente deseja as expansões entre aspas (1.).


Mais informações boas sobre matrizes do bash (e do tipo bash): https://lukeshu.com/blog/bash-arrays.html

PSkocik
fonte
11
Acabei de perceber que estou me referindo a um link que começa com Luke, enquanto usava uma máscara Vader. A força é forte neste post.
PSKocik
4

Desde quando você não aspas duplas $@, você deixou todos os problemas no link que você deu à 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 você vê, mesmo quando ocorreu um erro, run_thatdeu a você uma mensagem mais significativa.

A única maneira de expandir $@para palavras individuais é usar aspas duplas. Se você deseja executá-lo como um comando, deve passar o comando e seus parâmetros como palavras separadas. É o que você fez no lado do chamador, 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 o seu shell suportar matrizes:

$ 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 brincar com ele usando"$@" .

cuonglm
fonte
3

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

  • gasodutos (por exemplo ls | grep filename)
  • redirecionamentos de entrada / saída (por exemplo ls > /dev/null)
  • declarações shell como if whileetc.

Se tudo o que você quer fazer é 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ê deveria escrever

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

Se os comandos estiverem disponíveis apenas em tempo de execução, você deve usar o evalque foi projetado especificamente para lidar com todas as peculiaridades que causam run_thisfalhas:

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

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

Dmitry Grigoryev
fonte
1

A escolha é sua. Se você não citar $@nenhum dos seus valores, sofrerá expansão e interpretação adicionais. Se você citar todos os argumentos passados, a função será reproduzida literalmente em sua expansão. Você nunca poderá manipular de forma confiável tokens de sintaxe de shell como &>|e etc, de qualquer maneira, sem analisar os argumentos de qualquer maneira - e assim você terá as opções mais razoáveis ​​de entregar sua função em um dos seguintes:

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

...ou...

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

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

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... não é inútil , apenas raramente é de grande utilidade . E em um bashshell, porque bash, por padrão, não adere uma definição de variável ao seu ambiente, mesmo quando a definição é anexada à linha de comando de um built-in especial ou a uma função, o valor global de $IFSnão é afetado e sua declaração é local. apenas para a run_this()chamada.

Similarmente:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... o globbing também é configurável. As cotações servem a um propósito - não são à toa. Sem eles, a expansão do shell passa por uma interpretação extra - interpretação configurável . Ela costumava ser - com algumas muito antigas conchas - que $IFSfoi globalmente aplicadas a todos os de entrada, e não apenas expansões. De fato, as referidas conchas se comportaram da mesma run_this()maneira que quebraram todas as palavras de entrada no valor de $IFS. E assim, se o que você está procurando é que o comportamento shell muito antiga, então você deve usar run_this().

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

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Qualquer coisa pode ser citada. Os comandos serão executados entre aspas. Funciona porque, no momento em que o comando é realmente executado, todas as palavras de entrada já foram removidas entre aspas - que é o último estágio do processo de interpretação de entrada do shell. Portanto, a diferença entre 'ls'e lssó pode importar enquanto o shell estiver interpretando - e é por isso que a citação lsgarante que qualquer apelido nomeado lsnão seja substituído pela minha lspalavra de comando citada . Fora isso, as únicas coisas que as aspas afetam são a delimitação de palavras (que é como e por que funciona a citação de espaço em branco de variável / entrada) 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 será capaz de fazer isso com um run_this()ou outro run_that().

Mas nomes de funções, ou $PATHcomandos, ou builtins serão executados entre aspas ou sem aspas, e é exatamente assim run_this()e como run_that()funciona em primeiro lugar. Você não poderá fazer nada de útil com $<>|&(){}nenhum deles. Curto eval, é.

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

eval
printf
"%s\n"
"$@"

Mas sem ele, você está limitado aos limites de um comando simples em virtude das aspas que você usa (mesmo quando não o faz, porque $@age como uma cotação no início do processo quando o comando é analisado por metacaracteres) . A mesma restrição se aplica às atribuições e redirecionamentos da linha de comando, 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 <redirecionado a entrada ou >saída lá como abri o tubo.

De qualquer forma, de uma maneira geral, não há um caminho certo ou errado aqui - cada um tem seus usos. Você deve escrever como pretende usá-lo e saber o que pretende fazer. Citações omitindo pode ter um propósito - caso contrário, não haveria ser citações em tudo - mas se você omiti-los por razões não relevantes para a sua finalidade, você está apenas escrevendo código ruim. Faça o que você quer dizer; Eu tento de qualquer maneira.

mikeserv
fonte