Migrate to null safety (#159)

Initial migration to null safety, futher updates still pending and breaking changes possible.
diff --git a/.travis.yml b/.travis.yml
index a80f7c7..bdf0b3c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,19 +1,31 @@
 language: dart
 
 dart:
-- 2.3.0
 - dev
 
-dart_task:
-- test: -p vm
-- dartanalyzer: --fatal-infos --fatal-warnings .
-
-matrix:
+jobs:
   include:
-  - dart: dev
-    dart_task: dartfmt
+    - stage: analyze_and_format
+      name: "Analyze"
+      dart: dev
+      os: linux
+      script: dartanalyzer --fatal-warnings --fatal-infos .
+    - stage: analyze_and_format
+      name: "Format"
+      dart: dev
+      os: linux
+      script: dartfmt -n --set-exit-if-changed .
+    - stage: test
+      name: "Vm Tests"
+      dart: dev
+      os: linux
+      script: pub run test -p vm
+    - stage: test
+      name: "Web Tests"
+      dart: dev
+      os: linux
+      script: pub run test -p chrome
 
-# Only building master means that we don't run two builds for each pull request.
 branches:
   only: [master]
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4f324d0..fcc3229 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,4 @@
-## 1.6.1-dev
+## 2.0.0-nullsafety
 
 ## 1.6.0
 
diff --git a/lib/command_runner.dart b/lib/command_runner.dart
index 0fbf697..0a6566f 100644
--- a/lib/command_runner.dart
+++ b/lib/command_runner.dart
@@ -45,7 +45,7 @@
   ///
   /// If a subclass overrides this to return a string, it will automatically be
   /// added to the end of [usage].
-  String get usageFooter => null;
+  String? get usageFooter => null;
 
   /// Returns [usage] with [description] removed from the beginning.
   String get _usageWithoutDescription {
@@ -60,7 +60,7 @@
     buffer.write(_wrap(
         'Run "$executableName help <command>" for more information about a command.'));
     if (usageFooter != null) {
-      buffer.write('\n${_wrap(usageFooter)}');
+      buffer.write('\n${_wrap(usageFooter!)}');
     }
     return buffer.toString();
   }
@@ -77,7 +77,7 @@
   ArgParser get argParser => _argParser;
   final ArgParser _argParser;
 
-  CommandRunner(this.executableName, this.description, {int usageLineLength})
+  CommandRunner(this.executableName, this.description, {int? usageLineLength})
       : _argParser = ArgParser(usageLineLength: usageLineLength) {
     argParser.addFlag('help',
         abbr: 'h', negatable: false, help: 'Print this usage information.');
@@ -91,7 +91,7 @@
   void printUsage() => print(usage);
 
   /// Throws a [UsageException] with [message].
-  void usageException(String message) =>
+  Never usageException(String message) =>
       throw UsageException(message, _usageWithoutDescription);
 
   /// Adds [Command] as a top-level command to this runner.
@@ -108,7 +108,7 @@
   ///
   /// This always returns a [Future] in case the command is asynchronous. The
   /// [Future] will throw a [UsageException] if [args] was invalid.
-  Future<T> run(Iterable<String> args) =>
+  Future<T?> run(Iterable<String> args) =>
       Future.sync(() => runCommand(parse(args)));
 
   /// Parses [args] and returns the result, converting an [ArgParserException]
@@ -124,11 +124,10 @@
 
       var command = commands[error.commands.first];
       for (var commandName in error.commands.skip(1)) {
-        command = command.subcommands[commandName];
+        command = command!.subcommands[commandName];
       }
 
-      command.usageException(error.message);
-      return null;
+      command!.usageException(error.message);
     }
   }
 
@@ -142,10 +141,10 @@
   /// here to enable verbose logging before running the command.
   ///
   /// This returns the return value of [Command.run].
-  Future<T> runCommand(ArgResults topLevelResults) async {
+  Future<T?> runCommand(ArgResults topLevelResults) async {
     var argResults = topLevelResults;
     var commands = _commands;
-    Command command;
+    Command? command;
     var commandString = executableName;
 
     while (commands.isNotEmpty) {
@@ -170,11 +169,11 @@
       }
 
       // Step into the command.
-      argResults = argResults.command;
+      argResults = argResults.command!;
       command = commands[argResults.name];
-      command._globalResults = topLevelResults;
+      command!._globalResults = topLevelResults;
       command._argResults = argResults;
-      commands = command._subcommands;
+      commands = command._subcommands as Map<String, Command<T>>;
       commandString += ' ${argResults.name}';
 
       if (argResults.options.contains('help') && argResults['help']) {
@@ -184,20 +183,20 @@
     }
 
     if (topLevelResults['help']) {
-      command.printUsage();
+      command!.printUsage();
       return null;
     }
 
     // Make sure there aren't unexpected arguments.
-    if (!command.takesArguments && argResults.rest.isNotEmpty) {
+    if (!command!.takesArguments && argResults.rest.isNotEmpty) {
       command.usageException(
           'Command "${argResults.name}" does not take any arguments.');
     }
 
-    return (await command.run()) as T;
+    return (await command.run()) as T?;
   }
 
-  String _wrap(String text, {int hangingIndent}) => wrapText(text,
+  String _wrap(String text, {int? hangingIndent}) => wrapText(text,
       length: argParser.usageLineLength, hangingIndent: hangingIndent);
 }
 
@@ -229,7 +228,7 @@
     for (var command = parent; command != null; command = command.parent) {
       parents.add(command.name);
     }
-    parents.add(runner.executableName);
+    parents.add(runner!.executableName);
 
     var invocation = parents.reversed.join(' ');
     return _subcommands.isNotEmpty
@@ -241,31 +240,31 @@
   ///
   /// This will be `null` until [addSubcommand] has been called with
   /// this command.
-  Command<T> get parent => _parent;
-  Command<T> _parent;
+  Command<T>? get parent => _parent;
+  Command<T>? _parent;
 
   /// The command runner for this command.
   ///
   /// This will be `null` until [CommandRunner.addCommand] has been called with
   /// this command or one of its parents.
-  CommandRunner<T> get runner {
+  CommandRunner<T>? get runner {
     if (parent == null) return _runner;
-    return parent.runner;
+    return parent!.runner;
   }
 
-  CommandRunner<T> _runner;
+  CommandRunner<T>? _runner;
 
   /// The parsed global argument results.
   ///
   /// This will be `null` until just before [Command.run] is called.
-  ArgResults get globalResults => _globalResults;
-  ArgResults _globalResults;
+  ArgResults? get globalResults => _globalResults;
+  ArgResults? _globalResults;
 
   /// The parsed argument results for this command.
   ///
   /// This will be `null` until just before [Command.run] is called.
-  ArgResults get argResults => _argResults;
-  ArgResults _argResults;
+  ArgResults? get argResults => _argResults;
+  ArgResults? _argResults;
 
   /// The argument parser for this command.
   ///
@@ -289,9 +288,9 @@
   ///
   /// If a subclass overrides this to return a string, it will automatically be
   /// added to the end of [usage].
-  String get usageFooter => null;
+  String? get usageFooter => null;
 
-  String _wrap(String text, {int hangingIndent}) {
+  String _wrap(String text, {int? hangingIndent}) {
     return wrapText(text,
         length: argParser.usageLineLength, hangingIndent: hangingIndent);
   }
@@ -316,11 +315,11 @@
 
     buffer.writeln();
     buffer.write(
-        _wrap('Run "${runner.executableName} help" to see global options.'));
+        _wrap('Run "${runner!.executableName} help" to see global options.'));
 
     if (usageFooter != null) {
       buffer.writeln();
-      buffer.write(_wrap(usageFooter));
+      buffer.write(_wrap(usageFooter!));
     }
 
     return buffer.toString();
@@ -374,7 +373,7 @@
   ///
   /// The return value is wrapped in a `Future` if necessary and returned by
   /// [CommandRunner.runCommand].
-  FutureOr<T> run() {
+  FutureOr<T>? run() {
     throw UnimplementedError(_wrap('Leaf command $this must implement run().'));
   }
 
@@ -395,7 +394,7 @@
   void printUsage() => print(usage);
 
   /// Throws a [UsageException] with [message].
-  void usageException(String message) =>
+  Never usageException(String message) =>
       throw UsageException(_wrap(message), _usageWithoutDescription);
 }
 
@@ -404,13 +403,13 @@
 /// [isSubcommand] indicates whether the commands should be called "commands" or
 /// "subcommands".
 String _getCommandUsage(Map<String, Command> commands,
-    {bool isSubcommand = false, int lineLength}) {
+    {bool isSubcommand = false, int? lineLength}) {
   // Don't include aliases.
   var names =
-      commands.keys.where((name) => !commands[name].aliases.contains(name));
+      commands.keys.where((name) => !commands[name]!.aliases.contains(name));
 
   // Filter out hidden ones, unless they are all hidden.
-  var visible = names.where((name) => !commands[name].hidden);
+  var visible = names.where((name) => !commands[name]!.hidden);
   if (visible.isNotEmpty) names = visible;
 
   // Show the commands alphabetically.
@@ -420,7 +419,7 @@
   var buffer = StringBuffer('Available ${isSubcommand ? "sub" : ""}commands:');
   var columnStart = length + 5;
   for (var name in names) {
-    var lines = wrapTextAsLines(commands[name].summary,
+    var lines = wrapTextAsLines(commands[name]!.summary,
         start: columnStart, length: lineLength);
     buffer.writeln();
     buffer.write('  ${padRight(name, length)}   ${lines.first}');
diff --git a/lib/src/allow_anything_parser.dart b/lib/src/allow_anything_parser.dart
index e6f13d0..16f97fd 100644
--- a/lib/src/allow_anything_parser.dart
+++ b/lib/src/allow_anything_parser.dart
@@ -20,21 +20,21 @@
   @override
   bool get allowsAnything => true;
   @override
-  int get usageLineLength => null;
+  int? get usageLineLength => null;
 
   @override
-  ArgParser addCommand(String name, [ArgParser parser]) {
+  ArgParser addCommand(String name, [ArgParser? parser]) {
     throw UnsupportedError(
         "ArgParser.allowAnything().addCommands() isn't supported.");
   }
 
   @override
   void addFlag(String name,
-      {String abbr,
-      String help,
-      bool defaultsTo = false,
+      {String? abbr,
+      String? help,
+      bool? defaultsTo = false,
       bool negatable = true,
-      void Function(bool) callback,
+      void Function(bool)? callback,
       bool hide = false}) {
     throw UnsupportedError(
         "ArgParser.allowAnything().addFlag() isn't supported.");
@@ -42,15 +42,15 @@
 
   @override
   void addOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
-      String defaultsTo,
-      Function callback,
+      {String? abbr,
+      String? help,
+      String? valueHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
+      String? defaultsTo,
+      Function? callback,
       bool allowMultiple = false,
-      bool splitCommas,
+      bool? splitCommas,
       bool hide = false}) {
     throw UnsupportedError(
         "ArgParser.allowAnything().addOption() isn't supported.");
@@ -58,13 +58,13 @@
 
   @override
   void addMultiOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
-      Iterable<String> defaultsTo,
-      void Function(List<String>) callback,
+      {String? abbr,
+      String? help,
+      String? valueHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
+      Iterable<String>? defaultsTo,
+      void Function(List<String>)? callback,
       bool splitCommas = true,
       bool hide = false}) {
     throw UnsupportedError(
@@ -93,5 +93,5 @@
   }
 
   @override
-  Option findByAbbreviation(String abbr) => null;
+  Option? findByAbbreviation(String abbr) => null;
 }
diff --git a/lib/src/arg_parser.dart b/lib/src/arg_parser.dart
index aae89d6..7ccb5b4 100644
--- a/lib/src/arg_parser.dart
+++ b/lib/src/arg_parser.dart
@@ -44,7 +44,7 @@
   /// there is no whitespace at which to split).
   ///
   /// If null (the default), help messages are not wrapped.
-  final int usageLineLength;
+  final int? usageLineLength;
 
   /// Whether or not this parser treats unrecognized options as non-option
   /// arguments.
@@ -56,7 +56,7 @@
   /// flags and options that appear after positional arguments. If it's `false`,
   /// the parser stops parsing as soon as it finds an argument that is neither
   /// an option nor a command.
-  factory ArgParser({bool allowTrailingOptions = true, int usageLineLength}) =>
+  factory ArgParser({bool allowTrailingOptions = true, int? usageLineLength}) =>
       ArgParser._(<String, Option>{}, <String, ArgParser>{},
           allowTrailingOptions: allowTrailingOptions,
           usageLineLength: usageLineLength);
@@ -75,7 +75,7 @@
         options = UnmodifiableMapView(options),
         _commands = commands,
         commands = UnmodifiableMapView(commands),
-        allowTrailingOptions = allowTrailingOptions ?? false;
+        allowTrailingOptions = allowTrailingOptions;
 
   /// Defines a command.
   ///
@@ -85,7 +85,7 @@
   ///
   /// Note that adding commands this way will not impact the [usage] string. To
   /// add commands which are included in the usage string see `CommandRunner`.
-  ArgParser addCommand(String name, [ArgParser parser]) {
+  ArgParser addCommand(String name, [ArgParser? parser]) {
     // Make sure the name isn't in use.
     if (_commands.containsKey(name)) {
       throw ArgumentError('Duplicate command "$name".');
@@ -125,11 +125,11 @@
   /// * There is already an option named [name].
   /// * There is already an option using abbreviation [abbr].
   void addFlag(String name,
-      {String abbr,
-      String help,
-      bool defaultsTo = false,
+      {String? abbr,
+      String? help,
+      bool? defaultsTo = false,
       bool negatable = true,
-      void Function(bool) callback,
+      void Function(bool)? callback,
       bool hide = false}) {
     _addOption(
         name,
@@ -186,15 +186,15 @@
   /// * There is already an option using abbreviation [abbr].
   /// * [splitCommas] is passed but [allowMultiple] is `false`.
   void addOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
-      String defaultsTo,
-      Function callback,
+      {String? abbr,
+      String? help,
+      String? valueHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
+      String? defaultsTo,
+      Function? callback,
       @Deprecated('Use addMultiOption() instead.') bool allowMultiple = false,
-      @Deprecated('Use addMultiOption() instead.') bool splitCommas,
+      @Deprecated('Use addMultiOption() instead.') bool? splitCommas,
       bool hide = false}) {
     if (!allowMultiple && splitCommas != null) {
       throw ArgumentError(
@@ -255,13 +255,13 @@
   /// * There is already an option with name [name].
   /// * There is already an option using abbreviation [abbr].
   void addMultiOption(String name,
-      {String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
-      Iterable<String> defaultsTo,
-      void Function(List<String>) callback,
+      {String? abbr,
+      String? help,
+      String? valueHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
+      Iterable<String>? defaultsTo,
+      void Function(List<String>)? callback,
       bool splitCommas = true,
       bool hide = false}) {
     _addOption(
@@ -280,16 +280,16 @@
 
   void _addOption(
       String name,
-      String abbr,
-      String help,
-      String valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
+      String? abbr,
+      String? help,
+      String? valueHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
       defaultsTo,
-      Function callback,
+      Function? callback,
       OptionType type,
       {bool negatable = false,
-      bool splitCommas,
+      bool? splitCommas,
       bool hide = false}) {
     // Make sure the name isn't in use.
     if (_options.containsKey(name)) {
@@ -341,16 +341,19 @@
   /// Get the default value for an option. Useful after parsing to test if the
   /// user specified something other than the default.
   dynamic getDefault(String option) {
-    if (!options.containsKey(option)) {
+    var value = options[option];
+    if (value == null) {
       throw ArgumentError('No option named $option');
     }
-    return options[option].defaultsTo;
+    return value.defaultsTo;
   }
 
   /// Finds the option whose abbreviation is [abbr], or `null` if no option has
   /// that abbreviation.
-  Option findByAbbreviation(String abbr) {
-    return options.values
-        .firstWhere((option) => option.abbr == abbr, orElse: () => null);
+  Option? findByAbbreviation(String abbr) {
+    for (var option in options.values) {
+      if (option.abbr == abbr) return option;
+    }
+    return null;
   }
 }
diff --git a/lib/src/arg_parser_exception.dart b/lib/src/arg_parser_exception.dart
index f0d57d5..56ac851 100644
--- a/lib/src/arg_parser_exception.dart
+++ b/lib/src/arg_parser_exception.dart
@@ -9,7 +9,7 @@
   /// This will be empty if the error was on the root parser.
   final List<String> commands;
 
-  ArgParserException(String message, [Iterable<String> commands])
+  ArgParserException(String message, [Iterable<String>? commands])
       : commands = commands == null ? const [] : List.unmodifiable(commands),
         super(message);
 }
diff --git a/lib/src/arg_results.dart b/lib/src/arg_results.dart
index 6345076..fb2d0c8 100644
--- a/lib/src/arg_results.dart
+++ b/lib/src/arg_results.dart
@@ -13,8 +13,8 @@
 ArgResults newArgResults(
     ArgParser parser,
     Map<String, dynamic> parsed,
-    String name,
-    ArgResults command,
+    String? name,
+    ArgResults? command,
     List<String> rest,
     List<String> arguments) {
   return ArgResults._(parser, parsed, name, command, rest, arguments);
@@ -34,12 +34,12 @@
 
   /// The name of the command for which these options are parsed, or `null` if
   /// these are the top-level results.
-  final String name;
+  final String? name;
 
   /// The command that was selected, or `null` if none was.
   ///
   /// This will contain the options that were selected for that command.
-  final ArgResults command;
+  final ArgResults? command;
 
   /// The remaining command-line arguments that were not parsed as options or
   /// flags.
@@ -65,7 +65,7 @@
       throw ArgumentError('Could not find an option named "$name".');
     }
 
-    return _parser.options[name].getOrDefault(_parsed[name]);
+    return _parser.options[name]!.getOrDefault(_parsed[name]);
   }
 
   /// The names of the available options.
diff --git a/lib/src/help_command.dart b/lib/src/help_command.dart
index 255f2d4..c3c2bbf 100644
--- a/lib/src/help_command.dart
+++ b/lib/src/help_command.dart
@@ -13,37 +13,37 @@
 
   @override
   String get description =>
-      'Display help information for ${runner.executableName}.';
+      'Display help information for ${runner!.executableName}.';
 
   @override
-  String get invocation => '${runner.executableName} help [command]';
+  String get invocation => '${runner!.executableName} help [command]';
 
   @override
   bool get hidden => true;
 
   @override
-  T run() {
+  T? run() {
     // Show the default help if no command was specified.
-    if (argResults.rest.isEmpty) {
-      runner.printUsage();
+    if (argResults!.rest.isEmpty) {
+      runner!.printUsage();
       return null;
     }
 
     // Walk the command tree to show help for the selected command or
     // subcommand.
-    var commands = runner.commands;
-    Command command;
-    var commandString = runner.executableName;
+    var commands = runner!.commands;
+    Command? command;
+    var commandString = runner!.executableName;
 
-    for (var name in argResults.rest) {
+    for (var name in argResults!.rest) {
       if (commands.isEmpty) {
-        command.usageException(
+        command!.usageException(
             'Command "$commandString" does not expect a subcommand.');
       }
 
       if (commands[name] == null) {
         if (command == null) {
-          runner.usageException('Could not find a command named "$name".');
+          runner!.usageException('Could not find a command named "$name".');
         }
 
         command.usageException(
@@ -51,11 +51,11 @@
       }
 
       command = commands[name];
-      commands = command.subcommands;
+      commands = command!.subcommands as Map<String, Command<T>>;
       commandString += ' $name';
     }
 
-    command.printUsage();
+    command!.printUsage();
     return null;
   }
 }
diff --git a/lib/src/option.dart b/lib/src/option.dart
index 7fd9160..958929a 100644
--- a/lib/src/option.dart
+++ b/lib/src/option.dart
@@ -8,16 +8,16 @@
 /// get to it. This function isn't exported to the public API of the package.
 Option newOption(
     String name,
-    String abbr,
-    String help,
-    String valueHelp,
-    Iterable<String> allowed,
-    Map<String, String> allowedHelp,
+    String? abbr,
+    String? help,
+    String? valueHelp,
+    Iterable<String>? allowed,
+    Map<String, String>? allowedHelp,
     defaultsTo,
-    Function callback,
+    Function? callback,
     OptionType type,
-    {bool negatable,
-    bool splitCommas,
+    {bool? negatable,
+    bool? splitCommas,
     bool hide = false}) {
   return Option._(name, abbr, help, valueHelp, allowed, allowedHelp, defaultsTo,
       callback, type,
@@ -35,22 +35,22 @@
   ///
   /// For example, `abbr: "a"` will allow the user to pass `-a value` or
   /// `-avalue`.
-  final String abbr;
+  final String? abbr;
 
   @Deprecated('Use abbr instead.')
-  String get abbreviation => abbr;
+  String? get abbreviation => abbr;
 
   /// A description of this option.
-  final String help;
+  final String? help;
 
   /// A name for the value this option takes.
-  final String valueHelp;
+  final String? valueHelp;
 
   /// A list of valid values for this option.
-  final List<String> allowed;
+  final List<String>? allowed;
 
   /// A map from values in [allowed] to documentation for those values.
-  final Map<String, String> allowedHelp;
+  final Map<String, String>? allowedHelp;
 
   /// The value this option will have if the user doesn't explicitly pass it.
   final dynamic defaultsTo;
@@ -64,10 +64,10 @@
   /// value to `false`.
   ///
   /// This is `null` unless [type] is [OptionType.flag].
-  final bool negatable;
+  final bool? negatable;
 
   /// The callback to invoke with the option's value when the option is parsed.
-  final Function callback;
+  final Function? callback;
 
   /// Whether this is a flag, a single value option, or a multi-value option.
   final OptionType type;
@@ -93,13 +93,13 @@
       this.abbr,
       this.help,
       this.valueHelp,
-      Iterable<String> allowed,
-      Map<String, String> allowedHelp,
+      Iterable<String>? allowed,
+      Map<String, String>? allowedHelp,
       this.defaultsTo,
       this.callback,
       OptionType type,
       {this.negatable,
-      bool splitCommas,
+      bool? splitCommas,
       this.hide = false})
       : allowed = allowed == null ? null : List.unmodifiable(allowed),
         allowedHelp =
@@ -119,6 +119,7 @@
       throw ArgumentError('Name "$name" contains invalid characters.');
     }
 
+    var abbr = this.abbr;
     if (abbr != null) {
       if (abbr.length != 1) {
         throw ArgumentError('Abbreviation must be null or have length 1.');
diff --git a/lib/src/parser.dart b/lib/src/parser.dart
index cb15288..4860a2e 100644
--- a/lib/src/parser.dart
+++ b/lib/src/parser.dart
@@ -16,11 +16,11 @@
 class Parser {
   /// If parser is parsing a command's options, this will be the name of the
   /// command. For top-level results, this returns `null`.
-  final String commandName;
+  final String? commandName;
 
   /// The parser for the supercommand of this command parser, or `null` if this
   /// is the top-level parser.
-  final Parser parent;
+  final Parser? parent;
 
   /// The grammar being parsed.
   final ArgParser grammar;
@@ -35,7 +35,7 @@
   final Map<String, dynamic> results = <String, dynamic>{};
 
   Parser(this.commandName, this.grammar, this.args,
-      [this.parent, List<String> rest]) {
+      [this.parent, List<String>? rest]) {
     if (rest != null) this.rest.addAll(rest);
   }
 
@@ -50,7 +50,7 @@
           grammar, const {}, commandName, null, arguments, arguments);
     }
 
-    ArgResults commandResults;
+    ArgResults? commandResults;
 
     // Parse the args.
     while (args.isNotEmpty) {
@@ -71,7 +71,6 @@
         try {
           commandResults = commandParser.parse();
         } on ArgParserException catch (error) {
-          if (commandName == null) rethrow;
           throw ArgParserException(
               error.message, [commandName, ...error.commands]);
         }
@@ -95,8 +94,8 @@
 
     // Invoke the callbacks.
     grammar.options.forEach((name, option) {
-      if (option.callback == null) return;
-      option.callback(option.getOrDefault(results[name]));
+      var callback = option.callback;
+      if (callback != null) callback(option.getOrDefault(results[name]));
     });
 
     // Add in the leftover arguments we didn't parse to the innermost command.
@@ -134,7 +133,7 @@
     if (option == null) {
       // Walk up to the parent command if possible.
       validate(parent != null, 'Could not find an option or flag "-$opt".');
-      return parent.parseSoloOption();
+      return parent!.parseSoloOption();
     }
 
     args.removeFirst();
@@ -179,7 +178,7 @@
       // Walk up to the parent command if possible.
       validate(
           parent != null, 'Could not find an option with short name "-$c".');
-      return parent.parseAbbreviation(innermostCommand);
+      return parent!.parseAbbreviation(innermostCommand);
     } else if (!first.isFlag) {
       // The first character is a non-flag option, so the rest must be the
       // value.
@@ -213,7 +212,7 @@
       // Walk up to the parent command if possible.
       validate(
           parent != null, 'Could not find an option with short name "-$c".');
-      parent.parseShortFlag(c);
+      parent!.parseShortFlag(c);
       return;
     }
 
@@ -266,18 +265,18 @@
       if (option == null) {
         // Walk up to the parent command if possible.
         validate(parent != null, 'Could not find an option named "$name".');
-        return parent.parseLongOption();
+        return parent!.parseLongOption();
       }
 
       args.removeFirst();
       validate(option.isFlag, 'Cannot negate non-flag option "$name".');
-      validate(option.negatable, 'Cannot negate option "$name".');
+      validate(option.negatable!, 'Cannot negate option "$name".');
 
       setFlag(results, option, false);
     } else {
       // Walk up to the parent command if possible.
       validate(parent != null, 'Could not find an option named "$name".');
-      return parent.parseLongOption();
+      return parent!.parseLongOption();
     }
 
     return true;
@@ -325,7 +324,7 @@
   void _validateAllowed(Option option, String value) {
     if (option.allowed == null) return;
 
-    validate(option.allowed.contains(value),
+    validate(option.allowed!.contains(value),
         '"$value" is not an allowed value for option "${option.name}".');
   }
 }
diff --git a/lib/src/usage.dart b/lib/src/usage.dart
index d244cad..07bf6ed 100644
--- a/lib/src/usage.dart
+++ b/lib/src/usage.dart
@@ -26,7 +26,7 @@
   final List optionsAndSeparators;
 
   /// The working buffer for the generated usage text.
-  StringBuffer buffer;
+  late StringBuffer buffer;
 
   /// The column that the "cursor" is currently on.
   ///
@@ -35,7 +35,7 @@
   int currentColumn = 0;
 
   /// The width in characters of each column.
-  List<int> columnWidths;
+  late List<int> columnWidths;
 
   /// The number of sequential lines of text that have been written to the last
   /// column (which shows help info).
@@ -56,7 +56,7 @@
   /// The horizontal character position at which help text is wrapped. Help that
   /// extends past this column will be wrapped at the nearest whitespace (or
   /// truncated if there is no available whitespace).
-  final int lineLength;
+  final int? lineLength;
 
   Usage(this.optionsAndSeparators, {this.lineLength});
 
@@ -82,15 +82,15 @@
       write(0, getAbbreviation(option));
       write(1, getLongOption(option));
 
-      if (option.help != null) write(2, option.help);
+      if (option.help != null) write(2, option.help!);
 
       if (option.allowedHelp != null) {
-        var allowedNames = option.allowedHelp.keys.toList(growable: false);
+        var allowedNames = option.allowedHelp!.keys.toList(growable: false);
         allowedNames.sort();
         newline();
         for (var name in allowedNames) {
           write(1, getAllowedTitle(option, name));
-          write(2, option.allowedHelp[name]);
+          write(2, option.allowedHelp![name]!);
         }
         newline();
       } else if (option.allowed != null) {
@@ -122,7 +122,7 @@
 
   String getLongOption(Option option) {
     var result;
-    if (option.negatable) {
+    if (option.negatable!) {
       result = '--[no-]${option.name}';
     } else {
       result = '--${option.name}';
@@ -155,7 +155,7 @@
 
       // Make room for the allowed help.
       if (option.allowedHelp != null) {
-        for (var allowed in option.allowedHelp.keys) {
+        for (var allowed in option.allowedHelp!.keys) {
           title = math.max(title, getAllowedTitle(option, allowed).length);
         }
       }
@@ -252,7 +252,7 @@
     var allowedBuffer = StringBuffer();
     allowedBuffer.write('[');
     var first = true;
-    for (var allowed in option.allowed) {
+    for (var allowed in option.allowed!) {
       if (!first) allowedBuffer.write(', ');
       allowedBuffer.write(allowed);
       if (isDefault(allowed)) {
diff --git a/lib/src/utils.dart b/lib/src/utils.dart
index 09319ed..4516848 100644
--- a/lib/src/utils.dart
+++ b/lib/src/utils.dart
@@ -32,7 +32,7 @@
 ///
 /// If [length] is not specified, then no wrapping occurs, and the original
 /// [text] is returned unchanged.
-String wrapText(String text, {int length, int hangingIndent}) {
+String wrapText(String text, {int? length, int? hangingIndent}) {
   if (length == null) return text;
   hangingIndent ??= 0;
   var splitText = text.split('\n');
@@ -58,12 +58,12 @@
       notIndented = wrapTextAsLines(trimmedText,
           length: length - leadingWhitespace.length);
     }
-    String hangingIndentString;
+    String? hangingIndentString;
     result.addAll(notIndented.map<String>((String line) {
       // Don't return any lines with just whitespace on them.
       if (line.isEmpty) return '';
       var result = '${hangingIndentString ?? ''}$leadingWhitespace$line';
-      hangingIndentString ??= ' ' * hangingIndent;
+      hangingIndentString ??= ' ' * hangingIndent!;
       return result;
     }));
   }
@@ -80,7 +80,7 @@
 /// If [length] is not specified, then no wrapping occurs, and the original
 /// [text] is returned after splitting it on newlines. Whitespace is not trimmed
 /// in this case.
-List<String> wrapTextAsLines(String text, {int start = 0, int length}) {
+List<String> wrapTextAsLines(String text, {int start = 0, int? length}) {
   assert(start >= 0);
 
   /// Returns true if the code unit at [index] in [text] is a whitespace
diff --git a/pubspec.yaml b/pubspec.yaml
index 68915fc..48d74f4 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,13 +1,19 @@
 name: args
-version: 1.6.1-dev
+version: 2.0.0-nullsafety
 homepage: https://github.com/dart-lang/args
 description: >-
  Library for defining parsers for parsing raw command-line arguments into a set
  of options and values using GNU and POSIX style options.
 
 environment:
-  sdk: '>=2.3.0 <3.0.0'
+  sdk: '>=2.12.0-0 <3.0.0'
 
 dev_dependencies:
-  pedantic: ^1.4.0
-  test: '^1.5.1'
+  pedantic: ^1.10.0-nullsafety.3
+  test: ^1.16.0-nullsafety.9
+
+# Required to get a version solve due to cyclic dependency
+dependency_overrides:
+  test_core: ^0.3.12-nullsafety.9
+  coverage: ">=0.13.4 <0.15.0"
+  analyzer: ">=0.39.5 <0.41.0"
diff --git a/test/allow_anything_test.dart b/test/allow_anything_test.dart
index 29038a7..06a4e13 100644
--- a/test/allow_anything_test.dart
+++ b/test/allow_anything_test.dart
@@ -10,7 +10,7 @@
 
 void main() {
   group('new ArgParser.allowAnything()', () {
-    ArgParser parser;
+    late ArgParser parser;
     setUp(() {
       parser = ArgParser.allowAnything();
     });
@@ -49,10 +49,11 @@
       var commandParser = ArgParser()..addCommand('command', parser);
       var results =
           commandParser.parse(['command', '--foo', '-abc', '--', 'bar']);
-      expect(results.command.options, isEmpty);
-      expect(results.command.rest, equals(['--foo', '-abc', '--', 'bar']));
-      expect(results.command.arguments, equals(['--foo', '-abc', '--', 'bar']));
-      expect(results.command.name, equals('command'));
+      expect(results.command!.options, isEmpty);
+      expect(results.command!.rest, equals(['--foo', '-abc', '--', 'bar']));
+      expect(
+          results.command!.arguments, equals(['--foo', '-abc', '--', 'bar']));
+      expect(results.command!.name, equals('command'));
     });
 
     test('works as a subcommand in a CommandRunner', () async {
diff --git a/test/args_test.dart b/test/args_test.dart
index b5dd3ee..f101f90 100644
--- a/test/args_test.dart
+++ b/test/args_test.dart
@@ -86,7 +86,7 @@
 
     test('throws ArgumentError if the abbreviation is an invalid value', () {
       var parser = ArgParser();
-      for (var name in _invalidOptions.where((v) => v != null)) {
+      for (var name in _invalidOptions) {
         throwsIllegalArg(() => parser.addOption('flummox', abbr: name));
       }
     });
@@ -296,25 +296,25 @@
       parser.addMultiOption('multi-no');
       parser.addMultiOption('multi-def', defaultsTo: ['def']);
 
-      expect(parser.options['flag-no'].getOrDefault(null), equals(null));
-      expect(parser.options['flag-no'].getOrDefault(false), equals(false));
-      expect(parser.options['flag-def'].getOrDefault(null), equals(true));
-      expect(parser.options['flag-def'].getOrDefault(false), equals(false));
-      expect(parser.options['single-no'].getOrDefault(null), equals(null));
-      expect(parser.options['single-no'].getOrDefault('v'), equals('v'));
-      expect(parser.options['single-def'].getOrDefault(null), equals('def'));
-      expect(parser.options['single-def'].getOrDefault('v'), equals('v'));
-      expect(parser.options['allow-multi-no'].getOrDefault(null), equals([]));
+      expect(parser.options['flag-no']!.getOrDefault(null), equals(null));
+      expect(parser.options['flag-no']!.getOrDefault(false), equals(false));
+      expect(parser.options['flag-def']!.getOrDefault(null), equals(true));
+      expect(parser.options['flag-def']!.getOrDefault(false), equals(false));
+      expect(parser.options['single-no']!.getOrDefault(null), equals(null));
+      expect(parser.options['single-no']!.getOrDefault('v'), equals('v'));
+      expect(parser.options['single-def']!.getOrDefault(null), equals('def'));
+      expect(parser.options['single-def']!.getOrDefault('v'), equals('v'));
+      expect(parser.options['allow-multi-no']!.getOrDefault(null), equals([]));
       expect(
-          parser.options['allow-multi-no'].getOrDefault(['v']), equals(['v']));
-      expect(parser.options['allow-multi-def'].getOrDefault(null),
+          parser.options['allow-multi-no']!.getOrDefault(['v']), equals(['v']));
+      expect(parser.options['allow-multi-def']!.getOrDefault(null),
           equals(['def']));
-      expect(
-          parser.options['allow-multi-def'].getOrDefault(['v']), equals(['v']));
-      expect(parser.options['multi-no'].getOrDefault(null), equals([]));
-      expect(parser.options['multi-no'].getOrDefault(['v']), equals(['v']));
-      expect(parser.options['multi-def'].getOrDefault(null), equals(['def']));
-      expect(parser.options['multi-def'].getOrDefault(['v']), equals(['v']));
+      expect(parser.options['allow-multi-def']!.getOrDefault(['v']),
+          equals(['v']));
+      expect(parser.options['multi-no']!.getOrDefault(null), equals([]));
+      expect(parser.options['multi-no']!.getOrDefault(['v']), equals(['v']));
+      expect(parser.options['multi-def']!.getOrDefault(null), equals(['def']));
+      expect(parser.options['multi-def']!.getOrDefault(['v']), equals(['v']));
     });
   });
 }
diff --git a/test/command_parse_test.dart b/test/command_parse_test.dart
index afa8ebe..c0a2cf2 100644
--- a/test/command_parse_test.dart
+++ b/test/command_parse_test.dart
@@ -36,7 +36,7 @@
 
       var args = parser.parse(['install']);
 
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
       expect(args.rest, isEmpty);
     });
 
@@ -46,7 +46,7 @@
       command.addOption('path');
 
       var args = parser.parse(['install', '--path', 'some/path']);
-      expect(args.command['path'], equals('some/path'));
+      expect(args.command!['path'], equals('some/path'));
     });
 
     test('parses a parent solo option before the command', () {
@@ -56,7 +56,7 @@
 
       var args = parser.parse(['-m', 'debug', 'install']);
       expect(args['mode'], equals('debug'));
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent solo option after the command', () {
@@ -66,7 +66,7 @@
 
       var args = parser.parse(['install', '-m', 'debug']);
       expect(args['mode'], equals('debug'));
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent option before the command', () {
@@ -76,7 +76,7 @@
 
       var args = parser.parse(['--verbose', 'install']);
       expect(args['verbose'], isTrue);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent option after the command', () {
@@ -86,7 +86,7 @@
 
       var args = parser.parse(['install', '--verbose']);
       expect(args['verbose'], isTrue);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent negated option before the command', () {
@@ -96,7 +96,7 @@
 
       var args = parser.parse(['--no-verbose', 'install']);
       expect(args['verbose'], isFalse);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent negated option after the command', () {
@@ -106,7 +106,7 @@
 
       var args = parser.parse(['install', '--no-verbose']);
       expect(args['verbose'], isFalse);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent abbreviation before the command', () {
@@ -118,7 +118,7 @@
       var args = parser.parse(['-dv', 'install']);
       expect(args['debug'], isTrue);
       expect(args['verbose'], isTrue);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('parses a parent abbreviation after the command', () {
@@ -130,7 +130,7 @@
       var args = parser.parse(['install', '-dv']);
       expect(args['debug'], isTrue);
       expect(args['verbose'], isTrue);
-      expect(args.command.name, equals('install'));
+      expect(args.command!.name, equals('install'));
     });
 
     test('does not parse a solo command option before the command', () {
@@ -168,10 +168,10 @@
 
       var args = parser.parse(['cmd', 'subcmd', '-abc']);
       expect(args['apple'], isTrue);
-      expect(args.command.name, equals('cmd'));
-      expect(args.command['banana'], isTrue);
-      expect(args.command.command.name, equals('subcmd'));
-      expect(args.command.command['cherry'], isTrue);
+      expect(args.command!.name, equals('cmd'));
+      expect(args.command!['banana'], isTrue);
+      expect(args.command!.command!.name, equals('subcmd'));
+      expect(args.command!.command!['cherry'], isTrue);
     });
 
     test('option is given to innermost command that can take it', () {
@@ -183,9 +183,9 @@
 
       var args = parser.parse(['cmd', 'subcmd', '--verbose']);
       expect(args['verbose'], isFalse);
-      expect(args.command.name, equals('cmd'));
-      expect(args.command['verbose'], isTrue);
-      expect(args.command.command.name, equals('subcmd'));
+      expect(args.command!.name, equals('cmd'));
+      expect(args.command!['verbose'], isTrue);
+      expect(args.command!.command!.name, equals('subcmd'));
     });
 
     test('remaining arguments are given to the innermost command', () {
@@ -193,11 +193,11 @@
       parser.addCommand('cmd')..addCommand('subcmd');
 
       var args = parser.parse(['cmd', 'subcmd', 'other', 'stuff']);
-      expect(args.command.name, equals('cmd'));
+      expect(args.command!.name, equals('cmd'));
       expect(args.rest, isEmpty);
-      expect(args.command.command.name, equals('subcmd'));
-      expect(args.command.rest, isEmpty);
-      expect(args.command.command.rest, equals(['other', 'stuff']));
+      expect(args.command!.command!.name, equals('subcmd'));
+      expect(args.command!.rest, isEmpty);
+      expect(args.command!.command!.rest, equals(['other', 'stuff']));
     });
   });
 }
diff --git a/test/command_runner_test.dart b/test/command_runner_test.dart
index ce48401..ed29e08 100644
--- a/test/command_runner_test.dart
+++ b/test/command_runner_test.dart
@@ -19,7 +19,7 @@
 Run "test help <command>" for more information about a command.''';
 
 void main() {
-  var runner;
+  late var runner;
   setUp(() {
     runner = CommandRunner('test', 'A test command runner.');
   });
diff --git a/test/command_test.dart b/test/command_test.dart
index a5c4494..d4be4ae 100644
--- a/test/command_test.dart
+++ b/test/command_test.dart
@@ -7,7 +7,7 @@
 import 'test_utils.dart';
 
 void main() {
-  var foo;
+  late var foo;
   setUp(() {
     foo = FooCommand();
 
diff --git a/test/parse_test.dart b/test/parse_test.dart
index 9639567..1c50c4a 100644
--- a/test/parse_test.dart
+++ b/test/parse_test.dart
@@ -154,12 +154,11 @@
       });
 
       test('are invoked even if the option is not present', () {
-        var a = 'not called';
         var parser = ArgParser();
-        parser.addOption('a', callback: (value) => a = value);
+        parser.addOption('a',
+            callback: expectAsync1((value) => expect(value, isNull)));
 
         parser.parse([]);
-        expect(a, isNull);
       });
 
       group('with allowMultiple', () {
diff --git a/test/test_utils.dart b/test/test_utils.dart
index 68b96d3..5136f48 100644
--- a/test/test_utils.dart
+++ b/test/test_utils.dart
@@ -223,7 +223,7 @@
   }
 }
 
-void throwsIllegalArg(function, {String reason}) {
+void throwsIllegalArg(function, {String? reason}) {
   expect(function, throwsArgumentError, reason: reason);
 }
 
@@ -231,11 +231,7 @@
   expect(() => parser.parse(args), throwsFormatException);
 }
 
-Matcher throwsUsageException(message, usage) {
-  return throwsA(predicate((error) {
-    expect(error, TypeMatcher<UsageException>());
-    expect(error.message, message);
-    expect(error.usage, usage);
-    return true;
-  }));
-}
+Matcher throwsUsageException(String message, String usage) =>
+    throwsA(isA<UsageException>()
+        .having((e) => e.message, 'message', message)
+        .having((e) => e.usage, 'usage', usage));
diff --git a/test/trailing_options_test.dart b/test/trailing_options_test.dart
index 174a1d0..323f582 100644
--- a/test/trailing_options_test.dart
+++ b/test/trailing_options_test.dart
@@ -12,7 +12,7 @@
   });
 
   group('when trailing options are allowed', () {
-    var parser;
+    late var parser;
     setUp(() {
       parser = ArgParser(allowTrailingOptions: true);
     });
@@ -93,7 +93,7 @@
 
     results = parser.parse(['cmd', '-f', 'a', '-v', '--unknown']);
     expect(results['flag'], isTrue); // Not trailing.
-    expect(results.command['verbose'], isFalse);
-    expect(results.command.rest, equals(['a', '-v', '--unknown']));
+    expect(results.command!['verbose'], isFalse);
+    expect(results.command!.rest, equals(['a', '-v', '--unknown']));
   });
 }
diff --git a/test/usage_test.dart b/test/usage_test.dart
index 45818b9..3b63d45 100644
--- a/test/usage_test.dart
+++ b/test/usage_test.dart
@@ -502,7 +502,7 @@
 
   // Count the indentation of the last line.
   var whitespace = RegExp('^ *');
-  var indent = whitespace.firstMatch(lines[lines.length - 1])[0].length;
+  var indent = whitespace.firstMatch(lines[lines.length - 1])![0]!.length;
 
   // Drop the last line. It only exists for specifying indentation.
   lines.removeLast();