关于程序员:xcli一个简单易用的命令行工具

4次阅读

共计 4349 个字符,预计需要花费 11 分钟才能阅读完成。

我的项目地址:https://github.com/kingwel-xi…

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。实现的命令成果如图所示:

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。实现成果如图所示:

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

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

3.dht stats 显示以后状态


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

正文完
 0