اذهب إلى المحتوى

كتابة برنامج سطر أوامر Command Line بلغة رست Rust: التعامل مع الدخل والخرج


Naser Dakhel

يمثّل هذا المقال تطبيقًا لجميع المهارات التي تعلمتها حتى الآن باتباعك لهذه السلسلة البرمجة بلغة رست، ونظرةً عمليةً إلى المزيد من المزايا الموجودة في المكتبة القياسية 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 الخاص بنا عددًا من المفاهيم التي تعلمناها سابقًا:

سنتكلم أيضًا بإيجاز عن المغلّفات 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.

اقرأ أيضًا


تفاعل الأعضاء

أفضل التعليقات

لا توجد أية تعليقات بعد



انضم إلى النقاش

يمكنك أن تنشر الآن وتسجل لاحقًا. إذا كان لديك حساب، فسجل الدخول الآن لتنشر باسم حسابك.

زائر
أضف تعليق

×   لقد أضفت محتوى بخط أو تنسيق مختلف.   Restore formatting

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   جرى استعادة المحتوى السابق..   امسح المحرر

×   You cannot paste images directly. Upload or insert images from URL.


×
×
  • أضف...