التعامل مع الملفات


التعامل مع الملفات أو معالجة الملفات (files handling) يقصد منها إجراء عملية ما على الملفات المخزنة في الحاسوب، كقراءة محتوى ملف و عرضه، وحذفه، وتغيير موقعه، وإنشاء نسخة منه،و تعديل محتواه.
وللتعامل مع الملفات في لغة الأسس تحتاج إلى تضمين الوحدة `نـم` (Fs) (اختصار لـ نظام الملفات أو File System) التي تحتوي على الدالات والعناصر اللازمة للتعامل مع نظام الملفات.
يمكنك إجراء 5 عمليات أساسية على الملفات هي:

  1. إنشاء ملف جديد.
  2. فتح ملف موجود.
  3. قراءة البيانات من ملف.
  4. كتابة البيانات في ملف.
  5. إغلاق ملف.

فتح ملف


عند التعامل مع الملفات علينا تعريف مؤشر من نوع File للتعامل مع الملف من خلاله، وكما ذكرنا في بداية الدرس، لابد من تضمين الوحدة `نـم` (Fs).

  اشمل "مـتم/نـم"؛
  عرف مم: مؤشر[نـم.مـلف]؛
import "Srl/Fs.alusus";
def fp: ptr[Fs.File];

بعد تعريف مؤشر من نوع `مـلف` (File) للتعامل مع الملف يتعين علينا فتح الملف، لذا سوف نستخدام الدالة `افتح_ملف` (openFile)، وتحديد فيما إذا كنا نريد القراءة فقط من الملف أو الكتابة فيه أو كلاهما معاً.

مم = افتح_ملف("مسار/إلى/الملف"، "r")؛
fp = openFile("path/to/file", "r");

تقوم هذه الدالة بإرجاع مؤشر على الملف المطلوب، وسيتم تخزينه في المؤشر fp. المعطى الاول سلسلة نصية تحتوي اسم الملف مع المسار، بينما المعطى الثاني يحدد نمط فتح الملف (في هذا المثال يُفتح الملف بنمط القراءة). فيما يلي الخيارات المتاحة للنمط:

  1. "r" للقراءة فقط.
  2. "w" للكتابة فقط (تؤدي إلى إنشاء الملف إذا لم يكن موجوداً).
  3. "a" لإلحاق الملف ببيانات جديدة في نهايته (أي تبدأ الكتابة من نهاية الملف الموجود).
  4. "+r" للقراءة والكتابة في الملف.
  5. "+w" للقراءة والكتابة (تؤدي إلى إنشاء الملف إذا لم يكن موجوداً).
  6. "+a" للقراءة ,والتحديث (تؤدي إلى إنشاء الملف إذا لم يكن موجوداً). ستبدأ القراءة من البداية، لكن الكتابة يمكن إلحاقها فقط.

بعد ذلك يمكنك معالجة الملف بالشكل الذي تحتاجه و باستخدام الدوال المناسبة. وهنا غالباً مانحتاج لدوال مثل دالة القراءة `اقرأ` (read)، ودالة الكتابة `اكتب` (write)، ودالة التنقل `انتقل` (seek) التي تمكننا من التنقل داخل الملف أثناء القراءة أو الكتابة فيه، والدالة `هات_الموقع` (tell) التي تدلنا على مكان وجودنا داخل الملف.

كتابة ملف


نستخدم الدالة `اكتب` (write) للكتابة على الملفات.

اكتب(المحتوى، حجم_المحتوى، عدد_المحتوى، مؤشر_ملف)؛
write(content, contentSize, contentCount, fp);

يستخدم هذا التابع للكتابة بداخل الملف. ويُرجع عدد صحيح يمثل عدد المحتويات التي تمت كتابتها. إن لم يحصل خطأ أثناء الكتابة فإن القيمة المرجعة ستكون مطابقة لـ `عدد_المحتوى` (contentCount).

المعطى الأول للدالة مؤشر على مصفوفة المحتويات المراد كتابتها. بينما يحمل المعطى الثاني حجم كل عنصر من هذه المصفوفة ويمثل المعطى الثالث عدد عناصر هذه المصفوفة، بينما آخر معطى هو مؤشر الملف الذي حصلنا عليه من الدالة `افتح_ملف`.

لنجرب الآن كتابة نص في ملف:

عرف نص: نـص؛
نص = "لغة الأسس"؛
def text:String;
text= "Alusus Language";

المتغير `نص` (text) من النمط `نـص` (String) هو بطبيعة الحال مؤشر على مصفوفة محارف، لذا يمكننا تمريره مباشرةً إلى الدالة write كوسيط أول. الآن كل عنصر من هذه السلسلة هو عبارة عن محرف (char) حجمه بايت واحد وبالتالي سيكون حجم المحتوى 1 وبما أن أحجام أنواع البيانات قد تختلف من معمارية جهاز إلى آخر فسنضع `مـحرف~حجم` (Char~size) كثاني معطى وهي تكافئ كتابتنا ل 1 على أنظمة لينكس. أما بالنسبة لعدد المحتويات فهو عدد العناصر التي ستتم كتابتها ضمن الملف وبما أن السلسلة في نهاية الأمر هي سلسلة محارف، بالتالي فإن عدد العناصر هو عدد هذه المحارف وهذا يكافئ طول السلسلة النصية، لذا يمكننا أن نستخدم الدالة `هات_الطول` (getLength). وأخيراً نمرر المؤشر على الملف المطلوب الكتابة ضمنه.

نـم.اكتب(نص، مـحرف~حجم، نص.هات_الطول() + 1، مؤشر_ملف)؛
Fs.write(text, char~size, text.getLength() + 1, fp);

لماذا قمنا بإضافة 1 إلى هاتج `هات_الطول`؟

الإجابة: الصنف `نـص` (String) يضيف تلقائياً المحرف \0 إلى نهاية مصفوفة المحارف التي بداخله، كما و يتم حجز ذاكرة للمحرف 0\ كما باقي المحارف، لكن يُبقي ذلك مخفياً عنك، على سبيل المثال السلسلة "Alusus" يتم تمثيلها داخليا كمصفوفة محارف بالشكل التالي:

{'A', 'l', 'u', 's', 'u', 's', '\0}

لذا عند استخدام الدالة `هات_الطول` فإنه يُرجع طول السلسلة لكن من دون احتساب المحرف 0\ أي يُرجع هنا 6 بدلاً من 7. لذلك قمنا بإضافة 1 لنشمل المحرف 0\.

إغلاق ملف


الآن بعد أن انتهينا من كتابة مانريده ضمن الملف، لابد من إغلاقه باستخدام دالة `أغلق_ملف` (closeFile) التي تأخذ وسيطاً واحداً هو مؤشر الملف.

نـم.أغلق_ملف(مؤشر_ملف)؛
Fs.closeFile(fp);

الكود كاملاً:

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/نـم"؛
اشمل "مـتم/نـص"؛
استخدم مـتم؛
عرف مسار: نـص؛
عرف محتوى: نـص؛
عرف مم: مؤشر[نـم.مـلف]؛
مسار = "/home/alu/ملف.نص"؛ // مسار الملف
محتوى = "لغة الأسس"؛ // النص الذي سيُكتب في الملف
مم = نـم.افتح_ملف(مسار، "w")؛ // فتح الملف لغرض الكتابة
نـم.اكتب(محتوى، مـحرف~حجم، محتوى.هات_الطول() + 1، مم)؛ // كتابة النص في الملف
نـم.أغلق_ملف(مم)؛
import "Srl/Console.alusus";
import "Srl/Fs.alusus";
import "Srl/String.alusus";
use Srl;
def path: String;
def text: String;
def fp: ptr[Fs.File];
path = "/home/ali/textfile.txt"; // مسار الملف
text = "Alusus Language"; // النص الذي سنقوم بكتابته في الملف
fp = Fs.openFile(path, "w"); // قتح الملف لغرض الكتابة
Fs.write(text, char~size, text.getLength() + 1, fp); // كتابة النص في الملف
Fs.closeFile(fp);

قراءة ملف باستخدام الدالة `اقرأ`


الآن بعد أن قمنا بالكتابة ضمن الملف، سنقوم بكتابة كود آخر لقراءة محتويات الملف، لذا سنقوم أولاً بفتح الملف لكن مع تحديد وضع القراءة هذه المرة "r"، ثم سنقوم باستخدام الدالة `اقرأ` (read) لقراءة المحتويات.


عدد_العناصر_المقروءة = اقرأ(محتوى~مؤشر، حجم_العنصر، الحد_الأقصى_للعناصر، مؤشر_ملف)؛
readCount = read(content~ptr, contentSize, maxContentCount, fp);

لاحظ أن وسطاء هذه الدالة لاتختلف عن الدالة `اكتب` (write). لكن هناك بعض الاختلافات، فالمعطى الأول هنا سيتم فيه تخزين ماتم قرائته، كما أننا سنحتاج إلى تحديد عدد عناصر الملف الذي ستتم قرائته (أقصد `الحد_الأقصى_للعناصر` أو maxContentCount)، وذلك لأننا نريد حجز ذاكرة تتسع لما ستتم قرائته، ولمعرفة ذلك سنقوم باستخدام الدالتين `هات_الموقع` (tell) و `انتقل` (seek)، حيث سنقوم باستخدام الدالة `انتقل` للانتقال من خلالها إلى نهاية الملف، و تأخذ هذه الدالة ثلاث وسطاء.

نـم.انتقل(مؤشر_ملف، 0، نـم.انـتقال._نهاية_)؛
Fs.seek(fp, 0, Fs.Seek.END);

في الدالة أعلاه طلبنا الانتقال إلى الإزاحة 0 بدءًا من نهاية الملف، أي طلبنا الانتقال إلى نهاية الملف. المعطى الأخير يمكن أن يكون أحد القيم التالية:

  • 0 أو `انـتقال._نهاية_` (Seek.SET): تغيير موقع المؤشِّر إلى مقدار المعطى الثاني بالبايت، ابتداءًا من بداية الملف. أي تنقلنا تماماً إلى قيمة المعامل الثاني، فلو كان 1 ستنقلنا للمحرف الأول ولو كان 0 ستنقلنا لبداية الملف.
  • 1 أو `انـتقال._حالي_` (Seek.CUR): إزاحة موقع المؤشر من الموقع الحالي بمقدار المعطى الثاني، بالبايت. أي بعبارةٍ أوضح تنقلنا من مكاننا الحالي إلى الأمام بمقدار المعطى الثاني، وبالتالي إذا كان المعطى الثاني 1 ستنقلنا للأمام محرفًا واحدًا، وإذا كان -1 ستنقلنا للخلف محرفًا واحدًا.
  • 2 أو `انـتقال._نهاية_` (Seek.END): تغيير موقع المؤشِّر إلى مقدار المعطى الثاني بالباين، ابتداءًا من نهاية الملف. أي تنقلنا لنهاية الملف، ثم ترجع عن للخلف بمقدار المعطى الثاني.

الآن بعد أن انتقلنا إلى نهاية الملف أصبح بإمكاننا معرفة حجم الملف من خلال الدالة `هات_الموقع` (tell) التي ستخبرنا بالموقع الذي نحن فيه، وهذا يكافئ عدد البايتات من بداية الملف، وبالتالي نكون حصلنا على المطلوب.

الحجم_الكلي = نـم.هات_الموقع(مؤشر_ملف)؛
totalCount = Fs.tell(fp);

الآن بعد معرفة حجم الملف أصبح بإمكاننا حجز ذاكرة تتسع لما ستتم قرائته، لذلك سنستخدم الدالة `احجز` (alloc) المعرفة ضمن الصنف `نـص` (String) لإنجاز الأمر:

محتوى.احجز(الحجم_الكلي)؛
text.alloc(totalCount);

الآن نعود إلى بداية الملف من جديد:

  نـم.انتقل(مؤشر_ملف، 0، نـم.انـتقال._بداية_)؛
Fs.seek(fp, 0, Fs.Seek.SET);

ثم نقوم باستدعاء دالة القراءة بالشكل التالي:

نـم.اقرأ(محتوى.صوان، مـحرف~حجم، الحجم_الكلي، مؤشر_ملف)؛
Fs.read(text.buf, char~size, totalCount, fp);

الكود كاملاً:

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/نـم"؛
اشمل "مـتم/نـص"؛
استخدم مـتم؛
عرف مسار: نـص؛
عرف محتوى: نـص؛
عرف الحجم: صـحيح_متكيف؛
عرف مم: مؤشر[نـم.مـلف]؛
مسار = "/home/ali/ملف_نصي.نص"؛
مم = نـم.افتح_ملف(مسار، "r")؛ // فتح الملف للقراءة
نـم.انتقل(مم، 0، نـم.انـتقال._نهاية_)؛ // الانتقال لنهاية الملف
الحجم = نـم.هات_الموقع(مم)؛ معرفة الموقع الحالي
نـم.انتقل(مم، 0، نـم.انـتقال._بداية_)؛ // العودة إلى بداية الملف
محتوى.احجز(الحجم)؛ // حجز ذاكرة تتسع للملف
نـم.اقرأ(محتوى، مـحرف~حجم، الحجم، مم)؛ // القراءة من الملف
طـرفية.اطبع(محتوى)؛ // طباعة محتوى الملف
نـم.أغلق_ملف(مم)؛ // إغلاق الملف
import "Srl/Console.alusus";
import "Srl/Fs.alusus";
import "Srl/String.alusus";
use Srl;
def path: String;
def text: String;
def count: ArchInt;
def fp: ptr[Fs.File];
path = "/home/ali/textfile.txt";
fp = Fs.openFile(path, "r"); // فتح الملف للقراءة
Fs.seek(fp, 0, Fs.Seek.END); // الانتقال إلى نهاية الملف
count = Fs.tell(fp); // معرفة الموقع الحالي
Fs.seek(fp, 0, Fs.Seek.SET); // العودة إلى بداية الملف
text.alloc(count); // حجز ذاكرة تتسع للملف
Fs.read(text,char~size, count, fp); // القراءة من الملف
Console.print("%s", text.buf) // طباعة الملف
Fs.closeFile(fp) // إغلاق الملف

قراءة ملف باستخدام الدالة `اقرأ_ملف`


يمكنك استخدام الدالة `اقرأ_ملف` (readFile) للقراءة من الملف بشكل أكثر سهولة، وهذه الدالة لها الصيغة التالية:

دالة اقرأ_ملف (
    اسم_الملف: مؤشر[مصفوفة[مـحرف]]، ناتج: مؤشر[مؤشر]، حجم: مؤشر[صـحيح_متكيف]
): ثـنائي؛
readFile (
  filename: ptr[array[Char]], result: ptr[ptr], size: ptr[ArchInt]
): Bool;

ترجع الدالة 1 في حالة القراءة بنجاح من الملف و 0 عكس ذلك.

اسم_الملف (filename): اسم أو مسار الملف المطلوب قرائته.
ناتج (result): مؤشر على مؤشر. حيث تقوم الدالة بحجز الذاكرة تلقائياً وقراءة محتويات الملف، وتمكنك من الوصول إلى محتويات هذا الملف من خلاله.
حجم (size): مؤشر على متغير من الصنف `صـحيح_متكيف` (ArchInt) يتم فيه كتابة حجم الملف الذي تم تحميله.

سنقوم الآن بقراءة الملف السابق من خلال الدالة اقرأ_ملف (readFile):

اشمل "مـتم/طـرفية"؛
اشمل "مـتم/نـم"؛
اشمل "مـتم/نـص"؛
استخدم مـتم؛
عرف مسار: نـص = "/home/ali/ملف_نصي.نص"؛
عرف ناتج: مؤشر[مـحرف]؛
عرف حجم: صـحيح_متكيف؛ // تعريف متغير يتم فيه تخزين عدد المحارف التي تتم قراءتها
نـم.اقرأ_ملف(مسار، ناتج~مؤشر، حجم~مؤشر)؛ // قراءة محتوى الملف
طـرفية.اطبع(ناتج)؛
import "Srl/Console.alusus";
import "Srl/Fs.alusus";
import "Srl/String.alusus";
use Srl;
def path: String = "/home/ali/textfile.txt";
def result: ptr[Char];
def count: ArchInt; // تعريف متغير يتم فيه تخزين عدد المحارف التي تتم قراءتها
Fs.readFile(path, result~ptr, count~ptr); // قراءة محتوى الملف
Console.print(result);

عند استخدام هذه الطريقة يجب على المستخدم تحرير الذاكرة بنفسه بعد الأنتهاء منها.