Como posso gerar argumentos para outro comando via substituição de comando

11

Na sequência de: comportamento inesperado na substituição de comandos do shell

Eu tenho um comando que pode ter uma lista enorme de argumentos, alguns dos quais podem legitimamente conter espaços (e provavelmente outras coisas)

Escrevi um script que pode gerar esses argumentos para mim, com aspas, mas devo copiar e colar a saída, por exemplo

./somecommand
<output on stdout with quoting>
./othercommand some_args <output from above>

Eu tentei simplificar isso simplesmente fazendo

./othercommand $(./somecommand)

e encontrou o comportamento inesperado mencionado na pergunta acima. A questão é: a substituição de comandos pode ser usada com confiabilidade para gerar os argumentos, othercommanddado que alguns argumentos exigem citação e isso não pode ser alterado?

user1207217
fonte
Depende do que você quer dizer com "confiável". Se você quiser o próximo comando, pegue a saída exatamente como aparece na tela e aplique regras de shell a ela, então talvez evalpossa ser usado, mas geralmente não é recomendado. xargsé algo a considerar também
Sergiy Kolodyazhnyy
Eu gostaria (esperava) que a saída somecommandsofresse uma análise regular do shell
user1207217
Como eu disse na minha resposta, use algum outro caractere para dividir o campo (como :) ... assumindo que o caractere não estará na saída com segurança .
Olorin 21/01/19
Mas isso não é realmente certo porque ele não obedece citando regras, que é do que se trata
user1207217
2
Você poderia postar um exemplo do mundo real? Quero dizer, a saída real do primeiro comando e como você deseja que ele interaja com o segundo comando.
Nxnev 21/01/19

Respostas:

10

Eu escrevi um script que pode gerar esses argumentos para mim, com aspas

Se a saída for citada corretamente para o shell e você confiar na saída , poderá executá eval-lo.

Supondo que você tenha um shell que suporte matrizes, seria melhor usar um para armazenar os argumentos obtidos.

Se ./gen_args.shproduzir saída como 'foo bar' '*' asdf, então poderíamos executar eval "args=( $(./gen_args.sh) )"para preencher uma matriz chamada argscom os resultados. Isso seria os três elementos foo bar, *, asdf.

Podemos usar "${args[@]}"como de costume para expandir os elementos da matriz individualmente:

$ eval "args=( $(./gen_args.sh) )"
$ for var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

(Observe que as aspas. Se "${array[@]}"expandem para todos os elementos como argumentos distintos, sem modificação. Sem aspas, os elementos da matriz estão sujeitos à divisão de palavras. Veja, por exemplo, a página Matrizes no BashGuide .)

No entanto , evalé possível executar com êxito quaisquer substituições de shell, portanto $HOME, a saída seria expandida para o diretório inicial e uma substituição de comando realmente executaria um comando no shell em execução eval. Uma saída de "$(date >&2)"criaria um único elemento vazio da matriz e imprimiria a data atual no stdout. Isso é preocupante se gen_args.shobtém os dados de alguma fonte não confiável, como outro host na rede, nomes de arquivos criados por outros usuários. A saída pode incluir comandos arbitrários. (Se get_args.shele era malicioso, não precisaria produzir nada, apenas poderia executar os comandos maliciosos diretamente.)


Uma alternativa à citação de shell, que é difícil de analisar sem avaliação, seria usar algum outro caractere como separador na saída do seu script. Você precisaria escolher um que não seja necessário nos argumentos reais.

Vamos escolher #e ter a saída do script foo bar#*#asdf. Agora, podemos usar a expansão de comando sem aspas para dividir a saída do comando nos argumentos.

$ IFS='#'                          # split on '#' signs
$ set -f                           # disable globbing
$ args=( $( ./gen_args3.sh ) )     # assign the values to the arrayfor var in "${args[@]}"; do printf ":%s:\n" "$var"; done
:foo bar:
:*:
:asdf:

Você precisará IFSvoltar mais tarde se depender da divisão de palavras em algum lugar do script ( unset IFSdeve funcionar para torná-lo o padrão) e também usar set +fse quiser usar globbing mais tarde.

Se você não estiver usando o Bash ou algum outro shell que possua matrizes, use os parâmetros posicionais para isso. Substitua args=( $(...) )por set -- $(./gen_args.sh)e use em "$@"vez "${args[@]}"disso. (Também aqui você precisa de aspas "$@", caso contrário, os parâmetros posicionais estão sujeitos à divisão de palavras.)

ilkkachu
fonte
Melhor dos dois mundos!
Olorin 21/01/19
Você gostaria de adicionar um comentário que mostre a importância de citar ${args[@]}- isso não funcionou para mim de outra forma?
user1207217
@ user1207217, sim, você está certo. É a mesma coisa com matrizes e "${array[@]}"como com "$@". Ambos precisam ser citados, ou a divisão de palavras quebra os elementos da matriz em partes.
precisa saber é o seguinte
6

O problema é que, uma vez que o somecommandscript gera as opções para othercommand, as opções são realmente apenas texto e à mercê da análise padrão do shell (afetadas pelo que quer $IFSque seja e pelas opções do shell em vigor, o que você geralmente não faria) estar no controle).

Em vez de usar somecommandpara produzir as opções, seria mais fácil, mais seguro e mais robusto usá-lo para chamar othercommand . O somecommandscript seria então um script de invólucro, em othercommandvez de algum tipo de script auxiliar que você precisaria lembrar de chamar de uma maneira especial como parte da linha de comando do otherscript. Os scripts de wrapper são uma maneira muito comum de fornecer uma ferramenta que apenas chama outra ferramenta semelhante com outro conjunto de opções (basta verificar com filequais comandos /usr/binsão realmente os wrappers de script de shell).

Em bash, kshou zsh, você poderia facilmente um script de wrapper que usa uma matriz para conter as opções individuais da seguinte othercommandmaneira:

options=( "hi there" "nice weather" "here's a star" "*" )
options+=( "bonus bumblebee!" )  # add additional option

Em seguida, chame othercommand(ainda dentro do script do wrapper):

othercommand "${options[@]}"

A expansão de "${options[@]}"garantiria que cada elemento da optionsmatriz fosse citado individualmente e apresentado othercommandcomo argumentos separados.

O usuário do wrapper estaria alheio ao fato de estar realmente chamando othercommand, algo que não seria verdadeiro se o script apenas gerasse as opções da linha de comando para othercommandsaída.

Em /bin/sh, use $@para manter as opções:

set -- "hi there" "nice weather" "here's a star" "*"
set -- "$@" "bonus bumblebee!"  # add additional option

othercommand "$@"

( setÉ o comando usado para definir os parâmetros de posição $1, $2, $3etc. Estes são o que compõe a matriz $@em um shell padrão POSIX. A primeira --é para sinalizar setque não há opções dadas, apenas argumentos. O --é realmente necessário apenas se o o primeiro valor passa a ser algo que começa com -).

Observe que são as aspas duplas ao redor $@e ${options[@]}isso garante que os elementos não sejam divididos individualmente por palavra (e o nome do arquivo seja globbed).

Kusalananda
fonte
você poderia explicar set --?
precisa saber é o seguinte
@ user1207217 Adicionada explicação para responder.
Kusalananda
4

Se a somecommandsaída estiver em uma sintaxe confiável e confiável, você poderá usar eval:

$ eval sh test.sh $(echo '"hello " "hi and bye"')
hello 
hi and bye

Mas você deve ter certeza de que a saída possui aspas válidas e tal; caso contrário, você também poderá executar comandos fora do script:

$ cat test.sh 
for var in "$@"
do
    echo "|$var|"
done
$ ls
bar  baz  test.sh
$ eval sh test.sh $(echo '"hello " "hi and bye"; echo rm *')
|hello |
|hi and bye|
rm bar baz test.sh

Observe que echo rm bar baz test.shnão foi passado para o script (por causa do ;) e foi executado como um comando separado. Eu adicionei ao |redor $varpara destacar isso.


Geralmente, a menos que você possa confiar totalmente na saída de somecommand, não é possível usá-la com segurança para criar uma cadeia de comandos.

Olorin
fonte