SIGINT to cancel read in bash script?
Asked Answered
F

3

8

I'm writting a bash wrapper to learn some scripting concepts. The idea is to write a script in bash and set it as a user's shell at login.

I made a while loop that reads and evals user's input, and then noticed that, whenever user typed CTRL + C, the script aborted so the user session ends.

To avoid this, I trapped SIGINT, doing nothing in the trap.

Now, the problem is that when you type CTRL + C at half of a command, it doesn't get cancelled as one would do on bash - it just ignores CTRL + C.

So, if I type ping stockoverf^Cping stackoverflow.com, I get ping stockoverfping stackoverflow.com instead of the ping stackoverflow.com that I wanted.

Is there any way to do that?

#!/bin/bash

# let's trap SIGINT (CTRL + C)
trap "" SIGINT

while true
do
    read -e -p "$USER - SHIELD: `pwd`> " command
    history -s $command
    eval $command
done
Free answered 24/10, 2012 at 21:8 Comment(0)
H
2

You could use a tool like xdotool to send Ctrl-A (begin-of-line) Ctrl-K (delete-to-end-of-line) Return (to cleanup the line)

#!/bin/bash
trap "xdotool key Ctrl+A Ctrl+k Return" SIGINT;
unset command
while [ "$command" != "quit" ] ;do
    eval $command
    read -e -p "$USER - SHIELD: `pwd`> " command
  done
trap SIGINT

The please have a look a bash's manual page, searching for ``debug'' keyword...

man -Pless\ +/debug bash
Hughett answered 24/10, 2012 at 22:34 Comment(3)
xdotool would do the trick, as a 100% hack. You get a ^C^A^K and an empty line that wouldn't show in pure bash (just ^C), but it's better than nothing. Anyway, it would be awesome to not depend on xdotool, and to avoid those extra markers - say, make it just like bash works. Anyway, I didn't get the rtfm invitation... What should debug show me?Free
Yes, this is a huge hack. But in state, this do the job. The right way to follow the original idea need to access file descriptors via ioctl.Hughett
@Free About what debug could do and how it could be used. have a look at profiling bash in nanosecondsHughett
B
6

I know this is old as all heck, but I was struggling to do something like this and came up with this solution. Hopefully it helps someone else out!

#/usr/bin/env bash
# Works ok when it is invoked as a bash script, but not when sourced!
function reset_cursor(){
    echo
}
trap reset_cursor INT
while true; do
    command=$( if read -e -p "> " line ; then echo "$line"; else echo "quit"; fi )
    if [[ "$command" == "quit" ]] ; then
        exit
    else
        history -s $command
        eval "$command"
    fi
done
trap SIGINT

By throwing the read into a subshell, you ensure that it will get killed with a sigint signal. If you trap that sigint as it percolates up to the parent, you can ignore it there and move onto the next while loop. You don't have to have reset_cursor as its own function but I find it nice in case you want to do more complicated stuff.

I had to add the if statement in the subshell because otherwise it would ignore ctrl+d - but we want it to be able 'log us out' without forcing a user to type exit or quit manually.

Bicycle answered 2/9, 2020 at 21:28 Comment(1)
This works nicely. I have implemented it as follows to obtain user input as a list of words: trap echo INT; while true; do mapfile -t -d '' input < <(IFS=' ' read -r -e -p '> ' -a input; printf '%s\0' "${input[@]}"); [[ ${input[0]} == quit ]] && break; history -s "${input[*]}"; declare -p input; done; trap SIGINTFallible
H
2

You could use a tool like xdotool to send Ctrl-A (begin-of-line) Ctrl-K (delete-to-end-of-line) Return (to cleanup the line)

#!/bin/bash
trap "xdotool key Ctrl+A Ctrl+k Return" SIGINT;
unset command
while [ "$command" != "quit" ] ;do
    eval $command
    read -e -p "$USER - SHIELD: `pwd`> " command
  done
trap SIGINT

The please have a look a bash's manual page, searching for ``debug'' keyword...

man -Pless\ +/debug bash
Hughett answered 24/10, 2012 at 22:34 Comment(3)
xdotool would do the trick, as a 100% hack. You get a ^C^A^K and an empty line that wouldn't show in pure bash (just ^C), but it's better than nothing. Anyway, it would be awesome to not depend on xdotool, and to avoid those extra markers - say, make it just like bash works. Anyway, I didn't get the rtfm invitation... What should debug show me?Free
Yes, this is a huge hack. But in state, this do the job. The right way to follow the original idea need to access file descriptors via ioctl.Hughett
@Free About what debug could do and how it could be used. have a look at profiling bash in nanosecondsHughett
O
0

This builds on CGanote's answer. It seems to work fine when sourced. It doesn't change anything substantial about the read which is the subject of this question, but it puts the whole thing in a function and also puts the eval in a subshell so that SIGINT can be used to interrupt a command. I'm posting it in case it is useful to other users who are interested in simulating a Read-Eval-Print-Loop (REPL) in Bash.

# browser.bash

# Bash version of R's "browser()" debugging tool

# Enters a simulated Bash REPL within a script. Unlike with just
# invoking a subshell with 'bash', in a call to 'browser',
# functions and unexported variables are also available for
# inspection.

# Although there is no way in Bash to evaluate an expression in
# the caller's context, it seems that this is not a problem,
# because function-local variables seem to be implemented using
# dynamic scoping and are available to any function lower in the
# call tree.

# Some features of actual Bash interactive-shell line editing are
# not available here, such as line continuation for multi-line
# commands.

browser () {
  while true; do
    trap echo INT;
    # read a line, turning ^D into exit
    command=$(if read -e -p "browser$ " line; then echo "$line"; else echo "exit"; fi)
    trap INT;
    if [[ "$command" = exit || "$command" = "exit "* ]] ; then
      # don't exit shell, but return exit status
      return ${command#exit}
    else
      history -s $command
      (trap echo INT; eval "$command")
    fi
  done
}
Oligosaccharide answered 20/11, 2023 at 22:15 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.