14

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:

  • If I press up and add something to the previous command I would like to have the whole thing saved in the history, not just the addition. Example

    $ ./up_and_down
    Enter command: hello
    ENTER
    Enter command:
    Up
    Enter command: hello you
    ENTER
    Enter command:
    Up
    Enter command: you
    (instead of "hello you")

  • If I can't keep going up because I'm at the end of the history array, I don't want the cursor to move to the previous line, instead I want it to stay fixed.

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
nachocab
  • 13,328
  • 21
  • 91
  • 149

5 Answers5

18

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
micha
  • 196
  • 2
  • thanks so much! searched really long for this, and it works perfectly! I just needed the `-e -p` arguments though. What's the rest (setting IFS empty, raw input, setting \n delim) for? – v01pe Jun 26 '13 at 10:25
  • Good information. Version 1 should be used in almost every case. With version 2 the junk you enter ends up in bash history, where it could accidentally be recalled and inappropriately executed by bash. You could also be silently leaking private or secure information to the history. – Steve Zobell Sep 10 '13 at 13:29
  • A web search on how to do this and... wow, version 1 works a treat. This page just saved me ages. Thanks. – mattst Mar 23 '16 at 17:52
4

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
l0b0
  • 55,365
  • 30
  • 138
  • 223
  • That's what I was looking for. Could you explain your answer a little bit? Specifically: Are you reading the input as two separate characters? why? What's the `\x1b` escape code? Why do you prepend a `$`? Are you setting `IFS` or emptying it? Is there a difference between `local char=;` and `local char;`? Can you explain the options in `read -r -n 1 -s char`? How can I read about the options in `local` (like `-a`, `-i`)? – nachocab Jul 22 '11 at 15:02
  • 1
    Added some comments in the code. IFS= sets it to the empty string to avoid [word splitting](http://mywiki.wooledge.org/WordSplitting). `local char` just declares a variable local to the function, but it is undefined without the equals sign afterwards (at which point it is the empty string). As for `$'` and the `read`/`local` options, see the Bash man page. – l0b0 Jul 24 '11 at 12:33
  • Thanks. Got it. \x1b is the hexadecimal code for the escape key, arrow keys are encoded as the escape key (\x1b), the left bracket (\x5b, or '[') and one of four uppercase letters (ABCD), depending on the arrow. The construct $'...' is used to display escaped characters. I'm still not sure what the K and the \r\033 (carriage return and escape?) are doing – nachocab Jul 25 '11 at 11:12
  • 1
    `\r` means return to the start of the line (carriage return). The `K` escape code is to cut ("kill" in Emacs-sp33k) to the end of the line. – l0b0 Jul 25 '11 at 17:45
  • Micha's answer using read -e is much simpler and probably should be the official answer. – Steve Zobell Sep 10 '13 at 13:47
2

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.

Lynch
  • 9,174
  • 2
  • 23
  • 34
2

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'
mat
  • 21
  • 1
  • Thanks, this is useful, but I also wanted to be able to cycle through previous inputs, besides those from the command history – nachocab Jul 22 '11 at 12:41
0

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.

TC1
  • 1
  • 3
  • 20
  • 31
  • Interesting, I'll give it a try later and share my results. Actually, I don't really need command history, just being able to access previous inputs. Thanks for the idea – nachocab Jul 22 '11 at 11:08
  • Oh, then I guess I misunderstood you. Well, getting the previous input you had during the execution of the particular script wouldn't really be much different, except that you'd probably have to also keep a local array var or some temp file with previous user input & update it accordingly when accepting input. Obviously, if you need input also from previous executions of the script, you'll have no other option except for a file, and it could get complicated real quick if there's a limit to the commands stored (bash has it @ 500 by default). Retrieving would probably be a simple call to `tail`. – TC1 Jul 22 '11 at 11:30
  • Just one thing, how can I check that the input is an arrow key? I'm trying something like: `while true; do read -n 1 key; if [ $key == "\[A" ] ; then echo "up-arrow"; fi ; done` – nachocab Jul 22 '11 at 13:17