Come posso impostare una variabile sull'output di un comando in Bash?


1679

Ho una sceneggiatura piuttosto semplice che è simile alla seguente:

#!/bin/bash

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

echo $MOREF

Quando eseguo questo script dalla riga di comando e gli passo gli argomenti, non ottengo alcun output. Tuttavia, quando eseguo i comandi contenuti nella $MOREFvariabile, sono in grado di ottenere output.

Come si possono prendere i risultati di un comando che deve essere eseguito all'interno di uno script, salvarlo in una variabile e quindi emettere quella variabile sullo schermo?



40
A parte ciò, POSIX definisce le variabili in maiuscolo per i nomi delle variabili con significato per il sistema operativo o la shell stessa, mentre i nomi con almeno un carattere minuscolo sono riservati per l'uso dell'applicazione. Quindi, considera l'utilizzo di nomi minuscoli per le tue variabili di shell per evitare conflitti non intenzionali (tenendo presente che l'impostazione di una variabile di shell sovrascriverà qualsiasi variabile di ambiente con nome simile).
Charles Duffy,

1
Come uscita a parte, la cattura in una variabile solo così è possibile allora echola variabile è un uso inutile di echo, ed un uso inutile di variabili.
Tripleee,

1
Inoltre, la memorizzazione dell'output in variabili è spesso superflua. Per stringhe piccole e brevi dovrai fare riferimento più volte nel tuo programma, questo è completamente a posto ed è esattamente la strada da percorrere; ma per l'elaborazione di qualsiasi quantità non banale di dati, si desidera ridisegnare il processo in una pipeline o utilizzare un file temporaneo.
triplo il

Risposte:


2378

Oltre ai backtick `command`, la sostituzione dei comandi può essere eseguita con $(command)o "$(command)", che trovo più facile da leggere, e consente l'annidamento.

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

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

Quoting ( ") è importante per preservare i valori delle variabili multilinea ; è facoltativo sul lato destro di un compito, poiché la suddivisione delle parole non viene eseguita , quindi OUTPUT=$(ls -1)funzionerebbe bene.


59
Possiamo fornire un separatore per l'uscita multi linea?
Aryan,

20
Gli spazi bianchi (o la mancanza di spazi bianchi) sono importanti
Ali

8
@ timhc22, le parentesi graffe sono irrilevanti; sono solo le virgolette che sono importanti per quanto riguarda: se i risultati di espansione sono suddivisi in stringhe ed espansi glob prima di essere passati al echocomando
Charles Duffy,

4
Ah grazie! Quindi c'è qualche vantaggio per le parentesi graffe?
timhc22,

14
Le parentesi graffe possono essere utilizzate quando la variabile è immediatamente seguita da più caratteri che potrebbero essere interpretati come parte del nome della variabile. es ${OUTPUT}foo . Sono inoltre necessari quando si eseguono operazioni di stringa in linea sulla variabile, ad esempio${OUTPUT/foo/bar}
rich remer

282

Il modo giusto è

$(sudo run command)

Se avete intenzione di utilizzare un apostrofo, è necessario `, non è '. Questo personaggio si chiama "backtick" (o "accento grave").

Come questo:

#!/bin/bash

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

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

echo "$MOREF"

31
La sintassi del backtick è obsoleta e in realtà è necessario inserire virgolette doppie nell'interpolazione della variabile in echo.
Tripleee,

10
Vorrei aggiungere che devi stare attento con gli spazi intorno a '=' nell'assegnazione sopra. Non dovresti avere spazi lì, altrimenti otterrai un incarico errato
zbstof

4
Il commento di tripleeee è corretto. In cygwin (maggio 2016), `` non funziona mentre $()funziona. Impossibile risolvere fino a quando non ho visto questa pagina.
toddwz,

2
Elaborazione come un esempio sull'aggiornamento (2018) sarebbe apprezzata.
Eduard,

90

Alcuni trucchi Bash che uso per impostare variabili dai comandi

2a modifica 2018-02-12: aggiunto un altro modo, cerca in fondo per attività di lunga durata !

25/01/2018 Modifica: aggiunta una funzione di esempio (per popolare le variabili sull'uso del disco)

Primo modo semplice, vecchio e compatibile

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

Per lo più compatibile, secondo modo

Poiché la nidificazione potrebbe diventare pesante, è stata implementata la parentesi per questo

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

Campione nidificato:

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

Leggere più di una variabile (con Bashisms )

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

Se voglio solo un valore usato :

array=($(df -k /))

potresti vedere una variabile array :

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]="/")'

Poi:

echo ${array[9]}
529020

Ma preferisco questo:

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

Il primo read foosarà solo saltare riga di intestazione, ma in un solo comando, verrà popolare 7 diverse variabili:

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"

O anche:

{ 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")

... funzionerà anche con array associativi :read foo disk[total] disk[used] ...

Funzione di esempio per popolare alcune variabili:

#!/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 declareriga non è richiesta, solo per leggibilità.

Di sudo cmd | grep ... | cut ...

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

(Per favore, evita inutili cat! Quindi questo è solo un fork in meno:

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

Tutti i tubi ( |) implicano le forcelle. Dove è necessario eseguire un altro processo, accedere al disco, chiamare le librerie e così via.

Quindi, usando sedper esempio, limiterai il sottoprocesso a un solo fork :

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

E con Bashisms :

Ma per molte azioni, principalmente su file di piccole dimensioni, Bash potrebbe fare il lavoro da solo:

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

o

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

Andare oltre sulla suddivisione variabile ...

Dai un'occhiata alla mia risposta a Come divido una stringa su un delimitatore in Bash?

Alternativa: ridurre le forche utilizzando attività di lunga durata in background

2a modifica 2018-02-12:

Al fine di prevenire più forcelle come

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

o

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

Funziona bene, ma eseguire molte forcelle è pesante e lento.

E comandi come datee bcpotrebbero fare molte operazioni, riga per riga !!

Vedere:

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

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

Quindi potremmo usare un lungo processo in background per fare molti lavori, senza dover avviare un nuovo fork per ogni richiesta.

Abbiamo solo bisogno di alcuni descrittori di file e quindici per farlo correttamente:

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

(Certo, FD 5e 6devono essere inutilizzati!) ... Da lì, è possibile utilizzare questo processo da:

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

In una funzione newConnector

Puoi trovare la mia newConnectorfunzione su GitHub.Com o sul mio sito (Nota su GitHub: ci sono due file sul mio sito. La funzione e la demo sono raggruppate in un file che può essere acquistato per l'uso o semplicemente eseguito per la demo.)

Campione:

. 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 funzione myBcconsente di utilizzare l'attività in background con una sintassi semplice e per data:

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

Da lì, se vuoi terminare uno dei processi in background, devi solo chiudere il suo 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

che non è necessario, perché tutti i fd si chiudono al termine del processo principale.


Il campione nidificato sopra è quello che stavo cercando. Potrebbe esserci un modo più semplice, ma quello che stavo cercando era il modo per scoprire se esiste già un container docker dato il suo nome in una variabile di ambiente. Quindi per me: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")era la frase che stavo cercando.
Capricorno

2
@ capricorn1 Questo è un uso inutile diecho ; vuoi semplicementegrep "$CONTAINER_NAME"
tripleee


Probabilmente mi manca qualcosa qui: kubectl get ns | while read -r line; do echo $ line | termine grep | cut -d '' -f1 ; donestampa per ciascuna $lineuna riga vuota e quindi bash: xxxx: command not found. Tuttavia mi aspetterei che venga stampato soloxxx
papanito

77

Come ti hanno già indicato, dovresti usare "backtick".

Anche l'alternativa proposta $(command)funziona, ed è anche più facile da leggere, ma nota che è valida solo con Bash o KornShell (e shell derivate da quelle), quindi se i tuoi script devono essere davvero portabili su vari sistemi Unix, dovresti preferire la vecchia notazione dei backtick


23
Sono apertamente cauti. I backtick sono stati deprecati da POSIX molto tempo fa; la sintassi più moderna dovrebbe essere disponibile nella maggior parte delle shell di questo millennio. (Ci sono ancora ambienti legacy tosse HP-UX tosse che sono bloccati saldamente nei primi anni novanta.)
Tripleee

25
Non corretto. $()è pienamente compatibile con POSIX sh, come standardizzato oltre due decenni fa.
Charles Duffy,

3
Si noti che /bin/shsu Solaris 10 non viene ancora riconosciuto $(…)- e AFAIK è vero anche su Solaris 11.
Jonathan Leffler,

2
@JonathanLeffler In realtà non è più il caso di Solaris 11 dove si /bin/shtrova ksh93.
jlliagre,

2
@tripleee - risposta con tre anni di ritardo :-) ma ho usato $()la shell POSIX su HP-UX negli ultimi 10+ anni.
Bob Jarvis - Ripristina Monica il

54

Conosco tre modi per farlo:

  1. Le funzioni sono adatte a tali compiti: **

    func (){
        ls -l
    }

    Invocalo dicendo func.

  2. Anche un'altra soluzione adatta potrebbe essere la valutazione:

    var="ls -l"
    eval $var
  3. Il terzo utilizza direttamente le variabili:

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

Puoi ottenere l'output della terza soluzione in un buon modo:

echo "$var"

E anche in modo cattivo:

echo $var

1
I primi due non sembrano rispondere alla domanda così com'è attualmente, e il secondo è comunemente ritenuto dubbio.
Tripleee,

1
Come qualcuno che è del tutto nuovo per colpire, perché è "$var"buono e $varcattivo?
Peter,


30

Solo per essere diversi:

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

22

Quando si imposta una variabile, assicurarsi di NON avere spazi prima e / o dopo il segno = . Passò letteralmente un'ora cercando di capirlo, provando tutti i tipi di soluzioni! Questo non è bello.

Corretta:

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

Fallirà con errore "roba: non trovato" o simile

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

2
La versione con lo spazio significa qualcosa di diverso : var=value somecommandfunziona somecommandcon varnel suo ambiente con il valore value. Pertanto, var= somecommandsta esportando varnell'ambiente di somecommandcon un valore vuoto (zero byte).
Charles Duffy,

Sì, un Bash gotcha.
Peter Mortensen,

14

Se vuoi farlo con più comandi / comandi multipli, puoi farlo:

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

O:

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

Esempio:

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

Produzione:

first
second
third

Usando heredoc , puoi semplificare le cose abbastanza facilmente suddividendo il tuo codice a riga singola in un codice multilinea. Un altro esempio:

output="$( ssh -p $port $user@$domain <<EOF
# Breakdown your long ssh command into multiline here.
EOF
)"

2
Qual è il secondo bashnella sostituzione del comando? Stai già creando una subshell con la stessa sostituzione del comando. Se vuoi inserire più comandi, separali semplicemente con una nuova riga o punto e virgola. output=$(echo first; echo second; ...)
Tripleee,

Allo stesso modo 'bash -c "bash -c \"bash -c ...\""'sarebbe anche "diverso"; ma non vedo il punto.
Tripleee,

@tripleee heredoc significa qualcosa di più. Puoi fare lo stesso con alcuni altri comandi come l' ssh sudo -sesecuzione di comandi mysql all'interno, ecc. (Invece di bash)
Jahid

1
Non sento che stiamo comunicando correttamente. Sto sfidando l'utilità sopra variable=$(bash -c 'echo "foo"; echo "bar"')sopra variable=$(echo "foo"; echo "bar")- il documento è qui solo un meccanismo di quotazione e in realtà non aggiunge nulla ad eccezione di un'altra complicazione inutile.
Tripleee,

2
Quando uso heredoc con ssh, preciso il comando da eseguire ssh -p $port $user@$domain /bin/bash <<EOFper evitare Pseudo-terminal will not be allocated because stdin is not a terminal.avvertimenti
F. Hauri,


6

Questo è un altro modo ed è buono da usare con alcuni editor di testo che non sono in grado di evidenziare correttamente ogni codice intricato che crei:

read -r -d '' str < <(cat somefile.txt)
echo "${#str}"
echo "$str"

Ciò non riguarda la domanda di OP, che riguarda in realtà la sostituzione dei comandi , non la sostituzione dei processi .
codeforester

6

È possibile utilizzare apici inversi (noto anche come tombe accento) o $().

Piace:

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

Entrambi hanno lo stesso effetto. Ma OUTPUT = $ (x + 2) è più leggibile e l'ultimo.


2
La parentesi è stata implementata per consentire l'annidamento.
F. Hauri,

5

Se il comando che si sta tentando di eseguire non riesce, scriverebbe l'output sul flusso di errori e verrebbe quindi stampato sulla console.

Per evitarlo, è necessario reindirizzare il flusso di errori:

result=$(ls -l something_that_does_not_exist 2>&1)

4

Alcuni potrebbero trovare questo utile. Valori interi in sostituzione variabile, in cui il trucco utilizza $(())parentesi doppie:

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

1
Ciò non sembra avere alcuna rilevanza per questa domanda. Sarebbe una risposta ragionevole se qualcuno dovesse chiedere come moltiplicare un numero in un array per un fattore costante, anche se non ricordo di aver mai visto qualcuno che lo chiedesse (e quindi un for ((...))loop sembrerebbe una corrispondenza migliore per la variabile loop ). Inoltre, non dovresti usare maiuscole per le tue variabili private.
Tripleee,

Non sono d'accordo con la parte "pertinenza". La domanda dice chiaramente: come impostare una variabile uguale all'output di un comando in Bash? E ho aggiunto questa risposta come complemento perché sono arrivato qui alla ricerca di una soluzione che mi ha aiutato con il codice che ho postato in seguito. Per quanto riguarda le lettere maiuscole, grazie per quello.
Gus,

1
Questo potrebbe essere scritto ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}ma sono d'accordo con @tripleee: non capisco cosa faccia questo, lì!
F. Hauri,

@ F.Hauri ... bash sta diventando sempre più simile al perl, più in profondità ci vai dentro!
roblogic,

4

Ecco altri due modi:

Tieni presente che lo spazio è molto importante in Bash. Pertanto, se si desidera eseguire il comando, utilizzare così com'è senza introdurre altri spazi.

  1. Il seguente assegna harshila Le poi lo stampa

    L=$"harshil"
    echo "$L"
  2. Di seguito viene assegnato l'output del comando tra L2. trviene operato su un'altra variabile, L1.

    L2=$(echo "$L1" | tr [:upper:] [:lower:])

4
1. $"..."probabilmente non fa quello che pensi che faccia . 2. Questo è già stato dato nella risposta di Andy Lester.
gniourf_gniourf,

@gniourf_gniourf ha ragione: vedi la localizzazione bash non funzionerà con le multiline . Ma sotto bash , potresti usare echo ${L1,,}per downcase o echo ${L1^^}upcase.
F. Hauri,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.