nachocab
nachocab

Reputation: 14364

How to enable the up/down arrow keys to show previous inputs when using `read`?

I'm waiting for user input (using 'read') in an infinite loop and would like to have command history, that is being able to show previous inputs that were already entered, using the up and down arrow keys, instead of getting ^[[A and ^[[B. Is this possible?


Thanks to @l0b0 for your answer. It got me on the right direction. After playing with it for some time I've realized I also need the following two features, but I haven't managed to get them yet:

This is what I have so far (up_and_down):

#!/usr/bin/env bash
set -o nounset -o errexit -o pipefail

read_history() {
    local char
    local string
    local esc=$'\e'
    local up=$'\e[A'
    local down=$'\e[B'
    local clear_line=$'\r\e[K'


    local history=()
    local -i history_index=0

    # Read one character at a time
    while IFS="" read -p "Enter command:" -n1 -s char ; do
        if [[ "$char" == "$esc" ]]; then 
            # Get the rest of the escape sequence (3 characters total)
            while read -n2 -s rest ; do
                char+="$rest"
                break
            done
        fi

        if [[ "$char" == "$up" && $history_index > 0 ]] ; then
            history_index+=-1
            echo -ne $clear_line${history[$history_index]}
        elif [[ "$char" == "$down" && $history_index < $((${#history[@]} - 1)) ]] ; then
            history_index+=1
            echo -ne $clear_line${history[$history_index]}
        elif [[ -z "$char" ]]; then # user pressed ENTER
            echo
            history+=( "$string" )
            string=
            history_index=${#history[@]}
        else
            echo -n "$char"
            string+="$char"
        fi
    done
}
read_history

Upvotes: 15

Views: 11050

Answers (5)

l0b0
l0b0

Reputation: 58798

Interesting question - Here's the result so far:

#!/usr/bin/env bash
set -o errexit -o nounset -o pipefail
read_history() {
    local char=
    local string=
    local -a history=( )
    local -i histindex=0

    # Read one character at a time
    while IFS= read -r -n 1 -s char
    do
        if [ "$char" == $'\x1b' ] # \x1b is the start of an escape sequence
        then
            # Get the rest of the escape sequence (3 characters total)
            while IFS= read -r -n 2 -s rest
            do
                char+="$rest"
                break
            done
        fi

        if [ "$char" == $'\x1b[A' ]
        then
            # Up
            if [ $histindex -gt 0 ]
            then
                histindex+=-1
                echo -ne "\r\033[K${history[$histindex]}"
            fi
        elif [ "$char" == $'\x1b[B' ]
        then
            # Down
            if [ $histindex -lt $((${#history[@]} - 1)) ]
            then
                histindex+=1
                echo -ne "\r\033[K${history[$histindex]}"
            fi
        elif [ -z "$char" ]
        then
            # Newline
            echo
            history+=( "$string" )
            string=
            histindex=${#history[@]}
        else
            echo -n "$char"
            string+="$char"
        fi
    done
}
read_history

Upvotes: 4

micha
micha

Reputation: 196

Two solutions using the -e option to the read command combined with the builtin history command:

# version 1
while IFS="" read -r -e -d $'\n' -p 'input> ' line; do 
   echo "$line"
   history -s "$line"
done

# version 2
while IFS="" read -r -e -d $'\n' -p 'input> ' line; do 
   echo "$line"
   echo "$line" >> ~/.bash_history
   history -n
done

Upvotes: 18

mat
mat

Reputation: 21

Use the -e option to the read command (and make sure readline is configured to use the up/down arrow keys to loop through the command history).

help read | less -p '-e'

Upvotes: 2

Lynch
Lynch

Reputation: 9474

I use rlwrap to enable readline feature in program that does not support it. May be you could try this. rlwrap stand for readline wrapper. This command intercept your key up and key down and replace the prompt whit previous commands.

The sintax is simply rlwrap ./your-script.

Upvotes: 2

TC1
TC1

Reputation: 1

As far as I know, no. "up" and "down" are both just as good a symbol as any (for that matter, C-p and C-n do the same as "up" and "down" functionally in bash), and can be input as part of what you're trying to read.

That is, assuming you mean the bash builtin read. You could check the manpage for any options, but I can't think of any hack that would do what you want, at least not right now...

EDIT: Seemed interesting enough. @ work now & don't have bash or the time for it, but it could be done by setting -n 1 on read, then checking if you just read a "up" or "down" and using history to get the command needed. You would probably have to invent a local var to count the "up"s and "down"s, then get the relevant command from history with the appropriate offset, output it to the screen & if the next read returns with an empty string, use that found command.

As I said though, can't test this atm and no idea if it'll work.

Upvotes: 0

Related Questions