項(xiàng)目地址:https://github.com/kingwel-xie/xcli-rs
xcli是一個(gè)命令行的工具章贞,支持自定義添加命令备埃,每個(gè)命令支持縮寫使用尼桶,同時(shí)也支持tab方式補(bǔ)全命令。
設(shè)計(jì)思路
這個(gè)工具的設(shè)計(jì)初衷是為了能夠提供命令行功能搀庶,同時(shí)可以很容易的添加自定義的命令拐纱。
應(yīng)用場景
目前在libp2p-rs中,xcli提供了命令行的功能哥倔,可對swarm和kad進(jìn)行調(diào)試秸架。
類型轉(zhuǎn)換
從命令行中獲取到的參數(shù)args是一個(gè)引用類型的&str數(shù)組,即&[&str]咆蒿。在xcli中东抹,實(shí)現(xiàn)了一個(gè)名為check_param的宏蚂子,返回的值即為想要轉(zhuǎn)換的對應(yīng)類型。check_param!需要四個(gè)參數(shù)
($param_count:expr, $required:expr, $args:ident, ($($change_type:ty=>$has_from:expr), *))
分別代表參數(shù)總個(gè)數(shù)缭黔、必選參數(shù)個(gè)數(shù)食茎,參數(shù)列表,最后一個(gè)參數(shù)比較特殊馏谨,代表著需要轉(zhuǎn)換的類型别渔。書寫格式形如(String=>1),對于所有輸入?yún)?shù)都需要設(shè)置該轉(zhuǎn)換類型惧互。
需要注意的一點(diǎn)是哎媚,參數(shù)的總個(gè)數(shù)必須與最后的參數(shù)轉(zhuǎn)換類型個(gè)數(shù)相同。譬如總共有5個(gè)參數(shù)喊儡,那么后面的類型轉(zhuǎn)換也需要將這五個(gè)參數(shù)的類型都進(jìn)行設(shè)置拨与。
舉例說明:
let u = check_param!(3, 1, args, (String=>1, String=>1, String=>1))
這段代碼表示總共需要三個(gè)參數(shù),其中一個(gè)是必須的艾猜,另外兩個(gè)是可選的买喧,這三個(gè)參數(shù)都是String類型的。返回值的個(gè)數(shù)最少是1個(gè)(必須參數(shù)一定返回)箩朴,最多是3個(gè)岗喉。
命令補(bǔ)全
由于底層庫使用的是rustyline秋度,它提供了一個(gè)Completer的trait炸庞,實(shí)現(xiàn)fn complete()即可支持tab補(bǔ)全。
在App::run()中荚斯,我們對Command執(zhí)行了一個(gè)方法:
self.rl.borrow_mut().set_helper(Some(PrefixCompleter::new(&self.tree)));
這段代碼的邏輯是將Command單獨(dú)抽離出來形成一個(gè)類似樹的結(jié)構(gòu)埠居。
以下這段代碼是補(bǔ)全功能的核心:
- 初始化返回的vector,偏移量事期,下一個(gè)節(jié)點(diǎn)
- 循環(huán)當(dāng)前節(jié)點(diǎn)的子命令節(jié)點(diǎn)
- 如果輸入的字符串長度大于等于子命令的長度
- 字符串開頭是子命令的名稱
- 字符串長度與子命令長度相等滥壕,vector加一個(gè)空格
- 不相等,將子命令添加到vector中
- 記錄子命令長度為偏移量兽泣,將子命令標(biāo)記為下一個(gè)遞歸的起始節(jié)點(diǎn)绎橘。
- 字符串開頭是子命令的名稱
- 如果子命令的開頭與字符串匹配
- vector添加字符串,記錄偏移量唠倦,標(biāo)記子命令為下一個(gè)遞歸起始節(jié)點(diǎn)
- 如果輸入的字符串長度大于等于子命令的長度
- 如果vector不止一個(gè)數(shù)據(jù)称鳞,說明有多個(gè)匹配的命令,直接返回
- 如果滿足執(zhí)行子命令的遞歸情況稠鼻,從字符串的偏移量位開始繼續(xù)執(zhí)行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命令為例冈止,實(shí)現(xiàn)一個(gè)顯示可用命令的功能:
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)
}
調(diào)用add_subcommand()向cli實(shí)例中添加一個(gè)help命令,action方法參數(shù)是一個(gè)返回值為XcliResult的fn候齿。XcliResult是一個(gè)T為CmdExeCode熙暴,E為XcliError的Result類型:
pub type XcliResult = stdResult<CmdExeCode, XcliError>;
在這里我們定義了cli_help函數(shù)闺属,正常運(yùn)行時(shí)返回值為Ok。實(shí)現(xiàn)的命令效果如圖所示:
userdata
add_subcommand_with_userdata()是在v0.5.0新增支持的一個(gè)方法周霉。有時(shí)候使用者可能希望測試一些自定義的數(shù)據(jù)結(jié)構(gòu)掂器,這個(gè)方法可以支持用戶注冊自己的數(shù)據(jù)到xcli中,后續(xù)可以通過命令行的方式進(jìn)行調(diào)試俱箱。方法聲明如下:
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是一個(gè)HashMap,key為命令名稱匠楚,value是傳入的IAny類型值巍膘。
方法的第一個(gè)參數(shù)是Command,定義了命令的名稱芋簿、子命令峡懈、對應(yīng)的執(zhí)行函數(shù)等等屬性;第二個(gè)參數(shù)是相關(guān)的用戶數(shù)據(jù)与斤,IAny是Box<dyn std::any::Any>肪康,意味著可以放入絕大多數(shù)的自定義類型參數(shù)。
使用的邏輯也較為簡單撩穿,以下是示例代碼:
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)
);
在這里磷支,我們注冊了一個(gè)叫userdata的子命令,其中value設(shè)置為了100食寡。執(zhí)行userdata命令時(shí)雾狈,從handler中取出userdata對應(yīng)的值,downcast_ref解析出usize抵皱,再進(jìn)行println善榛。實(shí)現(xiàn)效果如圖所示:
libp2p-rs中的使用
由于userdata命令的存在,我們可以使用自己的數(shù)據(jù)去定義子命令呻畸。例如在libp2p-rs中移盆,提供有swarm和kad的controller與主循環(huán)交互,因此我們可以用這兩個(gè)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()));
實(shí)現(xiàn)效果圖:
-
swarm peer伤为,無參即展示當(dāng)前連接peer
-
swarm peer咒循,有參顯示對應(yīng)peer信息
-
dht stats顯示當(dāng)前狀態(tài)
Netwarps 由國內(nèi)資深的云計(jì)算和分布式技術(shù)開發(fā)團(tuán)隊(duì)組成,該團(tuán)隊(duì)在金融绞愚、電力叙甸、通信及互聯(lián)網(wǎng)行業(yè)有非常豐富的落地經(jīng)驗(yàn)。Netwarps 目前在深圳爽醋、北京均設(shè)立了研發(fā)中心蚁署,團(tuán)隊(duì)規(guī)模30+,其中大部分為具備十年以上開發(fā)經(jīng)驗(yàn)的技術(shù)人員蚂四,分別來自互聯(lián)網(wǎng)光戈、金融哪痰、云計(jì)算、區(qū)塊鏈以及科研機(jī)構(gòu)等專業(yè)領(lǐng)域久妆。Netwarps 專注于安全存儲(chǔ)技術(shù)產(chǎn)品的研發(fā)與應(yīng)用晌杰,主要產(chǎn)品有去中心化文件系統(tǒng)(DFS)、去中心化計(jì)算平臺(tái)(DCP)筷弦,致力于提供基于去中心化網(wǎng)絡(luò)技術(shù)實(shí)現(xiàn)的分布式存儲(chǔ)和分布式計(jì)算平臺(tái)肋演,具有高可用、低功耗和低網(wǎng)絡(luò)的技術(shù)特點(diǎn)烂琴,適用于物聯(lián)網(wǎng)爹殊、工業(yè)互聯(lián)網(wǎng)等場景。