shell script respond to keypress - bash

I have a shell script that essentially says something like
while true; do
read -r input
if ["$input" = "a"]; then
echo "hello world"
fi
done
That is all well, and good, but I just realized having to hit ENTER presents a serious problem in this situation. What I need is for the script to respond when a key is pressed, without having to hit enter.
Is there a way to achieve this functionality within a shell script?

read -rsn1
Expect only one letter (and don't wait for submitting) and be silent (don't write that letter back).

so the final working snippet is the following:
#!/bin/bash
while true; do
read -rsn1 input
if [ "$input" = "a" ]; then
echo "hello world"
fi
done

Another way of doing it, in a non blocking way(not sure if its what you want). You can use stty to set the min read time to 0.(bit dangerous if stty sane is not used after)
stty -icanon time 0 min 0
Then just run your loop like normal. No need for -r.
while true; do
read input
if ["$input" = "a"]; then
echo "hello world"
fi
done
IMPORTANT!
After you have finished with non blocking you must remember to set stty back to normal using
stty sane
If you dont you will not be able to see anything on the terminal and it will appear to hang.
You will probably want to inlcude a trap for ctrl-C as if the script is quit before you revert stty back to normal you will not be able to see anything you type and it will appear the terminal has frozen.
trap control_c SIGINT
control_c()
{
stty sane
}
P.S Also you may want to put a sleep statement in your script so you dont use up all your CPU as this will just continuously run as fast as it can.
sleep 0.1
P.S.S It appears that the hanging issue was only when i had used -echo as i used to so is probably not needed. Im going to leave it in the answer though as it is still good to reset stty to its default to avoid future problems.
You can use -echo if you dont want what you have typed to appear on screen.

You can use this getkey function:
getkey() {
old_tty_settings=$(stty -g) # Save old settings.
stty -icanon
Keypress=$(head -c1)
stty "$old_tty_settings" # Restore old settings.
}
It temporarily turns off "canonical mode" in the terminal settings
(stty -icanon) then returns the input of "head" (a shell built-in) with the -c1 option which is returning ONE byte of standard input. If you don't include the "stty -icanon" then the script echoes the letter of the key pressed and then waits for RETURN (not what we want). Both "head" and "stty" are shell built-in commands. It is important to save and restore the old terminal settings after the key-press is received.
Then getkey() can be used in combination with a "case / esac" statement for interactive one-key selection from a list of entries:
example:
case $Keypress in
[Rr]*) Command response for "r" key ;;
[Ww]*) Command response for "w" key ;;
[Qq]*) Quit or escape command ;;
esac
This getkey()/case-esac combination can be used to make many shell scripts interactive. I hope this helps.

How to read a single key press into variable c, and print it out. This prints out the key you pressed instantly, withOUT you having to press Enter first:
read -n1 c && printf "%s" "$c"
Or, with a little more "prettifying" in the output print:
read -n1 c && printf "\nYou Pressed: %s\n" "$c"
Example output of the latter command:
$ read -n1 c && printf "\nYou Pressed: %s\n" "$c"
M
You Pressed: M
To suppress your initial keypress from being echoed to the screen, add the -s option as well, which says from the read --help menu:
-s do not echo input coming from a terminal
Here is the final command:
read -sn1 c && printf "You Pressed: %s\n" "$c"
And a demo:
$ read -sn1 c && printf "You Pressed: %s\n" "$c"
You Pressed: p
You can also optionally separate the -sn1 argument into two arguments (-s -n1) for clarity.
References:
I learned about read -n1 from #pacholik here.
See also:
read_keypress.sh in my eRCaGuy_hello_world repo.
I use this bash cmd in C and C++ system calls to read keys here:
[my answer] Capture characters from standard input without waiting for enter to be pressed
[my answer] Read Key pressings in C ex. Arrow keys, Enter key

I have a way to do this in my project: https://sourceforge.net/p/playshell/code/ci/master/tree/source/keys.sh
It reads a single key everytime key_readonce is called. For special keys, a special parsing loop would run to also be able to parse them.
This is the crucial part of it:
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
KEY[0]=$K
if [[ $K == $'\e' ]]; then
if [[ BASH_VERSINFO -ge 4 ]]; then
T=(-t 0.05)
else
T=(-t 1)
fi
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
case "$K" in
\[)
KEY[1]=$K
local -i I=2
while
read -rn 1 -d '' "${T[#]}" "${S[#]}" "KEY[$I]" && \
[[ ${KEY[I]} != [[:upper:]~] ]]
do
(( ++I ))
done
;;
O)
KEY[1]=$K
read -rn 1 -d '' "${T[#]}" 'KEY[2]'
;;
[[:print:]]|$'\t'|$'\e')
KEY[1]=$K
;;
*)
__V1=$K
;;
esac
fi
fi
utils_implode KEY __V0

Related

How to "read" in bash without pressing enter

I've done a shell (bash) script that applies predefined rules to files dropped into the terminal.
It works quite well but because it uses 'read' it requires to press Enter once the files are dropped to the term window
This is part of the current code
while true ; do
echo "Drop file(s) here then press [ENTER]:"
echo "( x,q or exit,quit )"
read -p "> " read_file
while read dropped_file ;do
if [ -e ${dropped_file} ] ; then
...bunch of code here...
else
[[ "${dropped_file}" == *[xXqQ]* ]] && exit 1
fi
done <<< $(echo ${read_file} | tr " " "\n")
clear
done
I'd like to omit to press Enter each time I drop files and I was wondering if there is some wizardry to avoid to interact with the keyboard, except when I want to quit the script
Any help would be appreciated
Thanks in advance
EDIT
I've solved with this
while true ; do
read -sn1 read_file
while read splitted_filename; do
dropped_file="${dropped_file}${splitted_filename}"
done <<< $(echo ${read_file})
functionAction
[[ "${dropped_file}" == [xXqQ] ]] && exit 1
done
I think you can use the yes, printf, or the expect command and use the \n key, which should be able to perform what you are looking for.

Simulating command history and editing inside a looping bash script

I'd like to have a bash script that implements some of the functionality of the bash command line itself: namely, command history and vi-style command editing.
The script would loop forever (until crtl/d) and read input from the user in terminal, treating each line as a command. The commands are actually a set of shell scripts that I have already written which are designed to support a photo work flow. The same edit and recall functionality should be available in this interpreted environment.
Having bash command history and command editing functions in this script would be very desirable.
Was looking for a way to mimic command history within script as well, couldn't find much on it online, so built a simple one myself. Not exacly what you asked for, but might give you, or anyone else some references.
It really just is one big function that does nothing else than handle the prompt like behaviour and return the on screen string from pressing enter. It allows for browsing of appointed history file, while saving new input, to go back to. Auto indent or moving the marker is not implemented below. I think the script require bash version 4, with the arithmetic shells, but change to an older syntax and bash 3 should work. It's not fully tested yet.
Use it as:
./scriptname.sh /optional/path/to/history_file
The script
#!/bin/bash
# vim: ts=4:
function getInput() {
local hist_file="${1:-.script_hist}";
local result="";
local escape_char=$(printf "\u1b")
local tab=$(echo -e "\t");
local backspace=$(cat << eof
0000000 005177
0000002
eof
);
local curr_hist=0;
local curr_cmd="";
function browseHistory() {
! test -s "$hist_file" && return 1;
local max_hist="$(cat "$hist_file" | wc -l || echo 0)";
curr_hist=$((curr_hist + "$1"));
(( curr_hist > max_hist )) && curr_hist=$max_hist;
if (( curr_hist <= 0 )); then
curr_hist=0;
return 1;
fi
result="$(sed -n "$((max_hist - curr_hist + 1))p" < "$hist_file")";
return 0;
}
ifs=$IFS;
while true; do
# empty IFS, read one char
IFS= read -rsn1 input
if [[ $input == $escape_char ]]; then
# read two more chars, this is for non alphanumeric input
read -rsn2 input
fi
# check special case for backspace or tab first
# then move onto arrow keys or anything else in case
if [[ $(echo "$input" | od) = "$backspace" ]]; then
# delete last character of current on screen string
result=${result%?};
elif [ "$input" = "$tab" ]; then
# replace with function call for autofill or something
# it's unused but added in case it would be useful later on
continue;
else
case $input in
'[A')
! browseHistory '1' && result=$curr_cmd;
;;
'[B')
! browseHistory '-1' && result=$curr_cmd;
;;
'[D') continue ;; # left, does nothing right now
'[C') continue ;; # right, this is still left to do
*)
# matches enter and returns on screen string
[[ "$input" == "" ]] && break;
result+=$input
;;
esac
fi
# store current command, for going back after browsing history
(( curr_hist == 0 )) && curr_cmd="$result";
echo -en "\r\033[K";
echo -en "${result}"
done
IFS=$ifs;
test -n "$result" && echo "$result" >> "$hist_file";
return 0;
}
getInput $1

Wait for keypress in a while loop and stop script

I'd like to wait for keypress and exit when pressed the letter q.
The script isn't waiting for a key. How to correct it?
while read line
do
...
while :
do
read -n 1 key
if [[ $key = q ]]
then
break
fi
done
done < $1
read reads the input.
In your script, the input is changed to $1.
The first level while loop is reading a line from the file for which the name is stored into $1, and read -n 1 key reads and stores the first char of the next line from the same file.
Give a try to that :
while read line ; do
while : ; do
read -n 1 key <&1
if [[ $key = q ]] ; then
break
fi
done
done < $1
<&1 is the standard input.
The script isn't waiting for a key.
Because the command read is getting its input from the redirected file in:
done < $1 ### Should be "$1".
That file is consumed by both read commands (and anything else inside the loop that read stdin).
The correct solution for shell's read that have the option -u (and bash does), is to define the fd (file descriptor) to use in each read while the file is redirected to some fd number (greater than 2):
while read -u 3 line ; do
while : ; do
read -u 1 -n 1 key
if [[ $key = q ]] ; then
break
fi
done
echo "$line"
done 3< "$1"
That makes the first read get the input from fd 3 which comes from the file (done 3< "$1"), and the second read get the input from fd 1 (stdin).
For POSIX shells, read does not have the -u option, we need to perform some redirections to get the same general effect:
#!/bin/dash
while read line <&3; do
while : ; do
read key <&1
if [ "$key" = q ] ; then
break
fi
done
done 3< "$1"
Sadly, this also remove the -n 1 option from read and each key from the keyboard must be followed by pressing Enter.
To actually read one character we may use dd. And we also may set the actual terminal as /dev/tty (blocks any other redirection) and if we need to hide the text typed (or passwords) use stty -echo:
#!/bin/dash
while read line <&3; do
while : ; do
stty raw -echo
key=$(dd bs=1 count=1 </dev/tty 2> /dev/null)
stty -raw echo
if [ "$key" = q ] ; then
break
fi
done
echo "$line"
done 3< "$1"
Caveat: setting stty raw will prevent the effect of keys like CTRL-C (be careful).

How do I detect input from the user while a bash script is in execution? [duplicate]

I have a shell script that essentially says something like
while true; do
read -r input
if ["$input" = "a"]; then
echo "hello world"
fi
done
That is all well, and good, but I just realized having to hit ENTER presents a serious problem in this situation. What I need is for the script to respond when a key is pressed, without having to hit enter.
Is there a way to achieve this functionality within a shell script?
read -rsn1
Expect only one letter (and don't wait for submitting) and be silent (don't write that letter back).
so the final working snippet is the following:
#!/bin/bash
while true; do
read -rsn1 input
if [ "$input" = "a" ]; then
echo "hello world"
fi
done
Another way of doing it, in a non blocking way(not sure if its what you want). You can use stty to set the min read time to 0.(bit dangerous if stty sane is not used after)
stty -icanon time 0 min 0
Then just run your loop like normal. No need for -r.
while true; do
read input
if ["$input" = "a"]; then
echo "hello world"
fi
done
IMPORTANT!
After you have finished with non blocking you must remember to set stty back to normal using
stty sane
If you dont you will not be able to see anything on the terminal and it will appear to hang.
You will probably want to inlcude a trap for ctrl-C as if the script is quit before you revert stty back to normal you will not be able to see anything you type and it will appear the terminal has frozen.
trap control_c SIGINT
control_c()
{
stty sane
}
P.S Also you may want to put a sleep statement in your script so you dont use up all your CPU as this will just continuously run as fast as it can.
sleep 0.1
P.S.S It appears that the hanging issue was only when i had used -echo as i used to so is probably not needed. Im going to leave it in the answer though as it is still good to reset stty to its default to avoid future problems.
You can use -echo if you dont want what you have typed to appear on screen.
You can use this getkey function:
getkey() {
old_tty_settings=$(stty -g) # Save old settings.
stty -icanon
Keypress=$(head -c1)
stty "$old_tty_settings" # Restore old settings.
}
It temporarily turns off "canonical mode" in the terminal settings
(stty -icanon) then returns the input of "head" (a shell built-in) with the -c1 option which is returning ONE byte of standard input. If you don't include the "stty -icanon" then the script echoes the letter of the key pressed and then waits for RETURN (not what we want). Both "head" and "stty" are shell built-in commands. It is important to save and restore the old terminal settings after the key-press is received.
Then getkey() can be used in combination with a "case / esac" statement for interactive one-key selection from a list of entries:
example:
case $Keypress in
[Rr]*) Command response for "r" key ;;
[Ww]*) Command response for "w" key ;;
[Qq]*) Quit or escape command ;;
esac
This getkey()/case-esac combination can be used to make many shell scripts interactive. I hope this helps.
How to read a single key press into variable c, and print it out. This prints out the key you pressed instantly, withOUT you having to press Enter first:
read -n1 c && printf "%s" "$c"
Or, with a little more "prettifying" in the output print:
read -n1 c && printf "\nYou Pressed: %s\n" "$c"
Example output of the latter command:
$ read -n1 c && printf "\nYou Pressed: %s\n" "$c"
M
You Pressed: M
To suppress your initial keypress from being echoed to the screen, add the -s option as well, which says from the read --help menu:
-s do not echo input coming from a terminal
Here is the final command:
read -sn1 c && printf "You Pressed: %s\n" "$c"
And a demo:
$ read -sn1 c && printf "You Pressed: %s\n" "$c"
You Pressed: p
You can also optionally separate the -sn1 argument into two arguments (-s -n1) for clarity.
References:
I learned about read -n1 from #pacholik here.
See also:
read_keypress.sh in my eRCaGuy_hello_world repo.
I use this bash cmd in C and C++ system calls to read keys here:
[my answer] Capture characters from standard input without waiting for enter to be pressed
[my answer] Read Key pressings in C ex. Arrow keys, Enter key
I have a way to do this in my project: https://sourceforge.net/p/playshell/code/ci/master/tree/source/keys.sh
It reads a single key everytime key_readonce is called. For special keys, a special parsing loop would run to also be able to parse them.
This is the crucial part of it:
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
KEY[0]=$K
if [[ $K == $'\e' ]]; then
if [[ BASH_VERSINFO -ge 4 ]]; then
T=(-t 0.05)
else
T=(-t 1)
fi
if read -rn 1 -d '' "${T[#]}" "${S[#]}" K; then
case "$K" in
\[)
KEY[1]=$K
local -i I=2
while
read -rn 1 -d '' "${T[#]}" "${S[#]}" "KEY[$I]" && \
[[ ${KEY[I]} != [[:upper:]~] ]]
do
(( ++I ))
done
;;
O)
KEY[1]=$K
read -rn 1 -d '' "${T[#]}" 'KEY[2]'
;;
[[:print:]]|$'\t'|$'\e')
KEY[1]=$K
;;
*)
__V1=$K
;;
esac
fi
fi
utils_implode KEY __V0

Configure shell to always print prompt on new line, like zsh

If a command's output does not end with a \n, the next prompt appears, awkwardly, immediately afterwards:
$ echo -n hai
hai$
I just noticed a colleague whose shell (zsh, for what it's worth) is configured to print a % (with background and foreground colours inverted for emphasis) followed by a \n in such cases:
$ echo -n hai
hai%
$
I'd like to do the same. I use Bash. Is this possible? If so, what would I add to my ~/.bashrc?
UPDATE
I've spent several hours gaining an understanding of how gniourf_gniourf's solution works. I'll share my findings here, in case they are of use to others.
ESC[6n is the control sequence introducer for accessing the cursor position (http://en.wikipedia.org/wiki/ANSI_escape_code).
\e is not a valid representation of ESC when using echo on OS X (https://superuser.com/q/33914/176942). \033 can be used instead.
IFS is Bash's internal field separator (http://tldp.org/LDP/abs/html/internalvariables.html#IFSREF).
read -sdR looks like shorthand for read -s -d -R, but in fact the "R" is not a flag, it's the value of the -d (delimiter) option. I decided to write read -s -d R instead to avoid confusion.
The double-parentheses construct, (( ... )), permits arithmetic expansion and evaluation (http://tldp.org/LDP/abs/html/dblparens.html).
Here's the relevant snippet from my .bashrc:
set_prompt() {
# CSI 6n reports the cursor position as ESC[n;mR, where n is the row
# and m is the column. Issue this control sequence and silently read
# the resulting report until reaching the "R". By setting IFS to ";"
# in conjunction with read's -a flag, fields are placed in an array.
local curpos
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
curpos[0]="${curpos[0]:2}" # strip leading ESC[
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
# set PS1...
}
export PROMPT_COMMAND=set_prompt
Note: The curpos[0]="${curpos[0]:2}" line is unnecessary. I included it so this code could be used in a context where the row is also relevant.
A little trick using PROMPT_COMMAND:
The value of the variable PROMPT_COMMAND is examined just before Bash prints each primary prompt. If PROMPT_COMMAND is set and has a non-null value, then the value is executed just as if it had been typed on the command line.
Hence, if you put this in your .bashrc:
_my_prompt_command() {
local curpos
echo -en "\E[6n"
IFS=";" read -sdR -a curpos
((curpos[1]!=1)) && echo -e '\E[1m\E[41m\E[33m%\E[0m'
}
PROMPT_COMMAND=_my_prompt_command
you'll be quite good. Feel free to use other fancy colors in the echo "%" part. You can even put the content of that in a variable so that you can modify it on the fly.
The trick: obtain the column of the cursor (with echo -en "\E[6n" followed by the read command) before printing the prompt and if it's not 1, print a % and a newline.
Pros:
pure bash (no external commands),
no subshells,
leaves your PS1 all nice and clean: if you want to change your PS1 sometimes (I do this when I work in deeply nested directory — I don't like having prompts that run on several miles), this will still work.
As tripleee comments, you could use stty instead of echoing a hard-coded control sequence. But that uses an external command and is not pure bash anymore. Adapt to your needs.
Regarding your problem with the ugly character codes that get randomly printed: this might be because there's still some stuff in the tty buffer. There might be several fixes:
Turn off and then on the echo of the terminal, using stty.
set_prompt() {
local curpos
stty -echo
echo -en '\033[6n'
IFS=';' read -d R -a curpos
stty echo
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
}
PROMPT_COMMAND=set_prompt
the main difference is that the echo/read combo has been wrapped with stty -echo/stty echo that respectively disables and enables echoing on terminal (that's why the -s option to read is now useless). In this case you won't get the cursor position correctly and this might lead to strange error messages, or the % not being output at all.
Explicitly clear the tty buffer:
set_prompt() {
local curpos
while read -t 0; do :; done
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
}
PROMPT_COMMAND=set_prompt
Just give up if the tty buffer can't be cleaned:
set_prompt() {
local curpos
if ! read -t 0; then
echo -en '\033[6n'
IFS=';' read -s -d R -a curpos
(( curpos[1] > 1 )) && echo -e '\033[7m%\033[0m'
# else
# here there was still stuff in the tty buffer, so I couldn't query the cursor position
fi
}
PROMPT_COMMAND=set_prompt
As a side note: instead of reading in an array curpos, you can directly obtain the position of the cursor in variables, say, curx and cury as so:
IFS='[;' read -d R _ curx cury
If you only need the y-position cury:
IFS='[;' read -d R _ _ cury
Thanks to Gilles on unix.stackexchange:
You can make the bash display its prompt on the next line if the previous command left the cursor somewhere other than the last margin. Put this in your .bashrc (variation by GetFree of a proposal by Dennis Williamson)
From the two linked answers I distilled this solution:
PS1='\[\e[7m%\e[m\]$(printf "%$((COLUMNS-1))s")\r$ '
Explanation:
\[\e[7m%\e[m\] -- reverse video percent sign
printf "%$((COLUMNS-1))s" -- COLUMNS-1 spaces. The COLUMNS variable stores the width of your terminal if the checkwinsize options is set. Since the printf is within a $() sub-shell, instead of printing to the screen its output will be added to PS1
\r a carriage return character
So, basically, it's a % sign, a long sequence of spaces, followed by a return key. This works, but to be honest I don't understand why this has the desired effect. Specifically, why does it look like it adds a line break only when it's needed, otherwise no extra line break? Why are the spaces necessary there?
if you do echo $PS1 you will see de current code of your prompt like this:
\[\e]0;\u#\h: \w\a\]${debian_chroot:+($debian_chroot)}\u#\h:\w\$
now prepend it with a \n like this:
PS1="\n\[\e]0;\u#\h: \w\a\]${debian_chroot:+($debian_chroot)}\u#\h:\w\$"
now your prompt will always begin on a new line.

Resources