Sự khác biệt giữa $ * và $ @ là gì?


73

Hãy xem xét các mã sau đây:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

Nó xuất ra:

1 2 3 4

1 2 3 4

Tôi đang sử dụng Ksh88, nhưng tôi cũng quan tâm đến các shell thông thường khác. Nếu bạn tình cờ biết bất kỳ tính đặc biệt nào cho các vỏ cụ thể, vui lòng đề cập đến chúng.

Tôi tìm thấy follwing trong trang Ksh man trên Solaris:

Ý nghĩa của $ * và $ @ là giống hệt nhau khi không được trích dẫn hoặc khi được sử dụng làm giá trị gán tham số hoặc làm tên tệp. Tuy nhiên, khi được sử dụng làm đối số lệnh, $ * tương đương với `` $ 1d $ 2d ... '', trong đó d là ký tự đầu tiên của biến IFS, trong khi $ @ tương đương với $ 1 $ 2 ....

Tôi đã thử sửa đổi IFSbiến, nhưng nó không sửa đổi đầu ra. Có lẽ tôi đang làm gì đó sai?

Câu trả lời:


96

Khi chúng không được trích dẫn, $*$@giống nhau. Bạn không nên sử dụng một trong hai thứ này, bởi vì chúng có thể phá vỡ bất ngờ ngay khi bạn có các đối số có chứa khoảng trắng hoặc ký tự đại diện.


"$*"mở rộng thành một từ duy nhất "$1c$2c...". Thường clà một không gian, nhưng nó thực sự là nhân vật đầu tiên IFS, vì vậy nó có thể là bất cứ thứ gì bạn chọn.

Công dụng tốt duy nhất tôi từng thấy cho nó là:

tham gia đối số bằng dấu phẩy (phiên bản đơn giản)

join1() {
    typeset IFS=,
    echo "$*"
}

join1 a b c   # => a,b,c

tham gia đối số với dấu phân cách được chỉ định (phiên bản tốt hơn)

join2() {
    typeset IFS=$1   # typeset makes a local variable in ksh (see footnote)
    shift
    echo "$*"
}

join2 + a b c   # => a+b+c

"$@" mở rộng thành các từ riêng biệt: "$1" "$2" ...

Điều này gần như luôn luôn là những gì bạn muốn. Nó mở rộng từng tham số vị trí thành một từ riêng biệt, điều này làm cho nó hoàn hảo để đưa các đối số dòng lệnh hoặc hàm vào và sau đó chuyển chúng sang một lệnh hoặc hàm khác. Và bởi vì nó mở rộng bằng cách sử dụng dấu ngoặc kép, điều đó có nghĩa là mọi thứ không bị phá vỡ nếu, giả sử, "$1"chứa một khoảng trắng hoặc dấu hoa thị ( *).


Hãy viết một kịch bản gọi là svimchạy vimvới sudo. Chúng tôi sẽ làm ba phiên bản để minh họa sự khác biệt.

svim1

#!/bin/sh
sudo vim $*

svim2

#!/bin/sh
sudo vim "$*"

svim3

#!/bin/sh
sudo vim "$@"

Tất cả chúng sẽ ổn đối với các trường hợp đơn giản, ví dụ: một tên tệp không chứa dấu cách:

svim1 foo.txt             # == sudo vim foo.txt
svim2 foo.txt             # == sudo vim "foo.txt"
svim2 foo.txt             # == sudo vim "foo.txt"

Nhưng chỉ $*"$@"hoạt động đúng nếu bạn có nhiều đối số.

svim1 foo.txt bar.txt     # == sudo vim foo.txt bar.txt
svim2 foo.txt bar.txt     # == sudo vim "foo.txt bar.txt"   # one file name!
svim3 foo.txt bar.txt     # == sudo vim "foo.txt" "bar.txt"

Và chỉ "$*""$@"hoạt động đúng nếu bạn có đối số chứa khoảng trắng.

svim1 "shopping list.txt" # == sudo vim shopping list.txt   # two file names!
svim2 "shopping list.txt" # == sudo vim "shopping list.txt"
svim3 "shopping list.txt" # == sudo vim "shopping list.txt"

Vì vậy, chỉ "$@"sẽ làm việc đúng mọi lúc.


typesetlà cách tạo biến cục bộ trong ksh( bashashsử dụng localthay thế). Nó có nghĩa là IFSsẽ được khôi phục về giá trị trước đó của nó khi hàm trả về. Điều này rất quan trọng, bởi vì các lệnh bạn chạy sau đó có thể không hoạt động đúng nếu IFSđược đặt thành một cái gì đó không chuẩn.


2
Giải thích tuyệt vời, cảm ơn bạn rất nhiều.
rahmu

Cảm ơn bạn cho một ví dụ về việc sử dụng $*. Tôi đã luôn luôn coi nó là hoàn toàn vô dụng ... tham gia với dấu phân cách là một trường hợp sử dụng tốt.
anishsane

35

Câu trả lời ngắn: sử dụng"$@" (lưu ý dấu ngoặc kép). Các hình thức khác rất hiếm khi hữu ích.

"$@"là một cú pháp khá lạ. Nó được thay thế bởi tất cả các tham số vị trí, như các trường riêng biệt. Nếu không có tham số vị trí ( $#là 0), thì "$@"sẽ mở rộng thành không có gì (không phải là một chuỗi rỗng, mà là một danh sách có 0 phần tử), nếu có một tham số vị trí thì "$@"tương đương với "$1", nếu có hai tham số vị trí thì "$@"tương đương với "$1" "$2", Vân vân.

"$@"cho phép bạn chuyển các đối số của tập lệnh hoặc hàm sang lệnh khác. Nó rất hữu ích cho các trình bao bọc thực hiện những việc như cài đặt biến môi trường, chuẩn bị tệp dữ liệu, v.v. trước khi gọi một lệnh có cùng các đối số và tùy chọn mà trình bao bọc được gọi.

Ví dụ, chức năng sau đây lọc đầu ra của cvs -nq update. Ngoài bộ lọc đầu ra và trạng thái trả về (là trạng thái grepthay vì của cvs), việc gọi cvssmmột số đối số hoạt động giống như gọi cvs -nq updatevới các đối số này.

cvssm () { cvs -nq update "$@" | egrep -v '^[?A]'; }

"$@"mở rộng đến danh sách các tham số vị trí. Trong các shell hỗ trợ các mảng, có một cú pháp tương tự để mở rộng sang danh sách các phần tử của mảng: "${array[@]}"(dấu ngoặc là bắt buộc ngoại trừ trong zsh). Một lần nữa, dấu ngoặc kép có phần sai lệch: chúng bảo vệ chống phân tách trường và tạo mẫu của các phần tử mảng, nhưng mỗi phần tử mảng kết thúc trong trường riêng của nó.

Một số shell cổ có lỗi được cho là: khi không có đối số vị trí, được "$@"mở rộng thành một trường duy nhất chứa một chuỗi rỗng, thay vì không có trường. Điều này dẫn đến cách giải quyết${1+"$@"} ( nổi tiếng thông qua tài liệu Perl ). Chỉ các phiên bản cũ hơn của vỏ Bourne thực tế và việc triển khai OSF1 bị ảnh hưởng, không có sự thay thế tương thích hiện đại nào của nó (tro, ksh, bash, lỗi). /bin/shkhông bị ảnh hưởng trên bất kỳ hệ thống nào được phát hành trong thế kỷ 21 mà tôi biết (trừ khi bạn tính phát hành bảo trì Tru64 và thậm chí có /usr/xpg4/bin/shan toàn nên chỉ có #!/bin/shtập lệnh bị ảnh hưởng, không phải #!/usr/bin/env shtập lệnh miễn là PATH của bạn được thiết lập để tuân thủ POSIX) . Nói tóm lại, đây là một giai thoại lịch sử mà bạn không cần phải lo lắng.


"$*"luôn mở rộng thành một từ. Từ này chứa các tham số vị trí, được nối với một khoảng trắng ở giữa. (Nói chung, dấu phân cách là ký tự đầu tiên của giá trị của IFSbiến. Nếu giá trị của IFSlà chuỗi rỗng, thì dấu phân cách là chuỗi rỗng.) Nếu không có tham số vị trí thì đó "$*"là chuỗi trống, nếu có hai chuỗi trống tham số vị trí và IFScó giá trị mặc định của nó sau đó "$*"tương đương với "$1 $2", v.v.

$@$*báo giá bên ngoài là tương đương. Chúng mở rộng sang danh sách các tham số vị trí, như các trường riêng biệt, như "$@"; nhưng mỗi trường kết quả sau đó được chia thành các trường riêng biệt được coi là các mẫu ký tự đại diện tên tệp, như thường lệ với các mở rộng biến không được trích dẫn.

Ví dụ, nếu thư mục hiện hành chứa ba file bar, bazfoo, sau đó:

set --         # no positional parameters
for x in "$@"; do echo "$x"; done  # prints nothing
for x in "$*"; do echo "$x"; done  # prints 1 empty line
for x in $*; do echo "$x"; done    # prints nothing
set -- "b* c*" "qux"
echo "$@"      # prints `b* c* qux`
echo "$*"      # prints `b* c* qux`
echo $*        # prints `bar baz c* qux`
for x in "$@"; do echo "$x"; done  # prints 2 lines: `b* c*` and `qux`
for x in "$*"; do echo "$x"; done  # prints 1 lines: `b* c* qux`
for x in $*; do echo "$x"; done    # prints 4 lines: `bar`, `baz`, `c*` and `qux`

1
Ghi chú lịch sử: trên một số vỏ Bourne cổ đại, "$@"thực sự đã mở rộng thành một danh sách liên quan đến chuỗi trống: unix.stackexchange.com/questions/68484/ Lỗi
ninjalj

25

Đây là một kịch bản đơn giản để chứng minh sự khác biệt giữa $*$@:

#!/bin/bash

test_param() {
  echo "Receive $# parameters"
  echo Using '$*'

  echo
  for param in $*; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$*"'
  for param in "$*"; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '$@'
  for param in $@; do
    printf '==>%s<==\n' "$param"
  done;

  echo
  echo Using '"$@"';
  for param in "$@"; do
  printf '==>%s<==\n' "$param"
  done
}

IFS="^${IFS}"

test_param 1 2 3 "a b c"

Đầu ra:

% cuonglm at ~
% bash test.sh
Receive 4 parameters

Using $*
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$*"
==>1^2^3^a b c<==

Using $@
==>1<==
==>2<==
==>3<==
==>a<==
==>b<==
==>c<==

Using "$@"
==>1<==
==>2<==
==>3<==
==>a b c<==

Trong cú pháp mảng, không có sự khác biệt khi sử dụng $*hoặc $@. Nó chỉ có ý nghĩa khi bạn sử dụng chúng với dấu ngoặc kép "$*""$@".


Ví dụ tuyệt vời! Bạn có thể giải thích việc sử dụng IFS="^${IFS}", mặc dù?
Nga

@Russ: Nó cho bạn thấy giá trị của concat với ký tự đầu tiên trong IFS.
cuonglm

Theo cách tương tự IFS="^xxxxx"sẽ làm gì? ${IFS}Hậu tố kéo dài khiến tôi nghĩ rằng bạn đang làm một cái gì đó phức tạp hơn, như bằng cách nào đó tự động khôi phục IFS ban đầu ở cuối (ví dụ: char đầu tiên tự động chuyển ra hoặc một cái gì đó).
Nga

11

Mã bạn cung cấp sẽ cho kết quả tương tự. Để hiểu rõ hơn về nó, hãy thử điều này:

foo () {
    for i in "$*"; do
        echo "$i"
    done
}

bar () {
    for i in "$@"; do
        echo "$i"
    done
}

Đầu ra bây giờ phải khác. Đây là những gì tôi nhận được:

$ foo() 1 2 3 4
1 2 3 4
$ bar() 1 2 3 4
1
2
3
4

Điều này làm việc cho tôi trên bash. Theo tôi biết, ksh không nên khác nhau nhiều. Về cơ bản, trích dẫn $*sẽ coi mọi thứ là một từ và trích dẫn $@sẽ coi danh sách này là các từ riêng biệt, như có thể thấy trong ví dụ trên.

Như một ví dụ về việc sử dụng IFSbiến với $*, hãy xem xét điều này

fooifs () {
    IFS="c"            
    for i in "$*"; do
        echo "$i"
    done
    unset IFS          # reset to the original value
}

Tôi nhận được điều này là kết quả:

$ fooifs 1 2 3 4
1c2c3c4

Ngoài ra, tôi vừa xác nhận nó hoạt động tương tự ksh. Cả hai bashkshđược thử nghiệm ở đây đều thuộc OSX nhưng tôi không thể thấy điều đó sẽ quan trọng như thế nào.


"Thay đổi trong một chức năng - không ảnh hưởng đến toàn cầu". Bạn đã kiểm tra điều đó?
Mikel

Vâng, tôi đã kiểm tra điều đó. Để an tâm, tôi sẽ thêm phần unset IFScuối để đặt lại thành bản gốc, nhưng nó không có vấn đề gì với tôi và thực hiện echo $IFSkết quả là đầu ra tiêu chuẩn tôi nhận được từ nó. Đặt IFSdấu ngoặc nhọn giới thiệu một phạm vi mới, vì vậy trừ khi bạn xuất nó, nó sẽ không ảnh hưởng đến bên ngoài IFS.
Wojtek Rzepala

echo $IFSkhông chứng minh bất cứ điều gì, bởi vì shell nhìn thấy ,, nhưng sau đó chia tách từ bằng cách sử dụng IFS! Hãy thử echo "$IFS".
Mikel

điểm tốt. unset IFSnên giải quyết đó.
Wojtek Rzepala

Trừ khi IFScó một giá trị tùy chỉnh khác nhau trước khi gọi hàm. Nhưng có, hầu hết thời gian không đặt IFS sẽ hoạt động.
Mikel

6

Sự khác biệt rất quan trọng khi viết các tập lệnh nên sử dụng các tham số vị trí theo đúng cách ...

Hãy tưởng tượng cuộc gọi sau:

$ myuseradd -m -c "Carlos Campderrós" ccampderros

Ở đây chỉ có 4 tham số:

$1 => -m
$2 => -c
$3 => Carlos Campderrós
$4 => ccampderros

Trong trường hợp của tôi, myuseraddchỉ là một trình bao bọc cho useraddviệc chấp nhận các tham số tương tự, nhưng thêm một hạn ngạch cho người dùng:

#!/bin/bash -e

useradd "$@"
setquota -u "${!#}" 10000 11000 1000 1100

Thông báo cuộc gọi đến useradd "$@", với $@trích dẫn. Điều này sẽ tôn trọng các tham số và gửi chúng như chúng là useradd. Nếu bạn bỏ yêu cầu $@(hoặc sử dụng $*cũng không được trích dẫn), useradd sẽ thấy 5 tham số, vì tham số thứ 3 chứa một khoảng trắng sẽ được chia thành hai:

$1 => -m
$2 => -c
$3 => Carlos
$4 => Campderrós
$5 => ccampderros

(và ngược lại, nếu bạn đã sử dụng "$*", useradd sẽ chỉ thấy một tham số -m -c Carlos Campderrós ccampderros:)

Vì vậy, trong ngắn hạn, nếu bạn cần làm việc với các tham số tôn trọng các tham số nhiều từ, hãy sử dụng "$@".


4
   *      Expands  to  the positional parameters, starting from one.  When
          the expansion occurs within double quotes, it expands to a  sin
          gle word with the value of each parameter separated by the first
          character of the IFS special variable.  That is, "$*" is equiva
          lent to "$1c$2c...", where c is the first character of the value
          of the IFS variable.  If IFS is unset, the parameters are  sepa
          rated  by  spaces.   If  IFS  is null, the parameters are joined
          without intervening separators.
   @      Expands to the positional parameters, starting from  one.   When
          the  expansion  occurs  within  double  quotes,  each  parameter
          expands to a separate word.  That is, "$@" is equivalent to "$1"
          "$2"  ...   If the double-quoted expansion occurs within a word,
          the expansion of the first parameter is joined with  the  begin
          ning  part  of  the original word, and the expansion of the last
          parameter is joined with the last part  of  the  original  word.
          When  there  are no positional parameters, "$@" and $@ expand to
          nothing (i.e., they are removed).

// người đàn ông bash . là ksh, afair, hành vi tương tự.


2

Nói về sự khác biệt giữa zshbash:

Với các trích dẫn xung quanh $@$*, zshbashhành xử giống nhau, và tôi đoán kết quả khá chuẩn trong số tất cả các shell:

 $ f () { for i in "$@"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a+
 +b+
 ++
 $ f () { for i in "$*"; do echo +"$i"+; done; }; f 'a a' 'b' ''
 +a a b +

Không có dấu ngoặc kép, kết quả là giống nhau cho $*$@, nhưng khác nhau trong bashvà trong zsh. Trong trường hợp này zshcho thấy một số hành vi kỳ lạ:

bash$ f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''
+a+
+a+
+b+
zsh% f () { for i in $*; do echo +"$i"+; done; }; f 'a a' 'b' ''  
+a a+
+b+

(Zsh thường không phân chia dữ liệu văn bản bằng IFS, trừ khi được yêu cầu rõ ràng, nhưng lưu ý rằng ở đây đối số trống bị thiếu bất ngờ trong danh sách.)


1
Trong zsh, $@không đặc biệt về mặt này: $xmở rộng tối đa một từ, nhưng các biến rỗng mở rộng thành không có gì (không phải là một từ trống). Hãy thử print -l a $foo bvới foosản phẩm nào hoặc không xác định.
Gilles

0

Một trong những câu trả lời cho biết $*(mà tôi nghĩ là "splat") hiếm khi hữu ích.

Tôi tìm kiếm trên google với G() { IFS='+' ; w3m "https://encrypted.google.com/search?q=$*" ; }

Vì URL thường được phân chia bằng một +, nhưng bàn phím của tôi giúp truy cập   dễ dàng hơn +, $*+ $IFScảm thấy đáng giá.

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.