Bash Function Not Exiting With Errexit [duplicate] - bash

In the bash man page, it states:
Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as
part of a command list enclosed by braces...
So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:
function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }
( set -Eex ; a )
+ a
+ b
+ false
( set -Eex ; ap )
+ ap
+ b
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
Now if I apply a conditional to each of them...
( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false

You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:
-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.

bug-bash mailing list has an explanation by Eric Blake more explicit about functions:
Short answer: historical compatibility.
...
Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive. But
it is standardized, so we have to live with it.
And some words about whether set -e can be exploited to achieve the wanted behavior:
Because once you are in a context that ignores 'set -e', the historical
behavior is that there is no further way to turn it back on, for that
entire body of code in the ignored context. That's how it was done 30
years ago, before shell functions were really thought about, and we are
stuck with that poor design decision.

Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:
function on_error() {
echo "error happened!"
}
trap on_error ERR
echo "OK so far"
false
echo "this line should not execute"
The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436

not an answer but you might fix this counter intuitive behaviur by defining this helper function:
fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }
then call your functions via fixerrexit.
example:
f1()
{
mv unimportant-0.txt important.txt
rm unimportant-*.txt
}
set -e
if fixerrexit f1
then
echo here is the important file: important.txt
echo unimportant files are deleted
fi
if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.
the only downside is you can not set variables since it runs f1 inside a subshell.

Related

Bash - exit does not exit script (only subshell) [duplicate]

How would you exit out of a function if a condition is true without killing the whole script, just return back to before you called the function.
Example
# Start script
Do scripty stuff here
Ok now lets call FUNCT
FUNCT
Here is A to come back to
function FUNCT {
if [ blah is false ]; then
exit the function and go up to A
else
keep running the function
fi
}
Use:
return [n]
From help return
return: return [n]
Return from a shell function.
Causes a function or sourced script to exit with the return value
specified by N. If N is omitted, the return status is that of the
last command executed within the function or script.
Exit Status:
Returns N, or failure if the shell is not executing a function or script.
Use return operator:
function FUNCT {
if [ blah is false ]; then
return 1 # or return 0, or even you can omit the argument.
else
keep running the function
fi
}
If you want to return from an outer function with an error without exiting you can use this trick:
do-something-complex() {
# Using `return` here would only return from `fail`, not from `do-something-complex`.
# Using `exit` would close the entire shell.
# So we (ab)use a different feature. :)
fail() { : "${__fail_fast:?$1}"; }
nested-func() {
try-this || fail "This didn't work"
try-that || fail "That didn't work"
}
nested-func
}
Trying it out:
$ do-something-complex
try-this: command not found
bash: __fail_fast: This didn't work
This has the added benefit/drawback that you can optionally turn off this feature: __fail_fast=x do-something-complex.
Note that this causes the outermost function to return 1.
My use case is to run the function unless it's already running. I'm doing
mkdir /tmp/nice_exit || return 0
And then at the end of the function
rm -rf /tmp/nice_exit

Bash's errexit inside of a function behaves unexpectedly depending on calling context [duplicate]

In the bash man page, it states:
Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as
part of a command list enclosed by braces...
So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:
function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }
( set -Eex ; a )
+ a
+ b
+ false
( set -Eex ; ap )
+ ap
+ b
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
Now if I apply a conditional to each of them...
( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:
-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.
bug-bash mailing list has an explanation by Eric Blake more explicit about functions:
Short answer: historical compatibility.
...
Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive. But
it is standardized, so we have to live with it.
And some words about whether set -e can be exploited to achieve the wanted behavior:
Because once you are in a context that ignores 'set -e', the historical
behavior is that there is no further way to turn it back on, for that
entire body of code in the ignored context. That's how it was done 30
years ago, before shell functions were really thought about, and we are
stuck with that poor design decision.
Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:
function on_error() {
echo "error happened!"
}
trap on_error ERR
echo "OK so far"
false
echo "this line should not execute"
The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436
not an answer but you might fix this counter intuitive behaviur by defining this helper function:
fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }
then call your functions via fixerrexit.
example:
f1()
{
mv unimportant-0.txt important.txt
rm unimportant-*.txt
}
set -e
if fixerrexit f1
then
echo here is the important file: important.txt
echo unimportant files are deleted
fi
if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.
the only downside is you can not set variables since it runs f1 inside a subshell.

shell: race condition in assigning global variable

I am working on a script that goes:
#script_1.sh
function a() {
export var1='...some calculations on $#...';
}
function b() {
a "$#"
local loc_b=$var1
}
function c() {
a "$#"
local loc_c=$var1
}
#script_2.sh
source script_1.sh
b val1 &
c val2 &
b val2 &
function a has getopt command and parses parameters for b and c. This has been simplified to a great extent.
When I call function b, function c and function b again at the end as background processes, I worry that I might have a race condition on assigning the exported var1 variable to the local variable.
If I can guarantee that calls to function b or function c will be as background processes, do I have to worry about the race condition here?
If there is no race, can I get away by not declaring the local variables for each function?
Each background process gets its own copy of the environment. Child process can't change the environment of its parent process. So, there's no race condition: if you change a global variable before running a background process, the process will get the new value copied. If you run a background process before changing the value, the process will see the old value.
#!/bin/bash
global=1
inc () {
((++global))
echo inc: $global
}
dec () {
((--global))
echo dec: $global
}
inc & dec &
# 2 0
global=10
inc & dec & inc & dec &
# 11 9 11 9

Why is bash errexit not behaving as expected in function calls?

In the bash man page, it states:
Exit immediately if a pipeline (which may consist of a single simple command),
a subshell command enclosed in parentheses, or one of the commands executed as
part of a command list enclosed by braces...
So I assumed that a function should be considered a command list enclosed by braces. However, if you apply a conditional to the function call, errexit no longer persists inside the function body and it executes the entire command list before returning. Even if you explicitly create a subshell inside the function with errexit enabled for that subshell, all commands in the command list are executed. Here is a simple example that demonstrates the issue:
function a() { b ; c ; d ; e ; }
function ap() { { b ; c ; d ; e ; } ; }
function as() { ( set -e ; b ; c ; d ; e ) ; }
function b() { false ; }
function c() { false ; }
function d() { false ; }
function e() { false ; }
( set -Eex ; a )
+ a
+ b
+ false
( set -Eex ; ap )
+ ap
+ b
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
Now if I apply a conditional to each of them...
( set -Eex ; a || false )
+ a
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; ap || false )
+ ap
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
( set -Eex ; as )
+ as
+ set -e
+ b
+ false
+ c
+ false
+ d
+ false
+ e
+ false
+ false
You started to quote the manual but then you cut the bit that explained this behaviour, which was in the very next sentence:
-e Exit immediately if a pipeline, which may consist of a single simple command, a subshell command enclosed in parentheses, or one of the commands executed as part of a command list enclosed by braces returns a non-zero status. The shell does not exit if the command that fails is part of the command list immediately following a while or until keyword, part of the test in an if statement, part of any command executed in a && or || list except the command following the final && or ||, any command in a pipeline but the last, or if the command’s return status is being inverted with !.
bug-bash mailing list has an explanation by Eric Blake more explicit about functions:
Short answer: historical compatibility.
...
Indeed, the correct behavior mandated by POSIX (namely, that 'set -e' is
completely ignored for the duration of the entire body of f(), because f
was invoked in a context that ignores 'set -e') is not intuitive. But
it is standardized, so we have to live with it.
And some words about whether set -e can be exploited to achieve the wanted behavior:
Because once you are in a context that ignores 'set -e', the historical
behavior is that there is no further way to turn it back on, for that
entire body of code in the ignored context. That's how it was done 30
years ago, before shell functions were really thought about, and we are
stuck with that poor design decision.
Not an answer to the original question, but a work-around for the underlying problem: set up a trap on errors:
function on_error() {
echo "error happened!"
}
trap on_error ERR
echo "OK so far"
false
echo "this line should not execute"
The reason for the behavior itself is properly explained in other answers (basically it's expected bash behavior as per the manual and POSIX): https://stackoverflow.com/a/19789651/1091436
not an answer but you might fix this counter intuitive behaviur by defining this helper function:
fixerrexit() { ( eval "expr '$-' : '.*e' >/dev/null && set -e; $*"; ); }
then call your functions via fixerrexit.
example:
f1()
{
mv unimportant-0.txt important.txt
rm unimportant-*.txt
}
set -e
if fixerrexit f1
then
echo here is the important file: important.txt
echo unimportant files are deleted
fi
if outer context has errexit on, then fixerrexit turns on errexit inside f1() as well, so you dont need to worry about commands being executed after a failure occurs.
the only downside is you can not set variables since it runs f1 inside a subshell.

Difficulties understanding the mechanism of retrieving parameter values passed to a script in a function

The script passes two parameter values to another instance of the script. So the built-in parameter variable, 0, contains the number of passed parameters. 1 is in the below example "C:/Windows" and 2 is "/switchtest"
It is possible to assign the parameter values to strParam1 and strParam2 with the traditional method outside the function (with the single equal sign). However, inside a function, the assignments fail.
If they are assigned in a loop with the := sign, it seems to work.
Why is it? Can anybody explain this behavior?
strParam1 = %1%
strParam2 = %2%
msgbox, 64, Outside the Function, number of parameters:%0%`npath: %strParam1%`nswitch: %strParam2%
test_params()
strPath := "C:/Windows"
strSwitch := "/switchtest"
RunWait "%A_AhkPath%" "%A_ScriptFullPath%" "%strPath%" "%strSwitch%"
test_params() {
global 0
; this works
; loop %0%
; strParam%A_Index% := %A_Index%
; this causes an error: "This dynamic variable is blank. If this variable was not intended to be dynamic, remove the % symbols from it."
; strParam1 := %1%
; strParam2 := %2%
; this passes empty values; however, this method works outside the function.
strParam1 = %1%
strParam2 = %2%
msgbox, 64, Inside the Function, number of parameters:%0%`npath: %strParam1%`nswitch: %strParam2%
if strParam2
exitapp
}
You had the right idea with global 0; that allows %0% to carry into the function from toplevel. You just need to declare global 1, 2 as well.
Even if you do this, you can't use := to assign them to variables, because := deals with expressions and there is no syntax to use them in expressions (normally a variable is referred to in an expression with the variable name alone, without %%; obviously 1 and 2 are interpreted as actual numbers instead of variables).
#echristopherson answered the question, but I'd like to propose a workaround. This assumes you're using AutoHotkey_L.
If you run the test script with the args "a b c", it gives you this.
3
1, a
2, b
3, c
The test:
argv := args()
test := argv.MaxIndex() "`n"
for index,param in argv
test .= index ", " param "`n"
MsgBox % test
And the function:
args() {
global
local _tmp, _out
_out := []
Loop %0% {
_tmp := %A_Index%
if _tmp
_out.Insert(_tmp)
}
return _out
}

Resources