Jak przechowywać polecenie w zmiennej w skrypcie powłoki?

113

Chciałbym zapisać polecenie do użycia w późniejszym okresie w zmiennej (nie wynik polecenia, ale samo polecenie)

Mam następujący prosty skrypt:

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

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

Jednak kiedy próbuję czegoś bardziej skomplikowanego, kończy się to niepowodzeniem. Na przykład, jeśli zrobię

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

Wynik to:

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

Masz jakiś pomysł, jak mógłbym przechowywać takie polecenie (z potokami / wieloma poleceniami) w zmiennej do późniejszego wykorzystania?

Benzoes
źródło
10
Użyj funkcji!
gniourf_gniourf

Odpowiedzi:

146

Użyj eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
źródło
27
Zamiast tego zaleca się teraz $ (...) backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evaljest dopuszczalną praktyką tylko wtedy, gdy ufasz zawartości swoich zmiennych. Jeśli używasz, powiedzmy x="ls $name | wc"(a nawet x="ls '$name' | wc"), ten kod jest szybką ścieżką do luk w zabezpieczeniach wstrzykiwania lub eskalacji uprawnień, jeśli ta zmienna może być ustawiona przez osobę o mniejszych uprawnieniach. (Na przykład przeprowadzanie iteracji po wszystkich podkatalogach w programie /tmp? Lepiej zaufaj każdemu użytkownikowi w systemie, że nie wywoła żadnego $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/').
Charles Duffy
9
evaljest ogromnym magnesem na błędy, którego nigdy nie powinno się polecać bez ostrzeżenia o ryzyku nieoczekiwanego zachowania podczas analizowania (nawet bez złośliwych ciągów, jak w przykładzie @ CharlesDuffy). Na przykład spróbuj x='echo $(( 6 * 7 ))'i wtedy eval $x. Można by się spodziewać, że wypisze "42", ale prawdopodobnie nie. Czy możesz wyjaśnić, dlaczego to nie działa? Czy możesz wyjaśnić, dlaczego powiedziałem „prawdopodobnie”? Jeśli odpowiedzi na te pytania nie są dla Ciebie oczywiste, nigdy nie dotykaj eval.
Gordon Davisson,
1
@Student, spróbuj set -xwcześniej uruchomić, aby zarejestrować uruchomione polecenia, co ułatwi zobaczenie, co się dzieje.
Charles Duffy
1
@Student Polecam również shellcheck.net do wskazywania typowych błędów (i złych nawyków, których nie powinieneś wychwytywać).
Gordon Davisson,
41

Czy nie używać eval! Istnieje duże ryzyko wprowadzenia dowolnego kodu.

BashFAQ-50 - próbuję umieścić polecenie w zmiennej, ale złożone przypadki zawsze zawodzą.

Umieścić go w tablicy i rozwinąć wszystkie słowa z cudzysłowami "${arr[@]}"aby nie pozwól IFSrozdzielić słowa ze względu na podział na słowa .

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

i zobacz zawartość tablicy w środku. declare -pPozwala zobaczyć zawartość wewnątrz tablicy z każdego parametru poleceń w osobnych indeksów. Jeśli jeden z takich argumentów zawiera spacje, cytowanie wewnątrz podczas dodawania do tablicy zapobiegnie jej podzieleniu z powodu podziału na słowa.

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

i wykonaj polecenia jako

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

(lub) całkowicie użyj bashfunkcji do uruchomienia polecenia,

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

i wywołaj funkcję jako sprawiedliwą

cmd

POSIX shnie ma tablic, więc najbliższe możliwe jest utworzenie listy elementów w parametrach pozycyjnych. Oto shsposób POSIX-a na uruchomienie programu pocztowego

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

Zauważ, że to podejście może obsługiwać tylko proste polecenia bez przekierowań. Nie obsługuje przekierowań, potoków, pętli for / while, instrukcji if itp

Innym częstym przypadkiem użycia jest praca curlz wieloma polami nagłówka i ładunkiem. Zawsze możesz zdefiniować argumenty jak poniżej i wywołać curlna rozszerzonej zawartości tablicy

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

Inny przykład,

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

Teraz, gdy zmienne są zdefiniowane, użyj tablicy do przechowywania argumentów poleceń

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

a teraz wykonaj poprawnie cytowane rozszerzenie

curl "${curlCMD[@]}"
Inian
źródło
To nie działa dla mnie, próbowałem Command=('echo aaa | grep a')i "${Command[@]}", mając nadzieję, że działa dosłownie na polecenie echo aaa | grep a. Tak nie jest. Zastanawiam się, czy można bezpiecznie zastąpić eval, ale wydaje się, że każde rozwiązanie ma taką samą siłę, jak evalmogłoby być niebezpieczne. Prawda?
Student
Krótko mówiąc, jak to działa, jeśli oryginalny ciąg zawiera pionową kreskę „|”?
Student
@Student, jeśli oryginalny ciąg zawiera potok, to ten ciąg musi przejść przez niebezpieczne części parsera bash, aby został wykonany jako kod. W takim przypadku nie używaj łańcucha; zamiast tego użyj funkcji: Command() { echo aaa | grep a; }- po której możesz po prostu uruchomić Command, lub result=$(Command), lub tym podobne.
Charles Duffy
1
@Student, racja; ale to zawodzi celowo , ponieważ to, o co prosisz, jest z natury niepewne .
Charles Duffy
1
@Student: Na końcu dodałem notatkę, aby wspomnieć, że nie działa w pewnych warunkach
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

Przy użyciu tej metody polecenie jest natychmiast oceniane, a jego wartość zwracana jest przechowywana.

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

To samo z 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

Używanie eval w $(...)testamencie nie spowoduje, że zostanie on później oceniony

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

Używając eval, jest oceniany, gdy evaljest używany

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

W powyższym przykładzie, jeśli chcesz uruchomić polecenie z argumentami, umieść je w przechowywanym ciągu

stored_date="date -u"
# ...

W przypadku skryptów bash rzadko jest to istotne, ale ostatnia uwaga. Uważaj z eval. Oceniaj tylko łańcuchy, które kontrolujesz, nigdy ciągi pochodzące od niezaufanego użytkownika lub utworzone na podstawie niezaufanych danych wejściowych użytkownika.

  • Dzięki @CharlesDuffy za przypomnienie mi o zacytowaniu polecenia!
Nate
źródło
Nie rozwiązuje to pierwotnego problemu, w którym polecenie zawiera potok „|”.
Student
@Nate, pamiętaj, że eval $stored_datemoże to wystarczyć, gdy stored_datezawiera tylko date, ale eval "$stored_date"jest znacznie bardziej niezawodne. Na przykład uruchom str=$'printf \' * %s\\n\' *'; eval "$str"z cudzysłowami i bez cudzysłowów wokół finału "$str". :)
Charles Duffy
@CharlesDuffy Dzięki, zapomniałem o cytowaniu. Założę się, że mój liniowiec narzekałby, gdybym się trudził, żeby go uruchomić.
Nate
1

W przypadku basha zapisz swoje polecenie w ten sposób:

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

Uruchom polecenie w ten sposób:

echo $command | bash
Derek Hazell
źródło
1
Nie jestem pewien, ale być może ten sposób uruchomienia polecenia wiąże się z takim samym ryzykiem, jak użycie „eval”.
Derek Hazell
0

Próbowałem różnych metod:

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

Wynik:

$ 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

Jak widać, tylko trzecia "$@"dała poprawny wynik.

mpen
źródło
0

Zachowaj ostrożność rejestrując zamówienie w: X=$(Command)

Ten jest nadal wykonywany nawet przed wywołaniem. Aby to sprawdzić i potwierdzić, możesz:

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

Nie jest konieczne przechowywanie poleceń w zmiennych, nawet jeśli będziesz ich później używać. po prostu wykonaj to normalnie. Jeśli przechowujesz w zmiennej, będziesz potrzebować jakiejś evalinstrukcji lub wywołać niepotrzebny proces powłoki, aby „wykonać zmienną”.

kurumi
źródło
1
Polecenie, które zapiszę, będzie zależało od opcji, które wyślę, więc zamiast mieć mnóstwo instrukcji warunkowych w większości mojego programu, znacznie łatwiej jest zapisać polecenie, którego potrzebuję do późniejszego użycia.
Benjamin
1
@Benjamin, to przynajmniej zapisz opcje jako zmienne, a nie polecenie. np.var='*.txt'; find . -name "$var"
kurumi