Default Values AND Remove matching suffix - bash

Is there a way to do the default values ${foo:-bar} and the remove matching suffix ${foo%bar} bash feature in the same time ?
Something like that: ${foo%suffix:-default}
In other words, I want:
If foo is unset or null use the default value
If foo is set remove any suffix at the end

Something like that: ${foo%suffix:-default}
It's not possible to achieve what you want in a manner similar to what you've described.
However, you could use control operators to achieve the desired result without making use of temporary variables:
$ FOO=foobar
$ [ -z "$FOO" ] && FOO=default || FOO=${FOO%bar}
$ echo $FOO
foo
$ FOO=
$ [ -z "$FOO" ] && FOO=default || FOO=${FOO%bar}
$ echo $FOO
default

You could use default assignment instead of default substitution. This is also a use for the : "no-op" command:
: ${foo:=default}
echo ${foo%suffix}
testing:
$ default=foobar suffix=bar
$ foo=hello ; : ${foo:=$default}; echo $foo ${foo%$suffix}
hello hello
$ foo=ironbar; : ${foo:=$default}; echo $foo ${foo%$suffix}
ironbar iron
$ unset foo ; : ${foo:=$default}; echo $foo ${foo%$suffix}
foobar foo

Related

Bash add characters before and after $foo

So if I have
$foo = foo
I would like to add : at the beginning and ! at the end. I tried the below:
bar=$( ":" $foo "!" )
You can't use bar=$( ":" $foo "!" ), with this command you try to assign the result of :foo! command to your variable. You will probably get an output like :foo!: command not found and your $bar variable will be not set.
You can use :
foo=foo
bar=':'$foo'!'
echo $bar
# Will return :foo!
the builtin printf can do that also but I'm not sure if it is portable solution.
foo=foo
printf -v bar ":$foo!"
echo "$bar"
see
help printf
or
man printf

In Bash test if associative array is declared

How can I test if an associative array is declared in Bash? I can test for a variable like:
[ -z $FOO ] && echo nope
but I doesn't seem to work for associative arrays:
$ unset FOO
$ declare -A FOO
$ [ -z $FOO ] && echo nope
nope
$ FOO=([1]=foo)
$ [ -z $FOO ] && echo nope
nope
$ echo ${FOO[#]}
foo
EDIT:
Thank you for your answers, both seem to work so I let the speed decide:
$ cat test1.sh
#!/bin/bash
for i in {1..100000}; do
size=${#array[#]}
[ "$size" -lt 1 ] && :
done
$ time bash test1.sh #best of five
real 0m1.377s
user 0m1.357s
sys 0m0.020s
and the other:
$ cat test2.sh
#!/bin/bash
for i in {1..100000}; do
declare -p FOO >/dev/null 2>&1 && :
done
$ time bash test2.sh #again, the best of five
real 0m2.214s
user 0m1.587s
sys 0m0.617s
EDIT 2:
Let's speed compare Chepner's solution against the previous ones:
#!/bin/bash
for i in {1..100000}; do
[[ -v FOO[#] ]] && :
done
$ time bash test3.sh #again, the best of five
real 0m0.409s
user 0m0.383s
sys 0m0.023s
Well that was fast.
Thanks again, guys.
In bash 4.2 or later, you can use the -v option:
[[ -v FOO[#] ]] && echo "FOO set"
Note that in any version, using
declare -A FOO
doesn't actually create an associative array immediately; it just sets an attribute on the name FOO which allows you to assign to the name as an associative array. The array itself doesn't exist until the first assignment.
You can use declare -p to check if a variable has been declared:
declare -p FOO >/dev/null 2>&1 && echo "exists" || echo "nope"
And to check specifically associative array:
[[ "$(declare -p FOO 2>/dev/null)" == "declare -A"* ]] &&
echo "array exists" || echo "nope"
This is a Community Wiki version of an excellent answer by #user15483624 on a question which is now closed as duplicate. Should that user choose to add their own answer here, this should be deleted in favor of the one with their name on it.
The prior answers on this question should be used only when compatibility with bash 4.x and prior is required. With bash 5.0 and later, an expansion to check variable type is directly available; its use is far more efficient than parsing the output of declare -p, and it avoids some of the unintended side effects of other proposals as well..
The following can be used to test whether a bash variable is an associative array.
[[ ${x#a} = A ]]
${x#a} can be used to test whether it is a variable and an array as well.
$ declare x; echo "${x#a}"
$ declare -a y; echo "${y#a}"
a
$ declare -A z; echo "${z#a}"
A
One of the easiest ways is to the check the size of the array:
size=${#array[#]}
[ "$size" -lt 1 ] && echo "array is empty or undeclared"
You can easily test this on the command line:
$ declare -A ar=( [key1]=val1 [key2]=val2 ); echo "szar: ${#ar[#]}"
szar: 2
This method allow you to test whether the array is declared and empty or undeclared altogether. Both the empty array and undeclared array will return 0 size.

BASH: Substituting a variable inside a variable during echo

I explained my question in the comments:
VAR=
INS="Installing $VAR"
echo $INS
. # In each echo command I want to dynamically substitute
. # the $VAR variable in the $INS variable. I want to do
echo $INS # the substitution of the variable on echo command.
Is this possible?
You need a function to do the job gracefully.
say() {
echo "Installing $INS"
}
INS=hello
say
INS=world
say
Or just this:
say() {
echo "Installing $#"
}
say hello
say world
For example:
[ghoti#pc ~]$ cat varins
#!/bin/bash
msg='Installing "$VAR"'
for VAR in foo bar baz; do
eval echo "$msg"
done
[ghoti#pc ~]$ ./varins
Installing foo
Installing bar
Installing baz
[ghoti#pc ~]$
This relies on the fact that $VAR won't be expanded inside single quotes. The eval command will expand the $msg variable, in which the shell will find $VAR to replace.
Parameter substitution can do it also:
ins='aaa $var aaa'
var='xxx'
echo "'${ins//\$var/$var}'"
result:
'aaa xxx aaa'
INS='Installing $VAR'
result=`eval "echo $INS"`
echo $result
you have to use single quotes to not substitute $VAR on creation of INS
eval evaluates $INS at runtime and with echo and backticks it is returned as substituted string
As Hachi said, you need single quotes to prevent $VAR from premature evaluation. Graham suggests in the comments to avoid the subshell call like this:
#!/bin/bash
VAR=17
INS='Installing $VAR'
eval "echo \"$INS\""
VAR=42
eval "echo \"$INS\""
If you don't do it:
VAR=23
INS="Installing $VAR"
echo $(eval "echo $INS")
VAR=8
echo $(eval "echo $INS")
23 is bound immediately, and the assignment VAR=8 isn't noticed.
Result:
Installing 17
Installing 42
Installing 23
Installing 23

Loading variables from a text file into bash script

Is it possible to load new lines from a text file to variables in bash?
Text file looks like?
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
Variables become
$1 = EXAMPLEfoo
$2 = EXAMPLEbar
ans so on?
$ s=$(<file)
$ set -- $s
$ echo $1
EXAMPLEfoo
$ echo $2
EXAMPLEbar
$ echo $#
EXAMPLEfoo EXAMPLEbar EXAMPLE1 EXAMPLE2 EXAMPLE3 EXAMPLE4
I would improve the above by getting rid of temporary variable s:
$ set -- $(<file)
And if you have as input a file like this
variable1 = value
variable2 = value
You can use following construct to get named variables.
input=`cat filename|grep -v "^#"|grep "\c"`
set -- $input
while [ $1 ]
do
eval $1=$3
shift 3
done
cat somefile.txt| xargs bash_command.sh
bash_command.sh will receive these lines as arguments
saveIFS="$IFS"
IFS=$'\n'
array=($(<file))
IFS="$saveIFS"
echo ${array[0]} # output: EXAMPLEfoo
echo ${array[1]} # output: EXAMPLEbar
for i in "${array[#]}"; do echo "$i"; done # iterate over the array
Edit:
The loop in your pastebin has a few problems. Here it is as you've posted it:
for i in "${array[#]}"; do echo " "AD"$count = "$i""; $((count=count+1)); done
Here it is as it should be:
for i in "${array[#]}"; do declare AD$count="$i"; ((count=count+1)); done
or
for i in "${array[#]}"; do declare AD$count="$i"; ((count++)); done
But why not use the array directly? You could call it AD instead of array and instead of accessing a variable called "AD4" you'd access an array element "${AD[4]}".
echo "${AD[4]}"
if [[ ${AD[9]} == "EXAMPLE value" ]]; then do_something; fi
This can be done be with an array if you don't require these variables as inputs to a script. push() function lifted from the Advanced Scripting Guide
push() # Push item on stack.
{
if [ -z "$1" ] # Nothing to push?
then
return
fi
let "SP += 1" # Bump stack pointer.
stack[$SP]=$1
return
}
The contents of /tmp/test
[root#x~]# cat /tmp/test
EXAMPLEfoo
EXAMPLEbar
EXAMPLE1
EXAMPLE2
EXAMPLE3
EXAMPLE4
SP=0; for i in `cat /tmp/test`; do push $i ; done
Then
[root#x~]# echo ${stack[3]}
EXAMPLE1
None of the above will work, if your values are quoted with spaces.
However, not everythinf is lost.
Try this:
eval "$(VBoxManage showvminfo "$VMname" --details --machinereadable | egrep "^(name|UUID|CfgFile|VMState)")"
echo "$name {$UUID} $VMState ($VMStateChangeTime) CfgFile=$CfgFile"
P.S.
Nothing will ever work, if your names are quoted or contain dashes.
If you have something like that, as is the case with VBoxManage output ("IDE-1-0"="emptydrive" and so on), either egrep only specific values, as shown in my example, or silence the errors.
However, silencing erors is always dangerous. You never know, when the next value will have unquoted "*" in it, thus you must treat values loaded this way very careful, with all due precaution.

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

Resources