Έχω μια σύνθετη εντολή που θα ήθελα να κάνω ένα shell/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
Επαναδιατύπωση μιας διαγραμμένης πλέον απάντησης από τον VonC.</sup>,
Η συνοπτική απάντηση του Robert Gamble'ασχολείται άμεσα με το ερώτημα.
Αυτή εδώ ενισχύει κάποια ζητήματα σχετικά με τα ονόματα αρχείων που περιέχουν κενά.
Βλέπε επίσης: ${1:+"$@"} στο /bin/sh.
Βασική θέση: Το "$@"
είναι σωστό, και το $*
(χωρίς εισαγωγικά) είναι σχεδόν πάντα λάθος.
Αυτό συμβαίνει επειδή το "$@"
λειτουργεί καλά όταν τα ορίσματα περιέχουν κενά, και το
λειτουργεί το ίδιο με το $*
όταν δεν το κάνουν.
Σε ορισμένες περιπτώσεις, το "$*"
είναι επίσης εντάξει, αλλά το "$@"
συνήθως (αλλά όχι
πάντα) λειτουργεί στα ίδια μέρη.
Χωρίς εισαγωγικά, τα $@
και $*
είναι ισοδύναμα (και σχεδόν πάντα λάθος).
Ποια είναι λοιπόν η διαφορά μεταξύ των $*
, $@
, "$*"
, και "$@"
; Όλα σχετίζονται με το 'όλα τα ορίσματα στο κέλυφος', αλλά κάνουν διαφορετικά πράγματα. Όταν δεν έχουν εισαγωγικά, τα $*
και $@
κάνουν το ίδιο πράγμα. Αντιμετωπίζουν κάθε 'λέξη' (ακολουθία μη κενών διαστημάτων) ως ξεχωριστό όρισμα. Οι παρατιθέμενες μορφές είναι αρκετά διαφορετικές, όμως: "$*"
αντιμετωπίζει τη λίστα με τα ορίσματα ως ένα ενιαίο αλφαριθμητικό με διαχωρισμένα κενά, ενώ "$@"
αντιμετωπίζει τα ορίσματα σχεδόν όπως ακριβώς ήταν όταν καθορίστηκαν στη γραμμή εντολών.
Το "$@"
επεκτείνεται σε τίποτα όταν δεν υπάρχουν ορίσματα θέσης- το "$*"
επεκτείνεται σε μια κενή συμβολοσειρά &mdash- και ναι, υπάρχει διαφορά, αν και μπορεί να είναι δύσκολο να την αντιληφθείτε.
Δείτε περισσότερες πληροφορίες παρακάτω, μετά την εισαγωγή της (μη τυπικής) εντολής 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
$
Αυτό γίνεται πραγματικά δύσκολο όταν έχετε ονόματα αρχείων όπως "Είπε, "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, ένα όνομα αρχείου (μεμονωμένο συστατικό) μπορεί να περιέχει οποιουσδήποτε χαρακτήρες εκτός από
slash και NUL '\0'
.
Ωστόσο, τα κελύφη ενθαρρύνουν έντονα να μην υπάρχουν κενά ή newlines ή tabs
πουθενά σε ένα όνομα διαδρομής.
Αυτός είναι και ο λόγος για τον οποίο τα τυπικά ονόματα αρχείων του Unix δεν περιέχουν κενά κ.λπ.
Όταν έχετε να κάνετε με ονόματα αρχείων που μπορεί να περιέχουν κενά και άλλα
προβληματικούς χαρακτήρες, πρέπει να είστε εξαιρετικά προσεκτικοί, και βρήκα ότι
πριν από πολύ καιρό ότι χρειαζόμουν ένα πρόγραμμα που δεν είναι τυπικό στο Unix.
Το ονομάζω escape
(η έκδοση 1.1 είχε ημερομηνία 1989-08-23T16:01:45Z).
Εδώ είναι ένα παράδειγμα του escape
σε χρήση - με το σύστημα ελέγχου SCCS.
Πρόκειται για ένα σενάριο κάλυψης που κάνει τόσο ένα delta
(σκεφτείτε check-in) όσο και ένα
get
(σκεφτείτε check-out).
Διάφορα ορίσματα, ειδικά το -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"
(Πιθανώς δεν θα χρησιμοποιούσα την escape τόσο διεξοδικά στις μέρες μας - είναι
δεν χρειάζεται με το όρισμα -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
'.
<br>,
]
Χμμ, αυτή'είναι μια αρκετά μεγάλη απάντηση - ίσως η εξέταση είναι ο καλύτερος όρος.
Ο πηγαίος κώδικας για το escape
διατίθεται κατόπιν αιτήματος (email στο firstname dot
επώνυμο στο gmail dot com).
Ο πηγαίος κώδικας για το al
είναι απίστευτα απλός:
#include <stdio.h>
int main(int argc, char **argv)
{
while (*++argv != 0)
puts(*argv);
return(0);
}
Αυτό είναι όλο. Είναι ισοδύναμο με το σενάριο test.sh
που έδειξε ο Robert Gamble, και θα μπορούσε να γραφτεί ως συνάρτηση κελύφους (αλλά οι συναρτήσεις κελύφους δεν υπήρχαν στην τοπική έκδοση του Bourne shell όταν έγραψα για πρώτη φορά το al
).
Σημειώστε επίσης ότι μπορείτε να γράψετε το al
ως ένα απλό σενάριο κελύφους:
[ $# != 0 ] && printf "%s\n" "$@"
Η συνθήκη είναι απαραίτητη ώστε να μην παράγει καμία έξοδο όταν δεν περνάει κανένα όρισμα. Η εντολή printf
θα παράγει μια κενή γραμμή μόνο με το όρισμα format string, αλλά το πρόγραμμα 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 των Kernighan και Pike.
Στο bash
, η help for
το τεκμηριώνει αυτό:
for NAME [in WORDS ... ;] do COMMANDS; done
>, Εάν δεν υπάρχει το'in WORDS ...;'
, τότε θεωρείται ότι υπάρχει το'in "$@"'
.