6 个版本 (3 个破坏性更新)
使用旧的 Rust 2015
0.4.0 | 2018 年 12 月 5 日 |
---|---|
0.3.1 | 2018 年 1 月 10 日 |
0.3.0 | 2017 年 12 月 11 日 |
0.2.2 | 2017 年 9 月 13 日 |
0.1.0 | 2016 年 12 月 14 日 |
#791 在 解析实现
每月 173 次下载
在 8 crates 中使用
60KB
1K SLoC
命令行解析对于需要由其他人运行的任何程序都是必不可少的,这是一个复杂的任务,有很多边缘情况:这是一个不应该在项目中重复发明的轮子。它也不应该像 C 程序的 POSIX 接口 getopt_long
那样使用起来太丑陋。
这个包是 Lua 库 lapp 的 Rust 实现。与 docopt 一样,它从这样一个事实开始,即你无论如何都必须输出使用文本,为什么不从这个文本中提取标志名称和类型呢?这是一个那种经常发生多次的想法——我的第一个实现是在 2009 年,现在是 Penlight Lua 库的一部分;docopt 大约在 2011 年稍后出现。
鉴于有 docopt 的 Rust 实现,Lapp 在 Rust 中的理由是什么?它更容易使用和理解,并且满足不需要子命令等的基本命令行界面的需求。哲学是“尽早失败并失败得彻底”——如果程序有任何错误,它就会退出并返回非零代码。
考虑一个需要给文件和要输出到 stdout 的行数的 'head' 程序
// head.rs
extern crate lapp;
fn main() {
let args = lapp::parse_args("
Prints out first n lines of a file
-n, --lines (default 10) number of lines
-v, --verbose
<file> (string) input file name
");
let n = args.get_integer("lines");
let verbose = args.get_bool("verbose");
let file = args.get_string("file");
// your magic goes here
}
有 标志(短形式和长形式)如 lines
、verbose
以及 位置参数 如 file
。标志有一个关联的类型——对于 lines
,这是从默认值推断出来的,而对于 file
,它是明确指定的。没有默认值的标志或参数必须指定——除了简单的布尔标志,它们默认为 false。
这为你做了大量工作,因为你无论如何都必须编写使用文本
- 使用 'mini-language' 的用法相当简单
- 命令行参数的处理遵循GNU风格。您可以输入
--lines 20
或-n 20
;短选项可以组合使用-vn20
。--
表示命令行处理的结束。 - 未提供位置参数或必需的选项是错误的。
- 标志
lines
的值必须是有效的整数,并将进行转换。
所以,想法是让程序员使用起来简单直接,同时对于用户来说也足够自我解释。
Lapp迷你语言
Lapp规范中的一个重要行要么以'-'(标志)开头,要么以'<'(位置参数)开头。标志可以是'-s, --long'或'-s'。其他行都将被忽略。短标志只能由字母或数字组成;长标志可以是字母数字,包括'_'和'-'。
这些重要行之后可以跟一个类型默认指定符(括号内)。它可以是类型,如'(string)'或默认值,如'(default 10)'。如果不存在,则该标志是一个简单的布尔标志,默认为false。当前支持的类型有
- 字符串
- 整数(
i32
) - 浮点数(
f32
) - 布尔值
- 输入文件(
Box<Read>
)(默认可以是"stdin") - 输出文件(
Box<Write>
)(默认可以是"stdout") - 路径(
PathBuf
)(默认将被展开为波浪号)
如果使用'(default )',则类型将根据值推断——如果数值则推断为整数或浮点数,否则为字符串。始终可以在单引号中引用默认字符串值,如果默认值不是一个单词,您应该这样做。如果有疑问,请引用。
从版本0.3.0开始,也可以同时指定类型和默认值,例如"(integer default 0)"或"(path default ~/.bonzo)"。
如果没有默认值(除了简单的标志外),则该标志或参数必须在命令行上指定——它们是必需的。
此外,标志可以是多个或数组。两者都由一个基本类型的向量表示,但使用方式不同。例如,
-I, --include... (string) flag may appear multiple times
-p, --ports (integer...) the flag value itself is an array
<args> (string...)
...
./exe -I. --include lib
./exe --ports '9000 9100 9200'
./exe one two three
数组标志是使用空格或逗号分隔的列表。(但如果您使用逗号,将删除额外的空格。)
多个标志在标志后跟'...',数组标志在类型后跟'...'。例外的是位置标志,它们始终是多个。此语法不支持默认值,因为默认值是明确定义的——一个空向量。
支持范围。"(1..10)"表示介于1和10之间(包括10)的整数,"(0.0..5.0)"表示介于0.0和5.0之间的浮点数。
提供了两种方便的文件类型:"infile" 和 "outfile"。函数 get_infile()
将返回一个 Box<Read>
,而 get_outfile()
将返回一个 Box<Write>
。如果参数不是一个可以打开进行读取或写入的文件,则程序将退出。可以指定默认值,所以 "(default stdin)" 将在未提供标志的情况下为您包装 io.stdin()
。 (这就是我们返回boxed trait对象而不是实际的File
对象的原因 - 以处理这种情况。)
默认情况下,访问器函数在出错时退出程序。但对于每个像 args.get_string("flag")
这样的方法,都有一个返回错误的 args.get_string_result("flag")
。
更多代码示例
使用 args.get_strings("flag")
,args.get_integers("flag")
等,可以访问数组值标志(多个或数组)。
如果您需要除标准数值类型(i32
或 f32
)以外的其他类型,可以指定类型:args.get::<u8>("flag")
。那么,指定超出 0..255 的整数将是一个错误。同样,args.get_array::<u8>("flag")
将获取一个整数值数组标志,作为所需类型。
实际上,任何实现了 FromStr trait 的类型都可以工作。在这个例子中,我们希望让用户以十六进制的形式输入整数值。必须提前指定任何用户类型,否则 lapp 将会抱怨不认识类型。
extern crate lapp;
use std::str::FromStr;
use std::num::ParseIntError;
struct Hex {
value: u64
}
impl FromStr for Hex {
type Err = ParseIntError;
fn from_str(s: &str) -> Result<Self,Self::Err> {
let value = u64::from_str_radix(s,16)?;
Ok(Hex{value: value})
}
}
let mut args = lapp::Args::new("
--hex (hex default FF)
");
args.user_types(&["hex"]);
args.parse();
let res: Hex = args.get("hex");
println!("value was {}", res.value);
代码生成
这种方法的批评是它不是非常强类型;程序员需要自己使用正确的 get_<type>
访问器来获取标志,而且拼写错误在运行时会致命。为了正确地获取模板代码,'src/bin' 文件夹中有一个名为 lapp-gen
的工具。在示例文件夹中有一个 test.lapp
文件
Prints out first n lines of a file
-n, --lines (default 10) number of lines
-v, --verbose
<file> (string) input file name
这个文件作为环境变量传递给 lapp-gen
(因为我们不希望在这里混淆命令行参数)
~/rust/lapp/examples$ LAPP_FILE='test.lapp vars' lapp-gen
let lines = args.get_integer("lines");
let verbose = args.get_bool("verbose");
let file = args.get_string("file");
let help = args.get_bool("help");
Lapp 使用一些简单的规则从标志名称创建变量名;任何 '-' 都会被转换为 '';如果标志名称以数字或 '' 开头,那么名称将前置 'c_'。
您可以通过指定文件和任何命令行参数来测试您的规范
~/rust/lapp/examples$ LAPP_FILE='test.lapp' lapp-gen
flag 'lines' value Int(10)
flag 'verbose' value Bool(false)
flag 'file' value Error("required flag file")
flag 'help' value Bool(false)
~/rust/lapp/examples$ LAPP_FILE='test.lapp' lapp-gen hello -v
flag 'lines' value Int(10)
flag 'verbose' value Bool(true)
flag 'file' value Str("hello")
flag 'help' value Bool(false)
~/rust/lapp/examples$ LAPP_FILE='test.lapp' lapp-gen hello -v --lines 30
flag 'lines' value Int(30)
flag 'verbose' value Bool(true)
flag 'file' value Str("hello")
flag 'help' value Bool(false)
~/rust/lapp/examples$ LAPP_FILE='test.lapp' lapp-gen hello -vn 40
flag 'lines' value Int(40)
flag 'verbose' value Bool(true)
flag 'file' value Str("hello")
flag 'help' value Bool(false)
在 examples
中的 mony.lapp
测试文件给出了 Lapp 这个版本可能的所有排列组合。
真正的节省劳动力的代码生成选项是生成一个从 lapp 命令行初始化的结构体
~/rust/lapp/examples$ LAPP_FILE='test.lapp struct:Args' lapp-gen
~/rust/lapp/examples$ cat test.lapp.inc
const USAGE: &'static str = "
Prints out first n lines of a file
-n, --lines (default 10) number of lines
-v, --verbose
<file> (string) input file name
";
#[derive(Debug)]
struct Args {
lines: i32,
verbose: bool,
file: String,
help: bool,
}
impl Args {
fn new() -> (Args,lapp::Args<'static>) {
let args = lapp::parse_args(USAGE);
(Args{
lines: args.get_integer("lines"),
verbose: args.get_bool("verbose"),
file: args.get_string("file"),
help: args.get_bool("help"),
},args)
}
}
现在我们的程序看起来像这样,包括输出 test.lapp.inc
。
// lines.rs
extern crate lapp;
include!("test.lapp.inc");
fn main() {
let (values,args) = Args::new();
if values.lines < 1 {
args.quit("lines must be greater than zero");
}
println!("{:#?}",values);
}
(可能创建一个子模块会更优雅,但这样在示例文件夹中除非有子目录否则将不起作用。)
局限性
在最后一个例子中,有必要显式地 验证 参数并退出,显示适当的消息。但大多数验证都涉及检查多个参数,而更通用的解决方案可能是生成的代码中有一个 validate
方法占位符,您可以在其中放置您的约束。
然而,一般来说,我认为正确地实现一套简单的功能很重要,即使它们有限。还有更多通用的选项来处理更复杂的命令行程序(例如,支持 'cargo build' 或 'git status' 等命令的程序),我打算尽可能保持 lapp
的简单,不添加额外的依赖。