Bash betiğinde argümanlar üzerinde yineleme

Kabuk/bash betiği yapmak istediğim karmaşık bir komutum var. Bunu $1 cinsinden kolayca yazabilirim:

foo $1 args -o $1.ext

Script'e birden fazla girdi adı aktarabilmek istiyorum. Bunu yapmanın doğru yolu nedir?

Ve elbette, içinde boşluk olan dosya adlarını işlemek istiyorum.

Çözüm

Tüm argümanları temsil etmek için "$@" kullanın:

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

Bu, her argümanı yineleyecek ve ayrı bir satıra yazdıracaktır. $@, $* gibi davranır, ancak alıntılandığında argümanlar içinde boşluk varsa düzgün bir şekilde ayrılır:

sh test.sh 1 2 '3 4'
1
2
3 4
Yorumlar (7)

Şimdi silinmiş bir answer'ın VonC tarafından yeniden yazılması. Robert Gamble'ın kısa ve özlü cevabı doğrudan soruyla ilgilidir. Bu cevap, boşluk içeren dosya adları ile ilgili bazı sorunları güçlendirmektedir. Ayrıca bakınız: ${1:+"$@"} in /bin/sh Temel tez: "$@" doğrudur ve $* (tırnaksız) neredeyse her zaman yanlıştır. Çünkü "$@" argümanlar boşluk içerdiğinde iyi çalışır ve çalışmadığında $* ile aynı şekilde çalışır. Bazı durumlarda, "$*" da tamamdır, ancak "$@" genellikle (ancak her zaman) aynı yerlerde çalışır. Tırnak içine alınmamış, $@ ve $* eşdeğerdir (ve neredeyse her zaman yanlıştır). Peki, $*, $@, "$*" ve "$@" arasındaki fark nedir? Hepsi 'kabuğun tüm argümanları' ile ilgilidir, ancak farklı şeyler yaparlar. Tırnak içine alınmadığında, $* ve $@ aynı şeyi yapar. Her 'kelimeyi' (boşluksuz diziler) ayrı bir argüman olarak ele alırlar. Ancak alıntılanmış formlar oldukça farklıdır: "$*" argüman listesini boşluk bırakılarak ayrılmış tek bir dize olarak ele alırken, "$@" argümanları neredeyse tam olarak komut satırında belirtildikleri gibi ele alır. "$@" pozisyonel argümanlar olmadığında hiçbir şeye genişlemez; "$*" boş bir dizeye genişler — ve evet, algılamak zor olsa da bir fark var'dır. Aşağıda, (standart olmayan) al komutunun tanıtımından sonra daha fazla bilgiye bakın. İkincil tez: boşluk içeren argümanları işlemeniz gerekiyorsa ve ardından bunları diğer komutlara aktarırsanız, bazen standart olmayan yardımcı olacak araçlar. (Ya da dizileri dikkatli kullanmalısınız: "${array[@]}", "$@" ile benzer şekilde davranır). Örnek:

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

Bu neden işe yaramıyor? Çalışmıyor çünkü kabuk tırnak işaretlerini genişletmeden önce işliyor değişkenler. Yani, kabuğun $var içine gömülü tırnak işaretlerine dikkat etmesini sağlamak için, 'eval' kullanmanız gerekir:

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

gibi dosya adlarına sahip olduğunuzda bu gerçekten zorlaşır, "Don't do this!"`" (tırnak işaretleri, çift tırnak işaretleri ve boşluklarla).

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

Kabuklar (hepsi) bu tür işlemlerin yapılmasını özellikle kolaylaştırmaz şeyler, bu yüzden (yeterince komik bir şekilde) birçok Unix programı iyi bir iş yapmıyor onları idare etmek. Unix'te, bir dosya adı (tek bileşen) aşağıdakiler dışında herhangi bir karakter içerebilir eğik çizgi ve NUL '\0'. Bununla birlikte, kabuklar boşluk, yeni satır veya sekme olmamasını şiddetle tavsiye eder bir yol adının herhangi bir yerinde. Standart Unix dosya adlarının boşluk vb. içermemesinin nedeni de budur. Boşluk içerebilen dosya adları ile uğraşırken ve diğer sorunlu karakterler, son derece dikkatli olmak zorundasınız ve ben Uzun zaman önce Unix'te standart olmayan bir programa ihtiyacım olduğunu fark ettim. Ben buna escape adını verdim (1.1 sürümü 1989-08-23T16:01:45Z tarihlidir). İşte SCCS kontrol sistemi ile kullanılan bir escape örneği. Bu, hem delta (check-in olarak düşünün) hem de get (check-out olarak düşünün). Çeşitli argümanlar, özellikle -y (değişikliği neden yaptığınız) boşlukları ve satırsonlarını içerecektir. Kodun 1992'den kalma olduğunu unutmayın, bu nedenle $(cmd ...)notasyonunu kullanır ve ilk satırda#!/bin/sh` kullanmaz.

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

(Muhtemelen bugünlerde kaçışı bu kadar kapsamlı kullanmazdım - bu örneğin -e argümanı ile gerekli değildir - ancak genel olarak bu kullanarak daha basit komut dosyalarımdan biri). escapeprogramı,echogibi argümanlarını basitçe çıktı olarak verir yapar, ancak argümanların aşağıdakilerle kullanım için korunmasını sağlar eval(evalin bir seviyesi; uzaktan kabukla çalışan bir programım var yürütme veescape` çıktısından kaçmak için gerekli olan).

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

Argümanlarını her satırda bir listeleyen al adında başka bir programım var (ve daha da eskidir: 1987-01-27T14:35:49 tarihli 1.1 sürümü). En çok komut dosyalarında hata ayıklama yaparken kullanışlıdır, çünkü bir komut satırında komuta gerçekte hangi argümanların aktarıldığını görmek için.

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

[Eklendi: Ve şimdi çeşitli "$@" gösterimleri arasındaki farkı göstermek için, işte bir örnek daha:

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

Komut satırındaki * ve */* arasındaki orijinal boşlukların hiçbir şekilde korunmadığına dikkat edin. Ayrıca, kabukta 'komut satırı argümanlarını' kullanarak değiştirebileceğinizi unutmayın:

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

Bu, '-new', '-opt', 'and' ve 'arg with space' olmak üzere 4 seçenek belirler.
] Hmm, bu oldukça uzun bir cevap - belki de tefsir daha iyi bir terimdir. escapeiçin kaynak kodu istek üzerine mevcuttur (ilkisim nokta'ya e-posta lastname at gmail dot com). al için kaynak kodu inanılmaz derecede basittir:

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

Hepsi bu. Robert Gambleın gösterdiğitest.shbetiğine eşdeğerdir ve bir kabuk fonksiyonu olarak yazılabilir (ancakalı ilk yazdığımda Bourne kabuğunun yerel sürümünde kabuk fonksiyonları yoktu). Ayrıcaal` komutunu basit bir kabuk betiği olarak yazabileceğinizi de unutmayın:

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

Hiçbir argüman iletilmediğinde hiçbir çıktı üretmemesi için koşul gereklidir. printf` komutu sadece format string argümanı ile boş bir satır üretecektir, ancak C programı hiçbir şey üretmez.

Yorumlar (1)

Robert'ın cevabının doğru olduğunu ve `sh' içinde de çalıştığını unutmayın. Bunu (taşınabilir şekilde) daha da basitleştirebilirsiniz:

for i in "$@"

eşdeğerdir:

for i

Yani, hiçbir şeye ihtiyacınız yok!

Test ($ komut istemidir):

$ 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

Bu konuyu ilk olarak Kernighan ve Pike tarafından yazılan Unix Programming Environment adlı kitapta okumuştum.

bashiçinde,help for` bunu belgelemektedir:

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

Eğer 'in WORDS ...;' mevcut değilse, o zaman 'in "$@"' olduğu varsayılır.

Yorumlar (10)