อะไรคือความแตกต่างระหว่าง $ * และ $ @


72

พิจารณารหัสต่อไปนี้:

foo () {
    echo $*
}

bar () {
    echo $@
}

foo 1 2 3 4
bar 1 2 3 4

มันเอาท์พุท:

1 2 3 4

1 2 3 4

ฉันใช้ Ksh88 แต่ฉันสนใจเชลล์ทั่วไปอื่น ๆ ด้วย หากคุณทราบถึงความพิเศษใด ๆ ของเชลล์ที่เฉพาะเจาะจงโปรดอย่าพูดถึงมัน

ฉันพบ follwing ในหน้าคน Ksh บน Solaris:

ความหมายของ $ * และ $ @ เหมือนกันเมื่อไม่ได้อ้างถึงหรือเมื่อใช้เป็นค่าการกำหนดพารามิเตอร์หรือเป็นชื่อไฟล์ อย่างไรก็ตามเมื่อใช้เป็นอาร์กิวเมนต์คำสั่ง $ * จะเท่ากับ `` $ 1d $ 2d ... '' โดยที่ d คืออักขระตัวแรกของตัวแปร IFS ในขณะที่ $ @ เท่ากับ $ 1 $ 2 ....

ฉันพยายามแก้ไขIFSตัวแปร แต่ไม่ได้แก้ไขผลลัพธ์ บางทีฉันอาจจะทำอะไรผิดพลาด?

คำตอบ:


95

เมื่อพวกเขาจะไม่ยกมา$*และ$@เหมือนกัน คุณไม่ควรใช้อย่างใดอย่างหนึ่งเหล่านี้เนื่องจากพวกเขาสามารถแตกโดยไม่คาดคิดทันทีที่คุณมีอาร์กิวเมนต์ที่มีช่องว่างหรืออักขระตัวแทน


"$*""$1c$2c..."ขยายคำเดียว โดยปกติแล้วcจะมีช่องว่าง แต่จริงๆแล้วมันเป็นตัวอักษรตัวแรกของIFSดังนั้นมันสามารถเป็นอะไรก็ได้ที่คุณเลือก

การใช้งานที่ดีอย่างเดียวที่ฉันเคยพบคือ:

เข้าร่วมข้อโต้แย้งด้วยเครื่องหมายจุลภาค (เวอร์ชันง่าย)

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

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

เข้าร่วมข้อโต้แย้งด้วยตัวคั่นที่ระบุ (รุ่นที่ดีกว่า)

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

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

"$@" ขยายเป็นคำแยก: "$1" "$2" ...

นี่คือสิ่งที่คุณต้องการเกือบทุกครั้ง มันขยายแต่ละพารามิเตอร์ตำแหน่งเป็นคำที่แยกต่างหากซึ่งทำให้มันสมบูรณ์แบบสำหรับการรับบรรทัดคำสั่งหรือฟังก์ชั่นการขัดแย้งในแล้วส่งพวกเขาไปยังคำสั่งหรือฟังก์ชั่นอื่น และเนื่องจากมันขยายโดยใช้เครื่องหมายคำพูดคู่หมายความว่าสิ่งต่าง ๆ ไม่แตกถ้าพูด"$1"มีช่องว่างหรือเครื่องหมายดอกจัน ( *)


ลองเขียนสคริปต์ที่เรียกว่าsvimที่ทำงานด้วยvim sudoเราจะทำสามเวอร์ชันเพื่อแสดงความแตกต่าง

svim1

#!/bin/sh
sudo vim $*

svim2

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

svim3

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

ทั้งหมดจะใช้ได้กับกรณีง่ายๆเช่นชื่อไฟล์เดียวที่ไม่มีช่องว่าง:

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

แต่จะใช้ได้$*และ"$@"ทำงานอย่างถูกต้องหากคุณมีหลายข้อโต้แย้ง

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"

และมีเพียง"$*"และ"$@"ทำงานอย่างถูกต้องหากคุณมีข้อโต้แย้งที่มีช่องว่าง

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"

ดังนั้น"$@"จะทำงานได้อย่างถูกต้องตลอดเวลา


typesetเป็นวิธีการสร้างตัวแปรท้องถิ่นในksh( bashและashใช้localแทน) หมายความว่าIFSจะคืนค่าเป็นค่าก่อนหน้าเมื่อฟังก์ชันส่งคืน สิ่งนี้มีความสำคัญเนื่องจากคำสั่งที่คุณเรียกใช้หลังจากนั้นอาจทำงานไม่ถูกต้องหากIFSตั้งค่าเป็นสิ่งที่ไม่ได้มาตรฐาน


2
คำอธิบายที่ยอดเยี่ยมขอบคุณมาก
rahmu

$*ขอขอบคุณสำหรับตัวอย่างของการใช้ ฉันมักจะคิดว่ามันไร้ประโยชน์อย่างสมบูรณ์ ... การเข้าร่วมกับตัวคั่นเป็นกรณีการใช้งานที่ดี
anishsane

35

คำตอบสั้น ๆ : ใช้"$@" (จดเครื่องหมายคำพูดคู่) รูปแบบอื่นมีประโยชน์น้อยมาก

"$@"เป็นไวยากรณ์ที่ค่อนข้างแปลก มันถูกแทนที่ด้วยพารามิเตอร์ตำแหน่งทั้งหมดเป็นฟิลด์ที่แยกต่างหาก หากไม่มีพารามิเตอร์ตำแหน่ง ( $#เป็น 0) ดังนั้น"$@"จะขยายเป็นไม่มีอะไร (ไม่ใช่สตริงว่าง แต่เป็นรายการที่มีองค์ประกอบ 0 รายการ) หากมีพารามิเตอร์ตำแหน่งหนึ่งพารามิเตอร์"$@"จะเทียบเท่ากับ"$1"หากมีพารามิเตอร์ตำแหน่งสองพารามิเตอร์นั้น"$@"จะเทียบเท่ากับ"$1" "$2"ฯลฯ

"$@"อนุญาตให้คุณส่งอาร์กิวเมนต์ของสคริปต์หรือฟังก์ชันไปยังคำสั่งอื่น มันมีประโยชน์มากสำหรับ wrappers ที่ทำสิ่งต่าง ๆ เช่นการตั้งค่าตัวแปรสภาพแวดล้อมการเตรียมไฟล์ข้อมูล ฯลฯ ก่อนที่จะเรียกคำสั่งด้วยอาร์กิวเมนต์และตัวเลือกเดียวกันกับที่ wrapper ถูกเรียกด้วย

cvs -nq updateยกตัวอย่างเช่นฟังก์ชันต่อไปนี้กรองการส่งออกของ นอกเหนือจากการกรองผลลัพธ์และสถานะการคืนสินค้า (ซึ่งเป็นของgrepมากกว่าcvs) การเรียกcvssmอาร์กิวเมนต์บางตัวจะทำงานเหมือนกับการโทรcvs -nq updateด้วยอาร์กิวเมนต์เหล่านี้

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

"$@"ขยายไปยังรายการพารามิเตอร์ตำแหน่ง ในเชลล์ที่สนับสนุนอาร์เรย์มีไวยากรณ์ที่คล้ายกันเพื่อขยายไปยังรายการองค์ประกอบของอาร์เรย์: "${array[@]}"(วงเล็บปีกกาจำเป็นต้องมียกเว้นใน zsh) อีกครั้งเครื่องหมายคำพูดคู่จะทำให้เข้าใจผิดค่อนข้าง: พวกเขาป้องกันการแยกฟิลด์และการสร้างรูปแบบขององค์ประกอบอาร์เรย์ แต่แต่ละองค์ประกอบอาร์เรย์สิ้นสุดในฟิลด์ของตัวเอง

กระสุนโบราณบางตัวมีสิ่งที่เป็นข้อบกพร่อง: เมื่อไม่มีข้อโต้แย้งเกี่ยวกับตำแหน่งให้"$@"ขยายไปยังเขตข้อมูลเดียวที่มีสตริงว่างแทนที่จะเป็นไม่มีฟิลด์ สิ่งนี้นำไปสู่การแก้ปัญหา${1+"$@"} (ทำชื่อเสียงผ่านเอกสาร Perl ) เฉพาะ Bourne shell รุ่นเก่าและการใช้งาน OSF1 เท่านั้นที่ได้รับผลกระทบไม่มีการแทนที่ที่เข้ากันได้ที่ทันสมัย ​​(ash, ksh, bash, …) /bin/shไม่ได้รับผลกระทบในระบบใด ๆ ที่เปิดตัวในศตวรรษที่ 21 ที่ฉันรู้ (เว้นแต่คุณจะนับการบำรุงรักษา Tru64 และแม้จะมี/usr/xpg4/bin/shความปลอดภัยดังนั้น#!/bin/shสคริปต์เท่านั้นที่ได้รับผลกระทบไม่ใช่#!/usr/bin/env shสคริปต์ตราบใดที่ PATH ของคุณตั้งค่าสำหรับการปฏิบัติตาม POSIX) . กล่าวโดยสังเขปนี้เป็นบันทึกทางประวัติศาสตร์ที่คุณไม่จำเป็นต้องกังวล


"$*"ขยายเป็นหนึ่งคำเสมอ คำนี้มีพารามิเตอร์ตำแหน่งเชื่อมต่อกันด้วยช่องว่างระหว่าง (โดยทั่วไปตัวคั่นเป็นอักขระตัวแรกของค่าของIFSตัวแปรหากค่าของIFSคือสตริงว่างตัวคั่นเป็นสตริงว่าง) หากไม่มีพารามิเตอร์ตำแหน่ง"$*"จะเป็นสตริงว่างถ้ามีสอง พารามิเตอร์ตำแหน่งและIFSมีค่าเริ่มต้นแล้ว"$*"เทียบเท่ากับ"$1 $2"ฯลฯ

$@และ$*คำพูดนอกนั้นเทียบเท่า พวกเขาขยายไปยังรายการของพารามิเตอร์ตำแหน่งเป็นฟิลด์แยกเช่น"$@"; แต่แต่ละฟิลด์ผลลัพธ์จะถูกแยกออกเป็นฟิลด์ที่แยกต่างหากซึ่งถือว่าเป็นรูปแบบของชื่อไฟล์ไวด์การ์ดตามปกติด้วยการขยายตัวแปรที่ไม่มีเครื่องหมาย

ตัวอย่างเช่นถ้าไดเรกทอรีปัจจุบันมีสามไฟล์bar, bazและfooแล้ว:

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
บันทึกประวัติ: บนเชลล์ Bourne โบราณบาง"$@"ตัวได้ขยายไปยังรายการที่พิจารณาของสตริงว่าง: unix.stackexchange.com/questions/68484/…
ninjalj

25

นี่เป็นสคริปต์ง่ายๆที่แสดงให้เห็นถึงความแตกต่างระหว่าง$*และ$@:

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

เอาท์พุท:

% 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<==

ในไวยากรณ์อาร์เรย์ไม่มีความแตกต่างเมื่อใช้หรือ$* $@มันให้ความรู้สึกเมื่อคุณใช้พวกเขาด้วยคำพูดสองและ"$*""$@"


ตัวอย่างที่ยอดเยี่ยม! คุณช่วยอธิบายการใช้งานได้IFS="^${IFS}"มั้ย
รัส

@Russ: มันแสดงให้เห็นว่าค่า concat IFSกับตัวอักษรตัวแรกใน
cuonglm

ในทำนองเดียวกันว่าIFS="^xxxxx"จะทำอย่างไร? ต่อท้าย${IFS}คำต่อท้ายทำให้ฉันคิดว่าคุณกำลังทำบางสิ่งบางอย่างพลิกแพลงเช่นอย่างใดโดยอัตโนมัติฟื้นตัวไอเอฟเอเดิมที่สิ้นสุด (เช่นถ่านแรกขยับออกโดยอัตโนมัติหรือบางอย่าง)
รัส

11

รหัสที่คุณระบุจะให้ผลลัพธ์เดียวกัน เพื่อความเข้าใจที่ดีขึ้นลองสิ่งนี้:

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

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

ผลลัพธ์ควรแตกต่างกันในขณะนี้ นี่คือสิ่งที่ฉันได้รับ:

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

bashนี้ทำงานสำหรับฉันใน เท่าที่ฉันรู้ ksh ไม่ควรแตกต่างกันมาก โดยพื้นฐานแล้วการอ้างอิง$*จะถือว่าทุกอย่างเป็นคำเดียวและการอ้างอิง$@จะถือว่ารายการเป็นคำที่แยกจากกันดังที่เห็นในตัวอย่างด้านบน

เป็นตัวอย่างของการใช้IFSตัวแปรด้วย$*พิจารณาเรื่องนี้

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

ฉันได้รับสิ่งนี้เป็นผล:

$ fooifs 1 2 3 4
1c2c3c4

kshนอกจากนี้ผมได้รับการยืนยันเป็นเพียงแค่การทำงานที่เหมือนกันใน ทั้งสองbashและkshทดสอบที่นี่อยู่ภายใต้ OSX แต่ฉันไม่สามารถดูว่าจะมีความสำคัญมาก


"เปลี่ยนภายในฟังก์ชั่น - ไม่ส่งผลกระทบต่อโลก" คุณทดสอบหรือไม่
มิเคล

ใช่ฉันตรวจสอบแล้ว เพื่อความสบายใจฉันจะเพิ่มunset IFSที่ส่วนท้ายเพื่อรีเซ็ตเป็นต้นฉบับ แต่มันทำงานได้ดีสำหรับฉันโดยไม่มีปัญหาและการทำecho $IFSผลลัพธ์เป็นผลลัพธ์มาตรฐานที่ฉันได้รับจากมัน การตั้งค่าภายในวงเล็บปีกกาแนะนำขอบเขตใหม่ดังนั้นถ้าคุณส่งออกมันจะไม่ส่งผลกระทบภายนอกIFS IFS
Wojtek Rzepala

echo $IFSไม่ได้พิสูจน์อะไรเลยเพราะเชลล์เห็น,แล้ว แต่ก็ใช้คำแยกกันIFS! ลองecho "$IFS"ดู
Mikel

จุดดี. การตั้งค่าIFSควรแก้ปัญหานั้น
Wojtek Rzepala

ยกเว้นIFSว่ามีค่าที่กำหนดเองที่แตกต่างกันก่อนที่จะเรียกใช้ฟังก์ชัน แต่ใช่แล้วส่วนใหญ่การยกเลิกการตั้งค่า IFS จะใช้งานได้
มิเคล

6

ความแตกต่างสำคัญเมื่อเขียนสคริปต์ที่ควรใช้พารามิเตอร์ตำแหน่งในทางที่ถูกต้อง ...

ลองนึกภาพการโทรต่อไปนี้:

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

ที่นี่มีเพียง 4 พารามิเตอร์:

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

ในกรณีของฉันmyuseraddเป็นเพียงเสื้อคลุมสำหรับuseraddที่ยอมรับพารามิเตอร์เดียวกัน แต่เพิ่มโควต้าสำหรับผู้ใช้:

#!/bin/bash -e

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

แจ้งให้ทราบการโทรไปuseradd "$@"พร้อมกับใบ$@เสนอราคา useraddนี้จะเคารพพารามิเตอร์และส่งพวกเขาเป็นพวกเขาจะ- หากคุณต้องเลิกทำเครื่องหมายอัญประกาศ$@(หรือใช้$*เลิกทำเครื่องหมายอัญประกาศ) ผู้ใช้จะเห็น5พารามิเตอร์เนื่องจากพารามิเตอร์ตัวที่ 3 ซึ่งมีช่องว่างจะถูกแบ่งออกเป็นสองส่วน:

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

(และตรงกันข้ามถ้าคุณมีการใช้"$*", useradd จะเห็นเพียงหนึ่งพารามิเตอร์: -m -c Carlos Campderrós ccampderros)

"$@"ดังนั้นในระยะสั้นถ้าคุณต้องการที่จะทำงานกับพารามิเตอร์เคารพพารามิเตอร์หลายคำใช้


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

// คนทุบตี คือ ksh, afair, พฤติกรรมที่คล้ายกัน


2

พูดคุยเกี่ยวกับความแตกต่างระหว่างzshและbash:

ด้วยคำพูดรอบ$@และ$*, zshและbashประพฤติตัวเหมือนกันและผมคิดว่าผลที่ได้คือค่อนข้างมาตรฐานในหมู่หอยทั้งหมด:

 $ 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 +

ไม่ทราบราคาผลจะเหมือนกันสำหรับ$*และ$@แต่แตกต่างกันในและในbash zshในกรณีนี้zshแสดงพฤติกรรมแปลก ๆ :

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 จะไม่แยกข้อมูลที่เป็นข้อความโดยใช้ IFS เว้นแต่จะมีการร้องขออย่างชัดเจน แต่โปรดสังเกตว่าที่นี่อาร์กิวเมนต์ว่างเปล่าหายไปโดยไม่คาดคิดในรายการ)


1
ใน zsh $@ไม่ได้มีความพิเศษในแง่นี้: $xขยายได้สูงสุดหนึ่งคำ แต่ตัวแปรที่ว่างเปล่าจะขยายออกเป็นไม่มีอะไร (ไม่ใช่คำที่ว่างเปล่า) ลองprint -l a $foo bกับfooว่างเปล่าหรือไม่ได้กำหนด
Gilles

0

หนึ่งในคำตอบบอกว่า$*(ซึ่งฉันคิดว่าเป็น "เครื่องหมาย") ไม่ค่อยมีประโยชน์

ฉันค้นหา google ด้วย G() { IFS='+' ; w3m "https://encrypted.google.com/search?q=$*" ; }

ตั้งแต่ URL ที่มักจะถูกแยกออกด้วย+แต่แป้นพิมพ์ของฉันทำให้   ง่ายต่อการเข้าถึงกว่า+, $*+ $IFSรู้สึกคุ้มค่า

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.