Bash substitution: log when variable is not set - bash

I use bash substitutions to give neat one-line validation for input, e.g.:
#!/bin/bash
export PARAM1=${1?Error, please pass a value as the first argument"}
# do something...
In some cases though, I want to only log a message when something is unset and then continue as normal. Is this possible at all?

Maybe something along the lines of
test -n "$1" && export PARAM1="$1" || log "\$1 is empty!"
should do; here the test clause returns true if and only if $1 is non-empty.

For regular parameters (in bash 4 or later), you can use the -v operator to check if a parameter (or array element, as of version 4.3) is set:
[[ -v foo ]] || echo "foo not set"
bar=(1 2 3)
[[ -v bar[0] ]] || echo "bar[0] not set"
[[ -v bar[8] ]] || echo "bar[8] not set"
Unfortunately, -v does not work with the positional parameters, but you can use $# instead (since you can't set, say, $3 without setting $1).
(( $# >= 3 )) || echo "third argument not set"
Before -v became available, you would need to compare two default-value expansions to see if a parameter was unset.
[[ -z $foo && ${foo:-bar} == ${foo-bar} ]] && echo "foo is unset, not just empty"
There's nothing special about bar; it's just an arbitrary non-empty string.

Related

Run commands on same line as [[ ]] operator

I am attempting to use the [[ ]] operator in Bash and failing badly.
My script is:
#!/bin/bash
# Test driver for the ProtectDatae script
set -o xtrace
# [[ -z "$1" ]] || source="$1" && source=""
if [[ -z "%1" ]]
then
source=""
else
source=$1
fi
[[ -z "$2" ]] || target="$2" && target="" # This does not work
[[ -z "$3" ]] || sourceBackup="$3" && sourceBackup="/tmp/sourcebackup" # This does not work
source cmd.sh # Get a function to run a Linux command
if [[ -e "$sourceBackup" && "$sourceBackup" -ne "" ]]
then
# If a source backup directory is specified then get rid of any old directory and make a new backup
# and verify it. If OK, make the source directory be the source backup directory
# otherwise work directly in the source directory
if [[ -e "$sourceBackup" ]]
then
cmd "sudo rm -R $sourceBackup" "empty backup directory $sourceBackup failed"
cmd "cp -apu $source $sourceBackup" "backup home directory"
cmd "diff -aprN ~/$source/* $sourceBackup" "bad backup in $sourceBackup"
source="$sourceBackup"
fi
fi
exit 0
My command invocation is ./TestProtectData.sh "~" /tmp/jfghome /tmp/jfgbackup
The result of xtrace is:
+ source='~'
+ [[ -z /tmp/jfghome ]]
+ target=/tmp/jfghome
+ target=""
+ [[ -z /tmp/jfgbackup ]]
+ sourceBackup=/tmp/jfgbackup
+ sourceBackup=/tmp/sourcebackup
+ source cmd.sh
+ [[ -e /tmp/sourcebackup ]]
+ exit 0
What happens with the following line is the error. Both alternatives appear to be executed and the variable winds up being set incorrectly:
[[ -z "$2" ]] || target="$2" && target=""
I have tried both orders for && and || and they both give the same result with the variable target set to blank. On the next line, a similar thing happens with the variable sourceBackup set to the second alternative, and both alternatives appear to be executed. If I use the if then construct it works. What am I doing wrong?
What am I doing wrong?
Your intended logic doesn't match the bash constructs you're using. This line:
[[ -z "$2" ]] || target="$2" && target="" # This does not work
Breaks down to mean if 2 is not empty set target to $2. If that command succeeds, set target to "". The command to the left of && will always succeed - either the -z test succeeds or the target="$2" succeeds. Thus target="" always runs at the end.
You can use if ... ; then ...; else ...; fi or you can look at these ways to effect a ternary operator in bash, including:
#!/bin/bash -e
[[ -z "$3" ]] && sourceBackup="/tmp/sourcebackup" || sourceBackup="$3"
echo $sourceBackup
% ./t.sh 1 2 3
3
Here, if -z test succeeds we set sourceBackup to the default. If the test fails, $3 is not null and we set sourceBackup to $3.
To reiterate, this is not exactly the same as a tertiary operator. But if you get the order correct, it will work.
A plain assignement (foo=bar) always sets the status code to 0, so after target has been set to $2, it is immediately after set to empty. I would start by turning the logic inside out:
target= # Set to empty string
[[ -z $2 ]] && target=$2
However, this is redundant. You could easier simply just write
target=$2
without doing any [[...]]-test. If the second parameter is empty, target will be empty as well. If it is not empty, target will get that value.
There is one point to consider: In case you decide to turn on set -u, to catch uninitialized variables, a target=$2 would abort the script if there is no second parameter. Therefore, you could also write
target=${2:-}
which tells bash that a missing parameter is OK and should be treated as a null string.
Even though it is redundant, if you do not turn on -u, using ${2:-} shows your intent explicitly, and makes your program more maintainable.

How to check for space in a variable in bash?

I am taking baby steps at learning bash and I am developing a piece of code which takes an input and checks if it contains any spaces. The idea is that the variable should NOT contain any spaces and the code should consequently echo a suitable message.
Try this:
#!/bin/bash
if [[ $1 = *[[:space:]]* ]]
then
echo "space exist"
fi
You can use grep, like this:
echo " foo" | grep '\s' -c
# 1
echo "foo" | grep '\s' -c
# 0
Or you may use something like this:
s=' foo'
if [[ $s =~ " " ]]; then
echo 'contains space'
else
echo 'ok'
fi
You can test simple glob patterns in portable shell by using case, without needing any external programs or Bash extensions (that's a good thing, because then your scripts are useful to more people).
#!/bin/sh
case "$1" in
*' '*)
printf 'Invalid argument %s (contains space)\n' "$1" >&2
exit 1
;;
esac
You might want to include other whitespace characters in your check - in which case, use *[[:space:]]* as the pattern instead of *' '*.
You can use wc -w command to check if there are any words. If the result of this output is a number greater than 1, then it means that there are more than 1 words in the input. Here's an example:
#!/bin/bash
read var1
var2=`echo $var1 | wc -w`
if [ $var2 -gt 1 ]
then
echo "Spaces"
else
echo "No spaces"
fi
Note: there is a | (pipe symbol) which means that the result of echo $var1 will be given as input to wc -w via the pipe.
Here is the link where I tested the above code: https://ideone.com/aKJdyN
You could use parameter expansion to remove everything that isn't a space and see if what's left is the empty string or not:
var1='has space'
var2='nospace'
for var in "$var1" "$var2"; do
if [[ ${var//[^[:space:]]} ]]; then
echo "'$var' contains a space"
fi
done
The key is [[ ${var//[^[:space:]]} ]]:
With ${var//[^[:space:]]}, everything that isn't a space is removed from the expansion of $var.
[[ string ]] has a non-zero exit status if string is empty. It's a shorthand for the equivalent [[ -n string ]].
We could also quote the expansion of ${var//[^[:space:]]}, but [[ ... ]] takes care of the quoting for us.

How can I test whether a positional parameter is set in Bash?

I wonder why the test [[ ! -v 1 ]] fails no matter if I pass the 1st positional parameter to the function below:
shopt -os nounset
function foo {
echo -n "$FUNCNAME: 1st positional parameter "
[[ ! -v 1 ]] && echo "is missing." || echo is "\"$1\"."
}
I know there are other ways to test but why doesn't this particular test work?
In this case, you want to check if the parameter is unset.
has_1() {
if [[ -z "${1+present}" ]]; then
echo "no first param"
else
echo "given: $1"
fi
}
The parameter expansion ${var+word} will return "word" only if the parameter is not unset -- i.e. if you pass an empty string, the function will indicate the first parameter is given.
Bash 5.1 has a very straight-forward way to do this. As described in the release notes:
x. `test -v N' can now test whether or not positional parameter N is set.
So to check if the Nth parameter is set can use either of these:
test -v N
[ -v N ]
See a very basic example:
test -v 1 && echo "1st parameter is set: '$1'"
[ -v 2 ] && echo "2nd parameter is set: '$2'"
See some samples:
bash-5.1$ bash script.sh ""
1st parameter is set: ''
bash-5.1$ bash script.sh a b
1st parameter is set: 'a'
2nd parameter is set: 'b'
bash-5.1$ bash script.sh ""
1st parameter is set: ''
bash-5.1$ bash script.sh "" bla
1st parameter is set: ''
2nd parameter is set: 'bla'
if [ "$#" -gt "0" ]; then echo 'ok'; else echo '0'; fi
The following pattern covers most of the practical use cases:
[[ -z "${2:-}" ]] && echo "The 2'nd parameter is unset" >&2 && exit 1
Shell-Parameter-Expansion

Using unset vs. setting a variable to empty

I'm currently writing a bash testing framework, where in a test function, both standard bash tests ([[) as well as predefined matchers can be used. Matchers are wrappers to '[[' and besides returning a return code, set some meaningful message saying what was expected.
Example:
string_equals() {
if [[ ! $1 = $2 ]]; then
error_message="Expected '$1' to be '$2'."
return 1
fi
}
So, when a matcher is used, and it fails, only then an error_message is set.
Now, at some point later, I test whether the tests succeeded. If it succeeded, I print the expectation in green, if it failed in red.
Furthermore, there may be an error_message set, so I test if a message exists, print it, and then unset it (because the following test may not set an error_message):
if [[ $error_message ]]; then
printf '%s\n' "$error_message"
unset -v error_message
fi
Now my question is, if it is better to unset the variable, or to just set it to '', like
error_message=''
Which one is better? Does it actually make a difference? Or maybe should I have an additional flag indicating that the message was set?
Mostly you don't see a difference, unless you are using set -u:
/home/user1> var=""
/home/user1> echo $var
/home/user1> set -u
/home/user1> echo $var
/home/user1> unset var
/home/user1> echo $var
-bash: var: unbound variable
So really, it depends on how you are going to test the variable.
I will add that my preferred way of testing if it is set is:
[[ -n $var ]] # True if the length of $var is non-zero
or
[[ -z $var ]] # True if zero length
As has been said, using unset is different with arrays as well
$ foo=(4 5 6)
$ foo[2]=
$ echo ${#foo[*]}
3
$ unset foo[2]
$ echo ${#foo[*]}
2
So, by unset'ting the array index 2, you essentially remove that element in the array and decrement the array size (?).
I made my own test..
foo=(5 6 8)
echo ${#foo[*]}
unset foo
echo ${#foo[*]}
Which results in..
3
0
So just to clarify that unset'ting the entire array will in fact remove it entirely.
Based on the comments above, here is a simple test:
isunset() { [[ "${!1}" != 'x' ]] && [[ "${!1-x}" == 'x' ]] && echo 1; }
isset() { [ -z "$(isunset "$1")" ] && echo 1; }
Example:
$ unset foo; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's unset
$ foo= ; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's set
$ foo=bar ; [[ $(isunset foo) ]] && echo "It's unset" || echo "It's set"
It's set

Escaping in test comparisons

In the following, I would like check if a given variable name is set:
$ set hello
$ echo $1
hello
$ echo $hello
$ [[ -z \$$1 ]] && echo true || echo false
false
Since $hello is unset, I would expect the test to return true. What's wrong here? I would assume I am escaping the dollar incorrectly.
TYIA
You are testing if \$$1 is empty. Since it begins with a $, it is not empty. In fact, \$$1 expands to the string $hello.
You need to tell the shell that you want to treat the value of $1 as the name of a parameter to expand.
With bash: [[ -z ${!1} ]]
With zsh: [[ -z ${(P)1} ]]
With ksh: tmp=$1; typeset -n tmp; [[ -z $tmp ]]
Portably: eval "tmp=\$$1"; [ -z "$tmp" ]
(Note that these will treat unset and empty identically, which is usually the right thing.)

Resources