Comment définir une variable sur la sortie d'une commande dans Bash?

1679

J'ai un script assez simple qui ressemble à ceci:

#!/bin/bash

VAR1="$1"
MOREF='sudo run command against $VAR1 | grep name | cut -c7-'

echo $MOREF

Lorsque j'exécute ce script à partir de la ligne de commande et lui transmets les arguments, je n'obtiens aucune sortie. Cependant, lorsque j'exécute les commandes contenues dans la $MOREFvariable, je peux obtenir une sortie.

Comment prendre les résultats d'une commande qui doit être exécutée dans un script, l'enregistrer dans une variable, puis afficher cette variable à l'écran?

John
la source
1
Une question connexe stackoverflow.com/questions/25116521/…
Sandeepan Nath
40
Par ailleurs, les variables tout en majuscules sont définies par POSIX pour les noms de variables ayant une signification pour le système d'exploitation ou le shell lui-même, tandis que les noms avec au moins un caractère en minuscule sont réservés à l'utilisation de l'application. Par conséquent, envisagez d'utiliser des noms en minuscules pour vos propres variables shell pour éviter les conflits involontaires (en gardant à l'esprit que la définition d'une variable shell écrasera toute variable d'environnement de même nom).
Charles Duffy
1
En aparté, capturant la sortie dans une variable juste pour que vous pouvez alors echola variable est une utilisation inutile echo, et une utilisation inutile de variables.
tripleee
1
Par ailleurs, le stockage de la sortie dans des variables est souvent inutile. Pour les petites chaînes courtes, vous devrez référencer plusieurs fois dans votre programme, c'est très bien, et c'est exactement la voie à suivre; mais pour traiter toute quantité de données non triviale, vous souhaitez remodeler votre processus dans un pipeline ou utiliser un fichier temporaire.
tripleee

Réponses:

2379

En plus des raccourcis `command`, la substitution de commandes peut être effectuée avec $(command)ou "$(command)", que je trouve plus facile à lire, et permet l'imbrication.

OUTPUT=$(ls -1)
echo "${OUTPUT}"

MULTILINE=$(ls \
   -1)
echo "${MULTILINE}"

Quoting ( ") importe pour conserver les valeurs des variables multilignes ; il est facultatif sur le côté droit d'une affectation, car le fractionnement de mot n'est pas effectué , donc OUTPUT=$(ls -1)cela fonctionnerait bien.

Andy Lester
la source
59
Pouvons-nous fournir un séparateur pour la sortie multi-lignes?
Aryan
20
L'espace blanc (ou le manque d'espace blanc) compte
Ali
8
@ timhc22, les accolades ne sont pas pertinentes; ce ne sont que les guillemets qui sont importants pour savoir si les résultats d'expansion sont divisés en chaînes et étendus en glob avant d'être transmis à la echocommande.
Charles Duffy
4
Ah merci! Y a-t-il donc un avantage pour les accolades?
timhc22
14
Les accolades peuvent être utilisées lorsque la variable est immédiatement suivie par plus de caractères qui pourraient être interprétés comme faisant partie du nom de la variable. par exemple ${OUTPUT}foo . Ils sont également requis lors de l'exécution d'opérations de chaîne en ligne sur la variable, telles que${OUTPUT/foo/bar}
rich remer
284

La bonne façon est

$(sudo run command)

Si vous allez utiliser une apostrophe, vous en avez besoin `, non '. Ce personnage est appelé "backticks" (ou "grave accent").

Comme ça:

#!/bin/bash

VAR1="$1"
VAR2="$2"

MOREF=`sudo run command against "$VAR1" | grep name | cut -c7-`

echo "$MOREF"
Ilya Kogan
la source
31
La syntaxe du backtick est obsolète, et vous devez vraiment mettre des guillemets doubles autour de l'interpolation variable dans le echo.
tripleee
10
J'ajouterais que vous devez faire attention aux espaces autour de '=' dans l'affectation ci-dessus. Vous n'aurez aucun espace là-bas, sinon vous obtiendrez une affectation incorrecte
zbstof
4
Le commentaire de tripleeee est correct. Dans cygwin (mai 2016), `` ne fonctionne pas pendant qu'il $()fonctionne. Impossible de corriger tant que je n'ai pas vu cette page.
toddwz
2
Une élaboration telle qu'un exemple sur Update (2018) serait appréciée.
Eduard
90

Quelques astuces Bash que j'utilise pour définir des variables à partir de commandes

2nd Edit 2018-02-12: Ajouté une manière différente, recherchez en bas de celui-ci pour les tâches de longue durée !

25-01-2018 Édition: ajout d'un exemple de fonction (pour remplir les variables sur l'utilisation du disque)

Première méthode simple, ancienne et compatible

myPi=`echo '4*a(1)' | bc -l`
echo $myPi 
3.14159265358979323844

Principalement compatible, deuxième voie

L'imbrication pouvant devenir lourde, des parenthèses ont été mises en place pour cette

myPi=$(bc -l <<<'4*a(1)')

Échantillon imbriqué:

SysStarted=$(date -d "$(ps ho lstart 1)" +%s)
echo $SysStarted 
1480656334

Lecture de plus d'une variable (avec bashismes )

df -k /
Filesystem     1K-blocks   Used Available Use% Mounted on
/dev/dm-0         999320 529020    401488  57% /

Si je veux juste une valeur utilisée :

array=($(df -k /))

vous pouvez voir une variable de tableau :

declare -p array
declare -a array='([0]="Filesystem" [1]="1K-blocks" [2]="Used" [3]="Available" [
4]="Use%" [5]="Mounted" [6]="on" [7]="/dev/dm-0" [8]="999320" [9]="529020" [10]=
"401488" [11]="57%" [12]="/")'

Alors:

echo ${array[9]}
529020

Mais je préfère ceci:

{ read foo ; read filesystem size using avail prct mountpoint ; } < <(df -k /)
echo $using
529020

La première sauteraread foo simplement la ligne d'en-tête, mais dans une seule commande, vous remplirez 7 variables différentes :

declare -p avail filesystem foo mountpoint prct size using
declare -- avail="401488"
declare -- filesystem="/dev/dm-0"
declare -- foo="Filesystem     1K-blocks   Used Available Use% Mounted on"
declare -- mountpoint="/"
declare -- prct="57%"
declare -- size="999320"
declare -- using="529020"

Ou même:

{ read foo ; read filesystem dsk[{6,2,9}] prct mountpoint ; } < <(df -k /)
declare -p mountpoint dsk
declare -- mountpoint="/"
declare -a dsk=([2]="529020" [6]="999320" [9]="401488")

... fonctionnera également avec les tableaux associatifs :read foo disk[total] disk[used] ...

Exemple de fonction pour remplir certaines variables:

#!/bin/bash

declare free=0 total=0 used=0

getDiskStat() {
    local foo
    {
        read foo
        read foo total used free foo
    } < <(
        df -k ${1:-/}
    )
}

getDiskStat $1
echo $total $used $free

Nota: la declareligne n'est pas requise, juste pour la lisibilité.

À propos de sudo cmd | grep ... | cut ...

shell=$(cat /etc/passwd | grep $USER | cut -d : -f 7)
echo $shell
/bin/bash

(S'il vous plaît éviter inutile cat! Donc, c'est juste une fourche de moins:

shell=$(grep $USER </etc/passwd | cut -d : -f 7)

Tous les tuyaux ( |) impliquent des fourches. Où un autre processus doit être exécuté, accès au disque, aux appels de bibliothèques, etc.

Donc, utiliser sedpour exemple, limitera le sous-processus à une seule fourchette :

shell=$(sed </etc/passwd "s/^$USER:.*://p;d")
echo $shell

Et avec les bashismes :

Mais pour de nombreuses actions, principalement sur de petits fichiers, Bash pourrait faire le travail lui-même:

while IFS=: read -a line ; do
    [ "$line" = "$USER" ] && shell=${line[6]}
  done </etc/passwd
echo $shell
/bin/bash

ou

while IFS=: read loginname encpass uid gid fullname home shell;do
    [ "$loginname" = "$USER" ] && break
  done </etc/passwd
echo $shell $loginname ...

Pour en savoir plus sur le fractionnement variable ...

Jetez un œil à ma réponse à Comment diviser une chaîne sur un délimiteur dans Bash?

Alternative: réduction des fourches en utilisant des tâches de longue durée en arrière-plan

2nd Edit 2018-02-12:

Afin d'éviter plusieurs fourches comme

myPi=$(bc -l <<<'4*a(1)'
myRay=12
myCirc=$(bc -l <<<" 2 * $myPi * $myRay ")

ou

myStarted=$(date -d "$(ps ho lstart 1)" +%s)
mySessStart=$(date -d "$(ps ho lstart $$)" +%s)

Cela fonctionne bien, mais l'exécution de nombreuses fourches est lourde et lente.

Et des commandes comme dateet bcpourraient faire de nombreuses opérations, ligne par ligne !!

Voir:

bc -l <<<$'3*4\n5*6'
12
30

date -f - +%s < <(ps ho lstart 1 $$)
1516030449
1517853288

Nous pourrions donc utiliser un processus d'arrière-plan de longue durée pour effectuer de nombreux travaux, sans avoir à initier un nouveau fork pour chaque demande.

Nous avons juste besoin de descripteurs de fichiers et de fifos pour le faire correctement:

mkfifo /tmp/myFifoForBc
exec 5> >(bc -l >/tmp/myFifoForBc)
exec 6</tmp/myFifoForBc
rm /tmp/myFifoForBc

(Bien sûr, FD 5et 6doivent être inutilisés!) ... À partir de là, vous pouvez utiliser ce processus en:

echo "3*4" >&5
read -u 6 foo
echo $foo
12

echo >&5 "pi=4*a(1)"
echo >&5 "2*pi*12"
read -u 6 foo
echo $foo
75.39822368615503772256

Dans une fonction newConnector

Vous pouvez trouver ma newConnectorfonction sur GitHub.Com ou sur mon propre site (Remarque sur GitHub: il y a deux fichiers sur mon site. La fonction et la démo sont regroupées dans un seul fichier qui pourrait être recherché pour être utilisé ou simplement exécuté pour la démo.)

Échantillon:

. shell_connector.sh

tty
/dev/pts/20

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30745 pts/20   R+     0:00  \_ ps --tty pts/20 fw

newConnector /usr/bin/bc "-l" '3*4' 12

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  30952 pts/20   R+     0:00  \_ ps --tty pts/20 fw

declare -p PI
bash: declare: PI: not found

myBc '4*a(1)' PI
declare -p PI
declare -- PI="3.14159265358979323844"

La fonction myBcvous permet d'utiliser la tâche d'arrière-plan avec une syntaxe simple et pour la date:

newConnector /bin/date '-f - +%s' @0 0
myDate '2000-01-01'
  946681200
myDate "$(ps ho lstart 1)" boottime
myDate now now ; read utm idl </proc/uptime
myBc "$now-$boottime" uptime
printf "%s\n" ${utm%%.*} $uptime
  42134906
  42134906

ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
  29019 pts/20   Ss     0:00 bash
  30944 pts/20   S      0:00  \_ /usr/bin/bc -l
  32615 pts/20   S      0:00  \_ /bin/date -f - +%s
   3162 pts/20   R+     0:00  \_ ps --tty pts/20 fw

De là, si vous souhaitez mettre fin à l'un des processus d'arrière-plan, il vous suffit de fermer son fd :

eval "exec $DATEOUT>&-"
eval "exec $DATEIN>&-"
ps --tty pts/20 fw
    PID TTY      STAT   TIME COMMAND
   4936 pts/20   Ss     0:00 bash
   5256 pts/20   S      0:00  \_ /usr/bin/bc -l
   6358 pts/20   R+     0:00  \_ ps --tty pts/20 fw

ce qui n'est pas nécessaire, car tous les fd se ferment à la fin du processus principal.

F. Hauri
la source
L'échantillon imbriqué ci-dessus est ce que je cherchais. Il peut y avoir un moyen plus simple, mais ce que je cherchais était le moyen de savoir si un conteneur docker existe déjà étant donné son nom dans une variable d'environnement. Donc pour moi: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")était la déclaration que je cherchais.
Capricorn1
2
@ capricorn1 C'est une utilisation inutile deecho ; vous voulez simplementgrep "$CONTAINER_NAME"
tripleee
Je manque probablement quelque chose ici: kubectl get ns | while read -r line; do echo $ line | terme grep | cut -d '' -f1 ; doneimprime pour chacun $lineune ligne vide puis bash: xxxx: command not found. Cependant, je m'attends à ce qu'il s'imprime justexxx
papanito
77

Comme ils vous l'ont déjà indiqué, vous devez utiliser des «backticks».

L'alternative proposée $(command)fonctionne également, et elle est également plus facile à lire, mais notez qu'elle n'est valable qu'avec Bash ou KornShell (et les shells dérivés de ceux-ci), donc si vos scripts doivent être vraiment portables sur divers systèmes Unix, vous devriez préférer l'ancienne notation backticks.

bitwelder
la source
23
Ils sont ouvertement prudents. Les backticks sont déconseillés par POSIX depuis longtemps; la syntaxe la plus moderne devrait être disponible dans la plupart des shells de ce millénaire. (Il y a encore des environnements existants contre la toux HP-UX toux qui sont fermement coincé au début des années nonante.)
tripleee
25
Incorrect. $()est entièrement compatible avec POSIX sh, tel qu'il a été standardisé il y a plus de deux décennies.
Charles Duffy
3
Notez que /bin/shsur Solaris 10 ne reconnaît toujours pas $(…)- et AFAIK c'est vrai sur Solaris 11 aussi.
Jonathan Leffler
2
@JonathanLeffler Ce n'est en fait plus le cas avec Solaris 11 où se /bin/shtrouve ksh93.
jlliagre
2
@tripleee - réponse avec trois ans de retard :-) mais je l'ai utilisé $()dans le shell POSIX sur HP-UX depuis plus de 10 ans.
Bob Jarvis - Rétablir Monica
54

Je connais trois façons de le faire:

  1. Les fonctions conviennent à de telles tâches: **

    func (){
        ls -l
    }

    Invoquez-le en disant func.

  2. Une autre solution appropriée pourrait également être évaluée:

    var="ls -l"
    eval $var
  3. Le troisième utilise directement des variables:

    var=$(ls -l)
    
        OR
    
    var=`ls -l`

Vous pouvez obtenir la sortie de la troisième solution dans le bon sens:

echo "$var"

Et aussi de façon désagréable:

echo $var
MLSC
la source
1
Les deux premiers ne semblent pas répondre à la question telle qu'elle est actuellement, et le second est généralement considéré comme douteux.
tripleee
1
En tant que personne qui est entièrement nouvelle à bash, pourquoi est-elle "$var"bonne et $varméchante?
Peter
30

Juste pour être différent:

MOREF=$(sudo run command against $VAR1 | grep name | cut -c7-)
DigitalRoss
la source
22

Lorsque vous définissez une variable, assurez-vous de n'avoir AUCUN espace avant et / ou après le signe = . J'ai littéralement passé une heure à essayer de comprendre cela, à essayer toutes sortes de solutions! Ce n'est pas cool.

Correct:

WTFF=`echo "stuff"`
echo "Example: $WTFF"

Échouera avec l'erreur "stuff: not found" ou similaire

WTFF= `echo "stuff"`
echo "Example: $WTFF"
Emil
la source
2
La version avec l'espace signifie quelque chose de différent : var=value somecommandfonctionne somecommandavec vardans son environnement ayant la valeur value. Ainsi, var= somecommandexporte vardans l'environnement de somecommandavec une valeur vide (zéro octet).
Charles Duffy
Oui, un Bash gotcha.
Peter Mortensen
14

Si vous voulez le faire avec une ou plusieurs commandes multilignes / multiples, vous pouvez le faire:

output=$( bash <<EOF
# Multiline/multiple command/s
EOF
)

Ou:

output=$(
# Multiline/multiple command/s
)

Exemple:

#!/bin/bash
output="$( bash <<EOF
echo first
echo second
echo third
EOF
)"
echo "$output"

Production:

first
second
third

En utilisant heredoc , vous pouvez simplifier les choses assez facilement en décomposant votre long code de ligne unique en un code multiligne. Un autre exemple:

output="$( ssh -p $port $user@$domain <<EOF
# Breakdown your long ssh command into multiline here.
EOF
)"
Jahid
la source
2
Quel est le deuxième bashà l'intérieur de la substitution de commande? Vous créez déjà un sous-shell par la substitution de commande elle-même. Si vous souhaitez placer plusieurs commandes, séparez-les simplement par un saut de ligne ou un point-virgule. output=$(echo first; echo second; ...)
tripleee
De même, ce 'bash -c "bash -c \"bash -c ...\""'serait également «différent»; mais je ne vois pas l'intérêt de cela.
tripleee
@tripleee heredoc signifie quelque chose de plus que cela. Vous pouvez faire la même chose avec d'autres commandes comme ssh sudo -sexécuter des commandes mysql à l'intérieur, etc. (au lieu de bash)
Jahid
1
Je ne pense pas que nous communiquions correctement. Je conteste l'utilité à plusieurs variable=$(bash -c 'echo "foo"; echo "bar"')reprises variable=$(echo "foo"; echo "bar")- le document ici n'est qu'un mécanisme de citation et n'ajoute pas vraiment autre chose qu'une autre complication inutile.
tripleee
2
Lorsque j'utilise heredoc avec ssh, je précise la commande à exécuter ssh -p $port $user@$domain /bin/bash <<EOFafin d'éviter les Pseudo-terminal will not be allocated because stdin is not a terminal.avertissements
F. Hauri
9

Vous devez utiliser soit

$(command-here)

ou

`command-here`

Exemple

#!/bin/bash

VAR1="$1"
VAR2="$2"

MOREF="$(sudo run command against "$VAR1" | grep name | cut -c7-)"

echo "$MOREF"
Diego Velez
la source
2
$()est bien meilleur que les backticks. Voir: Quel est l'avantage d'utiliser $ () au lieu de backticks dans les scripts shell?
codeforester le
1
Je ne savais pas que vous pouviez imbriquer mais c'est parfaitement logique, merci beaucoup pour l'info!
Diego Velez
6

C'est une autre façon et est utile à utiliser avec certains éditeurs de texte qui ne peuvent pas mettre correctement en surbrillance chaque code complexe que vous créez:

read -r -d '' str < <(cat somefile.txt)
echo "${#str}"
echo "$str"
Puissance du Verseau
la source
Cela ne traite pas de la question d'OP, qui concerne vraiment la substitution de commandes , pas la substitution de processus .
codeforester
6

Vous pouvez utiliser des backticks (également appelés tombes d'accent) ou $().

Comme:

OUTPUT=$(x+2);
OUTPUT=`x+2`;

Les deux ont le même effet. Mais OUTPUT = $ (x + 2) est plus lisible et le dernier.

Pratik Patil
la source
2
Des parenthèses ont été mises en place afin de permettre l'imbrication.
F.Hauri
5

Si la commande que vous essayez d'exécuter échoue, elle écrira la sortie sur le flux d'erreur et sera ensuite imprimée sur la console.

Pour l'éviter, vous devez rediriger le flux d'erreur:

result=$(ls -l something_that_does_not_exist 2>&1)
cafebabe1991
la source
4

Certains peuvent trouver cela utile. Valeurs entières dans la substitution de variable, où l'astuce consiste à utiliser $(())des crochets doubles:

N=3
M=3
COUNT=$N-1
ARR[0]=3
ARR[1]=2
ARR[2]=4
ARR[3]=1

while (( COUNT < ${#ARR[@]} ))
do
  ARR[$COUNT]=$((ARR[COUNT]*M))
  (( COUNT=$COUNT+$N ))
done
Gus
la source
1
Cela ne semble pas avoir de pertinence pour cette question. Ce serait une réponse raisonnable si quelqu'un demandait comment multiplier un nombre dans un tableau par un facteur constant, bien que je ne me souvienne pas avoir vu quelqu'un demander cela (et alors une for ((...))boucle semblerait être une meilleure correspondance pour la variable de boucle ). En outre, vous ne devez pas utiliser de majuscules pour vos variables privées.
tripleee
Je ne suis pas d'accord avec la partie "pertinence". La question se lit clairement: comment définir une variable égale à la sortie d'une commande dans Bash? Et j'ai ajouté cette réponse en complément car je suis arrivé ici à la recherche d'une solution qui m'a aidé avec le code que j'ai posté plus tard. Concernant les vars majuscules, merci pour cela.
Gus
1
Cela pourrait être écrit ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}mais je suis d'accord avec @tripleee: je ne comprends pas quoi faire, là!
F.Hauri
@ F.Hauri ... bash devient de plus en plus comme perl plus vous y allez!
roblogic
4

Voici deux autres façons:

Veuillez garder à l'esprit que l'espace est très important dans Bash. Donc, si vous voulez que votre commande s'exécute, utilisez-la telle quelle sans introduire plus d'espace.

  1. Cédant suivant harshilà Lpuis imprime

    L=$"harshil"
    echo "$L"
  2. Ce qui suit affecte la sortie de la commande trà L2. trest opéré sur une autre variable, L1.

    L2=$(echo "$L1" | tr [:upper:] [:lower:])
Harshil
la source
4
1. $"..."ne fait probablement pas ce que vous pensez qu'il fait . 2. Ceci est déjà donné dans la réponse d'Andy Lester.
gniourf_gniourf
@gniourf_gniourf a raison: voir la localisation bash ne fonctionnera pas avec les multilignes . Mais sous bash , vous pouvez utiliser echo ${L1,,}pour downcase ou echo ${L1^^}upcase.
F.Hauri