Bash and Make
2024-01-01 | ๐ 1230 words | โฑ 13 mins | ๐งพ History | โ NHS Notify | ๐ NHS Notify
Known Issues / Todo
- โ This page is draft and is subject to rapid change, and may not be fully accurate or complete
Developer Guide: Bash and Make
Using Make
Sample make target signature definition:
some-target: # Target description - mandatory: foo=[description]; optional: baz=[description, default is 'qux'] @Category
# Recipe implementation...
some-target
: This is the name of the target you would specify when you want to run this particular target. Use the kebab-case naming convention and prefix with an underscore_
to mark it as a โprivateโ target. The first part of the name is used for grouping, e.g.docker-*
orterraform-*
.Target Description
: Provided directly after the target name as a single line, so be concise.mandatory
parameters: Parameters that must be provided when invoking the target. Each parameter has its own description. Please follow the specified format as it is used bymake help
.optional
parameters: Parameters that are not required when invoking the target. They may have a default value. Each parameter has its own description.@Category
label: Used for grouping by themake help
command.Recipe implementation
: This section defines the actual commands or steps the target will execute. Do not exceed 5 lines of effective code. For more complex operations, use a shell script. Refer to thedocker-build
implementation in the docker.mk file. More complex operations are implemented in the docker.sh script for readability and simplicity.
Run make target from a terminal:
foo=bar make some-target # Environment variable is passed to the make target execution process
make some-target foo=bar # Make argument is passed to the make target execution process
By convention we use uppercase variables for global settings that you would ordinarily associate with environment variables. We use lower-case variables as arguments to call functions or make targets, in this case.
All make targets should be added to the ${VERBOSE}.SILENT:
section of the make
file, which prevents make
from printing commands before executing them. The ${VERBOSE}
prefix on the .SILENT:
special target allows toggling it if needed. If you explicitly want output from a certain line, use echo
.
It is worth noting that by default, make
creates a new system process to execute each line of a recipe. This is not the desired behaviour for us and the entire content of a make recipe (a target) should be run in a single shell invocation. This has been configured in this repository by setting the .ONESHELL:
special target in the scripts/init.mk
file.
To see all available make targets, run make help
.
Using Bash
When working in the command-line ensure the environment variables are reset to their initial state. This can be done by reloading shell using the env -i $SHELL
command.
Sample Bash function definition:
# Short function description
# Arguments (provided as environment variables):
# foo=[description]
# baz=[description, default is 'qux']
function some-shell-function() {
# Function implementation...
Run Bash function from a terminal:
source scripts/a-suite-of-shell-functions
foo=bar some-shell-function # Environment variable is accessible by the function executed in the same operating system process
source scripts/a-suite-of-shell-functions
foo=bar
some-shell-function # Environment variable is still accessible by the function
Run Bash script from a terminal, bear in mind that executing a script creates a child operating system process:
# Environment variable has to be exported to be passed to a child process, DO NOT use this pattern
export foo=bar
scripts/a-shell-script
# or to be set in the same line before creating a new process, prefer this pattern over the previous one
foo=bar scripts/a-shell-script
# or when multiple variables are required
foo=bar \
baz=qux \
scripts/a-shell-script
By convention we use uppercase variables for global settings that you would ordinarily associate with environment variables. We use lower-case variables as arguments to be passed into specific functions we call, usually on the same line, right before the function name.
The command set -euo pipefail
is commonly used in the Bash scripts, to configure the behavior of the script in a way that makes it more robust and easier to debug. Here is a breakdown of each option switch:
-e
: Ensures that the script exits immediately if any command returns a non-zero exit status.-u
: Makes the script exit if there is an attempt to use an uninitialised variable.-o pipefail
: ensures that if any command in a pipeline fails (i.e., returns a non-zero exit status), then the entire pipeline will return that non-zero status. By default, a pipeline returns the exit status of the last command.
Make and Bash working together
Sample make target calling a Bash function. Notice that baz
is going to be accessible to the function as it is executed in the same operating system process:
some-target: # Run shell function - mandatory: foo=[description]
source scripts/a-suite-of-shell-function
baz=qux
some-shell-function # 'foo' and 'baz' are accessible by the function
Sample make target calling another make target. In this case a parameter baz
has to be passed as a variable to the make target, which is executed in a child process:
some-target: # Call another target - mandatory: foo=[description]
baz=qux \
make another-target # 'foo' and 'baz' are passed to the make target
Run it from a terminal:
foo=bar make some-target
Conventions
Debugging
To assist in investigating scripting issues, the VERBOSE
variable is available for both Make and Bash scripts. If it is set to true
or 1
, it prints all the commands that the script executes to the standard output. Here is how to use it:
for Make targets
VERBOSE=1 make docker-example-build
for Bash scripts
VERBOSE=1 scripts/shellscript-linter.sh
Scripts
Most scripts provided with this repository template can use tools installed on your PATH
if they are available or run them from within a Docker container. To force a script to use Docker, the FORCE_USE_DOCKER
variable is provided. This feature allows you to use custom tooling if it is present on the command-line path. Here is how to use it:
FORCE_USE_DOCKER=1 scripts/shellscript-linter.sh
You can combine it with the VERBOSE
flag to see the details of the execution flow:
VERBOSE=1 FORCE_USE_DOCKER=1 scripts/shellscript-linter.sh