shell scripting default value - bash

I am trying to learn shell scripting and I am kind of confused with the idea of := or default value
#!/bin/sh
echo "Please enter a number \c"
read input
input=$((input % 2))
if [ $input -eq 0 ]
then
echo "The number is even"
else
echo "The number is odd"
fi
echo "Beginning of second part"
a="BLA"
a="Dennis"
echo $a
unset a
echo "a after unsetting"
echo $a
${a:=HI}
echo "unsetting a again"
unset a
echo $a
And I get this
Dennis
a after unsetting
./ifstatement.sh: line 21: HI: command not found
unsetting a again

When you write
${a:=HI}
the shell splits the result of the expansion into words, and interprets the first word as a command, as it would for any command line.
Instead, write
: "${a:=HI}"
: is a no-op command. The quotes prevent the shell from trying to do globbing, which in rare circumstances could cause a slowdown or an error.

There isn't a way to set a value that a variable will always "fall back" to when you un-set it. When you use the unset command, you are removing the variable (not just clearing the value associated with it) so it can't have any value, default or otherwise.
Instead, try a combination of two things. First, make sure the variable gets initialized. Second, create a function that sets the variable to the desired default value. Call this variable instead of unset. With this combination, you can simulate a variable having a "default" value.

${a:=HI} expands to HI, which your shell then tries to run as a command. If you're just trying to set the value of a variable if it is not set, you may want to do something like [ -z "$b" ] && b=BYE

Instead of calling unset $a, you do ${a:=HI} again

Related

Case statement not catching empty string

I want to detect if either no arguments or an invalid argument is passed and print a help message. A separate check for an empty argument is possible, but not so elegant.
My bash script looks like this:
COMMAND="$1"
shift
case "$COMMAND" in
loop)
loop_
;;
...
*)
echo $"Usage: $0 {loop|...}"
exit 1
esac
When no arguments are passed, nothing executes; if I pass "" then the proper case is triggered. If I use $1 directly instead of using the temporary variable, then it works as expected.
I've even tried adding a specific case for "") but to no avail.
The only way your case statement isn't going to match with no $1 given is if it isn't entered in the first place.
Consider the following:
#!/usr/bin/env bash
set -e
command=$1
shift
case $command in
*) echo "Default case was entered";;
esac
This emits no output when $1 is unset -- but not because anything wrong with the case statement.
Rather, the issue is that shift exits with a nonzero exit status when there's nothing available to shift, and the set -e causes the script as a whole to exit on that failure.
First Moral Of This Story: Don't Use set -e (or #!/bin/bash -e)
See BashFAQ #105 for an extended discussion -- or the exercises included therein if in a hurry. set -e is wildly incompatible between different "POSIX-compliant" shells, and thus makes behavior hard to predict. Manual error handling may not be fun, but it's much more reliable.
Second: Consider A Usage Function
This gives you a terse way to have your usage message in one place, and re-use it where necessary (for example, if you don't have a $1 to shift):
#!/usr/bin/env bash
usage() { echo "Usage: $0 {loop|...}" >&2; exit 1; }
command=$1
shift || usage
case $command in
*) usage ;;
esac
Because of the || usage, the exit status of shift is considered "checked", so even if you do run your script with set -e, it will no longer constitute a fatal error.
Alternately, Mark The shift As Checked Explicitly
Similarly:
shift ||:
...will run shift, but then fall back to running : (a synonym for true, which historically/conventionally implies placeholder use) should shift fail, similarly preventing set -e from triggering.
Aside: Use Lower-Case Names For Your Own Variables
POSIX specifies that the shell (and other tools to which the standards applies) have their behavior modified only by environment variables with all-caps names:
Environment variable names used by the utilities in the Shell and Utilities volume of POSIX.1-2017 consist solely of uppercase letters, digits, and the ( '_' ) from the characters defined in Portable Character Set and do not begin with a digit. Other characters may be permitted by an implementation; applications shall tolerate the presence of such names. Uppercase and lowercase letters shall retain their unique identities and shall not be folded together. The name space of environment variable names containing lowercase letters is reserved for applications. Applications can define any environment variables with names from this name space without modifying the behavior of the standard utilities.
This applies even to regular, non-exported shell variables because specifying a shell variable with the same name as an environment variable overwrites the latter.
BASH_COMMAND, for example, has a distinct meaning in bash -- and thus can be set to a non-empty value at the front of your script. There's nothing stopping COMMAND from similarly being meaningful to, and already used by, a POSIX-compliant shell interpreter.
If you want to avoid side effects from cases where your shell has set a built-in variable with a name your script uses, or where your script accidentally overwrites a variable meaningful to the shell, stick to lowercase or mixed-case names when writing scripts for POSIX-compliant shells.
The easiest way to solve your problem without altering your general pattern is to use “advanced” parameter expansion features from the Bourne shell (this is not bash-specific). In this case, we can use the :- modifier to supply default values:
COMMAND="${1:-triggerusagemessage}"
shift
case "$COMMAND" in
loop)
loop_
;;
...
triggerusagemessage)
echo $"Usage: $0 {loop|...}"
exit 64
;;
esac
See the paragraph “Parameter Expansion” in the man page of your shell for a short presentation of the available parameter expansion modifiers.
(Note the exit code 64, which is reserved for this case on some operating systems.)
You can simply use $#. It represents the number of given arguments:
if [ $# = 0 ]
then
echo "help ..."
fi
in the line: echo $"Usage: $0 {loop|...}" what's the first $ for?
If you don't want to repeat the message, just put it in a function and check for an empty string before the case statement.
#! /bin/bash
die()
{
"Usage: $0 {loop|...}"
exit 1
}
COMMAND="$1"
[ -z $COMMAND ] && die
shift
case "$COMMAND" in
loop)
loop_
;;
*)
die
exit 1
;;
esac

Using `set -e` in a script prevents ((var++)) increment in bash

#!/bin/bash
set -e
a=0
echo $a
((a++))
echo $a
returns only 0, yet remove the set -e and it returns 0 and then 1, why does ((a++)) return a non-zero status?
Consider the following three facts, taken in combination:
(( a++ )) is a postincrement -- its return value depends on the value from before the increment takes place.
In a numeric context, a zero value is falsey.
set -e instructs the shell to exit if any unchecked command has a false exit status.
Thus, prior to bash 4.1 making an explicit exception for the case (preventing set -e from activating based on the exit status of an arithmetic expression), a numeric context whose contents evaluate to 0 will cause the shell to exit.
For the specific case at hand, you can work around the issue by using a preincrement -- (( ++a )) -- instead.
This is one of the many respects in which behavior of set -e is unintuitive and fault-prone (in addition to being widely incompatible across shells). Its use is thus rightly controversial.
Duffy explains the 'why' very well. After some searching I was also looking at this answer https://askubuntu.com/a/706683
And after considering the performance, I will be using this form in the future:
#!/bin/bash
set -e
a=0
echo $a
((++a))
echo $a
and I'll probably avoid set -e and instead test return values when needed.

Indirect reference in bash, why isn't this working?

I'm trying to tidy up one of my bash scripts by using a function for something that happens 6 times. The script sets a number of variables from a config.ini file and then lists them and asks for confirmation that the user wishes to proceed with these predefined values. If not, it steps through each variable and asks for a new one to be entered (or to leave it blank and press enter to use the predefined value). This bit of code accomplishes that:
echo Current output folder: $OUTPUT_FOLDER
echo -n "Enter new output folder: "
read C_OUTPUT_FOLDER
if [ -n "$C_OUTPUT_FOLDER" ]; then OUTPUT_FOLDER=$C_OUTPUT_FOLDER; fi
The idea is to set $OUTPUT_FOLDER to the value of $C_OUTPUT_FOLDER but only if $C_OUTPUT_FOLDER is not null. If $C_OUTPUT_FOLDER IS null, it will not do anything and leave $OUTPUT_FOLDER as it was for use later in the script.
There are 6 variables that are set from the config.ini so this block is currently repeated 6 times. I've made a function new_config () which is as follows:
new_config () {
echo Current $1: ${!2}
echo -n "Enter new $1: "
read $3
if [ -n "${!3}" ]; then $2=${!3}; fi
}
I'm calling it with (in this instance):
new_config "output folder" OUTPUT_FOLDER C_OUTPUT_FOLDER
When I run the script, it has an error on the if line:
./test.sh: line 9: OUTPUT_FOLDER=blah: command not found
So, what gives? The block of code in the script works fine and (in my quite-new-to-bash eyes), the function should be doing exactly the same thing.
Thanks in advance for any pointers.
The problem is that bash splits the command into tokens before variable substitution, see http://tldp.org/LDP/Bash-Beginners-Guide/html/sect_01_04.html#sect_01_04_01_01. Specifically there's rules for POSIX shells that make assignments a special case for tokenization: "If all the characters preceding '=' form a valid name (see XBD Name), the token ASSIGNMENT_WORD shall be returned." - it's the ASSIGNMENT_WORD token that triggers the assignment path. It doesn't repeat the tokenization after variable substitution, which is why your code doesn't work.
You can get your code to work like so:
new_config () {
echo Current $1: ${!2}
echo -n "Enter new $1: "
read $3
if [[ -n "${!3}" ]]; then echo setting "$2='${!3}'"; eval "$2='${!3}'"; fi
}
new_config "output folder" OUTPUT_FOLDER C_OUTPUT_FOLDER
echo $OUTPUT_FOLDER
As #chepner points out, you can use declare -g $2="${!3}" instead of eval here, and on newer bash versions that's a better answer. Unfortunately declare -g requires bash 4.2, and even though that's 3 years old it's still not everywhere - for example, OS X Mavericks is stuck on 3.2.51.

checking if output of a curl commnd is null or not in shell script

I am writing a shell script and the curl command is stored in a variable like this:
str="curl 'http...'"
and I am evaluating this using
eval $str
I want to check whether the output of this is null or not,means it is returning me something or not.How can I check this?
Why cant I store the output in a variable like this:
op=eval $str
Why does wriring this gives error?
Thanks in advance
Use command substitution:
str="curl 'http...'"
op=$(eval "$str")
if [[ -n $op ]]; then
echo "Output is not null."
else
echo "Output is null.
fi
A better approach actually instead of using eval, is to use arrays as it's safer:
cmd=("curl" "http://...")
op=$("${cmd[#]}")
if [[ -n $op ]]; then
echo "Output is not null."
else
echo "Output is null.
fi
When assigning the array, separate every argument as a single element. You can pick any quoting method whether by the use of single quotes or double quotes if quoting is necessary to keep an argument as one complete argument.
By the way you simply had an error since it wasn't the proper way to call a command in which you'd get the output. As explained you have to use command substitution. An advanced method called process substitution could also be applicable but it's not practical for requirement.

Bash scripting checking content of a variable containing a directory path

I am trying to write a simple bash script on Ubuntu 12.10 which stores the result of the pwd command in a variable and then checks the value of that variable in an if command to see if it matches a particular string. But I keep getting an error because it treats the content of that variable as a directory itself and keeps giving the error "No such file or directory"
The program is as below:
myvar=$(pwd)
if [$myvar -eq /home/vicky] #fails this check as variable myvar contains /home/vicky
then
echo correct
else
echo incorrect
fi
Any help would be appreciated
The proper form for that is
myvar=$(pwd)
if [ "$myvar" = /home/vicky ] ## Need spaces and use `=`.
And since you're in bash, you don't need to use pwd. Just use $PWD. And also, it's preferrable to use [[ ]] since variables are not just to word splitting and pathname expansion when in it.
myvar=$PWD
if [[ $myvar == /home/vicky ]]
Or simply
if [[ $PWD == /home/vicky ]]

Resources