A saída do script shell é dividida incorretamente enquanto é passada como argumento para o script

2

Digamos que eu tenha os dois scripts de shell a seguir:

#!/bin/sh
#This script is named: args.sh

echo 1 "\"Two words\"" 3

e:

#!/bin/sh
#This script is named: test.sh

echo "Argument 1: "$1
echo "Argument 2: "$2
echo "Argument 3: "$3

Quando eu chamo os scripts como:

sh test.sh $(sh args.sh)

, eu recebo:

Argument 1: 1
Argument 2: "Two
Argument 3: words"

Quando eu esperava, em vez disso:

Argument 1: 1
Argument 2: Two words
Argument 3: 3

Copiando a saída de sh args.sh e colando-a como a entrada de sh test.sh funciona bem; então eu assumo que isso não é realmente o que o shell está fazendo. Eu posso alcançar a saída desejada / esperada chamando sh args.sh | xargs sh test.sh .

No entanto, estou me perguntando se existe uma maneira equivalente de fazer isso sem canalizar a saída do primeiro script (args.sh) para xargs? Eu quero que os scripts sejam chamados na ordem original; o script de argumento para gerar os parâmetros para o segundo. Eu também estou procurando uma explicação sobre por que esta chamada não funciona como esperado?

    
por xequ4 21.03.2012 / 04:08

2 respostas

4

Parte do problema é que a string retornada por args.sh não é analisada da mesma forma que um comando direto, mas apenas pelo valor de $ IFS ( $' \t\n' ). Tente ativar o rastreio de comandos com set -x :

$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh 1 '"Two' 'words"' 3
Argument 1: 1
Argument 2: "Two
Argument 3: words"
$

Observe que a linha começa com um único + : há quatro argumentos, o '"Two' e 'words"' são dois analisados como argumentos separados. O que você gostaria de fazer é alterar o $ IFS.

$ set -x
$ IFS='"'
+ IFS='"'
$ sh /tmp/test.sh $(sh /tmp/args.sh)
++ sh /tmp/args.sh
+ sh /tmp/test.sh '1 ' 'Two words' ' 3'
Argument 1: 1
Argument 2: Two words
Argument 3:  3
$

Isso não funcionará para todas as saídas. A melhor coisa a fazer é alterar a saída de args.sh para separar a saída por algo diferente de um espaço, por exemplo, uma vírgula ou dois pontos:

$ cat /tmp/args.sh
#!/bin/sh
#This script is named: args.sh

echo "1,Two words,3"
$ IFS=,
$ sh /tmp/test.sh $(sh /tmp/args.sh)
+ sh /tmp/args.sh
+ sh /tmp/test.sh 1 Two words 3
Argument 1: 1
Argument 2: Two words
Argument 3: 3
$
    
por 21.03.2012 / 04:50
0

Quando você deixa uma substituição de variável $var ou uma substituição de comando $(cmd) unquoted, o resultado passa pelas seguintes transformações:

  1. Divida a string resultante em palavras. A divisão acontece no espaço em branco (sequências de espaço, tabulações e novas linhas); isso pode ser configurado definindo IFS (consulte 1 , 2 ).
  2. Cada palavra resultante é tratada como um padrão glob e, se corresponder a alguns arquivos, a palavra é substituída pela lista de nomes de arquivos.

Observe que o resultado não é uma string, mas uma lista de strings. Além disso, observe que caracteres de cotação como " não estão envolvidos aqui; eles fazem parte da sintaxe da fonte do shell, não da expansão de strings.

Uma regra geral de programação de shell é sempre colocar aspas duplas em torno das substituições de comando e variável, a menos que você saiba por que você precisa deixá-las desligadas. Então, em test.sh , escreva echo "Argument 1: $1" . Para passar argumentos para test.sh , você está com problemas: você precisa passar uma lista de palavras de args.sh para test.sh , mas o método escolhido envolve uma substituição de comando e fornece apenas o caminho para uma string simples.

Se você pode garantir que os argumentos a serem passados não contêm nenhuma nova linha, e é aceitável alterar ligeiramente o processo de invocação, você pode definir IFS para conter apenas uma nova linha. Certifique-se de que args.sh produza exatamente um nome de arquivo por linha sem aspas erradas.

IFS='
'
test.sh $(args.sh)
unset IFS

Se os argumentos puderem conter caracteres arbitrários (presumivelmente exceto os bytes nulos, que você não pode passar como argumentos), você precisará executar alguma codificação. Qualquer codificação serve; é claro, não será o mesmo que passar os argumentos diretamente: isso não é possível. Por exemplo, em args.sh (substitua \t por um caractere de tabulação real se seu shell não for compatível):

for x; do
  printf '%s_\n' "$x" |
  sed -e 's/q/qq/g' -e 's/ /qs/g' -e 's/\t/qt/g' -e 's/$/qn/'
done

e em test.sh :

for arg; do
  decoded=$(printf '%s\n' "$arg" |
            sed -e 's/qs/ /g' -e 's/qt/\t/g' -e 's/qn/\n/g' -e 's/qq/q/g')
  decoded=${decoded%_qn}
  # process "$decoded"
done

Você pode preferir alterar test.sh para aceitar uma lista de strings como entrada. Se as strings não contiverem novas linhas, invoque args.sh | test.sh e use isso em args.sh ( explicação ):

while IFS= read -r line; do
  # process "$line"
done

Outro método que impede a necessidade de citar completamente é invocar o segundo script a partir do primeiro.

…
args.sh "$@"
    
por 21.03.2012 / 21:13