Polish your CLI app
Improve the HelpCommand to provide more detailed information and add an onOutput argument for flexible output handling.
In this chapter, you'll
put the finishing touches on the command_runner package.
You'll refine the HelpCommand to provide more detailed usage information and
add an onOutput argument for more flexible output handling.
This finalizes the CommandRunner package and prepares it for
use in more complex scenarios.
What you'll accomplish
Prerequisites
#Before you begin this chapter, ensure you:
-
Have completed Chapter 7 and have a
working Dart development environment with the
dartpediaproject. - Are familiar with basic programming concepts like variables, functions, and control flow.
- Understand the concepts of packages and libraries in Dart.
- Are familiar with object-oriented programming principles like inheritance and abstract classes.
Tasks
#
You will polish the command_runner package to
make it more robust and user-friendly.
Task 1 Improve the HelpCommand output
#
Enhance the HelpCommand to provide more detailed usage information,
including options and their descriptions.
This will make it easier for users to
understand how to use your CLI application.
Open the
command_runner/lib/src/help_command.dartfile.-
Add imports for
console.dartandexceptions.dartat the top of the file. You need these to use the color extensions and to throw anArgumentException.dartimport 'dart:async'; import 'package:command_runner/command_runner.dart'; import 'console.dart'; import 'exceptions.dart'; -
Replace the existing
runmethod with the following. This new version uses aStringBufferto efficiently build the help string and includes logic to handle verbose output.dart@override FutureOr<String> run(ArgResults args) async { final buffer = StringBuffer(); buffer.writeln(runner.usage.titleText); if (args.flag('verbose')) { for (var cmd in runner.commands) { buffer.write(_renderCommandVerbose(cmd)); } return buffer.toString(); } if (args.hasOption('command')) { var (:option, :input) = args.getOption('command'); var cmd = runner.commands.firstWhere( (command) => command.name == input, orElse: () { throw ArgumentException( 'Input ${args.commandArg} is not a known command.', ); }, ); return _renderCommandVerbose(cmd); } // Verbose is false and no arg was passed in, so print basic usage. for (var command in runner.commands) { buffer.writeln(command.usage); } return buffer.toString(); }StringBufferis a Dart class that allows you to efficiently build strings. It's more performant than using the+operator, especially when performing many concatenations inside a loop. -
Add the
_renderCommandVerboseprivate helper method to theHelpCommandclass. This method formats the detailed output for a single command.dartString _renderCommandVerbose(Command cmd) { final indent = ' ' * 10; final buffer = StringBuffer(); buffer.writeln(cmd.usage.instructionText); //abbr, name: description buffer.writeln('$indent ${cmd.help}'); if (cmd.valueHelp != null) { buffer.writeln( '$indent [Argument] Required? ${cmd.requiresArgument}, Type: ${cmd.valueHelp}, Default: ${cmd.defaultValue ?? 'none'}', ); } buffer.writeln('$indent Options:'); for (var option in cmd.options) { buffer.writeln('$indent ${option.usage}'); } return buffer.toString(); }
Task 2 Add an onOutput callback
#
Next, add an onOutput argument to the CommandRunner to
allow for flexible output handling.
Open the
command_runner/lib/src/command_runner_base.dartfile.-
Add the
onOutputargument to theCommandRunnerconstructor, and add the correspondingonOutputmember to the class.dartclass CommandRunner { CommandRunner({this.onOutput, this.onError}); /// If not null, this method is used to handle output. Useful if you want to /// execute code before the output is printed to the console, or if you /// want to do something other than print output the console. /// If null, the onInput method will [print] the output. FutureOr<void> Function(String)? onOutput; FutureOr<void> Function(Object)? onError; // ... rest of the class } -
Update the
runmethod to use theonOutputargument.dartFuture<void> run(List<String> input) async { try { final ArgResults results = parse(input); if (results.command != null) { Object? output = await results.command!.run(results); if (onOutput != null) { await onOutput!(output.toString()); } else { print(output.toString()); } } } on Exception catch (e) { print(e); } }This updates the
runmethod to use theonOutputfunction if it is provided, otherwise it defaults to printing to the console.
Task 3 Use the onOutput callback
#
Finally, update your main application to use the new onOutput feature.
Open the
cli/bin/cli.dartfile.-
Update the
mainfunction to pass theonOutputfunction to theCommandRunner. You will also need to add an import forconsole.dartto make thewritefunction available.dartimport 'package:command_runner/command_runner.dart'; import 'package:command_runner/src/console.dart'; const version = '0.0.1'; void main(List<String> arguments) { var commandRunner = CommandRunner<String>( onOutput: (String output) async { await write(output); }, onError: (Object error) { if (error is Error) { throw error; } if (error is Exception) { print(error); } }, )..addCommand(HelpCommand()); commandRunner.run(arguments); }
Task 4 Test the changes
#Test the improved HelpCommand and the onOutput callback.
Open your terminal and navigate to the
clidirectory.-
Run the command
dart run bin/cli.dart help --verbose.You should see detailed usage information for the
helpcommand, printed using the customwritefunction.
Review
#What you accomplished
Here's a summary of what you built and learned in this lesson.Used StringBuffer for efficient string building
You replaced string concatenation with a StringBuffer, which can be more efficient for building strings in loops. Methods like
writeln() and write() append content, while toString() produces the final string.
Enhanced HelpCommand with verbose output
You improved the help system to show detailed usage information including options, default values, and descriptions. The
--verbose flag and --command option give users control over the level of detail.
Added an onOutput callback argument for flexible handling
You added onOutput to CommandRunner, allowing library consumers to customize how output is displayed. This pattern makes your package more flexible and adaptable, enabling delayed printing, logging, or output redirection.
Quiz
#Check your understanding
1 / 2StringBuffer class in Dart?
-
To efficiently build strings by appending multiple parts.
That's right!
StringBufferprovides efficient string concatenation by avoiding the creation of intermediate string objects during multiple, successive appends. -
To store a fixed-size string.
Not quite.
Strings in Dart are immutable and don't have a fixed size concept.
StringBufferis for a different purpose related to string creation. -
To encrypt strings.
Not quite.
Encryption requires specialized cryptography libraries.
StringBufferis a general-purpose utility, not security-related. -
To compare two strings for equality.
Not quite.
String comparison is done using
==orcompareTo().StringBufferserves a different purpose.
onOutput argument in the CommandRunner class allow you to do?
-
Customize the output handling of a command.
That's right!
onOutputlets you define custom behavior for command output, such as formatting, logging, or writing to different destinations. -
Specify the input for a command.
Not quite.
Input comes from command-line arguments passed to
run(). The nameonOutputsuggests it's about something else. -
Set the error message for a command.
Not quite.
Error handling has its own mechanism (
onError).onOutputis for a different kind of result. -
Define the name of a command.
Not quite.
Command names are defined in the
Commandclass'snamegetter.onOutputserves a different purpose.
Next lesson
#In the next lesson, you'll prepare for the Wikipedia portion of the application.
Unless stated otherwise, the documentation on this site reflects Dart 3.10.3. Page last updated on 2026-1-8. View source or report an issue.