Dart Lesson 92 of 102 4 min read
Building Command-Line Apps in Dart
Build command-line tools in Dart: read arguments and options with the args package, use stdin, stdout and exit codes, and compile to an executable.
On this page
Dart is an excellent language for command-line tools. It starts quickly, runs on Windows, macOS and Linux, and compiles to a single file that runs without Dart installed.
Create the project #
dart create -t cli word_count
cd word_count
dart run
The entry point is bin/word_count.dart.
Arguments #
Whatever follows the program name arrives in main as a list of strings.
void main(List<String> arguments) {
if (arguments.isEmpty) {
print('Usage: greet <name>');
return;
}
print('Hello, ${arguments.join(' ')}!');
}
dart run bin/greet.dart Asha Rai
Hello, Asha Rai!
Standard output, standard error and exit codes #
A well-behaved tool follows three conventions.
| Stream or value | Use for |
|---|---|
stdout | The actual result of the program |
stderr | Errors and diagnostics, so they do not pollute piped output |
| Exit code | 0 for success, anything else for failure |
import 'dart:io';
void main(List<String> arguments) {
if (arguments.length != 1) {
stderr.writeln('Usage: line_count <file>');
exitCode = 64; // "command used incorrectly"
return;
}
final file = File(arguments.first);
if (!file.existsSync()) {
stderr.writeln('Error: ${file.path} not found');
exitCode = 66; // "input not found"
return;
}
stdout.writeln(file.readAsLinesSync().length);
}
Setting exitCode and returning lets the program finish cleanly. Calling exit(1) stops immediately and can cut off pending output, so prefer exitCode.
Scripts and CI systems rely on the exit code to know whether your tool worked.
Parsing options with the args package #
Hand-parsing --verbose and -n 5 gets messy fast. Use the official args package.
dart pub add args
import 'dart:io';
import 'package:args/args.dart';
void main(List<String> arguments) {
final parser = ArgParser()
..addOption('name', abbr: 'n', help: 'Who to greet.', defaultsTo: 'World')
..addOption('times', abbr: 't', help: 'How many times.', defaultsTo: '1')
..addFlag('shout', abbr: 's', help: 'Use capital letters.', negatable: false)
..addFlag('help', abbr: 'h', help: 'Show this help.', negatable: false);
final ArgResults options;
try {
options = parser.parse(arguments);
} on FormatException catch (e) {
stderr.writeln(e.message);
stderr.writeln(parser.usage);
exitCode = 64;
return;
}
if (options.flag('help')) {
print('Usage: greet [options]\n${parser.usage}');
return;
}
final times = int.tryParse(options.option('times')!) ?? 1;
var message = 'Hello, ${options.option('name')}!';
if (options.flag('shout')) message = message.toUpperCase();
for (var i = 0; i < times; i++) {
print(message);
}
}
dart run bin/greet.dart --name Asha -t 2 --shout
HELLO, ASHA!
HELLO, ASHA!
- An option takes a value:
--name Ashaor-n Asha. - A flag is on or off:
--shout. - Anything left over is in
options.rest.
Interactive input #
import 'dart:io';
void main() {
stdout.write('Delete 3 files? [y/N] ');
final answer = stdin.readLineSync()?.trim().toLowerCase();
if (answer == 'y' || answer == 'yes') {
print('Deleted.');
} else {
print('Cancelled.');
}
}
Reading piped input #
Tools are often chained together with |. Read everything from stdin as a stream of lines.
import 'dart:convert';
import 'dart:io';
Future<void> main() async {
var lines = 0, words = 0;
await for (final line in stdin.transform(utf8.decoder).transform(const LineSplitter())) {
lines++;
words += line.split(RegExp(r'\s+')).where((w) => w.isNotEmpty).length;
}
print('$lines lines, $words words');
}
cat README.md | dart run bin/wc.dart
Environment variables and platform #
import 'dart:io';
void main() {
final apiKey = Platform.environment['API_KEY'];
if (apiKey == null) {
stderr.writeln('Set the API_KEY environment variable first.');
exitCode = 78;
return;
}
print('Running on ${Platform.operatingSystem} with ${Platform.numberOfProcessors} cores');
}
Read secrets from environment variables. Do not pass them as arguments, where they show up in shell history.
Running other programs #
import 'dart:io';
Future<void> main() async {
final result = await Process.run('git', ['status', '--short']);
if (result.exitCode != 0) {
stderr.write(result.stderr);
exitCode = result.exitCode;
return;
}
final changed = (result.stdout as String).trim();
print(changed.isEmpty ? 'Working tree is clean' : changed);
}
Compile to a standalone program #
dart compile exe bin/greet.dart -o greet
./greet --name Dart
The output is a single native executable. Users do not need the Dart SDK. It only runs on the operating system it was built on, so build once on each platform you support.
To install a tool for your own use from a package, run dart pub global activate --source path ..
Checklist for a pleasant tool #
- Support
--help, and print usage when arguments are wrong. - Results to
stdout, messages tostderr. - Exit with a non-zero code on failure.
- Do not ask questions when input is piped, or offer a
--yesflag. - Keep
mainthin: parse arguments there, and put the real work in functions underlib/so it can be tested.
Try it yourself #
Build todo, a command-line to-do list that stores tasks in a JSON file. Support todo add "Buy milk", todo list, todo done 2 and todo --help. Return a non-zero exit code for an unknown command.