Change variable named in argument to bash function [duplicate] - bash

This question already has answers here:
Dynamic variable names in Bash
(19 answers)
How to use a variable's value as another variable's name in bash [duplicate]
(6 answers)
Closed 5 years ago.
In my bash scripts, I often prompt users for y/n answers. Since I often use this several times in a single script, I'd like to have a function that checks if the user input is some variant of Yes / No, and then cleans this answer to "y" or "n". Something like this:
yesno(){
temp=""
if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
temp=$(echo "$1" | tr '[:upper:]' '[:lower:]' | sed 's/es//g' | sed 's/no//g')
break
else
echo "$1 is not a valid answer."
fi
}
I then would like to use the function as follows:
while read -p "Do you want to do this? " confirm; do # Here the user types "YES"
yesno $confirm
done
if [[ $confirm == "y" ]]; then
[do something]
fi
Basically, I want to change the value of the first argument to the value of $confirm, so that when I exit the yesno function, $confirm is either "y" or "n".
I tried using set -- "$temp" within the yesnofunction, but I can't get it to work.

You could do it by outputting the new value and overwriting the variable in the caller.
yesno() {
if [[ "$1" =~ ^([Yy](es|ES)?|[Nn][Oo]?)$ ]] ; then
local answer=${1,,}
echo "${answer::1}"
else
echo "$1 is not a valid answer." >&2
echo "$1" # output the original value
return 1 # indicate failure in case the caller cares
fi
}
confirm=$(yesno "$confirm")
However, I'd recommend a more direct approach: have the function do the prompting and looping. Move all of that repeated logic inside. Then the call site is super simple.
confirm() {
local prompt=$1
local reply
while true; do
read -p "$prompt" reply
case ${reply,,} in
y*) return 0;;
n*) return 1;;
*) echo "$reply is not a valid answer." >&2;;
esac
done
}
if confirm "Do you want to do this? "; then
# Do it.
else
# Don't do it.
fi
(${reply,,} is a bash-ism that converts $reply to lowercase.)

You could use the nameref attribute of Bash (requires Bash 4.3 or newer) as follows:
#!/bin/bash
yesno () {
# Declare arg as reference to argument provided
declare -n arg=$1
local re1='(y)(es)?'
local re2='(n)o?'
# Set to empty and return if no regex matches
[[ ${arg,,} =~ $re1 ]] || [[ ${arg,,} =~ $re2 ]] || { arg= && return; }
# Assign "y" or "n" to reference
arg=${BASH_REMATCH[1]}
}
while read -p "Prompt: " confirm; do
yesno confirm
echo "$confirm"
done
A sample test run looks like this:
Prompt: YES
y
Prompt: nOoOoOo
n
Prompt: abc
Prompt:
The expressions are anchored at the start, so yessss etc. all count as well. If this is not desired, an end anchor ($) can be added.
If neither expression matches, the string is set to empty.

Related

How to check if array element is substring of string in Bash [duplicate]

This question already has answers here:
How to check if a string contains a substring in Bash
(29 answers)
Closed 2 years ago.
This question does not answer my question.
I have a script that checks the existence of specific environment variables and prints them with their values. Now I want to mask the values of password variables (containing SECRET, PW, PASSWORD, KEY in its name, e.g. CLIENT_SECRET) with ****.
Currently I have a script like this:
expected_env_vars=("CLIENT_ID" "CLIENT_SECRET" "BACKEND_KEY" "BACKEND_NAME")
suppress_env_vars_with_substring=("SECRET" "PASSWORD" "PW" "KEY")
for env_var in "${expected_env_vars[#]}"; do
if [[ -z "${!env_var}" ]]; then
echo "Environment variable \"$env_var\" not defined"
exit 1
else
# Perform check if an element of $suppress_env_vars_with_substring is substring of $env_var
echo "$env_var=${!env_var}...OK"
fi
done
Question
How to check if an array element is substring of a string?
You may use this script with a grep:
expected_env_vars=("CLIENT_ID" "CLIENT_SECRET" "BACKEND_KEY" "BACKEND_NAME")
suppress_env_vars_with_substring=("SECRET" "PASSWORD" "PW" "KEY")
for env_var in "${expected_env_vars[#]}"; do
if [[ -z "${!env_var}" ]]; then
echo "Environment variable \"$env_var\" not defined"
exit 1
else
# Perform check if an element of $suppress_env_vars_with_substring is substring of $env_var
printf '%s=' "$env_var"
grep -qFf <(printf '%s\n' "${suppress_env_vars_with_substring[#]}") <<< "$env_var" &&
echo '****' || echo "${!env_var}"
fi
done
It does not matter, whether or not the operands to be tested belongs to an array or not. The general form is
[[ $x == *$y* ]] && echo "$y is a substring of $x"
You can substitute for $x and $y and parameter expansion you like, including array elements.

How can I read user input in a Bash script? [duplicate]

This question already has answers here:
How do I read user input into a variable in Bash?
(6 answers)
Closed 4 years ago.
Using Bash, I tried to read input from the user like this:
#!/bin/bash
function read_from_user {
cat | echo
}
echo 'Do you want to create the folder "new.folder" ?'
var=`read_from_user`
if [[ ${var} == yes ]]; then
mkdir new.folder
fi
echo 'var is: ${var}'
But it's not working, var is empty, even though the user input is not empty.
How can I read user input from my Bash script?
You should use read:
#!/bin/bash
echo 'Do you want to create the folder "new.folder" ?'
read var
if [[ "$var" == "yes" ]]; then
mkdir new.folder
fi
echo "var is: $var"
If you really want to use cat, you could do this, as cat without any argument reads from stdin:
#!/bin/bash
echo 'Do you want to create the folder "new.folder" ?'
var=$(cat)
if [[ "$var" == "yes" ]]; then
mkdir new.folder
fi
echo "var is: $var"
However, you would have to use CTRL + D to send on EOF to your program after typing your input. Otherwise cat will wait for more. read is a cleaner way to ask a user for input.
Your code is almost correct, you just need to change your function to read user input into a variable call var. Also you need to change your code in two place. One in the function and one at the place where you are calling your function. I have modified your code like below:-
#!/bin/bash
function read_from_user {
read -r var #here you are reading user input to variable `var`
}
echo 'Do you want to create the folder "new.folder" ?'
#var=`read_from_user`
read_from_user #here you are calling the function to read user input
if [[ ${var} == yes ]]; then
mkdir new.folder
fi
echo "var is: ${var}"
Also always compare two string like if [[ "${var}" == "yes" ]]; but still your above if condition will also work perfectly.
Also best way to do it like below where you don't need a separate echo statement and input will be read at the end out output message:-
#!/bin/bash
function read_from_user {
read -p 'Do you want to create the folder "new.folder" ? ' var
}
#echo 'Do you want to create the folder "new.folder" ?'
#var=`read_from_user`
read_from_user
if [[ "${var}" == "yes" ]]; then
mkdir new.folder
fi
echo "var is: ${var}"

Getting piped data to functions

Example output
Say I have a function, a:
function a() {
read -r VALUE
if [[ -n "$VALUE" ]]; then # empty variable check
echo "$VALUE"
else
echo "Default value"
fi
}
So, to demonstrate piping to that function:
nick#nick-lt:~$ echo "Something" | a
Something
However, piping data to this function should be optional. So, this should also be valid. and give the following output:
nick#nick-lt:~$ a
Default value
However, the function hangs, as the read command waits for data from stdin.
What I've tried
Honestly not a lot, because I don't know much about this, and searching on Google returned very little.
Conceptually, I thought there might be a way to "push" an empty (or whitespace, whatever works) value to the stdin stream, so that even empty stdin at least has this value appended/prepended, triggering read and then simply trim off that first/last character. I didn't find a way to do this.
Question
How can I, if possible, make both of the above scenarios work for function a, so that piping is optional?
EDIT: Apologies, quickly written question. Should work properly now.
One way is to check whether standard input (fd 0) is a terminal. If so, don't read, because that will cause the user to have to enter something.
function a() {
value=""
if [ \! -t 0 ] ; then # read only if fd 0 is a pipe (not a tty)
read -r value
fi
if [ "$value" ] ; then # if nonempty, print it!
echo "$value"
else
echo "Default value"
fi
}
I checked this on cygwin: a prints "Default value" and echo 42 | a prints "42".
Two issues:
Syntactic, You need a space, before closing ]]
Algorithmic, You need the -n (non-zero length) variable test, not -z (zero length)
So:
if [[ -n "$VALUE" ]]; then
Or simply:
if [[ "$VALUE" ]]; then
As [[ is a shell builtin, you don't strictly need the double quotes:
if [[ $VALUE ]]; then
Also refrain from using all uppercases as variable name, as these are usually used for denoting environment variables, and your defined one might somehow overwrite already existing one. So use lowercase variable name:
if [[ $value ]]; then
unless you are export-ing your variable, and strictly need it to be uppercased, also make sure it is not overwriting any already existing one.
Also, i would add a timeout to read e.g. -t 5 for 5 seconds, and if no input is entered, print the default value. Also change the function name to something more meaningful.
Do:
function myfunc () {
read -rt5 value
if [[ "$value" ]]; then
echo "$value"
else
echo "Default value"
fi
}
Example:
$ function myfunc () { read -rt5 value; if [[ "$value" ]]; then echo "$value"; else echo "Default value"; fi ;}
$ myfunc
Default value
$ echo "something" | myfunc
something
$ myfunc
foobar
foobar

how to write a Bash function that confirms the value of an existing variable with a user

I have a large number of configuration variables for which I want users to issue confirmation of the values. So, there could be some variable specifying a run number in existence and I want the script to ask the user if the current value of the variable is ok. If the user responds that the value is not ok, the script requests a new value and assigns it to the variable.
I have made an initial attempt at a function for doing this, but there is some difficulty with its running; it stalls. I would value some assistance in solving the problem and also any criticisms of the approach I'm using. The code is as follows:
confirmVariableValue(){
variableName="${1}"
variableValue="${!variableName}"
while [[ "${userInput}" != "n" && "${userInput}" != "y" ]]; do
echo "variable "${variableName}" value: "${variableValue}""
echo "Is this correct? (y: continue / n: change it / other: exit)"
read userInput
# Make the user input lowercase.
userInput="$(echo "${userInput}" | sed 's/\(.*\)/\L\1/')"
# If the user input is "n", request a new value for the variable. If the
# user input is anything other than "y" or "n", exit. If the user input
# is "y", then the user confirmation loop ends.
if [[ "${userInput}" == "n" ]]; then
echo "enter variable "${variableName}" value:"
read variableValue
elif [[ "${userInput}" != "y" && "${userInput}" != "n" ]]; then
echo "terminating"
exit 0
fi
done
echo "${variableValue}"
}
myVariable="run_2014-09-23T1909"
echo "--------------------------------------------------------------------------------"
echo "initial variable value: "${myVariable}""
myVariable="$(confirmVariableValue "myVariable")"
echo "final variable value: "${myVariable}""
echo "--------------------------------------------------------------------------------"
The problem is here:
myVariable="$(confirmVariableValue "myVariable")"
your questions, like
echo "Is this correct? (y: continue / n: change it / other: exit)"
are going into the myVariable and not to the screen.
Try print questions to STDERR, or any other file-descriptor but STDOUT.
Opinion based comment: I would be unhappy with such config-script. It is way too chatty. For me is better:
print out the description and the default value
and ask Press Enter for confirm or enter a new value or <something> for exit>
You can also, use the following technique:
use the bash readline library for the read command with -e
use the -i value for set the default value for the editing
use the printf -v variable to print into variable, so you don't need to use var=$(...) nor any (potentially) dangerous eval...
example:
err() { echo "$#" >&2; return 1; }
getval() {
while :
do
read -e -i "${!1}" -p "$1>" inp
case "$inp" in
Q|q) err "Quitting...." || return 1 ;;
"") err "Must enter some value" ;;
*)
#validate the input here
#and print the new value into the variable
printf -v "$1" "%s" "$inp"
return 0
;;
esac
done
}
somevariable=val1
anotherone=val2
x=val3
for var in somevariable anotherone x
do
getval "$var" || exit
echo "new value for $var is: =${!var}="
done
I would not have them answer "Yes" then type in the new value. Just have them type in the new value if they want one, or leave it blank to accept the default.
This little function lets you set multiple variables in one call:
function confirm() {
echo "Confirming values for several variables."
for var; do
read -p "$var = ${!var} ... leave blank to accept or enter a new value: "
case $REPLY in
"") # empty use default
;;
*) # not empty, set the variable using printf -v
printf -v "$var" "$REPLY"
;;
esac
done
}
Used like so:
$ foo='foo_default_value'
$ bar='default_for_bar'
$ confirm foo bar
Confirming values for several variables.
foo = foo_default_value ... leave blank to accept or enter a new value: bar
bar = default_for_bar ... leave blank to accept or enter a new value:
foo=[bar], bar=[default_for_bar]
Of course, if blank can be a default, then you would need to account for that, like #jm666 use of read -i.

How to check if a value is in a list?

I've got a script with a variable taken from command line parameters. I want to check if its value is one of dev, beta or prod. I've got a following code snippet:
#!/usr/bin/env bash
ENV_NAME=$1
echo "env name = $ENV_NAME"
ENVIRONMENTS=('dev','beta','prod')
if [[ $ENVIRONMENTS =~ $ENV_NAME ]]; then
echo 'correct'
exit
else
echo 'incorrect'
exit
fi
When I run my script, it doesn't matter which parameters I pass: ./script.sh beta or ./script.sh or ./script.sh whatever, I always get correct echoed. What is wrong in my script?
for i in ${ENVIRONMENTS[#]}; do
if [[ $i = $ENV_NAME ]]; then
echo "correct"
exit
fi
done
echo 'incorrect'
exit
For using bash re:
ENV_NAME=dev
ENVIRONMENTS="dev|beta|prod"
[[ $ENV_NAME =~ ^($ENVIRONMENTS)$ ]] && echo ${BASH_REMATCH[1]}
dev
Everyone's given a good suggestion already however there's another way. It's more efficient than using regex and probably more efficient than using a loop especially when having more values. The only thing is that this requires Bash 4.0 or newer.
declare -A ENVIRONMENTS=([dev]=. [beta]=. [prod]=.)
if [[ -n ${ENVIRONMENTS["$ENV_NAME"]} ]]; then
...
Put it in a function
I like to hide ugly implementation details, especially when it comes to bash.
function catPipe() # concatenate all arguments with a pipe character
{
local IFS='|'
echo "$*"
}
function matchList()
{
local needle="$1"
shift
local stack=$(catPipe "$#")
[[ "$needle" =~ ^($stack)$ ]]
}
If you don't like having a subshell, use this:
function matchList()
{
local needle="$1"
shift
IFS='|' eval 'local stack="$*"'
[[ "$needle" =~ ^($stack)$ ]]
}
Inspired by konsolebox' solution, I use the ^($var)$ syntax to avoid partial matches. First it looked like this:
function matchList()
{
local needle="$1"
shift
local stack="$#"
[[ ${stack[#]} =~ "$needle" ]] # would allow partial machtes like 'eta'
}
The function can be called with either a string, an array, or single items:
variants='dev beta prod'
varArray=(dev beta prod)
matchList prop $variants && echo 'prop: match!'
matchList beta $variants && echo 'beta: match!'
matchList beta ${varArray[#]} && echo 'beta: match!'
matchList eta alpha beta gamma && echo 'eta: match!'
# Output:
beta: match!
beta: match!
Or for the original example:
matchList $ENV_NAME ${ENVIRONMENTS[#]} && echo 'correct'
case (partial solution)
Side note: I tried to come up with a solution using case, as it would naturally fit the requirements.
It works, but I couldn't figure out if/how I can use ENVIRONMENTS in the statment.
ENV_NAME='beta'
ENVIRONMENTS='dev|beta|prod'
case $ENV_NAME in (dev|beta|prod) echo 'correct' ;; (*) echo 'incorrect' ;; esac

Resources