วิธีการวนซ้ำอาร์กิวเมนต์ในสคริปต์ Bash


900

ฉันมีคำสั่งที่ซับซ้อนที่ฉันต้องการสร้างเชลล์ / bash script ของ ฉันสามารถเขียน$1ได้อย่างง่ายดาย:

foo $1 args -o $1.ext

ฉันต้องการส่งชื่ออินพุตหลาย ๆ ตัวให้สคริปต์ วิธีที่ถูกต้องในการทำคืออะไร?

และแน่นอนฉันต้องการจัดการชื่อไฟล์ด้วยช่องว่างในนั้น


67
FYI เป็นความคิดที่ดีที่จะใส่พารามิเตอร์ไว้ในเครื่องหมายคำพูดเสมอ คุณไม่มีทางรู้ว่าเมื่อ parm อาจมีพื้นที่ฝังตัว
David R Tribble

คำตอบ:


1481

ใช้"$@"เพื่อเป็นตัวแทนของข้อโต้แย้งทั้งหมด:

for var in "$@"
do
    echo "$var"
done

สิ่งนี้จะวนซ้ำแต่ละอาร์กิวเมนต์และพิมพ์ออกมาเป็นบรรทัดแยก $ @ ทำงานเหมือน $ * ยกเว้นว่าเมื่ออ้างถึงข้อโต้แย้งจะถูกทำลายอย่างถูกต้องหากมีช่องว่างในพวกเขา:

sh test.sh 1 2 '3 4'
1
2
3 4

38
บังเอิญหนึ่งในข้อดีอื่น ๆ ของ"$@"มันคือมันจะขยายตัวเพื่ออะไรเมื่อไม่มีพารามิเตอร์ตำแหน่งในขณะที่"$*"ขยายไปยังสตริงที่ว่างเปล่า - และใช่มีความแตกต่างระหว่างไม่มีข้อโต้แย้งและอาร์กิวเมนต์ที่ว่างเปล่าอย่างใดอย่างหนึ่ง ดู/${1:+"$@"} in bin / sh`
Jonathan Leffler

9
โปรดทราบว่าควรใช้สัญกรณ์นี้ในฟังก์ชันเชลล์เพื่อเข้าถึงอาร์กิวเมนต์ทั้งหมดของฟังก์ชัน
Jonathan Leffler

โดยการใส่เครื่องหมายคำพูดคู่: $varฉันสามารถใช้อาร์กิวเมนต์ได้
eonist

ฉันจะเริ่มจากอาร์กิวเมนต์ที่สองได้อย่างไร ฉันหมายถึงฉันต้องการสิ่งนี้ แต่เริ่มจากอาร์กิวเมนต์ที่สองเสมอนี่คือข้าม $ 1
m4l490n

4
@ m4l490n: shiftโยนออกไป$1แล้วเลื่อนองค์ประกอบที่ตามมาทั้งหมดลง
MSalters

239

Rewrite ของตอนนี้ลบคำตอบโดยVonC

คำตอบสั้น ๆ ของRobert Gambleเกี่ยวข้องโดยตรงกับคำถาม อันนี้ขยายในบางประเด็นกับชื่อไฟล์ที่มีช่องว่าง

ดูเพิ่มเติม: $ {1: + "$ @"} ใน / bin / sh

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

ดังนั้นสิ่งที่เป็นความแตกต่างระหว่าง$*, $@, "$*"และ"$@"? พวกเขาทั้งหมดเกี่ยวข้องกับ 'อาร์กิวเมนต์ทั้งหมดของเชลล์' แต่พวกเขาทำสิ่งต่าง ๆ เมื่อไม่พูดถึง$*และ$@ทำในสิ่งเดียวกัน พวกเขาปฏิบัติต่อแต่ละคำว่า '' (ลำดับของช่องว่างที่ไม่ใช่ช่องว่าง) เป็นอาร์กิวเมนต์แยกต่างหาก รูปแบบที่ยกมามีความแตกต่างกันมาก แต่: "$*"ถือว่ารายการอาร์กิวเมนต์เป็นสตริงที่คั่นด้วยช่องว่างเดียวในขณะที่"$@"ถือว่าข้อโต้แย้งเกือบตรงตามที่พวกเขาเมื่อระบุไว้ในบรรทัดคำสั่ง "$@"ขยายไปสู่อะไรเลยเมื่อไม่มีข้อโต้แย้งตำแหน่ง; "$*"ขยายเป็นสตริงว่าง - และใช่มีความแตกต่างแม้ว่ามันจะยากที่จะรับรู้ alดูข้อมูลเพิ่มเติมที่ด้านล่างหลังจากการแนะนำของคำสั่ง (ที่ไม่ได้มาตรฐาน)

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

ตัวอย่าง:

    $ mkdir "my dir" anotherdir
    $ ls
    anotherdir      my dir
    $ cp /dev/null "my dir/my file"
    $ cp /dev/null "anotherdir/myfile"
    $ ls -Fltr
    total 0
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir/
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir/
    $ ls -Fltr *
    my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ ls -Fltr "./my dir" "./anotherdir"
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ var='"./my dir" "./anotherdir"' && echo $var
    "./my dir" "./anotherdir"
    $ ls -Fltr $var
    ls: "./anotherdir": No such file or directory
    ls: "./my: No such file or directory
    ls: dir": No such file or directory
    $

ทำไมจึงไม่ทำงาน มันไม่ทำงานเพราะเชลล์ประมวลผลอัญประกาศก่อนที่จะขยายตัวแปร ดังนั้นเพื่อให้เชลล์สนใจกับเครื่องหมายคำพูดที่ฝังอยู่$varคุณต้องใช้eval:

    $ eval ls -Fltr $var
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ 

สิ่งนี้จะยุ่งยากมากเมื่อคุณมีชื่อไฟล์เช่น " He said, "Don't do this!"" (พร้อมเครื่องหมายคำพูดและเครื่องหมายคำพูดคู่และช่องว่าง)

    $ cp /dev/null "He said, \"Don't do this!\""
    $ ls
    He said, "Don't do this!"       anotherdir                      my dir
    $ ls -l
    total 0
    -rw-r--r--   1 jleffler  staff    0 Nov  1 15:54 He said, "Don't do this!"
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir
    $ 

กระสุน (ทั้งหมด) ไม่ได้ทำให้ง่ายต่อการจัดการกับสิ่งเหล่านั้นดังนั้น (อย่างพอเพียง) โปรแกรม Unix จำนวนมากไม่สามารถจัดการกับมันได้ บน Unix ชื่อไฟล์ (ส่วนหนึ่ง) สามารถมีตัวอักษรใด ๆ ยกเว้นเฉือนและ '\0'NUL อย่างไรก็ตามเชลล์ไม่สนับสนุนการเว้นวรรคหรือการขึ้นบรรทัดใหม่หรือแท็บใด ๆ ในชื่อพา ธ มันเป็นสาเหตุที่ชื่อไฟล์ Unix มาตรฐานไม่มีช่องว่าง ฯลฯ

เมื่อต้องจัดการกับชื่อไฟล์ที่อาจมีช่องว่างและตัวอักษรที่มีปัญหาอื่น ๆ คุณต้องระวังอย่างมากและฉันพบเมื่อไม่นานมานี้ว่าฉันต้องการโปรแกรมที่ไม่ได้มาตรฐานบน Unix ฉันเรียกมันว่าescape(เวอร์ชั่น 1.1 ลงวันที่ 1989-08-23T16: 01: 45Z)

นี่คือตัวอย่างของการescapeใช้งาน - กับระบบควบคุม SCCS เป็นสคริปต์หน้าปกที่ทำทั้งdelta(คิดว่าเช็คอิน ) และ get(คิดว่าเช็คเอาท์ ) ข้อโต้แย้งต่าง ๆ โดยเฉพาะ-y(เหตุผลที่คุณทำการเปลี่ยนแปลง) จะมีช่องว่างและบรรทัดใหม่ โปรดทราบว่าสคริปต์วันที่จากปี 1992 ดังนั้นจึงใช้ back-ticks แทน $(cmd ...)สัญกรณ์และไม่ได้ใช้#!/bin/shในบรรทัดแรก

:   "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $"
#
#   Delta and get files
#   Uses escape to allow for all weird combinations of quotes in arguments

case `basename $0 .sh` in
deledit)    eflag="-e";;
esac

sflag="-s"
for arg in "$@"
do
    case "$arg" in
    -r*)    gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    -e)     gargs="$gargs `escape \"$arg\"`"
            sflag=""
            eflag=""
            ;;
    -*)     dargs="$dargs `escape \"$arg\"`"
            ;;
    *)      gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    esac
done

eval delta "$dargs" && eval get $eflag $sflag "$gargs"

(ฉันอาจจะไม่ใช้การหลบหลีกค่อนข้างมากในทุกวันนี้ - มันไม่จำเป็นกับการ-eโต้แย้งเช่น - แต่โดยรวมแล้วนี่เป็นหนึ่งในสคริปต์ที่เรียบง่ายของฉันโดยใช้escape)

escapeโปรแกรมเพียงแค่เอาท์พุทอาร์กิวเมนต์ค่อนข้างชอบecho ไม่ แต่จะตรวจสอบว่าข้อโต้แย้งที่ได้รับความคุ้มครองสำหรับใช้กับ eval(หนึ่งระดับของevalฉันจะมีโปรแกรมที่ได้ดำเนินการระยะไกลเปลือกและที่จำเป็นในการหลบหนีการส่งออกของescape)

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

ฉันมีโปรแกรมอื่นที่เรียกalว่าแสดงรายการอาร์กิวเมนต์หนึ่งรายการต่อบรรทัด (และมันเก่ากว่ามาก: เวอร์ชัน 1.1 ลงวันที่ 1987-01-27T14: 35: 49) มันมีประโยชน์มากที่สุดเมื่อทำการดีบักสคริปต์เนื่องจากสามารถเสียบเข้ากับบรรทัดคำสั่งเพื่อดูว่าอาร์กิวเมนต์ใดที่ส่งผ่านไปยังคำสั่งจริง

    $ echo "$var"
    "./my dir" "./anotherdir"
    $ al $var
    "./my
    dir"
    "./anotherdir"
    $ al "$var"
    "./my dir" "./anotherdir"
    $

[ เพิ่ม: และตอนนี้เพื่อแสดงความแตกต่างระหว่าง"$@"สัญลักษณ์ต่าง ๆนี่คืออีกตัวอย่าง:

$ cat xx.sh
set -x
al $@
al $*
al "$*"
al "$@"
$ sh xx.sh     *      */*
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file'
He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file
+ al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file'
He said, "Don't do this!"
anotherdir
my dir
xx.sh
anotherdir/myfile
my dir/my file
$

ขอให้สังเกตว่าไม่มีอะไรรักษาช่องว่างดั้งเดิมระหว่าง*และ*/*บนบรรทัดคำสั่ง นอกจากนี้โปรดทราบว่าคุณสามารถเปลี่ยน 'อาร์กิวเมนต์บรรทัดคำสั่ง' ในเชลล์โดยใช้:

set -- -new -opt and "arg with space"

ชุดนี้มี 4 ตัวเลือก ' -new', ' -opt', ' and' และ ' arg with space'
]

อืมนั่นเป็นคำตอบที่ค่อนข้างยาว- บางทีการตีความอาจเป็นคำที่ดีกว่า ซอร์สโค้ดสำหรับescapeตามคำขอ (อีเมลถึงชื่อนามสกุลที่ gmail dot com) ซอร์สโค้ดสำหรับalนั้นง่ายมาก:

#include <stdio.h>
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}

นั่นคือทั้งหมดที่ มันเทียบเท่ากับtest.shสคริปต์ที่ Robert Gamble แสดงและสามารถเขียนเป็นฟังก์ชั่นเชลล์ได้ (แต่ฟังก์ชั่นเชลล์ไม่มีอยู่ในบอร์นเชลล์รุ่นท้องถิ่นเมื่อฉันเขียนครั้งแรกal)

โปรดทราบว่าคุณสามารถเขียนalเป็นสคริปต์เชลล์แบบง่าย ๆ ได้:

[ $# != 0 ] && printf "%s\n" "$@"

เงื่อนไขเป็นสิ่งจำเป็นเพื่อสร้างเอาต์พุตไม่เมื่อไม่มีการขัดแย้ง printfคำสั่งจะผลิตบรรทัดว่างมีเพียงอาร์กิวเมนต์สตริงรูปแบบ แต่โปรแกรม C ผลิตอะไร


132

โปรดทราบว่าคำตอบของ Robert นั้นถูกต้องและสามารถใช้ได้shเช่นกัน คุณสามารถลดความซับซ้อนของมันลงไปอีก:

for i in "$@"

เทียบเท่ากับ:

for i

คือคุณไม่ต้องการอะไร!

การทดสอบ ( $พร้อมรับคำสั่ง):

$ set a b "spaces here" d
$ for i; do echo "$i"; done
a
b
spaces here
d
$ for i in "$@"; do echo "$i"; done
a
b
spaces here
d

ฉันแรกอ่านเกี่ยวกับเรื่องนี้ในUnix Programming Environmentโดย Kernighan และ Pike

ในbash, help forเอกสารนี้:

for NAME [in WORDS ... ;] do COMMANDS; done

หาก'in WORDS ...;'ไม่มีอยู่ก็'in "$@"'จะถือว่า


4
ฉันไม่เห็นด้วย. หนึ่งจะต้องรู้ว่าสิ่งที่เป็นความลับ"$@"วิธีการและเมื่อคุณรู้ว่าสิ่งที่หมายถึงมันเป็นไม่น้อยกว่าอ่านfor i for i in "$@"
Alok Singhal

10
ฉันไม่ได้ยืนยันว่า for iดีกว่าเพราะมันช่วยกดแป้นพิมพ์ ผมเทียบการอ่านของและfor i for i in "$@"
Alok Singhal

นี่คือสิ่งที่ฉันกำลังมองหา - พวกเขาเรียกสิ่งนี้ว่าข้อสมมติของ $ @ ในลูปที่คุณไม่จำเป็นต้องอ้างอิงอย่างชัดเจน? มีข้อเสียคือไม่ใช้การอ้างอิง $ @ หรือไม่
qodeninja

58

shiftสำหรับกรณีที่เรียบง่ายคุณยังสามารถใช้ มันถือว่ารายการอาร์กิวเมนต์เช่นคิว แต่ละshiftโยนอาร์กิวเมนต์แรกออกและดัชนีของแต่ละอาร์กิวเมนต์ที่เหลือจะลดลง

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done

ฉันคิดว่าคำตอบนี้ดีกว่าเพราะถ้าคุณทำshiftแบบวนรอบองค์ประกอบนั้นยังคงเป็นส่วนหนึ่งของอาเรshiftย์
DavidG

2
โปรดทราบว่าคุณควรใช้echo "$1"เพื่อรักษาระยะห่างในค่าของสตริงอาร์กิวเมนต์ นอกจากนี้ (เป็นปัญหาเล็กน้อย) สิ่งนี้เป็นอันตราย: คุณไม่สามารถนำข้อโต้แย้งกลับมาใช้ใหม่ได้หากคุณเลื่อนมันไปทั้งหมด บ่อยครั้งที่นั่นไม่ใช่ปัญหาคุณประมวลผลรายการอาร์กิวเมนต์เพียงครั้งเดียว
Jonathan Leffler

1
ยอมรับการเลื่อนนั้นดีกว่าเพราะคุณมีวิธีที่ง่ายในการหยิบอาร์กิวเมนต์สองตัวในกรณีสวิตช์เพื่อประมวลผลอาร์กิวเมนต์แฟล็กเช่น "-o outputfile"
Baxissimo

ในทางตรงกันข้ามถ้าคุณจะเริ่มต้นที่จะแยกข้อโต้แย้งธงคุณอาจต้องการที่จะต้องพิจารณาเป็นภาษาสคริปต์ที่มีประสิทธิภาพมากขึ้นกว่าทุบตี :)
nuoritoveri

4
วิธีนี้จะดีกว่าถ้าคุณต้องการพารามิเตอร์สองค่าตัวอย่างเช่น--file myfile.txt $ 1 คือพารามิเตอร์ $ 2 คือค่าและคุณเรียก shift เป็นสองครั้งเมื่อคุณต้องการข้ามอาร์กิวเมนต์อื่น
Daniele Licitra

16

นอกจากนี้คุณยังสามารถเข้าถึงพวกเขาเป็นองค์ประกอบอาร์เรย์ตัวอย่างเช่นถ้าคุณไม่ต้องการที่จะทำซ้ำพวกเขาทั้งหมด

argc=$#
argv=("$@")

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done

5
ฉันเชื่อว่าบรรทัด argv = ($ @) ควรเป็น argv = ("$ @") args ที่ฉลาดอื่น ๆ ที่มีช่องว่างไม่ได้รับการจัดการอย่างถูกต้อง
kdubs

ฉันยืนยันว่าไวยากรณ์ที่ถูกต้องคือ argv = ("$ @") นอกเสียจากว่าคุณจะไม่ได้รับค่าสุดท้ายหากคุณมีพารามิเตอร์ที่ยกมา ตัวอย่างเช่นถ้าคุณใช้สิ่งที่ชอบ./my-script.sh param1 "param2 is a quoted param": - ใช้ argv = ($ @) คุณจะได้รับ [param1, param2] - ใช้ argv = ("$ @") คุณจะได้รับ [param1, param2 เป็นพารามิเตอร์ที่ยกมา]
jseguillon

2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"

1
ตามที่ระบุไว้ด้านล่าง[ $# != 0 ]จะดีกว่า ในข้างต้น#$ควรจะ$#เป็นในwhile [[ $# > 0 ]] ...
hute 37

1

การขยายคำตอบของ baz หากคุณต้องการระบุรายการอาร์กิวเมนต์ด้วยดัชนี (เช่นเพื่อค้นหาคำที่เฉพาะเจาะจง) คุณสามารถทำได้โดยไม่ต้องคัดลอกรายการหรือทำให้กลายพันธุ์

สมมติว่าคุณต้องการแยกรายการอาร์กิวเมนต์ที่ double-dash ("-") และส่งอาร์กิวเมนต์ก่อนเครื่องหมายขีดคั่นหนึ่งคำสั่งและอาร์กิวเมนต์หลังจากเครื่องหมายขีดคั่นไปยังอีก:

 toolwrapper() {
   for i in $(seq 1 $#); do
     [[ "${!i}" == "--" ]] && break
   done || return $? # returns error status if we don't "break"

   echo "dashes at $i"
   echo "Before dashes: ${@:1:i-1}"
   echo "After dashes: ${@:i+1:$#}"
 }

ผลลัพธ์ควรมีลักษณะเช่นนี้:

 $ toolwrapper args for first tool -- and these are for the second
 dashes at 5
 Before dashes: args for first tool
 After dashes: and these are for the second

1

getoptใช้คำสั่งในสคริปต์ของคุณเพื่อจัดรูปแบบตัวเลือกบรรทัดคำสั่งหรือพารามิเตอร์ใด ๆ

#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift

ฉันจะค้นคว้าเรื่องนี้อย่างละเอียดถี่ถ้วน ตัวอย่าง: ไฟล์: ///usr/share/doc/util-linux/examples/getopt-parse.bash, คู่มือ: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.