Tôi vừa chỉ định một biến, nhưng biến echo $ hiển thị một cái gì đó khác


104

Dưới đây là một loạt các trường hợp echo $var có thể hiển thị giá trị khác với giá trị vừa được gán. Điều này xảy ra bất kể giá trị được chỉ định là "trích dẫn kép", "trích dẫn đơn" hay không được trích dẫn.

Làm cách nào để lấy shell để đặt biến của tôi một cách chính xác?

Dấu hoa thị

Kết quả mong đợi là /* Foobar is free software */, nhưng thay vào đó, tôi nhận được danh sách các tên tệp:

$ var="/* Foobar is free software */"
$ echo $var 
/bin /boot /dev /etc /home /initrd.img /lib /lib64 /media /mnt /opt /proc ...

Dấu ngoặc vuông

Giá trị mong đợi là vậy [a-z], nhưng đôi khi tôi nhận được một chữ cái thay thế!

$ var=[a-z]
$ echo $var
c

Nguồn cấp dữ liệu dòng (dòng mới)

Giá trị mong đợi là một danh sách các dòng riêng biệt, nhưng thay vào đó tất cả các giá trị nằm trên một dòng!

$ cat file
foo
bar
baz

$ var=$(cat file)
$ echo $var
foo bar baz

Nhiều không gian

Tôi mong đợi một tiêu đề bảng được căn chỉnh cẩn thận, nhưng thay vào đó nhiều khoảng trắng biến mất hoặc bị thu gọn thành một!

$ var="       title     |    count"
$ echo $var
title | count

Các tab

Tôi mong đợi hai giá trị được phân tách bằng tab, nhưng thay vào đó tôi nhận được hai giá trị được phân tách bằng dấu cách!

$ var=$'key\tvalue'
$ echo $var
key value

2
Cảm ơn vì đã làm điều này. Tôi gặp phải các nguồn cấp dữ liệu dòng thường xuyên. Vì vậy, var=$(cat file)là tốt, nhưng echo "$var"là cần thiết.
snd

3
BTW, đây cũng là BashPitfalls # 14: mywiki.wooledge.org/BashPitfalls#echo_.24foo
Charles Duffy


Câu trả lời:


139

Trong tất cả các trường hợp trên, biến được đặt đúng, nhưng không được đọc chính xác! Cách đúng là sử dụng dấu ngoặc kép khi tham chiếu :

echo "$var"

Điều này mang lại giá trị mong đợi trong tất cả các ví dụ đã cho. Luôn trích dẫn các tham chiếu biến!


Tại sao?

Khi một biến không được trích dẫn , nó sẽ:

  1. Undergo trường tách trong đó giá trị được chia thành nhiều từ trên khoảng trắng (theo mặc định):

    Trước: /* Foobar is free software */

    Sau: /*, Foobar, is, free, software,*/

  2. Mỗi từ này sẽ trải qua quá trình mở rộng tên đường dẫn , nơi các mẫu được mở rộng thành các tệp phù hợp:

    Trước: /*

    Sau: /bin, /boot, /dev, /etc, /home, ...

  3. Cuối cùng, tất cả các đối số được chuyển đến echo, viết chúng ra được phân tách bằng dấu cách đơn lẻ , cho

    /bin /boot /dev /etc /home Foobar is free software Desktop/ Downloads/

    thay vì giá trị của biến.

Khi biến được trích dẫn, nó sẽ:

  1. Được thay thế vì giá trị của nó.
  2. Không có bước 2.

Đây là lý do tại sao bạn nên luôn trích dẫn tất cả các tham chiếu biến , trừ khi bạn yêu cầu cụ thể tách từ và mở rộng tên đường dẫn. Các công cụ như shellcheck luôn sẵn sàng trợ giúp và sẽ cảnh báo về việc thiếu dấu ngoặc kép trong tất cả các trường hợp trên.


nó không phải lúc nào cũng hoạt động. Tôi có thể đưa ra một ví dụ: paste.ubuntu.com/p/8RjR6CS668
recolic

1
Đúng, $(..)dải nguồn cấp dữ liệu theo sau. Bạn có thể sử dụng var=$(cat file; printf x); var="${var%x}"để làm việc xung quanh nó.
anh chàng kia.

17

Bạn có thể muốn biết tại sao điều này đang xảy ra. Cùng với lời giải thích tuyệt vời của anh chàng kia , hãy tìm tài liệu tham khảo về Tại sao tập lệnh shell của tôi bị nghẹt trên khoảng trắng hoặc các ký tự đặc biệt khác? được viết bởi Gilles trong Unix & Linux :

Tại sao tôi cần phải viết "$foo"? Điều gì xảy ra nếu không có dấu ngoặc kép?

$fookhông có nghĩa là "lấy giá trị của biến foo". Nó có nghĩa là một cái gì đó phức tạp hơn nhiều:

  • Đầu tiên, lấy giá trị của biến.
  • Tách trường: coi giá trị đó như một danh sách các trường được phân tách bằng khoảng trắng và tạo danh sách kết quả. Ví dụ, nếu biến chứa foo * bar ​sau đó là kết quả của bước này là danh sách 3 yếu tố foo, *, bar.
  • Tạo tên tệp: coi mỗi trường như một khối cầu, tức là một mẫu ký tự đại diện và thay thế nó bằng danh sách các tên tệp phù hợp với mẫu này. Nếu mẫu không khớp với bất kỳ tệp nào, nó sẽ không được sửa đổi. Trong ví dụ của chúng tôi, điều này dẫn đến danh sách chứa foo, sau đó là danh sách các tệp trong thư mục hiện tại và cuối cùng bar. Nếu thư mục hiện thời trống rỗng, kết quả là foo, *, bar.

Lưu ý rằng kết quả là một danh sách các chuỗi. Có hai ngữ cảnh trong cú pháp shell: ngữ cảnh danh sách và ngữ cảnh chuỗi. Việc tách trường và tạo tên tệp chỉ xảy ra trong ngữ cảnh danh sách, nhưng đó là hầu hết thời gian. Dấu ngoặc kép phân định ngữ cảnh chuỗi: toàn bộ chuỗi được trích dẫn kép là một chuỗi đơn, không được tách. (Ngoại lệ: "$@"để mở rộng danh sách các tham số vị trí, ví dụ: "$@"tương đương với "$1" "$2" "$3"nếu có ba tham số vị trí. Xem Sự khác biệt giữa $ * và $ @ là gì? )

Điều tương tự cũng xảy ra với lệnh thay thế bằng $(foo)hoặc bằng `foo`. Một lưu ý nhỏ là không sử dụng `foo`: các quy tắc trích dẫn của nó rất kỳ lạ và không di động, và tất cả các shell hiện đại đều hỗ trợ $(foo)hoàn toàn tương đương ngoại trừ việc có các quy tắc trích dẫn trực quan.

Đầu ra của phép thay thế số học cũng trải qua các mở rộng tương tự, nhưng điều đó thường không phải là mối quan tâm vì nó chỉ chứa các ký tự không thể mở rộng (giả sử IFSkhông chứa chữ số hoặc -).

Xem Khi nào là cần thiết trích dẫn kép? để biết thêm chi tiết về các trường hợp khi bạn có thể để lại các báo giá.

Trừ khi bạn có ý định cho tất cả sự nghiêm ngặt này xảy ra, chỉ cần nhớ luôn sử dụng dấu ngoặc kép xung quanh các thay thế biến và lệnh. Hãy cẩn thận: bỏ qua các dấu ngoặc kép có thể không chỉ dẫn đến lỗi mà còn dẫn đến các lỗ hổng bảo mật .


7

Ngoài các vấn đề khác do không trích dẫn được -n-ecó thể được sử dụng echolàm đối số. (Chỉ có điều trước đây là hợp pháp theo thông số POSIX echo, nhưng một số triển khai phổ biến cũng vi phạm thông số kỹ thuật và tiêu dùng -e).

Để tránh điều này, hãy sử dụng printfthay vì echokhi chi tiết quan trọng.

Như vậy:

$ vars="-e -n -a"
$ echo $vars      # breaks because -e and -n can be treated as arguments to echo
-a
$ echo "$vars"
-e -n -a

Tuy nhiên, trích dẫn chính xác không phải lúc nào cũng giúp bạn tiết kiệm khi sử dụng echo:

$ vars="-n"
$ echo $vars
$ ## not even an empty line was printed

... trong khi nó sẽ cứu bạn với printf:

$ vars="-n"
$ printf '%s\n' "$vars"
-n

Yay, chúng tôi cần một dự phòng tốt cho việc này! Tôi đồng ý rằng điều này phù hợp với tiêu đề câu hỏi, nhưng tôi không nghĩ rằng nó sẽ có được khả năng hiển thị xứng đáng ở đây. Làm thế nào về một câu hỏi mới à la "Tại sao dấu gạch chéo ngược / -e/ của tôi -nkhông hiển thị?" Chúng tôi có thể thêm các liên kết từ đây nếu thích hợp.
anh chàng kia

Ý của bạn là tiêu thụ -ncũng ?
Pesa:

1
@PesaThe, không, ý tôi là -e. Tiêu chuẩn cho echokhông chỉ định đầu ra khi đối số đầu tiên của nó là -n, làm cho bất kỳ / tất cả đầu ra có thể hợp pháp trong trường hợp đó; không có điều khoản như vậy cho -e.
Charles Duffy

Ồ ... tôi không thể đọc. Hãy đổ lỗi cho tiếng Anh của tôi vì điều đó. Cảm ơn vì lời giải thích.
Pesa:

6

người dùng báo giá kép để nhận được giá trị chính xác. như thế này:

echo "${var}"

và nó sẽ đọc chính xác giá trị của bạn.


Hoạt động .. Cảm ơn
Tshilidzi Mudau

2

echo $varđầu ra phụ thuộc nhiều vào giá trị của IFSbiến. Theo mặc định, nó chứa các ký tự khoảng trắng, tab và dòng mới:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$

Điều này có nghĩa là khi shell đang thực hiện tách trường (hoặc tách từ), nó sử dụng tất cả các ký tự này làm dấu tách từ. Đây là những gì xảy ra khi tham chiếu một biến không có dấu ngoặc kép để lặp lại nó ( $var) và do đó kết quả mong đợi bị thay đổi.

Một cách để ngăn tách từ (ngoài việc sử dụng dấu ngoặc kép) là đặt IFSthành null. Xem http://pubs.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html#tag_02_06_05 :

Nếu giá trị của IFS là rỗng, thì sẽ không có việc tách trường nào được thực hiện.

Đặt thành null có nghĩa là đặt thành giá trị trống:

IFS=

Kiểm tra:

[ks@localhost ~]$ echo -n "$IFS" | cat -vte
 ^I$
[ks@localhost ~]$ var=$'key\nvalue'
[ks@localhost ~]$ echo $var
key value
[ks@localhost ~]$ IFS=
[ks@localhost ~]$ echo $var
key
value
[ks@localhost ~]$ 

2
Bạn cũng sẽ phải set -fđể ngăn chặn globbing
rằng anh chàng khác

@thatotherguy, nó có thực sự cần thiết cho ví dụ đầu tiên của bạn với việc mở rộng đường dẫn không? Với IFSđặt thành null, echo $varsẽ được mở rộng echo '/* Foobar is free software */'và mở rộng đường dẫn không được thực hiện bên trong các chuỗi được trích dẫn đơn lẻ.
ks1322,

1
Đúng. Nếu bạn mkdir "/this thing called Foobar is free software etc/"sẽ thấy rằng nó vẫn mở rộng. Rõ ràng là nó thực tế hơn cho [a-z]ví dụ.
đó là chàng trai khác,

Tôi hiểu, điều này có ý nghĩa [a-z]chẳng hạn.
ks1322,

2

Câu trả lời từ ks1322 đã giúp tôi xác định vấn đề khi sử dụng docker-compose exec:

Nếu bạn bỏ qua -Tcờ, hãy docker-compose execthêm một ký tự đặc biệt ngắt đầu ra, chúng tôi sẽ thấy bthay vì 1b:

$ test=$(/usr/local/bin/docker-compose exec db bash -c "echo 1")
$ echo "${test}b"
b
echo "${test}" | cat -vte
1^M$

Với -Tcờ, docker-compose exechoạt động như mong đợi:

$ test=$(/usr/local/bin/docker-compose exec -T db bash -c "echo 1")
$ echo "${test}b"
1b

-2

Ngoài việc đặt biến trong dấu ngoặc kép, người ta cũng có thể dịch đầu ra của biến bằng cách sử dụng trvà chuyển đổi khoảng trắng thành dòng mới.

$ echo $var | tr " " "\n"
foo
bar
baz

Mặc dù điều này phức tạp hơn một chút, nhưng nó làm tăng thêm sự đa dạng với đầu ra vì bạn có thể thay thế bất kỳ ký tự nào làm dấu phân cách giữa các biến mảng.


2
Nhưng điều này thay thế tất cả các khoảng trắng thành dòng mới. Trích dẫn duy trì các dòng mới và không gian hiện có.
user000001

Đúng, có. Tôi cho rằng nó phụ thuộc vào những gì bên trong biến. Tôi thực sự sử dụng trcách khác để tạo mảng từ các tệp văn bản.
Alek

3
Việc tạo ra một vấn đề bằng cách không trích dẫn biến đúng cách và sau đó giải quyết nó bằng một quy trình bổ sung hamfisted không phải là một chương trình tốt.
tripleee

@Alek, ... ờ, sao? Không trcần thiết để tạo đúng / chính xác một mảng từ tệp văn bản - bạn có thể chỉ định bất kỳ dấu phân tách nào bạn muốn bằng cách đặt IFS. Ví dụ: IFS=$'\n' read -r -d '' -a arrayname < <(cat file.txt && printf '\0')hoạt động trở lại thông qua bash 3.2 (phiên bản cũ nhất được lưu hành rộng rãi) và đặt chính xác trạng thái thoát thành false nếu catkhông thành công. Và nếu bạn muốn, giả sử, các tab thay vì dòng mới, bạn chỉ cần thay thế $'\n'bằng $'\t'.
Charles Duffy

1
@Alek, ... nếu bạn đang làm điều gì đó tương tự arrayname=( $( cat file | tr '\n' ' ' ) ), thì điều đó bị hỏng trên nhiều lớp: Nó làm mờ kết quả của bạn (vì vậy a *sẽ biến thành danh sách các tệp trong thư mục hiện tại) và nó sẽ hoạt động tốt nếu không cótr ( hoặc cat, đối với vấn đề đó; người ta chỉ có thể sử dụng arrayname=$( $(<file) )và nó sẽ bị phá vỡ theo những cách tương tự, nhưng ít kém hiệu quả hơn).
Charles Duffy
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.