Loop control from within a subshell - bash

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.

Related

Arithmetic comparison does not work in Bash

So, I have a script for moving files from one directory to another. I want to check whether the moving was successful or not, so I use this block of code:
...
mv "${!i}" "$dirname" 2>/dev/null
echo $? # Check MV exit code
if (( $?==1 ))
then
...
The problem is that whether moving was successful or not, then does not work. If I do this instead
if (( $?==0 ))
it instead works in any case. I have read that it may be because $? is treated like a string, and strings have 0 value. However, if I change it to this
if (( $?=="1" ))
it does not work either. I have tried using [[ ... ]] and [ ... ] instead of (( ... )), and -eq instead of ==, removing and adding spaces, adding and removing quotes, but nothing worked.
What am I doing wrong? Maybe there is another way of responding to certain exit code?
The problem is here:
echo $?
if (( $?==1 ))
The first echo $? will echo the return value of your mv command; however, in the if statement, using $? again will give you the return value of the echo command! $? is always the return value of the last command. The last command is the echo and it is always succeeding so you are always getting a 0 return value in that if statement.
What you should do instead is save the value into a variable and then compare things to that variable:
mv "${!i}" "$dirname" 2>/dev/null
ret_val=$?
echo ${ret_val}
if (( ${ret_val}==1 ))
You can check the exit status of your command directly:
if mv "${!i}" "$dirname" 2>/dev/null; then
# Code for successful move
else
# Code for unsuccessful move
fi
Or, to keep the happy path less indented:
if ! mv "${!i}" "$dirname" 2>/dev/null; then
# Code for unsuccessful move
return 1 # Or maybe exit 1 if in a script, not a function
fi
# Code for successful move
As for how the exit status of echo messes up your code, Tyler's answer has that covered.

better way to fail if bash `declare var` fails?

Problem
In some bash scripts, I don't want to set -e. So I write variable declarations like
var=$(false) || { echo 'var failed!' 1>&2 ; exit 1 ; }
which will print var failed! .
But using declare, the || is never taken.
declare var=$(false) || { echo 'var failed!' 1>&2 ; exit 1 ; }
That will not print var failed!.
Imperfect Solution
So I've come to using
declare var=$(false)
[ -z "${var}" ] || { echo 'var failed!' 1>&2 ; exit 1 ; }
Does anyone know how to turn the Imperfect Solution two lines into a neat one line ?
In other words, is there a bash idiom to make the declare var failure neater?
More Thoughts
This seems like an unfortunate mistake in the design of bash declare.
Firstly, the issue of two lines vs. one line can be solved with a little thing called Mr. semicolon (also note the && vs. ||; pretty sure you meant the former):
declare var=$(false); [ -z "${var}" ] && { echo 'var failed!' 1>&2 ; exit 1 ; }
But I think you're looking for a better way of detecting the error. The problem is that declare always returns an error code based on whether it succeeded in parsing its options and carrying out the assignment. The error you're trying to detect is inside a command substitution, so it's outside the scope of declare's return code design. Thus, I don't think there's any possible solution for your problem using declare with a command substitution on the RHS. (Actually there are messy things you could do like redirecting error infomation to a flat file from inside the command substitution and reading it back in from your main code, but just no.)
Instead, I'd suggest declaring all your variables in advance of assigning them from command substitutions. In the initial declaration you can assign a default value, if you want. This is how I normally do this kind of thing:
declare -i rc=-1;
declare s='';
declare -i i=-1;
declare -a a=();
s=$(give me a string); rc=$?; if [[ $rc -ne 0 ]]; then echo "s [$rc]." >&2; exit 1; fi;
i=$(give me a number); rc=$?; if [[ $rc -ne 0 ]]; then echo "i [$rc]." >&2; exit 1; fi;
a=($(gimme an array)); rc=$?; if [[ $rc -ne 0 ]]; then echo "a [$rc]." >&2; exit 1; fi;
Edit: Ok, I thought of something that comes close to what you want, but if properly done, it would need to be two statements, and it's ugly, although elegant in a way. And it would only work if the value you want to assign has no spaces or glob (pathname expansion) characters, which makes it quite limited.
The solution involves declaring the variable as an array, and having the command substitution print two words, the first of which being the actual value you want to assign, and the second being the return code of the command substitution. You can then check index 1 afterward (in addition to $?, which can still be used to check the success of the actual declare call, although that shouldn't ever fail), and if success, use index 0, which elegantly can be accessed directly as a normal non-array variable can:
declare -a y=($(echo value-for-y; false; echo $?;)); [[ $? -ne 0 || ${y[1]} -ne 0 ]] && { echo 'error!'; exit 1; }; ## fails, exits
## error!
declare -a y=($(echo value-for-y; true; echo $?;)); [[ $? -ne 0 || ${y[1]} -ne 0 ]] && { echo 'error!'; exit 1; }; ## succeeds
echo $y;
## value-for-y
I don't think you can do better than this. I still recommend my original solution: declare separately from command substitution+assignment.

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: export not passing variables correctly to parent

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"

Loop first run continue in shell script

How to break out of infinite while loop in a shell script?
I want to implement the following PHP code in shell script(s):
$i=1;
while( 1 ) {
if ( $i == 1 ) continue;
if ( $i > 9 ) break;
$i++;
}
break works in shell scripts as well, but it's better to check the condition in the while clause than inside the loop, as Zsolt suggested. Assuming you've got some more complicated logic in the loop before checking the condition (that is, what you really want is a do..while loop) you can do the following:
i=1
while true
do
if [ "$i" -eq 1 ]
then
continue
fi
# Other stuff which might even modify $i
if [ $i -gt 9 ]
then
let i+=1
break
fi
done
If you really just want to repeat something $count times, there's a much easier way:
for index in $(seq 1 $count)
do
# Stuff
done
i=1
while [ $i -gt 9 ] ; do
# do something here
i=$(($i+1))
done
Is one of the ways you can do it.
HTH

Resources