I'm writing a bash script that has these goals:
- to build a node program from sources
- to install the built program on Ubuntu Linux
- to allow the user to rebuilt from most recent sources
- to add a small CLI (it has none)
- to add a small GUI for support (like report-bug,fix common errors, etc.)
So, it's pretty complete in my opinion, and the good thing is, it works well. But what concerns me is that the code seems chaotic and not optimized, even if it's entirely fine from a user's point-of-view.
Here's the code's resume:
#Part1 : VARIABLES 11 variables I use for URLs, version number, log-files, etc. #Part 2 : FUNCTIONS 9 functions. - 4 of them are quite simple and "regular" ones, for example "error checking" and "exit if the user is root" - 1 of them contains almost only text to write inside text-files - 4 of them call other function #part 3 : SCRIPT - a 'case' to watch options like "-update" - the list of 7 functions that I need to run everytime with or without the susmentionned 'options' Total = 409 lines (~150 without all the simple text to write somewhere or to display)
Here's the full code. You can also watch what it does for the user on YouTube:
#!/bin/bash
installdir="/opt"
version="dev-0.3"
OfficialURL="http://get-popcorn.com"
githubURL="https://github.com/popcorn-official/popcorn-app"
issueURL="https://github.com/popcorn-official/popcorn-app/issues"
icon="https://github.com/popcorn-official/popcorn-app/raw/master/src/app/images/icon.png"
log="$HOME/popcorn-build.log"
buildscriptURL="https://raw.githubusercontent.com/MrVaykadji/misc/master/Popcorn-Time/0.3.0/"
buildscript="build-popcorn"
[ $(arch) == "x86_64" ] && arch=64 || arch=32
buildtime="`date +%Y.%m.%d-%Hh%M`"
#FUNCTIONS
func_apt() {
for lock in synaptic update-manager software-center apt-get "dpkg " aptitude
do
if ps -U root -u root u | grep "$lock" | grep -v grep > /dev/null; then
echo "
Unexpected Error:
=================
Please close $lock then try again.";
exit 1
fi
done
}
func_root() {
[ "$EUID" == "0" ] &&
echo "Error. You need to run this without 'root' or 'sudo' privileges." &&
exit 2
}
func_error() {
[ -n $error ] && return 0
echo "
Unexpected Error:
================="
cat $log
echo "
Please try again."
exit 1
}
func_clean() {
case $1 in
-save)
sudo mkdir -p /tmp/popcorn-config
sudo cp -r $HOME/.config/Popcorn-Time/data /tmp/popcorn-config/ &> /dev/null
sudo rm -rf $HOME/.config/Popcorn-Time/*
sudo cp -r /tmp/popcorn-config/data $HOME/.config/Popcorn-Time/ &> /dev/null &&
sudo chown -Rf $USER:$USER $HOME/.config/Popcorn-Time/data &&
sudo chmod -R 774 $HOME/.config/Popcorn-Time/data
;;
-all)
sudo rm -rf $installdir/Popcorn-Time /usr/share/pixmaps/popcorntime.png /usr/bin/popcorn-time $HOME/tmp $HOME/popcorn-app-$version $HOME/npm-debug.log $HOME/.npm $HOME/.cache/bower $HOME/.config/configstore/insight-bower.yml $HOME/.config/configstore/update-notifier-bower.yml $HOME/.local/share/bower $log $HOME/$version.zip
;;
-package)
sudo apt-get purge nodejs -y &> /dev/null &&
sudo apt-get autoremove -y &> /dev/null &&
sudo rm -rf /usr/bin/node &&
sudo add-apt-repository -yr ppa:chris-lea/node.js &> /dev/null &&
echo -e "... Done.\n"
;;
-building)
sudo rm -rf $HOME/tmp $HOME/popcorn-app-$version $HOME/npm-debug.log $HOME/.npm $HOME/.cache/bower $HOME/.config/configstore/insight-bower.yml $HOME/.config/configstore/update-notifier-bower.yml $HOME/.local/share/bower $log &&
echo -e "... Done.\n"
;;
esac
}
func_ptexists() {
if [ "$1" == "-update" ] ; then
func_clean -save
else
[ -e "$installdir/Popcorn-Time" ] &&
read -p "
WARNING: Popcorn-Time is already installed in '$installdir' and will be erased. Do you want to keep the configuration files (bookmarks, watched list, settings, ...) [y/n] ? "
if [ "$REPLY" == "y" ] ; then
func_clean -save
else
sudo rm -rf $HOME/.config/Popcorn-Time/
fi
sudo rm -rf /usr/share/applications/popcorn-time.desktop
fi
func_clean -all
}
func_dependencies() {
[[ -n `egrep -v '^#|^ *$' /etc/apt/sources.list /etc/apt/sources.list.d/* | grep chris-lea/node.js` ]] && nodeppa=1 || nodeppa=0
if [ -n "`dpkg-query -W -f='${Status}\n' nodejs wget unzip | grep not`" ] || [ $nodeppa == "0" ] ; then
echo "- Checking for dependencies 'nodejs', 'wget' and 'unzip'..."
sudo apt-add-repository -y ppa:chris-lea/node.js &> $log &&
sudo apt-get update &> $log &&
sudo apt-get install nodejs wget unzip -y &> $log && echo -e " ...Ok !" || error=1
func_error
fi
#npm dep
if [ -e "/usr/lib/node_modules/bower" ] && [ -e "/usr/lib/node_modules/grunt-cli" ] ; then
echo -e "\n- Updating NPM 'grunt-cli' and 'bower'..."
else
echo -e "\n- Installing NPM 'grunt-cli' and 'bower'..."
fi
sudo npm install -g grunt-cli bower &> $log && echo -e " ...Ok !\n" || error=1
func_error
#repair broken nodejs symlink
[ ! -e /usr/bin/node ] && sudo ln -s /usr/bin/nodejs /usr/bin/node
#symlink libudev.so on 12.04
[ `lsb_release -cs` == "precise" ] && [ ! -e /lib/$(arch)-linux-gnu/libudev.so.1 ] && sudo ln -s /lib/$(arch)-linux-gnu/libudev.so.0 /lib/$(arch)-linux-gnu/libudev.so.1
}
func_build() {
#get sources
echo "- Downloading '$version' sources from GitHub..."
cd
wget $githubURL/archive/$version.zip -O $version.zip &> $log && unzip -o $version.zip &> $log && rm $version.zip && echo -e " ...Ok !\n" || error=1
func_error
#npm
cd popcorn-app-$version
echo "- Running 'npm install'..."
sudo chown -Rf $USER:$USER $HOME/popcorn-app-$version/ $HOME/tmp
npm install --yes &> $log && echo -e " ...Ok !\n" || error=1
func_error
#build
if [ "$1" == "-update" ] ; then
buildcommand="linux$arch"
else
buildvar=0
echo -e "You can build for this machine only (linux$arch) or for all plateforms, including : Mac, Windows, Linux 32-bits, Linux 64-bits.\n\nFor what platforms do you wish to build (for multiple builds, separate each platform with a comma)"
read -p "[mac/win/linux32/linux64/all] : " input
IFS=',' read -a options <<< "$input"
shopt -s extglob
for option in "${options[@]}"; do
case "$option" in
win|mac|linux32|linux64|all)
buildcommand="${buildcommand:+$buildcommand,}$option"
buildvar=1;;
*)
printf 'Invalid option "%s" ignored.\n' "$option";;
esac
done
if (( !buildvar )); then
echo "Incorrect input. Default build 'linux$arch' selected."
buildcommand="linux$arch"
fi
[[ -n "`echo $buildcommand | grep all`" ]] && buildcommand="all"
fi
echo -e "\n- Building with 'grunt'..."
grunt build --platforms=$buildcommand &> $log && echo -e " ...Ok !\n" || error=1
func_error
echo -e "Popcorn-Time has been built in :\n «$HOME/popcorn-app-$version/build/releases/Popcorn-Time/»\n"
}
func_install() {
[ "`echo $buildcommand | grep -v linux$arch`" ] && exit 0
if [ "$1" != "-update" ] ; then
read -p "Do you wish to install Popcorn-Time on this computer [y/n] ? "
[ "$REPLY" != "y" ] && exit 0
fi
sudo mkdir -p $installdir
sudo cp -r $HOME/popcorn-app-$version/build/releases/Popcorn-Time/linux$arch/Popcorn-Time $installdir
echo -e "\n- Creating commandline launcher..."
echo "#!/bin/bash
echo \"Popcorn Time
============\"
[ \"\$EUID\" == \"0\" ] && echo \"Error: You need to run this without 'root' or 'sudo' privileges.\" && exit 2
helpsection() {
echo \"Version $version
Built on $buildtime from $githubURL
Official website : $OfficialURL
Options:
-h, --help Display this help.
-q,--quiet Launch Popcorn-Time without output.
--flush Flush databases.
--fix-node Fix the node-webkit 'blank' error.
--uninstall Uninstall Popcorn-Time.
--issue Report an issue.
--build Build latest version from sources.\"
}
flush_all() {
echo \"- Flushing databases...\"
sudo rm -rf $HOME/.config/Popcorn-Time
}
uninstall() {
echo \"- Uninstalling Popcorn-Time and removing configuration files...\"
sudo bash $installdir/Popcorn-Time/uninstall.sh
}
popcorntimequiet() {
echo \"Starting...\"
nohup $installdir/Popcorn-Time/Popcorn-Time &> /dev/null &
exit 0
}
popcorntime() {
$installdir/Popcorn-Time/Popcorn-Time
}
reportissue() {
echo \"Here is what a great bug report looks like:
###############################
Describe the problem here
Version: $version for Linux $arch bits
Built on $buildtime
Downloaded from: $githubURL
OS: `lsb_release -si` `lsb_release -sr` `arch`
Connection: X mbps
How to reproduce:
- Step 1
- Step 2
- Step 3
Actual result:
- X goes wrong
Expected result:
- X should go like that
###############################\"
xdg-open $issueURL &> /dev/null
}
fix_node() {
echo \"Fixing node-webkit...\"
rm -rf $HOME/.config/node-webkit
}
build_pt() {
cd
echo \"Building script fetched from GitHub...\"
wget -q $buildscriptURL$buildscript
bash $buildscript -update
}
case \$1 in
-h|--help)
helpsection
;;
--uninstall)
uninstall
;;
--flush)
flush_all
;;
--fix-node)
fix_node
;;
--issue)
reportissue
;;
-q|--quiet)
popcorntimequiet
;;
--build)
build_pt
;;
*)
popcorntime
;;
esac" | sudo tee /usr/bin/popcorn-time &> /dev/null
sudo chmod +x /usr/bin/popcorn-time
echo -e " «/usr/bin/popcorn-time»\n"
echo "- Creating launcher... "
sudo wget $icon -qO /tmp/popcorntime.png && sudo cp /tmp/popcorntime.png /usr/share/pixmaps/
echo "[Desktop Entry]
Comment=Watch movies in streaming with P2P.
Comment[fr]=Regarder des films en streaming.
Name=Popcorn Time
Exec=/usr/bin/popcorn-time
StartupNotify=false
Type=Application
Icon=popcorntime
Actions=ForceClose;ReportIssue;FlushDB;FixNode;BuildUpdate;
Keywords=P2P;streaming;movies;tv;series;shows;
Keywords[fr]=P2P;streaming;films;séries;télévision;tv;
[Desktop Action ForceClose]
Name=Force close
Name[fr]=Forcer la fermeture
Exec=killall Popcorn-Time
OnlyShowIn=Unity;
[Desktop Action ReportIssue]
Name=Report Issue
Name[fr]=Rapporter un problème
Exec=sh -c \"popcorn-time --issue\"
OnlyShowIn=Unity;
[Desktop Action FlushDB]
Name=Flush databases
Name[fr]=Vider les bases de données
Exec=sh -c \"killall Popcorn-Time ; rm -rf $HOME/.config/Popcorn-Time ; /usr/bin/popcorn-time\"
OnlyShowIn=Unity;
[Desktop Action FixNode]
Name=Fix Node-Webkit
Name[fr]=Réparer Node-Webkit
Exec=sh -c \"rm -rf $HOME/.config/node-webkit ; killall Popcorn-Time ; /usr/bin/popcorn-time\"
OnlyShowIn=Unity;
[Desktop Action BuildUpdate]
Name=Build latest version
Name[fr]=Construire la dernière version
Exec=sh -c 'killall Popcorn-Time ; xterm -fa monaco -fs 13 -bg black -fg white -title \"Build latest Popcorn Time\" -e \"popcorn-time --build\" ; /usr/bin/popcorn-time'
OnlyShowIn=Unity;" | sudo tee /usr/share/applications/popcorn-time.desktop &> /dev/null
sudo chmod +x /usr/share/applications/popcorn-time.desktop
echo -e " «/usr/share/applications/popcorn-time.desktop»\n"
echo "- Creating uninstall script..."
echo "#!/bin/bash
#uninstallation script for Popcorn-Time
#clean directory
sudo rm -rf $installdir/Popcorn-Time
#clean config
sudo rm -rf $HOME/.config/Popcorn-Time
#clean icon
sudo rm -rf /usr/share/pixmaps/popcorntime.png
#clean launchers
sudo rm -rf /usr/bin/popcorn-time
sudo rm -rf /usr/share/applications/popcorn-time.desktop
" | sudo tee $installdir/Popcorn-Time/uninstall.sh &> /dev/null
sudo chmod +x $installdir/Popcorn-Time/uninstall.sh
echo -e " «$installdir/Popcorn-Time/uninstall.sh»\n"
}
func_end() {
if [ "$buildcommand" == "linux$arch" ] ; then
if [ "$1" == "-update" ] ; then
func_clean -building
sudo rm -rf $0
else
read -p "Do you wish to remove all the building files [y/n] ? "
[ "$REPLY" == "y" ] && func_clean -building
fi
fi
if [ "$nodeppa" == "0" ] ; then
read -p "Do you wish to uninstall the packages installed for this build, they will be needed in case of new build [y/n] ? "
[ "$REPLY" == "y" ] && func_clean -package
fi
}
#SCRIPT#
func_root
func_apt
echo "
Popcorn-Time $version for Ubuntu-Linux
=====================================
Popcorn Time streams movies from Torrents.
Downloading copyrighted material may be illegal in your country.
!!! Use at your own risk !!!
"
sudo test
case $1 in
-update)
option="-update"
;;
*)
[ -n "$1" ] && echo -e "\nUnauthorized option '$1' will be ignored."
;;
esac
func_ptexists $option
func_dependencies
func_build $option
func_install $option
func_end $option
echo "=================================================
Popcorn-Time is now installed !
Type «popcorn-time --help» for more information."
exit 0
My questions concern some techniques that I use:
I call functions from within other functions. Can I do that?
Is 150 lines too long (without text to display and commentaries) for this kind of code?
Should I split the code if I can? Leaving, for example, one script for the build that will download the installation script and the needed text-files, and another that will only be downloaded when the user wants to rebuild, ... these kinds of things.
I would be grateful if you could skim through the code and give me an honest opinion about its globality.