Làm cách nào để đặt biến cho đầu ra của lệnh trong Bash?


1677

Tôi có một kịch bản khá đơn giản giống như sau:

#!/bin/bash

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

echo $MOREF

Khi tôi chạy tập lệnh này từ dòng lệnh và truyền cho nó các đối số, tôi không nhận được bất kỳ đầu ra nào. Tuy nhiên, khi tôi chạy các lệnh có trong $MOREFbiến, tôi có thể nhận được đầu ra.

Làm thế nào người ta có thể lấy kết quả của một lệnh cần được chạy trong một tập lệnh, lưu nó vào một biến và sau đó xuất biến đó trên màn hình?



40
Bên cạnh đó, các biến mũ tất cả được xác định bởi POSIX cho các tên biến có ý nghĩa đối với hệ điều hành hoặc chính vỏ, trong khi các tên có ít nhất một ký tự chữ thường được dành riêng cho sử dụng ứng dụng. Do đó, hãy cân nhắc sử dụng tên viết thường cho các biến shell của riêng bạn để tránh xung đột ngoài ý muốn (lưu ý rằng việc đặt biến shell sẽ ghi đè lên bất kỳ biến môi trường giống như tên nào).
Charles Duffy

1
Là một đầu ra sang một bên, chụp vào một biến chỉ để bạn có thể sau đó echobiến là sử dụng vô dụng của echo, và sử dụng vô dụng của các biến.
tripleee

1
Như một bên xa hơn, lưu trữ đầu ra trong các biến thường không cần thiết. Đối với các chuỗi nhỏ, ngắn, bạn sẽ cần tham chiếu nhiều lần trong chương trình của mình, điều này hoàn toàn tốt và chính xác là cách để đi; nhưng để xử lý bất kỳ lượng dữ liệu không cần thiết nào, bạn muốn định hình lại quy trình của mình thành một đường ống hoặc sử dụng tệp tạm thời.
tripleee

Câu trả lời:


2377

Ngoài backticks `command`, thay thế lệnh có thể được thực hiện bằng $(command)hoặc "$(command)", mà tôi thấy dễ đọc hơn và cho phép lồng nhau.

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

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

Trích dẫn ( ") không quan trọng để bảo tồn các giá trị biến đa dòng ; nó là tùy chọn ở phía bên phải của một bài tập, vì việc tách từ không được thực hiện , do đó OUTPUT=$(ls -1)sẽ hoạt động tốt.


59
Chúng tôi có thể cung cấp một số phân cách cho đầu ra nhiều dòng?
Aryan

20
Các vấn đề về khoảng trắng (hoặc thiếu khoảng trắng)
Ali

8
@ timhc22, niềng răng xoăn không liên quan; đó chỉ là những trích dẫn quan trọng: liệu kết quả mở rộng có được phân tách chuỗi và mở rộng toàn cầu hay không trước khi được truyền cho echolệnh.
Charles Duffy

4
À cảm ơn! Vì vậy, có bất kỳ lợi ích cho các niềng răng xoăn?
timhc22

14
Dấu ngoặc nhọn có thể được sử dụng khi biến ngay lập tức được theo sau bởi nhiều ký tự có thể được hiểu là một phần của tên biến. ví dụ ${OUTPUT}foo . Chúng cũng được yêu cầu khi thực hiện các hoạt động chuỗi nội tuyến trên biến, chẳng hạn như${OUTPUT/foo/bar}
remer phong phú

282

Cách đúng là

$(sudo run command)

Nếu bạn đang sử dụng một dấu nháy đơn, bạn cần `, không '. Nhân vật này được gọi là "backticks" (hay "trọng âm").

Như thế này:

#!/bin/bash

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

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

echo "$MOREF"

31
Cú pháp backtick là lỗi thời và bạn thực sự cần đặt dấu ngoặc kép xung quanh phép nội suy biến trong echo.
tripleee

10
Tôi sẽ nói thêm rằng bạn phải cẩn thận với các khoảng trắng xung quanh '=' trong bài tập ở trên. Bạn sẽ không có bất kỳ khoảng trống nào ở đó, nếu không, bạn sẽ nhận được một nhiệm vụ không chính xác
zbstof

4
Nhận xét của tripleeee là chính xác. Trong cygwin (tháng 5 năm 2016), `` không hoạt động trong khi $()hoạt động. Không thể sửa cho đến khi tôi thấy trang này.
chập chững

2
Xây dựng như một ví dụ về Cập nhật (2018) sẽ được đánh giá cao.
Eduard

90

Một số thủ thuật Bash tôi sử dụng để đặt biến từ các lệnh

Chỉnh sửa lần 2 2018-02-12: Đã thêm một cách khác, tìm kiếm ở dưới cùng của điều này cho các nhiệm vụ dài hạn !

2018-01-25 Chỉnh sửa: đã thêm một hàm mẫu (để điền các biến về việc sử dụng đĩa)

Cách đầu tiên đơn giản, cũ và tương thích

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

Chủ yếu là tương thích, cách thứ hai

Khi lồng có thể trở nên nặng nề, dấu ngoặc đơn đã được thực hiện cho điều này

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

Mẫu lồng nhau:

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

Đọc nhiều hơn một biến số (với Bashism )

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

Nếu tôi chỉ muốn một giá trị được sử dụng :

array=($(df -k /))

bạn có thể thấy một biến mảng :

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

Sau đó:

echo ${array[9]}
529020

Nhưng tôi thích điều này:

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

Đầu tiên read foosẽ chỉ bỏ qua dòng tiêu đề, nhưng chỉ trong một lệnh, bạn sẽ điền 7 biến khác nhau :

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"

Hoặc thậm chí:

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

... cũng sẽ làm việc với các mảng kết hợp :read foo disk[total] disk[used] ...

Hàm mẫu để điền một số biến:

#!/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: declaredòng không bắt buộc, chỉ để dễ đọc.

Trong khoảng sudo cmd | grep ... | cut ...

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

(Xin vui lòng tránh vô dụng cat! Vì vậy, đây chỉ là một ngã ba ít hơn:

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

Tất cả các ống ( |) ngụ ý dĩa. Trường hợp một quá trình khác phải được chạy, truy cập đĩa, thư viện gọi và như vậy.

Vì vậy, sử dụng sedcho mẫu, sẽ giới hạn quy trình con chỉ trong một ngã ba :

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

Và với Bashism :

Nhưng đối với nhiều hành động, chủ yếu là trên các tệp nhỏ, Bash có thể tự thực hiện công việc:

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

hoặc là

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

Đi xa hơn về việc chia nhỏ biến ...

Hãy xem câu trả lời của tôi về Làm thế nào để tôi chia một chuỗi trên một dấu phân cách trong Bash?

Thay thế: giảm dĩa bằng cách sử dụng các tác vụ chạy dài nền

Chỉnh sửa lần 2 2018-02-12:

Để ngăn chặn nhiều dĩa như

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

hoặc là

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

Điều này làm việc tốt, nhưng chạy nhiều dĩa là nặng và chậm.

Và các lệnh như datebccó thể thực hiện nhiều hoạt động, từng dòng một !!

Xem:

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

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

Vì vậy, chúng tôi có thể sử dụng một quá trình nền chạy dài để thực hiện nhiều công việc, mà không phải bắt đầu một ngã ba mới cho mỗi yêu cầu.

Chúng tôi chỉ cần một số mô tả tập tinfifos để làm điều này đúng:

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

(Tất nhiên, FD 56phải không được sử dụng!) ... Từ đó, bạn có thể sử dụng quy trình này bằng cách:

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

Vào một chức năng newConnector

Bạn có thể tìm thấy newConnectorchức năng của tôi trên GitHub.Com hoặc trên trang web của riêng tôi (Lưu ý trên GitHub: có hai tệp trên trang web của tôi. Chức năng và bản demo được gói vào một tệp có thể được lấy nguồn để sử dụng hoặc chỉ chạy để dùng thử.)

Mẫu vật:

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

Hàm myBccho phép bạn sử dụng tác vụ nền với cú pháp đơn giản và cho ngày:

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

Từ đó, nếu bạn muốn kết thúc một trong các tiến trình nền, bạn chỉ cần đóng fd của nó :

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

không cần thiết, bởi vì tất cả fd đóng khi quá trình chính kết thúc.


Các mẫu lồng nhau ở trên là những gì tôi đang tìm kiếm. Có thể có một cách đơn giản hơn, nhưng điều tôi đang tìm kiếm là cách để tìm hiểu xem một container docker đã tồn tại với tên của nó trong một biến môi trường hay chưa. Vì vậy, đối với tôi: EXISTING_CONTAINER=$(docker ps -a | grep "$(echo $CONTAINER_NAME)")là tuyên bố tôi đang tìm kiếm.
Ma Kết1

2
@ capricorn1 Đó là cách sử dụng vô dụngecho ; bạn muốn đơn giảngrep "$CONTAINER_NAME"
tripleee


Tôi có thể bỏ lỡ một cái gì đó ở đây: kubectl get ns | while read -r line; do echo $ line | hạn grep | cắt -d '' -f1 ; donein ra cho mỗi $linedòng trống và sau đó bash: xxxx: command not found. Tuy nhiên, tôi hy vọng rằng nó sẽ được in raxxx
papanito

77

Vì họ đã chỉ ra cho bạn, bạn nên sử dụng 'backticks'.

Giải pháp thay thế được đề xuất cũng $(command)hoạt động và cũng dễ đọc hơn, nhưng lưu ý rằng nó chỉ hợp lệ với Bash hoặc KornShell (và shell có nguồn gốc từ chúng), vì vậy nếu tập lệnh của bạn phải thực sự di động trên các hệ thống Unix khác nhau, bạn nên chọn các ký hiệu backticks cũ.


23
Họ thận trọng quá mức. Backticks đã bị POSIX phản đối từ lâu; cú pháp hiện đại hơn nên có sẵn trong hầu hết các shell từ thiên niên kỷ này. (Vẫn còn môi trường di sản ho HP-UX ho mà đang bị mắc kẹt vững chắc trong nineties sớm.)
tripleee

25
Sai. $()hoàn toàn tương thích với POSIX sh, như được tiêu chuẩn hóa từ hai thập kỷ trước.
Charles Duffy

3
Lưu ý rằng /bin/shtrên Solaris 10 vẫn không nhận ra $(…)- và AFAIK cũng đúng trên Solaris 11.
Jonathan Leffler

2
@JonathanLeffler Đó là thực sự không có nhiều trường hợp với Solaris 11 nơi /bin/shksh93.
jlliagre

2
@tripleee - phản hồi trễ ba năm :-) nhưng tôi đã sử dụng $()trình bao POSIX trên HP-UX trong hơn 10 năm qua.
Bob Jarvis - Phục hồi Monica

54

Tôi biết ba cách để làm điều đó:

  1. Các chức năng phù hợp cho các nhiệm vụ như vậy: **

    func (){
        ls -l
    }

    Gọi nó bằng cách nói func.

  2. Ngoài ra một giải pháp phù hợp khác có thể được eval:

    var="ls -l"
    eval $var
  3. Cái thứ ba là sử dụng các biến trực tiếp:

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

Bạn có thể nhận được đầu ra của giải pháp thứ ba một cách tốt:

echo "$var"

Và cũng theo một cách khó chịu:

echo $var

1
Hai cái đầu tiên dường như không trả lời câu hỏi vì nó hiện đang đứng, và cái thứ hai thường được coi là đáng ngờ.
tripleee

1
Là một người hoàn toàn mới với bash, tại sao lại "$var"tốt và $varkhó chịu?
Peter


30

Chỉ để khác biệt:

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

22

Khi đặt biến, hãy đảm bảo bạn KHÔNGkhông gian trước và / hoặc sau dấu = . Nghĩa đen đã dành một giờ cố gắng để tìm ra điều này, thử tất cả các loại giải pháp! Nó không tuyệt.

Chính xác:

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

Sẽ thất bại với lỗi "thứ: không tìm thấy" hoặc tương tự

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

2
Phiên bản với không gian có nghĩa là một cái gì đó khác nhau : var=value somecommandchạy somecommandvới vartrong môi trường của nó có giá trị value. Do đó, var= somecommandđang xuất vartrong môi trường somecommandcó giá trị trống (không byte).
Charles Duffy

Vâng, một gotcha Bash.
Peter Mortensen

14

Nếu bạn muốn làm điều đó với multiline / nhiều lệnh / s thì bạn có thể làm điều này:

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

Hoặc là:

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

Thí dụ:

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

Đầu ra:

first
second
third

Sử dụng heredoc , bạn có thể đơn giản hóa mọi thứ khá dễ dàng bằng cách chia nhỏ mã dòng đơn dài của bạn thành một dòng đa dòng. Một vi dụ khac:

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

2
Điều gì với thứ hai bashbên trong thay thế lệnh? Bạn đã tạo một lớp con bằng chính lệnh thay thế. Nếu bạn muốn đặt nhiều lệnh, chỉ cần tách chúng bằng dòng mới hoặc dấu chấm phẩy. output=$(echo first; echo second; ...)
tripleee

Sau đó, tương tự 'bash -c "bash -c \"bash -c ...\""'sẽ là "khác nhau", quá; nhưng tôi không thấy quan điểm đó.
tripleee

@tripleee heredoc có nghĩa là nhiều hơn thế. Bạn có thể làm tương tự với một số lệnh khác như ssh sudo -sthực thi các lệnh mysql bên trong, v.v. (thay vì bash)
Jahid

1
Tôi không cảm thấy chúng ta đang giao tiếp đúng cách. Tôi đang thách thức sự hữu ích hơn variable=$(bash -c 'echo "foo"; echo "bar"')trên variable=$(echo "foo"; echo "bar")- tài liệu ở đây chỉ là một cơ chế trích dẫn và không thực sự thêm bất cứ điều gì ngoại trừ một biến chứng vô dụng.
tripleee

2
Khi tôi sử dụng heredoc với ssh, tôi chính xác lệnh chạy ssh -p $port $user@$domain /bin/bash <<EOFđể ngăn Pseudo-terminal will not be allocated because stdin is not a terminal.cảnh báo
F. Hauri

9

Bạn cần sử dụng một trong hai

$(command-here)

hoặc là

`command-here`

Thí dụ

#!/bin/bash

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

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

echo "$MOREF"


1
Tôi không biết bạn có thể làm tổ nhưng nó có ý nghĩa hoàn hảo, cảm ơn bạn rất nhiều vì thông tin!
Diego Velez

6

Đây là một cách khác và rất tốt để sử dụng với một số trình soạn thảo văn bản không thể làm nổi bật chính xác mọi mã phức tạp bạn tạo:

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

Điều này không giải quyết câu hỏi của OP, mà thực sự là về thay thế lệnh , không phải thay thế quá trình .
codeforester

6

Bạn có thể sử dụng backticks (còn được gọi là mộ trọng âm) hoặc $().

Giống:

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

Cả hai đều có tác dụng như nhau. Nhưng OUTPUT = $ (x + 2) dễ đọc hơn và mới nhất.


2
Dấu ngoặc được thực hiện để cho phép làm tổ.
F. Hauri

5

Nếu lệnh mà bạn đang cố thực hiện không thành công, nó sẽ ghi đầu ra vào luồng lỗi và sau đó sẽ được in ra bàn điều khiển.

Để tránh nó, bạn phải chuyển hướng luồng lỗi:

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

4

Một số có thể tìm thấy điều này hữu ích. Các giá trị số nguyên thay thế biến, trong đó thủ thuật đang sử dụng $(())dấu ngoặc kép:

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
Điều này dường như không có bất kỳ liên quan cho câu hỏi này. Sẽ là một câu trả lời hợp lý nếu ai đó hỏi cách nhân một số trong một mảng với một yếu tố không đổi, mặc dù tôi không nhớ đã từng thấy ai hỏi điều đó (và sau đó một for ((...))vòng lặp có vẻ phù hợp hơn với biến vòng lặp ). Ngoài ra, bạn không nên sử dụng chữ hoa cho các biến riêng tư của mình.
tripleee

Tôi không đồng ý với phần "liên quan". Câu hỏi ghi rõ: Làm thế nào để đặt một biến bằng với đầu ra từ một lệnh trong Bash? Và tôi đã thêm câu trả lời này như một sự bổ sung vì tôi đã đến đây để tìm kiếm một giải pháp giúp tôi với mã mà tôi đã đăng sau đó. Về các chữ hoa, cảm ơn vì điều đó.
Gus

1
Điều này có thể được viết ARR=(3 2 4 1);for((N=3,M=3,COUNT=N-1;COUNT < ${#ARR[@]};ARR[COUNT]*=M,COUNT+=N)){ :;}nhưng tôi đồng ý với @tripleee: Tôi không hiểu những gì làm điều này, ở đó!
F. Hauri

@ F.Hauri ... bash đang ngày càng giống với perl càng đi sâu vào nó!
roblogic

4

Đây là hai cách khác:

Xin lưu ý rằng không gian rất quan trọng trong Bash. Vì vậy, nếu bạn muốn lệnh của bạn chạy, hãy sử dụng như không có thêm bất kỳ khoảng trắng nào.

  1. Sau đây gán harshilcho Lvà sau đó in nó

    L=$"harshil"
    echo "$L"
  2. Sau đây gán đầu ra của lệnh trcho L2. trđang được vận hành trên một biến khác, L1.

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

4
1. $"..."có thể không làm những gì bạn nghĩ nó làm . 2. Điều này đã được đưa ra trong câu trả lời của Andy Lester.
gniourf_gniourf

@gniourf_gniourf đã đúng: xem bản địa hóa bash sẽ không hoạt động với multilines . Nhưng dưới bash , bạn có thể sử dụng echo ${L1,,}để viết hoa hoặc echo ${L1^^}viết hoa.
F. Hauri
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.