Flutter命令行参数解析插件parse_args的使用
Flutter命令行参数解析插件parse_args的使用
特性
- 包含函数
parseArgs
和parseSubCmd
以及几个辅助类,包括自定义异常。 - 函数
parseSubCmd
接收一个命令行参数列表和一个<String, Function>{}
类型的映射。它仅检查第一个命令行参数。如果该参数是一个普通的参数(即不以破折号-
或加号+
开头),则将其视为映射中的键,并执行关联的函数。每个这样的函数都会接收其余参数的列表,并通常会调用parseArgs
来解析特定的选项及其值。 - 函数
parseArgs
识别选项,即任何以一个或多个破折号-
或加号+
开头,后跟一个英文字母,然后是其他字符的单词。它积累所有可能的值(每个参数直到下一个选项为止),根据用户定义的格式进行验证,并创建结果集合以供进一步使用。 - 它通过选项定义字符串(第一个参数)来验证用户指定的选项和值。此字符串可以多行,因为所有空白字符将被删除。让我们来看一个这样的字符串的例子:
|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:,:?
|::
- 每个选项定义由竖线
|
分隔。 - 普通参数被视为无名称选项的值。
- 如果命令行参数以加号
+
或-no
开头,则将其视为负选项,除非你定义了长选项如north
。在这种情况下,它不会被转换为负的rth
。请注意,只有标志(没有值的选项)可以是负的。 - 通过指定冒号
:
,你要求相应的选项有一个单一的参数。 - 通过指定双冒号
::
,你要求相应的选项有一个或多个参数:-inp-file abc.txt de.lst fghi.docx
- 通过在双冒号中指定逗号
:,
,你要求后续参数表示一个由逗号分隔的值列表。缺少逗号意味着单个值。不再有更多值与该选项相关联。你可以使用除管道符和冒号之外的任何其他字符作为分隔符。例如:-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
的一个值,后面跟着一个或两个普通参数,这取决于普通参数是否也有值分隔符。这会阻止混合列表,其中有时选项值作为单独的参数传递,有时作为由分隔符分隔的值列表。然而,无论是否存在值分隔符,普通参数列表都会增长。 - 通过指定
:?
或::?
或:,?
,你允许 0 或 1/多/分隔的值(使普通参数可选是有用的)。 - 你可以指定多个选项名称。
- 每个选项名称都被规范化:所有可能的空格、破折号
-
和加号+
(对于负标志)被移除,所有字母被转换为你所需的大小写:exact
(无转换)、lower
(小写,即不区分大小写)或smart
(短选项保持原样,而长选项转换为小写)。 - 短选项和子选项可以捆绑在一起:
-c -l -i
与-cli
相同,选项的出现顺序不影响选项本身,但会影响子选项。 - 函数
parseArgs
返回一个类型为List<CliOpt>
的对象。扩展类CliOptList
的方法用于检索一个或多个值并将其转换为所需的数据类型:isSet(optName)
、getIntValues(optName, {radix})
、getDateValues(optName)
、getStrValues(optName)
- 如果选项定义包含
>
,那么接下来直到下一个|
或字符串结束的部分将被视为由逗号分隔的子选项列表。这些将被视为普通值,依赖于调用者的解释。例如,你需要将多个过滤器字符串作为某个选项的值传递。一些过滤器可能需要区分大小写的比较,而另一些则不需要;一些可能需要直接匹配,而另一些则需要相反的匹配(未找到):
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
,等等。没有子选项的情况下,事情会变得非常复杂。
- 该函数允许一种‘奇怪’(甚至‘错误’)的方式来传递多个选项值。然而,这样做简化了情况,并使不再需要普通参数(那些没有选项的参数)成为多余。你可以通过使用值分隔符或等于号来覆盖这种行为:
-a="1,2" 3 4 -b -c 5
(选项-a
得到["1", "2"]
,-c
得到["5"]
,["3", "4"]
将被视为普通参数)。 - 如果你想传递普通参数,你应该在选项定义字符串中显式指定。格式与选项相同,但选项名应为空:
|:
,|::
,|::>and,or
(后者允许普通参数的子选项)。 - 该函数允许等号:
-name="value"
或-name='value'
。然而,单独的普通参数紧随其后将不会被视为该选项的附加值,而是被视为普通参数。即使选项被定义为允许多个值,且没有定义值分隔符。 - 该函数将独立的双破折号
--
解释为标志,这意味着从这一点起的任何参数都将被视为普通参数(没有选项)。 - 该函数将三破折号
---
解释为标志,这意味着不应再将任何参数视为选项名,而应将其添加到最后遇到的选项的值中。 - 该函数允许对短(单字符)选项名进行捆绑。有方法
testNames
用于CliOptDef
和CliOptDefList
,可以从单元测试中调用,以确保所有选项和子选项名称都是唯一的,而且没有长选项名可以被误认为是短选项名的组合。 - 子选项优先政策:如果选项
-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
更多关于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
:用于添加一个选项参数,例如--name
,abbr
是缩写形式,help
是帮助信息。addFlag
:用于添加一个标志参数,例如--verbose
,abbr
是缩写形式,help
是帮助信息,defaultsTo
是默认值。
获取帮助信息
parse_args
还支持自动生成帮助信息。你可以通过 parser.usage
来获取帮助信息:
if (arguments.isEmpty || arguments.contains('--help')) {
print(parser.usage);
return;
}