How to set stty -echo and also read from /dev/stdin - bash

I am trying to write a program which reads from stdin and also receives user input from the tty.
I would like to disable rendering of user input because it messes with my menu system and causes flickering if I redraw to remove it. However I cannot seem to use stty -echo if the script recieves input from stdin.
Here is a simplified example of the script:
trapinput
#!/bin/bash
hideinput()
{
if [ -t 0 ]; then
echo "Is tty"
save_state=$(stty -g)
stty -echo -icanon time 0 min 0
echo -ne "\e[?1049h\r" 1>&2;
else
echo "is not tty"
fi
}
cleanup()
{
if [ -t 0 ]; then
stty "$save_state"
echo -ne "\e[?1049l" 1>&2;
echo "exit tty"
else
echo "is not tty"
fi
}
trap cleanup EXIT
trap hideinput CONT
hideinput
input="$(< /dev/stdin)";
echo "$input"
while true;
do
read -r -sn1 < /dev/tty;
read -r -sn3 -t 0.001 k1 < /dev/tty;
REPLY+=$k1;
echo $REPLY
done
hello.txt
helloworld!
running $ ./trapinput will echo "Is tty" on start and "exit tty" when killed along with running the rest of the program as I would expect. It also prevents the user input from being displayed directly allowing me to print it on the screen in the correct location.
However if I run $ echo "test" | ./trapinput or $ ./trapinput < hello.txt it will echo "is not tty" and stty -echo is not set causing user input to be displayed where I do not want it.
How can I disable rendering of user input but retain the ability to pipe in text/use file redirection?

How can I disable rendering of user input but retain the ability to pipe in text/use file redirection?
Disable the echo on where you from take the input. Do:
trap 'cleanup < /dev/tty' EXIT
trap 'hideinput < /dev/tty' CONT
hideinput </dev/tty
You could also open a file descriptor specific for input exec 10</dev/tty, etc.

Related

Stop echo and consume all user input in Bash

TARGET
Until some sub-task in script completes its job:
stop echo;
disable cursor;
consume all user input;
do not block interrupts (Ctrl+C, etc.).
WHAT HAVE DONE
For now, using this answer, I create few functions for that, here they are:
function hide_input()
{
if [ -t 0 ]; then
stty -echo -icanon time 0 min 0
fi
}
function reset_input()
{
if [ -t 0 ]; then
stty sane
fi
}
function stop_interactive()
{
trap reset_input EXIT
trap hide_input CONT
hide_input
tput civis
}
function start_interactive()
{
tput cnorm
reset_input
}
function consume_input()
{
local line
while read line; do line=''; done
}
And here is the way they are used:
echo "Warn the user: the job will be started."
read -p "Continue? [yes/no] > "
if [ "$REPLY" == "yes" ]; then
stop_interactive # <== from here all input should be rejected
echo "Notify the user: job starting..."
# << ------ here goes some long job with output to terminal ------>
echo "Notify the user: job done!"
consume_input # <== here I trying to get all user input and put nowhere
start_interactive # <== from here restore normal operation
else
echo "Aborted!"
exit 0
fi
THE PROBLEM
The problem is: current 'solution' does not work. When I press keys during long running job they appears on the screen, and pressing 'Enter' ruins all output with cursor movements. Furthermore, after 'start_interactive' function call all input appears on the terminal screen.
What is the right solution for this task?
SOLUTION
Final working solution is:
function hide_input()
{
if [ -t 0 ]; then
stty -echo -icanon time 0 min 0
fi
}
function reset_input()
{
if [ -t 0 ]; then
stty sane
fi
}
function consume_input()
{
local line
while read line; do line=''; done
}
function stop_interactive()
{
trap reset_input EXIT
trap hide_input CONT
hide_input
tput civis
}
function start_interactive()
{
consume_input
trap - EXIT
trap - CONT
tput cnorm
reset_input
}
echo "Warn the user: the job will be started."
read -p "Continue? [yes/no] > "
if [ "$REPLY" == "yes" ]; then
stop_interactive
echo "Notify the user: job starting..."
do_the_job &
pid=$!
while ps $pid > /dev/null ; do
consume_input
done
echo "Notify the user: job done!"
start_interactive
else
echo "Aborted!"
exit 0
fi
Based in your question, there are many questions "why" if I look at your code. If you do not want to change the behavior of ^C etc., then don't use traps. All your functions test whether the file descriptor 0 is a terminal. Do you plan to use the script in a pipe? Also, your consuming of user input will continue until end-of-file, so the script may never end.
Based on your question, I would write something like this:
echo "Warn the user: the job will be started."
read -p "Continue? [yes/no] > "
if [ "$REPLY" == "yes" ]; then
stty -echo
echo "Notify the user: job starting..."
program_to_execute &
pid=$!
while ps $pid > /dev/null ; do
read -t 1 line
done
echo "Notify the user: job done!"
else
echo "Aborted!"
fi
stty sane

POSIX alternative to bash read with timeout and character limit

I am writing an interactive shell script that needs to run on as many systems as possible. Is there an alternative way to implement the following that is compatible with a standard POSIX system?
#! /bin/bash
echo -n "Do you wish to continue? (Y/n) 5 seconds to respond... "
read -n 1 -t 5 answer # accepts a single character, 5 second timeout.
if [ "$answer" = "n" ] || [ "$answer" = "N" ] ; then
echo -e "\nExiting..."
exit
fi
echo -e "\nContinuing with script..."
# More code
The timeout on read is most important to me (read -t 5). The one character limit for read is desirable but not essential (read -n 1).
Ideally, the script would work on POSIX systems and also within bash without having to enable a special POSIX compatibility mode.
Adapting the stty settings from Mr Dickey's answer, the following seems to work and continues with the script if anything other than 'n' or 'N' is pressed.
As far as I can tell, all of the stty settings are posix.
#!/bin/sh
read_char() {
old=$(stty -g)
stty raw -echo min 0 time 50
eval "$1=\$(dd bs=1 count=1 2>/dev/null)"
stty $old
}
printf "Do you wish to continue? (Y/n) 5 seconds to respond..."
read_char answer
# answer=$(getch)
# answer=$(getche)
if [ "$answer" = "n" ] || [ "$answer" = "N" ] ; then
printf "\nExiting...\n"
exit
fi
printf "\nContinuing with script...\n"
Alternatives to "read_char":
This avoids using eval (can be unsafe)
getch() {
old=$(stty -g)
stty raw -echo min 0 time 50
printf '%s' $(dd bs=1 count=1 2>/dev/null)
stty $old
}
This avoids eval and prints the pressed key
getche() {
old=$(stty -g)
stty raw min 0 time 50
printf '%s' $(dd bs=1 count=1 2>/dev/null)
stty $old
}
The stty program provides the means to do this. xterm has several scripts (in its source's "vttests" subdirectory) which save, modify and restore terminal settings to allow it to read the terminal's response to escape sequences. Here is a part of dynamic2.sh (the beginning sets up printf or echo, to address some old systems with the $CMD variable):
echo "reading current color settings"
exec </dev/tty
old=`stty -g`
stty raw -echo min 0 time 5
original=
for N in $FULL
do
$CMD $OPT "${ESC}]$N;?^G${SUF}" > /dev/tty
read reply
eval original$N='${reply}${SUF}'
original=${original}${reply}${SUF}
done
stty $old
if ( trap "echo exit" EXIT 2>/dev/null ) >/dev/null
then
trap '$CMD $OPT "$original" >/dev/tty; exit' EXIT HUP INT TRAP TERM
else
trap '$CMD $OPT "$original" >/dev/tty; exit' 0 1 2 5 15
fi

Disabling user input during an infinite loop in bash

I have this bash script which basically starts the web and selenium servers with progress indicator. Since it takes some time to selenium server to start I'm checking the status in an infinite loop.
The problem is, while waiting for it to start I press keys accidentaly it's displayed in the screen and if the loop ends (times out) it's also displayed in the command prompt too.
I want to disable all user input (except the control keys of course) while inside the loop:
start_selenium() {
echo -n "Starting selenium server"
java -jar ${selenium_jar} &> $selenium_log &
# wait for selenium server to spin up! (add -v for verbose output)
i=0
while ! nc -z localhost 4444; do
sleep 1
echo -n "."
((i++))
if [ $i -gt 20 ]; then
echo
echo -e $bg_red$bold"Selenium server connection timed out"$reset
exit 1
fi
done
}
The stty invocations are from http://www.unix.com/shell-programming-and-scripting/84624-nonblocking-i-o-bash-scripts.html
This still respects Ctrl-C, but doesn't show input, and consumes it so it's not left for the shell.
#!/bin/bash
hideinput()
{
if [ -t 0 ]; then
stty -echo -icanon time 0 min 0
fi
}
cleanup()
{
if [ -t 0 ]; then
stty sane
fi
}
trap cleanup EXIT
trap hideinput CONT
hideinput
n=0
while test $n -lt 10
do
read line
sleep 1
echo -n "."
n=$[n+1]
done
echo
Use stty to turn off keyboard input.
stty -echo
#### Ur Code here ####
stty echo
-echo turns off keyboard input and stty echo reenables keyboard input.

BASH - using trap ctrl+c

I'm trying to execute commands inside a script using read, and when the user uses Ctrl+C, I want to stop the execution of the command, but not exit the script.
Something like this:
#!/bin/bash
input=$1
while [ "$input" != finish ]
do
read -t 10 input
trap 'continue' 2
bash -c "$input"
done
unset input
When the user uses Ctrl+C, I want it to continue reading the input and executing other commands. The problem is that when I use a command like:
while (true) do echo "Hello!"; done;
It doesn't work after I type Ctrl+C one time, but it works once I type it several times.
Use the following code :
#!/bin/bash
# type "finish" to exit
stty -echoctl # hide ^C
# function called by trap
other_commands() {
tput setaf 1
printf "\rSIGINT caught "
tput sgr0
sleep 1
printf "\rType a command >>> "
}
trap 'other_commands' SIGINT
input="$#"
while true; do
printf "\rType a command >>> "
read input
[[ $input == finish ]] && break
bash -c "$input"
done
You need to run the command in a different process group, and the easiest way of doing that is to use job control:
#!/bin/bash
# Enable job control
set -m
while :
do
read -t 10 -p "input> " input
[[ $input == finish ]] && break
# set SIGINT to default action
trap - SIGINT
# Run the command in background
bash -c "$input" &
# Set our signal mask to ignore SIGINT
trap "" SIGINT
# Move the command back-into foreground
fg %-
done
For bash :
#!/bin/bash
trap ctrl_c INT
function ctrl_c() {
echo "Ctrl + C happened"
}
For sh:
#!/bin/sh
trap ctrl_c INT
ctrl_c () {
echo "Ctrl + C happened"
}

Bash: How to end infinite loop with any key pressed?

I need to write an infinite loop that stops when any key is pressed.
Unfortunately this one loops only when a key is pressed.
Ideas please?
#!/bin/bash
count=0
while : ; do
# dummy action
echo -n "$a "
let "a+=1"
# detect any key press
read -n 1 keypress
echo $keypress
done
echo "Thanks for using this script."
exit 0
You need to put the standard input in non-blocking mode. Here is an example that works:
#!/bin/bash
if [ -t 0 ]; then
SAVED_STTY="`stty --save`"
stty -echo -icanon -icrnl time 0 min 0
fi
count=0
keypress=''
while [ "x$keypress" = "x" ]; do
let count+=1
echo -ne $count'\r'
keypress="`cat -v`"
done
if [ -t 0 ]; then stty "$SAVED_STTY"; fi
echo "You pressed '$keypress' after $count loop iterations"
echo "Thanks for using this script."
exit 0
Edit 2014/12/09: Add the -icrnl flag to stty to properly catch the Return key, use cat -v instead of read in order to catch Space.
It is possible that cat reads more than one character if it is fed data fast enough; if not the desired behaviour, replace cat -v with dd bs=1 count=1 status=none | cat -v.
Edit 2019/09/05: Use stty --save to restore the TTY settings.
read has a number of characters parameter -n and a timeout parameter -t which could be used.
From bash manual:
-n nchars
read returns after reading nchars characters rather than waiting for a complete line of input, but honors a delimiter if fewer than nchars characters are read before the delimiter.
-t timeout
Cause read to time out and return failure if a complete line of input (or a specified number of characters) is not read within timeout seconds. timeout may be a decimal number with a fractional portion following the decimal point. This option is only effective if read is reading input from a terminal, pipe, or other special file; it has no effect when reading from regular files. If read times out, read saves any partial input read into the specified variable name. If timeout is 0, read returns immediately, without trying to read any data. The exit status is 0 if input is available on the specified file descriptor, non-zero otherwise. The exit status is greater than 128 if the timeout is exceeded.
However, the read builtin uses the terminal which has its own settings. So as other answers have pointed out we need to set the flags for the terminal using stty.
#!/bin/bash
old_tty=$(stty --save)
# Minimum required changes to terminal. Add -echo to avoid output to screen.
stty -icanon min 0;
while true ; do
if read -t 0; then # Input ready
read -n 1 char
echo -e "\nRead: ${char}\n"
break
else # No input
echo -n '.'
sleep 1
fi
done
stty $old_tty
Usually I don't mind breaking a bash infinite loop with a simple CTRL-C. This is the traditional way for terminating a tail -f for instance.
Pure bash: unattended user input over loop
I've done this without having to play with stty:
loop=true
while $loop; do
trapKey=
if IFS= read -d '' -rsn 1 -t .002 str; then
while IFS= read -d '' -rsn 1 -t .002 chr; do
str+="$chr"
done
case $str in
$'\E[A') trapKey=UP ;;
$'\E[B') trapKey=DOWN ;;
$'\E[C') trapKey=RIGHT ;;
$'\E[D') trapKey=LEFT ;;
q | $'\E') loop=false ;;
esac
fi
if [ "$trapKey" ] ;then
printf "\nDoing something with '%s'.\n" $trapKey
fi
echo -n .
done
This will
loop with a very small footprint (max 2 millisecond)
react to keys cursor left, cursor right, cursor up and cursor down
exit loop with key Escape or q.
Here is another solution. It works for any key pressed, including space, enter, arrows, etc.
The original solution tested in bash:
IFS=''
if [ -t 0 ]; then stty -echo -icanon raw time 0 min 0; fi
while [ -z "$key" ]; do
read key
done
if [ -t 0 ]; then stty sane; fi
An improved solution tested in bash and dash:
if [ -t 0 ]; then
old_tty=$(stty --save)
stty raw -echo min 0
fi
while
IFS= read -r REPLY
[ -z "$REPLY" ]
do :; done
if [ -t 0 ]; then stty "$old_tty"; fi
In bash you could even leave out REPLY variable for the read command, because it is the default variable there.
I found this forum post and rewrote era's post into this pretty general use format:
# stuff before main function
printf "INIT\n\n"; sleep 2
INIT(){
starting="MAIN loop starting"; ending="MAIN loop success"
runMAIN=1; i=1; echo "0"
}; INIT
# exit script when MAIN is done, if ever (in this case counting out 4 seconds)
exitScript(){
trap - SIGINT SIGTERM SIGTERM # clear the trap
kill -- -$$ # Send SIGTERM to child/sub processes
kill $( jobs -p ) # kill any remaining processes
}; trap exitScript SIGINT SIGTERM # set trap
MAIN(){
echo "$starting"
sleep 1
echo "$i"; let "i++"
if (($i > 4)); then printf "\nexiting\n"; exitScript; fi
echo "$ending"; echo
}
# main loop running in subshell due to the '&'' after 'done'
{ while ((runMAIN)); do
if ! MAIN; then runMain=0; fi
done; } &
# --------------------------------------------------
tput smso
# echo "Press any key to return \c"
tput rmso
oldstty=`stty -g`
stty -icanon -echo min 1 time 0
dd bs=1 count=1 >/dev/null 2>&1
stty "$oldstty"
# --------------------------------------------------
# everything after this point will occur after user inputs any key
printf "\nYou pressed a key!\n\nGoodbye!\n"
Run this script

Resources