开发者

Bourne/POSIX shell parameter splitting

开发者 https://www.devze.com 2023-02-06 05:09 出处:网络
Assume I have some utility that can take a number of options, each followed by a filename. E.g., I could call it as myutil, myutil -o somefile, myutil -p anotherfile, myutil -o somefile -p anotherfile

Assume I have some utility that can take a number of options, each followed by a filename. E.g., I could call it as myutil, myutil -o somefile, myutil -p anotherfile, myutil -o somefile -p anotherfile, etc.... I want to write a wrapper PO开发者_开发问答SIX shell script that is able to call myutil with arbitrary combinations of options (depending on some conditions internal to the wrapper script, which aren't relevant to this question).

I thought of doing something like:

#!/bin/sh
file1=somefile
file2=anotherfile

if [ somecriteria = true ]; then
  OPT1="-o $file1"
fi

if [ othercriteria = true ]; then
  OPT2="-p $file2"
fi

myutil $OPT1 $OPT2

This works great—as long as neither filename has spaces: Assuming both ifs are true, myutil gets $1 = [-o], $2 = [somefile], $3 = [-p], and $4 = [anotherfile]. However, if there are spaces, e.g., if file1="some file", $1 = [-o], $2 = [some], $3 = [file], etc. Of course, what I want is for $2 = [some file].

Putting another set of quotes around the filename in OPT1 and OPT2 doesn't help; e.g., if I change it to OPT1="-o \"$file1\"", that just gets me $2 = ["some] and $3=[file"]. And putting in quotes around $OPT1 and $OPT2 in the call to myutil doesn't work either: if I do that, $1 = [-o some file].

So, is there some trick to get this working, or some other approach that would do what I want? I'd like this to stick to standard shell features, so no bash-isms or ksh-isms, please :) See this for a description of what's in the standard.


After messing with it more, I found another approach that seems to do what I want:

#!/bin/sh
file1="some file"
file2=anotherfile

if [ somecriteria = true ]; then
  OPT1="$file1"
fi

if [ othercriteria = true ]; then
  OPT2="$file2"
fi

myutil ${OPT1:+-o "$OPT1"} ${OPT2:+-p "$OPT2"}

The construct ${parameter:+word} will be substituted with word if ${parameter} is set; if it's not set, it goes away. So if $OPT1 is unset, ${OPT1:+-o "$OPT1"} disappears—notably, it doesn't turn into an empty string in argv. If $OPT1 is set to some file, the above expression is substituted with -o "some file", and myutil gets $1 = [-o], $2 = [some file] as I want.

Note that myutil ${OPT1:+-o} "$OPT1" ${OPT2:+-p} "$OPT2" does not do exactly what I want, because if $OPT1 is unset, the -o goes away, but "$OPT1" turns into an empty string—$1 = [], $2 = [-p], $3 = [anotherfile]

(Edited per Dennis's suggestion)


Seems like you found a decent POSIX solution. You could, however, use set to maintain a call to your program as myutil "$@". Your solution gets a bit unwieldy as the number of possible parameters grow.

#!/bin/sh
file1=somefile
file2=anotherfile

if [ somecriteria = true ]; then
  set -- "-o" "$file1"
fi

if [ othercriteria = true ]; then
  set -- "$@" "-p" "$file2"
fi

myutil "$@"

Example

#!/bin/sh

file1="some file"
file2="another file"

# Default to '1' if not overwritten
: ${x:=1}
: ${y:=1}

if [ $x -eq 1 ]; then
  set -- "-o" "$file1"
fi

if [ $y -eq 1 ]; then
  set -- "$@" "-p" "$file2"
fi

printf "[%s]" "$@"
echo

Output

$ x=0 y=0 ./opt.sh
[]
$ x=0 y=1 ./opt.sh
[-p][another file]
$ x=1 y=0 ./opt.sh
[-o][some file]
$ x=1 y=1 ./opt.sh
[-o][some file][-p][another file]


First of all, you'll have to quote the options in this line sh myutil.sh "$OPT1" "$OPT2"

And here's a working implementation with no particular -isms that uses getopts on the myutil.sh side.

This script calls myutil.sh:

#!/bin/sh
somecriteria=true
othercriteria=true
file1="some file"
file2="other file"

if [ $somecriteria = true ]; then
  OPT1="-o$file1"
fi

if [ $othercriteria = true ]; then
  OPT2="-p$file2"
fi

sh myutil.sh "$OPT1" "$OPT2"

And this is what myutil.sh could look like:

#!/bin/sh
OPTIND=1
while getopts "o:p:" opt; do
  case "$opt" in
    o)  file1=$OPTARG
        ;;
    p)  file2=$OPTARG
        ;;
  esac
done
shift $((OPTIND-1))

echo 'File 1: "'$file1'"'
echo 'File 2: "'$file2'"'

As you can see in the output of myutil.sh, spaces in the filenames are preserved:

File 1: "some file"
File 2: "other file"


Why note simple quote instead of double quote ?

if [ somecriteria = true ]; then
  OPT1="-o '$file1'"
fi

if [ othercriteria = true ]; then
  OPT2="-p '$file2'"
fi


I think your own solution using ${OPT1+-o "$OPT1"} is a good one, and off the top of my head I don't see any problems with it in this usecase, but there's another approach using eval that no one mentioned, which is even closer to your original code:

#!/bin/sh
FILE1='some file'
FILE2='another file'

if [ somecriteria = true ]; then
 OPT1="-o '$FILE1'"
fi

if [ othercriteria = true ]; then
  OPT2="-p '$FILE2'"
fi

eval myutil "$OPT1" "$OPT2"

This will produce what you want.

But you need to be careful in case your filenames contain single-quotes as part of the literal filename string.

If you know exactly what your filenames look like when writing the script, just ensure your filename's quote literals don't break out of whatever escaping you put around the file name.

But this is even more important when you're handling user input, or otherwise getting input from the environment. - for example, if $FILE1 is defined as abc'; /tmp/malicious_program ' and then you do the eval, it will parse the myutil line into:

myutil -o 'abc'; /tmp/malicious_program '' -p 'another file'

..which is two separate commands, and could be a massive security hole, depending on how exactly this script is being executed relative to the entity which created /tmp/malicious_program and set $FILE1.

In those cases, if you are willing to introduce a dependency on sed, you can do something like this first:

FILE1=\'`printf %s "$FILE1" | sed "s/'/'\\\\''/g"`\'

..this will produce a nice, single-quote escaped filename, with any sungle-quotes inside it properly escaped too.

Since in Bourne/POSIX shell nothing but a single quote can "break out of" a single quoted string, this is why I use single-quoting in my example. Double quote escaping things this way is possible, but your sed command has to be much more complex since there's several other characters you need to escape inside double quotes (off the top of my head: instead of just escaping singe quotes, you escape double quotes, backslashes, backticks, dollar signs, and possibly other stuff I'm not thinking of).

P.S.: because I found this "wrap in shell escaping, then eval later" approach useful in several cases, in at least one where it was downright necessary, I wrote a tiny C program (and equivalent shell function using sed) which wraps all of its arguments in single quote shell escaping as described above, in case anyone wants to use it instead of having to implement your own: esceval


Possible implementation is below, bash arrays tutorial is here.

#!/bin/sh

function myutil {
  local a1=$1; shift;
  local a2=$1; shift;
  local a3=$1; shift;
  local a4=$1; shift;

  echo "a1=$a1,a2=$a2,a3=$a3,a4=$a4"
}

file1="some file"
file2="another file"

OPT1=(-o "$file1")
OPT2=(-p "$file2")

myutil "${OPT1[@]}" "${OPT2[@]}"


Use getopts http://mywiki.wooledge.org/BashFAQ/035

"The POSIX shell (and others) offer getopts which is safe to use instead."

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号