Como passar argumentos de script bash para um subshell

13

Eu tenho um script de wrapper que faz algum trabalho e depois passa os parâmetros originais para outra ferramenta:

#!/bin/bash
# ...
other_tool -a -b "$@"

Isso funciona bem, a menos que a "outra ferramenta" seja executada em um subshell:

#!/bin/bash
# ...
bash -c "other_tool -a -b $@"

Se eu chamar meu script wrapper assim:

wrapper.sh -x "blah blup"

então, apenas o primeiro argumento original (-x) é entregue para "other_tool". Na realidade, não crio um subshell, mas passo os argumentos originais para um shell em um telefone Android, o que não deve fazer diferença:

#!/bin/bash
# ...
adb sh -c "other_tool -a -b $@"
Ralf Holly
fonte

Respostas:

14

O printfcomando do Bash possui um recurso que citará / escapará / qualquer que seja uma string, desde que o pai e o subshell sejam realmente bash, isso deve funcionar:

[Editar: como siegi apontou em um comentário, se você fizer isso da maneira óbvia, haverá um problema quando nenhum argumento for fornecido, onde ele age como se realmente houvesse um único argumento vazio. Adicionei uma solução alternativa abaixo, envolvendo a string de formato ${1+}, que inclui apenas a string de formato se o primeiro argumento estiver definido . É um pouco estranho, mas funciona.]

#!/bin/bash

quoted_args="$(printf "${1+ %q}" "$@")" # Note: this will have a leading space before the first arg
# echo "Quoted args:$quoted_args" # Uncomment this to see what it's doing
bash -c "other_tool -a -b$quoted_args"

Observe que você também pode fazer isso em uma única linha: bash -c "other_tool -a -b$(printf "${1+ %q}" "$@")"

Gordon Davisson
fonte
Obrigado! Isso funcionou muito bem para mim.
DribblzAroundU82 13/03/19
Isso não funciona corretamente quando nenhum argumento é passado (ele passa um único argumento vazio em vez de nenhum argumento).
Siegi
11
@siegi Boa captura; Eu adicionei uma solução alternativa para isso.
Gordon Davisson
4

Nenhuma das soluções funciona bem. Apenas passe x / \ \ "b \" / aaaaa / \ 'xxx \ yyyy \' / zz \ ​​"offf \" como parâmetro e eles falharão.

Aqui está um invólucro simples que lida com todos os casos. Observe como ele escapa cada argumento duas vezes.

#!/usr/bin/env bash
declare -a ARGS
COUNT=$#
for ((INDEX=0; INDEX<COUNT; ++INDEX))
do
    ARG="$(printf "%q" "$1")"
    ARGS[INDEX]="$(printf "%q" "$ARG")"
    shift
done

ls -l ${ARGS[*]}
jcayzac
fonte
11
também é útil se você estiver tentando concatenar $ @ como parte de um eventual argumento de string citado como '/ bin / foo' "$ @" '--opt' e descobrindo que ele simplesmente não sai como você esperaria .
Jrg 17/06
11
Oh! Graças a deus. Estive lutando com esse problema e aparentemente nada funcionou. Isso resolveu. Seria bom se, de alguma forma, fosse um bash incorporado para chamar escapes duplos, considerando que esse problema surge muito.
MooGoo 22/09
2

Mude $@para $*. Fiz um pequeno teste local e funciona no meu caso.

#!/bin/sh
bash -c "echo $*"
bash -c "echo $@"

Salvar como test.she torná-lo executável fornece

$ ./test.sh foo bar
foo bar
foo

Há uma diferença sutil entre $*e $@, como você pode ver. Veja, por exemplo, http://ss64.com/bash/syntax-parameters.html


Para a pergunta de acompanhamento nos comentários: você precisa escapar, por exemplo, espaço em branco "duas vezes" para passar uma string com um separador como argumento combinado, por exemplo, com test.shmodificado para um wcwrapper:

#!/bin/sh
bash -c "wc $*"

Isso funciona:

$ touch test\ file
$ ./test.sh -l "test\ file"
0 test file

mas:

$ ./test.sh -l "test file"
wc: test: No such file or directory
wc: file: No such file or directory
0 total
Daniel Andersson
fonte
Infelizmente, isso também não funciona. Se você chamar o wrapper assim: wrapper.sh -x "blah blup"então o subshell recebe três parâmetros (-x, blá, BLUP) em vez de dois (-x "blup blah")
Ralf Holly
Você precisa "escapar duas vezes" do argumento se quiser que o separador de espaço seja preservado, ou seja "Blah\ blup". Experimente e veja se funciona.
Daniel Andersson
2
Parece que funciona, no entanto, isso exigiria que o chamador do wrapper.sh adicionasse escapes, e estou procurando uma solução transparente.
Ralf Holly
2

Está falhando porque você está coagindo uma matriz (os parâmetros posicionais) em uma string. "$@"é mágico porque fornece cada parâmetro separado como uma seqüência de caracteres corretamente citada. Adicionar texto adicional quebra a mágica: "blah $@"é apenas uma única string.

Isso pode aproximá-lo:

cmd="other_tool -a -b"
for parm in "$@"; do cmd+=" '$parm'"; done
adb sh -c "$cmd"

Obviamente, qualquer parâmetro que contenha uma aspas simples causará problemas.

Glenn Jackman
fonte
2

Ok, mais explicações:

$ cat /tmp/test.sh
#! /bin/bash

echo '$@='"$@"

set -v # "debug"

sh -c 'echo $@' "$@" # gremlins steal the first option

sh -c 'echo $@' -- "$@" # We defend the option with two knifes

$ bash -e /tmp/test.sh first second third
$@=first second third

sh -c 'echo $@' "$@" # gremlins steal the first option
second third

sh -c 'echo $@' -- "$@" # We defend the option with two knifes
first second third
Jerry
fonte
1

Tentei muitas soluções diferentes, um bom recurso, incluindo informações básicas e alternativas, é, por exemplo, o BashFAQ / 096 no Wiki de Greg (também conhecido como GreyCat) . No total, achei os dois seguintes os mais legíveis (dos que estão em funcionamento):


Desde o Bash 4.4 (até onde pude ver no NEWS ), é possível usar a expansão de parâmetros @Q como esta:

adb sh -c "other_tool -a -b ${*@Q}"

Observe que eu uso $*aqui em vez de, $@porque você deseja "other_tool -a -b ${*@Q}"ser uma única sequência em vez de uma sequência por argumento passado.

Se você quiser fazer o mesmo com uma variável do array bash , precisará da sintaxe ${ARRAY[*]@Q}(entre aspas).


Se você não tem o Bash 4.4 ou posterior disponível ou não tem certeza, esta é minha solução preferida:

function escapeBashArgs() {
    local arg separator=""
    for arg
    do
        printf "%s%q" "$separator" "$arg"
        separator=" "
    done
}

adb sh -c "other_tool -a -b $(escapeBashArgs "$@")"

Observe que aqui você precisa usar em "$@"vez de $@ou "$*"ou $*porque não deseja dividir palavras nos argumentos, portanto, as variantes sem citação não podem ser usadas, e você deseja que o número de argumentos seja preservado, portanto "$*"não pode ser usado, pois todos os argumentos para uma única sequência. A função retorna todos os argumentos em uma única sequência.

Se você não se importa com o espaço adicional na frente do primeiro argumento, pode alterar a printfsequência de formatação para " %q"e remover a separatorvariável. Ou você pode usar o verso da resposta de Gordon Davissons .


Essas soluções funcionam com todos os casos que eu pude apresentar, especialmente:

  • Sem argumentos: escapeBashArgsnada
  • Argumentos vazios: escapeBashArgs "" ""'' ''
  • Argumentos com apenas espaços: escapeBashArgs " " " "' ' ' 'ou \ \ \ \ \( o último espaço é consumido por este renderizador de sites )
  • Argumentos com espaçamento especial e novas linhas: escapeBashArgs "a b" c\ d "arg with newline"'a b' 'c d' $'arg with\nnewline'ou a\ \ \ \ \ \ b c\ d $'arg with\nnewline'( a nova linha está entre withe newline, em outras posições, é devido ao agrupamento de linhas neste site )
  • Argumentos com caracteres especiais: escapeBashArgs '$"'\''({:})''$"'\''({:})'ou\$\"\'\(\{:\}\)
  • Exemplo da resposta jcayzacs : escapeBashArgs x/\ \ \"b\"/aaaaa/\'xxx\ yyyy\'/zz\"offf\"'x/ "b"/aaaaa/'\''xxx yyyy'\''/zz"offf"'oux/\ \ \"b\"/aaaaa/\'xxx\ yyyy\'/zz\"offf\"

(Testado com o lançamento do GNU bash 5.0.3 (1).)

siegi
fonte
-2
#! /bin/bash
sh -c 'other_tool -a -b "$@"' -- "$@"
Jerry
fonte
3
Bem-vindo ao Super Usuário! Estamos procurando respostas longas que forneçam alguma explicação e contexto. Não basta dar uma resposta em uma linha; explique por que sua resposta está correta, idealmente com citações. As respostas que não incluem explicações podem ser removidas.
G-Man diz 'Reinstate Monica'