script with simple flag - shell

Suppose I have this simple script
#! /bin/sh
if [ $# -ne 2 ]
then
echo "Usage: $0 arg1 arg2"
exit 1
fi
head $1 $2
## But this is supposed to be:
## if -f flag is set,
## call [tail $1 $2]
## else if the flag is not set
## call [head $1 $2]
So what's the simplest way to add a 'flag' checking to my script?
Thanks

fflag=no
for arg in "$#"
do
test "$arg" = -f && fflag=yes
done
if test "$fflag" = yes
then
tail "$1" "$2"
else
head "$1" "$2"
fi
This simpler approach might also be viable:
prog=head
for i in "$#"
do
test "$i" = -f && prog=tail
done
$prog "$1" "$2"

I usually go for the "case" statement when parsing options:
case "$1" in
-f) call=tail ; shift ;;
*) call=head ;;
esac
$call "$1" "$2"
Remember to quote the positional parameters. They might contain file names or directory names with spaces.
If you can use e.g. bash instead of Bourne shell, you can use e.g. the getopts built-in command. For more information see the bash man page.

Related

cut -d, ignore delimiter in argument

I have written a script that gets a variable number of arguments:
test.sh -i <input1> <input2> ... -o <output1> <output2> ...
I'm parsing the arguments as follows:
while [ $# -gt 1 ]; do
TMP=$(echo "$#" | cut -d '-' -f 2) #i <input1> <input2>
TMP1=$(echo "$TMP" | cut -d ' ' -f 1) #i
CNT=$(echo "$TMP" | wc -w) #3
set -x
case "$TMP1" in
i)
INPUTS=$(echo "$TMP" | cut -c 3-)
shift "$CNT"
;;
o)
OUTPUTS=$(echo "$TMP" | cut -c 3-)
shift "$CNT"
;;
esac
done
This works everytime, except for files that happen to have a '-' in their name.
Example:
./test.sh -i file1.txt file-2.txt -o out1.txt out-2.txt
Is there anyway I can force cut to ignore delimiters that occur within the file names?
You don't need all this string manipulation; each argument is already a separate word.
while (( $# > 0 )); do
case $1 in
-i) shift
while [[ $# -gt 0 && $1 != -* ]]; do
inputs+=( "$1" )
shift
done
;;
-o) shift
while [[ $# -gt 0 && $1 != -* ]]; do
outputs+=( "$1" )
shift
done
;;
*) echo "Unrecognized option $1"
exit 1
;;
esac
done
This can be refactored a little to avoid the repeated checks for running out of arguments.
for arg in "$#"; do
case $1 in
-i) mode=input; continue ;;
-o) mode=output; continue ;;
esac
case $mode in
input) input+=("$arg") ;;
output) output+=("$arg") ;;
*) echo "Unknown mode: $mode"
exit 1
;;
esac
done
Here's an alternative approach that may benefit someone.
The fact is, argument parsing is always a tradeoff, hence there's benefit in tailoring it to the application. Here's a pretty generic solution that allows a little bit of error checking and disorder in the arguments.
It's very simple, but I have added some example output and comments, and for the sake of readability and compatibility, stayed away from complex ways to save a line or two (especially on the if statements).
Sample Usage:
bash #> touch file-1 file3 file4 file-8 file7
bash #> argparse -i file-1 file3 file4 -c -k --q --j -r -t -o file-8 file7
Output:
Input files: file-1 file3 file4
Output files: file-8 file7
Args are: c k q j r t
Doing action for argument "c"
Doing action for argument "k"
Doing action for argument "j"
Script:
#!/bin/bash
#argparse
#Assign arrays
until [[ $# < 1 ]]; do
#ignore args "-i" and "-o", and tell the script to check for files following
if [ "$1" == "-i" ] ; then unset output ; input=1 ; shift
elif [ "$1" == "-o" ] ; then unset input ; output=1 ; shift
fi
#Add input and output files to respective arrays
if [ -f "$1" ] ; then
if [[ $input == 1 ]]; then
infiles+=($1)
elif [[ $output == 1 ]]; then
outfiles+=($1)
fi
else
#Add args to array
arg="$(echo "$1" | sed 's/-//g')"
args+=($arg)
fi
shift
done
#Some debug feedback
echo -e "Input files: ${infiles[#]}\nOutput files: ${outfiles[#]}\nArgs are: ${args[#]}\n"
#Simulate actually "doing" something with the args
for arg in "${args[#]}" ; do
case $arg in
"c") echo "Doing action for argument \"c\"" ;;
"k") echo "Doing action for argument \"k\"" ;;
"j") echo "Doing action for argument \"j\"" ;;
*) ;;
esac
done
Update/Edit: I've just realised, that the OP didn't have any requirement for parsing actual arguments other than -i and -o. Well regardless, this may still come in handy for someone at some point.

How to check if arguments are legit?

I have to write a bash script which will count all the commands in a text file. Arguments to a script are -p, -n num, and a file. This means that commands like:
script.sh -n 3 -p file.txt
script -p -n 3 file.txt
and similar are all legit.
However, I have to echo an error for any commands that are not similar to this: script.sh -n -k file.txt for example.
Here is a link to my code.
I managed to make it work, but it is way too long and redundant. Is there a way I can do this in a short way?
You may want to have a look at one of the following standard commands:
getopts is a Bash builtin. It is newer and simple to use, but does not support long options (--option).
getopt is an external program which may involve a little more glue code. There are different implementations. getopt usually supports long options.
This is a small getopts example (modified one of the examples from this external site):
#!/bin/bash
flag=off
dir=
# iterate over each option with getopts:
while getopts fd: opt
do
case "$opt" in
f) flag=on;;
d) dir="$OPTARG";;
*) echo >&2 "usage: $0 [-f] [-d directory] [file ...]"
exit 1;;
esac
done
# remove all positional pararmeters we already
# handled from the command line:
shift $(( expr $OPTIND - 1 ))
# main part of your program, remaining arguments are now in
# $# resp. $0, $1, ...
I'd like to suggest another snippet that is a lot simpler to read than yours, because it exactly depicts the only two valid cases you specified in your comment:
If I want to "call" my script it has to look like this: script.sh -n +number -p file.txt. file.txt must be the last argument, however, -n and -p can be switched.
So the cases are ($0 to $4):
script.sh -n +number -p file.txt
script.sh -p -n +number file.txt
It uses only if and Bash's logical operators:
#!/bin/bash
if ! { [[ "$1" = "-n" ]] && [[ "$2" =~ ^-[0-9]+$ ]] && [[ "$3" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; } &&
! { [[ "$2" = "-n" ]] && [[ "$3" =~ ^-[0-9]+$ ]] && [[ "$1" = "-p" ]] && [[ "$4" =~ ".txt"$ ]] ; }
then
echo "Error" && exit 1
fi
Notes:
The group ({, }) syntax expects a ; at the end of its list.
You have to use a regex to check for *.txt
The number regex you gave will require the number to start with a -, while in your specification you say +.

Best way to parse command line args in Bash?

After several days of research, I still can't figure out the best method for parsing cmdline args in a .sh script. According to my references the getopts cmd is the way to go since it "extracts and checks switches without disturbing the positional parameter variables.Unexpected switches, or switches that are missing arguments, are recognized and reportedas errors."
Positional params(Ex. 2 - $#, $#, etc) apparently don't work well when spaces are involved but can recognize regular and long parameters(-p and --longparam). I noticed that both methods fail when passing parameters with nested quotes ("this is an Ex. of ""quotes""."). Which one of these three code samples best illustrates the way to deal with cmdline args? The getopt function is not recommended by gurus, so I'm trying to avoid it!
Example 1:
#!/bin/bash
for i in "$#"
do
case $i in
-p=*|--prefix=*)
PREFIX=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-s=*|--searchpath=*)
SEARCHPATH=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
-l=*|--lib=*)
DIR=`echo $i | sed 's/[-a-zA-Z0-9]*=//'`
;;
--default)
DEFAULT=YES
;;
*)
# unknown option
;;
esac
done
exit 0
Example 2:
#!/bin/bash
echo ‘number of arguments’
echo "\$#: $#"
echo ”
echo ‘using $num’
echo "\$0: $0"
if [ $# -ge 1 ];then echo "\$1: $1"; fi
if [ $# -ge 2 ];then echo "\$2: $2"; fi
if [ $# -ge 3 ];then echo "\$3: $3"; fi
if [ $# -ge 4 ];then echo "\$4: $4"; fi
if [ $# -ge 5 ];then echo "\$5: $5"; fi
echo ”
echo ‘using $#’
let i=1
for x in $#; do
echo "$i: $x"
let i=$i+1
done
echo ”
echo ‘using $*’
let i=1
for x in $*; do
echo "$i: $x"
let i=$i+1
done
echo ”
let i=1
echo ‘using shift’
while [ $# -gt 0 ]
do
echo "$i: $1"
let i=$i+1
shift
done
[/bash]
output:
bash> commandLineArguments.bash
number of arguments
$#: 0
using $num
$0: ./commandLineArguments.bash
using $#
using $*
using shift
#bash> commandLineArguments.bash "abc def" g h i j*
Example 3:
#!/bin/bash
while getopts ":a:" opt; do
case $opt in
a)
echo "-a was triggered, Parameter: $OPTARG" >&2
;;
\?)
echo "Invalid option: -$OPTARG" >&2
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
exit 1
;;
esac
done
exit 0
I find the use of getopt to be the easiest. It provides correct handling of arguments which is tricky otherwise. For example, getopt will know how to handle arguments to a long option specified on the command line as --arg=option or --arg option.
What is useful in parsing any input passed to a shell script is the use of the "$#" variables. See the bash man page for how this differs from $#. It ensures that you can process arguments that include spaces.
Here's an example of how I might write s script to parse some simple command line arguments:
#!/bin/bash
args=$(getopt -l "searchpath:" -o "s:h" -- "$#")
eval set -- "$args"
while [ $# -ge 1 ]; do
case "$1" in
--)
# No more options left.
shift
break
;;
-s|--searchpath)
searchpath="$2"
shift
;;
-h)
echo "Display some help"
exit 0
;;
esac
shift
done
echo "searchpath: $searchpath"
echo "remaining args: $*"
And used like this to show that spaces and quotes are preserved:
user#machine:~/bin$ ./getopt_test --searchpath "File with spaces and \"quotes\"."
searchpath: File with spaces and "quotes".
remaining args: other args
Some basic information about the use of getopt can be found here
If you want to avoid using getopt you can use this nice quick approach:
Defining help with all options as ## comments (customise as you wish).
Define for each option a function with same name.
Copy the last five lines of this script to your script (the magic).
Example script: log.sh
#!/bin/sh
## $PROG 1.0 - Print logs [2017-10-01]
## Compatible with bash and dash/POSIX
##
## Usage: $PROG [OPTION...] [COMMAND]...
## Options:
## -i, --log-info Set log level to info (default)
## -q, --log-quiet Set log level to quiet
## -l, --log MESSAGE Log a message
## Commands:
## -h, --help Displays this help and exists
## -v, --version Displays output version and exists
## Examples:
## $PROG -i myscrip-simple.sh > myscript-full.sh
## $PROG -r myscrip-full.sh > myscript-simple.sh
PROG=${0##*/}
LOG=info
die() { echo $# >&2; exit 2; }
log_info() {
LOG=info
}
log_quiet() {
LOG=quiet
}
log() {
[ $LOG = info ] && echo "$1"; return 1 ## number of args used
}
help() {
grep "^##" "$0" | sed -e "s/^...//" -e "s/\$PROG/$PROG/g"; exit 0
}
version() {
help | head -1
}
[ $# = 0 ] && help
while [ $# -gt 0 ]; do
CMD=$(grep -m 1 -Po "^## *$1, --\K[^= ]*|^##.* --\K${1#--}(?:[= ])" log.sh | sed -e "s/-/_/g")
if [ -z "$CMD" ]; then echo "ERROR: Command '$1' not supported"; exit 1; fi
shift; eval "$CMD" $# || shift $? 2> /dev/null
done
Testing
Running this command:
./log.sh --log yep --log-quiet -l nop -i -l yes
Produces this output:
yep
yes
By the way: It's compatible with posix!

How can I parse long-form arguments in shell?

Everything I see uses getopt or the slightly-fancier getopts which only supports one-character options (e.g., -h but not --help). I want to do fancy long options.
I've done something like this:
_setArgs(){
while [ "${1:-}" != "" ]; do
case "$1" in
"-c" | "--configFile")
shift
configFile=$1
;;
"-f" | "--forceUpdate")
forceUpdate=true
;;
"-r" | "--forceRetry")
forceRetry=true
;;
esac
shift
done
}
As you can see, this supports both the single-character and the longer options nicely. It allows for values to be associated with each argument, as in the case of --configFile. It's also quite extensible, with no artificial limitations as to what options can be configured, etc.
As included above, the "${1:-}" prevents an "unbound variable" error when running in bash "strict" mode (set -euo pipefail).
Assuming that you "want to do fancy long options" regardless of the tool, just go with getopt (getopts seems to be mainly used when portability is crucial). Here's an example of about the maximum complexity that you'll get:
params="$(getopt -o e:hv -l exclude:,help,verbose --name "$(basename "$0")" -- "$#")"
if [ $? -ne 0 ]
then
usage
fi
eval set -- "$params"
unset params
while true
do
case $1 in
-e|--exclude)
excludes+=("$2")
shift 2
;;
-h|--help)
usage
;;
-v|--verbose)
verbose='--verbose'
shift
;;
--)
shift
break
;;
*)
usage
;;
esac
done
With this code, you can specify -e/--exclude more than once, and ${excludes[#]} will contain all of the given excludes. After processing (-- is always present) anything remaining is stored in $#.
I have created a bash function that is the easiest to use and needs no customization. Just use the function and pass all long options with or without arguments and the function will set them as variables with the corresponding option arguments as values in your script.
function get_longOpt {
## Pass all the script's long options to this function.
## It will parse all long options with its arguments,
## will convert the option name to a variable and
## convert its option value to the variable's value.
## If the option does not have an argument, the
## resulting variable's value will be set to true.
## Works properly when providing long options, only.
## Arguments to options may not start with two dashes.
##
#### Usage
##
## get_longOpt $#
##
## May expand to:
##
## get_longOpt --myOption optimopti --longNumber 1000 --enableMe --hexNumber 0x16
##
### Results in the bash interpretation of:
## myOption=optimopti
## longNumber=1000
## enableMe=true
## hexNumber=0x16
##
local -a opt_list=( $# )
local -A opt_map
local -i index=0
local next_item
for item in ${opt_list[#]}; do
# Convert arg list to map.
let index++
next_item="${opt_list[$index]}"
if [[ "${item}" == --* ]] \
&& [[ "${next_item}" != --* ]] \
&& [[ ! -z "${next_item}" ]]
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]="${next_item}"
elif [[ "${item}" == --* ]] \
&& { [[ "${next_item}" == --* ]] \
|| [[ -z "${next_item}" ]]; }
then
item="$(printf '%s' "${item##*-}")"
opt_map[${item}]=true
fi
done
for item in ${!opt_map[#]}; do
# Convert map keys to shell vars.
value="${opt_map[$item]}"
[[ ! -z "${value}" ]] && \
printf -v "$item" '%s' "$value"
done
}
The up to date original source code is available here:
https://github.com/theAkito/akito-libbash/blob/master/bishy.bash

Converting Bash command line options to variable name

I am trying to write a bash script that takes in an option.
Lets call these options A and B.
In the script A and B may or may not be defined as variables.
I want to be able to check if the variable is defined or not.
I have tried the following but it doesn't work.
if [ ! -n $1 ]; then
echo "Error"
fi
Thanks
The "correct" way to test whether a variable is set is to use the + expansion option. You'll see this a lot in configure scripts:
if test -s "${foo+set}"
where ${foo+set} expands to "set" if it is set or "" if it's not. This allows for the variable to be set but empty, if you need it. ${foo:+set} additionally requires $foo to not be empty.
(That $(eval echo $a) thing has problems: it's slow, and it's vulnerable to code injection (!).)
Oh, and if you just want to throw an error if something required isn't set, you can just refer to the variable as ${foo:?} (leave off the : if set but empty is permissible), or for a custom error message ${foo:?Please specify a foo.}.
You did not define how these options should be passed in, but I think:
if [ -z "$1" ]; then
echo "Error"
exit 1
fi
is what you are looking for.
However, if some of these options are, err, optional, then you might want something like:
#!/bin/bash
USAGE="$0: [-a] [--alpha] [-b type] [--beta file] [-g|--gamma] args..."
ARGS=`POSIXLY_CORRECT=1 getopt -n "$0" -s bash -o ab:g -l alpha,beta:,gamma -- "$#"`
if [ $? -ne 0 ]
then
echo "$USAGE" >&2
exit 1
fi
eval set -- "$ARGS"
unset ARGS
while true
do
case "$1" in
-a) echo "Option a"; shift;;
--alpha) echo "Option alpha"; shift;;
-b) echo "Option b, arg '$2'"; shift 2;;
--beta) echo "Option beta, arg '$2'"; shift 2;;
-g|--gamma) echo "Option g or gamma"; shift;;
--) shift ; break ;;
*) echo "Internal error!" ; exit 1 ;;
esac
done
echo Remaining args
for arg in "$#"
do
echo '--> '"\`$arg'"
done
exit 0
Don't do it that way, try this:
if [[ -z $1 ]]; then
echo "Error"
fi
The error in your version is actually the lack of quoting.
Should be:
if [ ! -n "$1" ]; then
echo "Error"
fi
But you don't need the negation, use -z instead.
If you work on Bash, then use double brackets [[ ]] too.
from the man bash page:
-z string
True if the length of string is zero.
-n string
True if the length of string is non-zero.
Also, if you use bash v4 or greater (bash --version) there's -v
-v varname
True if the shell variable varname is set (has been assigned a value).
The trick is "$1", i.e.
root#root:~# cat auto.sh
Usage () {
echo "error"
}
if [ ! -n $1 ];then
Usage
exit 1
fi
root#root:~# bash auto.sh
root#root:~# cat auto2.sh
Usage () {
echo "error"
}
if [ ! -n "$1" ];then
Usage
exit 1
fi
root#root:~# bash auto2.sh
error

Resources