Every now and then we have to establish a new development environment on our machine. Installing lots of tools can get really cumbersome as we are obliged to remember what we need and in what order the installations should be run. In this post I will show you an example bash script that I use for installing a base set of tools. The script can be easily customized and you can fork it on GitHub and adjust to your needs. I strongly recommend using this in your company to make sure that every programmer will be able to install all tools required to work on a given project in a matter of minutes.
We will bundle installation of Angular, Docker, Guake, Node.js 12 (with npm) and OpenJDK 11 in a single bash script. As a result we will be able to set up everything with one simple command:
$ sudo ./install.sh guake docker openjdk nodejs angular
The script will also provide us with verification messages for all tools installed. In case of any failure the script will exit and the error message will be printed in the console.
The finished project is available in the GitHub repository – little-pinecone/ubuntu-web-dev-tools-installer.
I divided the script into separate files to ease the customization and to adhere to the Open/Closed Principle. The support
directory contains variables and helpers. In the tools
directory we have one installation and one verification script for every tool (e.g. angular.sh
, angular_verify.sh
). Testing the script is possible thanks to the custom Docker image enclosed. The install.sh
file holds the logic for the overall process and refers to the other files when needed. You can see the project tree on the image below:
$ lsb_release -d Description: Ubuntu 18.04.3 LTS
install.sh
file alongside with the scripts
directory to your machine, or even better, clone the whole repository:
$ git clone https://github.com/little-pinecone/ubuntu-web-dev-tools-installer.git
$ chmod 744 install.sh $ ls -la -rwxrwxr-- 1 root user 1123 Jan 15 17:09 install.sh
$ ./install.sh -h
apt update -y
command is performed automatically. Therefore, you are not required to update the system on your own.sudo
, providing files from the scripts/tools
directory as parameters. Do not specify the files extension. In the following example we install Angular after Node.js, because Node is required if you want to use Angular:
$ sudo ./install.sh nodejs angular guake
You will be prompted to accept the command:
-v
option. If you call a non existing script, the error message will be printed. You can see an example command below:
$ ./install.sh -v nodejs angular guake nonexisting
Requires docker to be already installed on your machine. Checkout scirpts/tools/docker and run $ sudo ./install.sh docker if the script satisfies your needs. |
The project contains Dockerfile
and docker-compose.yml
files to provide you with an Ubuntu18.04
instance where you can test the script. All *.sh
files are already copied to the /home
directory inside the container.
Start the container based on the provided image:
$ docker-compose up -d
Enter the container:
$ docker exec -it ubuntuwebdevtoolsinstaller_test_1 /bin/bash
List the /home
directory content to see that you can access the install.sh
file and the scripts
directory:
/home # ls -la total 16 drwxr-xr-x 1 root root 4096 Jan 15 14:45 . drwxr-xr-x 1 root root 4096 Jan 15 14:46 .. -rwxrwxr-- 1 root root 1129 Jan 15 13:21 install.sh drwxr-xr-x 4 root root 4096 Jan 15 14:45 scripts
You can test the ./install.sh
command without risking unwanted changes on your machine. Do not use sudo
inside the container, run the script like in the example below:
$ ./install.sh nodejs angular guake
If you’ve changed the image, or any script that you intend to test there, remember to remove the container and rebuild the image with the following commands:
$ docker container rm -f ubuntuwebdevtoolsinstaller_test_1 $ docker-compose build
Then you can create a new container and enter it:
$ docker-compose up -d $ docker exec -it ubuntuwebdevtoolsinstaller_test_1 /bin/bash
You can fork the project and add your own installation and verification scripts — it is easy to keep a whole “library” of these scripts in your GitHub repository. Every time you call the $ sudo ./install.sh
command you may pass any file that is suitable for you at the moment as a parameter.
Remember:
*.sh
file — it will be easier to debug failures and retry installation;_verify
suffix added to the same name as used in the installation file;scripts/tools/
directory;support
directory.We could keep all the code in a single *.sh
file. However, I wanted to ease adding new scripts according to the future needs. Therefore, the main install.sh
file uses the source
command to execute scripts from separate files in the same shell:
#!/bin/bash … source ./scripts/support/verify_installation.sh …
From the source
manual:
$ source --help Execute commands from a file in the current shell. … Exit Status: Returns the status of the last command executed in FILENAME; fails if FILENAME cannot be read.
The scripts still share settings and variables, but it is easier to customize and maintain the code.
When we are combining many commands to be automatically run one after another, we have to be able to terminate the script and debug in case any of them fails . In the install.sh
file we put the following settings for the shell:
#!/bin/bash set -e source ./scripts/support/debug.sh …
From the set
command manual we know that the option -e
terminates the execution in case of any error:
$ set --help Set or unset values of shell options and positional parameters. Options: … -e Exit immediately if a command exits with a non-zero status.
Then we include the debug.sh
file containing the code responsible for tracking the last executed command and printing its exit code:
trap 'last_command=$BASH_COMMAND' DEBUG trap 'echo $debug"\"${last_command}\" - the last command exited with code $?. $reset_colour"' EXIT
In order to see which command failed and what was the cause, we save information about the command that is currently being run as the last_command
during the DEBUG
SIGNAL_SPEC, and on EXIT
signal we print that command and its exit code to the console. From the trap
command manual:
$ trap --help Defines and activates handlers to be run when the shell receives signals or other conditions. … If a SIGNAL_SPEC is EXIT (0) ARG is executed on exit from the shell. If a SIGNAL_SPEC is DEBUG, ARG is executed before every simple command.
E.g. if we run our script with the following command:
$ ./install.sh -guake -docker -openjdk -nodejs -angular
and we got information about failure of a command executed from the openjdk.sh
file, we know that all previous commands were run without errors and, after fixing the bug, we can resume the installation by typing:
$ ./install.sh -openjdk -nodejs -angular
into the terminal.
When no errors occurred, we will see the last command printed with the 0
exit code; for my install.sh
script the last line of the console output will be:
"echo "$debug END OF THE install_tools.sh SCRIPT. $reset_colour"" - the last command exited with code 0.
To make the code readable you can always extract some logic into a separate block of code. Remember to define the function before it is used, like in the example from the install.sh
file:
… function install_tool() { if [ -e ./scripts/tools/"$file".sh ]; then source ./scripts/tools/"$file".sh else echo "$error Installation script not found for: $file. $reset_colour" echo "" fi } … for file in $tools; do install_tool done …
To enable users to call your scripts with options, we need to read them in a while loop using getops
, like in the example from the install.sh
file:
while getopts thv option; do case "${option}" in t) test=true ;; h) source ./scripts/support/help.sh exit 0 ;; v) verify_only=true ;; *) echo "$error Unknown option used. Type './install.sh -h' to see the available options. $reset_colour " exit 1 ;; esac done
getopts
is used by shell scripts to parse positional parameters. In my case, the thv string contains the characters that will be recognized as valid and put one after another in subsequent iterations into the option
variable.
Calling the command with the -h
option will result in executing commands enclosed in the help.sh
file (printing the manual) and exiting with code 0
:
Bash keeps the position of the latest argument in the OPTIND
variable.
From the bash documentation:
Each time it is invoked,
getopts
places the next option in the shell variable name, initializing name if it does not exist, and the index of the next argument to be processed into the variableOPTIND
.OPTIND
is initialized to 1 each time the shell or a shell script is invoked.
After we had traversed through the available options (thv
) we want to set position to the beginning of the rest of the arguments passed to the command (file names). We have to do it manually as stated in the docs:
The shell does not reset
OPTIND
automatically; it must be manually reset between multiple calls togetopts
within the same shell invocation if a new set of parameters is to be used.
We are going to apply the shift [n]
command that shifts the positional parameters to the left by n
. We want to start reading parameters from the position next to the one that was reached by the while loop — the first file name passed to the script. To remove all the options that have been already parsed by getopts
we use the following line:
shift "$((OPTIND-1))"
Just for illustration, calling the $ ./install.sh -v openjdk
command without shifting this position would result in something like this:
The -v
option was read again, the second time as a parameter for a file containing a verification script.
I use the array with file names in several places, therefore I assigned it to a custom variable, and then printed them for the confirmation message:
tools=$@ echo "$info The following tools will be installed: $tools. Would you like to continue?: y/n (y) $reset_colour"
In the docker_verify.sh
file you can find two simple if
statements. First one checks whether the test
option was omitted while calling the script; second one checks whether our user confirmed running a test docker container:
… if [ "$test" != true ]; then echo " Would you like to run test docker container?: y/n (n)" read -r decision if [ "$decision" = y ]; then echo "$(docker run hello-world)" fi elif [ "$test" = true ]; then echo "$(docker run hello-world)" fi
As you can see, the script will run a test docker container automatically if the -t
option was passed when calling the command. In other case, we can ask a user directly for their confirmation before running any testing command.
In the install.sh
script we have the following function sourcing the files that we are required to have in the /scripts/tools
directory:
function install_tool() { if [ -e ./scripts/tools/"$file".sh ]; then source ./scripts/tools/"$file".sh else echo "$error Installation script not found for: $file. $reset_colour" echo "" fi }
Furthermore, we assigned parameters left after reading options to the tools
variable:
tools=$@
Finally, we can easily call the function in a for
loop that iterates over the elements in the tools
variable:
for file in $tools; do install_tool done
Use the -e
expression to determine whether the file exists:
if [ -e ./scripts/tools/"$file".sh ]; then source ./scripts/tools/"$file".sh else echo "$error Installation script not found for: $file. $reset_colour" echo "" fi
Use the -z
expression to verify whether a string is empty, like in the example below:
echo "$info The following tools will be installed: $tools. Would you like to continue?: y/n (y) $reset_colour" read -r confirmation if [ "$confirmation" = y ] || [ -z "$confirmation" ]; then apt update -y for file in $tools; do install_tool done fi
According to the code above, a user can confirm the default decision “y” simply by pressing Enter. The confirmation variable will be null in that case, which results in proceeding with installation process.
I use the tput
command with the setaf
option to determine colours of the messages you can see in the console. In the support/colours.sh
file you will find all variables I use to set or reset output colours:
reset_colour=$(tput sgr0) error=$(tput setaf 1) info=$(tput setaf 4) success=$(tput setaf 2) debug=$(tput setaf 3)
To print a blue message, I start it with the $info
variable. At the end of the message i put the $reset_colour
variable to leave the rest of the output in the default colour:
echo "$info Tools installation summary$reset_colour"
You can change the colours as you wish, however I recommend to name variables according to their meaning and not necessarily the specific colour they are holding at the moment. After all, if you decide to print the debug messages in gray, you won’t have to fix every single occurrence of that variable in the code.
Photo by Anggoro Sakti on StockSnap
Spring Security allows us to use role-based control to restrict access to API resources. However,…
A custom annotation in Spring Boot tests is an easy and flexible way to provide…
Delegating user management to Keycloak allows us to better focus on meeting the business needs…
Swagger offers various methods to authorize requests to our Keycloak secured API. I'll show you…
Configuring our Spring Boot API to use Keycloak as an authentication and authorization server can…
Keycloak provides simple integration with Spring applications. As a result, we can easily configure our…