Comment stocker une commande dans une variable dans un script shell?

113

Je voudrais stocker une commande à utiliser ultérieurement dans une variable (pas la sortie de la commande, mais la commande elle-même)

J'ai un script simple comme suit:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

Cependant, lorsque j'essaye quelque chose d'un peu plus compliqué, cela échoue. Par exemple, si je fais

command="ls | grep -c '^'";

La sortie est:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

Une idée de la façon dont je pourrais stocker une telle commande (avec des tuyaux / plusieurs commandes) dans une variable pour une utilisation ultérieure?

Benjamin
la source
10
Utilisez une fonction!
gniourf_gniourf

Réponses:

146

Utilisez eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
la source
27
$ (...) est maintenant recommandé au lieu de backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evalest une pratique acceptable uniquement si vous faites confiance au contenu de vos variables. Si vous exécutez, disons, x="ls $name | wc"(ou même x="ls '$name' | wc"), alors ce code est une voie rapide vers les vulnérabilités d'injection ou d'escalade de privilèges si cette variable peut être définie par quelqu'un avec moins de privilèges. (En parcourant tous les sous-répertoires dans /tmp, par exemple? Vous feriez mieux de faire confiance à chaque utilisateur du système pour ne pas en appeler $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/').
Charles Duffy
9
evalest un énorme aimant de bogues qui ne devrait jamais être recommandé sans un avertissement sur le risque de comportement d'analyse inattendu (même sans chaînes malveillantes, comme dans l'exemple de @ CharlesDuffy). Par exemple, essayez x='echo $(( 6 * 7 ))'et ensuite eval $x. Vous pourriez vous attendre à ce que cela imprime "42", mais ce ne sera probablement pas le cas. Pouvez-vous expliquer pourquoi cela ne fonctionne pas? Pouvez-vous expliquer pourquoi j'ai dit «probablement»? Si les réponses à ces questions ne sont pas évidentes pour vous, vous ne devriez jamais y toucher eval.
Gordon Davisson
1
@Student, essayez d'exécuter set -xau préalable pour enregistrer les commandes exécutées, ce qui permettra de voir plus facilement ce qui se passe.
Charles Duffy
1
@Student Je recommande également shellcheck.net pour signaler les erreurs courantes (et les mauvaises habitudes que vous ne devriez pas prendre).
Gordon Davisson
41

Ne pas utiliser eval! Il présente un risque majeur d'introduire une exécution de code arbitraire.

BashFAQ-50 - J'essaie de mettre une commande dans une variable, mais les cas complexes échouent toujours.

Mettez - le dans un tableau et développer tous les mots avec des guillemets doubles "${arr[@]}"pour ne pas laisser IFSdiviser les mots en raison de césure de mots .

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

et voir le contenu du tableau à l'intérieur. Le declare -pvous permet de voir le contenu du tableau à l'intérieur avec chaque paramètre de commande dans des index séparés. Si l'un de ces arguments contient des espaces, les guillemets à l'intérieur lors de l'ajout au tableau l'empêcheront de se diviser en raison de Word-Splitting.

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

et exécutez les commandes comme

"${cmdArgs[@]}"
23:15:18

(ou) utiliser une bashfonction pour exécuter la commande,

cmd() {
   date '+%H:%M:%S'
}

et appelez la fonction comme juste

cmd

POSIX shn'a pas de tableaux, donc le plus proche est de construire une liste d'éléments dans les paramètres de position. Voici une shfaçon POSIX d'exécuter un programme de messagerie

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

Notez que cette approche ne peut gérer que des commandes simples sans redirections. Il ne peut pas gérer les redirections, les pipelines, les boucles for / while, les instructions if, etc.

Un autre cas d'utilisation courant est lors de l'exécution curlavec plusieurs champs d'en-tête et une charge utile. Vous pouvez toujours définir des arguments comme ci-dessous et les invoquer curlsur le contenu du tableau étendu

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

Un autre exemple,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

maintenant que les variables sont définies, utilisez un tableau pour stocker vos arguments de commande

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

et maintenant faire une expansion citée appropriée

curl "${curlCMD[@]}"
Inian
la source
Cela ne fonctionne pas pour moi, j'ai essayé Command=('echo aaa | grep a')et "${Command[@]}", en espérant qu'il exécute littéralement la commande echo aaa | grep a. Ce n'est pas le cas. Je me demande s'il existe un moyen sûr de remplacer eval, mais il semble que chaque solution qui a la même force evalpourrait être dangereuse. N'est-ce pas?
Étudiant
En bref, comment cela fonctionne-t-il si la chaîne d'origine contient un tube «|»?
Étudiant
@Student, si votre chaîne d'origine contient un tube, cette chaîne doit passer par les parties non sécurisées de l'analyseur bash pour être exécutée en tant que code. N'utilisez pas de chaîne dans ce cas; utilisez plutôt une fonction: Command() { echo aaa | grep a; }- après quoi vous pouvez simplement exécuter Command, ou result=$(Command), ou autre.
Charles Duffy
1
@Student, à droite; mais cela échoue intentionnellement , parce que ce que vous demandez de faire est intrinsèquement incertain .
Charles Duffy
1
@Student: J'ai enfin ajouté une note pour mentionner que cela ne fonctionne pas sous certaines conditions
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

En utilisant cette méthode, la commande est immédiatement évaluée et sa valeur de retour est stockée.

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

Idem avec backtick

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

Utiliser eval dans le $(...)ne le fera pas évaluer plus tard

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

En utilisant eval, il est évalué lorsqu'il evalest utilisé

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

Dans l'exemple ci-dessus, si vous devez exécuter une commande avec des arguments, placez-les dans la chaîne que vous stockez

stored_date="date -u"
# ...

Pour les scripts bash, c'est rarement pertinent, mais une dernière remarque. Soyez prudent avec eval. Évaluer uniquement les chaînes que vous contrôlez, jamais les chaînes provenant d'un utilisateur non approuvé ou créées à partir d'une entrée utilisateur non approuvée.

  • Merci à @CharlesDuffy de m'avoir rappelé de citer la commande!
Nate
la source
Cela ne résout pas le problème d'origine où la commande contient un tube «|».
Étudiant
@Nate, notez que cela eval $stored_datepeut être assez bien lorsqu'il stored_datene contient que date, mais eval "$stored_date"c'est beaucoup plus fiable. Exécutez str=$'printf \' * %s\\n\' *'; eval "$str"avec et sans les guillemets autour de la finale "$str"pour un exemple. :)
Charles Duffy
@CharlesDuffy Merci, j'ai oublié de citer. Je parie que mon linter se serait plaint si j'avais pris la peine de l'exécuter.
Nate le
1

Pour bash, stockez votre commande comme ceci:

command="ls | grep -c '^'"

Exécutez votre commande comme ceci:

echo $command | bash
Derek Hazell
la source
1
Pas sûr, mais peut-être que cette façon d'exécuter la commande présente les mêmes risques que l'utilisation de «eval».
Derek Hazell
0

J'ai essayé différentes méthodes:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

Production:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

Comme vous pouvez le voir, seul le troisième a "$@"donné le résultat correct.

mpen
la source
0

Soyez prudent lors de l'enregistrement d'une commande auprès du: X=$(Command)

Celui-ci est toujours exécuté avant même d'être appelé. Pour vérifier et confirmer cela, vous pouvez faire:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Azerty
la source
-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.
Silentexpert
la source
-8

Il n'est pas nécessaire de stocker des commandes dans des variables même si vous devez l'utiliser plus tard. exécutez-le simplement comme d'habitude. Si vous stockez dans une variable, vous auriez besoin d'une sorte d' evalinstruction ou d'appeler un processus shell inutile pour "exécuter votre variable".

Kurumi
la source
1
La commande que je vais stocker dépendra des options que j'envoie, donc au lieu d'avoir des tonnes d'instructions conditionnelles dans la majeure partie de mon programme, il est beaucoup plus facile de stocker la commande dont j'ai besoin pour une utilisation ultérieure.
Benjamin
1
@Benjamin, stockez au moins les options sous forme de variables, et non la commande. par exemplevar='*.txt'; find . -name "$var"
kurumi