博客 / 詳情

返回

文盤Rust——起手式,CLI程序 | 京東雲技術團隊

技術的學習從不會到會的過程是最有意思的,也是體會最多的。一旦熟練了,知識變成了常識,可能就失去了記錄學習過程的最佳時機。

在我看來學習一門計算機語言和學習人類語言有很多共通之處。我們學習人類語言是從單個的詞開始,然後是簡單句子,通過不斷的與他人交互練習掌握語法和語言習慣。當熟練到一定程度就可以表達思想。計算的語言也差不多,熟悉關鍵詞,基本邏輯,標準庫,寫應用。只是溝通的對象是機器而已。

既然是學就不能在開始搞的太難。學習本來就是個艱苦的差事。上來就幹特別複雜的事情往往會堅持不下去。天下難事必做於易,從簡入繁,從易到難,方為正道。

先聊聊最簡單的CLI(Command Line Interface)程序。其實我們每學習一門語言的 hello world 程序就是CLI,只是沒那麼多交互而已。

做命令行程序最繁瑣的事情是處理交互。交互大體分兩種。一種是我們最熟悉shell下的交互模式,每次一個命令,配合參數實現一次處理返回一組結果。這種模式處理起來比較容易Rust也有相當優秀的第三方lib (clap)。第二種是領域交互,就像我是使用MySql或者redis的客户端程序。這種程序可以玩兒的東西就比較多了像如何實現交互,如何來做子命令的提示。這些東西 clap 並沒有提供,需要我們自己來實現。

interactcli-rs是我在工作過程中做的一個交互模式命令行腳手架。實現了一些常用功能。

下面我們來看看如何通過幾個步驟快速的實現一個功能相對齊全的CLI程序。和做飯一樣,能夠快速獲得成就感的方式是找半成品直接下鍋炒一盤:)。

下面我們具體看看,如何通過interactcli-rs實現一個功能齊全的命令行程序

來點感性認識

先把項目clone下來運行個例子

  • clone 項目

    git clone https://github.com/jiashiwen/interactcli-rs.git
    cd interactcli-rs
    
    
    
    
  • 命令行模式

    cargo run requestsample baidu
    
    
    
    
  • 交互模式

    cargo run -- -i
    interact-rs> requestsample baidu
    
    
    
    

運行上面的命令是通過http來請求百度

四步做個CLI

首先我們先來看看框架的目錄結構


.
├── examples
├── log
├── logs
└── src
    ├── cmd
    ├── commons
    ├── configure
    ├── interact
    ├── logger
    └── request



cmd目錄是我們做自己功能時要動的主要目錄,下面我們一步一步的實現requestsample命令。

  • 定義命令
    cmd 模塊用於定義命令以及相關子命令,requestsample.rs 中定義了訪問百度的命令

    use clap::Command;
    
    pub fn new_requestsample_cmd() -> Command<'static> {
    clap::Command::new("requestsample")
    .about("requestsample")
    .subcommand(get_baidu_cmd())
    }
    
    pub fn get_baidu_cmd() -> Command<'static> {
    clap::Command::new("baidu").about("request www.baidu.com")
    }
    
    
    
    

    new\_requestsample\_cmd 函數定義了命令 "requestsample",get\_baidu\_cmd 函數定義了 requestsample 的子命令 baidu

  • 註冊命令
    src/cmd/rootcmd.rs 文件中定義了命令樹,可以在此註冊定義好的子命令

    lazy_static! {
        static ref CLIAPP: clap::Command<'static> = clap::Command::new("interact-rs")
            .version("1.0")
            .author("Your Name. ")
            .about("command line sample")
            .arg_required_else_help(true)
            .arg(
                Arg::new("config")
                    .short('c')
                    .long("config")
                    .value_name("FILE")
                    .help("Sets a custom config file")
                    .takes_value(true)
            )
            .arg(
                Arg::new("daemon")
                    .short('d')
                    .long("daemon")
                    .help("run as daemon")
            )
            .arg(
                Arg::new("interact")
                    .short('i')
                    .long("interact")
                    .conflicts_with("daemon")
                    .help("run as interact mod")
            )
            .arg(
                Arg::new("v")
                    .short('v')
                    .multiple_occurrences(true)
                    .takes_value(true)
                    .help("Sets the level of verbosity")
            )
            .subcommand(new_requestsample_cmd())
            .subcommand(new_config_cmd())
            .subcommand(new_multi_cmd())
            .subcommand(new_task_cmd())
            .subcommand(new_loop_cmd())
            .subcommand(
                clap::Command::new("test")
                    .about("controls testing features")
                    .version("1.3")
                    .author("Someone E. ")
                    .arg(
                        Arg::new("debug")
                            .short('d')
                            .help("print debug information verbosely")
                    )
            );
        static ref SUBCMDS: Vec = subcommands();
    }
    
    pub fn run_app() {
        let matches = CLIAPP.clone().get_matches();
        if let Some(c) = matches.value_of("config") {
            println!("config path is:{}", c);
            set_config_file_path(c.to_string());
        }
        set_config(&get_config_file_path());
        cmd_match(&matches);
    }
    
    pub fn run_from(args: Vec) {
        match clap_Command::try_get_matches_from(CLIAPP.to_owned(), args.clone()) {
            Ok(matches) => {
                cmd_match(&matches);
            }
            Err(err) => {
                err.print().expect("Error writing Error");
            }
        };
    }
    
    
    
    
    

    定義好的命令不需其他處理,框架會在系統運行時生成子命令樹,用於在領域交互模式下命令提示的支持

  • 命令解析
    src/cmd/rootcmd.rs 中的 cmd_match 負責解析命令,可以把解析邏輯寫在該函數中

    fn cmd_match(matches: &ArgMatches) {   
      if let Some(ref matches) = matches.subcommand_matches("requestsample") {
          if let Some(_) = matches.subcommand_matches("baidu") {
              let rt = tokio::runtime::Runtime::new().unwrap();
              let async_req = async {
                  let result = req::get_baidu().await;
                  println!("{:?}", result);
              };
              rt.block_on(async_req);
          };
      }
    }
    
    
    
    
  • 修改交互模式的命令提示
    提示符可以在src/interact/cli.rs 中定義

    pub fn run() {
    
      ...
    
      loop {
          let p = format!("{}> ", "interact-rs");
          rl.helper_mut().expect("No helper").colored_prompt = format!("\x1b[1;32m{}\x1b[0m", p);
    
          ...
      }
    
      ...
    }
    
    
    
    
    

先寫到這裏,下次為大家介紹一下interactcli-rs各種功能是如何實現的。

作者:京東科技 賈世聞

來源:京東雲開發者社區 轉載請註明來源

user avatar xiumubai 頭像
1 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.