Bad Substitution - Variable name inside other variable name - in Bash - bash

I have a problem in one of my scripts, here it is simplified.
I want to name a variable using another variable in it. My script is:
#! /bin/bash
j=1
SAMPLE${j}_CHIP=5
echo ${SAMPLE${j}_CHIP}
This script echoes:
line 3: SAMPLE1_CHIP=5: command not found
line 4: ${SAMPLE${j}_CHIP}: bad substitution
I'm trying to do that in order to name several samples in a while loop changing the "j" parameter.
Anyone knows how to name a variable like that?

It's possible with eval, but don't use dynamic variable names. Arrays are much, much better.
$ j=1
$ SAMPLES[j]=5
$ echo ${SAMPLES[j]}
5
You can initialize an entire array at once like so:
$ SAMPLES=(5 10 15 20)
And you can append with:
$ SAMPLES+=(25 30)
Indices start at 0.

To read the value of the variable, you may use indirection: ${!var}:
#! /bin/bash
j=1
val=get_5
var=SAMPLE${j}_CHIP
declare "$var"="$val"
echo "${!var}"
The problem is to make the variable get the value.
I used declare above, and the known options are:
declare "$var"="$val"
printf -v "$var" '%s' "$val"
eval $var'=$val'
export "$var=$val"
The most risky option is to use eval. If the contents of var or val may be set by an external user, you have set a way to get code injection. It may seem safe today, but after someone edit the code for some reason, it may get changed to give an attacker a chance to "get in".
Probably the best solution is to avoid all the above.
Associative Array
One alternative is to use Associative Arrays:
#! /bin/bash
j=1
val=get_5
var=SAMPLE${j}_CHIP
declare -A array
array[$var]=$val
echo "${array[$var]}"
Quite less risky and you get a similar named index.
Plain array
But it is clear that the safest solution is to use the simplest of solutions:
#! /bin/bash
j=1
val=get_5
array[j]=$val
echo "${array[j]}"
All done, little risk.

If you really want to use variable variables:
#! /bin/bash
j=1
var="SAMPLE${j}_CHIP"
export ${var}=5
echo "${!var}" # prints 5
However, there are other approaches to solving the parent issue, which are likely less confusing than this approach.

j=1
eval "SAMPLE${j}_CHIP=5"
echo "${SAMPLE1_CHIP}"
Or
j=1
var="SAMPLE${j}_CHIP"
eval "$var=5"
echo "${!var}"

As others said, it's normally not possible. Here is a workaround if you wish. Note that you have to use eval when declaring a nested variable, and ⭗ instead of $ when accessing it (I use ⭗ as a function name, because why not).
#!/bin/bash
function ⭗ {
if [[ ! "$*" = *\{*\}* ]]
then echo $*
else ⭗ $(eval echo $(echo $* | sed -r 's%\{([^\{\}]*)\}%$(echo ${\1})%'))
fi
}
j=1
eval SAMPLE${j}_CHIP=5
echo `⭗ {SAMPLE{j}_CHIP}`
c=CHIP
echo `⭗ {SAMPLE{j}_{c}}`

Related

Bash variable not working properly in a for loop [duplicate]

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

How to write for loop that will create specified numbered files [duplicate]

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

bash: pass associative array as a parameter to another script

I want to be able to do:
Script1.sh
declare -A map=(
['A']=1
['B']=2
['C']=3
['D']=4
)
sh script2.sh ???
Script2.sh
params = ...
echo ${params['A']}
ie, access parameters by keys. I have seen related questions for normal arrays and the answer to them has been to pass the array as:
sh script2.sh "${AR[#]}"
which I believe translates to:
sh script2.sh "${map[0]}" "${map[1]}" "${map[2]}"
But with that, I can only access the elements based on their order.
Is there a clever trick to achieve what I want? perhaps with something that passes on "A=1" "B=2" "C=3" "D=4" instead and have script2.sh parse them? or is there a neater solution?
If you are only calling script2.sh from inside script1.sh, then all you need to do (as #markp-fuso pointed out) is source script2.sh and it will run in the current context with all the data already loaded.
If you really want it to be on the command line, then pass it as key=val and have your code in script2.sh check each of it's args for that format and set them in an associative array.
declare -A map=()
for arg in "$#"
do if [[ "$arg" =~ ^[A-Z]=[0-9]$ ]] # more complex k/v will get ugly
then map[${arg/=?}]=${arg/?=} # as will the assignment w/o eval
fi
done
# And finally, just to see what got loaded -
declare -p map
$: script2.sh C=3 A=1
declare -A map=([A]="1" [C]="3" )
As mentioned above, a more complicated possible set of key names and/or values will require a suitably more complex test as well as assignment logic. Clearly, for anything but the simplest cases, this is going to quickly get problematic.
Even better, set up a full getopts loop, and pass your args with proper indicators. This takes more design and more implementation, but that's what it takes to get more functionality.
Assumptions:
the array is the only item being passed to script2 (this could be relaxed but would likely require adding some option flag processing to script2)
the array name will always be map (could probably make this dynamic but that's for another day)
the array indices and values do not contain any special/control characters (eg, line feeds) otherwise passing the array structure on the command line to script2 gets mucho complicated really quick (there are likely some workarounds for this scenario, too)
Some basic components:
The array named map:
$ declare -A map=(
['A']=1
['B']=2
['C']=3
['D']=4
)
Use typeset to generate a command to (re)produce the contents of array map:
$ typeset -p map
declare -A map=([A]="1" [B]="2" [C]="3" [D]="4" )
From here we can pass the typeset output to script2 and then have script2 evaluate the input, eg:
$ cat script1
echo "in script1"
declare -A map=(
['A']=1
['B']=2
['C']=3
['D']=4
)
./script2 $(typeset -p map)
$ cat script2
echo "in script2"
echo " \$# = $#"
eval "$#"
for i in "${!map[#]}"
do
echo "${i} : ${map[${i}]}"
done
Running script1 generates:
$ ./script1
in script1
in script2
$# = declare -A map=([A]="1" [B]="2" [C]="3" [D]="4" )
A : 1
B : 2
C : 3
D : 4
I know, I know, I know ... eval == evil. I'll have to think about a replacement for eval ... also open to suggestions.

Using a variable in brace expansion range fed to a for loop

Here is myscript.sh
#!/bin/bash
for i in {1..$1};
do
echo $1 $i;
done
If I run myscript.sh 3 the output is
3 {1..3}
instead of
3 1
3 2
3 3
Clearly $3 contains the right value, so why doesn't for i in {1..$1} behave the same as if I had written for i in {1..3} directly?
You should use a C-style for loop to accomplish this:
for ((i=1; i<=$1; i++)); do
echo $i
done
This avoids external commands and nasty eval statements.
Because brace expansion occurs before expansion of variables. http://www.gnu.org/software/bash/manual/bashref.html#Brace-Expansion.
If you want to use braces, you could so something grim like this:
for i in `eval echo {1..$1}`;
do
echo $1 $i;
done
Summary: Bash is vile.
You can use seq command:
for i in `seq 1 $1`
Or you can use the C-style for...loop:
for((i=1;i<=$1;i++))
Here is a way to expand variables inside braces without eval:
end=3
declare -a 'range=({'"1..$end"'})'
We now have a nice array of numbers:
for i in ${range[#]};do echo $i;done
1
2
3
I know you've mentioned bash in the heading, but I would add that 'for i in {$1..$2}' works as intended in zsh. If your system has zsh installed, you can just change your shebang to zsh.
Using zsh with the example 'for i in {$1..$2}' also has the added benefit that $1 can be less than $2 and it still works, something that would require quite a bit of messing about if you wanted that kind of flexibility with a C-style for loop.

Expanding variables Bash Scripting

I have two variables in bash that complete the name of another one, and I want to expand it but don't know how to do it
I have:
echo $sala
a
echo $i
10
and I want to expand ${a10} in this form ${$sala$i} but apparently the {} scape the $ signs.
There are a few ways, with different advantages and disadvantages. The safest way is to save the complete parameter name in a single parameter, and then use indirection to expand it:
tmp="$sala$i" # sets $tmp to 'a10'
echo "${!tmp}" # prints the parameter named by $tmp, namely $a10
A slightly simpler way is a command like this:
eval echo \${$sala$i}
which will run eval with the arguments echo and ${a10}, and therefore run echo ${a10}. This way is less safe in general — its behavior depends a bit more chaotically on the values of the parameters — but it doesn't require a temporary variable.
Use the eval.
eval "echo \${$sala$i}"
Put the value in another variable.
result=$(eval "echo \${$sala$i}")
The usual answer is eval:
sala=a
i=10
a10=37
eval echo "\$$sala$i"
This echoes 37. You can use "\${$sala$i}" if you prefer.
Beware of eval, especially if you need to preserve spaces in argument lists. It is vastly powerful, and vastly confusing. It will work with old shells as well as Bash, which may or may not be a merit in your eyes.
You can do it via indirection:
$ a10=blah
$ sala=a
$ i=10
$ ref="${sala}${i}"
$ echo $ref
a10
$ echo ${!ref}
blah
However, if you have indexes like that... an array might be more appropriate:
$ declare -a a
$ i=10
$ a[$i]="test"
$ echo ${a[$i]}
test

Resources