Is it possible to store something - for example user input - into a variable in bash? I have a script that reads a password...
#!/bin/bash
while [ $# -gt 0 ]; do
key="$1";
case $key in
-p|--prompt)
if [ ! "$2" ]; then
printf "\e31;1mMust pass in a prompt\e[0m\n";
exit 1;
fi
printf "$2";
shift 2;
;;
esac
done
psswd=;
c=;
while [ "$c" != "^M" ]; do
read -n 1 -s c;
if [ "$c" == "^?" ]; then
if [ ${#psswd} -gt 0 ]; then
psswd=${psswd:: -1};
printf "\b \b";
fi
elif [ ! "$c" ]; then # pressed enter
break;
else
psswd="$psswd$c";
printf "*";
fi
done;
printf "\nYour entered password is: $psswd\n"
(sorry, ^M is newline and ^? is backspace. Meaning you can't copy and paste, but that is how it shows up in vim, and I don't know how else to write it.)
... and I would like the password to be strored into a variable. You know how read stores a variable. If read can do it, why can't I? Does anyone have any ideas?
(Or perhaps there is a better way to store the password... like echo "$psswd" | base64 > .pass - which isn't really what I want to do.)
UPDATE
Basically, how do I get my psswd into a variable outside the shell? OR how do I access the password once that shell is closed? Is there a better way to store it other than a variable?
As rojomoke suggests, this is enough:
read -p "Enter your password: " -s pw
echo "You entered: $pw"
If you do this: read -n 1 -s c and you just hit Enter, then $c will be empty, it will not hold \r; and if you hit backspace, in my testing, $c holds a character with octal value 177
read -n 1 -s c
printf "%s" "$c" | od -c
Related
I have a flat file called items that I want to populate a select but I want to be able to choose multiple items at one time.
contents of items file:
cat 1
dog 1
pig 1
cherry 2
apple 2
Basic script:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
select choice in $options
do
echo "you selected: $choice"
done
exit 0
The way it flows now is I can only select one option at at time. I'd like to be able to answer 1,3 or 1 3 and have it respond "you selected: cat pig"
Thank you,
Tazmarine
I can offer a somewhat different approach that uses a different selection prompt style. Here's a bash function that allows user to select multiple options with arrow keys and Space, and confirm with Enter. It has a nice menu-like feel. I wrote it with the help of https://unix.stackexchange.com/a/415155. It can be called like this:
multiselect result "Option 1;Option 2;Option 3" "true;;true"
The result is stored as an array in a variable with the name supplied as the first argument. Last argument is optional and is used for making some options selected by default. It looks like this:
function prompt_for_multiselect {
# little helpers for terminal print control and key input
ESC=$( printf "\033")
cursor_blink_on() { printf "$ESC[?25h"; }
cursor_blink_off() { printf "$ESC[?25l"; }
cursor_to() { printf "$ESC[$1;${2:-1}H"; }
print_inactive() { printf "$2 $1 "; }
print_active() { printf "$2 $ESC[7m $1 $ESC[27m"; }
get_cursor_row() { IFS=';' read -sdR -p $'\E[6n' ROW COL; echo ${ROW#*[}; }
key_input() {
local key
IFS= read -rsn1 key 2>/dev/null >&2
if [[ $key = "" ]]; then echo enter; fi;
if [[ $key = $'\x20' ]]; then echo space; fi;
if [[ $key = $'\x1b' ]]; then
read -rsn2 key
if [[ $key = [A ]]; then echo up; fi;
if [[ $key = [B ]]; then echo down; fi;
fi
}
toggle_option() {
local arr_name=$1
eval "local arr=(\"\${${arr_name}[#]}\")"
local option=$2
if [[ ${arr[option]} == true ]]; then
arr[option]=
else
arr[option]=true
fi
eval $arr_name='("${arr[#]}")'
}
local retval=$1
local options
local defaults
IFS=';' read -r -a options <<< "$2"
if [[ -z $3 ]]; then
defaults=()
else
IFS=';' read -r -a defaults <<< "$3"
fi
local selected=()
for ((i=0; i<${#options[#]}; i++)); do
selected+=("${defaults[i]}")
printf "\n"
done
# determine current screen position for overwriting the options
local lastrow=`get_cursor_row`
local startrow=$(($lastrow - ${#options[#]}))
# ensure cursor and input echoing back on upon a ctrl+c during read -s
trap "cursor_blink_on; stty echo; printf '\n'; exit" 2
cursor_blink_off
local active=0
while true; do
# print options by overwriting the last lines
local idx=0
for option in "${options[#]}"; do
local prefix="[ ]"
if [[ ${selected[idx]} == true ]]; then
prefix="[x]"
fi
cursor_to $(($startrow + $idx))
if [ $idx -eq $active ]; then
print_active "$option" "$prefix"
else
print_inactive "$option" "$prefix"
fi
((idx++))
done
# user key control
case `key_input` in
space) toggle_option selected $active;;
enter) break;;
up) ((active--));
if [ $active -lt 0 ]; then active=$((${#options[#]} - 1)); fi;;
down) ((active++));
if [ $active -ge ${#options[#]} ]; then active=0; fi;;
esac
done
# cursor position back to normal
cursor_to $lastrow
printf "\n"
cursor_blink_on
eval $retval='("${selected[#]}")'
}
You can not do that as such, but you can always record each individual selection:
#!/bin/bash
PS3=$'\n\nSelect the animals you like: '
options=$(grep '1' items|grep -v '^#' |awk '{ print $1 }')
# Array for storing the user's choices
choices=()
select choice in $options Finished
do
# Stop choosing on this option
[[ $choice = Finished ]] && break
# Append the choice to the array
choices+=( "$choice" )
echo "$choice, got it. Any others?"
done
# Write out each choice
printf "You selected the following: "
for choice in "${choices[#]}"
do
printf "%s " "$choice"
done
printf '\n'
exit 0
Here's an example interaction:
$ ./myscript
1) cat
2) dog
3) pig
4) Finished
Select the animals you like: 3
pig, got it. Any others?
Select the animals you like: 1
cat, got it. Any others?
Select the animals you like: 4
You selected the following: pig cat
If you instead want to be able to write 3 1 on the same line, you'll have to make your own menu loop with echo and read
This is what I came up with. This seems to works as I want. I want the final output to be comma separated:
#!/bin/bash
newarray=(all $(grep '1' items|grep -v '^#' |awk '{ print $1 }'))
options() {
num=0
for i in ${newarray[#]}; do
echo "$num) $i"
((num++))
done
}
getanimal() {
while [[ "$show_clean" =~ [A-Za-z] || -z "$show_clean" ]]; do
echo "What animal do you like: "
options
read -p 'Enter number: ' show
echo
show_clean=$(echo $show|sed 's/[,.:;]/ /g')
selected=$(\
for s in $show_clean; do
echo -n "\${newarray[${s}]},"
done)
selected_clean=$(echo $selected|sed 's/,$//')
done
eval echo "You selected $selected_clean"
}
getanimal
exit 0
I have a password function that I borrowed from How do I echo stars (*) when reading password with read?
I tried to adapt it so that I can run through the function twice to do a password confirmation and then evaluate the 2 passwords to determine if they match but I seem to be missing some basics of how bash works in this case.
I tried replacing PASSWORD with $1 but kept getting command not found errors
passWord() {
unset PASSWORD
unset CHARCOUNT
stty -echo
CHARCOUNT=0
while IFS= read -p "$PROMPT" -r -s -n 1 CHAR; do
# Enter - accept password
if [[ $CHAR == $'\0' ]] ; then
break
fi
# Backspace
if [[ $CHAR == $'\177' ]] ; then
if [ $CHARCOUNT -gt 0 ] ; then
CHARCOUNT=$((CHARCOUNT-1))
PROMPT=$'\b \b'
PASSWORD="${PASSWORD%?}"
else
PROMPT=''
fi
else
CHARCOUNT=$((CHARCOUNT+1))
PROMPT='*'
PASSWORD+="$CHAR"
fi
done
stty echo; echo
${1}=${PASSWORD}
}
echo -n "Enter the password > "
passWord passOne
echo -n "Please re-enter the password > "
passWord passTwo
if [[ $passOne == $passTwo ]]; then
PASSWORD=$passOne
else
echo "Passwords did not match, please try again."
fi
Update
Here is the script with the latest updates
#!/bin/bash
passWord() {
unset password
local prompt char
stty -echo
charcount=0
while IFS= read -p "$prompt" -r -s -n 1 CHAR; do
# Enter - accept password
if [[ $char == $'\0' ]] ; then
break
fi
# Backspace
if [[ $char == $'\177' ]] ; then
if [ $charcount -gt 0 ] ; then
charcount=$((CHARCOUNT-1))
prompt=$'\b \b'
password="${password%?}"
else
prompt=''
fi
else
charcount=$((charcount+1))
prompt='*'
password+="$char"
fi
done
stty echo; echo
}
echo -n "Enter the password > "
passWord
pass1=$password
echo -n "Please re-enter the password > "
passWord
pass2=$password
if [[ "$pass1" == "$pass2" ]]; then
PassWord=$pass1
else
echo "Passwords did not match, please try again."
fi
You are missing a declaration of your shell.
Please add a shebang as the first line:
#!/bin/bash
The assignment of variables (the line ${1}=${PASSWORD}) doesn't work.
One way to solve it (not recomended) is to add eval:
eval "${1}=${PASSWORD}" # don't use quite risky.
But as that makes any input a security issue, you should use some other line.
One solution is to use declare (bash 4.2+):
declare -g "${1}=${PASSWORD}"
The -g is required (required and available since bash 4.2) to change General variables (not local to the function).
Or use printf (since bash 3.1):
printf -v "${1}" '%s' "${PASSWORD}"
Other than that, you should add a local command for variables used inside the function to avoid conflicts with external variables and should add a PROMPT='' just before the loop to avoid the printing of an initial asterisk when calling the function a second time.
It should be said that using variables in CAPS should be avoided. Variables in CAPS denote environment variables, the rest of variables use lower case to avoid conflicts.
Latest Version
#!/bin/bash
set -e
shopt -s nocasematch
#vars
redbgbold='\e[1;97;41m'
resetcolor='\e[0m'
RegExFQDN='(?=^.{4,253}$)(^((?!-)[a-zA-Z0-9-]{1,63}(?<!-)\.)+[a-zA-Z]{2,63}$)'
#functions
ask() {
local input
until
read -rp "$1 > " input >&2 || return 1
grep -q -P "$2" <<< "$input"
do
printf "ERROR - "${redbgbold}"\"$input\""${resetcolor}" is not a valid " >&2; sed "s/.*the //" <<< "$1" >&2
done
printf '%s\n' "$input"
}
#code
while [ -z $fqdn ]; do
fqdn=$(ask "Enter the FQDN" $RegExFQDN)
echo "FQDN is $fqdn"
done
The Question
I have a read line, and I want to take what the user entered and see if it matches my regex, if it matches we leave the loop, if it fails it prints an error and we do the loop again until we get a match. It looks redundant to me, and I assume there should be a better way but not sure what that should be.
Original Code
#!/bin/bash
set -e
shopt -s nocasematch
function RegexValidation() {
if [ "$2" = "fqdn" ]; then
if [ `echo $1 | grep -c -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'` == "0" ]; then
echo "ERROR - $1 is not a valid FQDN"
unset $!{1}
fi
fi
}
while [ -z $fqdn ]; do
read -e -r -p "Enter the Fully Qualified Domain Name > " fqdn
RegexValidation $fqdn fqdn
done
shopt -u nocasematch
any help is appreciated.
Update #1 - fixed formatting issues.
Update #2 - using that other guy's suggestions with a few additional tweaks
I would do basically the same thing, but split it differently to make it easier to reuse:
#!/bin/bash
set -e
ask() {
local input
until
read -rp "$1 > " input >&2 || return 1
grep -q -P "$2" <<< "$input"
do
echo "Invalid answer. Try again" >&2
done
printf '%s\n' "$input"
}
ask_fqdn() {
ask "$1" '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'
}
fqdn=$(ask_fqdn "Enter first FQDN")
echo "You wrote $fqdn"
fqdn=$(ask_fqdn "Enter second FQDN")
echo "This time it was $fqdn"
number=$(ask "And now a number because why not" '^\d+$')
echo "So $number"
Now you don't have to write a new loop every time you want new information, and you can easily ask for new things without modifying the existing functions.
Have the function return a status, which you can test with if in the loop.
And rather than use test to check the result of grep, just test it directly with if. grep returns a non-zero status if the input doesn't match.
function RegexValidation() {
if [ "$2" = "fqdn" ]; then
if ! echo "$1" | grep -q -P '(?=^.{1,254}$)(^(?>(?!\d+\.)[a-z0-9_\-]{1,63}\.?)+(?:[a-z]{2,})$)'; then
echo "ERROR - $1 is not a valid FQDN"
return 1
fi
return 0
fi
}
while :; do
read -e -r -p "Enter the Fully Qualified Domain Name > " fqdn
if RegexValidation "$fqdn" fqdn
then break
fi
done
Also, remember to quote your variables.
I've been stuck for quite some time with the following code. It works but the variable loses the value that was set during the iterations.
I have the following code
mistakes=0
entered_chars=()
word_length=0
answer=""
answer_guess=""
checkIfLetterInsideWord(){
exists=0
letter=$2
word_array=`echo $1 | grep -o . `;
for (( i=1; i <= $word_length; i++))
do
if [[ "${1:$i-1:1}" = ${letter} ]]; then
exists=1
answer_guess=$(echo $answer_guess | sed "s/-/${letter}/{i}" )
fi
done
echo $exists
}
askUserInput(){
answer=$answer
echo $answer
echo "Please type a letter"
read user_input
if [ ! -z $user_input ]; then
user_input=$(echo $user_input | tr '[:upper:]' '[:lower:]')
if [ $(checkIfAlreadyEntered "$user_input") -eq 0 ]; then
if [ $(checkIfLetterInsideWord $answer $user_input) -eq 0 ]; then
mistakes=$((mistakes + 1)); fi
echo "Current mistake count; $mistakes "
entered_chars+=($user_input)
else
echo "Char has already been entered"
fi
else
echo "You haven't entered any input!"
fi
}
guessTheWord() {
answer=$OPTARG
word_length=$(printf $answer | wc -m)
temp=$(echo $answer | sed 's/\(.\)/\1 /g')
array=($temp)
echo "The chosen word is $word_length long"
gameOngoing=true
for(( i=1; i<=$word_length; i++)) do
answer_guess="$answer_guess-"
done
while $gameOngoing
do
echo $answer_guess
askUserInput $answer
done
}
I want to preserve the value of the variable answer_guess. I understand that it loses the value because of the usage of a pipeline inside the loop but I don't know to approach this problem.
The problem has nothing do to with the pipe. Rather, it is that you call checkIfLetterInsideWord inside a command-substitution ($(...)). Command substitution executes in a subshell so environment changes in the function will not persist.
It would be better to rewrite checkIfLetterInsideWord so that it returns an exit status. Something like:
if [[ $exists ]]; then
return 0 # Success
else
return 1 # Failure
end
Then you could simply call it without worrying about a subshell:
if checkIfLetterInsideWord "$answer" "$user_input"; then
# letter is in word
else
# letter is not in word
fi
There are other issues with the code. I've limited this answer to the question about preserving the value of variables.
answer_guess=$(echo $answer_guess | sed "s/-/${letter}/{i}" )
replace the - with .
so your code becomes
answer_guess=$(echo $answer_guess | sed "s/./${letter}/{i}" )
What do I need to do for code in Bash, if I want to echo *s in place of password characters (or even just hide the characters completely) when the user types something in using read?
As Mark Rushakoff pointed out, read -s will suppress the echoing of characters typed at the prompt. You can make use of that feature as part of this script to echo asterisks for each character typed:
#!/bin/bash
unset password
prompt="Enter Password:"
while IFS= read -p "$prompt" -r -s -n 1 char
do
if [[ $char == $'\0' ]]
then
break
fi
prompt='*'
password+="$char"
done
echo
echo "Done. Password=$password"
I really liked the answer that Wirone gave, but I didn't like that the backspacing would continue removing characters even back into the "Enter password: " prompt.
I also had some issues where pressing keys too rapidly would cause some of the characters to actually print on the screen... never a good thing when prompting for a password. =)
The following is my modified version of Wirone's answer which addresses these issues:
#!/bin/bash
unset PASSWORD
unset CHARCOUNT
echo -n "Enter password: "
stty -echo
CHARCOUNT=0
while IFS= read -p "$PROMPT" -r -s -n 1 CHAR
do
# Enter - accept password
if [[ $CHAR == $'\0' ]] ; then
break
fi
# Backspace
if [[ $CHAR == $'\177' ]] ; then
if [ $CHARCOUNT -gt 0 ] ; then
CHARCOUNT=$((CHARCOUNT-1))
PROMPT=$'\b \b'
PASSWORD="${PASSWORD%?}"
else
PROMPT=''
fi
else
CHARCOUNT=$((CHARCOUNT+1))
PROMPT='*'
PASSWORD+="$CHAR"
fi
done
stty echo
echo $PASSWORD
read -s should put it in silent mode:
-s Silent mode. If input is coming from a terminal, characters are not echoed.
See the read section in man bash.
I would like to add something to Dennis Williamson's solution:
#!/bin/bash
unset password
echo -n "Enter password: "
while IFS= read -p "$prompt" -r -s -n 1 char
do
# Enter - accept password
if [[ $char == $'\0' ]] ; then
break
fi
# Backspace
if [[ $char == $'\177' ]] ; then
prompt=$'\b \b'
password="${password%?}"
else
prompt='*'
password+="$char"
fi
done
In above example script handles backspace correctly.
Source
I don't know about stars, but stty -echo is your friend:
#!/bin/sh
read -p "Username: " uname
stty -echo
read -p "Password: " passw; echo
stty echo
Source: http://www.peterbe.com/plog/passwords-with-bash
If you don't care about it being interactive, you can simply do
read -s pass
echo "$pass" | sed 's/./*/g'
This will show a * for each character of the entered password after enter is pressed.
stty -echo
read something
stty echo
will stop user input being echoed to the screen for that read. Depending on what you are doing with prompts, you may want to add an extra echo command to generate a newline after the read.
I just made this Bash-specific function based on Dennis Williamson's, Wirone's and Logan VanCuren's answers:
ask() {
local 'args' 'char' 'charcount' 'prompt' 'reply' 'silent'
# Basic arguments parsing
while [[ "${1++}" ]]; do
case "${1}" in
( '--silent' | '-s' )
silent='yes'
;;
( '--' )
args+=( "${#:2}" )
break
;;
( * )
args+=( "${1}" )
;;
esac
shift || break
done
if [[ "${silent}" == 'yes' ]]; then
for prompt in "${args[#]}"; do
charcount='0'
prompt="${prompt}: "
reply=''
while IFS='' read -n '1' -p "${prompt}" -r -s 'char'; do
case "${char}" in
# Handles NULL
( $'\000' )
break
;;
# Handles BACKSPACE and DELETE
( $'\010' | $'\177' )
if (( charcount > 0 )); then
prompt=$'\b \b'
reply="${reply%?}"
(( charcount-- ))
else
prompt=''
fi
;;
( * )
prompt='*'
reply+="${char}"
(( charcount++ ))
;;
esac
done
printf '\n' >&2
printf '%s\n' "${reply}"
done
else
for prompt in "${args[#]}"; do
IFS='' read -p "${prompt}: " -r 'reply'
printf '%s\n' "${reply}"
done
fi
}
It could be used like:
$ ask Username
Username: AzureDiamond
AzureDiamond
$ ask -s Password
Password: *******
hunter2
$ ask First Second Third
First: foo
foo
Second: bar
bar
Third: baz
baz
#nxnev's answer didn't quite work for me, at least on macOS. I simplified it a bit, and now it's flawless:
#!/bin/bash
ask() {
charcount='0'
prompt="${1}: "
reply=''
while IFS='' read -n '1' -p "${prompt}" -r -s 'char'
do
case "${char}" in
# Handles NULL
( $'\000' )
break
;;
# Handles BACKSPACE and DELETE
( $'\010' | $'\177' )
if (( charcount > 0 )); then
prompt=$'\b \b'
reply="${reply%?}"
(( charcount-- ))
else
prompt=''
fi
;;
( * )
prompt='*'
reply+="${char}"
(( charcount++ ))
;;
esac
done
printf '\n' >&2
printf '%s\n' "${reply}"
}
pwd="$(ask Password)"
echo "Your password is $pwd"
#!/bin/bash
echo "------------------------------"
n=7
echo " Enter Password :"
for (( i=1;i<n;i++ ))
do
stty -echo
read -r -s -n 1 char
stty echo
echo -n "*"
pass+="$char"
done
echo " "
echo " Your password : $pass "
echo ""
echo "-------------------------------"