Bash: Commande de passage avec des parameters cités pour fonctionner

J'ai la fonction bash suivante:

function exe { echo -e "Execute: $1" # Loops every 3s, outputting '...' until command finished executing LOOP=0 while true; do if ! [ $LOOP == 0 ]; then echo -e "..."; fi; sleep 3; LOOP=$LOOP+1 done & ERROR="$($2 2>&1)" # Execute the command and capture output to variable status=$? kill $!; trap 'kill $!' SIGTERM if [ $status -ne 0 ]; then echo -e "✖ Error" >&2 echo -e "$ERROR" >&2 else echo -e "✔ Success" fi return $status } 

L'intention est de l'appeler comme suit:

 exe "Update apt indexes" \ "sudo apt-get update" 

Quelles sorties:

 Execute: Update apt indexes ... ... ... ... ✔ Success 

Cela fonctionne correctement sauf si une string citée est utilisée comme paramètre dans la command passée.

Par exemple, ce qui suit ne fonctionne pas:

 exe "Create self signed certificatee" \ "sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\"" 

set -x révèle que la command ci-dessus est transformée pour l'exécution suivante:

 sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '"/C=GB/ST=London/L=London/O=Confetti' Celebrations Ltd/OU=IT 'Department/CN=dev.sign-in.confetti.local"' 

Ce qui semble avoir pris un certain nombre de guillemets simples et rend la command inefficace.

Je voudrais une version qui n'a pas cette limitation. Des idées?

===============

Mon code final, après les suggestions et quelques autres corrections de bugs est:

 exe () { echo -e "Execute: $1" LOOP=0 while true; do if ! [ $LOOP == 0 ]; then echo -e "..."; fi; sleep 3; LOOP=$((LOOP+1)) done & ERROR=$("${@:2}" 2>&1) status=$? kill $!; trap 'kill $!' SIGTERM if [ $status -ne 0 ]; then echo -e "✖ Error" >&2 echo -e "$ERROR" >&2 else echo -e "✔ Success" fi return $status } 

La fonction est conçue comme un «embellisseur» pour un script shell de provisioning vagrant et peut être appelée comme

 exe "Update apt indexes" sudo apt-get update 

La sortie apparaît comme

 Execute: Update apt indexes ... ... ... ... ✔ Success 

Les commands qui durent less de 3 secondes ne voient aucune sortie de points de progression

À less qu'il y ait une erreur, vous obtiendrez un état d'erreur et la sortie complète de la command.

L'intention principale est de débarrasser les scripts d'approvisionnement vagrants des lignes rouges affichées lors de la sortie des messages vers stderr. De nombreuses commands fournissent correctement des informations à stderr car elles sont destinées à des messages qui ne doivent pas être apathés vers d'autres commands. Les messages d'printing vagrant à stdout car Cela laisse un certain nombre de messages de provisioning qui ressemblent à des erreurs mais qui ne le sont pas.

Cette fonction n'imprime pas à stderr sauf si la command exécutée renvoie un état non nul. Cela signifie que sauf si une command indique un échec, vous ne verrez pas de messages rouges. Quand une command indiquait un échec avec un message non nul, nous envoyons la sortie complète de la command à stderr, donnant des lignes rouges.

Le provisionnement vagrant avec un script shell beaucoup plus soigné et signifie que nous pouvons vraiment regarder les messages rouges sachant qu'ils signifient quelque chose.

La fonction complète pour une utilisation avec vagrant, y compris un peu de fluff visuel que j'ai laissé de côté dans les extraits ci-dessus, peut être vu ici: https://gist.github.com/michaelward82/c1903f2b37a76975740e

Exemple de sortie utilisant la fonction exe, sans erreur: Exemple de sortie utilisant la fonction exe, avec des erreurs

Exemple de sortie utilisant la fonction exe, avec erreur: Exemple de sortie utilisant la fonction exe, avec erreur

Sortie par défaut de l'exécution directe des commands, pas d'erreurs réelles: Sortie par défaut de l'exécution directe des commandes, pas d'erreurs réelles

Vous ne voulez peut-être pas passer la command entière sous la forme d'une string. Nous avons des lists en shell, comme list d'arguments, et passer une list sous forme de list est beaucoup plus simple.

Au lieu d'écrire exe blah "blahh cmd" , vous écrivez la command directement comme exe blah blahh cmd . Ensuite, lorsque vous avez besoin d'utiliser toute la command directement, utilisez l' extension de découpage pour get tout ce qui suit le premier argument: ERROR=$("${@:1}" 2>&1) .

Traditionnellement, les gens peuvent utiliser shift pour déplacer la list entière des arguments «left» (voir help shift ):

 f(){ local j="$1" shift echo "$j,$3" shift 50 echo "$1" # guess what "$@" is now? } f {1..100} 

Mais ce n'est pas nécessaire pour bash apparantly.

En parlant de la chose de tranchage, vous pouvez également vérifier les arrays dans bash.


Ugh encore .. Vous pouvez utiliser eval pour exécuter une string directement, mais cela est souvent considéré comme une mauvaise chose, puisque vous autorisez alors beaucoup plus que de simples commands.


Et comme indice de style, préférez le plus court et plus (POSIX-) portable xxx() sur la function xxx et la function xxx() . En bash ils sont juste identiques.

Le problème principal de votre question est dans "comment split une string" à l'intérieur d'un $var .

Le "mal" (parce qu'il est sujet aux erreurs et à l'exécution de code) est d'utiliser eval:

  eval set -- $var ### Dangerous, not recommended, do not use. 

Cela définit la string divisée dans les arguments positionnels (un tableau est un peu plus complexe). Mais que la variable $var soit pas cotée (quelque chose à éviter à tout prix, à less que vous ne sachiez vraiment ce que vous faites), elle est sujette à la "division des mots" (ce que nous voulons). Vous pouvez essayer ces commands (utilisez un directory avec quelques files)

 $ var='hello * world' $ eval set -- $var $ echo "$@" 

Il est sûr de s'exécuter, il n'y a pas de valeur externe et l'expansion de * va juste définir les valeurs dans les parameters de position.

Pour éviter "l'expansion du nom de path", un set -f est utilisé, et dans ce cas, il est facile à intégrer dans la command:

 $ var='hello * world' $ set -f $ eval set -- $var $ echo "$@" hello * world 

C'est avec un IFS par défaut de l' espace Tab Nouvelle ligne .

Les choses peuvent devenir complexes si IFS aurait pu être défini en externe.

Plusieurs problèmes pourraient être résolus en utilisant read :

 $ IFS=' ' read -ra arr <<<"$var" $ echo "${arr[@]}" hello * world 

Cela définit IFS pour la command (évite de définir IFS en externe), lit sans traitement backslash (l'option -r), met tout à l'intérieur d'une variable tableau (l'option -a) et utilise la variable "$var" . La seule mise en garde est que les espaces répétés entre les mots seront effacés (du fait que l'IFS est un espace). Ce n'est pas un problème pour une command line exécutable.

Mais essayer d'exécuter des commands qui ont besoin d'arguments avec des espaces échouera:

 $ var='date -d "-1 day" +"%Y.%m.%d-%H:%M:%S"' $ IFS=' ' read -ra arr <<<"$var" $ "${arr[@]}" date: extra operand `+"%Y.%m.%d-%H:%M:%S"' 

La seule vraie solution est que vous construisez correctement le tableau de la command depuis le début:

 $ arr=( date -d "-1 day" +"%Y.%m.%d-%H:%M:%S" ) $ "${arr[@]}" 2016.03.05-00:25:17 

Pensez à cette solution en tant que CSV "Valeurs séparées par des virgules".

Ce script va fonctionner:

 #!/bin/bash function exe { echo "Execute: $1" # Loops every 3s, outputting '...' until command finished executing LOOP=0 while true; do if [ $LOOP -gt 0 ]; then echo -e "..."; fi; sleep 3; (( LOOP++ )) done & ERROR="$("${@:2}" 2>&1)" # Execute command and capture output. status=$? kill $!; trap 'kill $!' SIGTERM if [ $status -ne 0 ]; then echo "✖ Error" >&2 echo "$ERROR" >&2 else echo "✔ Success" fi return $status } cmd=( date -d '-1 day' +'%Y.%m.%d-%H:%M:%S' ) exe "give me yesterday date" "${cmd[@]}" cmd=( sudo apt-get update ) exe "update package list" "${cmd[@]}" 

Dans le cas de guillemets dans les strings de parameters à exécuter en tant que code, il est possible de reparer la string de parameters dans un tableau tel que le tableau de parameters de position $@ . Cela peut être obtenu – au less pour l'exemple donné – en utilisant ... & ERROR="$( printf "%s" "$2" | xargs sh -c 'exec "$0" "$@" 2>&1' ) ... (Il y a des cas avec des guillemets doubles supplémentaires dans cette string déjà citée qui peuvent causer des xargs: unterminated quote messages de xargs: unterminated quote ).

Pour quelques suggestions supplémentaires s'il vous plaît voir: Linux / Bash: Comment décountr? .

 # test cases # help : #set -- '' "ls -ld / 'a bc'" set -- '' ": sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj \"/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local\"" printf "%s" "$2" | xargs sh -c ' echo "arg 0: ${0}" for ((i=1; i<=$#; i++)); do echo "arg $i: ${@:i:1}" done set -xv "$0" "$@" ' # output arg 0: : arg 1: sudo arg 2: openssl arg 3: req arg 4: -x509 arg 5: -nodes arg 6: -days arg 7: 365 arg 8: -newkey arg 9: rsa:2048 arg 10: -keyout arg 11: /etc/apache2/ssl/apache.key arg 12: -out arg 13: /etc/apache2/ssl/apache.crt arg 14: -subj arg 15: /C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local "$0" "$@" + : sudo openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout /etc/apache2/ssl/apache.key -out /etc/apache2/ssl/apache.crt -subj '/C=GB/ST=London/L=London/O=Company Ltd/OU=IT Department/CN=dev.domain.local' 

(Et LOOP=$LOOP+1 dans votre code ci-dessus devrait être LOOP=$((LOOP+1)) btw.)