Bagaimana cara mengulangi argumen dalam skrip Bash

900

Saya memiliki perintah kompleks yang ingin saya buat skrip shell / bash. Saya bisa menulisnya $1dengan mudah:

foo $1 args -o $1.ext

Saya ingin dapat memberikan beberapa nama input ke skrip. Apa cara yang benar untuk melakukannya?

Dan, tentu saja, saya ingin menangani nama file dengan spasi di dalamnya.

Thelema
sumber
67
FYI, itu ide yang baik untuk selalu menempatkan parameter dalam tanda kutip. Anda tidak pernah tahu kapan parm mungkin mengandung ruang yang disematkan.
David R Tribble

Jawaban:

1482

Gunakan "$@"untuk mewakili semua argumen:

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

Ini akan mengulangi setiap argumen dan mencetaknya pada baris yang berbeda. $ @ berperilaku seperti $ * kecuali bahwa ketika dikutip argumen dipecah dengan benar jika ada spasi di dalamnya:

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
sumber
38
Kebetulan, salah satu kelebihan lainnya "$@"adalah ia tidak berkembang ketika tidak ada parameter posisi sedangkan "$*"ekspansi ke string kosong - dan ya, ada perbedaan antara tidak ada argumen dan satu argumen kosong. Lihat ${1:+"$@"} in / bin / sh` .
Jonathan Leffler
9
Perhatikan bahwa notasi ini juga harus digunakan dalam fungsi shell untuk mengakses semua argumen ke fungsi.
Jonathan Leffler
Dengan menjatuhkan tanda kutip ganda: $varSaya bisa mengeksekusi argumen.
eonist
bagaimana saya memulai dari argumen kedua? Maksudku, aku butuh ini tetapi selalu mulai dari argumen kedua, ini, lewati $ 1.
m4l490n
4
@ m4l490n: shiftmembuang $1, dan menggeser semua elemen berikutnya.
MSalters
239

Tulis ulang jawaban yang sekarang dihapus oleh VonC .

Jawaban singkat Robert Gamble berkaitan langsung dengan pertanyaan itu. Yang ini menguatkan pada beberapa masalah dengan nama file yang mengandung spasi.

Lihat juga: $ {1: + "$ @"} di / bin / sh

Tesis dasar: "$@" benar, dan $*(tidak dikutip) hampir selalu salah. Ini karena "$@"berfungsi dengan baik ketika argumen berisi spasi, dan berfungsi sama seperti $*ketika mereka tidak. Dalam beberapa keadaan, "$*"juga OK, tetapi "$@"biasanya (tetapi tidak selalu) bekerja di tempat yang sama. Tidak dikutip, $@dan $*setara (dan hampir selalu salah).

Jadi, apa perbedaan antara $*, $@, "$*", dan "$@"? Mereka semua terkait dengan 'semua argumen ke shell', tetapi mereka melakukan hal-hal yang berbeda. Ketika dikutip, $*dan $@lakukan hal yang sama. Mereka memperlakukan setiap 'kata' (urutan non-spasi putih) sebagai argumen terpisah. Bentuk-bentuk yang dikutip sangat berbeda, meskipun: "$*"memperlakukan daftar argumen sebagai string yang dipisahkan oleh spasi, sedangkan "$@"memperlakukan argumen hampir persis seperti ketika ditentukan pada baris perintah. "$@"tidak berkembang sama sekali ketika tidak ada argumen posisi; "$*"meluas ke string kosong - dan ya, ada perbedaan, meskipun sulit untuk melihatnya. Lihat informasi lebih lanjut di bawah ini, setelah pengenalan perintah (non-standar) al.

Tesis sekunder: jika Anda perlu memproses argumen dengan spasi dan kemudian meneruskannya ke perintah lain, maka Anda terkadang memerlukan alat yang tidak standar untuk membantu. (Atau Anda harus menggunakan array, dengan hati-hati: "${array[@]}"berperilaku analog dengan "$@".)

Contoh:

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

Mengapa itu tidak berhasil? Itu tidak berfungsi karena shell memproses tanda kutip sebelum memperluas variabel. Jadi, untuk mendapatkan shell agar memperhatikan kutipan yang tertanam di dalamnya $var, Anda harus menggunakan 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
    $ 

Ini menjadi sangat sulit ketika Anda memiliki nama file seperti " He said, "Don't do this!"" (dengan tanda kutip dan tanda kutip ganda dan spasi).

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

Kerang (semuanya) tidak membuatnya sangat mudah untuk menangani hal-hal seperti itu, jadi (cukup lucu) banyak program Unix tidak melakukan pekerjaan yang baik untuk menanganinya. Pada Unix, nama file (komponen tunggal) dapat berisi karakter apa pun kecuali slash dan NUL '\0'. Namun, shell sangat tidak mendorong spasi atau baris baru atau tab di mana pun dalam nama jalur. Itu juga mengapa nama file Unix standar tidak mengandung spasi, dll.

Ketika berhadapan dengan nama file yang mungkin mengandung spasi dan karakter merepotkan lainnya, Anda harus sangat berhati-hati, dan saya telah lama mengetahui bahwa saya memerlukan program yang tidak standar pada Unix. Saya menyebutnya escape(versi 1.1 bertanggal 1989-08-23T16: 01: 45Z).

Berikut adalah contoh escapepenggunaan - dengan sistem kontrol SCCS. Ini adalah skrip sampul yang melakukan a delta(think check-in ) dan a get(think check-out ). Berbagai argumen, terutama -y(alasan mengapa Anda melakukan perubahan) akan berisi baris kosong dan baru. Perhatikan bahwa skrip berasal dari tahun 1992, sehingga menggunakan back-ticks sebagai ganti $(cmd ...)notasi dan tidak digunakan #!/bin/shpada baris pertama.

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

(Saya mungkin tidak akan menggunakan escape dengan sangat teliti akhir-akhir ini - itu tidak diperlukan dengan -eargumen, misalnya - tetapi secara keseluruhan, ini adalah salah satu skrip saya yang lebih sederhana menggunakan escape.)

The escapeProgram hanya output argumen, agak seperti echo tidak, tetapi memastikan bahwa argumen dilindungi untuk digunakan dengan eval(satu tingkat dari eval; Saya punya sebuah program yang melakukan terpencil eksekusi shell, dan yang diperlukan untuk melarikan diri output escape).

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

Saya memiliki program lain yang disebut alyang daftar argumennya satu per baris (dan itu bahkan lebih kuno: versi 1.1 tanggal 1987-01-27T14: 35: 49). Ini paling berguna ketika men-debug skrip, karena dapat dicolokkan ke baris perintah untuk melihat argumen apa yang sebenarnya diteruskan ke perintah.

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

[ Ditambahkan: Dan sekarang untuk menunjukkan perbedaan antara berbagai "$@"notasi, berikut adalah satu contoh lagi:

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

Perhatikan bahwa tidak ada yang mempertahankan kosong asli antara *dan */*pada baris perintah. Juga, perhatikan bahwa Anda dapat mengubah 'argumen baris perintah' di shell dengan menggunakan:

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

Ini menetapkan 4 opsi, ' -new', ' -opt', ' and', dan ' arg with space'.
]

Hmm, itu jawaban yang cukup panjang - mungkin penafsiran adalah istilah yang lebih baik. Kode sumber escapetersedia berdasarkan permintaan (email ke firstname dot lastname di gmail dot com). Kode sumber alsangat sederhana:

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

Itu saja. Ini setara dengan test.shskrip yang diperlihatkan oleh Robert Gamble, dan dapat ditulis sebagai fungsi shell (tetapi fungsi shell tidak ada di versi lokal Bourne shell ketika saya pertama kali menulis al).

Perhatikan juga bahwa Anda dapat menulis alsebagai skrip shell sederhana:

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

Persyaratan diperlukan sehingga tidak menghasilkan output ketika melewati tidak ada argumen. The printfperintah akan menghasilkan baris kosong dengan hanya argumen format string, tetapi program C menghasilkan apa-apa.

Jonathan Leffler
sumber
132

Perhatikan bahwa jawaban Robert benar, dan itu juga berfungsi sh. Anda dapat (dengan mudah) menyederhanakannya lebih jauh:

for i in "$@"

setara dengan:

for i

Yaitu, Anda tidak perlu apa-apa!

Pengujian ( $adalah perintah prompt):

$ 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

Saya pertama kali membaca tentang ini di Lingkungan Pemrograman Unix oleh Kernighan dan Pike.

Dalam bash, help fordokumentasikan ini:

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

Jika 'in WORDS ...;'tidak ada, maka 'in "$@"'diasumsikan.

Alok Singhal
sumber
4
Saya tidak setuju. Seseorang harus tahu apa arti cryptic "$@", dan begitu Anda tahu apa for iartinya, itu tidak lebih mudah dibaca daripada for i in "$@".
Alok Singhal
10
Saya tidak menegaskan bahwa for iitu lebih baik karena menghemat penekanan tombol. Saya membandingkan keterbacaan for idan for i in "$@".
Alok Singhal
inilah yang saya cari - apa yang mereka sebut asumsi $ @ in loop di mana Anda tidak harus secara eksplisit merujuknya? Apakah ada kerugian untuk tidak menggunakan referensi $ @?
qodeninja
58

Untuk kasus sederhana, Anda juga dapat menggunakan shift. Ini memperlakukan daftar argumen seperti antrian. Masing-masing shiftmembuang argumen pertama dan indeks masing-masing argumen yang tersisa dikurangi.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
sumber
Saya pikir jawaban ini lebih baik karena jika Anda lakukan shiftdalam for for loop, elemen itu masih merupakan bagian dari array, sedangkan dengan ini shiftberfungsi seperti yang diharapkan.
DavidG
2
Perhatikan bahwa Anda harus menggunakan echo "$1"untuk menjaga jarak dalam nilai string argumen. Juga, (masalah kecil) ini destruktif: Anda tidak dapat menggunakan kembali argumen jika Anda sudah memindahkan semuanya. Seringkali, itu bukan masalah - Anda hanya memproses daftar argumen sekali saja.
Jonathan Leffler
1
Setuju bahwa shift lebih baik, karena Anda memiliki cara mudah untuk mengambil dua argumen dalam case switch untuk memproses argumen flag seperti "-o outputfile".
Baxissimo
Di sisi lain, jika Anda mulai mengurai argumen flag, Anda mungkin ingin mempertimbangkan bahasa skrip yang lebih kuat daripada bash :)
nuoritoveri
4
Solusi ini lebih baik jika Anda memerlukan pasangan nilai parameter, misalnya --file myfile.txt Jadi, $ 1 adalah parameter, $ 2 adalah nilai dan Anda memanggil shift dua kali ketika Anda perlu melewati argumen lain
Daniele Licitra
16

Anda juga dapat mengaksesnya sebagai elemen array, misalnya jika Anda tidak ingin mengulanginya semua

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
sumber
5
Saya percaya baris argv = ($ @) harus argv = ("$ @") argumen bijak lainnya dengan spasi tidak ditangani dengan benar
kdubs
Saya mengkonfirmasi sintaks yang benar adalah argv = ("$ @"). kecuali Anda tidak akan mendapatkan nilai terakhir jika Anda memiliki parameter yang dikutip. Misalnya jika Anda menggunakan sesuatu seperti./my-script.sh param1 "param2 is a quoted param" : - using argv = ($ @) Anda akan mendapatkan [param1, param2], - menggunakan argv = ("$ @"), Anda akan mendapatkan [param1, param2 adalah param yang dikutip]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
sumber
1
Seperti yang dinyatakan di bawah [ $# != 0 ]ini lebih baik. Di atas, #$harus $#seperti dalamwhile [[ $# > 0 ]] ...
hute37
1

Memperkuat jawaban baz, jika Anda perlu menyebutkan daftar argumen dengan indeks (seperti untuk mencari kata tertentu), Anda dapat melakukan ini tanpa menyalin daftar atau mengubahnya.

Katakanlah Anda ingin membagi daftar argumen di tanda hubung ganda ("-") dan meneruskan argumen sebelum tanda hubung ke satu perintah, dan argumen setelah tanda hubung ke yang lain:

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

Hasilnya akan terlihat seperti ini:

 $ 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
Kadel yang kaya
sumber
1

getopt Gunakan perintah dalam skrip Anda untuk memformat opsi atau parameter baris perintah.

#!/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
JimmyLandStudios
sumber
Yang rumit, saya akan meneliti ini secara menyeluruh. Contoh: file: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Manual: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios