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
Related
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.
I am having problems with finding how to exit my script when a keyphrase is entered: e.g. "Foo".
Essentially I wish to test every user input for this phrase and invoke the exit command. I could create a test function I call after every user entry but this seems inelegant.
I am using function:
function EXIT {
printf "\n\nSCRIPT IS NOW TERMINATING\n"
if [ -n $userLogged ]; then
local TIME="$username LOGGED OUT at: "$(date +%r)" on the "$(date +%d/%m/%Y)"\n"
printf "$TIME" >> usage.db
fi
exit
}
and:
trap EXIT SIGTERM
Can it be done using trap?
I'm not exactly sure but I guess you are after something like this:
#!/bin/bash
# Save this script as "my_exit"
function EXIT {
printf "\n\nSCRIPT IS NOW TERMINATING\n"
if [ -n $userLogged ]; then
local TIME="$username LOGGED OUT at: "$(date +%r)" on the "$(date +%d/%m/%Y)"\n"
printf "$TIME" >> usage.db
fi
exit
}
trap EXIT SIGUSR1
while :; do
read -p "Enter your test word: " word
if [ "$word" = "Foo" ];
then
pkill --signal SIGUSR1 my_exit
fi
done
I used SIGUSR1 instead of SIGTERM just to show the functionality better. It's also possible to change that into two separate scripts with minor modifications i.e. "EXIT+trap" block will be one, the eternal loop another and latter one would signal the first one via SIGUSR1 to do exit routines.
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.
I want to create a logfile with a log of certain events like:
log into gnome
log screen
unlock screen
logout
My plan was to write a script that runs in the background as a child process of the gnome session. It would start by appending "LOGIN", monitor for screen locking/unlocking, and append "LOGOUT" when it received a SIGHUP (meaning the session ended).
I wrote a script [1] which works if I start it in a shell, but it's clunky. I want this program running in the background -- I don't want to have to remember to start it each time I log in.
Can someone point me in the right direction?
[1] The script:
#!/bin/bash
# param $1: type, in:
# ["SCREEN_LOCKED",
# "SCREEN_UNLOCKED",
# "LOGIN",
# "LOGOUT",
# "SIGINT",
# "SIGTERM"]
function write_log {
if [ -z $1 ]; then
1="unspecified"
fi
echo -e "$1\t$(date)" >> "$LOG"
}
function notify {
echo "$#" >&2
}
function show_usage {
notify "Usage: $0 login <address> <logfile>"
notify "Parameters:"
notify " login: You must use the string 'login' to avoid seeing this message."
notify " <logfile>: File to store logs."
notify ""
notify "This script is designed to go in the bashrc file, and be called in the"
notify "form of: $0 login '$USER#$(uname -n)' >>/path/to/logfile &"
notify ""
}
if [ "$#" -eq 0 ]; then
show_usage
exit 1
fi
if [ "$1" != "login" ]; then
show_usage
notify "Error: first parameter must be the string 'login'."
exit 1
fi
LOG="$2"
if [ -z "$LOG" ]; then
notify "Error: please specify a logfile."
exit 1
elif [ -f "$LOG" ]; then
# If the logfile exists, verify that the last action was a LOGOUT.
LASTACTION=$(tail -1 "$LOG" | awk '{print $1}')
if [ $LASTACTION != "LOGOUT" ]; then
notify "Logfile '$LOG' exists but last action was not logout: $LASTACTION"
exit 1
fi
else
# If the file does not exist, create it.
touch "$LOG" || ( notify "Cannot create logfile: '$2'" && exit 1 )
fi
# Begin by logging in:
write_log "LOGIN"
# Handle signals by logging:
trap "write_log 'LOGOUT'; exit" SIGHUP
trap "write_log 'INTERRUPTED_SIGINT'; exit 1" SIGINT
trap "write_log 'INTERRUPTED_SIGTERM'; exit 1" SIGTERM
# Monitor gnome for screen locking. Log these events.
dbus-monitor --session "type='signal',interface='org.gnome.ScreenSaver'" | \
(
while true; do
read X;
if echo $X | grep "boolean true" &> /dev/null; then
write_log "SCREEN_LOCKED"
elif echo $X | grep "boolean false" &> /dev/null; then
write_log "SCREEN_UNLOCKED"
fi
done
)
I also have such a script, it works well starting it with a desktop file in the autostart directory:
$ cat ~/.config/autostart/watcher.sh.desktop
[Desktop Entry]
Type=Application
Exec=/home/<username>/hg/programs/system/watcher/watcher.sh
Hidden=false
X-GNOME-Autostart-enabled=true
Name[de_DE]=watcher
Name=watcher
Comment[de_DE]=
Comment=
Nowadays I think it's better to listen to the LockedHint rather than screensaver messages. That way you're not tied to a screensaver implementation.
Here's a simple script to do that:
gdbus monitor -y -d org.freedesktop.login1 | grep LockedHint
Gives this:
/org/freedesktop/login1/session/_32: org.freedesktop.DBus.Properties.PropertiesChanged ('org.freedesktop.login1.Session', {'LockedHint': <true>}, #as [])
/org/freedesktop/login1/session/_32: org.freedesktop.DBus.Properties.PropertiesChanged ('org.freedesktop.login1.Session', {'LockedHint': <false>}, #as [])
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