بدأنا عملية برمجة أداة سطر الأوامر المشابهة لأداة grep
التي تبحث داخل ملف معيّن عن سلسلة نصية محددة في مقال التعامل مع الدخل والخرج أين وضع أساس برنامج سطر الأوامر بلغة رست حيث برمجنا منطق التعامل مع الوسطاء المرّرة في سطر الأوامر، ثم حسناه وطورناه أكثر في مقال إعادة بناء التعليمات البرمجية لتحسين النمطية Modularity والتعامل مع الأخطاء حيث عملنا على تجزئة برنامجنا إلى وحدات منفصلة لتسهل عملية اختبار البرنامج، ثم اختبرنا البرنامج عبر كتابة اختبارات له في المقال السابق ، وسنوضّح أخيرًا في هذا المقال كيفية العمل مع متغيرات البيئة environment variables، إضافةً لكيفية طباعة الأخطاء إلى مجرى الأخطاء القياسي، وهما أمران مهمّان في برامج سطر الأوامر.
التعامل مع متغيرات البيئة
سنحسّن على برنامج "minigrep" باستخدام ميزة إضافية، ألا وهي خيار استخدام البحث بنمط عدم حساسية حالة الحروف case-insensitive (سواءٌ كانت أحرف صغيرة أو كبيرة) بحيث يمكن للمستخدم تفعيل هذا النمط أو تعطيله باستخدام متغيرات البيئة. يمكننا جعل هذه الميزة خيارًا لسطر الأوامر إلا أن المستخدم سيكون بحاجةٍ لكتابة هذا الخيار في سطر الأوامر في كل مرة يريد استخدام البرنامج، وباستخدام متغيرات البيئة نجعل ضبط هذا الخيار لمرة واحدة بحيث تكون كل عمليات البحث غير حساسة لحالة الأحرف في جلسة الطرفية تلك.
كتابة اختبار يفشل لدالة search لميزة عدم حساسية حالة الأحرف
نُضيف أولًا دالة search_case_insensitive
التي تُستدعى عندما يكون لمتغير البيئة قيمةً ما، وسنستمر باتباع عملية التطوير المُقاد بالاختبار هنا، وبالتالي ستكون الخطوة الأولى هي كتابة اختبار يفشل؛ إذ سنضيف اختبارًا جديدًا للدالة الجديدة search_case_insensitive
وسنعيد تسمية الاختبار القديم من اسمه السابق one_result
إلى case_sensitive
لتوضيح الفرق بين الاختبارين كما هو موضح في الشيفرة 20.
اسم الملف: src/lib.rs
#[cfg(test)] mod tests { use super::*; #[test] fn case_sensitive() { let query = "duct"; let contents = "\ Rust: safe, fast, productive. Pick three. Duct tape."; assert_eq!(vec!["safe, fast, productive."], search(query, contents)); } #[test] fn case_insensitive() { let query = "rUsT"; let contents = "\ Rust: safe, fast, productive. Pick three. Trust me."; assert_eq!( vec!["Rust:", "Trust me."], search_case_insensitive(query, contents) ); } }
الشيفرة 20: إضافة اختبار جديد يفشل لدالة عدم حساسية حالة الحرف التي سنضيفها لاحقًا
لاحظ أننا أضفنا اختبار contents
القديم أيضًا، وأضفنا سطرًا جديدًا للنص ".Duct tape" باستخدام حرف D كبير، والذي يجب ألا يطابق استعلام السلسلة النصية "duct" عندما نبحث في حالة حساسية حالة الأحرف. يساعد تغيير الاختبار القديم بهذه الطريقة في التأكد من أننا لن نعطّل خاصية البحث في حالة حساسية الأحرف (وهي الحالة التي طبقناها أولًا، والتي تعمل بنجاح للوقت الحالي). يجب أن ينجح هذا الاختبار الآن ويجب أن يستمر بالنجاح بينما نعمل على خاصية عدم حساسية حالة الأحرف.
يستخدم الاختبار الجديد للبحث بخاصية عدم حساسية حالة الأحرف "rUsT" مثل كلمة بحث، لذلك سنُضيف في الدالة search_case_insensitive
الكلمة "rUsT" والتي يجب أن تطابق ":Rust" بحرف R كبير وأن تطابق السطر ".Trust me" أيضًا رغم أن للنتيجتين حالة أحرف مختلفة عن الكلمة التي استخدمناها. هذا هو اختبارنا الذي سيفشل، وستفشل عملية تصريفه لأننا لم نعرّف بعد الدالة search_case_insensitive
. ضِف هيكلًا للدالة بحيث تُعيد شعاعًا فارغًا بطريقة مشابهة لما فعلناه في دالة search
في الشيفرة 16 حتى نستطيع تصريف الاختبار ورؤية أنه يفشل فعلًا.
تنفيذ دالة search_case_insensitive
ستكون الدالة search_case_insensitive
الموضحة في الشيفرة 21 مماثلة تقريبًا للدالة search
، والفارق الوحيد هنا هو أننا سنحوّل حال الأحرف للكلمة التي نبحث عنها إلى أحرف صغيرة (الوسيط query
) إضافةً إلى كل سطر line
، بحيث تكون حالة الأحرف متماثلة عند المقارنة بينهما بغضّ النظر عن حالة الأحرف الأصلية.
اسم الملف: src/lib.rs
pub fn search_case_insensitive<'a>( query: &str, contents: &'a str, ) -> Vec<&'a str> { let query = query.to_lowercase(); let mut results = Vec::new(); for line in contents.lines() { if line.to_lowercase().contains(&query) { results.push(line); } } results }
الشيفرة 21: تعريف الدالة search_case_insensitive
بحيث تحوّل أحرف الكلمة التي نبحث عنها مع السطر إلى أحرف صغيرة قبل مقارنتهما
نحوّل أولًا أحرف السلسلة النصية query
إلى أحرف صغيرة ونخزّنها في متغير يحمل الاسم ذاته، ولتحقيق ذلك نستدعي to_lowercase
على السلسلة النصية بحيث تكون النتيجة واحدة بغضّ النظر عن حالة الأحرف المدخلة "rust"
أو "RUST"
أو "Rust"
أو "rUsT"
وسنعامل السلسلة النصية المدخلة وكأنها "rust"
لإهمال حالة الأحرف، سيعمل التابع to_lowercase
على محارف يونيكود Unicode الأساسية إلا أن عمله لن يكون صحيحًا مئة بالمئة. إن كنّا نبرمج تطبيقًا واقعيًا فعلينا أن نقوم بالمزيد من العمل بخصوص هذه النقطة، إلا أننا نناقش في هذا القسم متغيرات البيئة وليس يونيكود، لذا لن نتطرق لذلك الآن.
لاحظ أن query
من النوع String
الآن وليس شريحة سلسلة نصية لأن استدعاء التابع to_lowercase
يُنشئ بيانات جديدة عوضًا عن استخدام مرجع للبيانات الموجودة مسبقًا. لنفرض بأن الكلمة هي "rUsT"
كمثال: لا تحتوي شريحة السلسلة النصية على حرف u
أو t
صغير لنستخدمه لذا علينا حجز مساحة جديدة لنوع String
يحتوي على "rust"
، وعندما نمرّر query
كوسيط إلى التابع contains
فنحن بحاجة لإضافة الرمز &
لأن شارة contains
معرفة بحيث تأخذ شريحة سلسلة نصية.
نضيف من ثمّ استدعاءً للتابع to_lowercase
لكل line
لتحويل أحرفه إلى أحرف صغيرة، وبذلك نكون حولنا كل من أحرف line
وquery
إلى أحرف صغيرة وسنجد حالات التطابق بغض النظر عن حالة الأحرف في السلسلتين الأصليتين.
دعنا نرى إذا كان تطبيقنا سيجتاز الاختبار:
$ cargo test Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished test [unoptimized + debuginfo] target(s) in 1.33s Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 2 tests test tests::case_insensitive ... ok test tests::case_sensitive ... ok test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94) running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s Doc-tests minigrep running 0 tests test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
عظيم، اجتزنا الاختبار. دعنا نستدعي الآن الدالة search_case_insensitive
من الدالة run
، وسنُضيف أولًا خيار الضبط إلى الهيكل Config
للتبديل بين البحث الحساس وغير الحساس لحالة الأحرف، إلا أن إضافة هذا الحقل ستتسبب بأخطاء عند التصريف لأننا لم نسند هذا الحقل إلى أي مكان بعد:
اسم الملف: src/lib.rs
pub struct Config { pub query: String, pub file_path: String, pub ignore_case: bool, }
أضفنا الحقل igonre_case
الذي يخزن متحول بولياني Boolean، وسنحتاج الدالة run
للتتحقق من قيمة ignore_case
لتحديد استدعاء أيّ من دالتي البحث: search
أو search_case_insensitive
كما هو موضح في الشيفرة 22. لن تُصرَّف هذه الشيفرة بنجاح بعد.
اسم الملف: src/lib.rs
pub fn run(config: Config) -> Result<(), Box<dyn Error>> { let contents = fs::read_to_string(config.file_path)?; let results = if config.ignore_case { search_case_insensitive(&config.query, &contents) } else { search(&config.query, &contents) }; for line in results { println!("{line}"); } Ok(()) }
الشيفرة 22: استدعاء الدالة search
أو الدالة search_case_insensitive
بحسب القيمة الموجودة في config.ignore_case
أخيرًا، نحن بحاجة إلى فحص متغير البيئة. الدوال الخاصة بالتعامل مع متغيرات البيئة موجودة في الوحدة module env
في المكتبة القياسية، لذا نضيف الوحدة إلى النطاق أعلى الملف src/lib.rs. نستخدم الدالة var
من الوحدة env
لفحص القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE
كما هو موضح في الشيفرة 23.
اسم الملف: src/lib.rs
use std::env; // --snip-- impl Config { pub fn build(args: &[String]) -> Result<Config, &'static str> { if args.len() < 3 { return Err("not enough arguments"); } let query = args[1].clone(); let file_path = args[2].clone(); let ignore_case = env::var("IGNORE_CASE").is_ok(); Ok(Config { query, file_path, ignore_case, }) } }
الشيفرة 23: التحقق من القيمة المضبوطة في متغير البيئة ذو الاسم IGNORE_CASE
نُنشئ هنا متغيرًا جديدًا يدعى ignore_case
ونُسند قيمته باستدعاء الدالة env::var
ونمرّر اسم متغير البيئة IGNORE_CASE
إليها. تُعيد الدالة env::var
قيمةً من النوع Result
تحتوي على متغاير variant يدعى Ok
يحتوي على قيمة متغير البيئة إذا كان متغير البيئة مضبوطًا إلى قيمة معينة وإلا فهي تعيد قيمة المتغاير Err
.
نستخدم التابع is_ok
على القيمة Result
للتحقق فيما إذا كان متغير البيئة مضبوطًا إلى قيمة معينة أم لا؛ فإذا كان مضبوطًا إلي قيمة فهذا يعني أنه علينا استخدام البحث بتجاهل حالة الأحرف؛ وإذا لم يكن مضبوطًا إلى قيمة معينة، فهذا يعني أن is_ok
ستُعيد القيمة false
وسينفذ البرنامج الدالة التي تجري البحث الحساس لحالة الأحرف. لا نهتم بقيمة متغير البيئة بل نهتم فقط فيما إذا كانت موجودة أو لا، ولذلك فنحن نستخدم التابع is_ok
بدلًا من استخدام unwrap
أو expect
أو أيًا من التوابع الأخرى التي استخدمناها مع Result
سابقًا.
نمرر القيمة في المتغير ignore_case
إلى نسخة Config
بحيث يمكن للدالة run
أن تقرأ هذه القيمة وتقرّر استدعاء الدالة search_case_insensitive
أو search
كما طبقنا سابقًا في الشيفرة 22.
دعنا نجرّب البرنامج، ولننفذ أولًا البرنامج دون ضبط متغير البيئة وباستخدام الكلمة to
، التي يجب أن تمنحنا جميع نتائج المطابقة للكلمة "to" بأحرف صغيرة فقط:
$ cargo run -- to poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep to poem.txt` Are you nobody, too? How dreary to be somebody!
يبدو أن البرنامج يعمل بنجاح. دعنا نجرّب الآن تنفيذ البرنامج مع ضبط IGNORE_CASE
إلى 1 باستخدام الكلمة ذاتها to
.
$ IGNORE_CASE=1 cargo run -- to poem.txt
إذا كنت تستخدم PowerShell، فعليك ضبط متغير البيئة، ثم تنفيذ البرنامج على أنهما أمرين منفصلين:
PS> $Env:IGNORE_CASE=1; cargo run -- to poem.txt
سيجعل ذلك متغير البيئة IGNORE_CASE
مستمرًا طوال جلسة الصدفة shell. ويمكن إزالة القيمة عن طريق الأمر Remove-Item
:
PS> Remove-Item Env:IGNORE_CASE
يجب أن نحصل على الأسطر التي تحتوي على الكلمة "to" بغض النظر عن حالة الأحرف:
Are you nobody, too? How dreary to be somebody! To tell your name the livelong day To an admiring bog!
عظيم، حصلنا على الكلمة "To" ضمن كلمات أخرى. يمكن لبرنامج minigrep
الآن البحث عن الكلمات بغض النظر عن حالة الأحرف عن طريق متغير بيئة، ويمكنك الآن التحكم بخيارات البرنامج عن طريق وسطاء سطر الأوامر، أو عن طريق متغيرات البيئة.
تسمح بعض البرامج بوسطاء سطر الأوامر ومتغيرات البيئة في ذات الوقت للخيار نفسه، وفي هذه الحالات يقرّر البرنامج أسبقية أحد الخيارين (وسيط سطر الأوامر أو متغير البيئة). تمرّن بنفسك عن طريق التحكم بحساسية حالة الأحرف عن طريق وسيط سطر أوامر أو متغير بيئة في الوقت ذاته، وحدّد أسبقية أحد الخيارين حسب تفضيلك إذا تعارض الخياران مع بعضهما.
تحتوي الوحدة std::env
على العديد من الخصائص الأخرى المفيدة للتعامل مع متغيرات البيئة، اقرأ توثيق الوحدة للاطّلاع على الخيارات المتاحة.
كتابة رسائل الخطأ إلى مجرى الخطأ القياسي بدلا من مجرى الخرج القياسي
نكتب رسائل الأخطاء حاليًا إلى الطرفية باستخدام الماكرو println!
، وفي معظم الطرفيات هناك نوعين من الخرج: خرج قياسي stdout
للمعلومات العامة وخطأ قياسي stderr
لرسائل الخطأ، ويساعد التمييز بين النوعين المستخدمين بتوجيه خرج نجاح البرنامج إلى ملف مع المحافظة على ظهور رسائل الخطأ على شاشة الطرفية.
الماكرو println!
قادرٌ فقط على الطباعة إلى الخرج القياسي، لذا علينا استخدام شيء مختلف لطباعة الأخطاء إلى مجرى الأخطاء القياسي standard error stream.
التحقق من مكان كتابة الأخطاء
دعنا أولًا نلاحظ كيفية طباعة المحتوى في برنامج minigrep
حاليًا إلى الخرج القياسي، متضمنًا ذلك رسائل الأخطاء التي نريد كتابتها إلى مجرى الأخطاء القياسي بدلًا من ذلك، وسنحقّق ذلك بإعادة توجيه مجرى الخرج القياسي إلى ملف والتسبب بخطأ عمدًا، بينما سنُبقي على مجرى الأخطاء القياسي ولن نعيد توجيهه، وبالتالي سيُعرض محتوى مجرى الأخطاء القياسي على الشاشة مباشرةً.
من المتوقع لبرامج سطر الأوامر أن ترسل رسائل الخطأ إلى مجرى الأخطاء القياسي بحيث يمكننا رؤية الأخطاء على الشاشة حتى لو كنّا نعيد توجيه مجرى الخرج القياسي إلى ملف ما. لا يسلك برنامجنا حاليًا سلوكًا جيدًا، إذ أننا على وشك رؤية أن رسائل الخطأ تُخزّن في الملف.
لتوضيح هذا السلوك سننفذ البرنامج باستخدام <
ومسار الملف output.txt وهو الملف الذي نريد إعادة توجيه مجرى الخرج القياسية إليه. لن نمرّر أي وسطاء عند التنفيذ، وهو ما سيتسبب بخطأ:
$ cargo run > output.txt
يخبر الرمز <
الصدفة بكتابة محتويات الخرج القياسي إلى الملف output.txt بدلًا من الشاشة. لم نحصل على أي رسالة خطأ على الرغم من توقعنا لها بالظهور على الشاشة مما يعني أن الرسالة قد كُتبت إلى الملف. إليك محتوى الملف output.txt:
Problem parsing arguments: not enough arguments
نعم، تُطبع رسالة الخطأ إلى الخرج القياسي كما توقعنا ومن الأفضل لنا طباعة رسائل الخطأ إلى مجرى الأخطاء القياسي بدلًا من ذلك بحيث يحتوي الملف على البيانات الناتجة عن التنفيذ الناجح، دعنا نحقّق ذلك.
طباعة الأخطاء إلى مجرى الأخطاء القياسي
سنستخدم الشيفرة البرمجية في الشيفرة 24 لتعديل طريقة طباعة الأخطاء. لحسن الحظ، الشيفرة البرمجية المتعلقة بطباعة رسائل الخطأ موجودة في دالة واحدة ألا وهي main
بفضل عملية إعادة بناء التعليمات البرمجية التي أنجزناها سابقًا. تقدّم لنا المكتبة القياسية الماكرو eprintln!
الذي يطبع إلى مجرى الأخطاء القياسي، لذا دعنا نعدّل من السطرين الذين نستخدم فيهما الماكرو println!
ونستخدم eprintln!
بدلًا من ذلك.
اسم الملف: src/main.rs
fn main() { let args: Vec<String> = env::args().collect(); let config = Config::build(&args).unwrap_or_else(|err| { eprintln!("Problem parsing arguments: {err}"); process::exit(1); }); if let Err(e) = minigrep::run(config) { eprintln!("Application error: {e}"); process::exit(1); } }
الشيفرة 24: كتابة رسائل الخطأ إلى مجرى الأخطاء القياسية بدلًا من مجرى الخرج القياسي باستخدام eprintln!
دعنا ننفذ البرنامج مجددًا بالطريقة ذاتها دون وسطاء وبإعادة توجيه الخرج القياسي إلى ملف باستخدام <
:
$ cargo run > output.txt Problem parsing arguments: not enough arguments
نستطيع رؤية الخطأ على الشاشة الآن، ولا يحتوي الملف output.txt أي بيانات وهو السلوك الذي نتوقعه من برامج سطر الأوامر.
دعنا ننفذ البرنامج مجددًا باستخدام الوسطاء لتنفيذ البرنامج دون أخطاء، وبتوجيه الخرج القياسي إلى ملف أيضًا كما يلي:
$ cargo run -- to poem.txt > output.txt
لن نستطيع رؤية أي خرج على الطرفية، وسيحتوي الملف output.txt على نتائجنا:
اسم الملف: output.txt
Are you nobody, too? How dreary to be somebody!
يوضح هذا الأمر أننا نستخدم مجرى الخرج القياسي للخرج في حالة النجاح، بينما نستخدم مجرى الأخطاء القياسي في حالة الفشل.
خاتمة المشروع
لخّصت جزئية سطر الأوامر من هذه السلسلة بمشروعها الكثير من المفاهيم المهمة التي تعلمناها لحد اللحظة كما أننا تكلمنا عن كيفية إجراء عمليات الدخل والخرج في رست، وذلك باستخدام وسطاء سطر الأوامر والملفات ومتغيرات البيئة والماكرو eprintln!
لطباعة الأخطاء، ويجب أن تكون الآن مستعدًا لكتابة تطبيقات سطر الأوامر المختلفة. يجب أن تبقى شيفرتك البرمجية منظمةً جيدًا بمساعدة المفاهيم التي تعلمتها في المقالات السابقة وأن تخزن البيانات بفعالية في هياكل بيانات مناسبة وأن تتعامل مع الأخطاء بصورةٍ مناسبة، إضافةً إلى إجراء الاختبارات.
سننظر في المقالات التالية إلى بعض مزايا رست التي تأثرت باللغات الوظيفية، ألا وهي المغلّفات closures والمكررات iterators.
ترجمة -وبتصرف- لقسم من الفصل An I/O Project: Building a Command Line Program من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.