Wie setze ich eine Variable auf die Ausgabe eines Befehls in Bash?

1676

Ich habe ein ziemlich einfaches Skript, das ungefähr so ​​aussieht:

#!/bin/bash

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

echo $MOREF

Wenn ich dieses Skript über die Befehlszeile ausführe und ihm die Argumente übergebe, erhalte ich keine Ausgabe. Wenn ich jedoch die in der $MOREFVariablen enthaltenen Befehle ausführe , kann ich eine Ausgabe erhalten.

Wie kann man die Ergebnisse eines Befehls, der in einem Skript ausgeführt werden muss, in einer Variablen speichern und diese Variable dann auf dem Bildschirm ausgeben?

John
quelle
1
Eine verwandte Frage stackoverflow.com/questions/25116521/…
Sandeepan Nath
40
Abgesehen davon werden All-Caps-Variablen von POSIX für Variablennamen definiert, die für das Betriebssystem oder die Shell selbst von Bedeutung sind, während Namen mit mindestens einem Kleinbuchstaben für die Verwendung durch Anwendungen reserviert sind. Verwenden Sie daher Kleinbuchstaben für Ihre eigenen Shell-Variablen, um unbeabsichtigte Konflikte zu vermeiden (beachten Sie, dass beim Festlegen einer Shell-Variablen alle gleichnamigen Umgebungsvariablen überschrieben werden).
Charles Duffy
1
Als beiseite, in eine Variable Erfassung Ausgang nur so können Sie dann echoist die Variable eine nutzlose Verwendung echo, und eine nutzlose Verwendung von Variablen.
Tripleee
1
Außerdem ist das Speichern der Ausgabe in Variablen häufig nicht erforderlich. Für kleine, kurze Zeichenfolgen müssen Sie in Ihrem Programm mehrmals referenzieren. Dies ist völlig in Ordnung und genau der richtige Weg. Für die Verarbeitung nicht trivialer Datenmengen möchten Sie Ihren Prozess jedoch in eine Pipeline umformen oder eine temporäre Datei verwenden.
Tripleee

Antworten:

2376

Zusätzlich zu Backticks `command`kann die Befehlssubstitution mit $(command)oder erfolgen "$(command)", was meiner Meinung nach leichter zu lesen ist und das Verschachteln ermöglicht.

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

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

Quoting ( ") ist wichtig, um mehrzeilige Variablenwerte beizubehalten . Es ist auf der rechten Seite einer Zuweisung optional, da keine Wortaufteilung durchgeführt wird und daher OUTPUT=$(ls -1)gut funktioniert.

Andy Lester
quelle
59
Können wir ein Trennzeichen für die Ausgabe mit mehreren Zeilen bereitstellen?
Aryan
20
Leerraum (oder Mangel an Leerzeichen) ist wichtig
Ali
8
@ timhc22, die geschweiften Klammern sind irrelevant; Es sind nur die Anführungszeichen, die wichtig sind, ob die Erweiterungsergebnisse vor der Übergabe an den echoBefehl durch Zeichenfolgen aufgeteilt und durch Glob erweitert werden .
Charles Duffy
4
Ah danke! Gibt es also einen Vorteil für die geschweiften Klammern?
Timhc22
14
Geschweifte Klammern können verwendet werden, wenn auf die Variable unmittelbar weitere Zeichen folgen, die als Teil des Variablennamens interpretiert werden können. zB ${OUTPUT}foo . Sie sind auch erforderlich, wenn Inline-String-Operationen für die Variable ausgeführt werden, z. B.${OUTPUT/foo/bar}
Rich Remer
282

Der richtige Weg ist

$(sudo run command)

Wenn Sie ein Apostroph verwenden möchten, brauchen Sie es `nicht '. Dieses Zeichen wird "Backticks" (oder "schwerwiegender Akzent") genannt.

So was:

#!/bin/bash

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

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

echo "$MOREF"
Ilya Kogan
quelle
31
Die Backtick-Syntax ist veraltet, und Sie müssen wirklich doppelte Anführungszeichen um die Variableninterpolation in der setzen echo.
Tripleee
10
Ich würde hinzufügen, dass Sie mit den Leerzeichen um '=' in der obigen Aufgabe vorsichtig sein müssen. Sie sollten dort keine Leerzeichen haben, sonst erhalten Sie eine falsche Zuordnung
zbstof
4
Der Kommentar von Tripleeee ist korrekt. In Cygwin (Mai 2016) funktioniert `` nicht, während es $()funktioniert. Konnte nicht behoben werden, bis ich diese Seite sah.
Toddwz
2
Eine Ausarbeitung wie ein Beispiel für Update (2018) wäre willkommen.
Eduard
90

Einige Bash- Tricks, mit denen ich Variablen aus Befehlen setze

2nd Edit 2018-02-12: Eine andere Möglichkeit hinzugefügt, suchen Sie unten nach langfristigen Aufgaben !

2018-01-25 Bearbeiten: Eine Beispielfunktion wurde hinzugefügt (zum Auffüllen von Variablen zur Festplattennutzung).

Erster einfacher, alter und kompatibler Weg

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

Meistens kompatibel, zweiter Weg

Da die Verschachtelung schwer werden könnte, wurde hierfür eine Klammer implementiert

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

Verschachtelte Probe:

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

Lesen von mehr als einer Variablen (mit Bashisms )

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

Wenn ich nur einen verwendeten Wert möchte :

array=($(df -k /))

Sie konnten eine Array- Variable sehen:

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

Dann:

echo ${array[9]}
529020

Aber ich bevorzuge das:

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

Die erste überspringtread foo nur die Kopfzeile, aber in nur einem Befehl werden 7 verschiedene Variablen ausgefüllt:

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"

Oder auch:

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

... funktioniert auch mit assoziativen Arrays :read foo disk[total] disk[used] ...

Beispielfunktion zum Auffüllen einiger Variablen:

#!/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

Hinweis: declareZeile ist nicht erforderlich, nur zur besseren Lesbarkeit.

Über sudo cmd | grep ... | cut ...

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

(Bitte vermeiden Sie nutzlos cat! Das ist also nur eine Gabel weniger:

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

Alle Rohre ( |) implizieren Gabeln. Wo ein anderer Prozess ausgeführt werden muss, Zugriff auf Festplatte, Bibliotheksaufrufe usw.

Wenn Sie also sedfür sample verwenden, wird der Unterprozess auf nur eine Gabel beschränkt :

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

Und mit Bashisms :

Aber für viele Aktionen, hauptsächlich für kleine Dateien, könnte Bash die Arbeit selbst erledigen:

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

oder

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

Weiter über die variable Aufteilung ...

Schauen Sie sich meine Antwort auf Wie teile ich eine Zeichenfolge auf einem Trennzeichen in Bash?

Alternative: Reduzieren von Gabeln durch Verwendung von Hintergrundaufgaben mit langer Laufzeit

2. Änderung 2018-02-12:

Um mehrere Gabeln wie zu verhindern

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

oder

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

Dies funktioniert gut, aber viele Gabeln laufen zu lassen ist schwer und langsam.

Und Befehle mögen dateund bckönnten viele Operationen machen, Zeile für Zeile !!

Sehen:

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

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

Wir könnten also einen lang laufenden Hintergrundprozess verwenden, um viele Jobs zu erledigen, ohne für jede Anforderung einen neuen Fork initiieren zu müssen.

Wir brauchen nur einige Dateideskriptoren und Fifos, um dies richtig zu machen:

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

(Natürlich, FD 5und 6müssen nicht verwendet werden!) ... Von dort aus können Sie diesen Prozess verwenden, indem Sie:

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 eine Funktion newConnector

Sie finden meine newConnectorFunktion möglicherweise auf GitHub.Com oder auf meiner eigenen Site (Hinweis auf GitHub: Auf meiner Site befinden sich zwei Dateien. Funktion und Demo sind in einer Datei zusammengefasst, die zur Verwendung bezogen oder nur für die Demo ausgeführt werden kann.)

Stichprobe:

. 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"

Mit dieser Funktion myBckönnen Sie die Hintergrundaufgabe mit einfacher Syntax und für Datum verwenden:

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

Wenn Sie von dort aus einen der Hintergrundprozesse beenden möchten, müssen Sie nur die fd schließen :

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

was nicht benötigt wird, da alle fd schließen, wenn der Hauptprozess abgeschlossen ist.

F. Hauri
quelle
Das verschachtelte Beispiel oben ist das, wonach ich gesucht habe. Es mag einen einfacheren Weg geben, aber ich habe nach dem Weg gesucht, um herauszufinden, ob ein Docker-Container bereits vorhanden ist, dessen Name in einer Umgebungsvariablen angegeben ist. Also für mich: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")war die Aussage, nach der ich gesucht habe.
Steinbock1
2
@ capricorn1 Das ist eine nutzlose Verwendung vonecho ; Sie wollen einfachgrep "$CONTAINER_NAME"
Tripleee
Ich vermisse hier wahrscheinlich etwas: kubectl get ns | while read -r line; do echo $ line | grep Begriff | cut -d '' -f1 ; donedruckt für jede $lineeine leere Zeile aus und dann bash: xxxx: command not found. Ich würde jedoch erwarten, dass es nur gedruckt wirdxxx
papanito
77

Wie sie Ihnen bereits angedeutet haben, sollten Sie "Backticks" verwenden.

Die vorgeschlagene Alternative $(command)funktioniert ebenfalls und ist auch leichter zu lesen. Beachten Sie jedoch, dass sie nur mit Bash oder KornShell (und von diesen abgeleiteten Shells) gültig ist. Wenn Ihre Skripte also auf verschiedenen Unix-Systemen wirklich portabel sein müssen, sollten Sie dies vorziehen die alte Backticks-Notation.

bitwelder
quelle
23
Sie sind offen vorsichtig. Backticks wurden von POSIX vor langer Zeit abgelehnt. Die modernere Syntax sollte in den meisten Shells dieses Jahrtausends verfügbar sein. (Es gibt immer noch Legacy-Umgebungen Husten HP-UX Husten, die in den frühen neunziger Jahren fest stecken.)
Tripleee
25
Falsch. $()ist voll kompatibel mit POSIX sh, wie vor über zwei Jahrzehnten standardisiert.
Charles Duffy
3
Beachten Sie, dass /bin/shSolaris 10 dies immer noch nicht erkennt $(…)- und AFAIK gilt auch für Solaris 11.
Jonathan Leffler
2
@JonathanLeffler es tatsächlich nicht mehr der Fall mit Solaris 11 ist , wo /bin/shist ksh93.
Jlliagre
2
@tripleee - Antwort drei Jahre zu spät :-) aber ich habe $()in den letzten 10+ Jahren in der POSIX-Shell auf HP-UX verwendet.
Bob Jarvis - Wiedereinsetzung Monica
54

Ich kenne drei Möglichkeiten:

  1. Funktionen sind für solche Aufgaben geeignet: **

    func (){
        ls -l
    }

    Rufen Sie es auf, indem Sie sagen func.

  2. Auch eine andere geeignete Lösung könnte eval sein:

    var="ls -l"
    eval $var
  3. Der dritte verwendet Variablen direkt:

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

Sie können die Ausgabe der dritten Lösung auf gute Weise erhalten:

echo "$var"

Und auch auf böse Weise:

echo $var
MLSC
quelle
1
Die ersten beiden scheinen die derzeitige Frage nicht zu beantworten, und die zweite wird allgemein als zweifelhaft angesehen.
Tripleee
1
Warum ist jemand, der völlig neu im Bash ist, "$var"gut und $varböse?
Peter
30

Nur um anders zu sein:

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

Stellen Sie beim Festlegen einer Variablen sicher, dass Sie vor und / oder nach dem = -Zeichen KEINE Leerzeichen haben . Ich habe buchstäblich eine Stunde damit verbracht, dies herauszufinden und alle Arten von Lösungen auszuprobieren! Das ist nicht cool.

Richtig:

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

Wird mit dem Fehler "stuff: not found" oder ähnlichem fehlschlagen

WTFF= `echo "stuff"`
echo "Example: $WTFF"
Emil
quelle
2
Die Version mit dem Raum etwas anderes bedeutet : var=value somecommandLäufe somecommandmit varin seiner Umgebung mit dem Wert value. Somit var= somecommandwird varin der Umgebung somecommandmit einem leeren Wert (Null-Byte) exportiert .
Charles Duffy
Ja, ein Bash Gotcha.
Peter Mortensen
14

Wenn Sie dies mit mehrzeiligen / mehreren Befehlen tun möchten, können Sie dies tun:

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

Oder:

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

Beispiel:

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

Ausgabe:

first
second
third

Mit heredoc können Sie die Dinge ganz einfach vereinfachen, indem Sie Ihren langen einzeiligen Code in einen mehrzeiligen Code zerlegen. Ein anderes Beispiel:

output="$( ssh -p $port $user@$domain <<EOF
# Breakdown your long ssh command into multiline here.
EOF
)"
Jahid
quelle
2
Was ist mit der Sekunde bashinnerhalb der Befehlsersetzung? Sie erstellen bereits eine Unterschale durch die Befehlsersetzung selbst. Wenn Sie mehrere Befehle einfügen möchten, trennen Sie diese einfach durch Zeilenumbruch oder Semikolon. output=$(echo first; echo second; ...)
Tripleee
Dann 'bash -c "bash -c \"bash -c ...\""'wäre ähnlich auch "anders"; aber ich verstehe den Sinn davon nicht.
Tripleee
@tripleee heredoc bedeutet etwas mehr als das. Sie können das gleiche mit einigen anderen Befehlen tun, wie z. B. das ssh sudo -sAusführen von MySQL-Befehlen usw. (anstelle von Bash)
Jahid
1
Ich glaube nicht, dass wir richtig kommunizieren. Ich fordere die Nützlichkeit immer variable=$(bash -c 'echo "foo"; echo "bar"')wieder heraus variable=$(echo "foo"; echo "bar")- das Dokument hier ist nur ein Zitiermechanismus und fügt nur eine weitere nutzlose Komplikation hinzu.
Tripleee
2
Wenn ich heredoc mit ssh verwende, präzisiere ich den Befehl zum Ausführen ssh -p $port $user@$domain /bin/bash <<EOF, um eine Pseudo-terminal will not be allocated because stdin is not a terminal.Warnung zu verhindern
F. Hauri
9

Sie müssen entweder verwenden

$(command-here)

oder

`command-here`

Beispiel

#!/bin/bash

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

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

echo "$MOREF"
Diego Velez
quelle
1
Ich wusste nicht, dass du nisten kannst, aber es macht vollkommen Sinn, vielen Dank für die Info!
Diego Velez
6

Dies ist eine andere Möglichkeit und eignet sich gut für einige Texteditoren, die nicht in der Lage sind, jeden von Ihnen erstellten komplexen Code korrekt hervorzuheben:

read -r -d '' str < <(cat somefile.txt)
echo "${#str}"
echo "$str"
Wassermann-Kraft
quelle
Hier geht es nicht um die Frage von OP, bei der es wirklich um die Substitution von Befehlen geht , nicht um die Substitution von Prozessen .
Codeforester
6

Sie können Backticks (auch als Akzentgräber bezeichnet) oder verwenden $().

Mögen:

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

Beide haben den gleichen Effekt. Aber OUTPUT = $ (x + 2) ist besser lesbar und das neueste.

Pratik Patil
quelle
2
Klammern wurden implementiert, um das Verschachteln zu ermöglichen.
F. Hauri
5

Wenn der Befehl, den Sie ausführen möchten, fehlschlägt, wird die Ausgabe in den Fehlerstrom geschrieben und dann auf der Konsole ausgedruckt.

Um dies zu vermeiden, müssen Sie den Fehlerstrom umleiten:

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

Einige mögen dies nützlich finden. Ganzzahlige Werte bei der Variablensubstitution, bei der der Trick in $(())doppelten Klammern steht:

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
quelle
1
Dies scheint für diese Frage keine Relevanz zu haben. Es wäre eine vernünftige Antwort, wenn jemand fragen würde, wie eine Zahl in einem Array mit einem konstanten Faktor multipliziert werden soll, obwohl ich mich nicht erinnern kann, jemals jemanden danach gefragt zu haben (und dann for ((...))scheint eine Schleife besser mit der Schleifenvariablen übereinzustimmen ). Außerdem sollten Sie für Ihre privaten Variablen keinen Großbuchstaben verwenden.
Tripleee
Ich bin mit dem Teil "Relevanz" nicht einverstanden. Die Frage lautet eindeutig: Wie setze ich eine Variable, die der Ausgabe eines Befehls in Bash entspricht? Und ich habe diese Antwort als Ergänzung hinzugefügt, weil ich hier nach einer Lösung gesucht habe, die mir bei dem Code geholfen hat, den ich später gepostet habe. In Bezug auf die Großbuchstaben, danke dafür.
Gus
1
Dies könnte geschrieben werden, ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}aber ich stimme @tripleee zu: Ich verstehe nicht, was das dort macht!
F. Hauri
@ F.Hauri ... Bash wird immer mehr wie Perl, je tiefer du hineingehst!
Roblogic
4

Hier sind zwei weitere Möglichkeiten:

Bitte denken Sie daran, dass Platz in Bash sehr wichtig ist. Wenn Sie also möchten, dass Ihr Befehl ausgeführt wird, verwenden Sie ihn unverändert, ohne weitere Leerzeichen einzufügen.

  1. Die folgenden Abtretungsempfänger harshilzu Lund druckt sie dann

    L=$"harshil"
    echo "$L"
  2. Im Folgenden wird die Ausgabe des Befehls trL2 zugewiesen. trwird an einer anderen Variablen, L1, betrieben.

    L2=$(echo "$L1" | tr [:upper:] [:lower:])
Harshil
quelle
4
1. macht $"..."wahrscheinlich nicht das, was du denkst . 2. Dies ist bereits in Andy Lesters Antwort angegeben.
gniourf_gniourf
@gniourf_gniourf ist richtig: siehe Bash-Lokalisierung funktioniert nicht mit Multilines . Aber unter Bash können Sie echo ${L1,,}Downcase oder echo ${L1^^}Upcase verwenden.
F. Hauri