Do you have a development task that is mostly a series of commands? Do you have to perform that task on a regular basis? If so, a script might be just what you need. Over the years I’ve wanted to learn bash scripting several times. Yet there’s always something else that takes priority and my desire to script takes a back seat. Not any more!
I finally decided to start learning how to write bash scripts and I want to share it with you in case you’d like to do the same. At the bottom of this post, I’ve listed several resources that I’m using to help me on this journey. If you’re an Android developer, you can use Gradle to handle many automated tasks. If you want to learn about Gradle on Android or how to create a Gradle plugin, check out my course here.
On to the script. It does the following:
- Runs the unit and widget tests for a Flutter application
- Generates a code coverage report
- Removes the unwanted files from the coverage output
- Converts the coverage data to HTML
- Opens the HTML page in your browser
I’m going to break down what each section of the script does so that you can start having fun creating your own scripts! (This post assumes you are already familiar with programming and related concepts.)
The Breakdown
#!/usr/bin/env bash
The first line specifies that we want to use bash to run our script. There are other shells available for writing scripts like zsh, ksh, or fish. But for this script, we’re using bash.
red=$(tput setaf 1)
none=$(tput sgr0)
These lines create two global variables that we will use later in our script for displaying error messages. They both use the tput command which helps you to format text on the command line. First, we use it to set the foreground color of text to red. The number 1
represents the color red in tput
‘s world. Then later we use it to clear our changes and go back to the default text settings.
Displaying Help
show_help() {
printf "usage: $0 [--help] [--report][--test] [<path to package>]
Script for running all unit and widget tests with code coverage.
(run from root of repo)
where:
<path to package>
runs all tests with coverage and reports
-t, --test
runs all tests with coverage, but no report
-r, --report
generate a coverage report
(requires lcov, install with Homebrew)
-h, --help
print this message
requires lcov command, install with Homebrew
"
}
Next, we create our first function, show_help
. This function prints to the screen the help documentation for the script. There is one string that contains information about the options for running the script. One thing that to note here is the use of the $0
positional variable. It represents the name of the script. When this command is executed, the name of the script will be included in the output string. See below for an example.
Running Tests
run_tests() {
if [[ -f "pubspec.yaml" ]]; then
rm -f coverage/lcov.info
rm -f coverage/lcov-final.info
flutter test --coverage
else
printf "\n${red}Error: this is not a Flutter project${none}"
exit 1
fi
}
Next up, the run_tests
function. If we find a pubspec.yaml
file in the current directory we remove the existing coverage files and then execute the tests. If you’re not familiar with Flutter, the pubspec.yaml
file is where we specify the dependencies for our app.
However, if we don’t find the pubspec.yaml
file, we assume that you are not inside a Flutter project and print an error message to the screen. This is where we use our red
and none
variables that we created earlier to set the text of the error message to red. You can see that below.
Generating a Report
Moving on to the run_report
function. We first do a check for if the code coverage file exists, with if [[ -f "coverage/lcov.info" ]]; then
. If it does then we do a little bit of fancy lcov
work.
lcov -r coverage/lcov.info lib/resources/l10n/\* lib/\*/fake_\*.dart \
-o coverage/lcov-final.info
genhtml -o coverage coverage/lcov-final.info
open coverage/index-sort-l.html
We take the generated lcov.info file and remove files that we don’t want to be included in the final code coverage report. In my case, I don’t want the localized strings (lib/resources/l10n/\*
) or any of the fakes (lib/\*/fake_\*.dart
) that I’ve created for debugging. Then we create a new file lcov-final.info to include the paired down coverage information.
With our new coverage file, we use the genhtml
command to turn it into an HTML page. And finally, we open that index page that has been created in the coverage directory with our default browser. Here’s an example of a generated report:
I’m still working on adding more tests, so don’t judge me on the current coverage numbers 😉
Giving the User Options
The final thing we do in our script is handle the user’s input. To do that we use a bash case
statement. If you’re familiar with any of the popular programming languages the bash case
statement is comparable to a switch
statement. It has the following format:
case $input in
pattern) commands ;;
esac
You open with the case
keyword and then close with the esac
keyword, which is just case spelled backward. Then you include the cases by providing a pattern followed by a parenthesis, the commands you want to execute, and then a closing ;;
. The ;;
is like a break
statement in a switch
.
Below is the case
statement for our script. It contains four different cases:
- Display the help information (-h or —help)
- Run the tests (-t or —test)
- Generate the report (-r or —report)
- Run the tests and generate the report (nothing or anything else)
case $1 in
-h|--help)
show_help
;;
-t|--test)
run_tests
;;
-r|--report)
run_report
;;
*)
run_tests
run_report
;;
esac
The input to the case
statement is the $1
positional variable. This is the first argument that is directly after the name of the script on the command line. If you have the following on the command line:
./tests_with_coverage.sh --help
then the value of $1
will be --help
. Now when the user calls our script we can handle the various options that they send in. By default, if they simply call ./tests_with_coverage.sh
we will fall into the *)
case and run the tests along with the report.
It was cool to investigate bash scripting finally. Thanks to @nocnoc for the inspiration (his blog post is below). I hope this helps you to get started as well!