Is this if condition equivalent to testing the existence of a variable? [duplicate] - bash

This question already has answers here:
What does the '-' (dash) after variable names do here?
(3 answers)
Closed 2 years ago.
I'm digging around in some the system files on my Mac. In /etc/profile I've found the following excerpt:
if [ "${BASH-no}" != "no" ]; then
[ -r /etc/bashrc ] && . /etc/bashrc
fi
I've tried echo "${x-no}" for various choices of x and it seems like it's printing the value of x whenever x exists (i.e. has been set), and no otherwise.
Which leads me to wonder: Is this condition simply testing whether the variable x has been set?
Further questions: What exactly does - do? Is there a better way to test whether a variable has been set?

The meaning of ${BASH-no} is documented in §2.6.2, Parameter Expansion, of the Single Unix Specification:
${parameter:-[word]}
Use Default Values. If parameter is unset or null, the expansion of word (or an empty string if word is omitted) shall be substituted; otherwise, the value of parameter shall be substituted.
The meaning when the colon is omitted (as in your example) is described slightly later:
In the parameter expansions shown previously, use of the in the format shall result in a test for a parameter that is unset or null; omission of the shall result in a test for a parameter that is only unset.
Then there is a table, which may be easier to understand. Here are the relevant rows:
parameterSet and Not Null
parameterSet but Null
parameterUnset
${parameter:-word}
substitute parameter
substitute word
substitute word
${parameter-word}
substitute parameter
substitute null
substitute word
Here is a reliable, portable way to check whether a variable is not set at all. Note that I am using a + modifier instead of a - modifier in the parameter expansion:
if [ "${BASH+set}" = "" ]; then
echo 'BASH not set at all'
else
echo 'BASH is set, perhaps to the empty string'
fi
The expansion of "${BASH+set}" can only be "" if BASH is entirely unset. If BASH is set, even to the empty string, then "${BASH+set}" expands to "set" instead.

Is this condition simply testing whether the variable x has been set?
Yes, though it gets confused in the unlikely event that BASH=no.
What exactly does - do?
Here's man bash:
[...] Omitting the colon results in a test only for a parameter that is un‐set.
${parameter:-word}
Use Default Values. If parameter is unset or null (see above),
the expansion of word is substituted. Otherwise, the
value of parameter is substituted.
Is there a better way to test whether a variable has been set?
Yes: [ -v BASH ]. However, this is bash specific, so it defeats the purpose of checking if the current shell is bash before doing bash specific operations.

Related

Bash: usage of `true`

In many scripts I've inherited from a former employee I keep seeing this pattern:
if (true $SOME_VAR)&>/dev/null; then
...
fi
or this one
(true $SOME_VAR)&>/dev/null || SOME_VAR="..."
The man page for true says it always returns true, hence I keep wondering, what is the point of these checks? In the first case the then part is always executed, in the second case the right hand part is never executed.
If set -u (a.k.a. set -o nounset) is in effect, true $SOME_VAR will fail when $SOME_VAR is not defined. This is therefore a way to test whether the variable is defined.
To complement jwodder's helpful answer and Fred's helpful answer:
In Bash v4.2+
, the less obscure and more efficient -v operator can be used to test if a variable is defined[1] (note that no $ must be used):
[[ -v SOME_VAR ]]
In older Bash versions and in POSIX-compliant scripts, use Fred's parameter-expansion-based approach, which is also more efficient than the (true ...) approach.
If the intent is to simply provide a default value, as in the (true $SOME_VAR)&>/dev/null || SOME_VAR="..." idiom, use the (POSIX-compliant) technique suggested by kojiro, also based on a parameter expansion:
SOME_VAR=${SOME_VAR-...} # keep $SOME_VAR value or default to '...'
Toby Speight suggests another POSIX-compliant variant, ${SOME_VAR=...}, which directly updates the variable with the default value, if it is undefined; however, it has the side effect of expanding to the (resulting) value - which may or may not be desired. A concise, but also slightly obscure way to suppress the expansion is to pass the expansion to the colon (null) utility (:), which expands, but otherwise ignores its arguments (compared to using true for the same purpose, it is perhaps slightly less confusing):
: ${SOME_VAR=...} # set $SOMEVAR to '...' only if not defined
Note that all parameter expansions shown/mentioned above have a variant that places : before the operator, which then acts not only when the variable is undefined, but also when it is defined but empty (contains the null string):
${SOME_VAR:+...}, ${SOME_VAR:-...}, ${SOME_VAR:=...}
Arguably, this variant behavior is the generally more robust technique, especially given that when set -u (set -o nunset) is not turned on, undefined variables expand to the null (empty) string.
To add to jwodder's explanation:
The use of (...) around true $SOME_VAR to create a subshell is crucial for this somewhat obscure test for variable existence to work as intended.
Without a subshell, the entire script would abort.
The need for a subshell makes the technique not just obscure, but also inefficient (although that won't really be noticeable with occasional use).
Additionally, if set -u (set -o nounset) happens not to be in effect, the technique treats all variables as defined.
With the subshell, only the subshell aborts, which is reflected in its exit code to the current shell: 1, if the subshell aborted (the variable doesn't exist), 0 otherwise.
Therefore, the (true ...) command only evaluates to (conceptually) true if the variable exists.
&>/dev/null suppresses the error message from the subshell that is emitted if the variable doesn't exist.
As an aside: true never produces no output, so it is sufficient to use (true $SOME_VAR)2>/dev/null (suppress stderr only) - this change makes the technique POSIX-compliant (though still not advisable).
It isn't just set -u (set -o nounset) statements inside a script that turn on aborting in case of access to an undefined variable - invoking bash explicitly with command-line option -u has the same effect.
[1] Since Bash v4.3, you can also test whether an array variable has an element with the specified index; e.g.:
a=( one two ); [[ -v a[0] ]] succeeds, because an array element with index 0 exists; works analogously with associative arrays.
The following is probably equivalent, and more straightforward :
if [ "${SOME_VAR+x}" ] then
...
fi
Or, in the assignment case :
[ "${SOME_VAR+x}" ] || SOME_VAR="..."
The + expansion operator expands to a null string if the variable is unset, and to x if it is assigned (assigned a null string still means assigned). In this case, you could replace x by whatever you want (except a null string).
There is also a ${SOME_VAR:+x} variant. The difference is with null strings : :+ expands to a null string if the variable is assigned a null string (while + expands to x if the value is assigned, even if it is a null string).
While not strictly the same,
if [ x"$SOME_VAR" = x ]; then
...
fi
tends to do what you want; that is the if is true if $SOME_VAR is undefined or (difference:) defined to be the zero-length string.
This code does not work if SOME_VAR is unset and -u is set. I believe the following bashism works though: "${SOME_VAR-}" = "".

What is the purpose of setting a variable default to empty in bash?

In general, this syntax is used to guarantee a value, potentially a default argument.
(from the Bash reference manual)
${parameter:-word}
If parameter is unset or null, the expansion of word is substituted.
Otherwise, the value of parameter is substituted.
What would be the purpose of defaulting a variable to empty if the substitution is only chosen when the variable is empty anyway?
For reference, I'm looking at /lib/lsb/init-functions.
"Null" means the variable has a value, and this value is an empty string. The shell knows the variable exists.
"Unset" means the variable has not been defined : it does not exist as far as the shell is concerned.
In its usual mode, the shell will expand null and unset variable to an empty string. But there is a mode (set -u) that allows the shell to throw a runtime error if a variable is expanded when it is unset. It is good practice to enable this mode, because it is very easy to simply mis-type a variable name and get difficult to debug errors.
It can actually be useful from a computing perspective to differentiate between unset and empty variables, you can assign separate semantics to each case. For instance, say you have a function that may receive an argument. You may want to use a (non-null) default value if the parameter is unset, or any value passed to the function (including an empty string) if the parameter is set. You would do something like :
my_function()
{
echo "${1-DEFAULT_VALUE}"
}
Then, the two commands below would provide different outputs:
my_function # Echoes DEFAULT_VALUE
my_function "" # Echoes an empty line
There is also a type of expansion that does not differentiate between null and not set :
"${VAR:-DEFAULT_VALUE}"
They are both useful depending on what you need.
The way to test if a variable is set or not (without running the risk of a runtime error) is the following type of expansion :
"${VAR+VALUE}"
This will expand to an empty string if VAR is unset, or to VALUE if it is set (empty or with a value). Very useful when you need it.
Generally, it is helpful to:
Declare variables explicitely
set -u to prevent silent expansion failure
Explicitly handle unset variables through the appropriate expansion
This will make your scripts more reliable, and easier to debug.

What does the POSIX spec mean when it says this is necessary to avoid ambiguity?

When responding to this comment:
Now I got the the two ":"s are independent, and that's why I couldn't find any document about them. Is the first one needed in this case?
I noticed this paragraph in the spec for the first time:
In the parameter expansions shown previously, use of the <colon> in the format shall result in a test for a parameter that is unset or null; omission of the <colon> shall result in a test for a parameter that is only unset. If parameter is '#' and the colon is omitted, the application shall ensure that word is specified (this is necessary to avoid ambiguity with the string length expansion).
I've seen the matching explanation in the bash reference manual:
When not performing substring expansion, using the form described below (e.g., ‘:-’), Bash tests for a parameter that is unset or null. Omitting the colon results in a test only for a parameter that is unset. Put another way, if the colon is included, the operator tests for both parameter’s existence and that its value is not null; if the colon is omitted, the operator tests only for existence.
before and I understand what the difference is with the colon versions of these expansions.
What confused me just now is this sentence from the spec:
If parameter is '#' and the colon is omitted, the application shall ensure that word is specified (this is necessary to avoid ambiguity with the string length expansion).
I don't understand what ambiguity is possible here if word is unspecified.
None of the expansion sigils are valid in shell variable names so they cannot possibly start a single-character variable name. If they could then using a parameter of # would always be ambiguous without a colon since you could never tell if ${#+foo} meant the length of the variable foo or an alternate expansion on #, etc.
What am I missing here? What ambiguity requires ensuring that word exist? (I mean not having word in this expansion is clearly not useful but that's not the same thing.)
- is also a shell special parameter, whose value is a string indicating which shell options are currently set. For example,
$ echo $-
himBH
${#parameter} is the syntax for the length of a parameter.
$ foo=bar
$ echo ${#foo}
3
The expression ${#-}, therefore is ambiguous: is it the length of the value of $-, or is does it expand to the empty string if $# is empty? (Unlikely, since $# is always an integer and cannot be unset, but syntactically legal.) I interpret the spec to meant that ${#-} should resolve the ambiguity by expanding to the length of $- (which is what most shells seem to do).

variable expansion in curly braces

This is code
a=''
b=john
c=${a-$b}
echo $c
And the output is empty line
And for similar code where first variable is not initialized
b1=doe
c1=${a1-$b1}
echo $c1
And the output is
doe
I do not understand how bash deals with expanding of variables leading to different results.
There are two variants of the ${var-value} notation, one without a colon, as shown, and one with a colon: ${var:-value}.
The first version, without colon, means 'if $var is set to any value (including an empty string), use it; otherwise, use value instead'.
The second version, with colon, means 'if $var is set to any value except the empty string, use it; otherwise, use value instead'.
This pattern holds for other variable substitutions too, notably:
${var:=value}
if $var is set to any non-empty string, leave it unchanged; otherwise, set $var to value.
${var=value}
if $var is set to any value (including an empty string), leave it unchanged; otherwise, set $var to value.
${var:?message}
if $var is set to any non-empty string, do nothing; otherwise, complain using the given message' (where a default message is supplied if message is itself empty).
${var?message}
if $var is set to any value (including an empty string), do nothing; otherwise, complain using the given message'.
These notations all apply to any POSIX-compatible shell (Bourne, Korn, Bash, and others). You can find the manual for the bash version online — in the section Shell Parameter Expansion. Bash also has a number of non-standard notations, many of which are extremely useful but not necessarily shared with other shells.

Bash script what is := for?

Does anyone know what is := for?
I tried googling but it seems google filters all symbol?
I know the below is something like checking if the variable HOME is a directory and then something is not equal to empty string.
if [ "${HOME:=}" != "" ] && [ -d ${HOME} ]
From Bash Reference Manual:
${parameter:=word}
If parameter is unset or null, the expansion of word is assigned to
parameter. The value of parameter is
then substituted. Positional
parameters and special parameters may
not be assigned to in this way.
Basically it will assign the value of word to parameter if and only if parameter is unset or null.
From the Bash man page:
Assign Default Values. If
parameter is unset or null, the
expansion of word is assigned to
parameter. The value of parameter
is then substituted. Positional
parameters and special parameters may
not be assigned to in this way.
Man pages are a wonderful thing. man bash will tell you almost everything you want to know about Bash.

Resources