استقبال وسائط سطر الأوامر (Command Line Arguments)
لنقم بإنشاء مشروع جديد كالعادة باستخدام cargo new. سنطلق على مشروعنا اسم minigrep لتمييزه عن أداة grep التي قد تكون موجودة بالفعل على نظامك:
$ cargo new minigrep
Created binary (application) `minigrep` project
$ cd minigrep
المهمة الأولى هي جعل minigrep يقبل وسيطي (Arguments) سطر الأوامر الخاصين به: مسار الملف وسلسلة نصية (String) للبحث عنها. أي أننا نريد أن نكون قادرين على تشغيل برنامجنا باستخدام cargo run متبوعاً بشرطتين للإشارة إلى أن الوسائط (Arguments) التالية مخصصة لبرنامجنا وليس لـ cargo نفسه، ثم السلسلة النصية المراد البحث عنها، ومسار الملف المراد البحث فيه، على النحو التالي:
$ cargo run -- searchstring example-filename.txt
في الوقت الحالي، لا يمكن للبرنامج الذي تم إنشاؤه بواسطة cargo new معالجة Arguments التي نمررها له. يمكن لبعض المكتبات (Libraries) الموجودة على crates.io المساعدة في كتابة برنامج يقبل Arguments سطر الأوامر، ولكن بما أنك تتعلم هذا المفهوم للتو، فلنقم بتنفيذ هذه الإمكانية بأنفسنا.
قراءة قيم الوسائط (Argument Values)
لتمكين minigrep من قراءة قيم Arguments سطر الأوامر التي نمررها إليه، سنحتاج إلى الدالة (Function) std::env::args المتوفرة في مكتبة Rust القياسية (Standard Library). تعيد هذه Function مكرراً (Iterator) لوسائط سطر الأوامر التي تم تمريرها إلى minigrep. سنغطي Iterators بالكامل في الفصل 13. في الوقت الحالي، تحتاج فقط إلى معرفة تفصيلين حول Iterators: تنتج Iterators سلسلة من القيم، ويمكننا استدعاء التابع (Method) collect على Iterator لتحويله إلى مجموعة (Collection)، مثل المتجه (Vector)، الذي يحتوي على جميع العناصر التي ينتجها Iterator.
تسمح الشفرة البرمجية (Code) في القائمة 12-1 لبرنامج minigrep الخاص بك بقراءة أي Arguments سطر أوامر يتم تمريرها إليه ثم جمع القيم في Vector.
use std::env;
fn main() {
let args: Vec<String> = env::args().collect();
dbg!(args);
}
أولاً، نقوم بجلب الوحدة (Module) std::env إلى النطاق (Scope) باستخدام عبارة (Statement) use حتى نتمكن من استخدام Function args الخاصة بها. لاحظ أن Function std::env::args متداخلة في مستويين من Modules. كما ناقشنا في الفصل 7، في الحالات التي تكون فيها Function المطلوبة متداخلة في أكثر من Module واحد، اخترنا جلب Module الأب إلى Scope بدلاً من Function نفسها. ومن خلال القيام بذلك، يمكننا بسهولة استخدام Functions أخرى من std::env. كما أنه أقل غموضاً من إضافة use std::env::args ثم استدعاء Function باستخدام args فقط، لأن args قد يتم الخلط بينها وبين Function معرفة في Module الحالي.
الدالة
argsوالترميز الموحد (Unicode) غير الصالحلاحظ أن
std::env::argsستتسبب في حالة ذعر (Panic) إذا كان أي Argument يحتوي على Unicode غير صالح. إذا كان برنامجك يحتاج إلى قبول Arguments تحتوي على Unicode غير صالح، فاستخدمstd::env::args_osبدلاً من ذلك. تعيد تلك Function مكرراً (Iterator) ينتج قيمOsStringبدلاً من قيمString. لقد اخترنا استخدامstd::env::argsهنا للتبسيط لأن قيمOsStringتختلف باختلاف نظام التشغيل (Platform) وهي أكثر تعقيداً في التعامل معها من قيمString.
في السطر الأول من main استدعينا env::args واستخدمنا collect على الفور لتحويل Iterator إلى Vector يحتوي على جميع القيم التي ينتجها Iterator. يمكننا استخدام Function collect لإنشاء أنواع عديدة من Collections، لذا قمنا بتحديد نوع (Type) args صراحة لنبين أننا نريد Vector من السلاسل النصية (Strings). على الرغم من أنك نادراً ما تحتاج إلى تحديد Types في Rust، إلا أن collect هي إحدى Functions التي غالباً ما تحتاج إلى تحديد نوعها لأن Rust لا يستطيع استنتاج نوع Collection الذي تريده.
أخيراً، نقوم بطباعة Vector باستخدام ماكرو التصحيح (Debug Macro). لنحاول تشغيل Code أولاً بدون Arguments ثم مع وسيطين:
$ 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",
]
لاحظ أن القيمة الأولى في Vector هي "target/debug/minigrep"، وهي اسم ملفنا الثنائي (Binary). هذا يطابق سلوك قائمة Arguments في لغة C، مما يسمح للبرامج باستخدام الاسم الذي تم استدعاؤها به أثناء تنفيذها. غالباً ما يكون من الملائم الوصول إلى اسم البرنامج في حال كنت ترغب في طباعته في الرسائل أو تغيير سلوك البرنامج بناءً على الاسم المستعار (Alias) لسطر الأوامر الذي تم استخدامه لاستدعاء البرنامج. ولكن لأغراض هذا الفصل، سنتجاهله ونحفظ فقط وسيطي (Arguments) اللذين نحتاجهما.
حفظ قيم الوسائط في متغيرات (Variables)
البرنامج قادر حالياً على الوصول إلى القيم المحددة كـ Arguments سطر الأوامر. نحتاج الآن إلى حفظ قيم وسيطي (Arguments) في متغيرات (Variables) حتى نتمكن من استخدام القيم في بقية البرنامج. نقوم بذلك في القائمة 12-2.
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}");
}
كما رأينا عندما قمنا بطباعة Vector، فإن اسم البرنامج يشغل القيمة الأولى في Vector عند args[0]، لذا سنبدأ Arguments عند الفهرس (Index) 1. أول Argument يأخذه minigrep هو السلسلة النصية التي نبحث عنها، لذا نضع مرجعاً (Reference) لـ Argument الأول في المتغير (Variable) query. سيكون Argument الثاني هو مسار الملف، لذا نضع Reference لـ Argument الثاني في Variable file_path.
نقوم بطباعة قيم هذه Variables مؤقتاً لإثبات أن Code يعمل كما نريد. لنقم بتشغيل هذا البرنامج مرة أخرى مع Arguments 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
رائع، البرنامج يعمل! يتم حفظ قيم Arguments التي نحتاجها في Variables الصحيحة. لاحقاً سنضيف بعض معالجة الأخطاء (Error Handling) للتعامل مع بعض المواقف الخاطئة المحتملة، مثل عندما لا يقدم المستخدم أي Arguments؛ في الوقت الحالي، سنتجاهل هذا الموقف ونعمل على إضافة إمكانيات قراءة الملفات بدلاً من ذلك.