Mass arguments (operands) at first place in command line argument passing - bash

I use following lines (hope this is best practice if not correct me please) to handle command line options:
#!/usr/bin/bash
read -r -d '' HELP <<EOF
OPTIONS:
-c Enable color output
-d Enable debug output
-v Enable verbose output
-n Only download files (mutually exclusive with -r)
-r Only remove files (mutually exclusive with -n)
-h Display this help
EOF
# DECLARE VARIABLES WITH DEFAULT VALUES
color=0
debug=0
verbose=0
download=0
remove=0
OPTIND=1 # Reset in case getopts has been used previously in the shell
invalid_options=(); # Array for invalid options
while getopts ":cdvnrh" opt; do
echo "Actual opt: $opt"
case $opt in
c)
color=1
;;
d)
debug=1
;;
v)
verbose=1
;;
n)
download=1
;;
r)
remove=1
;;
h)
echo "$HELP"
exit 1
;;
\?)
invalid_options+=($OPTARG)
;;
*)
invalid_options+=($OPTARG)
;;
esac
done
# HANDLE INVALID OPTIONS
if [ ${#invalid_options[#]} -ne 0 ]; then
echo "Invalid option(s):" >&2
for i in "${invalid_options[#]}"; do
echo $i >&2
done
echo "" >&2
echo "$HELP" >&2
exit 1
fi
# SET $1 TO FIRST MASS ARGUMENT, $2 TO SECOND MASS ARGUMENT ETC
shift $((OPTIND - 1))
# HANDLE CORRECT NUMBER OF MASS OPTIONS
if [ $# -ne 2 ]; then
echo "Correct number of mass arguments are 2"
echo "" >&2
echo "$HELP" >&2
exit 1
fi
# HANDLE MUTUALLY EXCLUSIVE OPTIONS
if [ $download -eq 1 ] && [ $remove -eq 1 ]; then
echo "Options for download and remove are mutually exclusive" >&2
echo "$HELP" >&2
exit 1
fi
echo "color: $color"
echo "debug: $debug"
echo "verbose: $verbose"
echo "download: $download"
echo "remove: $remove"
echo "\$1: $1"
echo "\$2: $2"
If I call the script way that mass arguments (those that are not switches or arguments for switches) are last arguments everything is working correctly:
$ ./getopts.sh -c -d -v -r a b
Actual opt: c
Actual opt: d
Actual opt: v
Actual opt: r
color: 1
debug: 1
verbose: 1
download: 0
remove: 1
$1: a
$2: b
The problem is when I want to call the script so the mass arguments are first (or somewhere in the middle of switches that do not use arguments)
$ ./getopts.sh a b -c -d -v -r
Correct number of mass arguments are 2
OPTIONS:
-c Enable color output
-d Enable debug output
-v Enable verbose output
-n Only download files (mutually exclusive with -r)
-r Only remove files (mutually exclusive with -d)
-h Display this help
or
$ ./getopts.sh -c a b -d -v -r
Actual opt: c
Correct number of mass arguments are 2
OPTIONS:
-c Enable color output
-d Enable debug output
-v Enable verbose output
-n Only download files (mutually exclusive with -r)
-r Only remove files (mutually exclusive with -d)
-h Display this help
I think this should be OK according (POSIX) standards, because following syntax which is basically the same is working as expected on my system:
$ cp test1/ test2/ -r
$ cp test1/ -r test2/
I have search over the Internet but only thing that was close to my problem was this one related to C.

getopts automatically breaks the while loop as soon as it detects a non-dash parameter (not including the argument given to dash parameters that take arguments). The POSIX standard is to have dashed parameters come first, and then have files. There's also none of this -- and + crap either. It's plain and simple.
However, Linux isn't Unix or POSIX compliant. It's just in the nature of the GNU utilities to be "better" than the standard Unix utilities. More features, more options, and handling things a bit differently.
On Linux, command line parameters can come after files in many GNU utilities.
For example:
$ cp -R foo bar
Work on my Unix certified Mac OS X and on Linux, However,
$ cp foo bar -R
Only works on Linux.
If you want getopts to work like a lot of Linux utilities, you need to do a wee bit of work.
First, you have to process your arguments yourself, and not depend upon $OPTIND to parse them. You also need to verify that you have an argument.
I came up with this as an example of doing what you want.
#! /bin/bash
while [[ $* ]]
do
OPTIND=1
echo $1
if [[ $1 =~ ^- ]]
then
getopts :a:b:cd parameter
case $parameter in
a) echo "a"
echo "the value is $OPTARG"
shift
;;
b) echo "b"
echo "the value is $OPTARG"
shift
;;
c) echo "c"
;;
d) echo "d"
;;
*) echo "This is an invalid argument: $parameter"
;;
esac
else
other_arguments="$other_arguments $1"
fi
shift
done
echo "$other_arguments"
I now loop as long as $* is set. (Maybe I should use $#?) I have to do a shift at the end of the loop. I also reset $OPTIND to 1 each time because I'm shifting the arguments off myself. $OPTARG is still set, but I have to do another shift to make sure everything works.
I also have to verify if a argument begins with a dash or not using a regular expression in my if statement.
Basic testing shows it works, but I can't say it's error free, but it does give you an idea how you have to handle your program.
There's still plenty of power you're getting from getopts, but it does take a bit more work.

Bash provides two methods for argument parsing.
The built-in command getopts is a newer, easy to use mechanism how to parse arguments but it is not very flexible. getopts does not allow to mix options and mass arguments.
The external command getopt is an older and more complex mechanism to parse arguments. It allows long/short options and the gnu extension allow to mix options and mass arguments.

Related

Getopt parsing error in bash script (option declaration mistake)

I need to get 4 options (each with a short and a long version) in a bash script.
Here is what I did:
OPTS=`getopt -l :author,icon,channel,message: -o :aicm: -- "$#"` ||
exit 1
eval set -- "$OPTS"
while true; do
case "$1" in
-a|--author) echo "A:'$2'"; shift;;
-i|--icon) echo "I:'$2'"; shift 2;;
-m|--message) echo "M:'$2'"; shift 2;;
-c|--channel) echo "C:'$2'"; shift 2;;
--) shift; break;;
*) echo Error; exit 1;;
esac
done
And here is what I get:
command
docker run --rm -e SLACK_TOKEN slacker notify --channel foo
output
C:'--'
Error
Of course, I would like to have this output:
C:'foo'
Your getopt command looks a little funky. You seem to be using : as some sort of delimiter, here:
-l :author,icon,channel,message:
And here:
-o :aicm:
That doesn't make any sense. The : has special meaning in the options definitions; take a look at the getopt(1) man page:
-l, --longoptions longopts
The long (multi-character) options to be recognized. More than one
option name may be specified at once, by separating the names
with commas. This option may be given more than once, the longopts
are cumulative. Each long option name in longopts may be followed
by one colon to indicate it has a required argument, and by two colons
to indicate it has an optional argument.
The same is true of short options.
So assuming that all of your options take arguments, you would write:
OPTS=`getopt -l author:,icon:,channel:,message: -o a:i:c:m: -- "$#"` ||

Pass commandline args into another script

I have couple of scripts which call into each other. However when I pass
Snippet from buid-and-run-node.sh
OPTIND=1 # Reset getopts in case it was changed in a previous run
while getopts "hn:c:f:s:" opt; do
case "$opt" in
h)
usage
exit 1
;;
n)
container_name=$OPTARG
;;
c)
test_command=$OPTARG
;;
s)
src=$OPTARG
;;
*)
usage
exit 1
;;
esac
done
$DIR/build-and-run.sh -n $container_name -c $test_command -s $src -f $DIR/../dockerfiles/dockerfile_node
Snippet from build-and-run.sh
OPTIND=1 # Reset getopts in case it was changed in a previous run
while getopts "hn:c:f:s:" opt; do
case "$opt" in
h)
usage
exit 1
;;
n)
container_name=$OPTARG
;;
c)
test_command=$OPTARG
;;
f)
dockerfile=$OPTARG
;;
s)
src=$OPTARG
;;
*)
usage
exit 1
;;
esac
done
I am calling it as such
build-and-run-node.sh -n test-page-helper -s ./ -c 'scripts/npm-publish.sh -r test/test-helpers.git -b patch'
with the intention that npm-publish.sh should run with the -r and -b parameters. However when I run the script I get
build-and-run.sh: illegal option -- r
which obviously means it is the build-and-run command that is consuming the -r. How do I avoid this?
You need double quotes around $test_command in buid-and-run-node.sh, otherwise that variable is being split on the white space and appears to contain arguments for buid-and-run.sh. Like this:
$DIR/build-and-run.sh -n $container_name -c "$test_command" -s $src -f $DIR/../dockerfiles/dockerfile_node
Further Info
As the comment below rightly points out, it's good practice to quote all variables in Bash, unless you know you want them off (for example, to enable shell globbing). It's also helpful, at least in cases where the variable name is part of a larger word, to use curly braces to delineate the variable name. This is to prevent later characters from being treated as part of the variable name if they're legal. So a better command call might look like:
"${DIR}/build-and-run.sh" -n "$container_name" -c "$test_command" -s "$src" -f "${DIR}/../dockerfiles/dockerfile_node"

Shell script on mac and ubuntu 14.04: Boolean command line args

I'm trying to write a generic shell script to archive X days older files matching pattern passed as parameter. I'm having tough time making the boolean parameter, regex parameter and regex variable in script work across mac and ubuntu. I'm new to shell scripting. Any suggestion related to the problem or best practices are welcome. Following is the script:
#!/usr/bin/env bash
#Default source dir
SOURCE_DIR=./logs/
# Delete by default
DELETE=YES
# Archive files older by these many days
OLD=7
# Pattern to archive
PATTERN="*.log*"
# Use -gt 1 to consume two arguments per pass in the loop
# (each argument has a corresponding value to go with it).
while [[ $# -gt 1 ]]; do
key="$1"
case $key in
-s|--source_dir)
SOURCE_DIR="$2"
shift # past argument
;;
-d|--dest_dir)
DEST_DIR="$2"
shift # past argument
;;
-o|--days)
OLD="$2"
shift # past argument
;;
-p|--pattern)
PATTERN="$2"
shift # past argument
;;
-n|--no-delete)
DELETE=NO
;;
*)
# unknown option
;;
esac
shift # past argument or value
done
if [[ ! -d "$SOURCE_DIR" ]]; then
echo 'Archive source does not exist'
exit 1
fi
SOURCE_DIR=${SOURCE_DIR%/}
if [[ -z "$DEST_DIR" ]]; then
DEST_DIR="${SOURCE_DIR%/}/backup"
fi
DEST_DIR=${DEST_DIR%/}
if [[ ! -d "$DEST_DIR" ]]; then
echo 'Creating destination '$DEST_DIR
mkdir -p -- "$DEST_DIR"
fi
echo $SOURCE_DIR
echo $DEST_DIR
echo $OLD
echo $PATTERN
echo $DELETE
files=$(find $SOURCE_DIR -mtime +$OLD -type f -name $PATTERN)
echo $files
if [[ $DELETE = YES ]]; then
echo "Delete files"
else
echo "Don't delete files"
fi
Outpout on Mac:
(mysql30):recon-etl anshuc$ ./archive.sh -s junk/ -p *.py -o 10 -n
junk
junk/backup
10
*.py
YES
junk/__init__.py junk/client.py
Delete files
(mysql30):recon-etl anshuc$
Output on ubuntu 14.04
anshuc:~/workspace/xyz$ ./archive.sh -s ae/tools/ -d ae/logs/backup/ -p *.py -n -o 420
ae/tools
ae/logs/backup
420
*.py
NO
ae/tools/services/__init__.py ae/tools/__init__.py
Don't delete files
anshuc:~/workspace/xyz$
DELETE not working on mac is the concern. Also, I was having problem with PATTERN argument before. Though on trial and error I have come across a way to do. But am not sure of side-effects in case someone doesn't use quotes or any other intricacies that may be involved. A li'l input on that would make me more knowledged. :-)
TIA

Check if an argument is a path

I'm writing a script in bash. It will receive from 2 to 5 arguments. For example:
./foo.sh -n -v SomeString Type Directory
-n, -v and Directory are optional.
If script doesn't receive argument Directory it will search in current directory for a string.
Otherwise it will follow received path and search there. If this directory doesn't exist it will send a message.
The question is: Is there a way to check if the last arg is a path or not?
You can get last argument using variable reference:
numArgs=$#
lastArg="${!numArgs}"
# check if last argument is directory
if [[ -d "$lastArg" ]]; then
echo "it is a directory"
else
echo "it is not a directory"
fi
you can use this:
#!/bin/bash
if [[ -d ${!#} ]]
then
echo "DIR EXISTS"
else
echo "dosen't exists"
fi
First, use getopts to parse the options -n and -v (they will have to be used before any non-options, but that's not usually an issue).
while getopts nv opt; do
case $opt in
n) nflag=1 ;;
v) vflag=1 ;;
*) printf >&2 "Unrecognized option $opt\n"; exit 1 ;;
esac
done
shift $((OPTIND-1))
Now, you will have only your two required arguments, and possibly your third optional argument, in $#.
string_arg=$1
type_arg=$2
dir_arg=$3
if [ -d "$dir_arg" ]; then
# Do something with valid directory
fi
Note that this code will work in any POSIX-compliant shell, not just bash.

how to use getopt(s) as technique for passing in argument in bash

Can someone show me an example how to use getopts properly or any other technique that I would be able to pass in an argument? I am trying to write this in unix shell/bash. I am seeing there is getopt and getopts and not sure which is better to use. Eventually, I will build this out to add for more options.
In this case, I want to pass the filepath as input to the shell script and place a description in the case it wasn't entered correctly.
export TARGET_DIR="$filepath"
For example: (calling on the command line)
./mytest.sh -d /home/dev/inputfiles
Error msg or prompt for correct usage if running it this way:
./mytest.sh -d /home/dev/inputfiles/
As a user, I would be very annoyed with a program that gave me an error for providing a directory name with a trailing slash. You can just remove it if necessary.
A shell example with pretty complete error checking:
#!/bin/sh
usage () {
echo "usage: $0 -d dir_name"
echo any other helpful text
}
dirname=""
while getopts ":hd:" option; do
case "$option" in
d) dirname="$OPTARG" ;;
h) # it's always useful to provide some help
usage
exit 0
;;
:) echo "Error: -$OPTARG requires an argument"
usage
exit 1
;;
?) echo "Error: unknown option -$OPTARG"
usage
exit 1
;;
esac
done
if [ -z "$dirname" ]; then
echo "Error: you must specify a directory name using -d"
usage
exit 1
fi
if [ ! -d "$dirname" ]; then
echo "Error: the dir_name argument must be a directory
exit 1
fi
# strip any trailing slash from the dir_name value
dirname="${dirname%/}"
For getopts documentation, look in the bash manual
Correction to the ':)' line:
:) echo "Error: -$OPTARG requires an argument"
because if no value got provided after the flag, then OPTARG gets the name of the flag and flag gets set to ":" which in the above sample printed:
Error: -: requires an argument
which wasn't useful info.
Same applies to:
\?) echo "Error: unknown option -$OPTARG"
Thanks for this sample!

Resources