Wait for keypress in a while loop and stop script - bash

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).

Related

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

shell script respond to keypress

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

How to read from file *and* stdin in bash

Here is my task: read some data from file line by line. For each line, if it satisfies some condition, then ask user to input something and proceed based on the user's input.
I know how to read content line-by-line from a shell script:
while read line; do
echo $line
done < file.txt
However, what if I want to interact with the user inside the loop body. Conceptually, here is what I want:
while read line; do
echo "Is this what you want: $line [Y]es/[n]o"
# Here is the problem:
# I want to read something from standard input here.
# However, inside the loop body, the standard input is redirected to file.txt
read INPUT
if [[ $INPUT == "Y" ]]; then
echo $line
fi
done < file.txt
Should I use another way to read file? or another way to read stdin?
You can open the file on a file descriptor other than standard input. For example:
while read -u 3 line; do # read from fd 3
read -p "Y or N: " INPUT # read from standard input
if [[ $INPUT == "Y" ]]; then
echo $line
fi
done 3< file.txt # open file on fd 3 for input

Read user input inside a loop

I am having a bash script which is something like following,
cat filename | while read line
do
read input;
echo $input;
done
but this is clearly not giving me the right output as when I do read in the while loop it tries to read from the file filename because of the possible I/O redirection.
Any other way of doing the same?
Read from the controlling terminal device:
read input </dev/tty
more info: http://compgroups.net/comp.unix.shell/Fixing-stdin-inside-a-redirected-loop
You can redirect the regular stdin through unit 3 to keep the get it inside the pipeline:
{ cat notify-finished | while read line; do
read -u 3 input
echo "$input"
done; } 3<&0
BTW, if you really are using cat this way, replace it with a redirect and things become even easier:
while read line; do
read -u 3 input
echo "$input"
done 3<&0 <notify-finished
Or, you can swap stdin and unit 3 in that version -- read the file with unit 3, and just leave stdin alone:
while read line <&3; do
# read & use stdin normally inside the loop
read input
echo "$input"
done 3<notify-finished
Try to change the loop like this:
for line in $(cat filename); do
read input
echo $input;
done
Unit test:
for line in $(cat /etc/passwd); do
read input
echo $input;
echo "[$line]"
done
I have found this parameter -u with read.
"-u 1" means "read from stdout"
while read -r newline; do
((i++))
read -u 1 -p "Doing $i""th file, called $newline. Write your answer and press Enter!"
echo "Processing $newline with $REPLY" # united input from two different read commands.
done <<< $(ls)
It looks like you read twice, the read inside the while loop is not needed. Also, you don't need to invoke the cat command:
while read input
do
echo $input
done < filename
echo "Enter the Programs you want to run:"
> ${PROGRAM_LIST}
while read PROGRAM_ENTRY
do
if [ ! -s ${PROGRAM_ENTRY} ]
then
echo ${PROGRAM_ENTRY} >> ${PROGRAM_LIST}
else
break
fi
done

bash: append newline when redirecting file

Here is how I read a file row by row:
while read ROW
do
...
done < file
I don't use the other syntax
cat file | while read ROW
do
...
done
because the pipe creates a subshell and makes me lose the environment variables.
The problem arises if the file doesn't end with a newline: last line is not read. It is easy to solve this in the latter syntax, by echoing just a newline:
(cat file; echo) | while read ROW
do
...
done
How do I do the same in the former syntax, without opening a subshell nor creating a temporary file (the list is quite big)?
A way that works in all shells is the following:
#!/bin/sh
willexit=0
while [ $willexit == 0 ] ; do
read ROW || willexit=1
...
done < file
A direct while read will exit as soon as read encounters the EOF, so the last line will not be processed. By checking the return value outside the while, we can process the last line. An additional test for the emptiness of $ROW should be added after the read though, since otherwise a file whose last line ends with a newline will generate a spurious execution with an empty line, so make it
#!/bin/sh
willexit=0
while [ $willexit == 0 ] ; do
read ROW || willexit=1
if [ -n "$ROW"] ; then
...
fi
done < file
#!/bin/bash
while read ROW
...
done < <(cat file ; echo)
The POSIX way to do this is via a named pipe.
#!/bin/sh
[ -p mypipe ] || mkfifo mypipe
(cat num7; echo) > mypipe &
while read line; do
echo "-->$line<--"
export CNT=$((cnt+1))
done < mypipe
rm mypipe
echo "CNT is '$cnt'"
Input
$ cat infile
1
2
3
4
5$
Output
$ (cat infile;echo) > mypipe & while read line; do echo "-->$line<--"; export CNT=$((cnt+1)); done < mypipe; echo "CNT is '$cnt'"
[1] 22260
-->1<--
-->2<--
-->3<--
-->4<--
-->5<--
CNT is '5'
[1]+ Done ( cat num7; echo ) > mypipe
From an answer to a similar question:
while IFS= read -r LINE || [ -n "${LINE}" ]; do
...
done <file
The IFS= part prevents read from stripping leading and trailing whitespace (see this answer).
If you need to react differently depending on whether the file has a trailing newline or not (e.g., warn the user) you'll have to make some changes to the while condition.

Resources