Flutter命令行参数解析插件parse_args的使用

Flutter命令行参数解析插件parse_args的使用

特性

  1. 包含函数 parseArgsparseSubCmd 以及几个辅助类,包括自定义异常。
  2. 函数 parseSubCmd 接收一个命令行参数列表和一个 <String, Function>{} 类型的映射。它仅检查第一个命令行参数。如果该参数是一个普通的参数(即不以破折号 - 或加号 + 开头),则将其视为映射中的键,并执行关联的函数。每个这样的函数都会接收其余参数的列表,并通常会调用 parseArgs 来解析特定的选项及其值。
  3. 函数 parseArgs 识别选项,即任何以一个或多个破折号 - 或加号 + 开头,后跟一个英文字母,然后是其他字符的单词。它积累所有可能的值(每个参数直到下一个选项为止),根据用户定义的格式进行验证,并创建结果集合以供进一步使用。
  4. 它通过选项定义字符串(第一个参数)来验证用户指定的选项和值。此字符串可以多行,因为所有空白字符将被删除。让我们来看一个这样的字符串的例子:
|q,quiet|v,verbose|?,h,help|d,dir:|c,app-config:|f,force|p,compression:
|l,filter:: >and,c,case   not,  or 
|i,inp,inp-files:,:
|o,out,out-files:,:?
|::
  1. 每个选项定义由竖线 | 分隔。
  2. 普通参数被视为无名称选项的值。
  3. 如果命令行参数以加号 +-no 开头,则将其视为负选项,除非你定义了长选项如 north。在这种情况下,它不会被转换为负的 rth。请注意,只有标志(没有值的选项)可以是负的。
  4. 通过指定冒号 :,你要求相应的选项有一个单一的参数。
  5. 通过指定双冒号 ::,你要求相应的选项有一个或多个参数:-inp-file abc.txt de.lst fghi.docx
  6. 通过在双冒号中指定逗号 :,,你要求后续参数表示一个由逗号分隔的值列表。缺少逗号意味着单个值。不再有更多值与该选项相关联。你可以使用除管道符和冒号之外的任何其他字符作为分隔符。例如:-inp-file abc.txt,de.lst fghi.docx 结果是两个 -inp-file 的值,后面跟着一个普通参数。同样的效果可以通过 -inp-file=abc.txt,de.lst fghi.docx 实现。但是 -inp-file abc.txt de.lst,fghi.docx 结果是 -inp-file 的一个值,后面跟着一个或两个普通参数,这取决于普通参数是否也有值分隔符。这会阻止混合列表,其中有时选项值作为单独的参数传递,有时作为由分隔符分隔的值列表。然而,无论是否存在值分隔符,普通参数列表都会增长。
  7. 通过指定 :?::?:,?,你允许 0 或 1/多/分隔的值(使普通参数可选是有用的)。
  8. 你可以指定多个选项名称。
  9. 每个选项名称都被规范化:所有可能的空格、破折号 - 和加号 +(对于负标志)被移除,所有字母被转换为你所需的大小写:exact(无转换)、lower(小写,即不区分大小写)或 smart(短选项保持原样,而长选项转换为小写)。
  10. 短选项和子选项可以捆绑在一起:-c -l -i-cli 相同,选项的出现顺序不影响选项本身,但会影响子选项。
  11. 函数 parseArgs 返回一个类型为 List<CliOpt> 的对象。扩展类 CliOptList 的方法用于检索一个或多个值并将其转换为所需的数据类型:isSet(optName)getIntValues(optName, {radix})getDateValues(optName)getStrValues(optName)
  12. 如果选项定义包含 >,那么接下来直到下一个 | 或字符串结束的部分将被视为由逗号分隔的子选项列表。这些将被视为普通值,依赖于调用者的解释。例如,你需要将多个过滤器字符串作为某个选项的值传递。一些过滤器可能需要区分大小写的比较,而另一些则不需要;一些可能需要直接匹配,而另一些则需要相反的匹配(未找到):
myapp -filter -case "Ab" "Cd" --no-case "xyz" -not "uvw"

选项 -filter 的值数组将是:["-case", "Ab", "Cd", "+case", "xyz", "-not", "uvw"]。这允许你遍历元素并在遇到子选项时打开或关闭某些标志。当然,有人可能会争辩说,可以通过引入 4 个不同的选项来实现相同的结果。但是首先,这是一个简单的例子。其次,在后一种情况下,你也必须处理序列排列的额外问题,如 --filter-case-not 应等效于 --filter-not-case,等等。没有子选项的情况下,事情会变得非常复杂。

  1. 该函数允许一种‘奇怪’(甚至‘错误’)的方式来传递多个选项值。然而,这样做简化了情况,并使不再需要普通参数(那些没有选项的参数)成为多余。你可以通过使用值分隔符或等于号来覆盖这种行为:-a="1,2" 3 4 -b -c 5(选项 -a 得到 ["1", "2"]-c 得到 ["5"]["3", "4"] 将被视为普通参数)。
  2. 如果你想传递普通参数,你应该在选项定义字符串中显式指定。格式与选项相同,但选项名应为空:|:, |::, |::>and,or(后者允许普通参数的子选项)。
  3. 该函数允许等号:-name="value"-name='value'。然而,单独的普通参数紧随其后将不会被视为该选项的附加值,而是被视为普通参数。即使选项被定义为允许多个值,且没有定义值分隔符。
  4. 该函数将独立的双破折号 -- 解释为标志,这意味着从这一点起的任何参数都将被视为普通参数(没有选项)。
  5. 该函数将三破折号 --- 解释为标志,这意味着不应再将任何参数视为选项名,而应将其添加到最后遇到的选项的值中。
  6. 该函数允许对短(单字符)选项名进行捆绑。有方法 testNames 用于 CliOptDefCliOptDefList,可以从单元测试中调用,以确保所有选项和子选项名称都是唯一的,而且没有长选项名可以被误认为是短选项名的组合。
  7. 子选项优先政策:如果选项 -a 有一个子选项 -b,并且还有一个主要选项 -b,那么在以下情况下 -b 将被视为子选项:-a -b -c。而在 -b -a -c 中,-b 将被视为主要选项。这允许混合具有相似子选项的选项。例如,|p,plain::>and,not,or,p,plain,r,regex|r,regex::>and,not,or,p,plain,r,regex 允许混合两种模式与逻辑运算。

使用方法

查看下文的 Example 部分。所有示例代码文件都在子目录 example 下。

示例代码

// Copyright (c) 2022-2023, Alexander Iurovetski
// All rights reserved under MIT license (see LICENSE file)

import 'package:file/local.dart';
import 'package:glob/glob.dart';
import 'package:parse_args/parse_args.dart';
import 'package:thin_logger/thin_logger.dart';

/// Pretty basic singleton for simple FileSystem
///
final _fs = LocalFileSystem();

/// Pretty basic singleton for simple logging
///
final _logger = Logger();

/// Simple filtering class
///
class Filter {
  /// Flag indicating positive match
  ///
  bool isPositive;

  /// Glob pattern to match filenames against
  ///
  Glob glob;

  /// Default constructor
  ///
  Filter(this.glob, this.isPositive);

  /// Serializer
  ///
  [@override](/user/override)
  String toString() => '${isPositive ? glob : '!($glob)'}';
}

/// Application options
///
class Options {
  /// Application name
  ///
  static const appName = 'sampleapp';

  /// Application version
  ///
  static const appVersion = '0.1.2';

  /// Access (octal)
  ///
  int get access => _access;
  var _access = 0;

  /// Application configuration path
  ///
  String get appConfigPath => _appConfigPath;
  var _appConfigPath = '';

  /// Compression level
  ///
  int get compression => _compression;
  var _compression = 6;

  /// List of lists of filters
  ///
  List<List<Filter>> get filterLists => _filterLists;
  final _filterLists = <List<Filter>>[];

  /// Force otherwise incremental processing
  ///
  bool get isForced => _isForced;
  var _isForced = false;

  /// List of input files
  ///
  List<String> get inputFiles => _inputFiles;
  final _inputFiles = <String>[];

  /// List of output files
  ///
  List<String> get outputFiles => _outputFiles;
  final _outputFiles = <String>[];

  /// Directory to start in (switch to at the beginning)
  ///
  List<String> get plainArgs => _plainArgs;
  var _plainArgs = <String>[];

  /// Directory to start in (switch to at the beginning)
  ///
  get startDirName => _startDirName;
  var _startDirName = '';

  /// Sample application's command-line parser
  ///
  Future parse(List<String> args) async {
    final ops = 'and,not,or,case';

    final optDefStr = '''
      |q,quiet|v,verbose|?,h,help|access:,:|d,dir:|app-config:|f,force|p,compression:
      |l,filter::>$ops
      |i,inp,inp-files:,:
      |o,out,out-files:,:
      |::$ops
    ''';

    final result = parseArgs(optDefStr, args, validate: true);

    if (result.isSet('help')) {
      usage();
    }

    if (result.isSet('quiet')) {
      _logger.level = Logger.levelQuiet;
    } else if (result.isSet('verbose')) {
      _logger.level = Logger.levelVerbose;
    }

    _logger.out('Parsed ${result.toString()}\n');

    _appConfigPath =
        _fs.path.join(_startDirName, result.getStrValue('appconfig'));
    _access = result.getIntValue('access', radix: 8) ?? 420 /* octal 644 */;
    _compression = result.getIntValue('compression') ?? 6;
    _isForced = result.isSet('force');
    _plainArgs = result.getStrValues('');

    setFilters(result.getStrValues('filter'));

    final optName = '-case';

    switch (optName) {
      case '-pattern':
        break;
      case '-case':
        break;
      case '+case':
        break;
    }

    await setStartDirName(result.getStrValue('dir') ?? '');
    await setPaths(_inputFiles, result.getStrValues('inpfiles'));
    await setPaths(_outputFiles, result.getStrValues('outfiles'));

    _logger.out('''
AppCfgPath: $_appConfigPath
Compress:   $_compression
isForced:   $_isForced
PlainArgs:  $_plainArgs
StartDir:   $_startDirName
Filters:    $_filterLists
InpFiles:   $_inputFiles
OutFiles:   $_outputFiles
''');
  }

  /// Add all filters with the appropriate pattern and flags
  ///
  void setFilters(List values) {
    var isNew = true;
    var isPositive = true;
    var isCaseSensitive = true;

    for (var value in values) {
      switch (value) {
        case '-and':
          isNew = false;
          isPositive = true;
          continue;
        case '+and':
          isNew = false;
          isPositive = false;
          continue;
        case '-not':
          isPositive = false;
          continue;
        case '-or':
          isNew = true;
          isPositive = true;
          continue;
        case '+or':
          isNew = true;
          isPositive = false;
          continue;
        case '-case':
          isCaseSensitive = true;
          continue;
        case '+case':
          isCaseSensitive = false;
          continue;
        default:
          final glob = Glob(value, caseSensitive: isCaseSensitive);
          final filter = Filter(glob, isPositive);

          if (isNew || _filterLists.isEmpty) {
            _filterLists.add([filter]);
          } else {
            _filterLists[_filterLists.length - 1].add(filter);
          }
          isPositive = true; // applies to a single (the next) value only
      }
    }
  }

  /// General-purpose method to add file paths to destinaltion list and check the existence immediately
  ///
  Future setPaths(List<String> to, List from, {bool isRequired = false}) async {
    for (var x in from) {
      final path = _fs.path.isAbsolute(x) ? x : _fs.path.join(_startDirName, x);
      to.add(path);

      if (isRequired && !(await _fs.file(path).exists())) {
        _logger.error('*** ERROR: Input file not found: "$path"');
      }
    }
  }

  /// General-purpose method to set start directory and check its existence immediately
  ///
  Future setStartDirName(String value, {bool isRequired = false}) async {
    _startDirName = value;

    if (isRequired && !(await _fs.directory(_startDirName).exists())) {
      _logger.error('*** ERROR: Invalid startup directory: "$_startDirName"');
    }
  }

  /// Displaying the help and optionally, an error message
  ///
  Never usage([String? error]) {
    throw Exception('''

${Options.appName} ${Options.appVersion} (c) 2022-2023 My Name

Long description of the application functionality

USAGE:

${Options.appName} [OPTIONS]

OPTIONS (case-insensitive and dash-insensitive):

-?, -h, -[-]help                     - this help screen
-c, -[-]app[-]config FILE            - configuration file path/name
-d, -[-]dir DIR                      - directory to start in
-f, -[-]force                        - overwrite existing output file
-l, -[-]filter F1 [op] F2 [op] ...   - a list of filters with operations
                                       (-and, -not, -or, -case, -nocase)
-i, -[-]inp[-files] FILE1 [FILE2...] - the input file paths/names
-o, -[-]out[-files] FILE1 [FILE2...] - the output file paths/names
-p, -[-]compression INT              - compression level
-v, -[-]verbose                      - detailed application log

EXAMPLE:

${Options.appName} -AppConfig default.json -filter "abc" --dir somedir/Documents -inp a*.txt ../Downloads/bb.xml --out-files ../Uploads/result.txt -- -result_more.txt
${Options.appName} -AppConfig default.json -filter "abc" -and "de" -or -not "fghi" -inp b*.txt ../Downloads/c.xml --out-files ../Uploads/result.txt -- -result_more.txt

${(error == null) || error.isEmpty ? '' : '*** ERROR: $error'}
''');
  }
}

/// Sample application entry point
///
Future main(List<String> args) async {
  try {
    var o = Options();
    await o.parse(args);
    // the rest of processing
  } on Exception catch (e) {
    _logger.error(e.toString());
  } on Error catch (e) {
    _logger.error(e.toString());
  }
}

更多关于Flutter命令行参数解析插件parse_args的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter命令行参数解析插件parse_args的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


在Flutter中,如果你想解析命令行参数,可以使用 parse_args 插件。parse_args 是一个简单易用的命令行参数解析库,可以帮助你解析和处理命令行参数。

安装 parse_args

首先,你需要在 pubspec.yaml 文件中添加 parse_args 依赖:

dependencies:
  parse_args: ^1.0.0

然后在终端中运行 flutter pub get 来安装依赖。

使用 parse_args

下面是一个简单的例子,展示了如何使用 parse_args 来解析命令行参数。

import 'package:parse_args/parse_args.dart';

void main(List<String> arguments) {
  // 定义命令行参数
  final parser = ArgParser()
    ..addOption('name', abbr: 'n', help: 'Your name')
    ..addFlag('verbose', abbr: 'v', help: 'Enable verbose output', defaultsTo: false);

  // 解析命令行参数
  final ArgResults args = parser.parse(arguments);

  // 获取参数值
  final String? name = args['name'];
  final bool verbose = args['verbose'];

  // 使用参数值
  if (name != null) {
    print('Hello, $name!');
  } else {
    print('Hello, World!');
  }

  if (verbose) {
    print('Verbose mode is enabled.');
  }
}

运行程序

假设你将上述代码保存为 main.dart,你可以在终端中运行以下命令来测试:

dart main.dart --name Alice --verbose

输出将会是:

Hello, Alice!
Verbose mode is enabled.

如果你不提供 --name 参数:

dart main.dart --verbose

输出将会是:

Hello, World!
Verbose mode is enabled.

参数说明

  • addOption:用于添加一个选项参数,例如 --nameabbr 是缩写形式,help 是帮助信息。
  • addFlag:用于添加一个标志参数,例如 --verboseabbr 是缩写形式,help 是帮助信息,defaultsTo 是默认值。

获取帮助信息

parse_args 还支持自动生成帮助信息。你可以通过 parser.usage 来获取帮助信息:

if (arguments.isEmpty || arguments.contains('--help')) {
  print(parser.usage);
  return;
}
回到顶部