Skip to main content
Reworked after a discussion with mikeserv. Now pointing out that local is executed after fname.
Source Link
hermannk
  • 319
  • 1
  • 4

Executing the exit in a subshell is one pitfall. Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script:

    #!/bin/bash
    function calc { echo 42; exit 1; }
    echo $(calc)

The script prints 42.

Here, exits from the subshell with return code exit1 does not abort, and continues with the script because. Even replacing the call by echo $(calcCALC) || exit 1 does not help because the return code of echo is executed in a subshell. Thank you, mikeserv, for pointing out that0 regardless of the return code of calc. For details seeAnd exit wrapped by local.calc is executed prior to echo.

Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script. I stumbled over the problem when I wrote a function to verify an input valuesvalue. Example:

I want to create a file named "year month day.log", i.e., 20141211.log for today. The date is input by a user who may fail to provide a reasonable value. Therefore, in my function fname I check the return value of date to verify the validity of the user input input:

    #!/bin/bash

    doit ()
        {
        local FNAME=$(fname "$1") || exit 1
        touch "${FNAME}"
        }

    fname ()
        {
        date +"%Y%m%d.log" -d"$1" 2>/dev/null
        if [ "$?" != 0 ] ; then
            echo "check_date"fname reports \"Illegal Date\"" >&2
            exit 1
        fi
        }

    doit "$1"
check_datefname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

The line check_date…fname… says that the bad input data has been detected in the subshell. But the exit 1 at the end of the local … line is never triggered because the local directive always return 0. This is because local is executed after $(fname) and thus overwrites its return code. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

Though not being a bug I feel that the behaviour of bash is counterintuitive. I am aware of the sequence of execution, local should not mask a broken assignment, nevertheless.

My initial answer contained some inaccurancies. After a revealing and in-depth discussion with mikeserv (thank you for that) I went for fixing them.

Executing the exit in a subshell is one pitfall. Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script

    #!/bin/bash
    function calc { echo 42; exit 1; }
    echo $(calc)

The script prints 42.

Here, the exit does not abort the script because $(calc) is executed in a subshell. Thank you, mikeserv, for pointing out that. For details see exit wrapped by local.

I stumbled over the problem when I wrote a function to verify input values. Example:

I want to create a file named "year month day.log", i.e., 20141211.log for today. The date is input by a user who may fail to provide a reasonable value. Therefore, in my function fname I check the return value of date to verify the validity of the user input:

    #!/bin/bash

    doit ()
        {
        local FNAME=$(fname "$1") || exit 1
        touch "${FNAME}"
        }

    fname ()
        {
        date +"%Y%m%d.log" -d"$1" 2>/dev/null
        if [ "$?" != 0 ] ; then
            echo "check_date reports \"Illegal Date\"" >&2
            exit 1
        fi
        }

    doit "$1"
check_date reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

The line check_date… says that the bad input data has been detected in the subshell. But the exit 1 at the end of the local … line is never triggered because the local directive always return 0. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

Executing the exit in a subshell is one pitfall:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

The script prints 42, exits from the subshell with return code 1, and continues with the script. Even replacing the call by echo $(CALC) || exit 1 does not help because the return code of echo is 0 regardless of the return code of calc. And calc is executed prior to echo.

Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script. I stumbled over the problem when I wrote a function to verify an input value. Example:

I want to create a file named "year month day.log", i.e., 20141211.log for today. The date is input by a user who may fail to provide a reasonable value. Therefore, in my function fname I check the return value of date to verify the validity of the user input:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"
fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

The line fname… says that the bad input data has been detected in the subshell. But the exit 1 at the end of the local … line is never triggered because the local directive always return 0. This is because local is executed after $(fname) and thus overwrites its return code. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

Though not being a bug I feel that the behaviour of bash is counterintuitive. I am aware of the sequence of execution, local should not mask a broken assignment, nevertheless.

My initial answer contained some inaccurancies. After a revealing and in-depth discussion with mikeserv (thank you for that) I went for fixing them.

Fixed example. My first example was broken.
Source Link
hermannk
  • 319
  • 1
  • 4

The fix is to split the line like

local FNAME
FNAME=$(fname "$1") || exit 1

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

The fix is to split the line like

local FNAME
FNAME=$(fname "$1") || exit 1

The strange behavior conforms to the documentation of local within the man page of bash: "The return status is 0 unless local is used outside a function, an invalid name is supplied, or name is a readonly variable."

Fixed example. My first example was broken.
Source Link
hermannk
  • 319
  • 1
  • 4

Executing the exit in a subshell is one pitfall. Even more puzzling is thwarting the effect of exit by wrapping it into a (possibly) implicit local builtin like in the following script

Here, the exit does not abort the script because $(calc) is an implicit local argument of echoexecuted in a subshell. Thank you, mikeserv, for pointing out that. For details see exit wrapped by local.

    #!/bin/bash

    doit ()
        {
        local FNAME=$(fname "$1") || exit 1
        touch "${FNAME}"
        }

    fname ()
        {
        date +"%Y%m%d.log" -d"$1" 2>/dev/null
        if [ "$?" != 0 ] ; then
            echo "check_date reports \"Illegal Date\"" >&2
            exit 1
        fi
        }

    doit "$1"

The line check_date… says that the bad input data has been detected in the subshell. But the following exit 1 at the end of the local … line is hidden bynever triggered because the local directive always return 0. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

But even real programmers can drop a brick when they write the terse variant of fnameTo make it clear:

doit ()
{
touch "$(fname "$1")"
}

Now, "$(fname "$1")" is an implicit local shell variable. And again, Without the now invisible local thwarts, the exit 1script aborts as expected when an invalid date is entered.

Executing the exit in a subshell is one pitfall. Even more puzzling is thwarting the effect of exit by wrapping it into a (possibly) implicit local builtin like in the following script

Here, the exit does not abort the script because $(calc) is an implicit local argument of echo. For details see exit wrapped by local.

    #!/bin/bash

    doit ()
        {
        local FNAME=$(fname "$1")
        touch "${FNAME}"
        }

    fname ()
        {
        date +"%Y%m%d.log" -d"$1" 2>/dev/null
        if [ "$?" != 0 ] ; then
            echo "check_date reports \"Illegal Date\"" >&2
            exit 1
        fi
        }

    doit "$1"

The line check_date… says that the bad input data has been detected. But the following exit 1 is hidden by the local directive. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

But even real programmers can drop a brick when they write the terse variant of fname:

doit ()
{
touch "$(fname "$1")"
}

Now, "$(fname "$1")" is an implicit local shell variable. And again, the now invisible local thwarts the exit 1.

Executing the exit in a subshell is one pitfall. Even more puzzling is thwarting the effect of exit by wrapping it into local builtin like in the following script

Here, the exit does not abort the script because $(calc) is executed in a subshell. Thank you, mikeserv, for pointing out that. For details see exit wrapped by local.

    #!/bin/bash

    doit ()
        {
        local FNAME=$(fname "$1") || exit 1
        touch "${FNAME}"
        }

    fname ()
        {
        date +"%Y%m%d.log" -d"$1" 2>/dev/null
        if [ "$?" != 0 ] ; then
            echo "check_date reports \"Illegal Date\"" >&2
            exit 1
        fi
        }

    doit "$1"

The line check_date… says that the bad input data has been detected in the subshell. But the exit 1 at the end of the local … line is never triggered because the local directive always return 0. And because of that, the script continues and invokes touch with an empty parameter. This example is simple but the behavior of bash can be quite confusing in a real application. I know, real programmers don't use locals.☺

To make it clear: Without the local, the script aborts as expected when an invalid date is entered.

deleted 5 characters in body
Source Link
hermannk
  • 319
  • 1
  • 4
Loading
typo
Source Link
mikeserv
  • 59.4k
  • 10
  • 123
  • 244
Loading
Provide an example to show the relevance.
Source Link
hermannk
  • 319
  • 1
  • 4
Loading
Provide an example to show the relevance.
Source Link
hermannk
  • 319
  • 1
  • 4
Loading
Source Link
hermannk
  • 319
  • 1
  • 4
Loading