# Generate a Dart CLI with Very Good CLI

> Unlock the potential of Dart in the command line with Very Good CLI

- Source: https://verygood.ventures/blog/generate-command-line-application-cli/
- Published: 2022-08-24
- Author: Renan Araújo
- Tags: Best Practices, Open Source

---

There are few resources more crucial to software development than [command-line interfaces (CLIs)](https://en.wikipedia.org/wiki/Command-line_interface). As one of the simplest forms of [Human-Computer Interface (HCI)](https://www.interaction-design.org/literature/book/the-glossary-of-human-computer-interaction/interaction-styles), a CLI allows you to simplify complex tasks and can be used to compose rich applications. Most developers use CLIs every day. Even Flutter comes with one: [**flutter_tools**](https://github.com/flutter/flutter/tree/master/packages/flutter_tools).

Knowing how to build a command-line application can unlock a world of possibilities for a Dart developer. There are several excellent examples of CLI packages out there, for instance: [**mason_cli**](https://pub.dev/packages/mason_cli), [**fvm**](https://pub.dev/packages/fvm), [**dart_frog_cli**](https://pub.dev/packages/dart_frog_cli), and [**umbra**](https://pub.dev/packages/umbra_cli).

Creating one from scratch is not an easy task though. If you're making a CLI package for the first time, you may face some challenges, such as handling exceptions and keeping the code organized. Due to Dart’s flexible nature, it can take some time to figure out how to organize the code, make it testable, and configure the details of what makes a CLI application that is not only good, but *very good*.

[Our team](https://verygood.ventures/resources/open-source-packages/) has gathered some experience building CLI tools and turned them into a package template, so that you don’t have to go through the same challenge. You can simply start creating great stuff.

**This is a guide on designing a kick-ass CLI application written in Dart using the** [**Very Good Dart CLI**](https://github.com/VeryGoodOpenSource/very_good_dart_cli) **template. Let’s go!**

## Getting started

[Very Good CLI](https://pub.dev/packages/very_good_cli) is a tool made by Very Good Ventures that, among other things, helps to ease the process of setting up any type of Dart project, including Flutter applications, [federated plugins](https://verygood.ventures/blog/generate-flutter-plugins-with-very-good-cli/), Flutter and Dart packages, and of course, Dart CLI applications.

If you didn’t yet, activate it via:

```shell
$ dart pub global activate very_good_cli
```

To take the first step into the *Dart-CLI-making* world, use the **create** command with the appropriate template:

```shell
$ very_good create dart_cli my_simple_cli
```

And voila! You now have generated a CLI package with everything ready to get started: a sample command with colorful outputs, a plug-and-play update mechanism, a file structure that scales, and last but not least, everything stamped with that [beautiful unicorn wearing shades](https://twitter.com/VGVentures/status/1554116996542889984): testable!

Now, let's look at the generated file structure.

## The anatomy of a Dart CLI package

Like everything created using Dart, a CLI application is essentially just a [pub package](https://dart.dev/guides/packages), which defines the basic structure: A **pubspec.yaml** in the project root, accompanied by the directories **lib** and **bin**.

These are three crucial things to be mindful of about the generated project’s structure:

1.  **The lib directory:** This may be familiar if you've worked with Dart before. This is where packages should put the classes and objects that are to be used by your scripts ([library code](https://dart.dev/tools/pub/package-layout#public-libraries)).
2.  **The bin directory:** This, combined with the lib directory [constitute the package's public API](https://dart.dev/tools/pub/package-layout#public-directories): This should include the actual script files, which will call the entry-point function (**main**) when executed. Most CLIs have only one script file.
3.  **And last but not least, the good old pubspec.yaml:** This describes the [metadata of the package](https://dart.dev/tools/pub/pubspec). Note that for CLI applications, there is one additional field: executables. This entry should match the executable name (the first word in a command that your application will interpret) as keys, to files in the bin directory (values). The value can be left blank if the executable's name is the same as its corresponding script file.

The executable name doesn't need to be the same as the package name. To customize that upon generation, use the **\--executable-name** option:

```shell
$ very_good create my_simple_cli -t dart_cli --executable-name my_simple
✓ Generated 19 file(s) (3.3s)
✓ Running "flutter pub get" in my_simple_cli (3.4s)
✓ Running "dart fix --apply" in my_simple_cli (3.2s)
Created a Very Good Dart CLI application! 🦄
```

Resulting **pubspec.yaml** file:

```yaml
name: my_simple_cli
# ...
executables:
  my_simple:
```

Then, after [activation](https://dart.dev/tools/pub/cmd/pub-global#activating-a-package) through the **dart pub global activate** command, the CLI application will be invoked by commands that start with **my\_simple**.

## What makes a CLI delightful

Before going deep into the code, let's look at a few essential components of CLI design and the common patterns that most CLI applications follow.

Although CLI apps have a very simple visual interface, we can't ignore UX completely. Some classical design principles such as familiarity, hierarchy, and consistency, can make your CLI app easier to use.

### Familiarity

People may be used to some expected outputs and input formats that are common in most CLIs. For example, it is almost intuitive to type the flag **\-h** at the end of a command to understand what it does and the options available.

Example of a great help message: **dart pub add**:

```shell
$ dart pub add -h
Add dependencies to pubspec.yaml.
Usage: dart pub add [:<constraint>] [[:<constraint>]...] [options]
-h, --help               Print this usage information.
-d, --dev                Adds to the development dependencies instead.
    --git-url            Git URL of the package
    --git-ref            Git branch or commit to be retrieved
    --git-path           Path of git package in repository
    --hosted-url         URL of package host server
...
```

### Hierarchy

CLI applications that have multiple scopes of actions may split into numerous sub-commands. Example: **commit** and **remote** are two sub-commands of the [git CLI](https://git-scm.com/book/en/v2/Getting-Started-The-Command-Line). Once you type them, you enter their context. The following options, arguments, and sub-commands will be specific to the context of the given command.

### Consistency

Sometimes, different commands may share similar options and argument formats, but people expect consistency across them. For example, **flutter build** has several platforms as subcommands, like **ios** and **android**. The user can execute each of these in either debug or release. For that, both accept **\--debug** or **\--release** as flags.

As with any HCI, A CLI is just a series of input and output standards. To know how to build each of these parts is the key to success.

## What makes excellent input syntax

Options? Arguments? Flags? Commands? Oh my! Do not fear these names. They are pretty helpful in understanding the input formats of a CLI.

Let's analyze a command with plenty of these things to understand what is what. For example, imagine one of the most used commands in history: creating a state management package for Flutter.

```shell
$ flutter create barc -t package --description="Just like bloc, but for dogs" -v --no-pub
```

-   **flutter**: This is called the root command or executable. It should be unique in the current path context, so avoid publishing a CLI with existing executable names, like **git** or **ls**. Commands are the ones mapped in the **pubspec.yaml** file under the **executables** property.
-   **create**: After that, you have the sub-command (or just command). Commands are predefined unique words that execute a task in a specific context. Some applications have zero sub-commands (ex: **cp** and **ls**), while others have multiple levels of sub-commands (ex: **git remote add**).
-   **barc**: In this example, it is the name of the package. This is called an argument. Unlike commands, arguments are not predefined, but the preceding command should know how many (if any) arguments are expected from the user.
-   **\-t package**: This is an option. Options are composed of a key (predefined) and a value. The latter can be either predefined by a set of acceptable strings or not. Specifically, in this case, **\-t** accepts only **package**, **app**, and **plugin**, among other values; anything other than what is predefined will result in an error. This option is in a shorthand format. That is why it starts with a single dash and consists of only one letter. It is the same as using **\--template**.
-   **\--description="..."**: This is also an **option**, except this one is not in a shorthand format (not all options need to have a shorthand available). It accepts any value from the user, as opposed to the previous option.
-   **\-v**: Now there are flags. The first one, **\-v**, indicates that this command will output lots of information that otherwise would be hidden. Flags are just like options: they are formatted with dashes and can also have shorthands. The difference is that they don't accept explicit values. Flags are ideal for options that have boolean values.
-   **\--no-pub**: The second flag, as opposed to the previous one, has a true default value. To override that behavior, the user has to use the negated form. For that, it contains a **no** prefix to indicate its value being set explicitly to false.

### The args package

As you may notice, a command syntax can assume several different forms. To specify which are the commands, options, flags, and arguments in an idiomatic way, we have the [**args**](https://pub.dev/packages/args) package. It contains classes and functions that help to bring the same organization and hierarchy of your desired CLI syntax into your code. In the template generated by **very\_good\_cli**, we have some sample commands and options to illustrate that mapping.

The root command, as said earlier, calls the entry-point function on the related file under **bin**. The main function receives a list of strings corresponding to everything in the user input after the root command, separated by space. That entry-point function invokes the **CommandRunner**, the root of our tree of sub-commands.

Each sub-command extends the **Command** class and an [**ArgParser** object](https://pub.dev/documentation/args/latest/args/ArgParser-class.html) describes its syntax:

```dart
/// Example generated by very good CLI
class SampleCommand extends Command {
  SampleCommand({
    Logger? logger,
  }) : _logger = logger ?? Logger() {
    argParser.addFlag(
      'cyan',
      abbr: 'c',
      help: 'Prints the same joke, but in cyan',
      negatable: false,
    );
  }
// ...
```

### Writing valuable “help” documentation

The best way to let the user discover the correct syntax of options and flags for a command is to provide a common “help” flag (**—help** or **\-h**). The **args** package generates the help flag automatically for you, given the option mapping (configured via the **argParser** object) and some overridable fields in the **Command** class, like **name**, **description**, and **argumentsDescription**. Override them in your **Command** class to customize the usage output.

```shell
# Help available in the sample command
$ my_simple sample -h
A sample sub command that just prints one joke
Usage: my_simple sample [arguments]
-h, --help    Print this usage information.
-c, --cyan    Prints the same joke, but in cyan
Run "my_simple help" to see global options.
```

## Enhancing outputs

Now that we learned how to provide a straightforward way for people to interact with your application, it is time to learn the best ways to respond to those inputs. There are two main ways you can communicate the progress and result of a user’s command: the standard output and exit codes.

One package that is useful is [**mason_logger**](https://pub.dev/packages/mason_logger). The template generated from the **very\_good\_cli create -t dart\_cli** has a sample command that shows:

```dart
import 'package:args/command_runner.dart';
import 'package:mason_logger/mason_logger.dart';

class SampleCommand extends Command {
  SampleCommand({
    Logger? logger, // <-- Look here: inject a logger in the constructor to make it more testable.
  }) : _logger = logger ?? Logger() {
    argParser.addFlag(
      'cyan',
      abbr: 'c',
      help: 'Prints the same joke, but in cyan',
      negatable: false,
    );
  }

  @override
  String get description => 'A sample sub command that just prints one joke';

  @override
  String get name => 'sample';

  final Logger _logger; // <-- Look here: a logger to abstract stdout

  @override
  Future run() async {
    var output = 'Which unicorn has a cold? The Achoo-nicorn!';
    if (argResults?['cyan'] == true) {
      output = lightCyan.wrap(output)!;
    }
    _logger.info(output);
    return ExitCode.success.code; // <--- Look here: Exit code
  }
}
```

Notice that it uses **mason\_logger**’s [**Logger**](https://pub.dev/documentation/mason_logger/latest/mason_logger/Logger-class.html) and [**ExitCode**](https://pub.dev/documentation/mason_logger/latest/mason_logger/ExitCode-class.html) classes. Let's understand a little bit about each:

### The standard output

The [standard output](https://en.wikipedia.org/wiki/Standard_streams#Standard_output_\(stdout\)), also known as “write stuff on the user’s screen,” is a vital part of any CLI. [**dart:io**](https://api.dart.dev/stable/2.17.5/dart-io/dart-io-library.html) has a **stdout** getter in which the raw API uses this system resource:

```dart
import 'dart:io' as io;

final stdout = io.stdout;
stdout.writeln("some output");
```

One problem with writing directly at this level is that there is no API to style the output. You will have to use [ANSI escape codes](https://www.lihaoyi.com/post/BuildyourownCommandLinewithANSIescapecodes.html). Also, using it directly in **Command** classes makes it hard to test with unit tests.

The **Logger** class abstracts the styling codes as wrapping API, which can be mocked for tests.

Example of a command that styles output:

```dart
@override
Future run() async {
  _logger.err('This is a error!');
  _logger.warn('This is your last warning!');
  _logger.confirm('Do you really want that?');
  _logger.success('Flawless victory');
  _logger.info(backgroundCyan.wrap(red.wrap('This will be colorful')));
  return ExitCode.success.code;
}
```

Output produced:

![Styled terminal output with error, warning, confirm, success, and colored text](/assets/images/blog/generate-command-line-application-cli/body-0.png)

Example of mocking the logger using **mocktail** on a test:

```dart
import 'package:mocktail/mocktail.dart';
import 'package:mason_logger/mason_logger.dart';

// ...

class MockLogger extends Mock implements Logger {}

test('prints stuff', () async {
  final logger = MockLogger();
  final command = SampleCommand(
    logger: logger,
  );
  final exitCode = await command.run();
  expect(exitCode, ExitCode.success.code);
  verify(
    () => logger.info('Which unicorn has a cold? The Achoo-nicorn!'),
  ).called(1);
});
```

### Exit codes

When a process or script finishes, it returns a code that indicates the circumstances in which it occurred. On most systems, 0 is the code for success, and anything besides that defines an error.

**Mason\_logger**’s **ExitCode** abstracts some error codes and success into constants.

```dart
@override
Future run() async {
  _logger.err('This command will fail');

  /// A service is unavailable.
  ///
  /// This may occur if a support program or file does not exist. This may also
  /// be used as a catch-all error when something you wanted to do does not
  /// work, but you do not know why.
  return ExitCode.unavailable.code;
}
```

Returning something like that rather than some random **int** makes your code more readable.

## Dealing with exceptions

As with any software, A CLI application execution can produce exceptions.

It is essential to deal with all exceptions and provide clear reports to the user on what went wrong aligned with the proper exit code. As an example, the generated CLI application has in the [**CommandRunner**](https://pub.dev/documentation/args/latest/command_runner/CommandRunner-class.html) a **try/catch** statement that trades thrown exceptions for exit codes:

```dart
Future run(Iterable args) async {
  try {
    final topLevelResults = parse(args);
    return await runCommand(topLevelResults) ?? ExitCode.success.code;
  } on FormatException catch (e, stackTrace) {
    // On format errors, show the commands error message, root usage and
    // exit with an error code
    _logger
      ..err(e.message)
      ..err('$stackTrace')
      ..info('')
      ..info(usage);
    return ExitCode.usage.code;
  } on UsageException catch (e) {
    // On usage errors, show the commands usage message and
    // exit with an error code
    _logger
      ..err(e.message)
      ..info('')
      ..info(e.usage);
    return ExitCode.usage.code;
  }
}
```

Since the **CommandRunner** is the root of a command execution, keeping the exception handlers in its **run** method can be more effective.

## Keeping the CLI up to date

One of the hardest things when publishing a CLI package into [pub.dev](http://pub.dev/) is making sure people are always using the most up-to-date version. Users have to check pub for newer versions, and when there is one, the package has to be activated again:

```shell
dart pub global activate my_cli
```

Fortunately, the project generated by the **very\_good\_cli** already includes infrastructure to make this process easier.

Through [**pub_updater**](https://pub.dev/packages/pub_updater) and with the help of [**build_version**](https://pub.dev/packages/build_version), all commands always check for newer versions of the CLI package on pub.dev. If there are any, the user will receive an alert to update:

![Terminal output: Update available! 0.0.1 -> 0.7.11](/assets/images/blog/generate-command-line-application-cli/body-1.png)

This mechanism works based on the **lib/src/version.dart** file generated by **build\_version**, which makes the current CLI runtime's version accessible. Always make sure to generate that code before publishing new versions of your CLI package to pub.dev.

The sample also includes an update command that uses **pub\_updater** to let the application update itself.

```shell
my_cli update # this will update my_cli
```

## Conclusion

This is just the tip of the iceberg to what is possible to do with a Dart CLI application. We created the Dart CLI template in [Very Good CLI](https://pub.dev/packages/very_good_cli) to provide the basics of what our team thinks any Dart CLI application should include from the start. Since it is an open source project, ideas and suggestions are always welcome. If you have any, head over to [**very_good_dart_cli**](https://github.com/VeryGoodOpenSource/very_good_dart_cli) and open an issue.

Go build something awesome!
