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
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Mar 8, 2019 at 18:22
\$\endgroup\$
1
  • 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\$ Commented Mar 8, 2019 at 19:19

2 Answers 2

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.

answered Mar 9, 2019 at 1:07
\$\endgroup\$
5
  • \$\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\$ Commented Mar 10, 2019 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\$ Commented Mar 10, 2019 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\$ Commented Mar 10, 2019 at 13:04
  • 1
    \$\begingroup\$ read it from an environment variable or file. Let the user specify which with a command-line option. \$\endgroup\$ Commented Mar 10, 2019 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\$ Commented Mar 22, 2019 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.
answered Mar 8, 2019 at 22:54
\$\endgroup\$
2
  • \$\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\$ Commented Mar 22, 2019 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\$ Commented Mar 22, 2019 at 19:32

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.