ArgParser源码解析

586 阅读6分钟

1. ArgParser背景

在flutter中通过如下命令行生成并运行App到手机

flutter clean && flutter run --verbose

在编译App的过程中执行如下命令,用于将dart代码编译成dill文件(查看终端输出)

/Users/xxx/Library/Android/flutter/bin/cache/dart-sdk/bin/dart --disable-dart-dev
/Users/xxx/Library/Android/flutter/bin/cache/artifacts/engine/darwin-x64/frontend_server.dart.snapshot
--sdk-root /Users/xxx/Library/Android/flutter/bin/cache/artifacts/engine/common/flutter_patched_sdk/
--target=flutter
-Ddart.developer.causal_async_stacks=true
-Ddart.vm.profile=false
-Ddart.vm.product=false
--bytecode-options=source-positions,local-var-info,debugger-stops,instance-field-initializers,keep-unreachable-code,avoid-closure-call-instructions
--enable-asserts
--track-widget-creation
--no-link-platform
--packages /Users/xxx/flutter-workspace/flutter_develop/flutter_tools/example/.packages
--output-dill /Users/xxx/flutter-workspace/flutter_develop/flutter_tools/example/.dart_tool/flutter_build/3ee0d67081b9933cfebe1ac3e5a4992d/app.dill
--depfile /Users/xxx/flutter-workspace/flutter_develop/flutter_tools/example/.dart_tool/flutter_build/3ee0d67081b9933cfebe1ac3e5a4992d/kernel_snapshot.d
package:example/main.dart

可以看得出,使用dart命令携带大量的参数执行frontend_server.dart.snapshot文件(相关的java -jar xxx.jar),而ArgParser类的作用就是用于解析这堆参数。

frontend_server.dart.snapshot文件的入口main方法位于flutter/engine(github.com/flutter/eng…)仓库 flutter_frontend_server/bin/starter.dart文件中,源码如下:

starter.dart

//List<String>就是上面的参数列表:--sdk-root...
void main(List<String> args) async { 
  //执行了server.dart中的starter方法 
  final int exitCode = await starter(args);
  if (exitCode != 0) {
    exit(exitCode);
  }
}

server.dart

Future<int> starter(List<String> args, {
  frontend.CompilerInterface compiler,
  Stream<List<int>> input,
  StringSink output,
  frontend.ProgramTransformer transformer,
}) async {
  ArgResults options;
  //argParser对象就是 ArgParser的实例,位于frontend_server.dart文件中,具体看下一个源码块
  //该方法就是补充更多可使用的参数
  frontend.argParser.addMultiOption(
    'delete-tostring-package-uri',
    help: 'Replaces implementations of `toString` with `super.toString()` for '
        'specified package',
    valueHelp: 'dart:ui',
    defaultsTo: const <String>[],
  );
  try {
      //解析参数
    options = frontend.argParser.parse(args);
  } catch (error) {
    print('ERROR: $error\n');
    print(frontend.usage);
    return 1;
  }
    
  ... 
  //使用参数
  final Set<String> deleteToStringPackageUris =
      (options['delete-tostring-package-uri'] as List<String>).toSet();
  ... 
}

frontend_server.dart

...
ArgParser argParser = ArgParser(allowTrailingOptions: true)
  ..addFlag('train',
      help: 'Run through sample command line to produce snapshot',
      negatable: false)
...  

ArgParser源码分析

ArgParser

class ArgParser {
  final Map<String, Option> _options;
  
  //将上面的_options包装成不可变对象,内部数据是一致的
  final Map<String, Option> options;

  factory ArgParser({bool allowTrailingOptions = true, int usageLineLength}) =>
      ArgParser._(
           <String, Option>{},
          <String, ArgParser>{},
          ...);

  ArgParser._(Map<String, Option> options, Map<String, ArgParser> commands,
      {bool allowTrailingOptions = true, this.usageLineLength})
      : _options = options,
        options = UnmodifiableMapView(options);

  
  //添加flag类型的options
  void addFlag(String name,
      {String abbr,
      String help,
      bool defaultsTo = false,
      bool negatable = true,
      void Function(bool) callback,
      bool hide = false}) {
    _addOption(name,abbr,help,null,null,null,defaultsTo,
        callback == null ? null : (value) => callback(value as bool),
        OptionType.flag,
        negatable: negatable,
        hide: hide);
  }
  
  //添加多参数的options,比如--bytecode-options=source-positions,local-var-info,debugger-stops,可选值用,隔开
  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,
      bool splitCommas = true,
      bool hide = false}) {
    _addOption(name,abbr,help,valueHelp,
        allowed,
        allowedHelp,
        defaultsTo?.toList() ?? <String>[],
        callback == null ? null : (value) => callback(value as List<String>),
        OptionType.multiple,
        splitCommas: splitCommas,
        hide: hide);
  }

  //添加Options
  void _addOption(
      String name,
      String abbr,
      String help,
      String valueHelp,
      Iterable<String> allowed,
      Map<String, String> allowedHelp,
      defaultsTo,
      Function callback,
      OptionType type,
      {bool negatable = false,
      bool splitCommas,
      bool hide = false}) {
    ...
    var option = newOption(name, abbr, help, valueHelp, allowed, allowedHelp,
        defaultsTo, callback, type,
        negatable: negatable, splitCommas: splitCommas, hide: hide);
    _options[name] = option;
    _optionsAndSeparators.add(option);
  }
  
  //根据option的名称获取该options的默认值
  dynamic getDefault(String option) {
    if (!options.containsKey(option)) {
      throw ArgumentError('No option named $option');
    }
    return options[option].defaultsTo;
  }
    
  //通过option配置的缩写,搜索options对象 
  Option findByAbbreviation(String abbr) {
    return options.values
        .firstWhere((option) => option.abbr == abbr, orElse: () => null);
  }
    
  //解析命令行中的参数列表,并返回解析结果对象 ArgResults
  //真的的解析过程交给Parser类处理, 从这里可以看出ArgsParser相当于Parser的包装类
  ArgResults parse(Iterable<String> args) =>
      Parser(null, this, Queue.of(args)).parse();


 //以下用于控制调用--help或者-h的输出内容
 final _optionsAndSeparators = [];
 
 void addSeparator(String text) {
    _optionsAndSeparators.add(text);
  }

  String get usage {
    return Usage(_optionsAndSeparators, lineLength: usageLineLength).generate();
  }
} 

Parser

class Parser {
    
  //把终端的所有的参数都解析完成以后,除去option项剩余的参数都存在rest中          
  final rest = <String>[];
  final ArgParser grammar;
  final Queue<String> args;
  
  //存放解析好的Option.name 和 value 键值对
  final Map<String, dynamic> results = <String, dynamic>{};
    
  Parser(this.grammar, this.args);
  
  //获取参数列表的第一个
  String get current => args.first;
  
 ArgResults parse() {
  var arguments = args.toList();
  ...
  ArgResults commandResults;
  while (args.isNotEmpty) {
    if (current == '--') {
      //如果参数列表第一个是 --, 则停止解析,剩余没有解析参数直接放入rest列表中 
      args.removeFirst();
      break;
    }
    ...

    //解析到Option,分为2种情况:
    //非Flag类型的Option: 比如:...-d test ..., 则从ArgParser中获取缩写为‘d’的Option对象,并把后面的值test赋值给option.value
    //Flag类型的Option: 比如 -f,等同于Option.value=true
    if (parseSoloOption()) continue;
    //解析到option,分为2种情况:
    //非Flag类型的Option: 比如 -mRelease, 则从ArgParser中获取缩写为‘m’的Option对象,并把后面的值Release赋值给option.value
    //Flag类型的Option: 比如 -ftrue
    if (parseAbbreviation(this)) continue;
    //解析到option,这里包括4种情况, 注意从ArgParer中获取option对象是根据全名,而不是缩写
    //1. --mode=release
    //2. --mode release
    //3. --opt ,名称是opt的Flag类型Option,将值(option.value)设置为true
    //4. --no-opt,名称是opt的Flag类型Option,将值(option.value)设置为false
    if (parseLongOption()) continue;
    
    //如果以上的条件成立,内部会调用 args.removeFirst(),
    ...
    
    //没有作为Option的参数添加到rest列表中
    rest.add(args.removeFirst());
  }
  
  //没有作为Option的参数添加到rest列表中
  rest.addAll(args);
  args.clear();
  return newArgResults(
      grammar, results, commandName, commandResults, rest, arguments);
  }    
} 

前面知道了forntent_server.dart中创建了ArgParser实例,代码如下,具体参数含义,不做过多解释。

ArgParser argParser = ArgParser(allowTrailingOptions: true)
  ..addFlag('train',
      help: 'Run through sample command line to produce snapshot',
      negatable: false)
  ..addFlag('incremental',
      help: 'Run compiler in incremental mode', defaultsTo: false)
  ..addOption('sdk-root',
      help: 'Path to sdk root',
      defaultsTo: '../../out/android_debug/flutter_patched_sdk')
  ..addOption('platform', help: 'Platform kernel filename')
  ..addFlag('aot',
      help: 'Run compiler in AOT mode (enables whole-program transformations)',
      defaultsTo: false)
  ..addFlag('tfa',
      help:
          'Enable global type flow analysis and related transformations in AOT mode.',
      defaultsTo: false)
  ..addFlag('tree-shake-write-only-fields',
      help: 'Enable tree shaking of fields which are only written in AOT mode.',
      defaultsTo: true)
  ..addFlag('protobuf-tree-shaker',
      help: 'Enable protobuf tree shaker transformation in AOT mode.',
      defaultsTo: false)
  ..addFlag('protobuf-tree-shaker-v2',
      help: 'Enable protobuf tree shaker v2 in AOT mode.', defaultsTo: false)
  ..addFlag('minimal-kernel',
      help: 'Produce minimal tree-shaken kernel file.', defaultsTo: false)
  ..addFlag('link-platform',
      help:
          'When in batch mode, link platform kernel file into result kernel file.'
          ' Intended use is to satisfy different loading strategies implemented'
          ' by gen_snapshot(which needs platform embedded) vs'
          ' Flutter engine(which does not)',
      defaultsTo: true)
  ..addOption('import-dill',
      help: 'Import libraries from existing dill file', defaultsTo: null)
  ..addOption('from-dill',
      help: 'Read existing dill file instead of compiling from sources',
      defaultsTo: null)
  ..addOption('output-dill',
      help: 'Output path for the generated dill', defaultsTo: null)
  ..addOption('output-incremental-dill',
      help: 'Output path for the generated incremental dill', defaultsTo: null)
  ..addOption('depfile',
      help: 'Path to output Ninja depfile. Only used in batch mode.')
  ..addOption('packages',
      help: '.packages file to use for compilation', defaultsTo: null)
  ..addOption('target',
      help: 'Target model that determines what core libraries are available',
      allowed: <String>[
        'vm',
        'flutter',
        'flutter_runner',
        'dart_runner',
        'dartdevc'
      ],
      defaultsTo: 'vm')
  ..addMultiOption('filesystem-root',
      help: 'File path that is used as a root in virtual filesystem used in'
          ' compiled kernel files. When used --output-dill should be provided'
          ' as well.',
      hide: true)
  ..addOption('filesystem-scheme',
      help:
          'Scheme that is used in virtual filesystem set up via --filesystem-root'
          ' option',
      defaultsTo: 'org-dartlang-root',
      hide: true)
  ..addFlag('enable-http-uris',
      defaultsTo: false, hide: true, help: 'Enables support for http uris.')
  ..addFlag('verbose', help: 'Enables verbose output from the compiler.')
  ..addOption('initialize-from-dill',
      help: 'Normally the output dill is used to specify which dill to '
          'initialize from, but it can be overwritten here.',
      defaultsTo: null,
      hide: true)
  ..addMultiOption('define',
      abbr: 'D',
      help: 'The values for the environment constants (e.g. -Dkey=value).')
  ..addFlag('embed-source-text',
      help: 'Includes sources into generated dill file. Having sources'
          ' allows to effectively use observatory to debug produced'
          ' application, produces better stack traces on exceptions.',
      defaultsTo: true)
  ..addFlag('unsafe-package-serialization',
      help: '*Deprecated* '
          'Potentially unsafe: Does not allow for invalidating packages, '
          'additionally the output dill file might include more libraries than '
          'needed. The use case is test-runs, where invalidation is not really '
          'used, and where dill filesize does not matter, and the gain is '
          'improved speed.',
      defaultsTo: false,
      hide: true)
  ..addFlag('incremental-serialization',
      help: 'Re-use previously serialized data when serializing. '
          'The output dill file might include more libraries than strictly '
          'needed, but the serialization phase will generally be much faster.',
      defaultsTo: true,
      negatable: true,
      hide: true)
  ..addFlag('track-widget-creation',
      help: 'Run a kernel transformer to track creation locations for widgets.',
      defaultsTo: false)
  ..addFlag('gen-bytecode', help: 'Generate bytecode', defaultsTo: false)
  ..addMultiOption('bytecode-options',
      help: 'Specify options for bytecode generation:',
      valueHelp: 'opt1,opt2,...',
      allowed: BytecodeOptions.commandLineFlags.keys,
      allowedHelp: BytecodeOptions.commandLineFlags)
  ..addFlag('drop-ast',
      help: 'Include only bytecode into the output file', defaultsTo: true)
  ..addFlag('enable-asserts',
      help: 'Whether asserts will be enabled.', defaultsTo: false)
  ..addFlag('null-safety',
      help:
          'Respect the nullability of types at runtime in casts and instance checks.',
      defaultsTo: null)
  ..addMultiOption('enable-experiment',
      help: 'Comma separated list of experimental features, eg set-literals.',
      hide: true)
  ..addFlag('split-output-by-packages',
      help:
          'Split resulting kernel file into multiple files (one per package).',
      defaultsTo: false)
  ..addOption('component-name', help: 'Name of the Fuchsia component')
  ..addOption('data-dir',
      help: 'Name of the subdirectory of //data for output files')
  ..addOption('far-manifest', help: 'Path to output Fuchsia package manifest')
  ..addOption('libraries-spec',
      help: 'A path or uri to the libraries specification JSON file')
  ..addFlag('debugger-module-names',
      help: 'Use debugger-friendly modules names', defaultsTo: false)
  ..addFlag('experimental-emit-debug-metadata',
      help: 'Emit module and library metadata for the debugger',
      defaultsTo: false)
  ..addOption('dartdevc-module-format',
      help: 'The module format to use on for the dartdevc compiler',
      defaultsTo: 'amd');

需要注意的时,-D的参数使用,但是由于编译时执行frontend_server.dart.snapshot命令,其中参数我们无法直接添加,但好在flutter给我们留了其他的入口:

flutter run --verbose --dart-define=test=true //key: dart-define value:test=true

最后这个dart-define配置会透传到执行frontend_server.dart.snapshot命令中:

/Users/xxx/Library/Android/flutter/bin/cache/dart-sdk/bin/dart --disable-dart-dev /Users/xxx/Library/Android/flutter/bin/cache/artifacts/engine/darwin-x64/frontend_server.dart.snapshot -Dtest=true ...

程序使用时,需要定义一个常量字段,参考: flutter/foundation.dart中的kRelease 字段

ArgParser.parse方法,最后会返回一个 ArgResult 对象

ArgResult

class ArgResults {
  
  //解析option获取到的name:value键值对,比如--target=flutter,则name=target value=flutter
  final Map<String, dynamic> _parsed;
  //剩余没有解析的字段
  final List<String> rest;
  
  ArgResults._(... this._parsed, this.rest...);

 //定义操作符,支持argResult['target']获取对应的值
  dynamic operator [](String name) {
    ...
    return _parser.options[name].getOrDefault(_parsed[name]);
  }
}