Clone a variable in bash? - bash

Before explaining my bash problem let me give you some context:
I am writing some scripts using a bash "framework" we use at my current job. One of the feature of the framework is to init a sets of environments variables useful to run jobs on our cluster infrastructure.
These variables depend on a date specified by $YY, $mm and $dd which are also environment variables (yes, this is wired). To use the framework you start by defining the date and then you call a function to init other vars. This works fine when you write scripts that need variables for a specific day only. Today I am writing something which needs variables for 2 different days. Writing this I face a strange issue. For you to better understand the problem I wrote this code that simulate it:
#!/bin/bash
function assign(){
date=$1
date[1]=$YY
date[2]=$mm
date[3]=$dd
}
function display() {
date=$1
echo "${date[1]}/${date[2]}/${date[3]}"
}
export YY=2012
export mm=09
export dd=20
declare -a my_date1=()
assign $my_date1
export YY=2012
export mm=08
export dd=20
declare -a my_date2=()
assign $my_date2
display $my_date1
display $my_date2
The expected output is:
2012/09/20
2012/08/20
But the output is:
2012/08/20
2012/08/20
At first I thought that the assign function filled the array with reference to $YY, $mm and $dd instead of their values. But then I try with the following code and it doesn't change the result.
date[1]=$(echo $YY)
date[2]=$(echo $mm)
date[3]=$(echo $dd)
Can somebody explain me what append?
Maybe something wired with date=$1...

Arrays are not passed by either value or reference in bash. Rather, the value of the expansion of the array is passed by value. When you write
assign $my_date1
the date variable inside assign is null, since $my_date1 expands to an empty string and disappears after word-splitting before the function is called. As a result, $1 is unset.
But date, being a global variable because it was not declared as local, is set correctly using YY et al, then reset on the second call to assign.
Also, note that the first line of your functions does not make date a reference to the argument; it's really just setting the 0th element of what becomes the global date array to the expansion of $1.
Having said I'll that, I'll show you a way to fake it using the declare built-in and indirect parameter expansion.
function assign () {
ref=$1
# Without the -g, we'd declare function-local parameters. The argument is a
# string to evaluate as a variable assignment. If $ref=my_date1, then we do
# 'my_date1[1]=$YY', 'my_date1[2]=$mm', etc.
declare -g "$ref[1]=$YY"
declare -g "$ref[2]=$mm"
declare -g "$ref[3]=$dd"
}
function display () {
ref=$1
# If $ref=my_date1, then idx
# iterates over my_date[1], my_date[2], my_date[3].
# E.g. ${!idx} in the first iteration is ${my_date[1]}.
arr=()
for idx in $ref[{1,2,3}]; do
arr+=( ${!idx} )
done
local IFS="/"
echo "${arr[*]}"
}
export YY=2012 mm=09 dd=20
assign my_date1 # The *name* of the array; no need to predeclare
export YY=2012 mm=08 dd=20
assign my_date2 # The *name* of the array; no need to predeclare
# Again, just the *names* of the arrays
display my_date1
display my_date2

Related

Combine a variable and string and get the value of the variable formed in a single line

I was writing a script where I came across a situation.
Audio_Repo = "/src/audio_123";
Audio_ImgTag = "aud021882";
Audio_Enable = 1;
.....
Video_Repo = "/src/vid_823";
Video_ImgTag = "video9282";
Video_Enable = 0;
....
#Say proj_var ="Audio"
#it could be either Audio or Video based on some conditional check
....
proj_var = "Audio"
....
PROJECT_REPO= ${!{$proj_var"_Repo"}}
#PROJECT_REPO should hold the value "src/audio_123"
But the above representation throws bad substitution error
I know that I could use a temporary variable as follows
temp= $proj_var"_Repo";
PROJECT_REPO = ${!temp};
But I have many properties and I do not want to use temporary variables for each of them. Instead I want single line substitutions.
One way to do it is to use eval:
#! /bin/bash -p
Audio_Repo="/src/audio_123"
Audio_ImgTag=aud021882
Audio_Enable=1
# ...
Video_Repo=/src/vid_823
Video_ImgTag=video9282
Video_Enable=0
# ....
# Say proj_var="Audio"
# it could be either Audio or Video based on some conditional check
# ....
proj_var="Audio"
# ....
eval "Project_Repo=\${${proj_var}_Repo}"
# Project_Repo should hold the value "src/audio_123"
printf '%s\n' "$Project_Repo"
The code prints /src/audio_123.
eval is dangerous, and should be avoided if possible. See Why should eval be avoided in Bash, and what should I use instead?. In this case the temporary variable, despite the increased verbosity, is a better option.
I replaced PROJECT_REPO with Project_Repo to avoid possible a possible clash with an environment variable. See Correct Bash and shell script variable capitalization.
I've fixed some Bash syntax issues in the code in the question. Spaces around = are errors. Semicolons at the ends of lines are unnecessary.
Shellcheck issues some warnings for the code, but they are all harmless.
Another option is to use a helper function:
# ...
# Set the value of the variable whose name is in the first parameter ($1)
# to the value of the variable whose name is in the second parameter ($2).
function setn { printf -v "$1" '%s' "${!2}" ; }
# ...
setn Project_Repo "${proj_var}_Repo"
Using the setn (a poor name, choose a better one) function avoids both a temporary variable and eval.
Uses arrays, not variable names you need to manipulate.
Repo=0
ImgTag=1
Enable=2
Audio=(/src/audio_123 aud021882 1)
Video=(/src/vid_823 video9282 0)
proj_repo=Audio[$Repo]
project_var=${!proj_repo}

Add up parameters in for loop

I've implemented a function which contains a while for loop defined as follows:
func()
{
...
for i in "$#"; do
enum="source"
sourceID=$i
ret_ID $FILE_NAME $sourceID $enum
ID=$req_ID
((ID+=ID))
done
echo $ID
}
The function ret_ID parses a file which contains some variables as
param1=0x00000001
param2=0x00000400
param3=0x000008000
...
No matter how many parameters I pass, echo $ID returns the ID associated with the last parameter and not the sum of all of them. For instance, func param1 param3 returns 32768 and not 32769.
Update: Judging by a comment by the OP, it sounds like glenn jackman's recommendation to switch from letting ret_ID() set a global variable in order to return its result to outputting its result to stdout and capturing the result in a command substitution ($(...)) solved the problem.
Assuming that the problem wasn't the simple logic error Glenn points out (((ID+=ID)) should be ((ID+=req_ID )): The exact cause of the original problem is not known, but since both func() and ret_ID() operate on global variables, it's easy to see how one function could interfere with the other, such as if ret_ID() accidentally also sets variable $ID.
Here's a rewrite of the function that shows this change, and also suggests a few other changes to make the function more robust, most notably the use of local variables:
func()
{
# Declare *local* variables.
local arg enum sourceID retID
# Declare the local result variable *as an integer*
# Also, better to use variable names that at least start with a *lowercase*
# letter to avoid conflicts with *environment* variables.
local -i id=0
# ...
for arg; do # loop over all args; the `in "$#"` part is optional
enum="source"
sourceID=$arg
# Call helper function and let it return its result via *stdout* captured
# through a command substitution rather than by setting a global variable.
# Note the use of double quotes to prevent problems with values with embedded spaces.
reqID=$(ret_ID "$FILE_NAME" "$sourceID" "$enum")
# Add the value returned to $id
# Note that since $id was declared as an integer,
# use of (( ... )) is optional.
id+=$reqID
done
echo "$id"
}

shell scripting passing 2D arrays to function

how to pass 2d array to function in shell script ?
i need to pass matrix to function but it do not work
tr(){
matrix="$3"
num_rows="$1"
num_columns="$2"
f1="%$((${#num_rows}+1))s"
f2=" %9s"
for ((i=1;i<=num_rows;i++)) do
for ((j=1;j<=num_columns;j++)) do
echo -ne "${matrix[$i,$j]}\t"
done
echo -e "\n"
done
tr $rows $columns $x
Use an associative array:
declare -A matrix
Then things like matrix[6,7]=42 will work because "6,7" ist just a string, and associative arrays accept strings as indices. You might as well write things like
matrix[one,two]=three
matrix[yet,another,dimension]="Perry Rhodan"
You can just write any string between [ and ]. Here is a complete example for how to use it.
#!/bin/bash
#
# Example for a function that accepts the name of an associative array ...
# ... and does some work on the array entries, e.g. compute their sum
# We assume that all *values* of the array are integers - no error check
sum() {
local s=0 # we don't want to interfere with any other s
declare -n local var="$1" # now var references the variable named in $1
for value in "${var[#]}" # value runs through all values of the array
do
let s+="$value"
done
echo sum is $s
}
declare -A m # now m is an associative array, accepting any kind of index
m[0,0]=4 # this looks like 2-dimensional indexing, but is is not
m[2,3]=5 # m will accept any reasonable string as an array index
m[678]=6 # m does not care about the number of commas between numbers
m[foo]=7 # m does not even care about the indices being numbers at all
sum m
As you see, the matrix m not really has 2 dimensions. It just takes any string as an index, as long as it does not contains certain shell syntax characters, and comma is allowed in the string.
Please note the reference declare -n ... - this allows simple access to the matrix from within the function and, most important, without knowing the name of the matrix. Thus you can call that function for several matrices with different names.
The keyword local is important. It means that, upon return, var is unset automatically. Otherwise you will have a reference "var" to an associative array. If you ever want to use var later, it will be hard to use it because you cannot use it as anything else but an associative array. And if you try to get rid of it by "unset var", bash will kindly remember that var refers to m, and delete your matrix m instead. In general, make variables in functions be local wherever possible. It even allows me to re-use a name. For example, using "s" as a variable name inside a function may appear dangerous because it might change the value of a global variable "s". But it doesn't - by declaring it local, the function has its own private variable s, and any s that might already exist is untouched.
Just as a demonstration: If you want to see the array indices in the loop, do this:
sum() {
local s=0 # we don't want to interfere with any other s
declare -n local var="$1" # now var references the variable named in $1
for i in "${!var[#]}" # !var means that we run through all indices
do # we really need a reference here because ...
let s+=${var["$i"]} # ... indirections like ${!$1[$i]} won't work
done
echo sum is $s
}

deducing the name of the variable from positional parameters in a posix shell

#!/bin/sh
foo()
{
printf "$1"
}
RANDOM_VAR="hello\n"
foo $RANDOM_VAR
When you pass 1 or more variables to a function, in this case foo, they are accessible through $1 $2 and so on; how do you go a step back from inside foo and print the name of the original variable which in this case means RANDOM_VAR ?
Succinctly, you don't or can't tell from inside the function which variables were used to create the argument to the function. There's no guarantee that the argument is a variable, or a single variable — for example:
foo 123
foo ${HOME}:${PATH}
Further, if the function cares, it is probably mis-written. It should be as nearly self-contained as possible, and therefore independent of such issues. If it isn't, it suggests that maybe the function is not functionally cohesive.

Change a referenced variable in BASH

I am intending to change a global variable inside a function in BASH, however I don't get a clue about how to do it. This is my code:
CANDIDATES[5]="1 2 3 4 5 6"
random_mutate()
{
a=$1 #assign name of input variable to "a"
insides=${!a} #See input variable value
RNDM_PARAM=`echo $[ 1 + $[ RANDOM % 5 ]]` #change random position in input variable
NEW_PAR=99 #value to substitute
ARR=($insides) #Convert string to array
ARR[$RNDM_PARAM]=$NEW_PAR #Change the random position
NEW_GUY=$( IFS=$' '; echo "${ARR[*]}" ) #Convert array once more to string
echo "$NEW_GUY"
### NOW, How to assign NEW_GUY TO CANDIDATES[5]?
}
random_mutate CANDIDATES[5]
I would like to be able to assign NEW_GUY to the variable referenced by $1 or to another variable that would be pointed by $2 (not incuded in the code). I don't want to do the direct assignation in the code as I intend to use the function for multiple possible inputs (in fact, the assignation NEW_PAR=99 is quite more complicated in my original code as it implies the selection of a number depending the position in a range of random values using an R function, but for the sake of simplicity I included it this way).
Hopefully this is clear enough. Please let me know if you need further information.
Thank you,
Libertad
You can use eval:
eval "$a=\$NEW_GUY"
Be careful and only use it if the value of $a is safe (imagine what happens if $a is set to rm -rf / ; a).

Resources