Bash: export not passing variables correctly to parent - bash

The variables exported in a child-script are undefined the parent-script (a.sh):
#!/bin/bash
# This is the parent script: a.sh
export var="5e-9"
./b.sh var
export result=$res; # $res is defined and recognized in b.sh
echo "result = ${result}"
The child-script (b.sh) looks like this:
#!/bin/bash
# This is the child script: b.sh
# This script has to convert exponential notation to SI-notation
var1=$1
value=${!1}
exp=${value#*e}
reduced_val=${value%[eE]*}
if [ $exp -ge -3 ] || [ $exp -lt 0 ]; then SI="m";
elif [ $exp -ge -6 ] || [ $exp -lt -3 ]; then SI="u";
elif[ $exp -ge -9 ] || [ $exp -lt -6 ]; then SI="n";
fi
export res=${reduced_val}${SI}
echo res = $res
If I now run the parent using ./a.sh, the output will be:
res = 5n
result = 4n
So there is some rounding problem going on here. Anybody any idea why and how to fix it?

To access variable's in b.sh use source instead :
source b.sh var
It should give what you wanted.

Exporting variables in bash includes them in the environment of any child shells (subshells). There is however no way to access the environment of the parent shell.
As far as your problem is concerned, I would suggest writing $res only to stdout in b.sh, and capturing the output via subshell in a.sh, i.e. result=$( b.sh ). This approach comes closer to structured programming (you call a piece of code that returns a value) than using shared variables, and it is more readable and less error-prone.

Michael Jaros' solution is a better approach IMO. You can expand on it to pass any number of variable back to the parent using read:
b.sh
#!/bin/bash
var1="var1 stuff"
var2="var2 stuff"
echo "$var1;$var2"
a.sh
IFS=";" read -r var1 var2 < <(./b.sh );
echo "var1=$var1"
echo "var2=$var2"

Related

How to echo (print) the value of a variable in Bash?

I am trying to write a little script, and I can not figure out how to choose the variable to be echo'ed (echo $'TEST'_"$response") dynamically depending on the user's input:
#!/usr/bin/env sh
response=response
TEST_1="Hi from 1!"
TEST_2="Hi from 2!"
while [ $response ]; do
read -p "Enter a choice between 1 - 2, or 'bye': " response
if [ $response = 'bye' ]; then
echo "See You !"; exit
elif [ $response -ge 1 ] && [ $response -le 2 ]; then
echo $'TEST'_"$response"
else
echo "Input is not a valid value."
fi
done
The desired output would be the value of one of the variables declared at the beginning of my script ("Hi from 1!" or "Hi from 2!"). Instead my script simple outputs the name of the variable as a string "TEST_1" or "TEST_2". I do not simply want to hardcode the variable that will be printed like:
if [ $response -ge 1 ]; then
echo $TEST_1
fi
since it is not scalable. Using backticks like
echo `$'TEST'_"$response"`
doesn't help either since bash will expect to run the result "TEST_1" or "TEST_2" as a command.
Any hint will be greatly appreciated.
You need indirect expansion, to be used with ${!var}:
$ TEST1="hello"
$ TEST2="bye"
$ v=1
$ var="TEST$v" #prepare the variable name of variable
$ echo ${!var} #interpret it
hello
$ v=2
$ var="TEST$v" #the same with v=2
$ echo ${!var}
bye
That is, you need to use a variable name of a variable and this is done with the indirect expansion: you use a variable with the name of the variable and then you evaluate it with the ${!var} syntax.
In your case, use:
myvar="TEST$response"
echo "${!myvar}"
Always use quotes, such as "$string", for anything other than numbers. For numbers, just keep it normal (i.e. $number).

How can I test if files given as an argument exist?

I am making a bash script that you have to give 2 files or more as arguments.
I want to test if the given files exist. I'm using a while loop because I don't know how many files are given. The problem is that the if statement sees the $t as a number and not as the positional parameter $number. Does somebody have a solution?
t=1
max=$#
while [ $t -le $max ]; do
if [ ! -f $t ]; then
echo "findmagic.sh: $t is not a regular file"
echo "Usage: findmagic.sh file file ...."
exit
fi
t=`expr $t + 1`
done
You can do it with the bash Special parameter # in this way:
script_name=${0##*/}
for t in "$#"; do
if [ ! -f "$t" ]; then
echo "$script_name: $t is not a regular file"
echo "Usage: $script_name file file ...."
exit 1
fi
done
With "$#" you are expanding the positional parameters, starting from one as separate words (your arguments).
Besides, remember to provide a meaningful exit status (e.g. exit 1 instead of exit alone). If not provided, the exit status is that of the last command executed (echo in your case, which succes, so you're exiting with 0).
And for last, instead of write the script name (findmagic.sh in your case), you can set a variable at the beginning in your script:
script_name=${0##*/}
and then use $script_name when necessary. In this way you don't need to update your script if it changes its name.

bash passing arguments/parameters?

#!/bin/bash
traverse() {
local x=$1
if [ -d $x ]
then
lst=(`ls $x`)
for((i=${#lst[#]}; --i;)); do
echo "${lst[i]}"
done
else echo "not a directory"
fi
}
traverse
I want to pass a parameter such as "/path/to/this/directory/" when executing program but only works if I'm running the program in the same directory as my bash script file and any other parameter I pass is completely ignored.
the script is supposed to take a parameter and check if it's a directory and if it's a directory then list all the files/folders in descending order. If not display error message.
What is wrong with code, thanks!
This happens because $1 in the function refers to traverse's parameters, not your script's parameters.
To run your function once with each argument, use
for arg in "$#" # "$#" is also the default, so you can drop the 'in ..'
do
traverse "$arg"
done
If you in the future want to pass all the script's parameters to a function, use
myfunc "$#"
This is just the problem at hand, though. Other problems include not quoting your variables and using command expansion of ls, lst=(`ls $x`), instead of globs, lst=( "$x"/* )
You don't need to call ls for that. You can use this code:
traverse() {
local x="$1"
if [ -d "$x" ]; then
arr=( "$x/"* )
for ((i=${#arr[#]}; i>0; i--)); do
echo "${arr[$i]}"
done
else
echo "not a directory"
fi
}
"That other guy" has the right answer. The reason it always looks at the current directory:
you invoke traverse with no arguments
the $1 in the traverse function is empty, therefore $x is empty
the test is therefore [ -d ], and when [ is given 1 argument, it returns success if the argument is not empty. Your if command always executes the "true" block and ls $x is just ls when x is empty
Use [[ ... ]] with bash: it is smarter about empty arguments. Otherwise, quote your variables:
$ x=; [ -d $x ] && echo always true || echo not a directory
always true
$ x=; [[ -d $x ]] && echo always true || echo not a directory
not a directory
$ x=; [ -d "$x" ] && echo always true || echo not a directory
not a directory

Loop control from within a subshell

I want to use subshells for making sure environment changes do not affect different iterations in a loop, but I'm not sure I can use loop control statements (break, continue) inside the subshell:
#!/bin/sh
export A=0
for i in 1 2 3; do
(
export A=$i
if [ $i -eq 2 ]; then continue ; fi
echo $i
)
done
echo $A
The value of A outside the loop is unaffected by whatever happens inside, and that's OK. But is it allowed to use the continue inside the subshell or should I move it outside? For the record, it works as it is written, but maybe that's an unreliable side effect.
Just add
echo "out $i"
after the closing parenthesis to see it does not work - it exits the subshell, but continues the loop.
The following works, though:
#! /bin/bash
export A=0
for i in 1 2 3; do
(
export A=$i
if [ $i -eq 2 ]; then exit 1 ; fi
echo $i
) && echo $i out # Only if the condition was not true.
done
echo $A
Can you simply wrap the entire loop in a subshell?
#!/bin/sh
export A;
A=0
(
for i in 1 2 3; do
A=$i
if [ $i -eq 2 ]; then continue ; fi
echo $i
done
)
echo $A
Note also that you don't need to use export every time you assign to the variable. export does not export a value; it marks the variable to be exported, so that any time a new process is created, the current value of that variable will be added to the environment of the new process.

Using the value of a variable in the name of an argument

I'm writing a bash script which will input arguments, the command would look like this:
command -a -b -c file -d -e
I would like to detect a specific argument (-b) with its specific location ($1, $2, $3)
#! /bin/bash
counter=0
while [ counter -lt $# ]
do
if [ $($counter) == "-b" ]
then
found=$counter
fi
let counter+=1
done
The problem rises in $($counter). Is there a way to use $counter to call the value of an argument? For instance if counter=2, I would like to call the value of argument $2. $($counter) doesn't work.
You can accomplish this without getopts (which is still recommended, though) by reworking your loop.
counter=1
for i in "$#"; do
if [[ $i == -b ]]; then
break
fi
((counter+=1))
done
Simply iterate over the arguments directly, rather than iterating over the argument positions.
bash also does allow indirect parameter expansion, using the following syntax:
#! /bin/bash
counter=0
while [ counter -lt $# ]
do
if [ ${!counter} = "-b" ] # ${!x} uses the value of x as the parameter name
then
found=$counter
fi
let counter+=1
done

Resources