How to use new line character in bash? - linux

I am trying to make a loop that will loop until the escape key is pressed but I have a struggle with printing the read message because if any key, except the enter key, it will continue to print in one line
Here is my code:
while : ; do
read -n1 -r -p "Press esc key to continue...\n" key
[[ $key != $'\e' ]] || break
done
It outputs Press esc key to continue...\n

You can use $'...' string like you're using for detecting escape character already:
while : ; do
read -n1 -r -p $'Press esc key to continue...\n' key
[[ $key != $'\e' ]] || break
done
As per man bash:
Words of the form $'string' are treated specially. The word expands to string, with backslash-escaped characters replaced as specified by the ANSI C
standard. Backslash escape sequences are \n, \t, \e etc.

I'd restructure just a little bit!
1.) The input message ("Press esc key to continue...") shouldn't have an embedded newline. This newline, I'm assuming, should appear after the user presses escape.
1a.) We also don't want user input popping up on the screen, so read -s (silently) after echoing -n (no newline) your input prompt.
Original:
read -n1 -r -p "Press esc key to continue...\n" key
Modified:
echo -n "Press esc key to continue..."; read -s -n1 -r key
2.) Now, when the user presses ESC, I believe you'd like to initiate a newline & exit the loop, so just echo AND break on the ESC key press. Boom.
2a.) On the issue of everything BUT an escape re-printing the message...Well, let's do a carriage return if the user DOESN'T hit escape, that way the message is printed in the same spot instead of on a new line!
Original:
[[ $key != $'\e' ]] || break
Modified:
if [[ $key == $'\e' ]];then echo;break; else echo -ne "\033[0K\r"; fi
Finished version:
while : ; do
echo -n "Press esc key to continue..."; read -s -n1 -r key
if [[ $key == $'\e' ]];then echo;break; else echo -ne "\033[0K\r"; fi
done
Testing it (a bunch of random NON-ESCAPE keys):
Let me know if that works for you!

Related

Count number of specific characters in input string

Im trying to count the number of letters, numbers and special characters in an input string
The user would type the string and then finish with a * to finish the program should then display a count for the number of letters numbers and special characters
So far i have this but i get errors on line 21 which i think is the else statement
The exact error message i get is "./masher3: line 21: 0: command not found"
#!/bin/bash
numcount=0
charcount=0
othercount=0
echo "Input string"
for char in $#
do
if [[ $char == "*" ]]
then
break
elif [[ $char == '0-9' ]]
then
$numcount = $numcount + 1
elif [[ $char == 'A-Z' ]]
then
$charcount = $charcount + 1
else
$othercount = $othercount + 1 <----- Error on this line
fi
done
echo $charcount
This program is written in pure bash (without calling any external programs).
Also look below the code – I added some more information.
#!/bin/bash
# Print the message without going to next line (-n)
echo -n "Your input string: "
# Read text from standard input. ‘-d '*'’ stops
# reading at first ‘*’ character. Remove it to
# terminate on press of key Enter.
# Result is stored in variable $input.
#
# -e enables backspace and other keys.
read -ed '*' input
# Jump to next line
echo
# Fill all counters with zeros
letters=0
digits=0
spaces=0
others=0
# While $input contains some text…
while [[ -n "$input" ]]
do
# Get the first character
char="${input:0:1}"
# Take everything from $input except
# the first character and store it again
# in $input
input="${input:1}"
# Is the character space?
if [[ "$char" == " " ]]
then
# Increase the $spaces variable by one
((spaces++))
# Else: If the $char after removal of all
# letters in (english) alphabet is empty string?
# That will be true when the $char is letter.
elif [[ -z "${char//[abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]/}" ]]
then
# Increase $letters
((letters++))
# Else: If the $char …
# Just the same for digits
elif [[ -z "${char//[0123456789]/}" ]]
then
((digits++))
# Else increase the $others variable
else
((others++))
fi
done
# Show values
echo "Letters: $letters"
echo "Digits: $digits"
echo "Spaces: $spaces"
echo "Other characters: $others"
Also open/download the Bash Reference Manual (available as single page, plaintext, PDF). You probably have one copy already installed if you use Linux. Try commands info bash (usually shows hypertext browser if installed) or man bash (single page documentation but usually the same). It is sometimes hard to understand for beginners but you will learn more information about this programming language.
Bash has many builtin commands (such as read, [[, echo, printf etc.) that work like ordinary commands. Their help is in the Reference Manual or can be shown by typing help command_name in your bash shell.
See my other answer for solution.
Your program is quite weird.
Assignment into variable looks like variable=42 (not $variable = 42)
You have to use $((…)) syntax to perform calculations
[[ $char == '0-9' ]] means “When the character is exactly 0-9
$char contains separate arguments of the program, not characters in input.
$othercount = … means “run command with name specified in variable $othercount with arguments = and ….
Assigning into variable in bash
You have to NOT write $ before variable name when you want to assign to it and you have to have NO whitespace before =:
my_variable=42
variable_2=$(($my_variable + 8))
echo $my_variable # Prints “50”

Bash, how to check for control character(non-printable character) in a variable?

I have a Bash statement to get user input(a single character) into tmpchar :
read -n 1 -t 1 tmpchar
and I can check for printable character input like this:
if [ "$tmpchar" = "n" ] || [ "$tmpchar" = "N" ]; then
# do something...
fi
Now my question is: If user input just a Return, or ESC, or Ctrl+a, Ctrl+b etc, how do I check for them?
ENV: openSUSE 12.3 , Bash 4.2.42(1)-release
Maybe you're looking for ANSI-C quoting. E.g., Ctrl-a is represented as $'\ca'.
Use the regex match operator =~ inside of [[ ... ]]:
if [[ $tmpchar =~ [[:cntrl:]] ]]; then
# It's a control character
else
# It's not a control character
fi
Note that read -n1 won't do what you expect for a variety of special characters. At a minimum, you should use:
IFS= read -r -n1
Even with that, you'll never see a newline character: if you type a newline, read will set the reply variable to an empty string.
If you want to know if a character isn't a member of the set of printable characters, use a complementary set expression. This seems to work fine with case:
for c in $'\x20' $'\x19'; do
case "$c" in
[[:print:]]) echo printable;;
[^[:print:]]) echo 'not printable';;
*) echo 'more than one character?';;
esac
done
(outputs printable and then non printable)
for c in $'\x20' $'\x19'; do
if [[ $c = [[:print:]] ]]; then
echo printable
fi
if [[ $c = [^[:print:]] ]]; then
echo not printable
fi
done
works as well. If you want to know what characters sets your system supports, look at man 7 regex on linux or man 7 re_format on OS X.
You can filter the input with tr:
read -n 1 -t 1 tmpchar
clean=$(tr -cd '[:print:]' <<< $tmpchar)
if [ -z "$clean"]; then
echo "No printable"
else
echo "$clean"
fi
I find a trick to check for a sole Return input.
if [ "$tmpchar" = "$(echo -e '')" ]; then
echo "You just pressed Return."
fi
In other word, the highly expected way by #ooga,
if [ "$tmpchar" = $'\x0a' ]; then
echo "You just pressed Return." # Oops!
fi
does not work for Return anyhow, hard to explain.

bash scripting - read single keystroke including special keys enter and space

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.

string select and append to another string

I have a file (FreshPIN.txt) contain lots of pin code in each line; I need a bash script to select one of the pin, print it out, and then remove it from the source file, adding it to end of another file (usedPIN.txt).
FreshpPIN.txt is like:
========
1111111111111111
2222222222222222
3333333333333333
....
nnnnnnnnnnnnnnnn
========
before it prints, I should be asked to enter a number from 0 to 31 and put the number in the command below:
at&g**00**=xtd*788*1111111111111111#
in above example at&g and =xtd*788* should be stable in all output commands.
fresh=FreshPIN.txt
used=usedPin.txt
echo "Please key in"
read key
pin=`head -1 "$fresh"`
printf '%s\n' "$pin" >>"$used"
sed -i~ 1d "$fresh"
printf 'at&g%s=xtd*788*%s\n' "$key" "$pin"
How about this?
#!/bin/bash
fresh=FreshpPIN.txt
used=usedPIN.txt
max=31
die() {
echo >&2 "$#"
exit 1
}
# Get a random pin
pin=$(sed -n '/[[:digit:]]\+/p' -- "$fresh" | shuf -n1)
[[ "$pin" ]] || die "No more pins in file \`$fresh'"
echo "Pin chosen: $pin"
# Prompt user:
while read -e -r -p "Enter a number between 0 and $max (q to quit): " n; do
if [[ "$n" = q ]]; then
echo "Aborting. Pin $pin remains in file \`$fresh'."
exit 0
elif [[ "$n" != +([[:digit:]]) ]]; then
echo "Not a valid number. Try again."
elif ((10#$n>max)); then
echo "Number must be between 0 and $max. Try again."
else
break
fi
done
# Guard if read fails (e.g., if user presses Ctrl-D)
[[ "$n" ]] || die "Something went wrong."
# Delete this pin from file
ed -s -- "$fresh" <<EOF
/^$pin\$/d
wq
EOF
# Save pin in file
printf >> "$used" "%s\n" "$pin"
# Output:
printf "at&g**%02d**=xtd*788*%s\n" "$((10#$n))" "$pin"
It's quite robust (the user must really enter a number between 0 and 31, and it won't be messed up if user enters, e.g., 09). Uses ed to delete old pin from file FreshpPIN.txt: very efficient (no auxiliary file or ugly stuff using sed -i). Uses good bash practice overall. Uses shuf to get a random pin (don't need to compute the number of lines and hack ugly stuff around to get a random pin). sed is used to select only pins from file FreshpPIN.txt, so you can leave your header, comment, etc. in there.

Hiding user input on terminal in Linux script

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

Resources