يمثّل هذا المقال تطبيقًا لجميع المهارات التي تعلمتها حتى الآن باتباعك لهذه السلسلة البرمجة بلغة رست، ونظرةً عمليةً إلى المزيد من المزايا الموجودة في المكتبة القياسية Standard Library. سنبني سويًّا أداة سطر أوامر command line tool تتفاعل مع ملف وتُجري عمليات الدخل والخرج باستخدام سطر الأوامر للتمرُّن على بعض مفاهيم رست التي تعلمتها حتى هذه اللحظة.
تجعل السرعة والأمان والخرج الثنائي الوحيد ودعم مختلف المنصات cross-platform من رست لغةً مثالية لإنشاء أدوات السطر البرمجي، لذا سيكون مشروعنا هو إصدار خاص من أداة بحث سطر الأوامر الكلاسيكية "grep" (اختصارًا للبحث العام باستخدام التعابير النمطية والطباعة globally search a regular expression and print). يبحث "grep" في حالات الاستخدام الأسهل على سلسلة نصية string محددة داخل ملف معين، ولتحقيق ذلك يأخذ "grep" مسار الملف وسلسلة نصية مثل وسطاء له، ثم يقرأ الملف ويجد الأسطر التي تحتوي على وسيط السلسلة النصية داخل الملف ويطبع هذه الأسطر.
سنوضّح لك كيف ستستخدم أداة سطر الأوامر لخصائص سطر الأوامر وهو ما تستخدمه العديد من أدوات سطر الأوامر الأخرى، إذ سنقرأ قيمة متغير البيئة environment variable للسماح للمستخدم بضبط سلوك أداتنا، كما أننا سنطبع رسالة خطأ إلى مجرى الخطأ القياسي الخاص بالطرفية standard error console stream -أو اختصارًا stderr- بدلًا من الخرج القياسي standard output -أو اختصارًا stdout. لذلك، يمكن للمستخدم مثلًا إعادة توجيه الخرج الناجح إلى ملف بحيث يظل قادرًا على رؤية رسالة الخطأ على الشاشة في الوقت ذاته.
أنشأ عضو من مجتمع رست يدعى آندرو غالانت Andrew Gallant إصدارًا سريعًا ومليئًا بالمزايا من grep وأطلق عليه تسمية ripgrep. سيكون إصدارنا الذي سننشئه في هذه المقالة أكثر بساطةً إلا أن هذه المقالة ستمنحك بعض الأساسيات التي تحتاجها لتفهم المشاريع العملية الواقعية مثل ripgrep.
سيجمع مشروع grep الخاص بنا عددًا من المفاهيم التي تعلمناها سابقًا:
- تنظيم الشيفرة البرمجية (استخدام ما تعلمناه بخصوص الوحدات modules في مقال الحزم packages والوحدات المصرفة crates في لغة رست Rust)
- استخدام الأشعة vectors والسلاسل النصية (في مقال تخزين لائحة من القيم باستخدام الأشعة Vectors في لغة رست Rust)
- التعامل مع الأخطاء (في مقال الأخطاء والتعامل معها في لغة رست Rust)
- استخدام السمات traits ودورات الحياة lifetimes عند الحاجة (في مقال السمات Traits في لغة رست Rust ومقال التحقق من المراجع References عبر دورات الحياة Lifetimes في لغة رست)
- كتابة الاختبارات (في مقال كتابة الاختبارات في لغة رست Rust)
سنتكلم أيضًا بإيجاز عن المغلّفات closures والمكرّرات iterators وسمة الكائنات trait objects، إلا أننا سنتكلم عن هذه المفاهيم بالتفصيل لاحقًا.
الحصول على الوسطاء من سطر الأوامر
دعنا نُنشئ مشروعًا جديدًا باستخدام 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 بكتابة برامج تقبل وسطاء سطر الأوامر، إلا أننا سنطبّق هذا الأمر بأنفسنا بهدف التعلُّم.
قراءة قيم الوسطاء
سنحتاج لاستخدام الدالة std::env::args
الموجودة في مكتبة رست القياسية لتمكين minigrep
من قراءة قيم وسطاء سطر الأوامر التي نمرّرها إليه، إذ تُعيد هذه الدالة مكرّرًا iterator إلى وسطاء سطر الأوامر الممرّرة إلى minigrep
. سنغطّي المكررات لاحقًا، إلا أنه من الكافي الآن معرفتك معلومتين بخصوص المكررات: تنتج المكررات مجموعةً من القيم، ويمكننا استدعاء التابع collect
على مكرّر لتحويله إلى تجميعة collection مثل الأشعة التي تحتوي جميع العناصر التي تنتجها المكررات.
تسمح الشيفرة 1 لبرنامجك minigrep
بقراءة أي وسطاء سطر أوامر تمررها إليه، ثمّ تجمّع القيم في شعاع.
اسم الملف: src/main.rs
use std::env; fn main() { let args: Vec<String> = env::args().collect(); dbg!(args); }
[الشيفرة 1: تجميع وسطاء سطر الأوامر في شعاع وطباعتها]
نُضيف أوّلًا الوحدة std::env
إلى النطاق scope باستخدام تعليمة use
، بحيث يمكننا استخدام الدالة args
المحتواة داخلها. لاحظ أن الدالة std::env::args
متداخلة nested مع مستويين من الوحدات. سنختار إحضار الوحدة الأب إلى النطاق بدلًا من الدالة كما ناقشنا سابقًا في الحالات التي تكون فيها الدالة المطلوبة متداخلة مع أكثر من وحدة واحدة، ويمكننا بذلك استخدام الدوال الأخرى من std::env
، كما أن هذه الطريقة أقل غموضًا من إضافة use std::env::args
، ثم استدعاء الدالة بكتابة args
فقط لأن args
قد يُنظر إليها بكونها دالة معرّفة بالوحدة الحالية بصورةٍ خاطئة.
ملاحظة: ستهلع std::env::args
إذا احتوى أي وسيط على يونيكود غير صالح، وإذا احتاج البرنامج لقبول الوسطاء التي تحتوي على يونيكود غير صالح، فعليك استخدام std::env::args_os
بدلًا منها، إذ تعيد هذه الدالة مكرّرًا ينتج قيم OsString
بدلًا من قيم String
، وقد اخترنا استخدام std::env::args
هنا لبساطتها، ونظرًا لاختلاف قيم OsString
بحسب المنصة وهي أكثر تعقيدًا في التعامل معها مقارنةً بقيم String
.
نستدعي env::args
في السطر الأول من main
ومن ثم نستخدم collect
مباشرةً لتحويل المكرّر إلى شعاع يحتوي على جميع القيم الموجودة في المكرّر، ويمكننا استخدام collect
هنا لإنشاء عدة أنواع من التجميعات، لذا يجب أن نشير صراحةً لنوع args
لتحديد أننا نريد شعاع من السلاسل النصية، وعلى الرغم من أن تحديد الأنواع في رست نادر، إلا أن collect
دالة يجب أن تحدد فيها النوع عادةً، لأن رست لا تستطيع استنتاج نوع التجميعة التي تريدها.
أخيرًا، نطبع الشعاع باستخدام ماكرو تنقيح الأخطاء debug macro. دعنا نجرّب تنفيذ الشيفرة البرمجية أولًا دون استخدام أي وسطاء ومن ثم باستخدام وسيطين:
$ cargo run Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.61s Running `target/debug/minigrep` [src/main.rs:5] args = [ "target/debug/minigrep", ]
$ cargo run -- needle haystack Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 1.57s Running `target/debug/minigrep needle haystack` [src/main.rs:5] args = [ "target/debug/minigrep", "needle", "haystack", ]
لاحظ أن القيمة الأولى في الشعاع هي "target/debug/minigrep"
وهو اسم ملفنا التنفيذي، وهذا يطابق سلوك لائحة الوسطاء في لغة سي C بالسماح للبرامج باستخدام الاسم الذي كان السبب في بدء تنفيذها، ومن الملائم عادةً الحصول على اسم البرنامج في حال أردت طباعته ضمن رسائل، أو تعديل سلوك البرنامج المبني على اسم سطر الأوامر البديل command line alias الذي نستخدمه لبدء تشغيل البرنامج، وسنتجاهله الآن ونحفظ فقط أول وسيطين نستخدمهما.
حفظ قيم الوسطاء في متغيرات
يستطيع البرنامج حاليًا الحصول على القيم المحدّدة مثل وسطاء سطر أوامر، أما الآن فنحن بحاجة لحفظ قيم الوسيطين في متغيرين بحيث يمكننا استخدام المتغيرين ضمن بقية البرنامج، وهو ما فعلناه في الشيفرة 2.
اسم الملف: src/main.rs
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); }
[الشيفرة 2: إنشاء متغيرين لتخزين وسيط السلسلة النصية ووسيط مسار الملف]
يحتلّ اسم البرنامج القيمة الأولى في الشعاع عند args[0]
كما رأينا سابقًا عندما طبعنا الشعاع، لذا نبدأ من الدليل "1" إذ أن الوسيط الأول للبرنامج minigrep
هو السلسلة النصية التي سنبحث عنها، لذا نضع مرجعًا reference على أول وسيط في المتغير query
، بينما يمثل الوسيط الثاني مسار الملف، لذا نضع مرجعًا عليه في المتغير file_path
.
نطبع مؤقتًا قيم المتغيرَين لنتأكد من أن الشيفرة البرمجية تعمل وفق ما هو متوقع. دعنا ننفذ البرنامج مجددًا بالوسيطين test
و sample.txt
:
$ cargo run -- test sample.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep test sample.txt` Searching for test In file sample.txt
عظيم، يعمل برنامجنا بنجاح، إذ تُمرّر قيم الوسطاء التي نحتاجها وتُحفظ في المتغيرات المناسبة. سنضيف لاحقًا شيفرةً برمجيةً للتعامل مع الأخطاء في حالات استخدام خاطئة محتملة، مثل الحالة التي لا يدخل فيها المستخدم أي وسطاء، والتي سنتجاهلها الآن ونبدأ بالعمل على إضافة شيفرة برمجية لقراء الملف بدلًا من ذلك.
قراءة ملف
سنضيف الآن إمكانية قراءة الملف المحدد في الوسيط file_path
. نحتاج أولًا لملف تجريبي لتجربة البرنامج باستخدامه، وسنستخدم ملفًا يحتوي على نصٍ قصير يحتوي على عدّة أسطر مع كلمات مكرّرة. تحتوي الشيفرة 3 على قصيدة لإيميلي ديكنز Emily Dickinson وهو ما سنستخدمه هنا. أنشئ ملفًا يدعى "poem.txt" في مستوى جذر المشروع وأدخل قصيدة "أنا لا أحد! من أنت؟ I'm Nobody! Who are you?".
اسم الملف: poem.txt
I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog!
[الشيفرة 3: قصيدة إيميلي ديكنز تمثّل ملف تجريبي مناسب]
بعد تهيئتك للنص، عدّل الملف "src/main.rs" وضِف شيفرة برمجية لقراءة الملف كما هو موضح في الشيفرة 4.
اسم الملف: src/main.rs
use std::env; use std::fs; fn main() { // --snip-- println!("In file {}", file_path); let contents = fs::read_to_string(file_path) .expect("Should have been able to read the file"); println!("With text:\n{contents}"); }
[الشيفرة 4: قراءة محتويات الملف المحدّد وفق الوسيط الثاني]
أضفنا أولًا جزءًا متعلقًا بالبرنامج من المكتبة القياسية باستخدام تعليمة use
إلى النطاق، لأننا نحتاج إلى std::fs
إلى التعامل مع الملفات.
تأخذ التعليمة fs::read_to_string
الجديدة في الدالة main
القيمة file_path
، ثم تفتح ذلك الملف وتُعيد قيمةً من النوع std::io::Result<String>
تمثّل محتويات الملف.
أضفنا مجددًا تعليمة println!
مؤقتة تطبع قيمة contents
بعد قراءة الملف، حتى نتأكد من عمل البرنامج بصورةٍ صحيحة.
لننفّذ هذه الشيفرة البرمجية باستخدام أي سلسلة نصية تمثّل وسيط سطر أوامر أول (لأننا لم نطبق جزء البحث عن السلسلة النصية بعد) والملف "poem.txt" بمثابة وسيط ثاني:
$ cargo run -- the poem.txt Compiling minigrep v0.1.0 (file:///projects/minigrep) Finished dev [unoptimized + debuginfo] target(s) in 0.0s Running `target/debug/minigrep the poem.txt` Searching for the In file poem.txt With text: I'm nobody! Who are you? Are you nobody, too? Then there's a pair of us - don't tell! They'd banish us, you know. How dreary to be somebody! How public, like a frog To tell your name the livelong day To an admiring bog!
عظيم، تقرأ الآن الشيفرة البرمجية محتويات الملف ثم تطبعها، إلا أن الشيفرة البرمجية تحتوي على بعض الثغرات، إذ تحتوي الدالة main
الآن على عدّة مسؤوليات، ومن الأفضل عمومًا استخدام دالة واحدة لمسؤولية واحدة للحصول على دوال أسهل بالتعامل وأوضح، والمشكلة الثانية هي أننا لم نتعامل مع الأخطاء كما ينبغي لنا، إلا أن البرنامج ما زال صغيرًا وبالتالي لا تشكل هذه المشاكل تهديدًا كبيرًا، لكنها ستصبح صعبة الحل مع زيادة حجم البرنامج، فمن الأفضل إعادة بناء التعليمات البرمجية refactor بمرحلة مبكرة من تطوير البرنامج لأن إعادة بناء التعليمات البرمجية سيكون أسهل بكثير من كميات قليلة من الشيفرات البرمجية، لذا دعنا نفعل ذلك تاليًا في المقال التالي.
ترجمة -وبتصرف- لقسم من الفصل Refactoring to Improve Modularity and Error Handling من كتاب The Rust Programming Language.
أفضل التعليقات
لا توجد أية تعليقات بعد
انضم إلى النقاش
يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.