Bash – How to print out the delimiter character and allow user to edit line while reading standard input

bashreadreadline

I'm trying to write a simple script that read from standard input, using ; character as delimiter to terminate the input line and that allows user to edit line.

Here is my test script:

#!/bin/bash

while true; do

  read -e -d ";" -t 180 -p "><> " srcCommand

  if [ $? != 0 ]; then
    echo "end;"
    echo ""
    exit 0
  fi
  case "$srcCommand" in
    startApp)
       echo "startApp command";;
    stopApp)
       echo "stopApp command";;
    end)
       echo ""
       exit 0
       ;;
    *)
       echo "unknown command";;
  esac
done

This works but doesn't print the delimiter ';' char:

# bash test.sh
><> startApp
startApp command
><> stopApp
stopApp command
><> end

If I remove -e option it prints out ; but user can't correct his mistake using backspace character and echoed strings are just right after the delimiter:

# bash test.sh
><> startApp;startApp command
><> stopApp;stopApp command
><> end;

How can I print out the delimiter character and allow user to edit line while reading standard input?

This is the expected behavior:

# bash test.sh
><> startApp;
startApp command
><> stopApp;
stopApp command
><> end;

Thanks

Best Answer

I'd use zsh where the line editor has many more capabilities and is a lot more customizable:

#! /bin/zsh -
insert-and-accept() {
  zle self-insert
  # RBUFFER= # to discard everything on the right
  zle accept-line
}
zle -N insert-and-accept
bindkey ";" insert-and-accept
bindkey "^M" self-insert
vared -p "><> " -c srcCommand

With bash-4.3 or above, you can do something similar with a hack like:

# bind ; to ^Z^C (^Z, ^C otherwide bypass the key binding when entered
# on the keyboard). Redirect stderr to /dev/null to discard the
# useless warning
bind '";":"\32\3"' 2> /dev/null

# new widget that inserts ";" at the end of the buffer.
# If we did bind '";":";\3"', readline would loop indefinitely
add_semicolon() {
  READLINE_LINE+=";"
  ((READLINE_POINT++))
}
# which we bind to ^Z
bind -x '"\32":add_semicolon' 2> /dev/null

# read until the ^C
read -e -d $'\3' -t 180 -p '><> ' srcCommand

Note that in that version, the ; is always inserted at the end of the input buffer, not at the current cursor position. Change the add_semicolon to:

add_semicolon() {
  READLINE_LINE="${READLINE_LINE:0:READLINE_POINT++};"
}

If you want it inserted at the cursor and everything on the right discarded. Or:

add_semicolon() {
  READLINE_LINE="${READLINE_LINE:0:READLINE_POINT};${READLINE_LINE:READLINE_POINT}"
  READLINE_POINT=${#READLINE_LINE}
}

if you want to insert it at the cursor but want to preserve what's on the right like in the zsh approach.

If you don't want the ; in $srcCommand, you can always strip it afterwards with srcCommand="${srcComman//;}" for instance, but you'd need to insert it in the widget for it to be displayed by zle/readline.