Bash read command - how to accept control characters - bash

I am using the following to read a bunch of parameters from the user.
read -p "Name`echo $'\n> '`" NAME
All my parameters have default values. I want to provide an option to the user to skip providing values at any point. i.e. user might provide values for first 3 parameters and press Ctrl+s to skip entering values for the rest of them.
How can I trap Ctrl+s?

Ctrl+S is the terminal scroll lock character, and is not immediately available. You have two options:
Work with the system, use the system standard key combos, and make life easier for yourself and everyone else.
Fight the system, insist on using this key combo, do anything it takes to make it happen, and then live with the consequences.
If you want to work with the system, consider using a blank line and/or Ctrl+D, which is already extensively used to end input. This is easy and robust:
if read -p "Name (or blank for done)"$'\n> ' name && [[ $name ]]
then
echo "User entered $name"
else
echo "User is done entering things"
fi
Alternatively, here's a start for fighting the system:
#!/bin/bash
settings=$(stty -g)
echo "Enter name or ctrl+s to stop:"
stty stop undef # or 'stty raw' to read other control chars
str=""
while IFS= read -r -n 1 c && [[ $c ]]
do
[[ $c = $'\x13' ]] && echo "Ctrl+S pressed" && break
str+="$c"
done
echo "Input was $str"
stty "$settings"
This will correctly end when the user hits Ctrl+S on a standard configuration, but since it doesn't work with the system, it needs additional work to support proper line editing and less common configurations.

Not sure what you mean by trapping. The user can input Ctrl+S by typing Ctrl+V and then Ctrl+S, and your script can then do the check:
if [[ $NAME == '^S' ]]; then
...
# skip asking for more values and continue with default values
fi

Related

Bash: Is there a way to require the enter key to be pushed with read -n once character limit is reached?

Is there a way to require the enter key to be pushed with read -n once character limit is reached, rather than automatically jumping to the following line in the script?
For example, in the following script:
echo "Please enter your credentials below"
read -p "Please enter your username: " -e -n 15 usern
read -p "Please enter your password: " -s -e -n 15 passw
Once 15 chars are entered for the username, it automatically jumps to the password prompt. However, it would be more user-friendly to simply stop allowing input once the 15 chars are reached (besides the backspace key and enter key), and require the user to press enter to continue to the password prompt. This is how most logins work, after all...
I'm aware that I could use a while loop/if statement restricting the char limit in the usern variable (e.g. with -gt), but I was wondering how I could specifically limit the user to pressing either backspace or enter (or even the arrow keys if they want to edit a single character from their username, but I'm not worried about this right now), once those 15 characters are reached, and REQUIRE the user to press enter to continue to the following prompt.
Hopefully what I am asking for makes sense. Thanks to everyone in advance!
As mentioned in the comments, bash's read isn't able to reproduce the expected behaviour for specific keys. However, you can avoid using read by rolling your own input parsing.
Some points to take into account:
Terminal line settings need to be adjusted with stty, to avoid printing input characters as they are typed (i.e. we only want to print our messages, otherwise characters would be duplicated);
Terminal emulator needs to support VT100 escape codes, in order to erase the line and move the cursor when redrawing the prompt;
Characters can be compared by ascii code, but when enter is pressed, we don't get a read character for the newline itself. However, since other whitespace characters are read, this is not an issue.
The following script implements the expected behaviour, by accepting characters up to a given limit (for the password, no typed characters are printed). The input is only submitted when enter is pressed.
#!/bin/sh
set -eu
parse() {
message=$1
var=$2
is_password=$3
input=
stty -icanon -echo
while true; do
# Clear line, move cursor to beginning, then print prompt
if [ "$is_password" -eq 1 ]; then
printf '\33[2K\r'"$message "
else
printf '\33[2K\r'"$message $input"
fi
# Read 1 character
i=$(dd bs=1 count=1 2>/dev/null)
# Was a backspace read?
if echo "$i" | grep -qP '\x7f'; then
# Remove last character
input=$(echo "$input" | sed 's/.$//')
# Only add read character if input field limit wasn't reached
elif [ ${#input} -lt 15 ]; then
input=$input$i
fi
# Was a newline read?
if [ -z "$i" ]; then
break
fi
done
stty icanon echo
eval "$var=$input"
}
echo "Please enter your credentials below"
parse "Please enter your username:" "usern" 0
printf "\n%s\n" "Read username: $usern"
parse "Please enter your password:" "userp" 1
printf "\n%s\n" "Read password: $userp"

Populating a variable from a script that needs to write to the terminal

How can I populate a variable passed to bash script, which is called from another bash script? I want to know about options and best practices.
Let me elaborate why the many questions answered by either setting a exit status or echoing are not sufficient:
exit n: n is restricted to 0 <= n <= 255
echo foo doesn't allow me to echo any relevant information in that script, alternate screen buffer also won't help with this.
I have figured out one possible solution:
#outer.sh
source inner.sh
populate result
echo "Evaluated: $result"
#inner.sh
function populate {
local __popvar=$1
eval $__popvar="'RETURN VALUE'"
}
I dislike this solution for three reasons:
The need to source the inner script, polluting global scope with helper functions.
The need to eval, especially because of the confusing multi-quotes.
Verbosity. I first need to source, and then call one function, whereas I'd much rather like to bash inner.sh result.
Further information on the inner script
The inner script is supposed to write to the alternate screen buffer. On this buffer, the user is able to select an option from an array (selection via arrow keys or ijkl style, confirmation with space or enter). This option should be returned from the script somehow. Returning the index is not an option, as the number of elements in the array can exceed 256. Code:
#!/usr/bin/bash
prompt=$1; shift
options=( "$#" )
c_opts=${#options[#]}
selected=0
# switch to alternate screen and trap the kill signal to switch back
tput smcup
trap ctrl_c INT
function ctrl_c {
tput rmcup
exit 1
}
function print_opts {
for (( i = 0; i < $c_opts; i++ )); do
if [[ i -eq $selected ]]; then
echo -e "\t\e[7m ${options[i]} \e[0m"
else
echo -e "\t ${options[i]} "
fi
done
}
function reset_term {
for (( i = 0; i < $c_opts; i++ )); do
tput cuu1 # move cursor up 1 line
tput el # delete current line
done
}
function reprint_opts {
reset_term
print_opts
}
echo $prompt
print_opts
while read -sN1 key; do
read -sN1 -t 0.0001 k1
read -sN1 -t 0.0001 k2
read -sN1 -t 0.0001 k3
key+="${k1}${k2}${k3}"
# colemak layout
case "$key" in
n|u|$'\e[A'|$'\e0A'|$'\e[D'|$'\e0D') # up or left
((selected > 0)) && ((selected--));;
e|i|$'\e[B'|$'\e0B'|$'\e[C'|$'\e0C') # down or right
((selected < $c_opts-1)) && ((selected++));;
$'\e[1~'|$'\e0H'|$'\e[H') # home key
selected=0;;
$'\e[4~'|$'\e0F'|$'\e[F') # end key
((selected = $c_opts-1));;
' '|$'\x0a') # enter or space
tput rmcup && echo ${options[$selected]} && exit 0;;
q|$'\e') # q or escape
tput rmcup && exit 0;;
esac
reprint_opts
done
Based on #JohnKugelman's comment, the script should be called as follows:
prompt="Your options are:"
options=(
"Option A"
"Option B"
"Option C"
"Option D"
)
result=$( exec 3>&1; bash select-menu.sh "$prompt" "${options[#]}" 2>&1 1>&3; exec 3>&- )
echo $result
This does seem an appealing solution, but it does not fix the problem. The Selection menu that is to be printed on the alternate screen buffer is not printed. Input however works correctly and the selection is stored in result.
To get a sense of the desired behavior you can replace the last two lines in the calling script like so:
bash select-menu.sh "$prompt" "${options[#]}"
Don't rule out result=$(inner.sh) just yet. If you want to display interactive prompts or dialogs in the script, do those on stderr, and have it write only the answer to stdout. Then you can have your cake and eat it too: interactive prompts and the result saved to a variable.
For example, dialog does exactly this if you use --output-fd 1 to tell it to write its answer to stdout. It uses curses to draw a dialog to the alternate screen but does it all on stderr.
$ value=$(dialog --keep-tite --output-fd 1 --inputbox title 10 40)
<dialog box shown>
<type "hello">
hello
(via Ask Ubuntu: How to get dialog box input directed to a variable?)
The script you posted can be made to do the same thing. It currently writes to stdout. Put exec 3>&1 1>&2 at the top so it'll write to stderr instead, and change the final echo ${options[$selected]} to echo ${options[$selected]} >&3 to write the answer to stdout. That'd get rid of the need for the caller to juggle file descriptors.
That said, prompts are not very UNIX-y. Consider eschewing interactivity entirely in favor of command-line arguments, configuration files, or environment variables. These options are better for power users who know how to use your script and want to automate it themselves.
My main purpose here is to commit a latest-stable-config from a selection of my last backups, which in my opinion needs the human judgement of when to consider a backup as appropriately stable.
The way I'd personally handle it is by writing a script with a couple of modes. Let's call it backups. backups --list would display a list of backups. You pick one and then call backups --commit <id> which would commit the named config. backups with no arguments would display usage for the unfamiliar user.
$ backups
Usage: backups --list
or: backups --commit <id>
Manages a selection of backups. Use --list to list available backups
and --commit to commit the latest stable config.
$ backups --list
4ac6 10 minutes ago
18f2 1 day ago
3019 7 days ago
$ backups --commit 4ac6

Read from active tty in %post-Script during rpm-package installation

I've built my own rpm-packages. After installation, I need some informations from the user via keyboard input, so I've created an post-Script which is working properly if I start it from bash.
In the first step, the script is asking you if a value is correct. If you press Nn you were ask to enter the new one. Here is one example:
while [[ ! ($REPLY =~ ^[NnJjYy]$) ]]
do
read -p "This is just an example, pleaser answer with NnJjYy only" -n 1 -r < $(tty)
if [[ $REPLY =~ ^[Nn]$ ]]
then
printf "\nValue="
read HOST < $(tty)
fi
echo
done
This part is working properly if I run it directly. If I put it on the %post-Part of my SPEC-File, it will run as an endless loop with the errormsg "Mehrdeutige Umlenkung" which should mean "amibigious redirect" in english.
I think the problem is caused by the "$(tty)" of the read command, because if I change this to my active tty (e.g. /dev/pts/0) it's working, but I would like to have it universal.
Do you have any ideas how I could do this?

Press any key to abort in 5 seconds

Hi I'm trying to implement an event that will happen after a 5 second countdown, unless a key is pressed. I have been using this code, but it fails if I press enter or space. It fails in the sense that enter or space is detected as "".
echo "Phoning home..."
key=""
read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key
echo
if [ "$key" = "" ] # No Keypress detected, phone home.
then python /home/myuser/bin/phonehome.py
else echo "Aborting."
fi
After reading this post,
Bash: Check if enter was pressed
I gave up and posted here. I feel like there must be a better way than what I have tried to implement.
The read manual says:
The return code for read is zero, unless end-of-file is encountered
or read times out.
In your case, when the user hits any key within allowed time you wish to abort else continue.
#!/bin/bash
if read -r -s -n 1 -t 5 -p "TEST:" key #key in a sense has no use at all
then
echo "aborted"
else
echo "continued"
fi
Reference:
Read Manual
Note:
The emphasis in the citation is mine.
The accepted answer in the linked question covers the "detecting enter" component of the question. You look at the exit code from read.
As to handling spaces there are two answers.
The problem with space is that under normal circumstances read trims leading and trailing whitespace from the input (and word-splits the input) when assigning the input to the given variables.
There are two ways to avoid that.
You can avoid using a custom named variable and use $REPLY instead. When assigning to $REPLY no whitespace trimming or word-splitting is performed. (Though looking for this just now I can't actually find this in the POSIX spec so this may be a non-standard and/or non-portable expansion of some sort.)
Explicitly set IFS to an empty string for the read command so it doesn't perform and whitespace trimming or word-splitting.
$ IFS= read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key; echo $?
# Press <space>
0
$ declare -p key
declare -- k=" "
$ unset -v k
$ IFS= read -r -s -n 1 -t 5 -p "Press any key to abort in the next 5 seconds." key; echo $?
# Wait
1
$ declare -p key
-bash: declare: k: not found

shell script : check variable and then execute interactive mode

I am writing a utility that can run in command line or interactive mode. In the code, I'd like to check if interactive flag is set and then echo the questions to the User for reading the input. However, for ever question , i dont want to check interactive flag with if condition. In bash script, is there a more efficient way to achieve this ?
Any pointers are greatly appreciated!
Thank you
Something like this maybe?
#! /bin/bash
function interactive {
shift
while read line; do
something with $line
done
}
getopts 'i' option
[[ $option = 'i' ]] && interactive "$#"
Note that this isn't the best style if you have multiple options. In that case use while getopts and shift using argument index.
If you want to tell if the shell is in "interactive mode", as defined by the shell, then you can use something like:
case $- in
*i*) # Interactive
;;
*) # Non-interactive
;;
esac
This, however, is probably not what you want. Shell scripts, for example, are, by default, "non-interactive".
If you want to know if you can ask the user a question, then you are more interested in finding out if stdin is connected to a terminal. In that case, you can use the -t test on file descriptor zero (which is standard input):
if [ -t 0 ]
then
# Interactive: ask user
read -p "Enter a color: " color
read -p "Enter a number: " number
else
# Non-interactive: assign defaults
color="Red"
number=3
fi
echo "color=$color and number=$number"
If there is the possibility that your script might be run remotely over, say, ssh, then a slightly more complicated test is needed:
if [ -t "$fd" ] || [ -p /dev/stdin ]
then
echo interactive
else
echo non-interactive
fi

Resources