Search code examples
bashsignalschild-processsigintbash-trap

bash SIGINT trap fires once but never again


I need help understanding exactly how SIGINT is propagated between a shell script's main flow and a function call inside that script. For my script, I have a main menu which takes user input and calls a child function based on that input. The child function interacts with the user.

My goals are the following:

  • I need the child function to have access to function definitions and variables from the main script, so subshells aren't a great option
  • The child function should be in its own file because otherwise my main shell script would be too big and unwieldy
  • I want a ctrl+c inside the child function to return the user to the main menu
  • I want a ctrl+c inside the main menu to ideally print something, and if done again, exit. If it has to exit the first time that is acceptable

The behavior I see is that I can hit ctrl+c either inside the main menu or from inside the child function and it will work as expected the first time, but all subsequent ctrl+c signals are ignored.

I feel like my issue is very close to these:

Though in their case they are calling the child in a new process, and I'm calling a sourced function, which I don't think would have the same implications for fg/bg, would it?

For a minimal example, let's say I have a file called main.sh:

trap catchInt SIGINT
reallyQuit=0
function catchInt(){
    echo "caught sigint"
    if (( $reallyQuit > 0 ));then
        echo "really quitting."
        exit 1
    else
        let reallyQuit++
        echo "reallyquit is now $reallyQuit"
    fi
    menu
}
function menu(){
    read -ep $'Please choose a number and press enter.\n\t' input
    case $input in
        1)
            child
            menu
            ;;
        *)
            echo "Quitting"
            exit 0
            ;;
    esac
}
source child.sh
# I also source other scripts with utility functions and exported vars
menu

And if I have a file called child.sh in the same directory:

function child(){
        read -p "Please type something"
        # I also use utility functions and exported vars
}

Here is an example run of the above code in which I hit ctrl+c inside the menu, and then try again inside the child function:

bash main.sh
Please choose a number and press enter.
    caught sigint
reallyquit is now 1
Please choose a number and press enter.
    1
Please type something^C^C^C^C^C (I finally pressed enter)
Please choose a number and press enter.
(I tapped ctrl-c a few times here and finally pressed enter)
Quitting

Here is an example in which I first type 1, then ctrl-c:

bash main.sh
Please choose a number and press enter.
    1
Please type something^Ccaught sigint
reallyquit is now 1
Please choose a number and press enter.
(I tapped ctrl-c a few times here and finally pressed enter)
Quitting

How can I get the trap to respond every time I send the INT signal?


Solution

  • I do not know for sure, but I think it's because you're still in the "trap handler" when menu is called for the second time. Because a sigint is still being handled, a second sigint isn't processed. If you would remove that call and wrap menu with while true; do ...; done, it does work:

    #! /bin/bash
    
    reallyQuit=0
    function catchInt(){
        echo "caught sigint"
        if (( $reallyQuit > 0 ));then
           echo "really quitting."
            exit 1
        else
            let reallyQuit++
            echo "reallyquit is now $reallyQuit"
        fi
    }
    
    trap catchInt SIGINT
    
    function menu(){
        read -ep $'Please choose a number and press enter.\n\t' input
        case $input in
            1)
                child
                menu
                ;;
            *)
                echo "Quitting"
                exit 0
                ;;
        esac
    }
    function child(){
            read -p "Please type something"
            # I also use utility functions and exported vars
    }
    
    # I also source other scripts with utility functions and exported vars
    while true; do
            menu
    done
    exit 0
    

    EDIT:
    Why do you include the child.sh in main.sh? Usually one would create common functions and include it in the child script. This way you can share the functions in main.sh among child1.sh,... , childN.sh. If would add source main.sh into child.sh, the traps will work as well.