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解析实现

Download history 58/week @ 2024-03-11 55/week @ 2024-03-18 33/week @ 2024-03-25 92/week @ 2024-04-01 33/week @ 2024-04-08 33/week @ 2024-04-15 49/week @ 2024-04-22 46/week @ 2024-04-29 41/week @ 2024-05-06 42/week @ 2024-05-13 45/week @ 2024-05-20 37/week @ 2024-05-27 42/week @ 2024-06-03 32/week @ 2024-06-10 54/week @ 2024-06-17 42/week @ 2024-06-24

每月 173 次下载
8 crates 中使用

MIT 许可证

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
}

标志(短形式和长形式)如 linesverbose 以及 位置参数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") 等,可以访问数组值标志(多个或数组)。

如果您需要除标准数值类型(i32f32)以外的其他类型,可以指定类型: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 的简单,不添加额外的依赖。

无运行时依赖