可以将文章内容翻译成中文,广告屏蔽插件可能会导致该功能失效(如失效,请关闭广告屏蔽插件后再试):
问题:
I need help with getopts
.
I created a Bash script which looks like this when run:
$ foo.sh -i env -d directory -s subdirectory -f file
It works correctly when handling one argument from each flag. But when I invoke several arguments from each flag I am not sure how to pull the multiple variable information out of the variables in getopts
.
while getopts ":i:d:s:f:" opt
do
case $opt in
i ) initial=$OPTARG;;
d ) dir=$OPTARG;;
s ) sub=$OPTARG;;
f ) files=$OPTARG;;
esac
done
After grabbing the options I then want to build directory structures from the variables
foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
Then the directory structure would be
/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3
Any ideas?
回答1:
You can use the same option multiple times and add all values to an array.
For the very specific original question here, Ryan's mkdir -p
solution is obviously the best.
However, for the more general question of getting multiple values from the same option with getopts, here it is:
#!/bin/bash
while getopts "m:" opt; do
case $opt in
m) multi+=("$OPTARG");;
#...
esac
done
shift $((OPTIND -1))
echo "The first value of the array 'multi' is '$multi'"
echo "The whole list of values is '${multi[@]}'"
echo "Or:"
for val in "${multi[@]}"; do
echo " - $val"
done
The output would be:
$ /tmp/t
The first value of the array 'multi' is ''
The whole list of values is ''
Or:
$ /tmp/t -m "one arg with spaces"
The first value of the array 'multi' is 'one arg with spaces'
The whole list of values is 'one arg with spaces'
Or:
- one arg with spaces
$ /tmp/t -m one -m "second argument" -m three
The first value of the array 'multi' is 'one'
The whole list of values is 'one second argument three'
Or:
- one
- second argument
- three
回答2:
I know this question is old, but I wanted to throw this answer on here in case someone comes looking for an answer.
Shells like BASH support making directories recursively like this already, so a script isn't really needed. For instance, the original poster wanted something like:
$ foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
/test/directory/subdirectory/file1
/test/directory/subdirectory/file2
/test/directory/subdirectory/file3
/test/directory/subdirectory2/file1
/test/directory/subdirectory2/file2
/test/directory/subdirectory2/file3
This is easily done with this command line:
pong:~/tmp
[10] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/{file1,file2,file3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
Or even a bit shorter:
pong:~/tmp
[12] rmclean$ mkdir -pv test/directory/{subdirectory,subdirectory2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory’
mkdir: created directory ‘test/directory/subdirectory/file1’
mkdir: created directory ‘test/directory/subdirectory/file2’
mkdir: created directory ‘test/directory/subdirectory/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
Or shorter, with more conformity:
pong:~/tmp
[14] rmclean$ mkdir -pv test/directory/subdirectory{1,2}/file{1,2,3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
Or lastly, using sequences:
pong:~/tmp
[16] rmclean$ mkdir -pv test/directory/subdirectory{1..2}/file{1..3}
mkdir: created directory ‘test’
mkdir: created directory ‘test/directory’
mkdir: created directory ‘test/directory/subdirectory1’
mkdir: created directory ‘test/directory/subdirectory1/file1’
mkdir: created directory ‘test/directory/subdirectory1/file2’
mkdir: created directory ‘test/directory/subdirectory1/file3’
mkdir: created directory ‘test/directory/subdirectory2’
mkdir: created directory ‘test/directory/subdirectory2/file1’
mkdir: created directory ‘test/directory/subdirectory2/file2’
mkdir: created directory ‘test/directory/subdirectory2/file3’
回答3:
getopts options can only take zero or one argument. You might want to change your interface to remove the -f option, and just iterate over the remaining non-option arguments
usage: foo.sh -i end -d dir -s subdir file [...]
So,
while getopts ":i:d:s:" opt; do
case "$opt" in
i) initial=$OPTARG ;;
d) dir=$OPTARG ;;
s) sub=$OPTARG ;;
esac
done
shift $(( OPTIND - 1 ))
path="/$initial/$dir/$sub"
mkdir -p "$path"
for file in "$@"; do
touch "$path/$file"
done
回答4:
I fixed the same problem you had like this:
Instead of:
foo.sh -i test -d directory -s subdirectory -s subdirectory2 -f file1 file2 file3
Do this:
foo.sh -i test -d directory -s "subdirectory subdirectory2" -f "file1 file2 file3"
With the space separator you can just run through it with a basic loop.
Here's the code:
while getopts ":i:d:s:f:" opt
do
case $opt in
i ) initial=$OPTARG;;
d ) dir=$OPTARG;;
s ) sub=$OPTARG;;
f ) files=$OPTARG;;
esac
done
for subdir in $sub;do
for file in $files;do
echo $subdir/$file
done
done
Here's a sample output:
$ ./getopts.sh -s "testdir1 testdir2" -f "file1 file2 file3"
testdir1/file1
testdir1/file2
testdir1/file3
testdir2/file1
testdir2/file2
testdir2/file3
回答5:
There actually is a way to retrieve multiple arguments using getopts
, but it requires some manual hacking with getopts
' OPTIND
variable.
See the following script (reproduced below): https://gist.github.com/achalddave/290f7fcad89a0d7c3719. There's probably an easier way, but this was the quickest way I could find.
#!/bin/sh
usage() {
cat << EOF
$0 -a <a1> <a2> <a3> [-b] <b1> [-c]
-a First flag; takes in 3 arguments
-b Second flag; takes in 1 argument
-c Third flag; takes in no arguments
EOF
}
is_flag() {
# Check if $1 is a flag; e.g. "-b"
[[ "$1" =~ -.* ]] && return 0 || return 1
}
# Note:
# For a, we fool getopts into thinking a doesn't take in an argument
# For b, we can just use getopts normal behavior to take in an argument
while getopts "ab:c" opt ; do
case "${opt}" in
a)
# This is the tricky part.
# $OPTIND has the index of the _next_ parameter; so "\$$((OPTIND))"
# will give us, e.g., $2. Use eval to get the value in $2.
eval "a1=\$$((OPTIND))"
eval "a2=\$$((OPTIND+1))"
eval "a3=\$$((OPTIND+2))"
# Note: We need to check that we're still in bounds, and that
# a1,a2,a3 aren't flags. e.g.
# ./getopts-multiple.sh -a 1 2 -b
# should error, and not set a3 to be -b.
if [[ $((OPTIND+2)) > $# ]] || is_flag "$a1" || is_flag "$a2" || is_flag "$a3"
then
usage
echo
echo "-a requires 3 arguments!"
exit
fi
echo "-a has arguments $a1, $a2, $a3"
# "shift" getopts' index
OPTIND=$((OPTIND+3))
;;
b)
# Can get the argument from getopts directly
echo "-b has argument $OPTARG"
;;
c)
# No arguments, life goes on
echo "-c"
;;
esac
done
回答6:
The original question deals with getopts, but there is another solution that provides more flexible functionality without getopts (this is perhaps a bit more verbose, but provides a far more flexible command line interface). Here is an example:
while [[ $# > 0 ]]
do
key="$1"
case $key in
-f|--foo)
nextArg="$2"
while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
case $nextArg in
bar)
echo "--foo bar found!"
;;
baz)
echo "--foo baz found!"
;;
*)
echo "$key $nextArg found!"
;;
esac
if ! [[ "$2" =~ -.* ]]; then
shift
nextArg="$2"
else
shift
break
fi
done
;;
-b|--bar)
nextArg="$2"
while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
case $nextArg in
foo)
echo "--bar foo found!"
;;
baz)
echo "--bar baz found!"
;;
*)
echo "$key $nextArg found!"
;;
esac
if ! [[ "$2" =~ -.* ]]; then
shift
nextArg="$2"
else
shift
break
fi
done
;;
-z|--baz)
nextArg="$2"
while ! [[ "$nextArg" =~ -.* ]] && [[ $# > 1 ]]; do
echo "Doing some random task with $key $nextArg"
if ! [[ "$2" =~ -.* ]]; then
shift
nextArg="$2"
else
shift
break
fi
done
;;
*)
echo "Unknown flag $key"
;;
esac
shift
done
In this example we are looping through all of the command line options looking for parameters that match our accepted command line flags (such as -f or --foo). Once we find a flag, we loop through every parameter until we run out of parameters or encounter another flag. This breaks us back out into our outer loop which only processes flags.
With this setup, the following commands are equivalent:
script -f foo bar baz
script -f foo -f bar -f baz
script --foo foo -f bar baz
script --foo foo bar -f baz
You can also parse incredibly disorganized parameter sets such as:
script -f baz derp --baz herp -z derp -b foo --foo bar -q llama --bar fight
To get the output:
--foo baz found!
-f derp found!
Doing some random task with --baz herp
Doing some random task with -z derp
--bar foo found!
--foo bar found!
Unknown flag -q
Unknown flag llama
--bar fight found!
回答7:
If you want to specify any number of values for an option, you can use a simple loop to find them and stuff them into an array. For example, let's modify the OP's example to allow any number of -s parameters:
unset -v sub
while getopts ":i:d:s:f:" opt
do
case $opt in
i ) initial=$OPTARG;;
d ) dir=$OPTARG;;
s ) sub=("$OPTARG")
until [[ $(eval "echo \${$OPTIND}") =~ ^-.* ]] || [ -z $(eval "echo \${$OPTIND}") ]; do
sub+=($(eval "echo \${$OPTIND}"))
OPTIND=$((OPTIND + 1))
done
;;
f ) files=$OPTARG;;
esac
done
This takes the first argument ($OPTARG) and puts it into the array $sub. Then it will continue searching through the remaining parameters until it either hits another dashed parameter OR there are no more arguments to evaluate. If it finds more parameters that aren't a dashed parameter, it adds it to the $sub array and bumps up the $OPTIND variable.
So in the OP's example, the following could be run:
foo.sh -i test -d directory -s subdirectory1 subdirectory2 -f file1
If we added these lines to the script to demonstrate:
echo ${sub[@]}
echo ${sub[1]}
echo $files
The output would be:
subdirectory1 subdirectory2
subdirectory2
file1
回答8:
As you don't show how you hope to construct your list
/test/directory/subdirectory/file1
. . .
test/directory/subdirectory2/file3
it's a little unclear how to proceed, but basically you need to keep appending any new values to the appropriate variable, i.e.
case $opt in
d ) dirList="${dirList} $OPTARG" ;;
esac
Note that on the first pass dir will be empty, and you'll wind up with a space leading at the from of your final value for ${dirList}
. (If you really need code that doesn't include any extra spaces, front or back, there is a command I can show you, but it will be hard to understand, and it doesn't seem that you'll need it here, but let me know)
You can then wrap your list variables in for loops to emit all the values, i.e.
for dir in ${dirList} do
for f in ${fileList} ; do
echo $dir/$f
done
done
Finally, it is considered good practice to 'trap' any unknown inputs to your case statement, i.e.
case $opt in
i ) initial=$OPTARG;;
d ) dir=$OPTARG;;
s ) sub=$OPTARG;;
f ) files=$OPTARG;;
* )
printf "unknown flag supplied "${OPTARG}\nUsageMessageGoesHere\n"
exit 1
;;
esac
I hope this helps.