Wie speichere ich einen Befehl in einer Variablen in einem Shell-Skript?

113

Ich möchte einen Befehl speichern, der zu einem späteren Zeitpunkt in einer Variablen verwendet werden soll (nicht die Ausgabe des Befehls, sondern den Befehl selbst).

Ich habe ein einfaches Skript wie folgt:

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

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

Wenn ich jedoch etwas Komplizierteres versuche, schlägt dies fehl. Zum Beispiel, wenn ich mache

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

Die Ausgabe ist:

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

Haben Sie eine Idee, wie ich einen solchen Befehl (mit Pipes / mehreren Befehlen) zur späteren Verwendung in einer Variablen speichern könnte?

Benjamin
quelle
10
Verwenden Sie eine Funktion!
gniourf_gniourf

Antworten:

146

Verwenden Sie eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
quelle
27
$ (...) wird jetzt anstelle von empfohlen backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evalDies ist nur dann akzeptabel, wenn Sie dem Inhalt Ihrer Variablen vertrauen. Wenn Sie beispielsweise x="ls $name | wc"(oder sogar x="ls '$name' | wc") ausgeführt werden, ist dieser Code ein schneller Weg, um Sicherheitsanfälligkeiten bezüglich Injection oder Eskalation von Berechtigungen zu beheben, wenn diese Variable von jemandem mit weniger Berechtigungen festgelegt werden kann. (Durchlaufen Sie beispielsweise alle Unterverzeichnisse in /tmp? Sie sollten jedem einzelnen Benutzer im System vertrauen, dass er keinen anruft . $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/')
Charles Duffy
9
evalist ein riesiger Fehlermagnet, der niemals ohne Warnung vor dem Risiko eines unerwarteten Analyseverhaltens empfohlen werden sollte (auch ohne böswillige Zeichenfolgen, wie im Beispiel von @ CharlesDuffy). Versuchen Sie es zum Beispiel x='echo $(( 6 * 7 ))'und dann eval $x. Sie könnten erwarten, dass "42" gedruckt wird, aber wahrscheinlich nicht. Können Sie erklären, warum es nicht funktioniert? Können Sie erklären, warum ich "wahrscheinlich" gesagt habe? Wenn die Antworten auf diese Fragen für Sie nicht offensichtlich sind, sollten Sie sie niemals berühren eval.
Gordon Davisson
1
@Student, versuchen Sie set -xvorher, die ausgeführten Befehle zu protokollieren, damit Sie leichter sehen können, was passiert.
Charles Duffy
1
@Student Ich würde Shellcheck.net auch empfehlen, um auf häufige Fehler (und schlechte Gewohnheiten, die Sie nicht lernen sollten) hinzuweisen.
Gordon Davisson
41

Sie nicht verwenden eval! Es besteht ein großes Risiko, dass eine willkürliche Codeausführung eingeführt wird.

BashFAQ-50 - Ich versuche, einen Befehl in eine Variable einzufügen , aber die komplexen Fälle schlagen immer fehl.

Legen Sie es in einem Array und erweitern , um alle Wörter mit doppelten Anführungszeichen "${arr[@]}"zu nicht der LET IFSSpaltung aufgrund der Worte Wort Splitting .

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

und sehen Sie den Inhalt des Arrays im Inneren. Mit declare -pkönnen Sie den Inhalt des Arrays mit jedem Befehlsparameter in separaten Indizes anzeigen. Wenn ein solches Argument Leerzeichen enthält, wird durch Anführungszeichen beim Hinzufügen zum Array verhindert, dass es aufgrund von Word-Splitting aufgeteilt wird.

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

und führen Sie die Befehle als aus

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

(oder) insgesamt eine bashFunktion verwenden, um den Befehl auszuführen,

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

und rufen Sie die Funktion als gerecht auf

cmd

POSIX shhat keine Arrays. Sie können also am ehesten eine Liste von Elementen in den Positionsparametern erstellen. Hier ist eine POSIX- shMethode zum Ausführen eines Mail-Programms

# 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" "$@"
}

Beachten Sie, dass dieser Ansatz nur einfache Befehle ohne Umleitungen verarbeiten kann. Es kann keine Umleitungen, Pipelines, for / while-Schleifen, if-Anweisungen usw. Behandeln

Ein weiterer häufiger Anwendungsfall ist die Ausführung curlmit mehreren Headerfeldern und Nutzdaten. Sie können jederzeit Argumente wie unten definieren und curlden erweiterten Array-Inhalt aufrufen

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

Ein anderes Beispiel,

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

Nachdem die Variablen definiert sind, verwenden Sie ein Array, um Ihre Befehlsargumente zu speichern

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

und jetzt machen Sie eine richtig zitierte Erweiterung

curl "${curlCMD[@]}"
Inian
quelle
Dies funktioniert bei mir nicht, ich habe es versucht Command=('echo aaa | grep a')und "${Command[@]}"in der Hoffnung, dass es buchstäblich den Befehl ausführt echo aaa | grep a. Das tut es nicht. Ich frage mich, ob es einen sicheren Weg zum Ersetzen gibt eval, aber es scheint, dass jede Lösung die gleiche Kraft hat, die evalgefährlich sein könnte. Ist es nicht?
Student
Kurz gesagt, wie funktioniert das, wenn die ursprüngliche Zeichenfolge eine Pipe '|' enthält?
Student
@Student: Wenn Ihre ursprüngliche Zeichenfolge eine Pipe enthält, muss diese Zeichenfolge die unsicheren Teile des Bash-Parsers durchlaufen, um als Code ausgeführt zu werden. Verwenden Sie in diesem Fall keine Zeichenfolge. Verwenden Sie stattdessen eine Funktion: Command() { echo aaa | grep a; }- Danach können Sie einfach Commandoder result=$(Command)oder dergleichen ausführen .
Charles Duffy
1
@Student, richtig; Dies schlägt jedoch absichtlich fehl , da das, was Sie tun möchten, von Natur aus unsicher ist .
Charles Duffy
1
@Student: Ich habe zuletzt eine Notiz hinzugefügt, um zu erwähnen, dass es unter bestimmten Bedingungen nicht funktioniert
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

Mit dieser Methode wird der Befehl sofort ausgewertet und sein Rückgabewert gespeichert.

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

Gleiches gilt für den 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

Wenn Sie eval in the verwenden, $(...)wird es später nicht ausgewertet

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

Mit eval wird ausgewertet, wann evales verwendet wird

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

Wenn Sie im obigen Beispiel einen Befehl mit Argumenten ausführen müssen, fügen Sie diese in die Zeichenfolge ein, die Sie speichern

stored_date="date -u"
# ...

Für Bash-Skripte ist dies selten relevant, aber eine letzte Anmerkung. Sei vorsichtig mit eval. Eval nur Zeichenfolgen, die Sie steuern, niemals Zeichenfolgen, die von einem nicht vertrauenswürdigen Benutzer stammen oder aus nicht vertrauenswürdigen Benutzereingaben erstellt wurden.

  • Vielen Dank an @CharlesDuffy, der mich daran erinnert hat, den Befehl zu zitieren!
Nate
quelle
Dies löst nicht das ursprüngliche Problem, bei dem der Befehl eine Pipe '|' enthält.
Student
@Nate, beachten Sie, dass eval $stored_datedies in Ordnung sein kann, wenn es stored_datenur enthält date, aber eval "$stored_date"viel zuverlässiger ist. Führen Sie ein Beispiel str=$'printf \' * %s\\n\' *'; eval "$str"mit und ohne Anführungszeichen um das Finale "$str"aus. :)
Charles Duffy
@ CharlesDuffy Danke, ich habe das Zitieren vergessen. Ich wette, mein Linter hätte sich beschwert, wenn ich mir die Mühe gemacht hätte, es zu betreiben.
Nate
0

Speichern Sie Ihren Befehl für Bash wie folgt:

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

Führen Sie Ihren Befehl folgendermaßen aus:

echo $command | bash
Derek Hazell
quelle
1
Ich bin mir nicht sicher, aber vielleicht birgt diese Art der Befehlsausführung die gleichen Risiken wie die Verwendung von 'eval'.
Derek Hazell
0

Ich habe verschiedene Methoden ausprobiert:

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

Ausgabe:

$ 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

Wie Sie sehen können, "$@"lieferte nur der dritte das richtige Ergebnis.

mpen
quelle
0

Seien Sie vorsichtig bei der Registrierung einer Bestellung mit: X=$(Command)

Dieser wird noch ausgeführt, noch bevor er aufgerufen wird. Um dies zu überprüfen und zu bestätigen, müssen Sie Folgendes tun:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Azerty
quelle
-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
quelle
-8

Es ist nicht erforderlich, Befehle in Variablen zu speichern, auch wenn Sie sie später verwenden müssen. Führen Sie es einfach wie gewohnt aus. Wenn Sie in einer Variablen speichern, benötigen Sie eine evalAnweisung oder rufen einen unnötigen Shell-Prozess auf, um "Ihre Variable auszuführen".

kurumi
quelle
1
Der Befehl, den ich speichern werde, hängt von den Optionen ab, die ich einschicke. Anstatt Tonnen von bedingten Anweisungen in der Masse meines Programms zu haben, ist es viel einfacher, den Befehl zu speichern, den ich für die spätere Verwendung benötige.
Benjamin
1
@Benjamin, dann speichere dann zumindest die Optionen als Variablen und nicht den Befehl. zBvar='*.txt'; find . -name "$var"
Kurumi