Persist variable in success or fail shell command - bash

I have the following program.sh:
#!/bin/bash
(true && { echo true1; echo true2; TEST=1; } || { echo false1; echo false2; TEST=0; }) >> program.log
echo test: $TEST
Why the output of program.sh is:
test:
What is a workaround to persist value in TEST?

Using parentheses creates a subshell. Variable assignments in a subshell don't propagate back to the parent shell. Try replacing () with {}.
{ true && { echo true1; echo true2; TEST=1; } || { echo false1; echo false2; TEST=0; }; } >> program.log

Related

Interpret return value of bash function

I would like to interpret the return value of the function a in the parent bash.
I want to use return to stop an intermediate script in the parent bash.
In this case it means, that test2 shouldn't be executed.
But it doesn't work.
And I don't want to use exit, because it stops "everything" in the parent process.
Does exist a solution to do that?
Script:
#!/bin/bash
function a {
return 1
}
echo "test1"
a
echo "test2"
Output:
test1
test2
The output should be just
test1
Perhaps you want
#!/bin/bash
a() {
return 1
}
echo "test1"
if ! a; then
echo "test2"
fi
Or for short
echo "test1"
a || echo "test2"
It seems that set -e can do what you want :
#!/usr/bin/env bash
set -e
function a {
return 1
}
echo "test1"
a
echo "test2"
set -e might be a "bad idea" : https://mywiki.wooledge.org/BashFAQ/105#Exercises

how to pass a function with parameters as a parameter?

There are two shell functions like belows
function call_function {
func=$1
desc=$2
log_file=$3
$func >> ${log_file} 2>&1
...
}
function echo_str {
str=$1
echo "$str"
}
How can I pass a shell function with parameters as a parameter?
I tried this:
call_function $( echo_str "test" ) "Echoing something" /var/logs/my_log.log
but only got
command not found
I googled it but nothing helps.
Thanks a lot in advance!
If you want to do this right, reorder your arguments to put the function to call last, and use shift to pop off the other arguments -- which will leave the function to call and its arguments in the "$#" array.
call_function() {
local log_file desc
log_file=$1; shift || return
desc=$1; shift || return
"$#" >>"$log_file" 2>&1
}
echo_str() {
local str
str=$1
echo "$str"
}
# log_file desc func args
call_function myfile.log "a function to echo something" echo_str "string to echo"
call_function $( echo_str "test" ) "Echoing something" /var/logs/my_log.log
This $( echo_str "test" ) call will execute echo_str "test" which will result in test, so call_function will execute:
test >> /var/logs/my_log.log 2>&1
So, you either create a dedicated function to log messages easily to a log file:
log_msg() {
current_date=$(date -u)
echo "[$current_date] $1" >> $2
}
or change call_function as suggested by #Darkman

Bash get exit status of command when 'set -e' is active?

I generally have -e set in my Bash scripts, but occasionally I would like to run a command and get the return value.
Without doing the set +e; some-command; res=$?; set -e dance, how can I do that?
From the bash manual:
The shell does not exit if the command that fails is [...] part of any command executed in a && or || list [...].
So, just do:
#!/bin/bash
set -eu
foo() {
# exit code will be 0, 1, or 2
return $(( RANDOM % 3 ))
}
ret=0
foo || ret=$?
echo "foo() exited with: $ret"
Example runs:
$ ./foo.sh
foo() exited with: 1
$ ./foo.sh
foo() exited with: 0
$ ./foo.sh
foo() exited with: 2
This is the canonical way of doing it.
as an alternative
ans=0
some-command || ans=$?
Maybe try running the commands in question in a subshell, like this?
res=$(some-command > /dev/null; echo $?)
Given behavior of shell described at this question it's possible to use following construct:
#!/bin/sh
set -e
{ custom_command; rc=$?; } || :
echo $rc
Another option is to use simple if. It is a bit longer, but fully supported by bash, i.e. that the command can return non-zero value, but the script doesn't exit even with set -e. See it in this simple script:
#! /bin/bash -eu
f () {
return 2
}
if f;then
echo Command succeeded
else
echo Command failed, returned: $?
fi
echo Script still continues.
When we run it, we can see that script still continues after non-zero return code:
$ ./test.sh
Command failed, returned: 2
Script still continues.
Use a wrapper function to execute your commands:
function __e {
set +e
"$#"
__r=$?
set -e
}
__e yourcommand arg1 arg2
And use $__r instead of $?:
if [[ __r -eq 0 ]]; then
echo "success"
else
echo "failed"
fi
Another method to call commands in a pipe, only that you have to quote the pipe. This does a safe eval.
function __p {
set +e
local __A=() __I
for (( __I = 1; __I <= $#; ++__I )); do
if [[ "${!__I}" == '|' ]]; then
__A+=('|')
else
__A+=("\"\$$__I\"")
fi
done
eval "${__A[#]}"
__r=$?
set -e
}
Example:
__p echo abc '|' grep abc
And I actually prefer this syntax:
__p echo abc :: grep abc
Which I could do with
...
if [[ ${!__I} == '::' ]]; then
...

A function that can make caller return

I am writing some BASH script and I want it have some error handling mechanism:
function f()
{
command1 || { echo "command1 failed"; return 1; }
command2 || { echo "command2 failed"; return 1; }
command3 || { echo "command3 failed"; return 1; }
command4 || { echo "command4 failed"; return 1; }
}
I want to make this repetitive structure more readable by defining some function:
function print_and_return()
{
echo "$#"
# some way to exit the caller function
}
so that I can write function f as
function f()
{
command1 || print_and_return "command1 failed"
command2 || print_and_return "command2 failed"
command3 || print_and_return "command3 failed"
command4 || print_and_return "command4 failed"
}
What's the best way to achieve this? Thanks.
The Problem
You want to:
Pretty up your code a bit.
DRY up the code a little.
Stop execution the first time a command fails.
One Possible Solution
You can refactor your code. For example:
print_and_return() {
echo "command failed: $#" >&2
return 1
}
commands=("cmd1" "cmd2" "cmd3" "cmd4")
for cmd in "${commands[#]}"; do
{ $cmd || print_and_return "$cmd"; } || break
done
Maybe you can use set -e, though you have to be careful since it exits the shell:
function f()
{
(
set -e
command1
command2
command3
command4
)
}
As long as the commands diagnose any problems, this function will stop when the first command fails.
Test version:
function f()
{
(
set -e
true
echo true
true
echo true
false
echo false
true
echo true
)
}
echo calling function
f
echo still here
Output:
calling function
true
true
still here
NB: When I used the sequence:
echo calling function
if f
then echo still here - f passed
else echo still here - f failed
fi
Then the function behaved differently under bash 3.2.48 on Mac OS X 10.7.4:
calling function
true
true
false
true
still here - f passed
So, inventive, but not wholly reliable.
Similiar to CodeGnome's solution, but without using arrays (which might not be present in every shell):
function callthem()
{
for cmd; do # short form for "for cmd in "$#"; do"
"$cmd" || { echo "$cmd failed." >&2; return 1; }
done
}
function f()
{
callthem command1 command2 command3 command4
# or maybe even
callthem command{1,2,3,4}
}

Behavior of the 'return' statement in Bash functions

I'm having trouble understanding the behavior of the return built-in in Bash. Here is a sample script.
#!/bin/bash
dostuff() {
date | while true; do
echo returning 0
return 0
echo really-notreached
done
echo notreached
return 3
}
dostuff
echo returncode: $?
The output of this script is:
returning 0
notreached
returncode: 3
If, however, the date | is removed from line 4, the output is as I expected:
returning 0
returncode: 0
It seems like the return statement as used above is acting the way I thought the break statement ought to behave, but only when the loop is on the right hand side of a pipe. Why is this the case? I couldn't find anything to explain this behavior in the Bash man page or online. The script acts the same way in Bash 4.1.5 and Dash 0.5.5.
In the date | while ... scenario, that while loop is executed in a subshell due to the presence of the pipe. Thus, the return statement breaks the loop and the subshell ends, leaving your function to carry on.
You'll have to reconstruct the code to remove the pipeline so that no subshells are created:
dostuff() {
# redirect from a process substitution instead of a pipeline
while true; do
echo returning 0
return 0
echo really-notreached
done < <(date)
echo notreached
return 3
}
If you return inside a function, that function will stop executing but the overall program will not exit.
If you exit inside a function, the overall program will exit.
You cannot return in the main body of a Bash script. You can only return inside a function or sourced script.
For example:
#!/usr/bin/env bash
function doSomething {
echo "a"
return
echo "b" # this will not execute because it is after 'return'
}
function doSomethingElse {
echo "d"
exit 0
echo "e" # this will not execute because the program has exited
}
doSomething
echo "c"
doSomethingElse
echo "f" # this will not execute because the program exited in 'doSomethingElse'
Running the above code will output:
a
c
d
But return should terminate a function call, not a subshell. exit is intended to terminate (sub)shell. I think, it's some undocumented bug/feature.
echo | return typed in a commandline gives an error. That's correct - return should be in a function.
f(){ echo|return; } is accepted in the Bash and Dash, but return doesn't terminate a function call.
If return terminates a subshell, it would work outside a function. So, the conclusion is: return terminates a subshell in a function which is strange.
The thing is: the subshell is a separate process.
It doesn't really have a way to say to the parent shell: "I'm exiting because of a return"
There is no such thing in the exit status, which is the only thing the parent shell gets.
To cover this interesting feature of Bash...
return inside if (or any control command with expression like if/while/...)
return inside if with simple and less simple expressions
The subshell explanation is good. Control returns out of the current subshell. Which might be the Bash function. Or it might be any of the nested control commands with an expression which has caused a subshell to be invoked.
for very simple expressions, e.g., "true" or "1 == 1" there is no subshell invoked. So return behaves as ~normal/expected~.
for less simple expressions, e.g., variable expanded and compared with something then return behaves like break;
Simple (no subshell) examples:
$ rtest () { if true; then echo one; return 2; echo two; fi; echo not simple; return 7; }
$ rtest
one
$ echo $?
2
$ rtest () { if [[ 1 == 1 ]] ; then echo one; return 2; echo two; fi; echo not simple; return 7; }
$ rtest
one
$ echo $?
2
$ rtest () { if [[ 1 =~ 1 ]] ; then echo one; return 2; echo two; fi; echo not simple; return 7; }
$ rtest
one
$ echo $?
2
$ rtest () { if $DO ; then echo one; return 2; echo two; else echo three; return 3; fi; echo not simple; return 7; }
$ rtest
one
$ echo $?
2
$ rtest () { if [[ $DO ]]; then echo one; return 2; echo two; else echo three; return 3; fi; echo not simple; return 7; }
$ rtest
three
$ echo $?
3
$ rtest () { if [[ $DO == 1 ]] ; then echo one; return 2; echo two; else echo three; return 3; echo four; fi; echo not simple; return 7; }
$ rtest; echo $?
one
2
$ DO=1; rtest; echo $?
one
2
$ DO=0; rtest; echo $?
three
3
Expression not simple and presuming subshell is invoked, return behaviour is like break;
Not simple (subshell) example ... =~ inside [[ ]]:
$ rtest () { if [[ $DO =~ 1 ]] ; then echo one; return 2; echo two; fi; echo not simple; return 7; }
$ rtest
not simple
$ echo $?
7

Resources