2
\$\begingroup\$

Review for optimization, code standards, missed validation and dependency checks.

Review version 3. What's new:

  • apply previous code review
  • input arguments and validation
  • MySQL management (optional)
  • install option (.desktop file), what allows some input values be predefined
  • attempt to install dependency when no choice to get user input (GUI version without zenity)

I'm a web programmer, don't known things to make system GUI. So, that's why use bash with zenity. It was interesting to learn bash features. Answers are good, new version to come. Web is unsuitable for task more as bash.

GitHub

#!/bin/bash

webmaster="$(id -un)"   # user who access web files. group is www-data
webgroup="www-data"     # apache2 web group, does't need to be webmaster group. SGID set for folder.
maindomain=".localhost" # domain for creating subdomains, leading dot required
virtualhost="*"         # ip of virtualhost in case server listen on many interfaces or "*" for all
virtualport="80"        # port of virtualhost. apache2 must listen on that ip:port
serveradmin="webmaster@localhost" # admin email
# declare -a webroots=("/home/\${webmaster}/Web/\${host}"
#                      "/var/www/\${host}"
#                      "/var/www/\${webmaster}/\${host}")
# declared below, after a chanse to edit inlined variadles
webroot=0              # folder index in webroots array
a2ensite="050-"        # short prefix for virtualhost config file
apachehost="/etc/apache2/sites-available/${a2ensite}"   # prefix for virtualhost config file
tmphost=$(mktemp)      # temp file for edit config
trap 'rm "$tmphost"' EXIT # rm temp file on exit
host=""                # no default subdomain
skipmysql=""
mysqladmin="root"
mysqladminpwd=""


have_command() {
  type -p "$1" >/dev/null
}

in_terminal() {
  [ -t 0 ]
}
in_terminal && start_in_terminal=1

info_user() {
  local msg=$1
  local windowicon="info" # 'error', 'info', 'question' or 'warning' or path to icon
  [ "$2" ] && windowicon="$2"
  if in_terminal; then
    echo "$msg"
  else
    zenity --info --text="$msg" --icon-name="${windowicon}" \
           --no-wrap --title="Virtualhost" 2>/dev/null
  fi
}

notify_user () {
  echo "$1" >&2
  in_terminal && return
  local windowicon="info" # 'error', 'info', 'question' or 'warning' or path to icon
  local msgprefix=""
  local prefix="Virtualhost: "
  [ "$2" ] && windowicon="$2" && msgprefix="$2: "
  if have_command zenity; then
    zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
    return
  fi
  if have_command notify-send; then
    notify-send "${prefix}${msgprefix}$1"
  else
    xmessage -buttons Ok:0 -nearmouse "${prefix}${msgprefix}$1" -timeout 10
  fi
}

# sudo apt hangs when run from script, thats why terminal needed
install_zenity() {
  have_command gnome-terminal && gnome-terminal --title="$1" --wait -- $1
  have_command zenity && exit 0
  exit 1
}
# check if zenity must be installed or text terminal can be used
# if fails = script exit
# to install zenity, run: ./script.sh --gui in X terminal
# ask user to confirm install if able to ask
check_gui() {
  local msg="Use terminal or install zenity for gui. '$ sudo apt install zenity'"
  local cmd="sudo apt install zenity"
  local notfirst=$1
  if ! have_command zenity;then
    notify_user "$msg" "error"
    # --gui set and input/output from terminal possible
    if [[ "${gui_set}" && "${start_in_terminal}" ]]; then
      read -p "Install zenity for you? sudo required.[y/N]" -r autozenity
      reg="^[yY]$"
      if [[ "$autozenity" =~ $reg ]]; then
        $(install_zenity "$cmd") || exit
        exit 0
      else
        exit 1
      fi
    else
      if [[ "${gui_set}" ]]; then
        $(install_zenity "$cmd") || exit
        exit 0
      else
        if ! in_terminal;then
          $(install_zenity "$cmd") || exit
          exit 0
        fi
      fi
    fi

  fi
  exit 0
}
$(check_gui) || exit


validate_input() {
  local msg="$1"
  local var="$2"
  local reg=$3
  if ! [[ "$var" =~ $reg ]]; then
    notify_user "$msg" "error"
    exit 1
  fi
  exit 0
}

validate_domain() {
  $(LANG=C; validate_input "Bad domain with leading . (dot)" "$1" \
    "^\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9\.])*$") || exit 1
  exit 0
}

validate_username() {
  $(LANG=C; validate_input "Bad username." "$1" \
    "^[[:lower:]_][[:lower:][:digit:]_-]{2,15}$") || exit 1
  if ! id "$1" >/dev/null 2>&1; then
    notify_user "User not exists" "error"
    exit 1
  fi
  exit 0
}

validate_group() {
  getent group "$1" > /dev/null 2&>1
  [ $? -ne 0 ] && notify_user "Group $1 not exists" "error" && exit 1
  exit 0
}

validate_email() {
  $(LANG=C; validate_input "Bad admin email." "$1" \
    "^[a-z0-9!#\$%&'*+/=?^_\`{|}~-]+(\.[a-z0-9!#$%&'*+/=?^_\`{|}~-]+)*@([a-z0-9]([a-z0-9-]*[a-z0-9])?\.)*[a-z0-9]([a-z0-9-]*[a-z0-9])?\$")     || exit 1
  exit 0
}

validate_subdomain() {
  $(LANG=C; validate_input "Bad subdomain" "$1" \
    "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
  exit 0
}

getopt --test > /dev/null
if [ "$?" -gt 4 ];then
  echo 'I’m sorry, `getopt --test` failed in this environment.'
else
  OPTIONS="w:d:a:ghi"
  LONGOPTS="webmaster:,domain:,adminemail:,gui,help,webroot:,webgroup:,install,subdomain:,skipmysql,mysqlauto"
  ! PARSED=$(getopt --options=$OPTIONS --longoptions=$LONGOPTS --name "$0" -- "$@")
  if [[ ${PIPESTATUS[0]} -ne 0 ]]; then
    # e.g. return value is 1
    # then getopt has complained about wrong arguments to stdout
    exit 2
  fi
  # read getopt’s output this way to handle the quoting right:
  eval set -- "$PARSED"
  while true; do
    case "$1" in
        "--skipmysql")
            skipmysql=1
            shift
            ;;
        "--mysqlauto")
            mysqlauto=1
            shift
            ;;
        "-w"|"--webmaster")
            webmaster="$2"
            webmaster_set=1
            shift 2
            ;;
        "--webgroup")
            webgroup="$2"
            webgroup_set=1
            shift 2
            ;;
        "--subdomain")
            host="$2"
            host_set=1
            shift 2
            ;;
        "-d"|"--domain")
            maindomain="$2"
            maindomain_set=1
            shift 2
            ;;
        "-a"|"--adminemail")
            serveradmin="$2"
            serveradmin_set=1
            shift 2
            ;;
        "--webroot")
            declare -i webroot="$2"
            webroot_set=1
            shift 2
            ;;
        "-g"|"--gui")
            if [ -z "$DISPLAY" ];then
              notify_user "GUI failed. No DISPLAY." "error"
              exit 1
            else
              gui_set=1
              # GUI can be enabled at this point, when run from terminal /
              # with --gui, so check again
              $(check_gui) || exit
              in_terminal() {
                false
              }
            fi
            shift
            ;;
        "-i"|"--install")
            install_set=1
            shift
            ;;
        "-h"|"--help")
            cat << EOF
usage: ./${0}                    # if some arguments specified in command
                                 # line, script will not ask to input it value
                                 # --install of script makes some values predefined
       [--webmaster="userlogin"] # user with permissions to edit www
                                 # files. group is 'www-data'
       [--webgroup="www-data"]   # group of apache2 service user
       [--domain=".localhost"]   # leading dot required
       [--subdomain="Example"]   # Subdomain and webroot folder name
                                 # (folder case censetive)
       [--adminemail="admin@localhost"]
       [--webroot="0"]           # documentroot of virtualhost, zero based index
       # webroots[0]="/home/\${webmaster}/Web/\${subdomain}"
       # webroots[1]="/var/www/\${subdomain}"
       # webroots[2]="/var/www/\${webmaster}/\${subdomain}"
       [--skipmysql]             # don't create mysql db and user
       [--mysqlauto]             # use subdomain as mysql db name, username, empty password
       [--gui]                   # run gui version from terminal else be autodetected without this.
                                 # attemps to install zenity

       ${0}
       --help                    # this help

       ${0}
       --gui --install           # install .desktop shortcut for current user
                                 # and optionally copy script to $home/bin
                                 # --install requires --gui
                                 # shortcut have few options to run, without mysql for example
       ${0} "without arguments"  # will read values from user
EOF
            exit 0
            ;;
        --)
            shift
            break
            ;;
        *)
            echo "Error" >&2
            exit 3
            ;;
    esac
  done
  if [ "$install_set" ]; then
    ! [ "$gui_set" ] && notify_user "--gui must be set with --install" && exit 1
    homedir=$( getent passwd "$(id -un)" | cut -d: -f6 )

    source="${BASH_SOURCE[0]}"
    while [ -h "$source" ]; do # resolve $SOURCE until the file is no longer a symlink
      dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )"
      source="$(readlink "$source")"
      [[ $source != /* ]] && source="$dir/$source"
      # ^ if $SOURCE was a relative symlink, we need to resolve it relative to
      # the path where the symlink file was located
    done
    dir="$( cd -P "$( dirname "$source" )" >/dev/null 2>&1 && pwd )"
    scriptname=$( basename "$source" )

    # make a form where script args can be predefined, so not needed every launch
    pipestring=$(zenity --forms --add-entry="Domain(leading dot)(Default .localhost)" \
      --add-entry="Webmaster(default: current username)" \
      --add-entry="Webgroup(Default: www-data)" \
      --add-entry="Server admin(Default: webmaster@localhost)" \
      --text="Predefined values(empty=interactive edit)" --title="Desktop shortcut" \
      --add-combo="Install path" --combo-values="${homedir}/bin/|${dir}/" 2>/dev/null)

    # zenity stdout if like value1|value2|etc
    IFS='|' read -r -a form <<< "$pipestring"
    args=""
    if [ "${form[0]}" ]; then $(validate_domain "${form[0]}") || exit 60; fi
    if [ "${form[1]}" ]; then $(validate_username "${form[1]}") || exit 61; fi
    if [ "${form[2]}" ]; then $(validate_group "${form[2]}") || exit 62; fi
    if [ "${form[3]}" ]; then $(validate_email "${form[3]}") || exit 63; fi
    if [ "${form[4]}" ]; then mkdir -p "${form[4]}"; fi

    [ "${form[0]}" ] && args="$args --domain='${form[0]}'"
    [ "${form[1]}" ] && args="$args --webmaster='${form[1]}'"
    [ "${form[2]}" ] && args="$args --webgroup='${form[2]}'"
    [ "${form[3]}" ] && args="$args --adminemail='${form[3]}'"
    installpath="${dir}/${scriptname}"
    if [ "${form[4]}" != " " ]; then installpath="${form[4]}${scriptname}"; fi

    cp "${dir}/${scriptname}" "$installpath" >/dev/null 2>&1
    chmod u+rx "$installpath"

    desktop="$homedir/.local/share/applications/virtualhost.desktop"
    cat >"$desktop" <<EOF
[Desktop Entry]
Version=1.0
Name=Create Virtualhost
Comment=Easy to create new virtualhost for apache2 server
Keywords=Virtualhost;Apache;Apache2
Exec=/bin/bash ${installpath} ${args}
Terminal=false
X-MultipleArgs=true
Type=Application
Icon=network-wired
Categories=GTK;Development;
StartupNotify=false
Actions=without-arguments;new-install

[Desktop Action without-arguments]
Name=Clean launch
Exec=/bin/bash ${installpath}

[Desktop Action without-mysql]
Name=Without mysql config
Exec=/bin/bash ${installpath} ${args} --skipmysql

[Desktop Action new-install]
Name=Reinstall (define new configuration)
Exec=/bin/bash ${installpath} --install --gui
X-MultipleArgs=true
EOF

    exit 0
  fi

  # if arguments passed - validate them.
  msg=""
  if [ "$maindomain_set" ]; then
    $(validate_domain "$maindomain") \
      || msg="Bad value for --domain. Should have leading dot.  \".localhost\"\n"
  fi
  if [ "$serveradmin_set" ]; then
    $(validate_email "$serveradmin") || msg="Bad value for --adminemail\n$msg"
  fi
  if [ "$host_set" ]; then
    $(validate_subdomain "$host") || msg="Bad value for --subdomain\n$msg"
  fi
  if [ "$webmaster_set" ]; then
    $(validate_username "$webmaster") || msg="Bad value for --webmaster\n$msg"
  fi
  if [ "$webgroup_set" ]; then
    $(validate_group "$webgroup") || msg="Bad value for --webgroup\n$msg"
  fi
  have_command apache2 || msg="Apache2 not installed\n$msg"
  if [ "$msg" ];then
    [ "$gui_set" ] && info_user "$msg" "error"
    exit 1
  fi
fi

if [ "$(id -un)" == "root" ];then
   notify_user "You should not run this script as root but as user with 'sudo' rights." "error"
   exit 1
fi

get_text_input() {
    if in_terminal; then
        defaulttext=""
        [ "$3" ] && defaulttext="[Default: ${3}]"
        read -p "${2}$defaulttext" -r input
        # default
        [ "$3" ] && [ -z "$input" ] && input="$3"
    else
       input=$(zenity --entry="$1" --title="Virtualhost" --text="$2" --entry-text="$3" 2>/dev/null)
       if [ "$?" == "1" ]; then
         echo "[$1]Cancel button" 1>&2
         exit 1     # Cancel button pressed
     fi
    fi
    if [ -z "$4" ]; then
      case "$input" in
          "")            notify_user "[$1]Bad input: empty" "error" ;      exit 1 ;;
          *"*"*)         notify_user "[$1]Bad input: wildcard" "error" ;   exit 1 ;;
          *[[:space:]]*) notify_user "[$1]Bad input: whitespace" "error" ; exit 1 ;;
      esac
    fi
    echo "$input"
}

# get input and validate it
if [ -z "$host" ]; then host=$(get_text_input "Subdomain" "Create virtualhost (= Folder name,case sensitive)" "") || exit; fi
$(validate_subdomain "$host") || exit

if [ -z "$maindomain_set" ]; then maindomain=$(get_text_input "Domain" "Domain with leading dot." "$maindomain") || exit; fi
$(validate_domain "$maindomain") || exit

if [ -z "$webmaster_set" ]; then webmaster=$(get_text_input "Username" "Webmaster username" "$webmaster") || exit; fi
$(validate_username "$webmaster") || exit

if [ -z "$serveradmin_set" ]; then serveradmin=$(get_text_input "Admin email" "Server admin email" "$serveradmin") || exit; fi
$(validate_email "$serveradmin") || exit

homedir=$( getent passwd "$webmaster" | cut -d: -f6 )
# webroot is a choise of predefined paths array
declare -a webroots=("${homedir}/Web/${host}" "/var/www/${host}" "/var/www/${webmaster}/${host}")
zenitylistcmd=""
# zenily list options is all columns of all rows as a argument, one by one
for (( i=0; i<${#webroots[@]}; i++ ));do
  if in_terminal; then
    echo "[${i}] ${webroots[$i]}" # reference for text read below
  else
    zenitylistcmd="${zenitylistcmd}${i} ${i} ${webroots[$i]} "
  fi
done
dir=""
[ -z "$webroot_set" ] && if in_terminal; then
  webroot=$(get_text_input 'Index' 'Website folder' "$webroot") || exit
else
  webroot=$(zenity --list --column=" " --column="Idx" --column="Path" --hide-column=2 \
    --hide-header --radiolist --title="Choose web folder" $zenitylistcmd 2>/dev/null)
fi
if [ -z "${webroots[$webroot]}" ]; then notify_user "Invalid webroot index"; exit 1; fi

dir="${webroots[$webroot]}"          # folder used as document root for virtualhost
hostfile="${apachehost}${host}.conf" # apache virtualhost config file

# virtualhost template
cat >"$tmphost" <<EOF
<VirtualHost ${virtualhost}:${virtualport}>
    ServerAdmin $serveradmin
    DocumentRoot $dir
    ServerName  ${host}${maindomain}
    ServerAlias ${host}${maindomain}
    <Directory "$dir">
      AllowOverride All
      Require local
    </Directory>
    # Available loglevels: trace8, ..., trace1, debug, info, notice, warn,
    # error, crit, alert, emerg.
    LogLevel error
</VirtualHost>
# vim: syntax=apache ts=4 sw=4 sts=4 sr noet
EOF

find_editor() {
  local editor=${VISUAL:-$EDITOR}
  if [ "$editor" ]; then
    echo "$editor"
    return
  fi

  for cmd in nano vim vi pico; do
    if have_command "$cmd"; then
      echo "$cmd"
      return
    fi
  done
}

if in_terminal; then
  # edit virtualhost config
  editor=$(find_editor)
  if [ -z "$editor" ];then
      echo "$tmphost:"
      cat  $tmphost
      echo "edit '$tmphost' to your liking, then hit Enter"
      read -p "I'll wait ... "
  else
      "$editor" "$tmphost"
  fi
else
  # edit virtualhost config GUI
  text=$(zenity --text-info --title="Virtualhost config" --filename="$tmphost" --editable 2>/dev/null)
  if [ -z "$text" ];then
    # cancel button pressed
    exit 0
  fi
  echo "$text" > "$tmphost"
fi
# probably want some validating here that the user has not broken the config
# apache will not reload config if incorrect

mysqlskip="$skipmysql"  # skip if --skipmysql set
[ -z "$mysqlskip" ] && if have_command mysqld; then
  if [ -z "$mysqlskip" ]; then mysqladminpwd=$(get_text_input "Admin password" "Admin password (${mysqladmin})" "" "skipcheck") || mysqlskip=1; fi
  if [ "$mysqlauto" ]; then
    mysqldb="$host"
    mysqluser="$host"
    mysqlpwd=""
  else
    if [ -z "$mysqlskip" ]; then
      mysqldb=$(get_text_input "Database" "Database name(Enter for default)" "$host") || mysqlskip=1
    fi
    if [ -z "$mysqlskip" ]; then
      mysqluser=$(get_text_input "Username" "Mysql user for db:$mysqldb host:localhost(Enter for default)" "$mysqldb") || mysqlskip=1
    fi
    if [ -z "$mysqlskip" ]; then
      mysqlpwd=$(get_text_input "Password" "Mysql password for user:$mysqluser db:$mysqldb host:localhost(Enter for empty)" "" "skipcheck") || mysqlskip=1
    fi
  fi

  if [ -z "$mysqlskip" ]; then
    tmpmysqlinit=$(mktemp)
    trap 'rm "$tmpmysqlinit"' EXIT
    cat >"$tmpmysqlinit" <<EOF
CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS \`${mysqldb}\` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON \`${mysqldb}\`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;
EOF
  fi
fi

getsuperuser () {
  if in_terminal;then
    echo "sudo"
  else
    echo "pkexec"
  fi
}

notify_user "execute root commands with $(getsuperuser) to create virtualhost" "warning"

tmpresult=$(mktemp)
trap 'rm "$tmpresult"' EXIT
tmpmysqlresult=$(mktemp)
trap 'rm "$tmpmysqlresult"' EXIT

$(getsuperuser) /bin/bash <<EOF
mkdir -p "$dir"
chown ${webmaster}:${webgroup} "$dir"
chmod u=rwX,g=rXs,o= "$dir"
cp "$tmphost" "$hostfile"
chown root:root "$hostfile"
chmod u=rw,g=r,o=r "$hostfile"
a2ensite "${a2ensite}${host}.conf"
systemctl reload apache2
echo "\$?" > "$tmpresult"
if [ -z "${mysqlskip}" ]; then
  systemctl start mysql
  mysql --user="$mysqladmin" --password="$mysqladminpwd" <"$tmpmysqlinit"
  echo "\$?" > "$tmpmysqlresult"
fi
EOF

if [ $(cat "$tmpmysqlresult") ]; then $mysqlresult="\nMysql db,user created"; fi
if [ $(cat "$tmpresult") ]; then notify_user "Virtualhost added. Apache2 reloaded.${mysqlresult}"; fi
\$\endgroup\$
  • 2
    \$\begingroup\$ apt has a warning (somewhere) about an unstable CLI that's optimized for interactive use and not recommended for scripts. apt-get is a good choice for a script, and may solve your hanging problem. \$\endgroup\$ – Oh My Goodness Mar 8 at 19:19
2
\$\begingroup\$

As bash scripts go, this is well done. You've made pretty good use of bash features and the code clearly reflects time spent debugging and refining.

There are two big things I would do differently:

  1. I wouldn't use bash. As the number of code paths and lines of code increase, access to a real debugger and static syntax checking outweigh the convenience of a shell.
  2. I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.

What's done is done, though, and here are some changes I'd recommend to the code as-is:

refactor the death pattern

There's a lot of code like this:

if [ -z "$DISPLAY" ];then
    notify_user "GUI failed. No DISPLAY." "error"
    exit 1
fi

That could be shortened by using an error-handling function:

[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."

This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:

die() {
    notify_user "$1" "${2:-error}"
    exit ${3:-1}
}

tempfiles considered harmful

Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.

The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.

mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS \`${mysqldb}\` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON \`${mysqldb}\`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit

If the quoting is hairy, you can cat a heredoc and capture with $( … ):

foo=$( 
    cat <<EOF
…
EOF
)

find_editor() could be a lot shorter

Instead of:

find_editor() {
  local editor=${VISUAL:-$EDITOR}
  if [ "$editor" ]; then
    echo "$editor"
    return
  fi

  for cmd in nano vim vi pico; do
    if have_command "$cmd"; then
      echo "$cmd"
      return
    fi
  done
}

Just:

find_editor() { 
    type -P $VISUAL $EDITOR nano vim vi pico | head -1
}

refactor the "if I have" pattern

Instead of:

if have_command zenity; then
    zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
    return
fi

How about:

try() { 
    have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return

check for errors

What happens if cp "${dir}/${scriptname}" "$installpath" only copies half of the script before the disk fills up? Probably nothing good.

Consider set -e to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.

some of these $( ) invocations are weird

You have a bunch of $(function) as the first argument on a command line, which runs the output of function as a second command. That's hacky but kind-of okay, except function doesn't produce any output. So it's just weird and it's going to break if function ever writes anything to stdout.

If you want to contain the effects of an exit, use plain () without the dollar sign. Better yet, use return in the function, instead of exit, and omit the parens altogether.

little stuff

$(LANG=C; validate_input "Bad subdomain" "$1" \
    "^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1

A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).

>& /dev/null is bash shorthand for > /dev/null 2>&1.

args="$args --domain='${form[0]}'" can be replaced by args+=" --domain=${form[0]}"

Declare your read-only globals as such (declare -r x=y) to avoid accidentally clobbering them.

\$\endgroup\$
  • \$\begingroup\$ I'm going this way: "quote: Develop a solid non-interactive tool, then build a GUI on top of it", but have difficulty to pass password from GUI tool to script, question related to it: stackoverflow.com/questions/55084251/… \$\endgroup\$ – LeonidMew Mar 10 at 3:52
  • \$\begingroup\$ I don't understand that question at all. If password is unset on command line, then read a line from stdin. Sounds easy. \$\endgroup\$ – Oh My Goodness Mar 10 at 10:46
  • \$\begingroup\$ I'm going to make non interactive tool(and separate GUII script on top of it), so password should be set by arguments or somehow else \$\endgroup\$ – LeonidMew Mar 10 at 13:04
  • 1
    \$\begingroup\$ read it from an environment variable or file. Let the user specify which with a command-line option. \$\endgroup\$ – Oh My Goodness Mar 10 at 13:17
  • \$\begingroup\$ Is it temporally files more harmful then running whole script as root, not just few lines of it? I'm try to avoid asking for root permissions by pkexec more then once thats why use temp file, then use heredoc to run root commands. \$\endgroup\$ – LeonidMew Mar 22 at 15:49
3
\$\begingroup\$

Some suggestions:

  • Because of length and complexity alone I would say Bash is not a good fit for this purpose. A language like Python will be much better at validating strings, argument parsing, escaping SQL and especially error handling. Case in point:
  • Your traps are clobbering each other. See this example:

    $ trap echo EXIT
    $ trap ls EXIT
    $ trap -p
    trap -- 'ls' EXIT
    
  • ".localhost" is not a domain - "localhost" is, and the dot is the separator between the hostname and domain.
  • The exit trap should be set up before creating the temporary directory, to avoid a race condition.
  • Shell scripts should not be interactive unless absolutely required (such as top or vim). Instead of asking the user to install some software I would make Zenity an optional dependency of this script, and possibly warn the user about it being missing if necessary. This will shorten and simplify your script considerably.
  • Shell scripts should not run sudo (or equivalent). Privilege escalation should always be at the discretion of the user, so the script should simply fail with a useful message if it can't do what it needs to because of missing privileges.
  • notify_user could just as well echo "$@" to support any number of arguments, or could also include useful information such as the time of the message if you use printf instead.
  • A more portable shebang line is #!/usr/bin/env bash.
  • Use [[ rather than [.
  • Run the script through shellcheck to get more linting tips.
\$\endgroup\$
  • \$\begingroup\$ Quote "Shell scripts should not run sudo (or equivalent).". Only few commands need root permissions, may be add an option to elevate privileges inside script? \$\endgroup\$ – LeonidMew Mar 22 at 15:54
  • 1
    \$\begingroup\$ That makes the script break if you don't specify the option as a normal user, and makes it more complicated than it has to be. You could split this into two scripts, one which does the root work. \$\endgroup\$ – l0b0 Mar 22 at 19:32

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.