Cách lặp lại các đối số trong tập lệnh Bash


900

Tôi có một lệnh phức tạp mà tôi muốn tạo một kịch bản shell / bash. Tôi có thể viết nó $1một cách dễ dàng:

foo $1 args -o $1.ext

Tôi muốn có thể chuyển nhiều tên đầu vào cho tập lệnh. Cách đúng đắn để làm điều đó là gì?

Và, tất nhiên, tôi muốn xử lý tên tập tin có khoảng trắng trong đó.


67
FYI, đó là một ý tưởng tốt để luôn luôn đặt tham số trong dấu ngoặc kép. Bạn không bao giờ biết khi nào một parm có thể chứa một không gian nhúng.
David R Tribble

Câu trả lời:


1482

Sử dụng "$@"để đại diện cho tất cả các đối số:

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

Điều này sẽ lặp lại qua từng đối số và in ra trên một dòng riêng biệt. $ @ hành xử như $ * ngoại trừ khi được trích dẫn, các đối số được chia nhỏ đúng cách nếu có khoảng trắng trong chúng:

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

38
Ngẫu nhiên, một trong những ưu điểm khác của "$@"nó là nó mở rộng thành không có gì khi không có tham số vị trí trong khi "$*"mở rộng thành chuỗi rỗng - và vâng, có một sự khác biệt giữa không có đối số và một đối số trống. Xem ${1:+"$@"} in / thùng / sh` .
Jonathan Leffler

9
Lưu ý rằng ký hiệu này cũng nên được sử dụng trong các hàm shell để truy cập tất cả các đối số vào hàm.
Jonathan Leffler

Bằng cách bỏ dấu ngoặc kép: $varTôi đã có thể thực thi các đối số.
eonist

Làm thế nào để tôi bắt đầu từ đối số thứ hai? Ý tôi là, tôi cần cái này nhưng luôn bắt đầu từ đối số thứ hai, đây là, bỏ qua $ 1.
m4l490n

4
@ m4l490n: shiftvứt bỏ $1và dịch chuyển tất cả các yếu tố tiếp theo.
MSalters

239

Viết lại câu trả lời đã bị xóa bởi VonC .

Câu trả lời ngắn gọn của Robert Gamble liên quan trực tiếp đến câu hỏi. Cái này khuếch đại một số vấn đề với tên tệp chứa khoảng trắng.

Xem thêm: $ {1: + "$ @"} trong / bin / sh

Luận án cơ bản: "$@" là chính xác, và $*(không trích dẫn) hầu như luôn luôn sai. Điều này là do "$@"hoạt động tốt khi các đối số chứa khoảng trắng và hoạt động giống như $*khi chúng không hoạt động. Trong một số trường hợp, "$*"cũng ổn, nhưng "$@"thường (nhưng không phải luôn luôn) hoạt động ở cùng một nơi. Không trích dẫn, $@$*tương đương (và hầu như luôn luôn sai).

Vì vậy, sự khác biệt giữa là những gì $*, $@, "$*", và "$@"? Tất cả chúng đều liên quan đến 'tất cả các đối số cho vỏ', nhưng chúng làm những việc khác nhau. Khi không được trích dẫn, $*$@làm điều tương tự. Họ coi mỗi "từ" (chuỗi không phải khoảng trắng) là một đối số riêng biệt. Mặc dù vậy, các biểu mẫu được trích dẫn khá khác nhau: "$*"coi danh sách đối số là một chuỗi được phân tách bằng dấu cách, trong khi "$@"xử lý các đối số gần như chính xác khi chúng được chỉ định trên dòng lệnh. "$@"mở rộng thành không có gì cả khi không có đối số vị trí; "$*"mở rộng thành một chuỗi trống - và vâng, có một sự khác biệt, mặc dù có thể khó nhận biết nó. Xem thêm thông tin bên dưới, sau khi giới thiệu lệnh (không chuẩn) al.

Luận án phụ: nếu bạn cần xử lý các đối số với khoảng trắng và sau đó chuyển chúng sang các lệnh khác, thì đôi khi bạn cần các công cụ không chuẩn để hỗ trợ. (Hoặc bạn nên sử dụng mảng, cẩn thận: "${array[@]}"hành xử tương tự "$@".)

Thí dụ:

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

Tại sao nó không hoạt động? Nó không hoạt động vì shell xử lý dấu ngoặc kép trước khi nó mở rộng các biến. Vì vậy, để có được trình bao chú ý đến các trích dẫn được nhúng trong $var, bạn phải sử dụng 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
    $ 

Điều này thực sự khó khăn khi bạn có tên tệp như " He said, "Don't do this!"" (với dấu ngoặc kép và dấu ngoặc kép và dấu cách).

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

Các shell (tất cả chúng) không làm cho nó dễ dàng xử lý những thứ như vậy, vì vậy (đủ vui) nhiều chương trình Unix không làm tốt việc xử lý chúng. Trên Unix, một tên tệp (thành phần đơn) có thể chứa bất kỳ ký tự nào ngoại trừ dấu gạch chéo và NUL '\0'. Tuy nhiên, hệ vỏ khuyến khích mạnh mẽ không có khoảng trắng hoặc dòng mới hoặc tab ở bất kỳ đâu trong tên đường dẫn. Đó cũng là lý do tại sao tên tệp Unix tiêu chuẩn không chứa khoảng trắng, v.v.

Khi xử lý các tên tệp có thể chứa dấu cách và các ký tự rắc rối khác, bạn phải cực kỳ cẩn thận và từ lâu tôi đã thấy rằng tôi cần một chương trình không chuẩn trên Unix. Tôi gọi nó escape(phiên bản 1.1 là ngày 1989-08-23T16: 01: 45Z).

Dưới đây là một ví dụ về escapeviệc sử dụng - với hệ thống điều khiển SCCS. Nó là một kịch bản bao gồm cả delta(nghĩ kiểm tra ) và get(nghĩ kiểm tra ). Nhiều đối số khác nhau, đặc biệt -y(lý do tại sao bạn thực hiện thay đổi) sẽ chứa khoảng trống và dòng mới. Lưu ý rằng tập lệnh có từ năm 1992, vì vậy nó sử dụng dấu tick thay vì $(cmd ...)ký hiệu và không sử dụng #!/bin/shtrên dòng đầu tiên.

:   "@(#)$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"

(Tôi có thể sẽ không sử dụng lối thoát khá kỹ lưỡng trong những ngày này - chẳng hạn như không cần thiết với -eđối số - nhưng về tổng thể, đây là một trong những tập lệnh đơn giản hơn của tôi sử dụng escape.)

Các escapechương trình chỉ đơn giản là kết quả đầu ra đối số của nó, chứ không phải như echo hiện, nhưng nó đảm bảo rằng các đối số được bảo vệ để sử dụng với eval(một mức độ eval, tôi không có một chương trình mà đã thực hiện remote shell, và điều đó cần thiết để thoát sản lượng escape).

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

Tôi có một chương trình khác được gọi là alliệt kê các đối số của nó trên mỗi dòng (và nó thậm chí còn cổ hơn: phiên bản 1.1 ngày 1987-01-27T14: 35: 49). Nó hữu ích nhất khi gỡ lỗi các tập lệnh, vì nó có thể được cắm vào một dòng lệnh để xem những đối số nào thực sự được truyền cho lệnh.

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

[ Đã thêm: Và bây giờ để hiển thị sự khác biệt giữa các "$@"ký hiệu khác nhau , đây là một ví dụ nữa:

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

Lưu ý rằng không có gì bảo tồn khoảng trống ban đầu giữa **/*trên dòng lệnh. Ngoài ra, lưu ý rằng bạn có thể thay đổi 'đối số dòng lệnh' trong trình bao bằng cách sử dụng:

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

Điều này đặt 4 tùy chọn, ' -new', ' -opt', ' and' và ' arg with space'.
]

Hmm, đó là một câu trả lời khá dài - có lẽ exegesis là thuật ngữ tốt hơn. Mã nguồn escapecó sẵn theo yêu cầu (gửi email đến tên đầu tiên dấu chấm cuối cùng tại gmail dot com). Mã nguồn alrất đơn giản:

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

Đó là tất cả. Nó tương đương với test.shkịch bản mà Robert Gamble đã trình bày và có thể được viết dưới dạng hàm shell (nhưng các hàm shell không tồn tại trong phiên bản shell của Bourne khi tôi viết lần đầu tiên al).

Cũng lưu ý rằng bạn có thể viết aldưới dạng một tập lệnh shell đơn giản:

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

Điều kiện là cần thiết để nó không tạo ra đầu ra khi thông qua không có đối số. Các printflệnh sẽ sản xuất một dòng trống chỉ với những lập luận chuỗi định dạng, nhưng chương trình C sản xuất gì cả.


132

Lưu ý rằng câu trả lời của Robert là chính xác và nó cũng hoạt động sh. Bạn có thể (đơn giản) đơn giản hóa hơn nữa:

for i in "$@"

tương đương với:

for i

Tức là bạn không cần gì cả!

Kiểm tra ( $là dấu nhắc lệnh):

$ 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

Lần đầu tiên tôi đọc về điều này trong Môi trường lập trình Unix của Kernighan và Pike.

Trong bash, help fortài liệu này:

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

Nếu 'in WORDS ...;'không có mặt, thì 'in "$@"'được giả định.


4
Tôi không đồng ý. Người ta phải biết mật mã "$@"có nghĩa là gì, và một khi bạn biết ý for inghĩa của nó, nó không thể đọc được hơn for i in "$@".
Alok Singhal

10
Tôi không khẳng định điều đó for itốt hơn bởi vì nó tiết kiệm tổ hợp phím. Tôi so sánh khả năng đọc của for ifor i in "$@".
Alok Singhal

đây là những gì tôi đang tìm kiếm - họ gọi giả định này là $ @ trong các vòng lặp mà bạn không phải tham chiếu rõ ràng về nó? Có một nhược điểm nào khi không sử dụng tham chiếu $ @?
qodeninja

58

Đối với trường hợp đơn giản, bạn cũng có thể sử dụng shift. Nó xử lý danh sách đối số như một hàng đợi. Mỗi lần shiftném ra đối số đầu tiên và chỉ số của từng đối số còn lại được giảm xuống.

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

Tôi nghĩ rằng câu trả lời này là tốt hơn bởi vì nếu bạn thực hiện shifttrong một vòng lặp for, phần tử đó vẫn là một phần của mảng, trong khi với điều này shifthoạt động như mong đợi.
DavidG

2
Lưu ý rằng bạn nên sử dụng echo "$1"để duy trì khoảng cách trong giá trị của chuỗi đối số. Ngoài ra, (một vấn đề nhỏ) đây là sự phá hoại: bạn không thể sử dụng lại các đối số nếu bạn đã chuyển tất cả chúng đi. Thông thường, đó không phải là vấn đề - dù sao bạn cũng chỉ xử lý danh sách đối số.
Jonathan Leffler

1
Đồng ý rằng sự thay đổi là tốt hơn, bởi vì sau đó bạn có một cách dễ dàng để lấy hai đối số trong trường hợp chuyển đổi để xử lý các đối số cờ như "-o outputfile".
Baxissimo

Mặt khác, nếu bạn đang bắt đầu phân tích các đối số cờ, bạn có thể muốn xem xét một ngôn ngữ kịch bản mạnh hơn bash :)
nuoritoveri

4
Giải pháp này tốt hơn nếu bạn cần một vài giá trị tham số, ví dụ: --file myfile.txt Vì vậy, $ 1 là tham số, $ 2 là giá trị và bạn gọi shift hai lần khi bạn cần bỏ qua một đối số khác
Daniele Licitra

16

Bạn cũng có thể truy cập chúng dưới dạng một phần tử mảng, ví dụ nếu bạn không muốn lặp qua tất cả chúng

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

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

5
Tôi tin rằng dòng argv = ($ @) nên là argv = ("$ @") các đối số khôn ngoan khác có khoảng trắng không được xử lý chính xác
kdub

Tôi xác nhận cú pháp đúng là argv = ("$ @"). trừ khi bạn sẽ không nhận được giá trị cuối cùng nếu bạn có một tham số được trích dẫn. Ví dụ: nếu bạn sử dụng một cái gì đó như ./my-script.sh param1 "param2 is a quoted param": - sử dụng argv = ($ @) bạn sẽ nhận được [param1, param2], - sử dụng argv = ("$ @"), bạn sẽ nhận được [param1, param2 là một param được trích dẫn]
jseguillon

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

aparse "$@"

1
Như đã nêu dưới đây [ $# != 0 ]là tốt hơn. Ở trên, #$nên $#như trongwhile [[ $# > 0 ]] ...
hute37

1

Khuếch đại câu trả lời của baz, nếu bạn cần liệt kê danh sách đối số bằng một chỉ mục (chẳng hạn như tìm kiếm một từ cụ thể), bạn có thể làm điều này mà không cần sao chép danh sách hoặc thay đổi nó.

Giả sử bạn muốn phân tách một danh sách đối số tại dấu gạch ngang kép ("-") và chuyển các đối số trước dấu gạch ngang sang một lệnh và các đối số sau dấu gạch ngang sang dấu khác:

 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:$#}"
 }

Kết quả sẽ như thế này:

 $ 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 Sử dụng lệnh trong tập lệnh của bạn để định dạng bất kỳ tùy chọn hoặc tham số dòng lệnh nào.

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

Lừa một cái, tôi sẽ nghiên cứu kỹ điều này. Ví dụ: file: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Hướng dẫn sử dụng: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios
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.