Jak ustawić zmienną na wyjściu polecenia w Bash?


1675

Mam dość prosty skrypt, który jest podobny do następującego:

#!/bin/bash

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

echo $MOREF

Kiedy uruchamiam ten skrypt z wiersza poleceń i przekazuję mu argumenty, nie otrzymuję żadnych danych wyjściowych. Jednak po uruchomieniu poleceń zawartych w $MOREFzmiennej jestem w stanie uzyskać dane wyjściowe.

Jak wziąć wyniki polecenia, które należy uruchomić w skrypcie, zapisać je w zmiennej, a następnie wypisać na ekranie?



40
Nawiasem mówiąc, zmienne wielkimi literami są definiowane przez POSIX dla nazw zmiennych mających znaczenie dla systemu operacyjnego lub samej powłoki, podczas gdy nazwy zawierające co najmniej jedną małą literę są zarezerwowane do użytku aplikacji. Dlatego rozważ użycie małych liter dla własnych zmiennych powłoki, aby uniknąć niezamierzonych konfliktów (pamiętając, że ustawienie zmiennej powłoki zastąpi każdą zmienną środowiskową o podobnej nazwie).
Charles Duffy

1
Tak na marginesie, przechwytywanie wyjścia do zmiennej tak można następnie echozmienna jest bezużyteczny wykorzystanie echo, a bezużyteczny stosowanie zmiennych.
tripleee

1
Ponadto przechowywanie danych wyjściowych w zmiennych jest często niepotrzebne. W przypadku małych, krótkich ciągów będziesz musiał odwoływać się wiele razy w swoim programie, jest to całkowicie w porządku i dokładnie tak jest; ale do przetwarzania wszelkich nietrywialnych ilości danych chcesz przekształcić proces w potok lub użyć pliku tymczasowego.
tripleee

Odpowiedzi:


2375

Oprócz backticksów `command`, podstawianie poleceń można wykonać za pomocą $(command)lub "$(command)", co uważam za łatwiejsze do odczytania i pozwala na zagnieżdżanie.

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

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

Quoting ( ") ma znaczenie dla zachowania wartości zmiennych wieloliniowych ; jest to opcjonalne po prawej stronie zadania, ponieważ dzielenie słów nie jest wykonywane , więc OUTPUT=$(ls -1)działałoby dobrze.


58
Czy możemy zapewnić separator dla wyjścia wieloliniowego?
Aryjczyk

20
Białe spacje (lub brak białych znaków) mają znaczenie
Ali

8
@ timhc22, nawiasy klamrowe są nieistotne; ważne są tylko cytaty: czy wyniki interpretacji są dzielone na łańcuchy i globalnie, zanim zostaną przekazane do echopolecenia.
Charles Duffy,

4
Ach dzięki! Czy są jakieś korzyści dla nawiasów klamrowych?
timhc22

14
Nawiasów klamrowych można używać, gdy po zmiennej natychmiast następuje więcej znaków, które można interpretować jako część nazwy zmiennej. np ${OUTPUT}foo . Są one również wymagane podczas wykonywania wbudowanych operacji na łańcuchach zmiennej, takich jak${OUTPUT/foo/bar}
rich remer

282

Właściwa droga to

$(sudo run command)

Jeśli masz zamiar użyć apostrofu, trzeba `, nie '. Ta postać nazywa się „backticks” (lub „grave accent”).

Lubię to:

#!/bin/bash

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

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

echo "$MOREF"

31
Składnia wsteczna jest przestarzała i naprawdę musisz wstawić podwójne cudzysłowy wokół interpolacji zmiennej w echo.
tripleee

10
Dodałbym, że musisz uważać na spacje wokół „=” w powyższym zadaniu. Nie powinieneś mieć tam żadnych spacji , w przeciwnym razie dostaniesz niepoprawne przypisanie
zbstof

4
komentarz tripleeee jest poprawny. W cygwin (maj 2016) `` nie działa podczas $()pracy. Nie udało się naprawić, dopóki nie zobaczyłem tej strony.
toddwz

2
Docenione zostanie opracowanie takie jak przykład aktualizacji (2018) .
Eduard,

90

Niektóre triki Bash, których używam do ustawiania zmiennych z poleceń

2. edycja 2018-02-12: Dodano inny sposób, wyszukaj na dole tego zadania długoterminowe !

2018-01-25 Edytuj: dodano przykładową funkcję (do zapełniania zmiennych dotyczących użycia dysku)

Pierwszy prosty, stary i zgodny sposób

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

W większości kompatybilny, drugi sposób

Ponieważ zagnieżdżanie może stać się ciężkie, zastosowano w tym celu nawiasy

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

Próbka zagnieżdżona:

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

Odczytywanie więcej niż jednej zmiennej (z Bashisms )

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

Jeśli chcę tylko użyć wartości:

array=($(df -k /))

możesz zobaczyć zmienną tablicową :

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

Następnie:

echo ${array[9]}
529020

Ale wolę to:

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

Pierwszy z read foonich po prostu pominie wiersz nagłówka, ale tylko w jednym poleceniu zapełnisz 7 różnych zmiennych:

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"

Lub nawet:

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

... będzie działać również z tablicami asocjacyjnymi :read foo disk[total] disk[used] ...

Przykładowa funkcja do zapełniania niektórych zmiennych:

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

Uwaga: declarelinia nie jest wymagana, tylko ze względu na czytelność.

O sudo cmd | grep ... | cut ...

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

(Unikaj bezużytecznego cat! Więc to tylko jeden widelec mniej:

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

Wszystkie potoki ( |) oznaczają widelce. Gdzie trzeba uruchomić inny proces, dostęp do dysku, wywołania bibliotek i tak dalej.

Zatem użycie sedpróbki, ograniczy podproces do tylko jednego rozwidlenia :

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

I z Bashismami :

Ale w przypadku wielu działań, głównie na małych plikach, Bash mógł wykonać to samo:

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

lub

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

Idąc dalej na temat dzielenia zmiennych ...

Spójrz na moją odpowiedź na temat Jak podzielić ciąg na ograniczniku w Bash?

Alternatywa: zmniejszenie liczby widelców przy użyciu długoterminowych zadań w tle

2. edycja 2018-02-12:

Aby zapobiec wielu rozwidleniom, takim jak

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

lub

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

Działa to dobrze, ale uruchamianie wielu widelców jest ciężkie i wolne.

Polecenia takie jak datei bcmogą wykonywać wiele operacji, linia po linii !!

Widzieć:

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

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

Możemy więc użyć długiego procesu w tle, aby wykonać wiele zadań, bez konieczności inicjowania nowego rozwidlenia dla każdego żądania.

Potrzebujemy tylko kilku deskryptorów plików i fifos, aby zrobić to poprawnie:

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

(Oczywiście, FD 5i 6muszą być nieużywane!) ... Stamtąd możesz skorzystać z tego procesu poprzez:

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

W funkcję newConnector

Możesz znaleźć moją newConnectorfunkcję na GitHub.Com lub na mojej własnej stronie (uwaga na GitHub: w mojej witrynie są dwa pliki. Funkcja i wersja demo są umieszczone w jednym pliku, który można wykorzystać lub uruchomić w celu pokazu).

Próba:

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

Funkcja myBcumożliwia użycie zadania w tle z prostą składnią i dla daty:

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

Stamtąd, jeśli chcesz zakończyć jeden z procesów w tle, wystarczy zamknąć jego 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

co nie jest potrzebne, ponieważ wszystkie fd zamykają się po zakończeniu głównego procesu.


Zagnieżdżona próbka powyżej była tym, czego szukałem. Może być prostszy sposób, ale szukałem sposobu, aby dowiedzieć się, czy kontener dokowania już istnieje, biorąc pod uwagę jego nazwę w zmiennej środowiskowej. Więc dla mnie: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")to stwierdzenie, którego szukałem.
Koziorożec1

2
@ capricorn1 To bezużyteczne użycieecho ; chcesz po prostugrep "$CONTAINER_NAME"
tripleee


Prawdopodobnie coś mi brakuje: kubectl get ns | while read -r line; do echo $ line | grep Termin | cut -d '-f1 ; donewypisuje dla każdej $linepustej linii, a następnie bash: xxxx: command not found. Spodziewam się jednak, że zostanie wydrukowanyxxx
papanito

77

Jak już ci wskazali, powinieneś użyć „backticks”.

Proponowana alternatywa również $(command)działa i jest łatwiejsza do odczytania, ale należy pamiętać, że jest ważna tylko z Bash lub KornShell (i powłokami z nich pochodnymi), więc jeśli twoje skrypty muszą być naprawdę przenośne na różnych systemach uniksowych, powinieneś preferować stara notacja wsteczna.


23
Są jawnie ostrożni. Backticks już dawno były przestarzałe; bardziej nowoczesna składnia powinna być dostępna w większości powłok tego tysiąclecia. (Istnieje jeszcze starszych środowisk kaszel HP-UX kaszel , które tkwią mocno na początku lat dziewięćdziesiątych.)
tripleee

25
Błędny. $()jest w pełni kompatybilny z POSIX sh, znormalizowanym ponad dwie dekady temu.
Charles Duffy

3
Należy pamiętać, że /bin/shw systemie Solaris 10 nadal nie rozpoznaje $(…)- i AFAIK to prawda również w systemie Solaris 11.
Jonathan Leffler,

2
@JonathanLeffler Tak naprawdę nie jest tak w przypadku Solaris 11, gdzie /bin/shjest ksh93.
jlliagre,

2
@tripleee - odpowiedź trzy lata później :-), ale używałem $()w powłoce POSIX na HP-UX od ponad 10 lat.
Bob Jarvis - Przywróć Monikę

54

Znam trzy sposoby, aby to zrobić:

  1. Funkcje są odpowiednie do takich zadań: **

    func (){
        ls -l
    }

    Przywołaj to, mówiąc func.

  2. Ewentualne może być także inne odpowiednie rozwiązanie:

    var="ls -l"
    eval $var
  3. Trzeci używa bezpośrednio zmiennych:

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

Możesz uzyskać wynik trzeciego rozwiązania w dobry sposób:

echo "$var"

A także w paskudny sposób:

echo $var

1
Pierwsze dwa wydają się nie odpowiadać na pytanie w obecnej formie, a drugie powszechnie uważa się za wątpliwe.
tripleee

1
Jako ktoś zupełnie nowy w bash, dlaczego jest "$var"dobry i $varpaskudny?
Peter


30

Żeby być innym:

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

22

Ustawiając zmienną, upewnij się, że NIE masz spacji przed i / lub po znaku = . Dosłownie spędziłem godzinę próbując to rozgryźć, próbując wszelkiego rodzaju rozwiązań! To nie jest fajne.

Poprawny:

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

Zakończy się niepowodzeniem z błędem „rzeczy: not found” lub podobny

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

2
Wersja z przestrzeni oznacza coś innego : var=value somecommandprzejazdy somecommandz varw środowisku o wartości value. Tak więc var= somecommandeksportuje varw środowisku somecommandz pustą wartością (zero-bajtową).
Charles Duffy,

Tak, Bash gotcha.
Peter Mortensen

14

Jeśli chcesz to zrobić za pomocą polecenia multiline / multiple / s, możesz to zrobić:

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

Lub:

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

Przykład:

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

Wynik:

first
second
third

Korzystając z heredoc , możesz bardzo łatwo uprościć sprawę , dzieląc swój długi, jednoliniowy kod na wielowierszowy. Inny przykład:

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

2
Co jest z drugim bashw podstawieniu polecenia? Już tworzysz podpowłokę przez samo podstawienie polecenia. Jeśli chcesz wstawić wiele poleceń, po prostu oddziel je znakiem nowej linii lub średnikiem. output=$(echo first; echo second; ...)
tripleee

Wtedy podobnie 'bash -c "bash -c \"bash -c ...\""'byłoby też „inaczej”; ale nie widzę sensu tego.
tripleee

@tripleee heredoc oznacza coś więcej niż to. Możesz zrobić to samo z kilkoma innymi poleceniami, takimi jak ssh sudo -swykonywanie poleceń mysql wewnątrz itp. (Zamiast bash)
Jahid

1
Nie czuję, że komunikujemy się poprawnie. Ja kwestionuje przydatność przez variable=$(bash -c 'echo "foo"; echo "bar"')ponad variable=$(echo "foo"; echo "bar")- tu dokument jest po prostu mechanizm cytowanie i naprawdę nie dodaje niczego oprócz innego bezużyteczne komplikacji.
tripleee

2
Kiedy używam heredoc z ssh, precyzuję polecenie uruchomienia ssh -p $port $user@$domain /bin/bash <<EOF, aby zapobiec Pseudo-terminal will not be allocated because stdin is not a terminal.ostrzeżeniu
F. Hauri


6

Jest to inny sposób i dobrze jest go używać z niektórymi edytorami tekstu, które nie są w stanie poprawnie wyróżnić każdego skomplikowanego kodu, który tworzysz:

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

To nie dotyczy pytania OP, które tak naprawdę dotyczy podstawiania poleceń , a nie zastępowania procesów .
codeforester

6

Możesz użyć tyknięć (znanych również jako groby akcentowe) lub $() .

Lubić:

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

Oba mają ten sam efekt. Ale WYJŚCIE = $ (x + 2) jest bardziej czytelne i najnowsze.


2
Wdrożono nawiasy, aby umożliwić zagnieżdżanie.
F. Hauri,

5

Jeśli polecenie, które próbujesz wykonać, nie powiedzie się, zapisuje dane wyjściowe w strumieniu błędów, a następnie zostanie wydrukowane w konsoli.

Aby tego uniknąć, musisz przekierować strumień błędów:

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

4

Niektórzy mogą uznać to za przydatne. Wartości całkowite w podstawianiu zmiennych, gdzie lewica używa $(())podwójnych nawiasów:

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
Wydaje się, że nie ma to żadnego związku z tym pytaniem. Byłoby rozsądną odpowiedzią, gdyby ktoś zapytał, jak pomnożyć liczbę w tablicy przez stały czynnik, chociaż nie przypominam sobie, aby ktokolwiek kiedykolwiek o to pytał (a wtedy for ((...))pętla wydawałaby się lepszym dopasowaniem do zmiennej pętli ). Nie powinieneś także używać wielkich liter w swoich zmiennych prywatnych.
tripleee

Nie zgadzam się z częścią „trafność”. Pytanie wyraźnie brzmi: Jak ustawić zmienną równą wynikowi polecenia w Bash? Dodałem tę odpowiedź jako uzupełnienie, ponieważ szukałem rozwiązania, które pomogło mi z kodem, który później opublikowałem. Dzięki za wielkie litery, dziękuję za to.
Gus

1
Można to napisać, ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}ale zgadzam się z @tripleee: Nie rozumiem, co to robi!
F. Hauri,

@ F.Hauri ... bash robi się coraz bardziej podobny do perla, im głębiej się w to zagłębiasz!
roblogic,

4

Oto dwa kolejne sposoby:

Pamiętaj, że w Bash przestrzeń jest bardzo ważna. Tak więc, jeśli chcesz, aby twoje polecenie działało, używaj bez zmian, bez wprowadzania spacji.

  1. Poniższy przypisuje harshildo L, a następnie wydrukowanie go

    L=$"harshil"
    echo "$L"
  2. Poniżej przypisuje dane wyjściowe polecenia trdo L2. trjest obsługiwany na innej zmiennej, L1.

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

4
1. $"..."prawdopodobnie nie robi tego, co myślisz, że robi . 2. Jest to już podane w odpowiedzi Andy'ego Lestera.
gniourf_gniourf

@gniourf_gniourf ma rację: zobacz, że lokalizacja bash nie będzie działać z multiliniami . Ale pod bash możesz użyć echo ${L1,,}do zmniejszenia lub echo ${L1^^}zwiększenia.
F. Hauri,
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.