Bash – Using shell’s read command with live editing functionality (readline like)

bashposixreadlineshell

Is there a standard (POSIX) way of asking the user some data from within a shell script, with read for example, while allowing live edition of the text being typed (what readline does)?

I know bash has read -e varname that allows for the person launching the script to use keyboard arrows for exemple, to edit or correct what has just been typed without deleting the last entered characters with backspace.

However, read -e is bash specific. And still, it is pretty cumbersome to delete all that has been wrote if you realise you made a mistake at the beginning of your long sentence…

Best Answer

The terminal driver does have line editing capabilities on most systems. You'll notice that you can use Backspace, Ctrl-U, sometimes Ctrl-W.

readline is a GNU library maintained alongside bash. There's nothing POSIX about it. POSIX defines an optional line editor (with vi key binding) for sh, but no provision to use it outside of sh.

The ksh93 shell uses that vi-style line editor (also supports emacs or gmacs-style) for its read builtin when both stdin and stderr are a terminal and the corresponding option has been set: set -o emacs; IFS= read -r var for instance for read to use an emacs-style line-editor.

POSIX does specify the vi editor though (optional), so you could invoke vi to edit the content of a temporary file.

The zsh equivalent of bash's read -e is vared (a lot more advanced as it's using zsh's zle (zsh line editor)).

In other shells, you can use some wrappers around readline or other line-editing libraries (like rlwrap), or you can invoke bash -c 'read -e...' or zsh -c 'vared...'.

What you could also do is give the opportunity to the user to launch an editor.

Like:

if ! IFS= read -r var; then
  if [ -n "$var" ]; then
    tmp=$(create_tempfile) # create_tempfile left as an exercise
    printf '%s\n' "$var" > "$tmp"
    "${VISUAL:-${EDITOR:-vi}}" -- "$tmp"
    var=$(cat < "$tmp")
    rm -f -- "$tmp"
  else
    exit 1 # real EOF?
  fi
fi

Then the user can press Ctrl-D twice to launch an editor on what he has already entered.

Otherwise, I once wrote that function that should work on most terminals on most Unices that implements a simple line editor.

LE() {
# shell Line Editor. Extremely slow and stupid code. However it
# should work on ansi/vt100/linux derived terminals on POSIX
# systems.
# Understands some emacs key bindings: CTRL-(A,B,D,E,F,H,K,L)
# plus the CTRL-W and CTRL-U normal killword and kill.
# no Meta-X key, but handling of <Left>, <Right>, <Home>, <End>
# <Suppr>.
# 
# Args:
#  [1]: prompt (\x sequences recognized, defaults to "")
#  [2]: max input length (unlimited if < 0, (default))
#  [3]: fill character when erasing (defaults to space)
#  [4]: initial value.
# Returns:
#  0: OK
#  1: od(d) error or CTRL-C hit

  LE_prompt=$1
  LE_max=${2--1}
  LE_fill=${3-" "}

  LE_backward() {
    LE_s=$1
    while [ "x$LE_s" != x ]; do
      printf '\b%s' "$2"
      LE_s=${LE_s%?}
    done
  }

  LE_fill() {
    LE_s=$1
    while [ "x$LE_s" != x ]; do
      printf %s "$LE_fill"
      LE_s=${LE_s%?}
    done
  }

  LE_restore='stty "$LE_tty"
              LC_COLLATE='${LC_COLLATE-"; unset LC_COLLATE"}
  LE_ret=1 LE_tty=$(stty -g) LE_px=$4 LE_sx= LC_COLLATE=C

  stty -icanon -echo -isig min 100 time 1 -istrip
  printf '%b%s' "$LE_prompt" "$LE_px"

  while set -- $(dd bs=100 count=1 2> /dev/null | od -vAn -to1); do
    while [ "$#" -gt 0 ]; do
      LE_k=$1
      shift
      if [ "$LE_k" = 033 ]; then
        case "$1$2$3" in
          133103*|117103*) shift 2; LE_k=006;;
          133104*|117104*) shift 2; LE_k=002;;
          133110*|117110*) shift 2; LE_k=001;;
          133120*|117120*) shift 2; LE_k=004;;
          133106*|117106*) shift 2; LE_k=005;;
          133061176) shift 3; LE_k=001;;
          133064176) shift 3; LE_k=005;;
          133063176) shift 3; LE_k=004;;
          133*|117*)
            shift
            while [ "0$1" -ge 060 ] && [ "0$1" -le 071 ] ||
                  [ "0$1" -eq 073 ]; do
              shift
            done;;
        esac
      fi

      case $LE_k in
        001) # ^A beginning of line
          LE_backward "$LE_px"
          LE_sx=$LE_px$LE_sx
          LE_px=;;
        002) # ^B backward
          if [ "x$LE_px" = x ]; then
            printf '\a'
          else
            printf '\b'
            LE_tmp=${LE_px%?}
            LE_sx=${LE_px#"$LE_tmp"}$LE_sx
            LE_px=$LE_tmp
          fi;;
        003) # CTRL-C
          break 2;;
        004) # ^D del char
          if [ "x$LE_sx" = x ]; then
            printf '\a'
          else
            LE_sx=${LE_sx#?}
            printf '%s\b' "$LE_sx$LE_fill"
            LE_backward "$LE_sx"
          fi;;
        012|015) # NL or CR
          LE_ret=0
          break 2;;
        005) # ^E end of line
          printf %s "$LE_sx"
          LE_px=$LE_px$LE_sx
          LE_sx=;;
        006) # ^F forward
          if [ "x$LE_sx" = x ]; then
            printf '\a'
          else
            LE_tmp=${LE_sx#?}
            LE_px=$LE_px${LE_sx%"$LE_tmp"}
            printf %s "${LE_sx%"$LE_tmp"}"
            LE_sx=$LE_tmp
          fi;;
        010|177) # backspace or del
          if [ "x$LE_px" = x ]; then
            printf '\a'
          else
            printf '\b%s\b' "$LE_sx$LE_fill"
            LE_backward "$LE_sx"
            LE_px=${LE_px%?}
          fi;;
        013) # ^K kill to end of line
          LE_fill "$LE_sx"
          LE_backward "$LE_sx"
          LE_sx=;;
        014) # ^L redraw
          printf '\r%b%s' "$LE_prompt" "$LE_px$LE_sx"
          LE_backward "$LE_sx";;
        025) # ^U kill line
          LE_backward "$LE_px"
          LE_fill "$LE_px$LE_sx"
          LE_backward "$LE_px$LE_sx"
          LE_px=
          LE_sx=;;
        027) # ^W kill word
          if [ "x$LE_px" = x ]; then
            printf '\a'
          else
            LE_tmp=${LE_px% *}
            LE_backward "${LE_px#"$LE_tmp"}"
            LE_fill "${LE_px#"$LE_tmp"}"
            LE_backward "${LE_px#"$LE_tmp"}"
            LE_px=$LE_tmp
          fi;;
        [02][4-7]?|[13]??) # 040 -> 177, 240 -> 377
                           # was assuming iso8859-x at the time
          if [ "$LE_max" -ge 0 ] && LE_tmp=$LE_px$LE_sx \
             && [ "${#LE_tmp}" -eq "$LE_max" ]; then
            printf '\a'
          else
            LE_px=$LE_px$(printf '%b' "\0$LE_k")
            printf '%b%s' "\0$LE_k" "$LE_sx"
            LE_backward "$LE_sx"
          fi;;
        *)
          printf '\a';;
      esac
    done
  done
  eval "$LE_restore"
  REPLY=$LE_px$LE_sx
  echo
  return "$LE_ret"
}

To be used as:

LE 'Prompt: '

Or:

LE 'Prompt: [....]\b\b\b\b\b' 4 . DEF

if you want a maximum length and/or different filling character and/or an initial value.