5

I want to incrementally read a line of input from the terminal, and allow the user some basic line editing functionality; INS, DEL, RIGHT, LEFT HOME, END, BACKSPACE

Each time the string is modified, I want to process it, to do an incremental regex search of a text file.

These edit-keys, and others, generate multiple input characters which
make it rather difficult to interpret the input, eg C-Left generates 6 characters.

Is there a simple way to achieve this char-by-char editable input?
I'm especialy interested in knowing how to do this in bash, because the rest of the processing will be bash.. Other suggestions are welcome too..

Here is how I started out, but it gets a bit out of hand with such a potential variety of control codes ..

#!/bin/bash
IFS=$'\n' 
while true ;do
  read -n 1 c
  ((${#c}==0)) && break # Exit the loop. Input length is 0  
                        # ie. The user has pressed Enter
  echo "xx=$(echo -n "$c"|xxd -p)="
  # 1b 5b 32 7e  "INS"
  # 1b 5b 33 7e  "DEL"
  # 1b 5b 43     "RIGHT"
  # 1b 5b 44     "LEFT"
  # 1b 5b 46     "END"
  # 1b 5b 48     "HOME"
  # 7f           "BACKSPACE"
done

4 Answers 4

3

If you read a character at a time with read -n, you'll have to implement a key sequence parser. You can build a slow-and-dirty solution that works on most terminals with this: consider that a function key escape sequence begins with an escape character and continues with any number of characters amongst 0-9;[]O followed by one final character not in this set.

A better way to read input is to use a proper input library. Bash uses one for its own purposes (readline). You get a limited interface to it by declaring your own keybindings with the bind built-in; specifically bind -x to run a shell command on a key press. Because of this limited interface, implementing what you want is likely to be possible but difficult.

Zsh has its own input library, zle. Its interface is a lot richer than bash's. With zle, you can define arbitrary keymaps, and you get more access to the internals of zle from shell code. Use zle to assign shell functions to zle user-defined commands (called widgets), bindkey to create and populate your own keymap, and finally vared to read a line of input using the keymap of your choice.

2
  • I'm still plugging away at this one.. It is an "interesting" learning curve, to say the least. I've managed to get the read -n method working well enough, but I've just now realized that tmux and screen use different keybindings for HOME and END than do konsole and gnome-terminal without the multiplexers... Maybe this is an emacs vs vi keybinding issue... I'm getting there, and the 'readline` method is starting to make some sense to me now, but I'd better stick with read -n 1 for now. It's quite fast enough (but slow to write :). When I finished, I'll look further at readline.
    – Peter.O
    Commented Aug 9, 2011 at 19:33
  • @fred Different terminals using different escape sequences (sometimes the same terminal will use a different sequence in full-screen mode and line mode) is a fact of life. It's one of the reasons to use a library where someone has already done the work for you. Fortunately there is no overlap amongst common terminals (i.e. if a sequence corresponds to a certain key on a certain terminal, it doesn't correspond to another key on a different terminal). Commented Aug 9, 2011 at 19:43
2

You may put terminal into raw mode after the first read if there is an esc character, and then use a second read to read and parse the remaining bytes if any (cf. BASH escape character trauma and see also arrows.txt ).

#!/bin/bash

# tested on Mac OS X 10.6.8

IFS=$'\n'
old_tty_settings="$(stty -g)"
exec 0</dev/tty

tput smir  # enable insert mode

while IFS="" read -r -s -n1 key; do    # first read (reads only a single byte)

#od -c <<<"$key"
#continue

((${#key}==0)) && break

case "$key" in
  $'\001')  printf '\r'                          #  ctrl-a
            continue;;   
  $'\177')  tput rmir 
            printf "\010\040\010\033[P"          # backspace
            tput smir
            continue;;                          
  $'\025')  printf "\033[1K"                     # tput el1 does not work on Mac OS X 10.6.8 
            continue;;                           # ctrl-u  (clear to start of line)
  $'\v')    tput el
            continue;;                           # ctrl-k  (clear to end of line)
esac

# if the first char is esc (i.e. \e or \033 respectively)
if [[ "$key" == $'\033' ]]; then    

  stty cbreak -echo min 0 time 0   # set raw terminal 

  IFS="" read -r bytes     # second read (reads remaining bytes)

  if [[ ${#bytes} -gt 0 ]]; then

     stty "$old_tty_settings"

     case "${key}${bytes}" in 
       $'\033[3~')   tput dch1         # delete one char
                     continue;; 
     esac

     printf "${key}${bytes}"

  else
     stty "$old_tty_settings"
  fi

 else

  #stty "$old_tty_settings"
  printf '%s' "$key"

fi

done

echo

exit 0
1

Have a look at the command line tool selector.

# usage examples
selector -v -x @ <(find . -maxdepth 2 -type d | awk '{print $0"@cd "$0}')
selector -v -x @ <(grep -E -o 'http[^ ]+' fileWithURLS)
0

Whitout using any script by using bash builtins only:

read -e INPUT  # single line

INPUT=$(</dev/stdin)  # multiple lines

(source)

Alternatively, install the ledit package, then you can use this:

INPUT=$(ledit)

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .