How can I create a generic shell command to exit o

2019-08-08 19:42发布

问题:

I am trying to implement a single command that I assume would be a wrapper for the normal 'exit' and 'return' shell builtins that come with Bash (Bourne, et al), a command that is not plagued by the incompatibility issues of these in that if I use 'exit 1' to end a script with error level 1, if I source that script it will cause the shell I am in to terminate.

Likewise, if I use return, it has the problems that: a) It will only return to the calling function, not end the whole script being run without additional logic. b) If I use return in the main script and execute it, it will error rather than terminating with a level.

The best solution I have come up with is this:

All shell scripts I write start with this preamble anyway to get some stable base variables:

# Core Functions and Alias definitions Used For Script Setup
getScriptFile () { basename "${BASH_SOURCE[0]}" ; } # This filename as executed
getScriptPath () { local f="${BASH_SOURCE[0]}" ; local p ; while test -L "${f}"
    do p="$( cd -P "$( dirname "${f}" )" && pwd )" ; f="$( readlink "${f}" )" &&
    f="${p}/${f}" ; done ; p="$( cd -P "$( dirname "${f}" )" && pwd )" ;
    printf "%s\n" "${p}" ; }
shopt -s expand_aliases
SCRIPTpath="$( getScriptPath ; )"
SCRIPTfile="$( getScriptFile ; )"

So, now I add this to it:

testEq () { [ "${1}" = "${2}" ] ; } # True if there is equality
testMatch () { [[ ${1} =~ ${2} ]] ; } # True if ARG1 MATCHES the Extended RegEx in ARG2
testInt () { [ "${1}" -eq "${1}" ] 2>/dev/null ; }  # I know this is okay with bash, to test with bourne
testIntLt () { testInt "${1}" && testInt "${2}" && [ "${1}" -lt "${2}" ] ; }
testSourced () { local c="0" ; local m="${#BASH_SOURCE[@]}" ;
    until testEq "$( basename "${BASH_SOURCE[${c}]}" )" "${SCRIPTfile}" &&
    testMatch "${FUNCNAME[${c}]}" "source|main" &&
    testIntLt "${c}" "${m}" ; do ((c++)) ; done ; 
    if testEq "source" "${FUNCNAME[${c}]}" ; then return 0 ; 
    else return 1 ; fi ; } # True if calling script is sourced into environment
getValidTerminationCommand () { testSourced && printf "%s" "return" || printf "%s" "exit" ; }
alias finish='eval $( getValidTerminationCommand ; )'

...and when I want to finish a script abruptly, I use my new finish command, i.e.

$ finish 5  <-- to finish with an error level of 5 

Now, regardless of if the script is sourced or executed, I get an exit code of 5. No errors for the wrong type of "exit" or "return" .

The reason I have the complication on the getScriptPath command is to deal with script files that might live on a filesystem path that is under symlinks.

The reason for the complication on testSourced is that the test needs to work even if the script that is testing if it is sourced is being done so by a calling script, i.e. it needs to make sure that it is testing the fact that that itself is sourced, not say a calling script.

Now, I know that the normal way of doing this is as simple as follows:

$ return 5 2>/dev/null || exit 5  # If I want to end a script with error level 5

Problem with that is that I cannot figure out how to wrap it to parameterize it so that I can use it as a 'finish' or 'terminate' command that will terminate the script with this error level as if I alias it, I can do this without a code for the return, or if it functionise it then it will just return to the calling function/script if it has one.

There has got to be a portable, simpler way than what I came up with on my implementation of 'finish' above!?! Surely this is a standard thing to do?

Anyone solved this before?? Am I missing something obvious?

Just to confirm, what I want to make is this: - single command to instantly terminate a script - same command if script is executed, sourced - same command if used in a function, the main script body, or a sub function - behave the same if the script is called or sourced either directly or by another script that may be executed or sourced, (or potentially n levels of execution leading up to the script that is being run). - use as standard commands to the Bash shell (Bourne if possible) so as portable as possible.

Anyone have some tool they use for this job? please let me know?

Thanks in anticipation!?! :)

Test script that I used to try this:

#!/bin/bash

# Core Functions and Alias definitions Used For Script Setup
getScriptFile () { basename "${BASH_SOURCE[0]}" ; } # This filename as executed
getScriptPath () { local f="${BASH_SOURCE[0]}" ; local p ; while test -L "${f}"
    do p="$( cd -P "$( dirname "${f}" )" && pwd )" ; f="$( readlink "${f}" )" &&
    f="${p}/${f}" ; done ; p="$( cd -P "$( dirname "${f}" )" && pwd )" ;
    printf "%s\n" "${p}" ; }
shopt -s expand_aliases
SCRIPTpath="$( getScriptPath ; )"
SCRIPTfile="$( getScriptFile ; )"
testEq () { [ "${1}" = "${2}" ] ; } # True if there is equality
testMatch () { [[ ${1} =~ ${2} ]] ; } # True if ARG1 MATCHES the Extended RegEx in ARG2
testInt () { [ "${1}" -eq "${1}" ] 2>/dev/null ; }  # I know this is okay with bash, to test with bourne
testIntLt () { testInt "${1}" && testInt "${2}" && [ "${1}" -lt "${2}" ] ; }
testSourced () { local c="0" ; local m="${#BASH_SOURCE[@]}" ; until testEq "$( basename "${BASH_SOURCE[${c}]}" )" "${SCRIPTfile}" && testMatch "${FUNCNAME[${c}]}" "source|main" && testIntLt "${c}" "${m}" ; do ((c++)) ; done ; if testEq "source" "${FUNCNAME[${c}]}" ; then return 0 ; else return 1 ; fi ; } # True if calling script is sourced into environment
getValidTerminationCommand () { testSourced && printf "%s" "return" || printf "%s" "exit" ; }
alias finish='eval $( getValidTerminationCommand ; )'

anotherTestFunc ()
{
    echo "anotherTestFunc ()"
    finish 10
    echo "anotherTestFunc () finish"
}

testFunc ()
{
    echo "testFunc ()"
    anotherTestFunc
    echo "testFunc () finish"
}

echo test start
testFunc
echo more outside text

回答1:

Unfortunately there is no universal notion of exiting from just the script that was sourced; for example take the following scenario:

  • script a.sh has methods a1() a2()
  • script b.sh has methods b1() b2()
  • now a1 is calling b1 which itself is calling a2

From within a2, what does exiting from script a.sh mean? just leaving a2()? Or unwinding the call stack through b1() and a1() as well?

Hence, even notwithstanding the alias issue (*), there is no simple command that you can run from within a method to exit just from the current script. return will return from only the method, and exit will kill the entire shell. Nothing in-between.

Now, bash makes the call stack available through the FUNCNAME array, so it should be possible to write a system where you would force all methods to return immediately (using a DEBUG trap handler) up to the point you want in the call stack. But I wouldn't bet my projects on such a feature :)

Perhaps a better solution is to introduce some "exit boundaries" by explicitly starting sub-shells at some point. In my example above, assuming a1 calls b1 from within a sub-shell, then an exit in a2 will automatically fall back to a1. However this won't help if you need to modify the shell state (e.g. variables) from within the methods you call.


(*) IMHO your work-around for the alias limitation is quite smart by the way



回答2:

combining bash-exit-script-from-inside-a-function with your test example, this returns 10 for both cases:

#!/bin/bash

returnBreak () { SCRIPTresult=${1:-0} ; break 10000 ; }
setExitCode () { local s=$? ; [[ -z $SCRIPTresult ]] && return $s ; return $SCRIPTresult ; }

anotherTestFunc ()
{
    echo "anotherTestFunc ()"
    returnBreak 10
    echo "anotherTestFunc () finish"
}

testFunc ()
{
    echo "testFunc ()"
    anotherTestFunc
    echo "testFunc () finish"
}

SCRIPTresult=""
for null in null;
do
    echo test start
    testFunc
    echo more outside text

done
setExitCode