Parameters work properly when remove their quoting - bash

I am puzzled about the verbose of quoting in the script. Take an example from the instruction I followed:
min_val=1
max_val=100
int=50
if [[ "$int" =~ ^-?[0-9]+$ ]]; then
if [[ "$int" -ge "$min_val" && "$int" -le "$max_val" ]]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
Run it and come by:
$ bash test_integer3.sh
50 is within 1 to 100.
When I removed all the quoting in testing:
if [[ $int =~ ^-?[0-9]+$ ]]; then
if [[ $int -ge $min_val && $int -le $max_val ]]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
It's still working properly.
$ bash test_integer3.sh
50 is within 1 to 100.
Why should live with the habit of writing redundant quoting?

The real problem comes when you start to use [ command over [[ in your scripts. [[ is bash's improvement to the [ command. It has several enhancements that make it a better choice to write scripts targeting bash.
One such improvement would be that you no longer have to quote variables because [[ handles empty strings and strings with white-space more intuitively. For example consider your script written with [ for the un-quoted case and for discussions sake, one of your variables is empty
#!/usr/bin/env bash
min_val=
max_val=
int=50
if [[ $int =~ ^-?[0-9]+$ ]]; then
if [ $int -ge $min_val -a $int -le $max_val ]; then
echo "$int is within $min_val to $max_val."
else
echo "$int is out of range."
fi
else
echo "int is not an integer." >&2
exit 1
fi
One thing to note is I've re-written the combined conditional using the -a syntax since [ does not support && operator within but could be combined using && as [ $int -ge $min_val ] && [ $int -le $max_val ]
You would see things going bad and seeing errors as below which means that one of the conditionals involving -le is gone wrong on seeing an empty string.
1_script.sh: line 7: [: -a: integer expression expected
50 is out of range.
whereas with same code for undefined variables and replacing the expression to use [[ would gracefully handle the empty strings to produce just an incorrect result as
50 is out of range.
So to sum it up, from the many advantages over using [[, the particular advantage on your case is to handle variables if there could be empty strings in your conditionals.

Quoting is used to to stop the word splitting. In the case above it is not necessary but consider a case like this: you have a directory and having theses files file1.txt, file2.txt, old.txt and file1 old.txt.
If you wish to remove the file file1 old.txt and run the command
rm file1 old.txt
then it will remove the file old.txt instead of what you expected.

In your piece of code you don't need quotes as you discovered. However, using quotes is considered "good practice" because unexpected things can happen without quotes. For example if you run the code with int equal to say "foo bar" you might get some strange results without quotes.
It is like the double and triple equals in JavaScript. You could probably get away with only double equals but some unexpected results might occur.

Related

Why do you need quotes here in Bash?

if [ "$a" -gt "$b" ]; then
...
fi
Why can't I have something like:
if [ $a -gt $b ]; then
...
fi
for Bash?
With arithmetic operator -gt (greater-than) $a and $b must contain integers and no quotes are necessary.
You need quotes here for correction -- for example, to avoid code injection. Let's say you have that script unquoted and you let me input numbers to tell me which one is greater.
I input:
a="1"
b="1 -o -e /home/foo"
Since you didn't quote, you're now going to evaluate
[ 1 -gt 1 -o -e /home/foo ]
and tell me that b is greater if /home/foo exists.
Your number comparison script has suddenly become a simplistic file browser that I can use to examine your system to find active users, running processes, installed software, hardware configuration and such.
This may not matter in practice, but when it's so easy to do right, you might as well quote.
TL;DR
You have provided no sample input, but it's likely that your variables are unset, empty, or don't contain integers. To make your scripts more robust, you should always quote your variables, test for conditions like empty or unset variables, or perform other validations if you can't guarantee the contents of your variables or are consuming user input.
Invalid Expansions
Unless you've explicitly declared your variables as integers, and sometimes even when you do, there are several cases where your comparison will be invalid. For example:
$ unset a b; a=1; [ $a -gt $b ]
-bash: [: 1: unary operator expected
You have a number of options, but the easiest is to use the Bash expression operators instead of the single brackets which are actually equivalent to /bin/test. This handles empty expansions and other use cases more gracefully. For example:
# Here, *b* is unset so *a* is "greater."
$ unset a b; a=1; [[ $a -gt $b ]]; echo $?
0
# This time *a* is unset, so the expression is false.
$ unset a b; b=1; [[ $a -gt $b ]]; echo $?
1
Declaring Variables as Integers May Help
You still have to beware of unexpected casts, though. For example:
$ unset a b; a='foo'; b='1'; [[ $a -gt $b ]]; echo $?
1
$ echo $a
foo
Better is the use of declare -i to force integer casting, but that will still cause unexpected results if you don't validate your inputs. For example:
$ unset a b; declare -i a='foo' b='1'; [[ $a -gt $b ]]; echo $?
1
$ echo $a
0
By declaring your variables, you force the values to be integers, but strings will be assigned as zero. This is likely not what you're expecting.
Validations and Defensive Quoting
While overkill for simple scripts, this is what a more robust script would look like.
unset a b
a='foo'
b='1'
for var in "$a" "$b"; do
if [[ -z "$var" ]] || [[ ! "$var" =~ ^[[:digit:]]+$ ]]; then
echo "Variable invalid: ${var}" > /dev/stderr
fi
done
[[ "$a" -gt "$b" ]]
echo "$?"
Note that in this case, the quotes and braces are not strictly necessary, but are there to show good defensive programming. You can rarely go wrong in Bash by using them liberally, and they will often save you a great deal of head-scratching and debugging.

0-9 and [[:digit:]] will match 1-10, but not more

I'm trying to make a script, just for fun which creates a new User and Generates a Password for him.
Now I have to check if the user enters something stupid, instead of a digit.
function checkifcorrectnum() {
#Check if the User did something else than enter a number
#And check if the Number is absurdly big
case "$1" in
[[:digit:]] ) if [ "$1" -gt "255" ]; then echo "Too big!" ; else : ; fi ;;
*) echo "Please enter a number!"; exit ;;
esac
}
But when I run the script, and enter 1-9 it works, but anything higher wont
You are only matching for a single digit by just [[:digit:]]. The bash globbing cannot be used like Regex and match any token any number of times by operators like * or +. If you want to stick with your method and you know exactly how many digits you want to allow then use e.g. for 2 digits:
case "$1" in
[[:digit:]][[:digit:]])
If you are not sure then:
case "$1" in
[[:digit:]]*)
* expands to any number of character.
But i think you should look at Regex matching offered by bash by the =~ operator of [[, so your whole function can be rewritten as:
if [[ $1 =~ ^[[:digit:]]+$ ]]; then
[[ $1 -gt 255 ]] && echo "Too big!"
else
echo 'Please enter a number!' && exit
fi
Also as you are not doing anything if the number is <=255 so [[ $1 -gt 255 ]] && echo "Too big!" is enough.
Since it is easier to test if a string is not a number than to test if it is, I suggest inverting the order of the tests:
function checkifcorrectnum() {
case "$1" in
*[^[:digit:]]*) echo "Please enter a number";;
*) [ "$1" -gt "255" ] && echo "Too big!" ;;
esac;
}
The glob *[^[:digit:]]* matches if any character in $1 is not a digit.
Examples:
$ checkifcorrectnum 255
$ checkifcorrectnum 256
Too big!
$ checkifcorrectnum 25z
Please enter a number
As an aside, the keyword function is bash-only and generally not necessary. With the keyword removed, the code works not only in bash but also in any POSIX compatible shell.

basic shell bash checking/switch arguments

This is a beginner question, I have already checked that Check existence of input argument in a Bash shell script but it doesn't fully explain what I want to do.
gcc -Wall cx17.$1.c -o cx17.$1
if [ -z "$1" ]
then
echo "No argument supplied"
else if [ -z "$2"]
then
echo "Data file is missing!!"
else if [ -z "$3"]
then
./cx17.$1 $2 > ./cx17.$1.$2
else
./cx17.$1 $2 $3 > ./cx17.$1.$2
fi
So you understand this very basic use case, depending on arguments (if there is 1, 2 or 3) the script will perform a different task.
I know it's really simple that's why I think I'm missing something obvious.
Thanks for your help
The answered I validate gave me some errors but lead me to the right stuff:
if [ -z "$1" ]; then
echo 'No argument supplied';
elif [ -z "$2" ]; then
echo 'Data file is missing!!';
elif [ -z "$3" ]; then
./cx17.$1 $2 >./cx17.$1.$2;
else
./cx17.$1 $2 $3 >./cx17.$1.$2;
fi;
Replace else if with elif:
if [[ -z "$1" ]]; then
echo 'No argument supplied';
elif [[ -z "$2" ]]; then
echo 'Data file is missing!!';
elif [[ -z "$3" ]]; then
"./cx17.$1" "$2" >"./cx17.$1.$2";
else
"./cx17.$1" "$2" "$3" >"./cx17.$1.$2";
fi;
Other recommendations:
Always double-quote words that contain variable substitutions, otherwise word splitting and shell globbing can take effect on the expanded variable content.
Always use [[ instead of [, since the former is more powerful, and it's good to be consistent.
If interpolation is not required, use single-quotes rather than double-quotes, since single-quotes do not interpolate anything; it's just safer that way.
You can dispense with the if statement altogether using the ${var:?msg} construct, which will exit the script if the given variable doesn't have a non-null value.
: ${1:?No argument given}
: ${2:?Data file is missing!}
# $1 and $2 guaranteed to be non-null; the program
# will receive 1 or 2 arguments, depending on how many
# arguments are present in $#
./cx17."$1" "${#:2:2}" > "./cx17.$1.$2"

Multiple `if` statements in bash script

I'm trying to write a short bash script that optionally accepts arguments from the command line, or prompts for their input
if [ [ -z "$message" ] && [ -z "$predefined" ] ] ; then
read -p "Enter message [$defaultMessage]: " message
message=${message:-$defaultMessage}
else
if [ -n "$predefined" ]; then
if [ -f $base/$environment/vle/data/$predefined.txt ]; then
echo Predefined message file $predefined.txt does not exist
exit 1
fi
fi
fi
If neither message nor predefined has been passed in as command line arguments, then the code should prompt for a value for message; otherwise if predefined has been passed in as a command line argument, then the script should test for the existence of a file of that name and only continue if the file does exist
But I'm getting the following error
[: -z: binary operator expected
at the first of those if tests
Any help in explaining what's wrong with my syntax for that first if statement? Or providing an alternative syntax to achieve the same objectives.
The first if is not well-formed. This would work:
if [ -z "$message" ] && [ -z "$predefined" ]; then
or this:
if test -z "$message" && test -z "$predefined"; then
or this bash-specific, easy but dirty way:
if [[ -z "$message" ]] && [[ -z "$predefined" ]]; then
or this bash-specific proper way:
if [[ -z $message && -z $predefined ]]; then
In this last version the double-quotes are unnecessary, not a typo.
Thanks #mklement0 for the corrections in the bash-specific style, and for this final comment:
I should note that there's one case where double-quoting is still a must inside [[ ... ]], namely if you want a variable reference on the right side of a string comparison (==) to be treated as a literal:
v='[a]'
[[ $v == $v ]] # FALSE!
[[ $v == "$v" ]] # true
Without double-quoting, the right-hand side is interpreted as a pattern. Some people advocate always double-quoting variable references so as not to have to remember such subtleties. That said (from bash 3.2 on), you must NOT double-quote the right operand when regex matching with =~
test expression1 -a expression2
is true if both expressions are true.
test expression1 -o expression2
is true if either or both expressions are true.
if [ -z "$message" -a -z "$predefined" ]; then
read -p "Enter message [$defaultMessage]: " message
message=${message:-$defaultMessage}
else
if [ -n "$predefined" -a -f $base/$environment/vle/data/$predefined.txt ]; then
echo Predefined message file $predefined.txt does not exist
exit 1
fi
fi
This was able to combine 4 test into 2 while also getting rid of one nested if expression; then ; fi

Check number of arguments passed to a Bash script

I would like my Bash script to print an error message if the required argument count is not met.
I tried the following code:
#!/bin/bash
echo Script name: $0
echo $# arguments
if [$# -ne 1];
then echo "illegal number of parameters"
fi
For some unknown reason I've got the following error:
test: line 4: [2: command not found
What am I doing wrong?
Just like any other simple command, [ ... ] or test requires spaces between its arguments.
if [ "$#" -ne 1 ]; then
echo "Illegal number of parameters"
fi
Or
if test "$#" -ne 1; then
echo "Illegal number of parameters"
fi
Suggestions
When in Bash, prefer using [[ ]] instead as it doesn't do word splitting and pathname expansion to its variables that quoting may not be necessary unless it's part of an expression.
[[ $# -ne 1 ]]
It also has some other features like unquoted condition grouping, pattern matching (extended pattern matching with extglob) and regex matching.
The following example checks if arguments are valid. It allows a single argument or two.
[[ ($# -eq 1 || ($# -eq 2 && $2 == <glob pattern>)) && $1 =~ <regex pattern> ]]
For pure arithmetic expressions, using (( )) to some may still be better, but they are still possible in [[ ]] with its arithmetic operators like -eq, -ne, -lt, -le, -gt, or -ge by placing the expression as a single string argument:
A=1
[[ 'A + 1' -eq 2 ]] && echo true ## Prints true.
That should be helpful if you would need to combine it with other features of [[ ]] as well.
Take note that [[ ]] and (( )) are keywords which have same level of parsing as if, case, while, and for.
Also as Dave suggested, error messages are better sent to stderr so they don't get included when stdout is redirected:
echo "Illegal number of parameters" >&2
Exiting the script
It's also logical to make the script exit when invalid parameters are passed to it. This has already been suggested in the comments by ekangas but someone edited this answer to have it with -1 as the returned value, so I might as well do it right.
-1 though accepted by Bash as an argument to exit is not explicitly documented and is not right to be used as a common suggestion. 64 is also the most formal value since it's defined in sysexits.h with #define EX_USAGE 64 /* command line usage error */. Most tools like ls also return 2 on invalid arguments. I also used to return 2 in my scripts but lately I no longer really cared, and simply used 1 in all errors. But let's just place 2 here since it's most common and probably not OS-specific.
if [[ $# -ne 1 ]]; then
echo "Illegal number of parameters" >&2
exit 2
fi
References
Bash Conditional Expressions
Conditional Constructs
Pattern Matching
Word Splitting
Filename Expansion (prev. Pathname Expansion)
Simple Commands
It might be a good idea to use arithmetic expressions if you're dealing with numbers.
if (( $# != 1 )); then
>&2 echo "Illegal number of parameters"
fi
>&2 is used to write the error message to stderr.
On []: !=, =, == ... are string comparison operators and -eq, -gt ... are arithmetic binary ones.
I would use:
if [ "$#" != "1" ]; then
Or:
if [ $# -eq 1 ]; then
If you're only interested in bailing if a particular argument is missing, Parameter Substitution is great:
#!/bin/bash
# usage-message.sh
: ${1?"Usage: $0 ARGUMENT"}
# Script exits here if command-line parameter absent,
#+ with following error message.
# usage-message.sh: 1: Usage: usage-message.sh ARGUMENT
A simple one liner that works can be done using:
[ "$#" -ne 1 ] && ( usage && exit 1 ) || main
This breaks down to:
test the bash variable for size of parameters $# not equals 1 (our number of sub commands)
if true then call usage() function and exit with status 1
else call main() function
Things to note:
usage() can just be simple echo "$0: params"
main can be one long script
Check out this bash cheatsheet, it can help alot.
To check the length of arguments passed in, you use "$#"
To use the array of arguments passed in, you use "$#"
An example of checking the length, and iterating would be:
myFunc() {
if [[ "$#" -gt 0 ]]; then
for arg in "$#"; do
echo $arg
done
fi
}
myFunc "$#"
This articled helped me, but was missing a few things for me and my situation. Hopefully this helps someone.
Here a simple one liners to check if only one parameter is given otherwise exit the script:
[ "$#" -ne 1 ] && echo "USAGE $0 <PARAMETER>" && exit
There is a lot of good information here, but I wanted to add a simple snippet that I find useful.
How does it differ from some above?
Prints usage to stderr, which is more proper than printing to stdout
Return with exit code mentioned in this other answer
Does not make it into a one liner...
_usage(){
_echoerr "Usage: $0 <args>"
}
_echoerr(){
echo "$*" >&2
}
if [ "$#" -eq 0 ]; then # NOTE: May need to customize this conditional
_usage
exit 2
fi
main "$#"
In case you want to be on the safe side, I recommend to use getopts.
Here is a small example:
while getopts "x:c" opt; do
case $opt in
c)
echo "-$opt was triggered, deploy to ci account" >&2
DEPLOY_CI_ACCT="true"
;;
x)
echo "-$opt was triggered, Parameter: $OPTARG" >&2
CMD_TO_EXEC=${OPTARG}
;;
\?)
echo "Invalid option: -$OPTARG" >&2
Usage
exit 1
;;
:)
echo "Option -$OPTARG requires an argument." >&2
Usage
exit 1
;;
esac
done
see more details here for example http://wiki.bash-hackers.org/howto/getopts_tutorial
You should add spaces between test condition:
if [ $# -ne 1 ];
then echo "illegal number of parameters"
fi
I hope this helps.

Resources