rust學習十二、一個I/O程式練習

正在战斗中發表於2024-12-03

本文內容,基本同原書本,只是稍作了一些改動:

  1. 可以輸入 --help展示用法
  2. 如果檔案超過1MB,則會採用逐行查詢的方法,避免記憶體不足

整個程式包含個檔案:main.rs,lib.rs

一、程式碼

閒話少敘,直接上程式碼。

main.rs

/**
 * 完全模仿書本的例子
 * 
 * 執行示例 
 * cargo run 宋 E:\learning\gitee\rust-org\test-1\minigrep\src\古詩.txt  -- ok
 * cargo run 宋 古詩.txt  -- ok
 * cargo run 宋   -- 錯誤,因為引數太少了
 */
use std::env;
use minigrep::{config,search};

fn main() {
    //1.0 args() 返回的字元是unicode編碼的 ,args()本身返回迭代器,collect()方法將迭代器中的元素收集到Vec<String>中                                                                           
    let args: Vec<String> = env::args().collect();

    //2.0 解析引數,並構建配置
    // unwrap_or_else()方法,如果Config::build(&args)返回Ok(config),則執行閉包中的內容,否則執行|err|{...}中的內容
    let con=config::build(&args).unwrap_or_else(|err|{
        eprintln!("程式異常:{}",err);
        std::process::exit(1);
    });    

    if con.show_help {
        config::show_help();
        std::process::exit(0);
    }
    
    println!("配置資訊:{:?}",&con);

    //3.0 根據配置查詢檔案,並列印查詢到的結果
    //如果檔案比較小,那麼就直接在記憶體處理;否則直接在search中逐行處理
    if config::is_too_big(&con.file_name){
print!("正在查詢大檔案...");
       let result= search::searh_big_file(&con.file_name,&con.target);
       match result{
        Ok(_)=>println!("查詢完成!"),
        Err(e)=>println!("讀取檔案失敗: {:?}", e)
       }
    }
    else{
print!("正在查詢小檔案...");
        let result=search::search_small_file(&con);
        for line in result.iter() {
            println!("{}",line);
        }
    }
}

lib.rs

/**
 * 建立兩個子模組
 */
#[derive(Debug)]
pub struct Config {
    pub file_name: String,
    pub target: String,
    pub show_help: bool,
}
/**
 * 模組執行配置
 */
pub mod config {
    use super::Config;
    use std::env;
    use std::fs;
    use std::path::Path;
    /**
     * 第二個引數: 被查詢的字串
     * 第三個引數: 檔案路徑,可以是全路徑或者不包含路徑的檔名
     */
    pub fn build(args: &[String]) -> Result<Config, &str> {
        if args.len() < 3 {
            if args.len() == 2 {
                //如果target_name=="--help"則列印幫助資訊,否則執行檔案
                let target_name = args[1].clone();
                if target_name == "--help" {
                    return Ok(Config {
                        target: target_name,
                        file_name: "".to_string(),
                        show_help: true,
                    });
                }
            }
            return Err("必須有兩個引數");
        }
        println!("rust返回的程式路徑:{}", args[0]);
        let target_name = args[1].clone();
        let file_path = args[2].clone();

        if !file_exists(&file_path) {
            //使用者輸入的是檔名
            let exec_path = get_current_exec_path();
            println!("當前執行路徑:{}", exec_path);
            let mut tmp_path = exec_path.clone() + &file_path;
            if !file_exists(&tmp_path) {
                tmp_path = exec_path.clone().replace("\\target\\debug\\", "\\src\\") + &file_path;
                if !file_exists(&tmp_path) {
                    tmp_path =
                        exec_path.clone().replace("\\target\\release\\", "\\src\\") + &file_path;
                    if !file_exists(&tmp_path) {
                        return Err("檔案不存在");
                    }
                }
                //在src下
                Ok(Config {
                    target: target_name,
                    file_name: tmp_path,
                    show_help: false,
                })
            } else {
                //在當前目錄下=執行路徑+檔名
                Ok(Config {
                    target: target_name,
                    file_name: tmp_path,
                    show_help: false,
                })
            }
        } else {
            //在當前目錄下 -- 即使用者輸入的是全路徑
            println!("{}", file_path);
            Ok(Config {
                target: target_name,
                file_name: file_path,
                show_help: false,
            })
        }
    }

    /**
     * 判斷檔案是否存在,如果存在則返回true,否則返回false
     */
    fn file_exists(file_name: &String) -> bool {
        Path::new(&file_name).exists()
    }

    /**
     * 獲取當前執行路徑,最後會包含分隔符號\
     * 例如 d:\soft\rust-test\
     * 注意:僅限用於windows平臺
     */
    fn get_current_exec_path() -> String {
        let cur_dir = match env::current_exe() {
            Ok(exe_path) => exe_path.display().to_string(),
            Err(e) => panic!("獲取當前路徑失敗:{}", e),
        };
        // 獲取cur_dir的目錄,不需要包含檔名稱
        let path = Path::new(&cur_dir);
        let result = path.parent().unwrap().to_str().unwrap().to_string() + "\\";
        result
    }

    pub fn is_too_big(file_name: &String) -> bool {
        //判斷file_name是否超過1Mb,如果是,則返回true,否則返回false
        let file_size = fs::metadata(file_name).unwrap().len();
        if file_size > 1 * 1024 * 1024 {
            return true;
        } else {
            false
        }
    }

    pub fn show_help() {
        println!("用法:minigrep target path");
        println!("     minigrep --help");
        println!("引數:");
        println!("  target  需要查詢的目標字串");
        println!("  path    包含target的檔案,可以是全路徑,也可以是檔名");
        println!("          如果是檔名,則會嘗試在當前目錄下,或者是在相對路徑..\\..\\src");
        println!("  --help  檢視幫助");
        println!("示例:");
        println!("  minigrep 宋 E:\\learning\\gitee\\rust-org\\test-1\\minigrep\\src\\古詩.txt ");
        println!("  minigrep 宋 古詩.txt");
        println!("版本 1.0 for windows, 作者->lzfto");
    }
}

/**
 * 模組執行搜尋
 */
pub mod search {
    use super::Config;
    use std::fs;
    use std::fs::File;
    use std::io::{BufRead, BufReader};
    use std::path::Path;
    /**
     * 搜尋小於1M的檔案,並返回查詢的結果
     * @return  返回查詢的結果 Vec<String>
     */
    pub fn search_small_file(con: &Config) -> Vec<String> {
        let contents = fs::read_to_string(con.file_name.clone()).expect("檔案不存在");
        println!("文章內容:\n{}", contents);
        //把contents根據換行符切割成向量
        let mut result: Vec<String> = vec![];
        let lines = contents.lines();
        for line in lines {
            if line.find(&con.target).is_some() {
                result.push(line.to_string());
            }
        }
        return result;
    }
    /**
     * 搜尋大於1M的檔案
     * @return  表示成功與否的資訊 Result<()>
     */
    pub fn searh_big_file(file_name: &String, target: &String) -> std::io::Result<()> {
        //開啟檔案file_name,逐行查詢,找到則直接列印
        let path = Path::new(file_name);
        let file = File::open(&path)?;
        let reader = BufReader::new(file);
        for line in reader.lines() {
            match line {
                Ok(line) => {
                    if line.find(target).is_some() {
                        println!("{}", line);
                    }
                }
                Err(e) => println!("Error reading line: {}", e),
            }
        }
        Ok(())
    }
}

二、測試

為了便於測試,在src目錄下放了兩個檔案:

古詩.txt 1kb

武林外史_utf8.txt 2120kb

顯示幫助

查詢小檔案

查詢大檔案

三、小結

本例用到了截止12章節的大部分知識:

  1. 型別-主要是Vec,String,列舉,struct
  2. 結果處理Result,錯誤處理
  3. 程式組織,包括模組,use等等
  4. i/o-讀取程式引數、讀取檔案,分行讀取

必須得承認,rust的程式碼不好寫,也不好看,幸好有優異的編譯器和優秀的ide幫了大忙!

這些語法對於初學者而言,還是比較彆扭,充斥著怪異的符號和奇特的語法。也許rust的發明者需要花費一些心思再最佳化下編譯器,讓工程師能夠

以更自然優雅的方式寫出不違背初心的程式碼。工程效率也是很重要的!

在編寫的過程中,對引用借用有了更具體的體會!

相關文章