Nested default arguments in bash? - linux

Is it possible to do nested parameter expansion in bash? (e.g.: VAR=${{1:-$ENV_VAR}:-hard-coded default})
I want to set command line arguments with default values. However, before using a hard-coded default I would like to check for an environmental variable. Thus, the expected order would be (e.g.):
$1 -> $ENV_VAR -> "hard-coded default"
I can solve this problem in two ways (see below), but both look bad:
1:
VAR=${1:-$ENV_VAR}
VAR=${VAR:-hard-coded default}
2:
VAR2=$([ -n "${1:-$ENV_VAR}" ] && echo "${1:-$ENV_VAR}" || echo "hard-coded default")
Minimal example:
$ cat test.sh
#!/bin/bash
VAR=${1:-$ENV_VAR}
VAR=${VAR:-hard-coded default}
VAR2=$([ -n "${1:-$ENV_VAR}" ] && echo "${1:-$ENV_VAR}" || echo "hard-coded default")
echo ENV_VAR is "'$ENV_VAR'"
echo VAR is "'$VAR'"
echo VAR2 is "'$VAR2'"
$ ./test.sh
ENV_VAR is ''
VAR is 'hard-coded default'
VAR2 is 'hard-coded default'
$ env ENV_VAR=test ./test.sh
ENV_VAR is 'test'
VAR is 'test'
VAR2 is 'test'
$ ./test.sh parameter
ENV_VAR is ''
VAR is 'parameter'
VAR2 is 'parameter'
$ env ENV_VAR=test ./test.sh parameter
ENV_VAR is 'test'
VAR is 'parameter'
VAR2 is 'parameter'

in this particular case following should work VAR=${1:-${ENV_VAR:-hardcoded}} (the right side of :-) documentation:
${parameter:-word}
If parameter is unset or null, the expansion of word is substituted. Otherwise, the value of parameter is substituted.
the left side is a parameter not the dereferenced value

Parameter expansion in bash supports parameter evaluation in the RHS of expansion modifiers, but by default does not on the LHS.
$: unset a; b=foo; echo "${a:-$b}"; echo "${$b}";
foo
bash: ${$b}: bad substitution
It is possible to achieve similar results with an eval.
$: unset a; b=foo; foo=bar; echo "${a:=$b}"; eval "echo \${$a}";
foo
bar
It's probably better to just break your logic out into several statements, and add comments.

Related

Is there a reason for the backticks and echo in “export var=`echo ${var1}/…`”?

This query is with regards to how environment variables in Linux are declared in a environment setup file viz environment_var.sh. This file has a bunch of export commands (something like below ) -
echo "Input Variable: "$1
if [ $1 = "value1" ]; then
variable1=""
else
variable1=$1
fi
export variable2=app_name${variable1}.subcategory2
export variable3=`echo ${variable1}/subfolder1/`
My query is related to the second export statement where echo has been used for variable 3. Is there any specific reason for using echo or could it have been declared as below without the need to use backtick and echo -
export variable3=${variable1}/subfolder1/
Appreciate your inputs in this regard.
The echo is worse than useless. With echo, your directory name is subject to both pathname expansion and word splitting.
For example, consider a directory with these subdirectories:
$ find . -name 'subfolder1'
./dir1/subfolder1
./dir*/subfolder1
./dir2/subfolder1
Now, let's execute the command with echo:
$ var1='dir*'
$ var3=`echo ${var1}/subfolder1/`
To display the precise value of var3, let's use declare -p:
$ declare -p var3
declare -- var3="dir1/subfolder1/ dir2/subfolder1/ dir*/subfolder1/"
Because of pathname expansion, var3 is changed from what one might expect.
Now, let's try again without echo:
$ var1='dir*'
$ var3=${var1}/subfolder1/
$ declare -p var3
declare -- var3="dir*/subfolder1/"
In this case, no pathname expansion was performed and var3 is exactly what we assigned it to be.
Conclusion: don't use the echo form unless you explicitly want pathname expansion or word splitting.
As Charles Duffy points out, if you did want pathname expansion, then you probably also want to use arrays, not echo. Observe:
$ var1='dir*'
$ arr3=(${var1}/subfolder1/)
$ declare -p arr3
declare -a arr3=([0]="dir1/subfolder1/" [1]="dir2/subfolder1/" [2]="dir*/subfolder1/")
Here, all three directory names appear correctly as separate entries in arr3.
Still more problems with echo
Using bash, observe:
$ var1='-n dir'
$ var3=`echo ${var1}/subfolder1/`
$ declare -p var3
declare -- var3="dir/subfolder1/"
In this case, we assigned var1 to -n dir but, in var3, the -n disappeared. That is because it was interpreted not as an argument to be printed but as an option to echo and echo gobbled it up.

Check if multiple variables are empty and set their values bourne shell

I have many variables var1 var2 ... varx that may or may not be empty. I don't want to have multiple if statements and was wondering how to make a loop, to which I supply a list of variables and it checks if they are empty. If a variable is empty, its value should be set to -
This puts you into eval territory:
for var in var1 var2 varx; do
eval 'val=$'"$var"
if [ -z "$val" ]; then
eval "${var}=-"
fi
done
Note that any source that can provide arbitrary variable names can abuse this to run arbitrary code; ensure that you only use it in scenarios, like the above, where the values for var are under your control.
As #hek2mgl first mentioned, you can assign a default value to a variable if it does not already have a value with
: ${var1:=--}
: ${var2:=--}
: ${var3:=--}
# etc
Note that this takes more space than a loop would, but it is more readable, as it doesn't require any indirection to decipher.
I solved it by creating an array arr, instead of having multiple variables ,iterating over the elements of the array and replacing empty ones with --
for element in "${!arr[#]}"; do
out[$element]=${out[$element]:---}
done
Bash allows you to do recursive substitution of variable names, so you can use things like ${var${suffix}}, but sh(1) doesn't, so you have to use eval expressions to allow for deferred variable expansion, like in:
#!/bin/sh
TEMPFILE="/tmp/defaults.sh-$$"
trap 'rm -f "$TEMPFILE"' 1 2 3 15
# FORMAT IS FIRST WORD IS VARIABLE NAME, REST OF LINE IF DEFAULT VALUE.
cat > "$TEMPFILE" << EOF
A default value for variable A
B default value for variable B
C default value for variable C
PATH /bin:/usr/bin
HOME $HOME
EOF
while read name value
do
eval echo "$name==\$$name"
if
[ -z "`eval echo \\\$$name`" ]
then
echo "$name=\"$value\""
eval "$name=\"$value\""
fi
done <"$TEMPFILE"
while read name dumb
do
eval echo "[$name] =\> \$$name"
done >&2 <"$TEMPFILE"
rm -f "$TEMPFILE"
I've used a temporary file, as I make two passes through it (I did this for debugging purposes) but if you don't need the second pass, just do
#!/bin/sh
while read name value
do
eval echo "$name==\$$name"
if
[ -z "`eval echo \\\$$name`" ]
then
echo "$name=\"$value\""
eval "$name=\"$value\""
fi
done <<EOF
A default value for variable A
B default value for variable B
C default value for variable C
PATH /bin:/usr/bin
HOME $HOME
EOF
So, finally, without traces, you get something like this:
#!/bin/sh
while read name value
do
if [ -z "`eval echo \\\$$name`" ]
then
eval "$name=\"$value\""
fi
done <<EOF
A default value for variable A
B default value for variable B
C default value for variable C
PATH /bin:/usr/bin
HOME $HOME
ANOTHER_VARIABLE default value for ANOTHER_VARIABLE.
EOF
NOTE
I have tested this against GNU Bash and FreeBSD Bourne Shell. The golden test should be to test it against AT&T V7 bourne shell, but I don't have one at hand to make the tests. My apologies for that (but working on it!) :)

How to assemble variable by string

I have the following shell script, let's say its' name test.sh
#!/bin/bash
b_x='it is the value of bx'
c_x='it is the value of cx'
case "$1" in
"b")
echo $1_x # it's doesn't work; I want to echo $b_x
;;
"c")
echo $1_x # it's doesn't work; I want to echo $c_x
;;
esac
And then I want to call the script;
./test.sh b # I want the output is "it is the value of bx"
./test.sh c # I want the output is "it is the value of cx"
You don't need case. Just use indirect variable name expansion:
b_x='it is the value of bx'
c_x='it is the value of cx'
var="${1}_x"
echo "${!var}"
Then run it as:
$> bash test.sh b
it is the value of bx
$> bash test.sh c
it is the value of cx
You're asking how to use a variable to name a variable. The standard POSIX method would be to use eval.
$ a_x="Hello world"
$ foo="a"
$ eval echo "\$${foo}_x"
Hello world
Note the escaped dollar sign, which is used to expand the evaluated variable caused by expanding the first variable.
Most folks will tell you that you probably shouldn't use eval, it's dangerous and unruly. I will also tell you this, though there are cases where eval does exactly what you need and you can control its input.
Instead, bash offers something called "indirection" (which you can search for in man bash). You use it like this:
$ a_x="Hello world"
$ foo=a
$ bar="${foo}_x"
$ echo "${!bar}"
Hello world
Note the extra variable.

Checking if the first argument passed to a bash script is set?

I'm attempting to see whether the first argument passed to my script is set. I used the instructions found here to create a test:
How to check if a variable is set in Bash?
Here's my script:
var=$1
if [ -z ${var+x} ]; then echo "var is unset"; else echo "var is set to '$var'"; fi
Suppose I run it without any arguments:
ole#MKI:./test.sh
var is set to ''
Suppose I run it with an argument:
ole#MKI:./test.sh foo
var is set to 'foo'
In neither case does it report that the var is unset.
Thoughts?
TIA,
Ole
The problem is with this line:
var=$1
This sets var and it does so regardless of whether $1 is set or not. The solution is to test $1:
if [ -z ${1+x} ]; then echo "var is unset"; else echo "var is set to '$1'"; fi
This approach works:
$ test.sh
var is unset
$ test.sh a
var is set to 'a'
Even if no parameter is provided when the script is run, the var variable is set. It is assigned the empty string.
The shell also set the $# special parameter to the number of parameters.
Give a try to this:
if [[ $# = 0 ]] ; then printf "no parameter\n"; exit 1; else printf "At least one parameter\n"; var="${1}"; fi
If you go ahead with other additional parameters such as options, then you may consider to use the getopts - parse utility options - from The Open Group Base Specifications Issue 7

How to do the expansion of variable in shell script? [duplicate]

This question already has an answer here:
Bash expand variable in a variable
5 answers
I hava a column in database which contains the following data:
sql_proc|test_sql.sql|/home/Desktop/myfile.txt|$IMP_FILES/myFolder|convert
I have fetched this into a variable and I have used the cut command with "|" as delimiter and saved each field in a different variable. Now let's consider VAR4 holds 4th field, i.e $IMP_FILES/myFolder. $IMP_FILES is an environment variable with value /home/OffFiles/Module.
How can I use VAR4 in order to get /home/OffFiles/Module/myFolder?
Using bash -c:
newvar="$(bash -c "echo $var")"
Using eval:
newvar="$(eval "echo $var")"
Example:
#!/bin/bash
var='$PATH'
echo "$var" #This will show that $var contains the string $PATH literally
#Using bash -c
newvar="$(bash -c "echo "$var"")"
echo "$newvar"
#using eval
newvar="$(eval "echo "$var"")"
echo "$newvar"
It will print the environment variable paths twice.
The insecure solution is to use eval:
eval echo "$VAR4"
Don't do this, though, imagine what happens when VAR4='|rm -rf /'!
You can use associative array instead of environment variables.
#!/bin/bash
declare -A dict
dict[IMP_FILES]=/home/OffFiles/Module
# ...
VAR4='$IMP_FILES/myFolder'
while [[ $VAR4 =~ \$([_A-Z]+) ]] ; do
key=${BASH_REMATCH[1]}
VAR4=${VAR4/\$$key/${dict[$key]}}
done
echo "$VAR4"
Note that the solution is recursive, i.e. it can grow the string endlessly if the dictionary contains a key in a value. I'd drop shell and move to Perl, which can handle similar problems more easily.

Resources