I've set set_prompt to print prompt always on a new line.
set_prompt() {
local curpos
stty -echo
while read -t 0; do :; done
echo -en "\033[6n"
IFS=';' read -s -d R -a curpos
stty echo
(( curpos[1] > 1 )) && printf "\n"
}
but now, if I edit a file in text editors like emacs or nano, the text gets scrambled like printing character at wrong place or cursor jumps ahead or back while navigating, making text overlap.
Related
If you enter a file called 'quit' the script exits. If you enter any other text file, it will return only the lines in the file that start with 'b' or 'B'. Here is my code so far.
first part of script
second part of script
I'm not sure what I am doing wrong. For reference, I have two text files that I am using. One is blank and is named 'quit' and the other is a random text file with words starting with 'b', 'B' and others that don't start with B. The script should print an asterisk every two seconds. Upon pressing CTRL-C, you are then prompted to enter a file. The script will exit after inputting the 'quit' file or it will display every line starting with 'b' or 'B' upon entering the random word file.
#!/bin/bash
trap f1 INT
f1 ()
{
read -p "Enter a filename [enter quit to quit]: " z
cat $z | while read -r line
do
if ((z == "quit"))
then
exit
fi
if [ $z == "^b"* ]
then
echo "$line"
fi
done
}
while true
do
sleep 2
echo -e "*\c"
done
When you pipe into a while loop (cat file.txt | while ....), you create a subshell. When you call exit within the subshell, only the subshell exits. The original shell still.... continues on comfortably. Provide the input via redirection to avoid the subshell in this case.
Redirection:
# This avoids a subshell.
while read line; do
declare -p line
done < file.txt
# file.txt redirects directly into the while loop
# This avoids a subshell also.
while read line; do
declare -p line
done < <(echo hello; echo world)
# Process substitution rather than a file. Just some extra salt to tickle your brain.
Lastly, depending on your comfort with some other bash utilities, you can avoid the loop entirely. sed does waay more than just substituting letters or phrases. -n as a sed argument means "don't print the lines all the time, only print lines when I tell you to". '/pattern/ means that you'll only do the following sed action if the line matches the pattern (in our case, the pattern is /^b/). After the pattern, you could do a sed command like s/from/to/ which is the s command that prints. Or you could do a different command: p, which means "just print the line that's being processed" (the line that matched the pattern).
#!/bin/bash
trap f1 INT
f1 () {
read -p "Enter a filename [enter quit to quit]: " z
# Exit if the user typed "quit".
[ "$z" == "quit" ] && exit
# Print all the lines that start with "b"
sed -n '/^b/p' < "$z"
}
# ... other code
I have the following code:
while ...
echo -n "some text"
done | while read; do
echo "$REPLY" >> file
done
but echo works only when used without "-n" flag.
looks like when using -n, the output is not flushed/read by next while loop
How can I make sure that "some text" will be read even when not followed by EOL?
You can't distinguish between
echo -n "some text"
and
echo -n "some t"
echo -n "ext"
so you need some kind of delimiting rule. Usually EOL is used for that. read supports custom delimiter via -d or can split based on number of chars via -n or -N. For example you can make read fire on each symbol:
echo -n qwe | while read -N 1 ch; do echo $ch; done
The workaround would be (following original example):
while ...
echo -n "some text"
done | (cat && echo) | while read; do
echo "$REPLY" >> file
done
This will append EOL to the test stream & allow read to read it.
The side effect will be an additional EOL at the end of stream.
You can start with defining your own delimiter:
while :; do
echo -n "some text"
sleep 2
done | while read -d' ' reply; do
echo "-$reply-"
done
This prints:
-some-
-textsome-
-textsome-
For an email perhaps it makes sense to use . as a delimiter, but you need to decide on some tokenization scheme.
You can make read read one char a time, but should add something for reading special characters (newlines, spaces): IFS=.
I want to show that I really capture the characters, so I will uppercase the replies.
i=0
while (( i++<5 )) ; do
echo -n "some text $i. "
sleep 1;
done | while IFS= read -rn1 reply; do
printf "%s" "${reply^^}"
done
This solution has one feature: You will not see any newlines.
When you want to see them too, you need to fix this with
i=1
while (( i++<5 )) ; do
echo -n "some text $i.
second line."
sleep 1;
done | while IFS= read -rn1 reply; do
if (( ${#reply} == 0 )); then
echo
else
printf "%s" "${reply^^}"
fi
done
Not sure if I should put this on stackoverflow or unix.stackexchange but I found some similar questions here, so here it goes.
I'm trying to create a script to be called by .bashrc that allows me to select one of two options based on a single keystroke. That wouldn't be hard normally but I want the two keys corresponding to the two options to be space and enter.
Here's what I got so far:
#!/bin/bash
SELECT=""
while [[ "$SELECT" != $'\x0a' && "$SELECT" != $'\x20' ]]; do
echo "Select session type:"
echo "Press <Enter> to do foo"
echo "Press <Space> to do bar"
read -s -N 1 SELECT
echo "Debug/$SELECT/${#SELECT}"
[[ "$SELECT" == $'\x0a' ]] && echo "enter" # do foo
[[ "$SELECT" == $'\x20' ]] && echo "space" # do bar
done
The following output is what I get if I press enter, space, backspace and x:
:~$ bin/sessionSelect.sh
Select session type:
Press <Enter> to start/resume a screen session
Press <Space> for a regular ssh session
Debug//0
Select session type:
Press <Enter> to start/resume a screen session
Press <Space> for a regular ssh session
Debug//0
Select session type:
Press <Enter> to start/resume a screen session
Press <Space> for a regular ssh session
Debug//1
Select session type:
Press <Enter> to start/resume a screen session
Press <Space> for a regular ssh session
Debug/x/1
So both enter and space result in an empty SELECT. No way to distinguish the two. I tried to add -d 'D' to the read options, but that didn't help. Maybe someone can point me in the right direction.
The bash version would be 4.2 btw.
Try setting the read delimiter to an empty string then check the builtin $REPLY variable:
read -d'' -s -n1
For some reason I couldn't get it to work specifying a variable.
#!/bin/bash
SELECT=""
# prevent parsing of the input line
IFS=''
while [[ "$SELECT" != $'\x0a' && "$SELECT" != $'\x20' ]]; do
echo "Select session type:"
echo "Press <Enter> to do foo"
echo "Press <Space> to do bar"
read -s -N 1 SELECT
echo "Debug/$SELECT/${#SELECT}"
[[ "$SELECT" == $'\x0a' ]] && echo "enter" # do foo
[[ "$SELECT" == $'\x20' ]] && echo "space" # do bar
done
There are a couple of things about read that are relevant here:
It reads a single line
The line is split into fields as with word splitting
Since you're reading one character, it implies that entering Enter would result into an empty variable.
Moreover, by default rules for word splitting, entering Space would also result into an empty variable. The good news is that you could handle this part by setting IFS.
Change your read statement to:
IFS= read -s -n 1 SELECT
and expect a null string instead of $'\x0a' when entering Enter.
I have an old shell script which needs to be moved to bash. This script prints progress of some activity and waits for user's commands. If no action is taken by user for 15 seconds screen is redrawn with new progress and timer starts again. Here's my problem:
I am trying to use read -t 15 myVar - this way after 15 seconds of waiting loop will be restarted. There is however a scenario which brings me a problem:
screen redrawn and script waits for input (prints 'Enter command:')
user enters foo but doesn't press enter
after 15 seconds screen is again redrawn and script waits for input - note, that foo is not displayed anywhere on the screen (prints 'Enter command:')
user enters bar and presses enter
At this moment variable $myVar holds 'foobar'.
What do I need? I am looking for a way to find the first string typed by user, so I could redisplay it after refreshing status. This way user will see:
Enter command: foo
On Solaris I could use stty -pendin to save input into some sort of a buffer, and after refresh run stty pendin to get this input from buffer and print it on a screen.
Is there a Linux equivalent to stty pendin feature? Or maybe you know some bash solution to my problem?
I guess one way would be to manually accumulate the user input ... use read with -n1 so that it returns after every character, then you can add it to your string. To prevent excessive drawing you would have to calculate how many remaining seconds are on the 15 second clock ...
Addendum:
For your comment about space/return - you basically want to use an IFS without the characters you want, such as space
example:
XIFS="${IFS}" # backup IFS
IFS=$'\r' # use a character your user is not likely to enter (or just the same but w/o the space)
# Enter will look like en empty string
$ read -n1 X; echo --; echo -n "${X}" | od -tx1
--
0000000
# space will be presented as a character
$ read -n1 X; echo --; echo -n "${X}" | od -tx1
--
0000000 20
0000001
# after you are all done, you probably wantto restore the IFS
IFS="${XIFS}"
Expanding on what #nhed was saying, perhaps something like this:
#!/bin/bash
limit=5
draw_screen () {
clear;
echo "Elapsed time: $SECONDS" # simulate progress indicator
printf 'Enter command: %s' "$1"
}
increment=$limit
str=
end=0
while ! [ $end -eq 1 ] ; do
draw_screen "$str"
while [ $SECONDS -lt $limit ] && [ $end -eq 0 ] ; do
c=
IFS= read -t $limit -r -n 1 -d '' c
if [ "$c" = $'\n' ] ; then
end=1
fi
str="${str}${c}"
done
let limit+=increment
done
str="${str%$'\n'}" # strip trailing newline
echo "input was: '$str'"
The solution is not ideal:
You can sometimes be typing in the middle of the loop and mess up input
You can't edit anything nicely (but this is fixable with a lot more work)
But maybe it's enough for you.
If you have Bash 4:
read -p 'Enter something here: ' -r -t 15 -e -i "$myVar" myVar
The -e turns on readline support for the user's text entry. The -i uses the following text as the default contents of the input buffer which it displays to the user. The following text in this case is the previous contents of the variable you're reading into.
Demonstration:
$ myVar='this is some text' # simulate previous entry
$ read -p 'Enter something here: ' -r -t 15 -e -i "$myVar" myVar
Enter something here: this is some text[]
Where [] represents the cursor. The user will be able to backspace and correct the previous text, if needed.
OK, I think I have the solution. I took nhed's proposition and worked a bit on it :)
The main code prints some status and waits for input:
while :
do
# print the progress on the screen
echo -n "Enter command: "
tput sc # save the cursor position
echo -n "$tmpBuffer" # this is buffer which holds typed text (no 'ENTER' key yet)
waitForUserInput
read arguments <<< $(echo $mainBuffer) # this buffer is set when user presses 'ENTER'
mainBuffer="" # don't forget to clear it after reading
# now do the action requested in $arguments
done
Function waitForUserInput waits 10 seconds for a keypress. If nothing typed - exits, but already entered keys are saved in a buffer. If key is pressed, it is parsed (added to buffer, or removed from buffer in case of backspace). On Enter buffer is saved to another buffer, from which it is read for further processing:
function waitForUserInput {
saveIFS=$IFS # save current IFS
IFS="" # change IFS to empty string, so 'ENTER' key can be read
while :
do
read -t10 -n1 char
if (( $? == 0 ))
then
# user pressed something, so parse it
case $char in
$'\b')
# remove last char from string with sed or perl
# move cursor to saved position with 'tput rc'
echo -n "$tmpBuffer"
;;
"")
# empty string is 'ENTER'
# so copy tmpBuffer to mainBuffer
# clear tmpBuffer and return to main loop
IFS=$saveIFS
return 0
;;
*)
# any other char - add it to buffer
tmpBuffer=$tmpBuffer$char
;;
esac
else
# timeout occured, so return to main function
IFS=$saveIFS
return 1
fi
done
}
Thanks to all of you for your help!
I have bash script like the following:
#!/bin/bash
echo "Please enter your username";
read username;
echo "Please enter your password";
read password;
I want that when the user types the password on the terminal, it should not be displayed (or something like *******) should be displayed). How do I achieve this?
Just supply -s to your read call like so:
$ read -s PASSWORD
$ echo $PASSWORD
Update
In case you want to get fancy by outputting an * for each character they type, you can do something like this (using andreas' read -s solution):
unset password;
while IFS= read -r -s -n1 pass; do
if [[ -z $pass ]]; then
echo
break
else
echo -n '*'
password+=$pass
fi
done
Without being fancy
echo "Please enter your username";
read username;
echo "Please enter your password";
stty -echo
read password;
stty echo
you can use stty to disable echo
this solution works without bash or certain features from read
stty_orig=$(stty -g)
stty -echo
read password
stty $stty_orig
If you use this in a shell script then also set an exit handler which restores echo:
#! /bin/sh
stty_orig=$(stty -g)
trap "stty ${stty_orig}" EXIT
stty -echo
...
this is to make sure echo is restored regardless of how the script exits. otherwise the echo will stay off if the script errors out.
to turn echo back on manually type the following command
stty echo
you will have to type blindly because you do not see what you type.
i suggest to press ctrl+c first to clear anything else you might have typed before.
trivia
echo means to echo your typed input back to your screen.
this is from the time we worked on teletypewriters (that is what the tty means). a teletypewriter is like a typewriter but connected to another teletypewriter or computer. typically via telephone cable.
the workflow on a teletypewriter is roughly as follows: you type in your command (or message for the other side). then the teletypewriter will print the response from the other side.
when you work on a teletypewriter you see your input as you type. this is because the teletypewriter is also a typewriter and as such prints the characters as you press them.
when teletypewriters where replaced by screens there was no longer a typewriter which types your input. instead we had to deliberate code an "echo" function which prints your input as you type.
i do not know whether stty -echo also disabled printing on a teletypewriter.
see here for a teletypewriter in action: https://www.youtube.com/watch?v=2XLZ4Z8LpEE (first part is restoration. action starting at about 12 minutes in)
more teletypewriter restoration: https://www.youtube.com/playlist?list=PL-_93BVApb5-9eQLTCk9xx16RAGEYHH1q
Here's a variation on #SiegeX's excellent *-printing solution for bash with support for backspace added; this allows the user to correct their entry with the backspace key (delete key on a Mac), as is typically supported by password prompts:
#!/usr/bin/env bash
password=''
while IFS= read -r -s -n1 char; do
[[ -z $char ]] && { printf '\n' >/dev/tty; break; } # ENTER pressed; output \n and break.
if [[ $char == $'\x7f' ]]; then # backspace was pressed
# Remove last char from output variable.
[[ -n $password ]] && password=${password%?}
# Erase '*' to the left.
printf '\b \b' >/dev/tty
else
# Add typed char to output variable.
password+=$char
# Print '*' in its stead.
printf '*' >/dev/tty
fi
done
Note:
As for why pressing backspace records character code 0x7f: "In modern systems, the backspace key is often mapped to the delete character (0x7f in ASCII or Unicode)" https://en.wikipedia.org/wiki/Backspace
\b \b is needed to give the appearance of deleting the character to the left; just using \b moves the cursor to the left, but leaves the character intact (nondestructive backspace). By printing a space and moving back again, the character appears to have been erased (thanks, The "backspace" escape character '\b': unexpected behavior?).
In a POSIX-only shell (e.g., sh on Debian and Ubuntu, where sh is dash), use the stty -echo approach (which is suboptimal, because it prints nothing), because the read builtin will not support the -s and -n options.
A bit different from (but mostly like) #lesmana's answer
stty -echo
read password
stty echo
simply: hide echo
do your stuff
show echo
I always like to use Ansi escape characters:
echo -e "Enter your password: \x1B[8m"
echo -e "\x1B[0m"
8m makes text invisible and 0m resets text to "normal." The -e makes Ansi escapes possible.
The only caveat is that you can still copy and paste the text that is there, so you probably shouldn't use this if you really want security.
It just lets people not look at your passwords when you type them in. Just don't leave your computer on afterwards. :)
NOTE:
The above is platform independent as long as it supports Ansi escape sequences.
However, for another Unix solution, you could simply tell read to not echo the characters...
printf "password: "
let pass $(read -s)
printf "\nhey everyone, the password the user just entered is $pass\n"
Here is a variation of #SiegeX's answer which works with traditional Bourne shell (which has no support for += assignments).
password=''
while IFS= read -r -s -n1 pass; do
if [ -z "$pass" ]; then
echo
break
else
printf '*'
password="$password$pass"
fi
done
Get Username and password
Make it more clear to read but put it on a better position over the screen
#!/bin/bash
clear
echo
echo
echo
counter=0
unset username
prompt=" Enter Username:"
while IFS= read -p "$prompt" -r -s -n 1 char
do
if [[ $char == $'\0' ]]; then
break
elif [ $char == $'\x08' ] && [ $counter -gt 0 ]; then
prompt=$'\b \b'
username="${username%?}"
counter=$((counter-1))
elif [ $char == $'\x08' ] && [ $counter -lt 1 ]; then
prompt=''
continue
else
counter=$((counter+1))
prompt="$char"
username+="$char"
fi
done
echo
unset password
prompt=" Enter Password:"
while IFS= read -p "$prompt" -r -s -n 1 char
do
if [[ $char == $'\0' ]]; then
break
elif [ $char == $'\x08' ] && [ $counter -gt 0 ]; then
prompt=$'\b \b'
password="${password%?}"
counter=$((counter-1))
elif [ $char == $'\x08' ] && [ $counter -lt 1 ]; then
echo
prompt=" Enter Password:"
continue
else
counter=$((counter+1))
prompt='*'
password+="$char"
fi
done
A variation on both #SiegeX and #mklement0's excellent contributions: mask user input; handle backspacing; but only backspace for the length of what the user has input (so we're not wiping out other characters on the same line) and handle control characters, etc... This solution was found here after so much digging!
#!/bin/bash
#
# Read and echo a password, echoing responsive 'stars' for input characters
# Also handles: backspaces, deleted and ^U (kill-line) control-chars
#
unset PWORD
PWORD=
echo -n 'password: ' 1>&2
while true; do
IFS= read -r -N1 -s char
# Note a NULL will return a empty string
# Convert users key press to hexadecimal character code
code=$(printf '%02x' "'$char") # EOL (empty char) -> 00
case "$code" in
''|0a|0d) break ;; # Exit EOF, Linefeed or Return
08|7f) # backspace or delete
if [ -n "$PWORD" ]; then
PWORD="$( echo "$PWORD" | sed 's/.$//' )"
echo -n $'\b \b' 1>&2
fi
;;
15) # ^U or kill line
echo -n "$PWORD" | sed 's/./\cH \cH/g' >&2
PWORD=''
;;
[01]?) ;; # Ignore ALL other control characters
*) PWORD="$PWORD$char"
echo -n '*' 1>&2
;;
esac
done
echo
echo $PWORD