How to validate the number of arguments (mandatory and optional) in a shell script? - linux

I am having trouble validating my script arguments. I am trying to achieve 5 mandatory arguments and 2 optional arguments to my script. Here is what I have tried so far.
#!/bin/sh
if [ $# -lt 5 ] || [ $# -gt 7 ]
then
echo "Please supply 5 or 6 or 7 parameters. Aborting."
exit 1
fi
echo MP1 = "$1"
echo MP2 = "$2"
echo MP3 = "$3"
echo MP4 = "$4"
echo MP5 = "$5"
while getopts c:a: optionalargs
do
case $optionalargs in
c)copt=$OPTARG;;
a)aopt=$OPTARG;;
*)echo "Invalid arg";;
esac
done
if [ ! -z "$copt" ]
then
export CHAR_SET=$copt
fi
if [ ! -z "$aopt" ]
then
export ADDITIONAL_FLAGS=$aopt
fi
shift $((OPTIND -1))
echo OP_C = "${CHAR_SET}"
echo OP_A = "${ADDITIONAL_FLAGS}"
The problem is validating the total number of arguments to this script as I can have a total of 5 or 7 arguments. Providing -a additional -c character is treated as 9 arguments.
./a.sh 1 2 3 4 5 -a additional -c character
Please supply 5 or 6 or 7 parameters. Aborting.
I am open to designs with no - as long as I am able to have both mandatory and optional parameters.
How to get this properly validated?

First: the shell doesn't know anything about optional vs. mandatory arguments, doesn't treat arguments that start with "-" specially, anything like that. It just has a list of "words", and it's up to your script to figure out what they mean. The getopts command can help with parsing the arguments, but it handles a fairly limited syntax:
Options start with a single dash, and are a single letter (maybe with an argument after that). For options that don't take arguments, you can stack multiple options on a single dash (e.g. ls -la).
After all the options, there can be a number of positional parameters (the meaning of these is defined -- as the name implies -- by their position in the list, e.g. first, second, etc).
There are a number of syntax extensions (mostly GNU conventions) that getopts does not support:
Putting options after positional parameters (e.g. ls filename -l). Options must always come first.
Long options (multi-letter and/or double-dash, e.g. ls --all).
Using -- to separate the options from the positional parameters.
So if you want to use getopts-style optional arguments, you need to put them first in the argument list. And when parsing them, you need to parse and remove* them from the argument list (with shift) before you check the number of positional parameters, and use $1 etc to access the positional parameters. Your current script is running into trouble because it's trying to handle the positional parameters first, and that won't work with getopts (at least without some heavy-duty kluging).
If you need a more general argument syntax, you might be able to use getopt (note the lack of "s" in the name). Some versions of getopt support the GNU conventions, some don't. Some have other problems. IMO this is a can of worms that's best left unopened.
Another possibility is to abandon the - option syntax, and give the script 7 positional parameters where the last two can be omitted. The problem with this is that you can't omit the sixth but pass the seventh (unless you're willing to consider the sixth being blank as equivalent to omitting it). The code for this would look something like this:
if [ $# -lt 5 ] || [ $# -gt 7 ]
then
echo "Please supply 5 or 6 or 7 parameters. Aborting."
exit 1
fi
echo "MP1 = $1"
echo "MP2 = $2"
echo "MP3 = $3"
echo "MP4 = $4"
echo "MP5 = $5"
if [ -n "$6" ]; then
# This will run if a sixth argument was specified AND it wasn't blank.
export CHAR_SET=$6
echo "OP_C = ${CHAR_SET}"
fi
if [ $# -ge 7 ]; then
# This will run if a seventh argument was specified EVEN IF it was blank.
# If you want to omit this for a blank arg, use the `-n` test instead.
export ADDITIONAL_FLAGS=$7
echo "OP_A = ${ADDITIONAL_FLAGS}"
fi
...and then run the script with e.g.
./a.sh 1 2 3 4 5 character additional # Both optional args supplied
./a.sh 1 2 3 4 5 character # Only first optional arg supplied
./a.sh 1 2 3 4 5 "" additional # Only second optional arg supplied
Or, if you really want the more extended syntax and don't want to risk the vagaries of the getopt command, you can spend a lot of time and effort writing your own parsing system. IMO this is way more work than it's worth.

Every space-separated string will be interpreted as a new argument by the shell. Are you allowed to have the -a flag or -c arguments without parameters? Assuming those need to have something following them, you actually need to allow 5 or 7 or 9 arguments. Getopts is separate from the initial shell argument parsing.
Other than that, the script looks good. For quality check, I'd recommend Shellcheck

Related

Counting the number of inputs provided by user

Description: I have a script that begins with a warning/question posed to user. I need the user to answe with yes/y or no/n.
Issue: Though I have a conditional to make sure the user provides one of the following answers presented above I also need to make that only ONE input is provided by the user. I have attempted using
[ "$#" -eq 1 ] or [ $# -eq 1 ] neither of these coniditionals seems to work to solve the problem
here is what I have thus far:...
#!/bin/bash
#
# Descritpion: some Windows OS script
#
#
printf "(!)WARNING(!): 1. The this program is ONLY compatible with Windows operating systems. "
printf "2. You will NEED to be logged in as an ADMIN in order to fully make use of the '*****' script.\n"
printf "Would you like to continue? yes or no (y/n): "
#would it be cleaner to use "case" rather than a "while" w/ multiple conditionals? (01.19.2020)
read opt
while (true)
do
if [ $opt == yes ] || [ $opt == y ]
then
printf "continue. \n"
break
elif [ $opt == no ] || [ $opt == n ]
then
printf "OK, exiting the script! \n"
exit 0
#elif [ "$#" -ne 1 ]
#then
# "Too many arguments have been provided, please try again. \n"
# read opt
else
printf "The opition you provided is not recognized, please try again. \n"
read opt
fi
done
Not the ideal solution, but it includes an explanation of why $# is not working as you expect:
$# returns the number of arguments passed to a function. You have read 'opt' in as a user input - not the arguments to the function.
One solution could be to wrap your infinite while loop inside a function, and then pass $opt into it. It would then be the argument(s) to your function and you could use $# to count how many there are.
For details of the workings of many built-in functions in bash, you could try:
man bash
but I accept that there is a lot of information in there. You can search for the relevant words, normally using "/{searchstring}" (but you will probably need to "escape" the special characters, in this case: "/\$\#")
Take a look at read options
read: read [-ers] [-u fd] [-t timeout] [-p prompt] [-a array] [-n nchars] [-d delim] [name ...]
One line is read from the standard input, or from file descriptor FD if the
-u option is supplied, and the first word is assigned to the first NAME,
the second word to the second NAME, and so on, with leftover words assigned
to the last NAME. Only the characters found in $IFS are recognized as word
delimiters. If no NAMEs are supplied, the line read is stored in the REPLY
variable. If the -r option is given, this signifies `raw' input, and
backslash escaping is disabled. The -d option causes read to continue
until the first character of DELIM is read, rather than newline. If the -p
option is supplied, the string PROMPT is output without a trailing newline
before attempting to read. If -a is supplied, the words read are assigned
to sequential indices of ARRAY, starting at zero. If -e is supplied and
the shell is interactive, readline is used to obtain the line. If -n is
supplied with a non-zero NCHARS argument, read returns after NCHARS
characters have been read. The -s option causes input coming from a
terminal to not be echoed.
you would be able to simplify your script using some of them.
Read the input and parse it exactly. Decide, if you want to ignore spaces or not. You could even use a regex to make the input formatted as exactly as you want.
while (true) is an unnecessary use of subshell. Just while true.
if [ $opt == yes ] will not work if opt contains whitespaces, for example user inputs y e s. It will exit with a nonzero exit status and print an error message on standard error stream. As a rule, always quote your variable expansions. Always "$opt", never $opt. This is most probably the error you are asking for fixing.
Also the == is a bash extension. Use = for string comparison.
Cosmetics: [ $opt == yes ] || [ $opt == y ] unnecessary runs two processes in case the first one fails. Just run one process [ "$opt" = yes -o "$opt" = y ]. But the first one may be just more clear.
read opt ignores leading and trailing whitespaces and removes the \ slashes. Use IFS= read -r opt to read the whole line exactly as it is.
Use shellcheck.net to check your scripts.
The $# is the number of arguments passed to the script. It is unrelated to what read does. read saves the input to the variable.
So:
while true; do
if ! IFS= read -r opt; then
echo "ERROR: END OF INPUT!" >&2
exit 2
fi
case "$opt" in
y|yes) printf 'continue. \n'; break; ;;
n|no) printf 'OK, exiting the script! \n'; exit 1; ;;
*) printf 'The opition you provided is not recognized, please try again. \n'; ;;
esac
done

Strange behavior with parameter expansion in program arguments

I'm trying to conditionally pass an argument to a bash script only if it has been set in the calling script and I've noticed some odd behavior.
I'm using parameter expansion to facilitate this, outputting an option only if the corresponding variable is set. The aim is to pass an argument from a 'parent' script to a 'child' script.
Consider the following example:
The calling script:
#!/bin/bash
# 1.sh
ONE="TEST_ONE"
TWO="TEST_TWO"
./2.sh \
--one "${ONE}" \
"${TWO:+"--two ${TWO}"}" \
--other
and the called script:
#!/bin/bash
# 2.sh
while [[ $# -gt 0 ]]; do
key="${1}"
case $key in
-o|--one)
ONE="${2}"
echo "ONE: ${ONE}"
shift
shift
;;
-t|--two)
TWO="${2}"
echo "TWO: ${TWO}"
shift
shift
;;
-f|--other)
OTHER=1
echo "OTHER: ${OTHER}"
shift
;;
*)
echo "UNRECOGNISED: ${1}"
shift
;;
esac
done
output:
ONE: TEST_ONE
UNRECOGNISED: --two TEST_TWO
OTHER: 1
Observe the behavior of the option '--two', which will be unrecognised. It looks like it is being expanded correctly, but is not recognised as being two distinct strings.
Can anyone explain why this is happening? I've seen it written in one source that it will not work with positional parameter arguments, but I'm still not understanding why this behaves as it does.
It is because when you pass $2 as a result of parameter expansion from 1.sh you are quoting it in a way that --two TEST_TWO is evaluated as one single argument, so that the number of arguments in 2.sh result in 4 instead of 5
But that said, using your $2 as ${TWO:+--two ${TWO}} would solve the problem, but that would word-split the content of $2 if it contains spaces. You need to use arrays.
As a much more recommended and fail-proof approach use arrays as below on 1.sh as
argsList=(--one "${ONE}" ${TWO:+--two "${TWO}"} --other)
and pass it along as
./2.sh "${argsList[#]}"
or if you are familiar with how quoting rules work (how and when to quote to prevent word-splitting from happening) use it directly on the command line as below. This would ensure that the contents variables ONE and TWO are preserved even if they have spaces.
./2.sh \
--one "${ONE}" \
${TWO:+--two "${TWO}"} \
--other
As a few recommended guidelines
Always use lower-case variable names for user defined variables to not confuse them with the environment variables maintained by the shell itself.
Use getopts() for more robust argument flags parsing

"test: too many arguments" message because of special character * while using test command on bash to compare two strings [duplicate]

This question already has answers here:
Meaning of "[: too many arguments" error from if [] (square brackets)
(6 answers)
Closed 5 years ago.
I'm new to shell scripting and I'm having some trouble while using the "test" command and the special character * to compare two strings.
I have to write a shell script which, for every element(both files and directories) contained in the directory passed as the first argument, has to write something(for the solving of my problem it is not relevant to know what has to be written down) on the file "summary.out". What's more, there's a string passed as the second argument. Those files/directories beginning with this string must be ignored(nothing has to be written on summary.out).
Here is the code:
#!/bin/bash
TEMP=NULL
cd "$1"
for i in *
do
if test "$i" != "$2"*;then #Here is where the error comes from
if test -f "$i";then
TEMP="$i - `head -c 10 "$i"`"
elif test -d "$i";then
TEMP="$i - `ls -1 "$i" | wc -l`"
fi
echo $TEMP >> summary.out
fi
done
The error comes from the test which checks whether the current file/directory begins with the string passed as second argument, and it takes place every iteration of the for cycle. It states:"test: too many arguments"
Now, I have performed some tests which showed that the problem has nothing to do with blank spaces inside the $i or $1. The problem is linked to the fact that I use the special character * in the test(if I remove it, everything works fine).
Why can't "test" handle * ? What can I do to fix that?
* gets expanded by the shell.
In bash, you can use [[ ... ]] for conditions instead of test. They support patterns on the right hand side - * is not expanded, as double square brackets are a keyword with higher precedence.
if [[ a == * ]] ; then
echo Matches
else
echo Doesn\'t match
fi

Saving the arguments in different variables passed to a shell script

I need to save two command line arguments in two different variables and rest all in third variable.
I am using following code
while [ $# -ge 2 ] ; do
DirFrom=$1
Old_Ver=`basename $1`
shift
DirTo=$1
shift
pdct_code=$#
shift
done
This code is failing if I send more than three arguments . Please suggest how can I save 3rd 4th and so on variable in pdct_code variable.
You're not entering the loop when you have more than two arguments. You can bump the argument limit like so:
while [ $# -ge 3 ]; do
:
done
or better yet just parse your arguments without looping at all. For example:
DirFrom="$1"
Old_Ver=`basename "$1"`
DirTo="$2"
pdct_code="$*"
No loop or shifting is needed. Note that pdct_code may need to be an array, to preserve the exact arguments passed to your script.
if [ $# -ge 2 ]; then
DirFrom=$1
Old_Ver=$(basename "$1")
DirTo=$2
pdct_code="${#:3}"
# pdct_code=( "${#:3}" )
done

ksh script + print argument content in shell script

I want to run the script.sh with one argument.
If the first argument = action then script.sh will print the action parameter - restart machine each 1 min
My example not work but please advice what need to fix in the script so I will print the $action parameter if argument is action.
Remark I not want to set the following solution - [[ $1 = action ]] && echo action "restart machine each 1 min
My example script:
#!/bin/ksh
action="restart machine each 1 min"
echo "action" ${$1}
Example how to run the script
./script.sh action
Expected results that I need to get :
action restart machine each 1 min
Well with pdksh this works:
echo "action" `eval echo '$'$1`
You want to use eval:
action="restart machine each 1 min"
eval echo $1 \$$1
Note that doing something like this is a huge security risk. Consider what happens if the user invokes the script with the first argument "; rm -rf /"
You can probably alleviate such problems with:
eval "echo '$1' \"\$$1\""
but really you're just asking for trouble (This last version will struggle if the first argument contains a double-quote, and a $() construct will permit an arbitrary command to be executed). It is much safer to simply use a case statement and check that the argument matches exactly a string that you are looking for. Or, at least check that the argument you are eval'ing does not contain any of the following characters: ;()$"'. It's probably safest to check that it only contains alphanumerics (a-zA-Z0-9)
It's been two years, but here's an example of using nameref (a.k.a. typeset -N).
It includes three consecutive tests for validity of the given argument.
Is an argument given?
Does the argument match a known variable? nameref checks this.
Does the target variable have a value set?
action='This is the value of $action'
word='This it the value of ${word}'
list='This is a list'
lie='This is a lie'
(
typeset name=${1:?Usage: script.sh varname} || exit
nameref arg1=${name} || exit
: ${arg1:?} || exit
echo "$name $arg1"
)

Resources