1

I'm building an automated bash script that will install and configure several packages on a Linux Server.

I'm hoping to re-use this script on multiple servers but change some parameters accustom to the needs. So that I don't have to go through lots of code in the future making multiple changes using find and replace, I've used variables throughout the script as they can simply have their values defined at the top/beginning of the script.

As some of the variables are passwords I want to avoid storing the passwords in a plain text file. My way around this is to have the variable values stored in encrypted document which I can read from and then define the variables at the beginning of the script. This will be done by running through a series of questions and asking me for my input to set the values for the variables. I've worked out how to do this using the following commands:

read -p "`echo -e 'Enter admin user password: \n\b'`" admin_user_password
echo "You entered '$admin_user_password"

The area that gets tricky in regards to bash script writing is, I would like to ensure all of the variables are set (not left blank) and ensure they are entered correctly before the script can continue and automatically do its own thing. Setting the variables at the beginning is the only part of the script that requires user interactivity. In order to get this to work, I'm fairly certain I will be looking at loops.

This is the order I'm hoping to run the loop(s) before the automated section of the script starts.

  1. The script asks for variables by asking for users input
  2. The user set variables will be checked against a list of all the variables. Any variables not entered/set in step 1 will run through the remaining questions warning the user "you have not set a value for $some-variable".
  3. Once all of the variables are set it will then print out all of the set variables and their values and ask if they are correct with a simple yes/no. If they are not correct I want to return to step 1 again to re-run through the question. However, if the variables and values are correct the script will continue into the automated section.

This is what I have so far.

## TOP OF SCRIPT ##
script_variables=(
    admin_user_name
    admin_user_password
)
for i in "${script_variables[@]}"; do 
    read -p "`echo -e 'Enter value for: \n\b'`" ${script_variables[@]}
    echo "You entered '${script_variables[@]}'"
    
    if test -z "${script_variables[@]}"
    then
        echo "${script_variables[@]} has not been set"
        # loopback to the top of the script
        continue
    else
        while true; do
            read -p "Are the variables ccorrect?" yn
            case $yn in
                [Yy]* ) echo "All variables have been set. The script will now continue."; sleep 2; break;;
                [Nn]* ) continue;;
                * ) echo "Please answer yes or no.";;
            esac
    fi
done

# Automated script commands below

I'm not sure if the above commands will work. Even if it does where continue is on line 14, where the loop will return to the ## TOP OF SCRIPT ## section it will ask me all of the questions in the array again. On the second pass of asking me the questions it needs to check itself if the variables have been set. For those variables that have not been set, it should be the only questions I get re-asked.

The only way I could possibly do it is to put the variable testing section at the beginning. This will just mean that on the first instance of being asked the questions to set the variables I will get a message to say that none of the variables have been set.

The reason I'm asking for help is I rarely deal with loops. My Unix knowledge is solely self-taught mostly through looking on the internet on forums like this one or experimenting with the commands on Linux PC in a virtual environment, and I haven't ventured into the depths of loops yet. Anytime I have used loops, they are snippets I've taken from the internet and used in my own scripts. Obviously with my script above I'm looking at nested loops and that increases the complexity even further.

Update 1

In regards to your brilliant answers, I want to tweak the displayed text so when entering the variables it is entered on a new line below.

For example, how would I place a line break in the

read -p ("Enter value for $varName \n\b " script_variables[$varName]

I want to display something like this:

Enter value for admin_user_name
>

I saw to guides on the internet talking about using echo -e so that \n\b could be used. I did have a play around but the variables weren't expanding.

https://stackoverflow.com/questions/8467424/echo-newline-in-bash-prints-literal-n

UPDATE 2

I'm thinking of expanding your script and implementing an optional way of reading and storing the variables to and from a temporary file. I will be able to keep this safe from other system users by setting appropriate Unix permissions and then I'm going to delete the file at then very end of the script so there are not traces of it.

At the beginning of the script I would like to check if a specific text file containing the variables exists. If the file does exist then check if all of the variables have been set and compare against the array of variable names. If the stored variables in the file are not complete, run through the loop until they are set. Obviously the variables stored in the file will be set only if the user has said to "yes" when they are asked "are you sure this is correct?"

N.B once I have got this script working I am hoping to place it on GitHub and update my answer on here with the final version so that others can use it, as it is bound to help others out.

1 Answer 1

1

OK, first a few obvious issues. ${script_variables[@]} expands to the entire $script_variables array separated by a space. So your test -z "${script_variables[@]}" will always be false seeing as you define the array at the beginning of the script, so "${script_variables[@]}" will never be empty. When you want to refer to an element of the array, you need to use a specific numeric index: "${script_variables[0]}" for the 1st element, "${script_variables[1]}" for the second etc.

Second, when you read a value into a variable, you need to store it in a variable. However, you are giving read the expanded array which is just the values stored in the array:

$ echo "${script_variables[@]}"
admin_user_name admin_user_password

More importantly, you seem to want to store the value the user gives in a variable that you can then call by name. This, where you set var="foo" and then have variableName="var" and are trying to get the value of the variable named var (so, "foo" in this case) is called "indirect expansion". The syntax for that is ${!variableName}. For example:

$ var="foo"
$ variableName="var"
$ echo "${!variableName}"
foo

So, you can either use that or you can use two arrays: one to store the variable names and another to store their values. Or, even better with modern versions of bash, use a single associative array whose keys will be the variable names. Here's a working version of your script using indirect expansion:

#/bin/bash

script_variables=(
    admin_user_name
    admin_user_password
)

## This will be used to exit the loop
allSet="";

while [[ -z $allSet ]]; do
  for varName in "${script_variables[@]}"; do 
    ## No need to loop the whole thing, just loop
    ## until this particular variable has been set
    while [[ -z ${!varName} ]]; do
      read -p "Enter value for $varName: " $varName
    done
  done

  ## We will only exit the loop once all vars have been set.
  ## Now print and check them.
  printf '\n=========\nYou have entered:\n'
  for varName in "${script_variables[@]}"; do 
    printf '%s=%s\n' "$varName" "${!varName}"
  done

  while true; do
    read -p "Are the variables correct? " yn
    case $yn in
      [Yy]* )
        echo "All variables have been set. The script will now continue.";
        ## Setting this to 1 exits the top "while [[ -z $allSet ]]; do" loop
        allSet=1
        break;;
      [Nn]* )
        ## Clear the stored values to start again
        for varName in "${script_variables[@]}"; do
          unset $varName
        done
        
        break;;
      * )
        echo "Please answer yes or no.";;
    esac
  done
done

And here is a version using an associative array:

#/bin/bash

declare -A script_variables=(
    [admin_user_name]=""
    [admin_user_password]=""
)

## This will be used to exit the loop
allSet="";

while [[ -z $allSet ]]; do
  ## '${!array[@]}' returns all keys of an associative array
  for varName in "${!script_variables[@]}"; do
    read -p "Enter value for $varName: " script_variables[$varName]
  done

  ## We will only exit the loop once all vars have been set.
  ## Now print and check them.
  printf '\n=========\nYou have entered:\n'
  for varName in "${!script_variables[@]}"; do 
    printf '%s=%s\n' "$varName" "${script_variables[$varName]}"
  done

  while true; do
    read -p "Are the variables correct? " yn
    case $yn in
      [Yy]* )
        echo "All variables have been set. The script will now continue.";
        ## Setting this to 1 exits the top "while [[ -z $allSet ]]; do" loop
        allSet=1
        break;;
      [Nn]* )
        ## Clear the stored values to start again
        for varName in "${!script_variables[@]}"; do
          script_variables[$varName]=""
        done
        
        break;;
      * )
        echo "Please answer yes or no.";;
    esac
  done
done

Now, personally, I would use a slightly different approach. Instead of setting everything, then letting the user check in the end, I would check everything as it is entered. That way, you get the errors at the beginning and don't need to re-set all variables but only the one you got wrong. Something like this:

#/bin/bash

declare -A script_variables=(
    [admin_user_name]=""
    [admin_user_password]=""
)

allSet=0;

while [[ $allSet -lt ${#script_variables[@]} ]]; do
  for varName in "${!script_variables[@]}"; do
        ok=""
        while [[ $ok != [Yy]* ]]; do
          read -p "Enter value for $varName: " script_variables[$varName]
          read -p "You entered '${script_variables[$varName]}'. Is this correct? " ok
        done
        ## If we're satisfied, increment the value of allSet
        ((allSet++))
  done
done

## You can add a second test here, but just exit the script if it's wrong.
## There's no point in complicating your code if you're just going back to
## the beginning anyway: just exit and rerun it.
printf '\n=========\nYou have entered:\n'
for varName in "${!script_variables[@]}"; do 
  printf '%s=%s\n' "$varName" "${script_variables[$varName]}"
done

read -p "Is everything OK? (y/n; n will exit the script)": yn
if [[ $yn != [Yy]* ]]; then
   exit
fi

echo "Everything correctly set, continuing!"
1
  • How easy is it to export and read the variables to and from a file? Nov 27, 2020 at 8:24

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.