Create Apache virtualhost, MySQL db and user, install option - desktop shortcut for GUI version
$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
bash
$endgroup$
add a comment |
$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
bash
$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
add a comment |
$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
bash
$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
bash
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
add a comment |
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
add a comment |
2 Answers
2
active
oldest
votes
$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:
- 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.
- I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.
What's done is done, though, and here are some changes I'd recommend to the code as-is:
refactor the death pattern
There's a lot of code like this:
if [ -z "$DISPLAY" ];then
notify_user "GUI failed. No DISPLAY." "error"
exit 1
fi
That could be shortened by using an error-handling function:
[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."
This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:
die() {
notify_user "$1" "${2:-error}"
exit ${3:-1}
}
tempfiles considered harmful
Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.
The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.
mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS `${mysqldb}` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `${mysqldb}`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit
If the quoting is hairy, you can cat
a heredoc and capture with $( … )
:
foo=$(
cat <<EOF
…
EOF
)
find_editor()
could be a lot shorter
Instead of:
find_editor() {
local editor=${VISUAL:-$EDITOR}
if [ "$editor" ]; then
echo "$editor"
return
fi
for cmd in nano vim vi pico; do
if have_command "$cmd"; then
echo "$cmd"
return
fi
done
}
Just:
find_editor() {
type -P $VISUAL $EDITOR nano vim vi pico | head -1
}
refactor the "if I have" pattern
Instead of:
if have_command zenity; then
zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
return
fi
How about:
try() {
have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return
check for errors
What happens if cp "${dir}/${scriptname}" "$installpath"
only copies half of the script before the disk fills up? Probably nothing good.
Consider set -e
to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true
to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.
some of these $( )
invocations are weird
You have a bunch of $(function)
as the first argument on a command line, which runs the output of function
as a second command. That's hacky but kind-of okay, except function
doesn't produce any output. So it's just weird and it's going to break if function
ever writes anything to stdout.
If you want to contain the effects of an exit
, use plain ()
without the dollar sign. Better yet, use return
in the function, instead of exit
, and omit the parens altogether.
little stuff
$(LANG=C; validate_input "Bad subdomain" "$1"
"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).
>& /dev/null
is bash shorthand for > /dev/null 2>&1
.
args="$args --domain='${form[0]}'"
can be replaced by args+=" --domain=${form[0]}"
Declare your read-only globals as such (declare -r x=y
) to avoid accidentally clobbering them.
$endgroup$
add a comment |
$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 astop
orvim
). 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 runsudo
(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 wellecho "$@"
to support any number of arguments, or could also include useful information such as the time of the message if you useprintf
instead.- A more portable shebang line is
#!/usr/bin/env bash
.
Use[[
rather than[
.- Run the script through
shellcheck
to get more linting tips.
$endgroup$
add a comment |
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
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
$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:
- 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.
- I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.
What's done is done, though, and here are some changes I'd recommend to the code as-is:
refactor the death pattern
There's a lot of code like this:
if [ -z "$DISPLAY" ];then
notify_user "GUI failed. No DISPLAY." "error"
exit 1
fi
That could be shortened by using an error-handling function:
[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."
This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:
die() {
notify_user "$1" "${2:-error}"
exit ${3:-1}
}
tempfiles considered harmful
Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.
The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.
mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS `${mysqldb}` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `${mysqldb}`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit
If the quoting is hairy, you can cat
a heredoc and capture with $( … )
:
foo=$(
cat <<EOF
…
EOF
)
find_editor()
could be a lot shorter
Instead of:
find_editor() {
local editor=${VISUAL:-$EDITOR}
if [ "$editor" ]; then
echo "$editor"
return
fi
for cmd in nano vim vi pico; do
if have_command "$cmd"; then
echo "$cmd"
return
fi
done
}
Just:
find_editor() {
type -P $VISUAL $EDITOR nano vim vi pico | head -1
}
refactor the "if I have" pattern
Instead of:
if have_command zenity; then
zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
return
fi
How about:
try() {
have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return
check for errors
What happens if cp "${dir}/${scriptname}" "$installpath"
only copies half of the script before the disk fills up? Probably nothing good.
Consider set -e
to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true
to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.
some of these $( )
invocations are weird
You have a bunch of $(function)
as the first argument on a command line, which runs the output of function
as a second command. That's hacky but kind-of okay, except function
doesn't produce any output. So it's just weird and it's going to break if function
ever writes anything to stdout.
If you want to contain the effects of an exit
, use plain ()
without the dollar sign. Better yet, use return
in the function, instead of exit
, and omit the parens altogether.
little stuff
$(LANG=C; validate_input "Bad subdomain" "$1"
"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).
>& /dev/null
is bash shorthand for > /dev/null 2>&1
.
args="$args --domain='${form[0]}'"
can be replaced by args+=" --domain=${form[0]}"
Declare your read-only globals as such (declare -r x=y
) to avoid accidentally clobbering them.
$endgroup$
add a comment |
$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:
- 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.
- I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.
What's done is done, though, and here are some changes I'd recommend to the code as-is:
refactor the death pattern
There's a lot of code like this:
if [ -z "$DISPLAY" ];then
notify_user "GUI failed. No DISPLAY." "error"
exit 1
fi
That could be shortened by using an error-handling function:
[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."
This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:
die() {
notify_user "$1" "${2:-error}"
exit ${3:-1}
}
tempfiles considered harmful
Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.
The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.
mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS `${mysqldb}` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `${mysqldb}`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit
If the quoting is hairy, you can cat
a heredoc and capture with $( … )
:
foo=$(
cat <<EOF
…
EOF
)
find_editor()
could be a lot shorter
Instead of:
find_editor() {
local editor=${VISUAL:-$EDITOR}
if [ "$editor" ]; then
echo "$editor"
return
fi
for cmd in nano vim vi pico; do
if have_command "$cmd"; then
echo "$cmd"
return
fi
done
}
Just:
find_editor() {
type -P $VISUAL $EDITOR nano vim vi pico | head -1
}
refactor the "if I have" pattern
Instead of:
if have_command zenity; then
zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
return
fi
How about:
try() {
have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return
check for errors
What happens if cp "${dir}/${scriptname}" "$installpath"
only copies half of the script before the disk fills up? Probably nothing good.
Consider set -e
to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true
to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.
some of these $( )
invocations are weird
You have a bunch of $(function)
as the first argument on a command line, which runs the output of function
as a second command. That's hacky but kind-of okay, except function
doesn't produce any output. So it's just weird and it's going to break if function
ever writes anything to stdout.
If you want to contain the effects of an exit
, use plain ()
without the dollar sign. Better yet, use return
in the function, instead of exit
, and omit the parens altogether.
little stuff
$(LANG=C; validate_input "Bad subdomain" "$1"
"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).
>& /dev/null
is bash shorthand for > /dev/null 2>&1
.
args="$args --domain='${form[0]}'"
can be replaced by args+=" --domain=${form[0]}"
Declare your read-only globals as such (declare -r x=y
) to avoid accidentally clobbering them.
$endgroup$
add a comment |
$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:
- 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.
- I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.
What's done is done, though, and here are some changes I'd recommend to the code as-is:
refactor the death pattern
There's a lot of code like this:
if [ -z "$DISPLAY" ];then
notify_user "GUI failed. No DISPLAY." "error"
exit 1
fi
That could be shortened by using an error-handling function:
[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."
This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:
die() {
notify_user "$1" "${2:-error}"
exit ${3:-1}
}
tempfiles considered harmful
Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.
The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.
mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS `${mysqldb}` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `${mysqldb}`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit
If the quoting is hairy, you can cat
a heredoc and capture with $( … )
:
foo=$(
cat <<EOF
…
EOF
)
find_editor()
could be a lot shorter
Instead of:
find_editor() {
local editor=${VISUAL:-$EDITOR}
if [ "$editor" ]; then
echo "$editor"
return
fi
for cmd in nano vim vi pico; do
if have_command "$cmd"; then
echo "$cmd"
return
fi
done
}
Just:
find_editor() {
type -P $VISUAL $EDITOR nano vim vi pico | head -1
}
refactor the "if I have" pattern
Instead of:
if have_command zenity; then
zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
return
fi
How about:
try() {
have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return
check for errors
What happens if cp "${dir}/${scriptname}" "$installpath"
only copies half of the script before the disk fills up? Probably nothing good.
Consider set -e
to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true
to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.
some of these $( )
invocations are weird
You have a bunch of $(function)
as the first argument on a command line, which runs the output of function
as a second command. That's hacky but kind-of okay, except function
doesn't produce any output. So it's just weird and it's going to break if function
ever writes anything to stdout.
If you want to contain the effects of an exit
, use plain ()
without the dollar sign. Better yet, use return
in the function, instead of exit
, and omit the parens altogether.
little stuff
$(LANG=C; validate_input "Bad subdomain" "$1"
"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).
>& /dev/null
is bash shorthand for > /dev/null 2>&1
.
args="$args --domain='${form[0]}'"
can be replaced by args+=" --domain=${form[0]}"
Declare your read-only globals as such (declare -r x=y
) to avoid accidentally clobbering them.
$endgroup$
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:
- 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.
- I'd separate the heavy lifting from the GUI. A lot of the complexity and sheer bulk of this script arises from its constant jumping between performing the task and checking in with the user. Develop a solid non-interactive tool, then build a GUI on top of it.
What's done is done, though, and here are some changes I'd recommend to the code as-is:
refactor the death pattern
There's a lot of code like this:
if [ -z "$DISPLAY" ];then
notify_user "GUI failed. No DISPLAY." "error"
exit 1
fi
That could be shortened by using an error-handling function:
[ -n $DISPLAY ] || die "GUI failed. No DISPLAY."
This makes it easy to add cleanup or other functionality to fatal errors. The function can be as simple as:
die() {
notify_user "$1" "${2:-error}"
exit ${3:-1}
}
tempfiles considered harmful
Most tempfile contents are better stored in a variable. Variables avoid all kinds of failure modes—disk full, no permission, file deleted from /tmp by cleanup job—and save you having to worry about if anyone can read the password saved in the mysql temp file.
The only real drawback is that you're limited by memory; variables are unsuitable for gigabytes of data.
mysqlinit="CREATE USER '${mysqluser}'@'localhost' IDENTIFIED BY '${mysqlpwd}';
GRANT USAGE ON *.* TO '${mysqluser}'@'localhost';
CREATE DATABASE IF NOT EXISTS `${mysqldb}` CHARACTER SET utf8 COLLATE utf8_general_ci;
GRANT ALL PRIVILEGES ON `${mysqldb}`.* TO '${mysqluser}'@'localhost';
FLUSH PRIVILEGES;"
…
mysql --user="$mysqladmin" --password="$mysqladminpwd" <<<$mysqlinit
If the quoting is hairy, you can cat
a heredoc and capture with $( … )
:
foo=$(
cat <<EOF
…
EOF
)
find_editor()
could be a lot shorter
Instead of:
find_editor() {
local editor=${VISUAL:-$EDITOR}
if [ "$editor" ]; then
echo "$editor"
return
fi
for cmd in nano vim vi pico; do
if have_command "$cmd"; then
echo "$cmd"
return
fi
done
}
Just:
find_editor() {
type -P $VISUAL $EDITOR nano vim vi pico | head -1
}
refactor the "if I have" pattern
Instead of:
if have_command zenity; then
zenity --notification --text="${prefix}$1" --window-icon="$windowicon"
return
fi
How about:
try() {
have_command "$1" && "$@"
}
…
try zenity --notification --text="${prefix}$1" --window-icon="$windowicon" && return
check for errors
What happens if cp "${dir}/${scriptname}" "$installpath"
only copies half of the script before the disk fills up? Probably nothing good.
Consider set -e
to have bash terminate on error. It's going to be a bit of work, because you'll need to mask ignorable errors (usually by adding || true
to the command). The benefit comes when you get to the end of the script and start doing system configuration, you know that there isn't some early error messing everything up.
some of these $( )
invocations are weird
You have a bunch of $(function)
as the first argument on a command line, which runs the output of function
as a second command. That's hacky but kind-of okay, except function
doesn't produce any output. So it's just weird and it's going to break if function
ever writes anything to stdout.
If you want to contain the effects of an exit
, use plain ()
without the dollar sign. Better yet, use return
in the function, instead of exit
, and omit the parens altogether.
little stuff
$(LANG=C; validate_input "Bad subdomain" "$1"
"^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?$") || exit 1
A domain can't start with a digit. If this is for internal use, consider disallowing caps (or folding them to lowercase).
>& /dev/null
is bash shorthand for > /dev/null 2>&1
.
args="$args --domain='${form[0]}'"
can be replaced by args+=" --domain=${form[0]}"
Declare your read-only globals as such (declare -r x=y
) to avoid accidentally clobbering them.
answered yesterday
Oh My GoodnessOh My Goodness
1,169213
1,169213
add a comment |
add a comment |
$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 astop
orvim
). 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 runsudo
(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 wellecho "$@"
to support any number of arguments, or could also include useful information such as the time of the message if you useprintf
instead.- A more portable shebang line is
#!/usr/bin/env bash
.
Use[[
rather than[
.- Run the script through
shellcheck
to get more linting tips.
$endgroup$
add a comment |
$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 astop
orvim
). 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 runsudo
(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 wellecho "$@"
to support any number of arguments, or could also include useful information such as the time of the message if you useprintf
instead.- A more portable shebang line is
#!/usr/bin/env bash
.
Use[[
rather than[
.- Run the script through
shellcheck
to get more linting tips.
$endgroup$
add a comment |
$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 astop
orvim
). 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 runsudo
(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 wellecho "$@"
to support any number of arguments, or could also include useful information such as the time of the message if you useprintf
instead.- A more portable shebang line is
#!/usr/bin/env bash
.
Use[[
rather than[
.- Run the script through
shellcheck
to get more linting tips.
$endgroup$
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 astop
orvim
). 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 runsudo
(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 wellecho "$@"
to support any number of arguments, or could also include useful information such as the time of the message if you useprintf
instead.- A more portable shebang line is
#!/usr/bin/env bash
.
Use[[
rather than[
.- Run the script through
shellcheck
to get more linting tips.
edited yesterday
answered yesterday
l0b0l0b0
4,5841023
4,5841023
add a comment |
add a comment |
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.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
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
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
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
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