Nginx-WordPress installer for Debian

Posted on

Problem

I have written the following Bash script which includes an Nginx WordPress bootstrapper: A program establish WordPress apps fast on a Debian-Nginx environment.

It utilizes CertBot for HTTPS, SSHguard for protection from BFA (see Nginx conf), WP-CLI for creating a WP app dir and conf file fast, MySQL CLI to create a dbstack and a local function named rse to restart the server and giving proper permissions to the app dir.

#!/bin/bash
domain="$1" && test -z "$domain" && exit 2

environment() {
    read -sp "Please enter the app DB root password:      " dbrootp_1 && echo
    read -sp "Please enter the app DB root password again:" dbrootp_2 && echo
    if [ "$dbrootp_1" != "$dbrootp_2" ]; then echo "Values unmatched. Please try again." && exit 2 fi

    read -sp "Please enter the app DB user password:      " dbuserp_1 && echo
    read -sp "Please enter the app DB user password again:" dbuserp_2 && echo
    if [ "$dbuserp_1" != "$dbuserp_2" ]; then echo "Values unmatched. Please try again." && exit 2 fi
}
environment

wordpress() {
    rm -rf "$drt"/"$domain"/ 2>/dev/null
    wp core download --path="$drt"/"$domain"/ --allow-root
    wp config create --path="$drt"/"$domain"/ --dbname="$domain" --dbuser="$domain" --dbpass="$dbuserp" --dbhost="localhost" --allow-root
}
wordpress

nginx() {
    rm "$s_a/$domain.conf" 2>/dev/null
    rm "$s_e/$domain.conf" 2>/dev/null

    cat <<-EOF > "$s_a/$domain.conf"
        server {
            root ${drt}/${domain}/;
            server_name ${domain} www.${domain};
            location ~* .(jpg|jpeg|png|gif|ico|css|js|ttf|woff|pdf)$ {expires 365d;}
            location "/wp-login.php" {access_log "/var/log/httpd/wordpress-logins.log";}
        }
    EOF

    ln -sf "$s_a"/"$domain".conf "$s_e"
    rse
}
nginx

certbot() {
    certbot --nginx -d "$domain" -d www."$domain"
    rse
}
certbot

database() {
    cat <<-EOF | mysql -u root -p"$dbrootp_1"
        DROP USER IF EXISTS "$domain"@"localhost";
        DROP database IF EXISTS "$domain";
        CREATE USER "$domain"@"localhost" IDENTIFIED BY "$dbuserp_1";
        CREATE DATABASE "$domain";
        GRANT ALL PRIVILEGES ON "$domain".* TO "$domain"@"localhost";
    EOF
}
database

finalize() {
    echo "Change http to http2 in your Nginx app conf and run rse"
}
finalize

Note

Before executing the script I declared in the end of $HOME/.bashrc:

drt="/var/www/html"
s_a="etc/nginx/sites-available"
s_e="etc/nginx/sites-enabled"

Solution

My answer is going to focus on your use of shell functions and global variables. There aren’t any actual syntax errors in your script AFAICT, but your misuse of functions in this script is dangerously error-prone.

There are two main reasons for using a function in bash (or any language, really).

The first is to parameterise a frequently performed operation – e.g. by passing arguments to it rather than hard-coding values (or relying on side-effects). Optionally it should return a result to the caller (in shell, by printing something to stdout to be captured with command substitution. Or sometimes by changing a variable in the environment – but see below about that)

You’re not doing that.

Worse, you’re making your functions rely on global variables. This makes your functions (and your scripts) dependent on side-effects of previous variable definitions – which may still hold old values from previous runs (and you forgot to update them for the current run) or, worse, may be empty because they haven’t been initialised yet in the current shell. This will inevitably lead to catastrophic data-loss one day when you forget to set a value for “$drt” and/or “$domain” and/or your many other global variables.

Better to pass values as parameters when and as needed. Don’t trust that a crucial variable happened to have been set some time in the past – i.e. be explicit, not implicit.

As a rule, functions should be self-contained and not rely on variables that may or may not be defined outside of their scope. They should also define variables as being local to them so that they explicitly don’t use (or worse, change) a global variable that happens to have the same name as the function’s variable.

Anyway, an example of how your wordpress function could be written:

wordpress() {
    local drt="$1"
    local domain="$2"
    local dbpass="$3"

    # should do more checking here, this is the bare minimum.
    # it avoids running `rm -rf /` if both $drt and $domain
    # are empty, and running `rm -rf "$drt/"` if $domain is empty.
    [ -z "$drt"    ] && error 1 '$drt is empty. aborting!'
    [ -z "$domain" ] && error 1 '$domain is empty. aborting!'
    [ -z "$dbpass" ] && error 1 '$dbpass is empty. aborting!'

    # should check exit status of each command run here and take
    # appropriate action on any failures.  Figure out what you want
    # to happen if any of the following fail and implement it.
    rm -rf "$drt"/"$domain"/ 2>/dev/null
    wp core download --path="$drt"/"$domain"/ --allow-root
    wp config create --path="$drt"/"$domain"/ --dbname="$domain" 
      --dbuser="$domain" --dbpass="$dbpass" --dbhost=localhost 
      --allow-root
}

You’d call it as wordpress "$drt" "$domain" "$dbpass". It uses a function called error:

error() {
  local ec msg

  ec="$1" ; shift
  msg="$*"

  [ -n "$msg" ] && echo "$msg" >&2
  # don't exit if $ec==0 - just return after printing a warning to stderr
  [ "$ec" != 0 ] && exit "$ec"
}

The second reason is to make the structure of the script easier to follow. For example, instead of having a lot of shell code for each pattern matched in a case statement (which makes it difficult to see at a glance what the cases are and what each one does), write a function for each case and call it – with or without arguments.

You’re not doing that either – you’re using each function as soon as you define it. That’s both weird and pointless….especially so since your functions don’t take arguments.

A more usual way of defining and using functions would be something like:

function1 () { ... ; }
function2 () { ... ; }
function3 () { ... ; }
function4 () { ... ; }

main () {
  function1
  function2
  function3
  function4
}

main

Note: using amain function allows you to define and call functions in any order, and allows you to define your functions (other than main() itself) at the end of the script so that the main code comes first. Otherwise you can get errors in situations where, e.g., your main non-function script code calls function2 before it is defined.

as noted above, each function call can, or should, have explicit arguments passing in the values they will use.

BTW, for the case statement mentioned above:

case "$var" in
  1) function1 ;;
  2) function2 ;;
  3) function3 ;;
  4) function4 ;;
  *) error 1 'Unknown case' ;;
esac

That case statement would be a lot harder to read if each case had 5 or 10 or more lines of code.

@cas has already pointed out the most critical issues.
I add a few minor points on top of that.

Syntax errors

A semicolon is missing before the fi on this line and another similar line:

if [ "$dbrootp_1" != "$dbrootp_2" ]; then echo "Values unmatched. Please try again." && exit 2 fi

Usability

The script is not very user-friendly.
For example,
when the entered passwords don’t match,
it prints “Please try again”, and exits.
I would expect “try again” to mean that I can enter the passwords one more time, and not that I have to rerun the script one more time.
If I accidentally mistyped the 2nd password for the DB user,
and I’m forced to re-enter the DB root passwords too,
I’ll be very unhappy with the script.

A also have a pickle with this prompt:

read -sp "Please enter the app DB root password:      " dbrootp_1 && echo

As a user, I find the many blanks after the : weird. I would find one blank natural.

Don’t repeat yourself

The input of DB root and DB user passwords are very similar,
and a good candidate to parameterize and extract to a reusable function.

Something like this (thanks @cas for the > /dev/tty tip):

readpw() {
    local pw1 pw2 label=$1
    while :; do
        read -sp "Please enter the $label password: " pw1 && echo
        read -sp "Please enter the $label password again: " pw2 && echo
        [ "$pw1" = "$pw2" ] && break
        echo "The values don't match. Please try again."
    done > /dev/tty
    echo "$pw1"
}

dbuserpw=$(readpw "app DB user")
dbrootpw=$(readpw "app DB root")

Note that the value of $(...) is everything that was written on stdout, minus trailing newlines. The > /dev/tty is to prevent output on stdout that we don’t want to capture by $(...) (thanks @cas!).

Leave a Reply

Your email address will not be published. Required fields are marked *