Reputation: 14364
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
Upvotes: 15
Views: 11050
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
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
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
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
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