Bash: Hide global variable using local variable with same name - bash

I'd like to use a global variable in a function but don't want the change to go outside the function. So I defined a local variable initialized to the value of the global variable. The global variable has a great name, so I want to use the same name on the local variable. This seems doable in Bash, but I'm not sure if this is undefined behavior.
#!/bin/bash
a=3
echo $a
foo() {
local a=$a ## defined or undefined?
a=4
echo $a
}
foo
echo $a
Gives output:
3
4
3

Expansion happen before assignment (early on) as the documentation states:
Expansion is performed on the command line after it has been split into words.
So the behavior should be predictable (and defined). In local a=$a when expanding $a it's still the global one. The command execution (assignment/declaration) happens later (when $a has already been replaced by its value).
However I am not sure this would not get confusing to have essentially two different variables (scope dependent) with the same name (i.e. appearing to be the one and same). So, I'd rather question wisdom of doing so on coding practices / readability / ease of navigation grounds.

There is a new shell option in Bash 5.0, localvar_inherit, to have local variables with the same name inherit the value of a variable with the same name in the preceding scope:
#!/usr/bin/env bash
shopt -s localvar_inherit
myfunc() {
local globalvar
echo "In call: $globalvar"
globalvar='local'
echo "In call, after setting: $globalvar"
}
globalvar='global'
echo "Before call: $globalvar"
myfunc
echo "After call: $globalvar"
with the following output:
Before call: global
In call: global
In call, after setting: local
After call: global
If you don't have Bash 5.0, you have to set the value in the function, as you did in your question, with the same result.

Related

Bash local variable scope best practice

I've seen that some people when writing bash script they define local variables inside an if else statement like example 1
Example 1:
#!/bin/bash
function ok() {
local animal
if [ ${A} ]; then
animal="zebra"
fi
echo "$animal"
}
A=true
ok
For another example, this is the same:
Example 2:
#!/bin/bash
function ok() {
if [ ${A} ]; then
local animal
animal="zebra"
fi
echo "$animal"
}
A=true
ok
So, the example above printed the same result but which one is the best practice to follow. I prefer the example 2 but I've seen a lot people declaring local variable inside a function like example 1. Would it be better to declare all local variables on top like below:
function ok() {
# all local variable declaration must be here
# Next statement
}
the best practice to follow
Check your scripts with https://shellcheck.net .
Quote variable expansions. Don't $var, do "$var". https://mywiki.wooledge.org/Quotes
For script local variables, prefer to use lowercase variable names. For exported variables, use upper case and unique variable names.
Do not use function name(). Use name(). https://wiki.bash-hackers.org/scripting/obsolete
Document the usage of global variables a=true. Or add local before using variables local a; then a=true. https://google.github.io/styleguide/shellguide.html#s4.2-function-comments
scope best practice
Generally, use the smallest scope possible. Keep stuff close to each other. Put local close to the variable usage. (This is like the rule from C or C++, to define a variable close to its usage, but unlike in C or C++, in shell declaration and assignment should be on separate lines).
Note that your examples are not the same. In the case variable A (or a) is an empty string, the first version will print an empty line (the local animal variable is empty), the second version will print the value of the global variable animal (there was no local). Although the scope should be as smallest, animal is used outside of if - so local should also be outside.
The local command constrains the variables declared to the function scope.
With that said, you can deduce that doing so inside an if block will be the same as if you did outside of it, as long as it's inside of a function.

Use variable's value to get another variable [duplicate]

I am trying to create an environment variable in bash script, user will input the name of environment variable to be created and will input its value as well.
this is a hard coded way just to elaborate my question :
#!/bin/bash
echo Hello
export varName="nameX" #
echo $varName
export "$varName"="val" #here I am trying to create an environment
#variable whose name is nameX and assigning it value val
echo $nameX
it works fine
it's output is :
Hello
nameX
val
But, I want a generic code. So I am trying to take input from user the name of variable and its value but I am having trouble in it. I don't know how to echo variable whose name is user-defined
echo "enter the environment variable name"
read varName
echo "enter the value to be assigned to env variable"
read value
export "$varName"=$value
Now, I don't know how to echo environment variable
if I do like this :
echo "$varName"
it outputs the name that user has given to environment variable not the value that is assigned to it. how to echo value in it?
Thanks
To get closure: the OP's question boils down to this:
How can I get the value of a variable whose name is stored in another variable in bash?
var='value' # the target variable
varName='var' # the variable storing $var's *name*
gniourf_gniourf provided the solution in a comment:
Use bash's indirection expansion feature:
echo "${!varName}" # -> 'value'
The ! preceding varName tells bash not to return the value of $varName, but the value of the variable whose name is the value of $varName.
The enclosing curly braces ({ and }) are required, unlike with direct variable references (typically).
See https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
The page above also describes the forms ${!prefix#} and ${!prefix*}, which return a list of variable names that start with prefix.
bash 4.3+ supports a more flexible mechanism: namerefs, via declare -n or, inside functions, local -n:
Note: For the specific use case at hand, indirect expansion is the simpler solution.
var='value'
declare -n varAlias='var' # $varAlias is now another name for $var
echo "$varAlias" # -> 'value' - same as $var
The advantage of this approach is that the nameref is effectively just an another name for the original variable (storage location), so you can also assign to the nameref to update the original variable:
varAlias='new value' # assign a new value to the nameref
echo "$var" # -> 'new value' - the original variable has been updated
See https://www.gnu.org/software/bash/manual/html_node/Shell-Parameters.html
Compatibility note:
Indirect expansion and namerefs are NOT POSIX-compliant; a strictly POSIX-compliant shell will have neither feature.
ksh and zsh have comparable features, but with different syntax.

creating environment variable with user-defined name - indirect variable expansion

I am trying to create an environment variable in bash script, user will input the name of environment variable to be created and will input its value as well.
this is a hard coded way just to elaborate my question :
#!/bin/bash
echo Hello
export varName="nameX" #
echo $varName
export "$varName"="val" #here I am trying to create an environment
#variable whose name is nameX and assigning it value val
echo $nameX
it works fine
it's output is :
Hello
nameX
val
But, I want a generic code. So I am trying to take input from user the name of variable and its value but I am having trouble in it. I don't know how to echo variable whose name is user-defined
echo "enter the environment variable name"
read varName
echo "enter the value to be assigned to env variable"
read value
export "$varName"=$value
Now, I don't know how to echo environment variable
if I do like this :
echo "$varName"
it outputs the name that user has given to environment variable not the value that is assigned to it. how to echo value in it?
Thanks
To get closure: the OP's question boils down to this:
How can I get the value of a variable whose name is stored in another variable in bash?
var='value' # the target variable
varName='var' # the variable storing $var's *name*
gniourf_gniourf provided the solution in a comment:
Use bash's indirection expansion feature:
echo "${!varName}" # -> 'value'
The ! preceding varName tells bash not to return the value of $varName, but the value of the variable whose name is the value of $varName.
The enclosing curly braces ({ and }) are required, unlike with direct variable references (typically).
See https://www.gnu.org/software/bash/manual/html_node/Shell-Parameter-Expansion.html
The page above also describes the forms ${!prefix#} and ${!prefix*}, which return a list of variable names that start with prefix.
bash 4.3+ supports a more flexible mechanism: namerefs, via declare -n or, inside functions, local -n:
Note: For the specific use case at hand, indirect expansion is the simpler solution.
var='value'
declare -n varAlias='var' # $varAlias is now another name for $var
echo "$varAlias" # -> 'value' - same as $var
The advantage of this approach is that the nameref is effectively just an another name for the original variable (storage location), so you can also assign to the nameref to update the original variable:
varAlias='new value' # assign a new value to the nameref
echo "$var" # -> 'new value' - the original variable has been updated
See https://www.gnu.org/software/bash/manual/html_node/Shell-Parameters.html
Compatibility note:
Indirect expansion and namerefs are NOT POSIX-compliant; a strictly POSIX-compliant shell will have neither feature.
ksh and zsh have comparable features, but with different syntax.

How to use eval to force variable update

I was reading the bash advanced scripting guide (if memory serves me right), and it said something to the extent that eval can be used to force variable updates.
So I tried this:
randomPath="/path/$var/here/" # var is not defined at this point
echo $randomPath
/path//here/
var="is" # initially defining var
eval $randomPath
zsh: no such file or directory: /path//here/
I don't understand the error message, and I'm wondering if I'm using eval properly.
The output I was expecting is:
eval $randomPath
echo $randomPath
/path/is/here
The problem is that $var is already being substituted in randomPath="/path/$var/here/", and because it is blank, randomPath is set to /path//here. You want to use single quotes to prevent the early substitution:
randomPath='/path/$var/here/'
The second problem is that eval x runs x as a command. What you want to do is return the newly evaluated variable as a string:
eval echo $randomPath
You can store it in a variable in the usual way:
randomPath=`eval echo $randomPath`

Scope of variables in KSH

I have written a sample KornShell function to split a String, put it in an array and then print out the values.
The code is as below
#!/usr/bin/ksh
splitString() {
string="abc#hotmail.com;xyz#gmail.com;uvw#yahoo.com"
oIFS="$IFS";
IFS=';'
set -A str $string
IFS="$oIFS"
}
splitString
echo "strings count = ${#str[#]}"
echo "first : ${str[0]}";
echo "second: ${str[1]}";
echo "third : ${str[2]}";
Now the echo does not print out the values of the array, so I assume it has something to do with the scope of the array defined.
I am new to Shell scripting, can anybody help me out with understanding the scope of variables in the example above?
The default scope of a variable is the whole script.
However, when you declare a variable inside a function, the variable becomes local to the function that declares it. Ksh has dynamic scoping, so the variable is also accessible in functions that are invoked by the function that declares the variable. This is tersely documented in the section on functions in the manual. Note that in AT&T ksh (as opposed to pdksh and derivatives, and the similar features of bash and zsh), this only applies to functions defined with the function keyword, not to functions defined with the traditional f () { … } syntax. In AT&T ksh93, all variables declared in functions defined with the traditional syntax are global.
The main way of declaring a variable is with the typeset builtin. It always makes a variable local (in AT&T ksh, only in functions declared with function). If you assign to a variable without having declared it with typeset, it's global.
The ksh documentation does not specify whether set -A makes a variable local or global, and different versions make it either. Under ksh 93u, pdksh or mksh, the variable is global and your script does print out the value. You appear to have ksh88 or an older version of ksh where the scope is local. I think that initializing str outside the function would create a global variable, but I'm not sure.
Note that you should use a local variable to override the value of IFS: saving to another variable is not only clumsy, it's also brittle because it doesn't restore IFS properly if it was unset. Furthermore, you should turn off globbing, because otherwise if the string contains shell globbing characters ?*\[ and one of the words happens to match one or more file on your system it will be expanded, e.g. set -A $string where string is a;* will result in str containing the list of file names in the current directory.
set -A str
function splitString {
typeset IFS=';' globbing=1
case $- in *f*) globbing=;; esac
set -f
set -A str $string
if [ -n "$globbing" ]; then set +f; fi
}
splitString "$string"
Variables are normally global to the shell they're defined in from the time they're defined.
The typeset command can make them local to the function they're defined in, or alternatively to make them automatically exported (even when they're updated.)
Read up "typeset" and "integer" in the manpage, or Korn's book.

Resources