简体   繁体   中英

How to handle “--” in the shell script arguments?

This question has 3 parts, and each alone is easy, but combined together is not trivial (at least for me):)

Need write a script what should take as its arguments:

  1. one name of another command
  2. several arguments for the command
  3. list of files

Examples:

./my_script head -100 a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' *.txt

and so on.

Inside my script for some reason I need distinguish

  • what is the command
  • what are the arguments for the command
  • what are the files

so probably the most standard way write the above examples is:

./my_script head -100 -- a.txt b.txt ./xxx/*.txt
./my_script sed -n 's/xxx/aaa/' -- *.txt

Question1: Is here any better solution?

Processing in./my_script (first attempt):

command="$1";shift
args=`echo $* | sed 's/--.*//'`
filenames=`echo $* | sed 's/.*--//'`

#... some additional processing ...

"$command" "$args" $filenames #execute the command with args and files

This solution will fail when the filenames will contain spaces and/or '--', eg
/some--path/to/more/idiotic file name.txt

Question2: How properly get $command its $args and $filenames for the later execution?

Question3: - how to achieve the following style of execution?

echo $filenames | $command $args #but want one filename = one line (like ls -1)

Is here nice shell solution, or need to use for example perl?

First of all, it sounds like you're trying to write a script that takes a command and a list of filenames and runs the command on each filename in turn. This can be done in one line in bash:

$ for file in a.txt b.txt ./xxx/*.txt;do head -100 "$file";done
$ for file in *.txt; do sed -n 's/xxx/aaa/' "$file";done

However, maybe I've misinterpreted your intent so let me answer your questions individually.

Instead of using "--" (which already has a different meaning), the following syntax feels more natural to me:

./my_script -c "head -100" a.txt b.txt ./xxx/*.txt
./my_script -c "sed -n 's/xxx/aaa/'" *.txt

To extract the arguments in bash, use getopts :

SCRIPT=$0

while getopts "c:" opt; do
    case $opt in
        c)
            command=$OPTARG
            ;;
    esac
done

shift $((OPTIND-1))

if [ -z "$command" ] || [ -z "$*" ]; then
    echo "Usage: $SCRIPT -c <command> file [file..]"
    exit
fi

If you want to run a command for each of the remaining arguments, it would look like this:

for target in "$@";do
     eval $command \"$target\"
done

If you want to read the filenames from STDIN, it would look more like this:

while read target; do
     eval $command \"$target\"
done

The $@ variable, when quoted will be able to group parameters as they should be:

for parameter in "$@"
do
    echo "The parameter is '$parameter'"
done

If given:

head -100 test this "File name" out

Will print

the parameter is 'head'
the parameter is '-100'
the parameter is 'test'
the parameter is 'this'
the parameter is 'File name'
the parameter is 'out'

Now, all you have to do is parse the loop out. You can use some very simple rules:

  1. The first parameter is always the file name
  2. The parameters that follow that start with a dash are parameters
  3. After the "--" or once one doesn't start with a "-", the rest are all file names.

You can check to see if the first character in the parameter is a dash by using this:

if [[ "x${parameter}" == "x${parameter#-}" ]]

If you haven't seen this syntax before, it's a left filter . The # divides the two parts of the variable name. The first part is the name of the variable, and the second is the glob filter (not regular expression) to cut off. In this case, it's a single dash. As long as this statement isn't true, you know you have a parameter. BTW, the x may or may not be needed in this case. When you run a test, and you have a string with a dash in it, the test might mistake it for a parameter of the test and not the value.

Put it together would be something like this:

parameterFlag=""
for parameter in "$@"     #Quotes are important!
do
    if [[ "x${parameter}" == "x${parameter#-}" ]]
    then
         parameterFlag="Tripped!"
    fi
    if [[ "x${parameter}" == "x--" ]]
    then
         print "Parameter \"$parameter\" ends the parameter list"
         parameterFlag="TRIPPED!"
    fi
    if [ -n $parameterFlag ]
    then
        print "\"$parameter\" is a file"
    else
        echo "The parameter \"$parameter\" is a parameter"
    fi
done

Question 1

I don't think so, at least not if you need to do this for arbitrary commands.

Question 3

command=$1
shift
while [ $1 != '--' ]; do
    args="$args $1"
    shift
done
shift
while [ -n "$1" ]; do
    echo "$1"
    shift
done | $command $args

Question 2

How does that differ from question 3?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM