CLI applications made easy with Dart & dcli
Tags: flutter, dart
Published: 2021-11-02 09:15:12



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.

<!--more-->

## 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](https://github.com/orevial/dart-release-cli).

The main steps we will follow are :
1. Write our CLI
1. Run our CLI within Dart environment
1. Create a standalone CLI that can be run as a simple script

Let's start by generating a simple Dart application : 

```dart
dart create dart-release-cli
dart-release-cli
```

Great, we now have a sample project that we can work from :

![Project structure](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/gw870u7nsrrzt3g555qm.png)

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`](https://pub.dev/packages/dcli). And, bonus, the [documentation](https://dcli.noojee.dev/) is pretty cool and rather comprehensive !

Enough talking, let's add this package as a dev dependency. We will also add [`yaml` package](https://pub.dev/packages/yaml), we will need it to easily read our pubspec.

```yaml
#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: 

```dart
#! /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](https://dcli.noojee.dev/getting-started#option-2-install-dart-dcli). Note that this is [dcli recommended solution](https://dcli.noojee.dev/dcli-tools-1/use-a-shebang) but it works just as well with a simple dart shebang (`#! /usr/bin/env dart`) 😉
* `print` (Dart's one) and `printerr` (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](https://dcli.noojee.dev/what-does-dcli-do#user-input), a one-line solution to ask for user input and optionally [validate user input](https://dcli.noojee.dev/dcli-api/user-input/ask-validators) at the same time. 
Another example is the [`write`](https://dcli.noojee.dev/what-does-dcli-do#read-write-files) 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 :

```shell
dart bin/release.dart
```

The second solution, as explained above, is to use [globally installed DCli tools](https://dcli.noojee.dev/getting-started#option-2-install-dart-dcli) or `dart` shebang to run our Dart file as if it were a simple script, so we just have have to execute it :

```shell
# 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`](https://dart.dev/tools/dart-compile) command:

```shell
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 :

![CLI demo](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dcy7n4nxsykc04xrhvej.gif)

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](https://github.com/orevial/dart-release-cli) 👋

---

__References:__

* [`dcli` package](https://pub.dev/packages/dcli)
* [`yaml` package](https://pub.dev/packages/yaml)
* [DCli documentation](https://dcli.noojee.dev/)
* [Dart compile](https://dart.dev/tools/dart-compile)
* [Dart command-line documentation](https://dart.dev/tutorials/server/cmdline#running-an-app-with-the-standalone-dart-vm)