Create Apache virtualhost, MySQL db and user, install option - desktop shortcut for GUI version












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 --adminemailn$msg"
fi
if [ "$host_set" ]; then
$(validate_subdomain "$host") || msg="Bad value for --subdomainn$msg"
fi
if [ "$webmaster_set" ]; then
$(validate_username "$webmaster") || msg="Bad value for --webmastern$msg"
fi
if [ "$webgroup_set" ]; then
$(validate_group "$webgroup") || msg="Bad value for --webgroupn$msg"
fi
have_command apache2 || msg="Apache2 not installedn$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









share|improve this question











$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
    yesterday
















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 --adminemailn$msg"
fi
if [ "$host_set" ]; then
$(validate_subdomain "$host") || msg="Bad value for --subdomainn$msg"
fi
if [ "$webmaster_set" ]; then
$(validate_username "$webmaster") || msg="Bad value for --webmastern$msg"
fi
if [ "$webgroup_set" ]; then
$(validate_group "$webgroup") || msg="Bad value for --webgroupn$msg"
fi
have_command apache2 || msg="Apache2 not installedn$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









share|improve this question











$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
    yesterday














2












2








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 --adminemailn$msg"
fi
if [ "$host_set" ]; then
$(validate_subdomain "$host") || msg="Bad value for --subdomainn$msg"
fi
if [ "$webmaster_set" ]; then
$(validate_username "$webmaster") || msg="Bad value for --webmastern$msg"
fi
if [ "$webgroup_set" ]; then
$(validate_group "$webgroup") || msg="Bad value for --webgroupn$msg"
fi
have_command apache2 || msg="Apache2 not installedn$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









share|improve this question











$endgroup$




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 --adminemailn$msg"
fi
if [ "$host_set" ]; then
$(validate_subdomain "$host") || msg="Bad value for --subdomainn$msg"
fi
if [ "$webmaster_set" ]; then
$(validate_username "$webmaster") || msg="Bad value for --webmastern$msg"
fi
if [ "$webgroup_set" ]; then
$(validate_group "$webgroup") || msg="Bad value for --webgroupn$msg"
fi
have_command apache2 || msg="Apache2 not installedn$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






bash






share|improve this question















share|improve this question













share|improve this question




share|improve this question








edited 13 mins ago









Jamal

30.4k11120227




30.4k11120227










asked yesterday









LeonidMewLeonidMew

1477




1477








  • 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
    yesterday














  • 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
    yesterday








2




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
yesterday




$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
yesterday










2 Answers
2






active

oldest

votes


















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.






share|improve this answer









$endgroup$





















    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.






    share|improve this answer











    $endgroup$













      Your Answer





      StackExchange.ifUsing("editor", function () {
      return StackExchange.using("mathjaxEditing", function () {
      StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix) {
      StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
      });
      });
      }, "mathjax-editing");

      StackExchange.ifUsing("editor", function () {
      StackExchange.using("externalEditor", function () {
      StackExchange.using("snippets", function () {
      StackExchange.snippets.init();
      });
      });
      }, "code-snippets");

      StackExchange.ready(function() {
      var channelOptions = {
      tags: "".split(" "),
      id: "196"
      };
      initTagRenderer("".split(" "), "".split(" "), channelOptions);

      StackExchange.using("externalEditor", function() {
      // Have to fire editor after snippets, if snippets enabled
      if (StackExchange.settings.snippets.snippetsEnabled) {
      StackExchange.using("snippets", function() {
      createEditor();
      });
      }
      else {
      createEditor();
      }
      });

      function createEditor() {
      StackExchange.prepareEditor({
      heartbeatType: 'answer',
      autoActivateHeartbeat: false,
      convertImagesToLinks: false,
      noModals: true,
      showLowRepImageUploadWarning: true,
      reputationToPostImages: null,
      bindNavPrevention: true,
      postfix: "",
      imageUploader: {
      brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
      contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
      allowUrls: true
      },
      onDemand: true,
      discardSelector: ".discard-answer"
      ,immediatelyShowMarkdownHelp:true
      });


      }
      });














      draft saved

      draft discarded


















      StackExchange.ready(
      function () {
      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215051%2fcreate-apache-virtualhost-mysql-db-and-user-install-option-desktop-shortcut%23new-answer', 'question_page');
      }
      );

      Post as a guest















      Required, but never shown

























      2 Answers
      2






      active

      oldest

      votes








      2 Answers
      2






      active

      oldest

      votes









      active

      oldest

      votes






      active

      oldest

      votes









      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.






      share|improve this answer









      $endgroup$


















        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.






        share|improve this answer









        $endgroup$
















          2












          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.






          share|improve this answer









          $endgroup$



          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.







          share|improve this answer












          share|improve this answer



          share|improve this answer










          answered yesterday









          Oh My GoodnessOh My Goodness

          1,169213




          1,169213

























              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.






              share|improve this answer











              $endgroup$


















                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.






                share|improve this answer











                $endgroup$
















                  3












                  3








                  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.






                  share|improve this answer











                  $endgroup$



                  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.







                  share|improve this answer














                  share|improve this answer



                  share|improve this answer








                  edited yesterday

























                  answered yesterday









                  l0b0l0b0

                  4,5841023




                  4,5841023






























                      draft saved

                      draft discarded




















































                      Thanks for contributing an answer to Code Review Stack Exchange!


                      • Please be sure to answer the question. Provide details and share your research!

                      But avoid



                      • Asking for help, clarification, or responding to other answers.

                      • Making statements based on opinion; back them up with references or personal experience.


                      Use MathJax to format equations. MathJax reference.


                      To learn more, see our tips on writing great answers.




                      draft saved


                      draft discarded














                      StackExchange.ready(
                      function () {
                      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f215051%2fcreate-apache-virtualhost-mysql-db-and-user-install-option-desktop-shortcut%23new-answer', 'question_page');
                      }
                      );

                      Post as a guest















                      Required, but never shown





















































                      Required, but never shown














                      Required, but never shown












                      Required, but never shown







                      Required, but never shown

































                      Required, but never shown














                      Required, but never shown












                      Required, but never shown







                      Required, but never shown







                      Popular posts from this blog

                      How to make a Squid Proxy server?

                      Is this a new Fibonacci Identity?

                      19世紀