xcli,一个简单易用的命令行工具

项目地址:https://github.com/kingwel-xie/xcli-rs

xcli是一个命令行的工具,支持自定义添加命令,每个命令支持缩写使用,同时也支持tab方式补全命令。

设计思路

这个工具的设计初衷是为了能够提供命令行功能,同时可以很容易的添加自定义的命令。

应用场景

目前在libp2p-rs中,xcli提供了命令行的功能,可对swarm和kad进行调试。

类型转换

从命令行中获取到的参数args是一个引用类型的&str数组,即&[&str]。在xcli中,实现了一个名为check_param的宏,返回的值即为想要转换的对应类型。check_param!需要四个参数

($param_count:expr, $required:expr, $args:ident, ($($change_type:ty=>$has_from:expr), *))

分别代表参数总个数、必选参数个数,参数列表,最后一个参数比较特殊,代表着需要转换的类型。书写格式形如(String=>1),对于所有输入参数都需要设置该转换类型。

需要注意的一点是,参数的总个数必须与最后的参数转换类型个数相同。譬如总共有5个参数,那么后面的类型转换也需要将这五个参数的类型都进行设置。

举例说明:

let u = check_param!(3, 1, args, (String=>1, String=>1, String=>1))

这段代码表示总共需要三个参数,其中一个是必须的,另外两个是可选的,这三个参数都是String类型的。返回值的个数最少是1个(必须参数一定返回),最多是3个。

命令补全

由于底层库使用的是rustyline,它提供了一个Completer的trait,实现fn complete()即可支持tab补全。

在App::run()中,我们对Command执行了一个方法:

self.rl.borrow_mut().set_helper(Some(PrefixCompleter::new(&self.tree)));

这段代码的逻辑是将Command单独抽离出来形成一个类似树的结构。
以下这段代码是补全功能的核心:

  1. 初始化返回的vector,偏移量,下一个节点
  2. 循环当前节点的子命令节点
    1. 如果输入的字符串长度大于等于子命令的长度
      1. 字符串开头是子命令的名称
        1. 字符串长度与子命令长度相等,vector加一个空格
        2. 不相等,将子命令添加到vector中
      2. 记录子命令长度为偏移量,将子命令标记为下一个递归的起始节点。
    2. 如果子命令的开头与字符串匹配
      1. vector添加字符串,记录偏移量,标记子命令为下一个递归起始节点
  3. 如果vector不止一个数据,说明有多个匹配的命令,直接返回
  4. 如果满足执行子命令的递归情况,从字符串的偏移量位开始继续执行tab completion.
    pub fn _complete_cmd(node: &PrefixNode, line: &str, pos: usize) -> Vec<String> {
        debug!("cli to complete {} for node {}", line, node.name);
        let line = line[..pos].trim_start();
        let mut go_next = false;

        let mut new_line: Vec<String> = vec![];
        let mut offset: usize = 0;
        let mut next_node = None;

        //var lineCompleter PrefixCompleterInterface
        for child in &node.children {
            //debug!("try node {}", child.name);
            if line.len() >= child.name.len() {
                if line.starts_with(&child.name) {
                    if line.len() == child.name.len() {
                        // add a fack new_line " "
                        new_line.push(" ".to_string());
                    } else {
                        new_line.push(child.name.to_string());
                    }
                    offset = child.name.len();
                    next_node = Some(child);

                    // may go next level
                    go_next = true;
                }
            } else if child.name.starts_with(line) {
                new_line.push(child.name[line.len()..].to_string());
                offset = line.len();
                next_node = Some(child);
            }
        }

        // more than 1 candidates?
        if new_line.len() != 1 {
            debug!("offset={}, candidates={:?}", offset, new_line);
            return new_line;
        }

        if go_next {
            let line = line[offset..].trim_start();
            return PrefixCompleter::_complete_cmd(next_node.unwrap(), line, line.len());
        }

        debug!("offset={}, nl={:?}", offset, new_line);
        new_line
    }

使用方法

以help命令为例,实现一个显示可用命令的功能:

    app.add_subcommand(
        Command::new_with_alias("help", "h")
            .about("displays help information")
            .usage("help [command]")
            .action(cli_help)),
    );
    
    /// Action of help command
    fn cli_help(app: &App, args: &[&str]) -> XcliResult {
        if args.is_empty() {
            app.tree.show_subcommand_help();
        } else if let Some(cmd) = app.tree.locate_subcommand(args) {
            cmd.show_command_help();
        } else {
            println!("Unrecognized command {:?}", args)
        }
        Ok(CmdExeCode::Ok)
    }

调用add_subcommand()向cli实例中添加一个help命令,action方法参数是一个返回值为XcliResult的fn。XcliResult是一个T为CmdExeCode,E为XcliError的Result类型:

pub type XcliResult = stdResult<CmdExeCode, XcliError>;

在这里我们定义了cli_help函数,正常运行时返回值为Ok。实现的命令效果如图所示:


image

userdata

add_subcommand_with_userdata()是在v0.5.0新增支持的一个方法。有时候使用者可能希望测试一些自定义的数据结构,这个方法可以支持用户注册自己的数据到xcli中,后续可以通过命令行的方式进行调试。方法声明如下:

pub fn add_subcommand_with_userdata(&mut self, subcmd: Command<'a>, value: IAny) {
    self.handlers.insert(subcmd.name.clone(), value);
    self.tree.subcommands.push(subcmd);
}

这段代码的逻辑是将value添加到全局的handler中,handler是一个HashMap,key为命令名称,value是传入的IAny类型值。

方法的第一个参数是Command,定义了命令的名称、子命令、对应的执行函数等等属性;第二个参数是相关的用户数据,IAny是Box<dyn std::any::Any>,意味着可以放入绝大多数的自定义类型参数。

使用的逻辑也较为简单,以下是示例代码:

    app.add_subcommand_with_userdata(
        Command::new_with_alias("userdata", "ud")
            .about("controls testing features")
            .action(|app, _args| -> XcliResult {
                let data_any = app.get_handler("userdata").unwrap();

                let data = data_any.downcast_ref::<usize>().expect("usize");

                println!("userdata = {}", data);
                Ok(CmdExeCode::Ok)
            }),
        Box::new(100usize)
    );

在这里,我们注册了一个叫userdata的子命令,其中value设置为了100。执行userdata命令时,从handler中取出userdata对应的值,downcast_ref解析出usize,再进行println。实现效果如图所示:


image

libp2p-rs中的使用

由于userdata命令的存在,我们可以使用自己的数据去定义子命令。例如在libp2p-rs中,提供有swarm和kad的controller与主循环交互,因此我们可以用这两个controller去定义命令:

app.add_subcommand_with_userdata(swarm_cli_commands(), Box::new(swarm_control.clone()));
app.add_subcommand_with_userdata(dht_cli_commands(), Box::new(kad_control.clone()));

实现效果图:

  1. swarm peer,无参即展示当前连接peer


    image
  2. swarm peer,有参显示对应peer信息


    image
  3. dht stats显示当前状态


    image

Netwarps 由国内资深的云计算和分布式技术开发团队组成,该团队在金融、电力、通信及互联网行业有非常丰富的落地经验。Netwarps 目前在深圳、北京均设立了研发中心,团队规模30+,其中大部分为具备十年以上开发经验的技术人员,分别来自互联网、金融、云计算、区块链以及科研机构等专业领域。Netwarps 专注于安全存储技术产品的研发与应用,主要产品有去中心化文件系统(DFS)、去中心化计算平台(DCP),致力于提供基于去中心化网络技术实现的分布式存储和分布式计算平台,具有高可用、低功耗和低网络的技术特点,适用于物联网、工业互联网等场景。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,088评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,715评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,361评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,099评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 60,987评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,063评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,486评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,175评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,440评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,518评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,305评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,190评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,550评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,880评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,152评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,451评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,637评论 2 335

推荐阅读更多精彩内容