ฉันมีคำสั่งที่ซับซ้อนที่ฉันต้องการสร้างเชลล์ / bash script ของ ฉันสามารถเขียน$1ได้อย่างง่ายดาย:
foo $1 args -o $1.ext
ฉันต้องการส่งชื่ออินพุตหลาย ๆ ตัวให้สคริปต์ วิธีที่ถูกต้องในการทำคืออะไร?
และแน่นอนฉันต้องการจัดการชื่อไฟล์ด้วยช่องว่างในนั้น
ฉันมีคำสั่งที่ซับซ้อนที่ฉันต้องการสร้างเชลล์ / bash script ของ ฉันสามารถเขียน$1ได้อย่างง่ายดาย:
foo $1 args -o $1.ext
ฉันต้องการส่งชื่ออินพุตหลาย ๆ ตัวให้สคริปต์ วิธีที่ถูกต้องในการทำคืออะไร?
และแน่นอนฉันต้องการจัดการชื่อไฟล์ด้วยช่องว่างในนั้น
คำตอบ:
ใช้"$@"เพื่อเป็นตัวแทนของข้อโต้แย้งทั้งหมด:
for var in "$@"
do
echo "$var"
done
สิ่งนี้จะวนซ้ำแต่ละอาร์กิวเมนต์และพิมพ์ออกมาเป็นบรรทัดแยก $ @ ทำงานเหมือน $ * ยกเว้นว่าเมื่ออ้างถึงข้อโต้แย้งจะถูกทำลายอย่างถูกต้องหากมีช่องว่างในพวกเขา:
sh test.sh 1 2 '3 4'
1
2
3 4
"$@"มันคือมันจะขยายตัวเพื่ออะไรเมื่อไม่มีพารามิเตอร์ตำแหน่งในขณะที่"$*"ขยายไปยังสตริงที่ว่างเปล่า - และใช่มีความแตกต่างระหว่างไม่มีข้อโต้แย้งและอาร์กิวเมนต์ที่ว่างเปล่าอย่างใดอย่างหนึ่ง ดู/${1:+"$@"} in bin / sh`
$varฉันสามารถใช้อาร์กิวเมนต์ได้
shiftโยนออกไป$1แล้วเลื่อนองค์ประกอบที่ตามมาทั้งหมดลง
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 ผลิตอะไร
โปรดทราบว่าคำตอบของ 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 "$@"'จะถือว่า
"$@"วิธีการและเมื่อคุณรู้ว่าสิ่งที่หมายถึงมันเป็นไม่น้อยกว่าอ่านfor i for i in "$@"
for iดีกว่าเพราะมันช่วยกดแป้นพิมพ์ ผมเทียบการอ่านของและfor i for i in "$@"
shiftสำหรับกรณีที่เรียบง่ายคุณยังสามารถใช้ มันถือว่ารายการอาร์กิวเมนต์เช่นคิว แต่ละshiftโยนอาร์กิวเมนต์แรกออกและดัชนีของแต่ละอาร์กิวเมนต์ที่เหลือจะลดลง
#this prints all arguments
while test $# -gt 0
do
echo "$1"
shift
done
shiftแบบวนรอบองค์ประกอบนั้นยังคงเป็นส่วนหนึ่งของอาเรshiftย์
echo "$1"เพื่อรักษาระยะห่างในค่าของสตริงอาร์กิวเมนต์ นอกจากนี้ (เป็นปัญหาเล็กน้อย) สิ่งนี้เป็นอันตราย: คุณไม่สามารถนำข้อโต้แย้งกลับมาใช้ใหม่ได้หากคุณเลื่อนมันไปทั้งหมด บ่อยครั้งที่นั่นไม่ใช่ปัญหาคุณประมวลผลรายการอาร์กิวเมนต์เพียงครั้งเดียว
--file myfile.txt $ 1 คือพารามิเตอร์ $ 2 คือค่าและคุณเรียก shift เป็นสองครั้งเมื่อคุณต้องการข้ามอาร์กิวเมนต์อื่น
นอกจากนี้คุณยังสามารถเข้าถึงพวกเขาเป็นองค์ประกอบอาร์เรย์ตัวอย่างเช่นถ้าคุณไม่ต้องการที่จะทำซ้ำพวกเขาทั้งหมด
argc=$#
argv=("$@")
for (( j=0; j<argc; j++ )); do
echo "${argv[j]}"
done
./my-script.sh param1 "param2 is a quoted param": - ใช้ argv = ($ @) คุณจะได้รับ [param1, param2] - ใช้ argv = ("$ @") คุณจะได้รับ [param1, param2 เป็นพารามิเตอร์ที่ยกมา]
aparse() {
while [[ $# > 0 ]] ; do
case "$1" in
--arg1)
varg1=${2}
shift
;;
--arg2)
varg2=true
;;
esac
shift
done
}
aparse "$@"
[ $# != 0 ]จะดีกว่า ในข้างต้น#$ควรจะ$#เป็นในwhile [[ $# > 0 ]] ...
การขยายคำตอบของ 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
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