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。實現的命令效果如圖所示:
xcli,一個簡單易用的命令行工具

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。實現效果如圖所示:
xcli,一個簡單易用的命令行工具

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
    xcli,一個簡單易用的命令行工具

  2. swarm peer,有參顯示對應peer信息
    xcli,一個簡單易用的命令行工具

  3. dht stats顯示當前狀態
    xcli,一個簡單易用的命令行工具

Netwarps 由國內資深的雲計算和分佈式技術開發團隊組成,該團隊在金融、電力、通信及互聯網行業有非常豐富的落地經驗。Netwarps 目前在深圳、北京均設立了研發中心,團隊規模30+,其中大部分爲具備十年以上開發經驗的技術人員,分別來自互聯網、金融、雲計算、區塊鏈以及科研機構等專業領域。
Netwarps 專注於安全存儲技術產品的研發與應用,主要產品有去中心化文件系統(DFS)、去中心化計算平臺(DCP),致力於提供基於去中心化網絡技術實現的分佈式存儲和分佈式計算平臺,具有高可用、低功耗和低網絡的技術特點,適用於物聯網、工業互聯網等場景。
公衆號:Netwarps

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章