יום 4 - מימוש tail ב Rust

30/12/2022

אם יש משהו שלמדתי מהתרגיל של אתמול זה החשיבות (והקושי) בפיענוח ארגומנטים שהגיעו משורת הפקודה. היום נדבר על הספריה clap שהופכת את הכתיבה של יישומי שורת פקודה למשימה הרבה יותר נעימה.

1. מה אנחנו בונים

אחרי שכתבנו את wc נוכל להמשיך לכלי יוניקס מפורסם נוסף - זהו tail. הפקודה tail קצת יותר מאתגרת מ wc, כי בשבילה נצטרך ללמוד איך לשמור מספר שורות יחד בוקטור, ובנוסף נרצה להתקין ספריה חיצונית בשם clap שתעזור לנו בפיענוח ארגומנטים שקיבלנו משורת הפקודה.

כמו תמיד הקוד לפוסט נמצא בגיטהאב בקישור https://github.com/ynonp/rust-8-days/tree/main/day4%20-%20tail/tail. בואו נראה את עיקרי הדברים ואז נדבר עליהם.

2. קוד התוכנית

אני מתחיל את סקירת הקוד מהקובץ Cargo.toml, קובץ שהופיע בכל הפרויקטים שכתבנו עד עכשיו אבל לא זכה לתשומת לב. קובץ זה מגדיר את מבנה הפרויקט ובין השאר מגדיר את התלויות - ספריות חיצוניות ממאגר חבילות רשמי של rust שהפרויקט שלנו צריך. הכלי Cargo באופן אוטומטי קורא את הקובץ ומתקין את התלויות כשאנחנו מפעילים cargo run. הקובץ מכיל את התוכן הבא:

[package]
name = "tail"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
clap = { version = "4.0.19", features = ["derive"] }

הפורמט נקרא toml והוא מהווה אלטרנטיבה נוחה ל yml עבור קבצי קונפיגורציה. הקובץ מחולק לבלוקים כאשר כל בלוק מקבל כותרת בתוך סוגריים מרובעים, במקרה שלנו הבלוקים הם package ו dependencies. הבלוק package מספר על הפרויקט, והבלוק dependencies מכיל רשימה של הספריות החיצוניות בהן הפרויקט משתמש. חבילה חיצונית בראסט נקראת Crate וכולן מאוחסנות במאגר https://crates.io/. את הדף של clap נוכל למצוא בקישור https://crates.io/crates/clap.

בחזרה ל main.rs שמתחיל עם הבלוק:

#[derive(Parser)]
#[command(author, version, about, long_about = None)]
struct Cli {
    name: Option<String>,

    #[arg(short)]
    reverse: bool,

    #[arg(short='n')]
    lines: Option<String>,
}

אנחנו כבר יודעים להגדיר struct, אבל מה זה התחביר שמעל ה Struct? ב Rust אפשר להוסיף למבנים מסוימים Metadata בעזרת סימן הסולמית. זה נקרא בראסטית Attributes ואפשר לקרוא עליהם בקישור https://doc.rust-lang.org/rust-by-example/attribute.html. המאפיין derive למשל מגדיר מימוש בסיסי ל Trait אותו הקומפיילר ישתול בצורה אוטומטית. המאפיין command ייקרא על ידי הספריה clap ויגיד לקלאפ שזה הסטראקט שמגדיר את הארגומנטים שאנחנו מצפים לקבל משורת הפקודה. מה שנמצא בסוגריים זה פרמטרים שקלאפ ישתמש בהם, במקרה שלנו כדי לדעת איזה מידע להציג כשמישהו מפעיל את התוכנית.

מה שיותר מעניין זה מה שקורה בתוך אותו הסטראקט - כל אחד מהשדות בו יהפוך לארגומנט שהתוכנית יכולה לקבל משורת הפקודה, ובעזרת המאפיין arg אפשר להגדיר עוד פרטים לגבי אותו ארגומנט, למשל להגדיר אותו בתור short (שזה אומר שהוא יופעל עם אות אחת) ולציין מה האות.

שימו לב מה קורה כשאני מפעיל את התוכנית עם המתג --help:

$ cargo run -- --help
    Finished dev [unoptimized + debuginfo] target(s) in 0.04s
     Running `target/debug/tail --help`
Usage: tail [OPTIONS] [NAME]

Arguments:
  [NAME]

Options:
  -r
  -n <LINES>
  -h, --help      Print help information
  -V, --version   Print version information

מסך העזרה מיוצר על ידי ספריית clap, ומציג את כל הארגומנטים והאופציות שהתוכנית מצפה לקבל. הרשימה הזאת נבנית ישירות מהסטראקט CLI. בואו נמשיך ונראה איך main מפענח את הארגומנטים אחרי שהגדרנו את הסטראקט:

fn main() {
    let cli = Cli::parse();

    // You can check the value provided by positional arguments, or option arguments
    let lines: usize = cli.lines.unwrap_or(String::from("10")).parse().expect("n is not a number");

    println!("Value for reverse: {}", cli.reverse);
    println!("Value for n = {}", lines);
    println!("name: {:?}", cli.name.as_deref());

    tail_file(cli.name.as_deref(), lines, cli.reverse);
}

מעט שורות עם הרבה תוכן:

את הפקודה unwrap אנחנו כבר מכירים - היא מחזירה את התוכן של האופציה Ok מ Result, או מרסקת את התוכנית עם שגיאה אם אותו Result מייצג שגיאה. הפקודה unwrap_or היא גירסה פחות אלימה של אותה unwrap. אם היא מופעלת על Result עם שגיאה, היא מקבלת ערך (עבור ה Or), ואם יש ב Result שגיאה היא תחזיר את אותו ערך. הפקודה expect של Option מחזירה את הערך אם קיים, או מרסקת את התוכנית אם יש שגיאה. סך הכל במשתנה lines נקבל את הערך של השדה lines מ cli, אבל אם לא הוגדר אחד נקבל 10. זיכרו שהשדה lines היה מוגדר בסטראקט כך:

#[arg(short='n')]
lines: Option<String>,

מאפיין אופציונאלי מסוג String והאות שמתאימה לאופציה היא n.

המאפיינים reverse ו name לא צריכים טיפול מיוחד, ואחרי הפיענוח אפשר להעביר את כולם לפונקציה tail_file. זה המימוש שלה:

fn tail_file(filename: Option<&str>, lines_count: usize, reverse: bool) {    
    let mut last_lines: Vec<String> = Vec::with_capacity(lines_count);

    let reader: Box<dyn BufRead> = match filename {
        None => Box::new(BufReader::new(stdin())),
        Some(filename) => Box::new(BufReader::new(fs::File::open(filename).unwrap()))
    };

    let mut idx = 0;
    for line in reader.lines() {        
        if idx < last_lines.len() {
            last_lines[idx] = line.unwrap();
        } else {
            last_lines.push(line.unwrap());
        }
        idx = (idx + 1) % lines_count;
    }

    for i in 0..lines_count {
        if reverse {
            let j = lines_count - (i + 1);
            println!("[{}] {}", j, last_lines[(idx + j) % lines_count]);
        } else {
            println!("[{}] {}", i, last_lines[(idx + i) % lines_count]);
        }

    }
}

קצת ארוכה אבל אין הרבה חדש. הקוד מגדיר משתנה מסוג Vec שזה וקטור של מחרוזות. וקטור זה ישמור את השורות האחרונות בקובץ, אותן נרצה להדפיס כשהקלט ייגמר. אני יוצר BufReader מהקובץ בדיוק כמו שעשיתי אתמול עם wc, ואז רץ על הקובץ ושומר את השורות לוקטור. בעזרת אופרטור השארית אני יכול למלא את הוקטור וכשהוא ייגמר אוטומטית השורה הבאה תיכתב להתחלה, וכך בסוף הקלט הוקטור יכיל את ה n שורות האחרונות. החלק האחרון של הפונקציה רץ בלולאה על הוקטור ומדפיס את השורות, לפעמים בסדר הנכון ולפעמים בסדר הפוך, לפני ערך המשתנה reverse.

3. תרגילים להרחבה

אפשר לקרוא די הרבה על clap בדף התיעוד שלה בקישור: https://docs.rs/clap/latest/clap/. התחילו שם ונסו לעדכן את הקוד לפי הסעיפים הבאים:

  1. הוסיפו הסבר קצר על התוכנית שיופיע כשמפעילים את מסך העזרה.

  2. הוסיפו אפשרות לקבל את מספר השורות גם באמצעות האופציה הארוכה --lines, כלומר שאפשר יהיה להפעיל את התוכנית עם tail --lines 15.

  3. הוסיפו תמיכה ב tail על מספר קבצים. אם התוכנית קיבלה מספר שמות של קבצים בשורת הפקודה יש להפעיל tail על כל אחד מהם.

  4. פקודת tail האמיתית תומכת באפשרות שנקראת Follow, בה מציגים את השורות האחרונות ואז מחכים שייכתב מידע נוסף לקובץ ואז ממשיכים להציג גם אותו. קראו על הספריה notify של ראסט והוסיפו תמיכה ב Follow לפקודת ה tail שלנו.

עד לכאן טיפול בקלט משורת הפקודה ב Rust. מחר ניקח הפסקה מתוכניות שורת פקודה ונלך לראות איך Rust מתמודד עם ביצוע משימות במקביל ועבודה ברשת. כמו תמיד אם פתרתם חלק מתרגילי ההרחבה מוזמנים להדביק את הקוד כאן בתגובות.