Comment puis-je générer des arguments pour une autre commande via la substitution de commande

11

Suite de: comportement inattendu dans la substitution de commandes shell

J'ai une commande qui peut prendre une énorme liste d'arguments, dont certains peuvent légitimement contenir des espaces (et probablement d'autres choses)

J'ai écrit un script qui peut générer ces arguments pour moi, avec des guillemets, mais je dois copier et coller la sortie par exemple

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

J'ai essayé de rationaliser cela en faisant simplement

./othercommand $(./somecommand)

et a rencontré le comportement inattendu mentionné dans la question ci-dessus. La question est - la substitution de commandes peut-elle être utilisée de manière fiable pour générer les arguments othercommandétant donné que certains arguments nécessitent des guillemets et cela ne peut pas être changé?

user1207217
la source
Cela dépend de ce que vous entendez par «de manière fiable». Si vous voulez que la commande suivante prenne la sortie exactement comme elle apparaît à l'écran et lui applique des règles de shell, alors peut eval-être pourrait être utilisé, mais ce n'est généralement pas recommandé. xargsest aussi quelque chose à considérer
Sergiy Kolodyazhnyy
J'aimerais (attendez) que la sortie de somecommandsubisse une analyse régulière du shell
user1207217
Comme je l'ai dit dans ma réponse, utilisez un autre caractère pour le fractionnement de champ (comme :) ... en supposant que ce caractère ne sera pas fiable dans la sortie.
Olorin
Mais ce n'est pas vraiment vrai car il n'obéit pas aux règles de citation, c'est de cela dont il s'agit
user1207217
2
Pourriez-vous publier un exemple concret? Je veux dire, la sortie réelle de la première commande et comment vous voulez qu'elle interagisse avec la deuxième commande.
nxnev

Réponses:

10

J'ai écrit un script qui peut générer ces arguments pour moi, avec des citations

Si la sortie est correctement citée pour le shell et que vous faites confiance à la sortie , vous pouvez l'exécuter eval.

En supposant que vous avez un shell qui prend en charge les tableaux, il serait préférable d'en utiliser un pour stocker les arguments que vous obtenez.

Si ./gen_args.shproduit une sortie comme 'foo bar' '*' asdf, alors nous pourrions exécuter eval "args=( $(./gen_args.sh) )"pour remplir un tableau appelé argsavec les résultats. Ce serait les trois éléments foo bar, *, asdf.

Nous pouvons utiliser "${args[@]}"comme d'habitude pour développer individuellement les éléments du tableau:

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

(Notez que les guillemets "${array[@]}"s'étendent à tous les éléments en tant qu'arguments distincts non modifiés. Sans guillemets, les éléments du tableau sont sujets au fractionnement des mots. Voir par exemple la page Tableaux sur BashGuide .)

Cependant , evalexécutera heureusement toutes les substitutions de shell, donc $HOMEdans la sortie s'étendrait à votre répertoire personnel, et une substitution de commande exécuterait en fait une commande dans le shell en cours d'exécution eval. Une sortie de "$(date >&2)"créerait un seul élément de tableau vide et afficherait la date actuelle sur stdout. C'est un problème si vous gen_args.shobtenez les données d'une source non fiable, comme un autre hôte sur le réseau, des noms de fichiers créés par d'autres utilisateurs. La sortie pourrait inclure des commandes arbitraires. (Si get_args.shlui-même était malveillant, il n'aurait besoin de rien produire, il pourrait simplement exécuter directement les commandes malveillantes.)


Une alternative à la citation de shell, qui est difficile à analyser sans eval, serait d'utiliser un autre caractère comme séparateur dans la sortie de votre script. Vous devez en choisir un qui n'est pas nécessaire dans les arguments réels.

Choisissons #et ayons la sortie du script foo bar#*#asdf. Nous pouvons maintenant utiliser l' expansion de commande sans guillemets pour diviser la sortie de la commande en arguments.

$ 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:

Vous devrez IFSreculer plus tard si vous dépendez de la division des mots ailleurs dans le script ( unset IFSdevrait fonctionner pour en faire la valeur par défaut), et également utiliser set +fsi vous souhaitez utiliser la globalisation plus tard.

Si vous n'utilisez pas Bash ou un autre shell qui a des tableaux, vous pouvez utiliser les paramètres de position pour cela. Remplacez args=( $(...) )par set -- $(./gen_args.sh)et utilisez "$@"plutôt "${args[@]}"qu'alors. (Ici aussi, vous avez besoin de guillemets "$@", sinon les paramètres de position sont sujets au fractionnement des mots.)

ilkkachu
la source
Le meilleur des deux mondes!
Olorin
Souhaitez-vous ajouter une remarque montrant l'importance de citer ${args[@]}- cela n'a pas fonctionné pour moi autrement
user1207217
@ user1207217, oui, vous avez raison. C'est la même chose avec les tableaux et "${array[@]}"qu'avec "$@". Les deux doivent être cités, sinon la séparation des mots rompt les éléments du tableau en parties.
ilkkachu
6

Le problème est qu'une fois que votre somecommandscript affiche les options pour othercommand, les options ne sont en fait que du texte et à la merci de l'analyse standard du shell (affectées par tout ce qui $IFSse passe et quelles options de shell sont en vigueur, ce que vous ne feriez pas dans le cas général contrôler).

Au lieu d'utiliser somecommandpour afficher les options, il serait plus facile, plus sûr et plus robuste de l'utiliser pour appeler othercommand . Le somecommandscénario serait alors un script wrapper autour de la othercommandplace d'une sorte de script d'aide que vous devez vous rappeler d'appeler d'une manière spéciale dans le cadre de la ligne de commande otherscript. Les scripts Wrapper sont un moyen très courant de fournir un outil qui appelle simplement un autre outil similaire avec un autre ensemble d'options (vérifiez simplement avec filequelles commandes /usr/binsont en fait des wrappers de script shell).

Dans bash, kshou zsh, vous pourriez facilement un script wrapper qui utilise un tableau pour contenir les options individuelles othercommandcomme suit:

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

Appelez ensuite othercommand(toujours dans le script wrapper):

othercommand "${options[@]}"

L'extension de "${options[@]}"garantirait que chaque élément du optionstableau est cité individuellement et présenté othercommandcomme des arguments distincts.

L'utilisateur du wrapper serait inconscient du fait qu'il appelle réellement othercommand, ce qui ne serait pas vrai si le script générait simplement les options de ligne de commande pour en othercommandtant que sortie.

Dans /bin/sh, utilisez $@pour maintenir les options:

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

othercommand "$@"

( setEst la commande utilisée pour le réglage des paramètres de position $1, $2, $3etc. Ce sont ce qui fait le tableau $@dans une coquille standard POSIX. La première --est de signaler à setqu'il n'y a pas d' options données, seuls les arguments. Le --est vraiment nécessaire que si la la première valeur se trouve être quelque chose commençant par -).

Notez que ce sont les guillemets autour $@et ${options[@]}cela garantit que les éléments ne sont pas séparés par des mots (et le nom de fichier globbed).

Kusalananda
la source
pourriez-vous expliquer set --?
user1207217
@ user1207217 Ajout d'une explication à la réponse.
Kusalananda
4

Si la somecommandsortie est dans une syntaxe shell fiable, vous pouvez utiliser eval:

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

Mais vous devez être sûr que la sortie a des guillemets valides et autres, sinon vous pourriez également exécuter des commandes en dehors du 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

Notez que cela echo rm bar baz test.shn'a pas été transmis au script (à cause de ;) et a été exécuté comme une commande distincte. J'ai ajouté le |tour $varpour le souligner.


En règle générale, à moins que vous ne puissiez faire entièrement confiance à la sortie de somecommand, il n'est pas possible d'utiliser de manière fiable sa sortie pour créer une chaîne de commande.

Olorin
la source