پذیرش آرگومانهای خط فرمان
بیایید با استفاده از cargo new یک پروژه جدید ایجاد کنیم. پروژه خود را minigrep مینامیم تا آن را از ابزار grep که ممکن است در سیستم شما وجود داشته باشد متمایز کنیم.
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
اولین کار این است که minigrep آرگومانهای خط فرمان خود، شامل مسیر فایل و رشتهای برای جستجو، را بپذیرد. به عبارت دیگر، میخواهیم بتوانیم برنامه خود را با cargo run، دو خط تیره برای نشان دادن اینکه آرگومانهای بعدی برای برنامه ما هستند و نه برای cargo، یک رشته برای جستجو و یک مسیر فایل برای جستجو اجرا کنیم، مانند زیر:
$ cargo run -- searchstring example-filename.txt
در حال حاضر، برنامهای که توسط cargo new تولید شده است نمیتواند آرگومانهایی که به آن میدهیم را پردازش کند. برخی کتابخانههای موجود در crates.io میتوانند برای نوشتن برنامهای که آرگومانهای خط فرمان را بپذیرد کمک کنند، اما چون شما تازه با این مفهوم آشنا میشوید، بیایید این قابلیت را خودمان پیادهسازی کنیم.
خواندن مقادیر آرگومانها
برای اینکه minigrep بتواند مقادیر آرگومانهای خط فرمان را که به آن میدهیم بخواند، به تابع std::env::args که در کتابخانه استاندارد Rust ارائه شده است نیاز خواهیم داشت. این تابع یک iterator از آرگومانهای خط فرمانی که به minigrep داده شده است بازمیگرداند. ما در فصل ۱۳ به طور کامل iteratorها را پوشش خواهیم داد. در حال حاضر، فقط باید دو نکته درباره iteratorها بدانید: iteratorها یک سری مقادیر تولید میکنند و ما میتوانیم تابع collect را روی یک iterator فراخوانی کنیم تا آن را به یک collection، مانند یک بردار، که شامل تمام عناصر تولیدشده توسط iterator است، تبدیل کنیم.
کد موجود در لیست ۱۲-۱ به برنامه minigrep شما اجازه میدهد تا هر آرگومان خط فرمانی که به آن داده شده را بخواند و سپس مقادیر را به یک بردار جمعآوری کند.
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
ابتدا ماژول std::env را با یک دستور use به دامنه وارد میکنیم تا بتوانیم از تابع args آن استفاده کنیم. توجه کنید که تابع std::env::args در دو سطح ماژول تو در تو قرار دارد. همانطور که در فصل ۷ بحث کردیم، در مواردی که تابع موردنظر در بیش از یک ماژول تو در تو قرار دارد، ترجیح میدهیم ماژول والد را به دامنه وارد کنیم نه تابع. با این کار، میتوانیم به راحتی از توابع دیگر std::env استفاده کنیم. همچنین این روش کمتر مبهم است نسبت به اضافه کردن use std::env::args و سپس فراخوانی تابع با فقط args، چون args ممکن است به راحتی با یک تابع تعریفشده در ماژول جاری اشتباه گرفته شود.
تابع args و یونیکد نامعتبر
توجه داشته باشید که std::env::args اگر هر آرگومانی شامل یونیکد نامعتبر باشد، پانیک خواهد کرد. اگر برنامه شما نیاز به پذیرش آرگومانهایی با یونیکد نامعتبر دارد، به جای آن از std::env::args_os استفاده کنید. این تابع یک iterator بازمیگرداند که مقادیر OsString به جای String تولید میکند. ما برای سادگی std::env::args را اینجا انتخاب کردهایم زیرا مقادیر OsString بسته به پلتفرم متفاوت هستند و کار با آنها پیچیدهتر از مقادیر String است.
در اولین خط از تابع main، ما تابع env::args را فراخوانی میکنیم و بلافاصله از تابع collect استفاده میکنیم تا iterator را به یک بردار که شامل تمام مقادیر تولیدشده توسط iterator است، تبدیل کنیم. میتوانیم از تابع collect برای ایجاد انواع مختلفی از collectionها استفاده کنیم، بنابراین نوع args را به طور صریح با ذکر میکنیم که میخواهیم یک بردار از رشتهها داشته باشیم. با اینکه به ندرت نیاز به ذکر نوعها در Rust دارید، تابع collect یکی از تابعهایی است که اغلب باید نوع آن را ذکر کنید، زیرا Rust نمیتواند نوع collection مورد نظر شما را استنباط کند.
در نهایت، بردار را با استفاده از ماکروی debug چاپ میکنیم. بیایید ابتدا کد را بدون آرگومان اجرا کنیم و سپس با دو آرگومان:
$ cargo run
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.61s
Running `target/debug/minigrep`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
]
$ cargo run -- needle haystack
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 1.57s
Running `target/debug/minigrep needle haystack`
[src/main.rs:5:5] args = [
"target/debug/minigrep",
"needle",
"haystack",
]
توجه کنید که اولین مقدار در بردار "target/debug/minigrep" است، که نام باینری ما است. این رفتار با لیست آرگومانها در زبان C مطابقت دارد و به برنامهها اجازه میدهد از نامی که با آن اجرا شدهاند، در اجرای خود استفاده کنند. دسترسی به نام برنامه اغلب مفید است، مثلاً برای چاپ آن در پیامها یا تغییر رفتار برنامه بر اساس نام مستعار خط فرمانی که برای اجرای برنامه استفاده شده است. اما برای اهداف این فصل، آن را نادیده میگیریم و فقط دو آرگومان مورد نیاز را ذخیره میکنیم.
ذخیره مقادیر آرگومانها در متغیرها
در حال حاضر، برنامه قادر به دسترسی به مقادیر مشخصشده به عنوان آرگومانهای خط فرمان است. اکنون نیاز داریم مقادیر دو آرگومان را در متغیرهایی ذخیره کنیم تا بتوانیم از آنها در بقیه برنامه استفاده کنیم. این کار را در لیست ۱۲-۲ انجام میدهیم.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
let query = &args[1];
let file_path = &args[2];
println!("Searching for {query}");
println!("In file {file_path}");
}
همانطور که هنگام چاپ بردار مشاهده کردیم، نام برنامه اولین مقدار در بردار را در args[0] اشغال میکند، بنابراین آرگومانها را از اندیس (index)۱ شروع میکنیم. اولین آرگومان که minigrep دریافت میکند، رشتهای است که میخواهیم جستجو کنیم، بنابراین یک مرجع به اولین آرگومان را در متغیر query قرار میدهیم. آرگومان دوم مسیر فایل خواهد بود، بنابراین یک مرجع به آرگومان دوم را در متغیر file_path قرار میدهیم.
ما به طور موقت مقادیر این متغیرها را چاپ میکنیم تا اثبات کنیم که کد همانطور که میخواهیم کار میکند. بیایید دوباره این برنامه را با آرگومانهای test و sample.txt اجرا کنیم:
$ cargo run -- test sample.txt
Compiling minigrep v0.1.0 (file:///projects/minigrep)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
Running `target/debug/minigrep test sample.txt`
Searching for test
In file sample.txt
عالی است، برنامه کار میکند! مقادیر آرگومانهای مورد نیاز ما در متغیرهای درست ذخیره میشوند. بعداً برخی از خطاها را مدیریت خواهیم کرد، مثل وقتی که کاربر هیچ آرگومانی ارائه نمیدهد؛ فعلاً، آن شرایط را نادیده میگیریم و روی افزودن قابلیت خواندن فایل تمرکز میکنیم.