I've been developing Flutter applications for a while now and I love Flutter and Dart, in particular since Dart 2.12 / Flutter 2.0 when null safety was officially released !
Sometimes however, you need a good ol' bash script to run some chore stuff or for your CI... and I have to admit, I suck at writing clean bash scripts.
💭 If only I could write my scripts using the same language I use everyday, the very language I'm the most familiar with at the moment ? 🤔
Today I will show you how we can write command-line applications using only Dart with a few librairies.
What we will do
The goal of our application will be to simply release a new version of our app by updating the version number in our project's pubspec.yaml
. This is something that could later be executed by a CI to automate this, we will do it by hand here for the sake of the example. Also, you can find the code in this repository.
The main steps we will follow are :
- Write our CLI
- Run our CLI within Dart environment
- Create a standalone CLI that can be run as a simple script
Let's start by generating a simple Dart application :
dart create dart-release-cli
dart-release-cli
Great, we now have a sample project that we can work from :
It's now time to add actual CLI-related code !
To do that, we could use Dart's out-of-the-box support for command line code (e.g. reading input or printing output, I/O related operations and so on). The process to do a bunch of CLI-related operations is very well documented by Dart team in this great Dart tutorial "[Write command-line apps]"(https://dart.dev/tutorials/server/cmdline). However I find that default libraries are not so simple to use and I don't like to have a lot of boilerplate code to do simple CLI things such as reading an input or writing to a file.
Fortunately I'm not the only one and so people have created a dedicated wrapper around Dart native CLI code : a package named dcli
. And, bonus, the documentation is pretty cool and rather comprehensive !
Enough talking, let's add this package as a dev dependency. We will also add yaml
package, we will need it to easily read our pubspec.
#pubspec.yaml:
dev_dependencies:
dcli: ^1.9.6
yaml: ^3.1.0
Of course, don't forget to run pub get
to update dependencies.
We can now write our CLI to bin/release.dart
file:
#! /usr/bin/env dcli
import 'dart:io';
import 'package:dcli/dcli.dart';
import 'package:yaml/yaml.dart';
void main() {
// Display output to console
print('''# ------------------------------
# 🚀 Release new version
# ------------------------------''');
// Ask for user input
final newVersion = ask(
'Enter new version number:',
validator: Ask.regExp(
r'^\d+\.\d+\.\d+$',
error: 'You must pass a valid semver version in the form X.Y.Z',
),
);
try {
// Read a file
final pubspec = File('./pubspec.yaml').readAsStringSync();
// Parse yaml content to retrieve current version number
final currentVersion = loadYaml(pubspec)['version'];
print('# Updating from version $currentVersion to version $newVersion...');
// Write to a file
'pubspec.yaml'.write(pubspec.replaceFirst(
'version: $currentVersion',
'version: $newVersion',
));
print('# ✅ Pubspec updated to version $newVersion');
} catch (e) {
// Print or treat error as needed
printerr('# ❌ Failed to update pubspec to version $newVersion\nError: $e');
}
}
And that's it !
I won't go through all the code because it's pretty basic stuff, but a few things can be noted:
- The very first line is a shebang that allows us to run our dart file as if it were a simple shell script. To do that you first need to globally install DCli tools. Note that this is dcli recommended solution but it works just as well with a simple dart shebang (
#! /usr/bin/env dart
) 😉 -
print
(Dart's one) andprinterr
(DCli's one) will both output to the console, respectively to stdout and stderr of course. 📝 - DCli offers cool APIs such as the
ask
operator, a one-line solution to ask for user input and optionally validate user input at the same time. Another example is thewrite
command we use, a String extension that wraps Dart file handling.
Alright, time to run our CLI ! We have quite a few options here.
The first is to run our file using Dart VM :
dart bin/release.dart
The second solution, as explained above, is to use globally installed DCli tools or dart
shebang to run our Dart file as if it were a simple script, so we just have have to execute it :
# Make sure your file is executable first
chmod +x bin/release.dart
# Run as a simple script
./bin/release.dart
Hopefully you had expected output by now ! This is great and allows for quick testing, however you may have noticed that it takes quite some time before the first line is actually displayed. Well, it's normal because the code needs to be compiled before it can be run. Fortunately, Dart offers the ability to generate a platform executable easily using dart compile
command:
dart compile exe bin/release.dart -o release
🕙 Info: Compiling with sound null safety
🕙 Generated: dart-release-cli/release
# Time to run !
./release
We now have an executable that run instantly. In a real world it would probably be the CI job to compile and produce the executable. Also, note that the only drawback is that the executable is much larger than a simple Dart file (about 5MB for our example CLI).
Alright, that's about it ! Here's a demo of the running CLI :
I hope you enjoyed this article where I tried to show you a simple way to generate powerful CLI applications using Dart and a few libraries. And if you want to test the CLI, head over to the Github repository 👋
References: