التعامل مع Streams والمجلدات في C#
بنهاية هذا الدرس ستتمكن من قراءة وكتابة الملفات وأشجار المجلدات (directory trees) في C# باستخدام الـ streams والـ adapters والـ decorators، وتوسيع تطبيق console يشبه الـ shell بأوامر جديدة للملفات والمجلدات.
الأهداف
- تشرح ما هو الـ stream ولماذا تقسم .NET الأنواع المرتبطة بالـ streams إلى backing stores و adapters و decorators.
- تقرأ وتكتب الملفات باستخدام
FileStream، وتختار القيم الصحيحة لـFileMode،FileAccess، وFileShare. - تحرر resources الـ stream بشكل deterministic باستخدام جملة
using، وتشرح علاقتها بـIDisposableوDispose(). - تفحص الملفات والمجلدات من خلال خصائص
FileInfoوDirectoryInfoمثلLength،Extension،LastWriteTime،Parent، وGetDirectoryRoot. - تستخدم الـ static convenience methods في
FileوDirectory(WriteAllText،WriteAllBytes،WriteAllLines،Move،Delete،GetFiles،GetDirectories) لتنفيذ مهام شائعة بنداء واحد. - تقرأ بيانات binary primitive باستخدام
BinaryReader/BinaryWriter، وتحول محتوى الـ stream باستخدام decorator مثلDeflateStream. - تعبر directory tree كاملا بشكل recursive لقياس حجمه، أو نسخه، أو البحث في ملفاته عن نص معين.
- توسع تطبيق console يشبه shell نظام Linux بأوامر جديدة مبنية على هذه الأنواع.
المتطلبات السابقة
- إنهاء تطبيق console الخاص بالدرس السابق (الـ Linux-shell emulator بأوامره
pwd،ls،mkdir،touch،rm،cat، وnano). - أساسيات C#: classes، static methods، جمل
switch، loops، وأنواع الـ exceptions. - .NET SDK يعمل بشكل صحيح، وIDE أو محرر نصوص يستطيع تشغيل مشروع C# console.
- فهم namespace
System.IOمن الدرس السابق. - تحويل النص الذي يدخله المستخدم إلى قيم typed باستخدام
int.Parse،double.Parse، وbool.Parse(كل واحدة منها ترميFormatExceptionإذا لم يطابق النص الشكل المتوقع).
ما هو الـ Stream
الـ stream هو سلسلة من الـ bytes تنتقل بين تطبيق ومصدر أو وجهة معينة: ملف على الـ disk، موقع على الشبكة، أو block في الذاكرة. في أي وقت يحتاج فيه برنامج إلى data موجودة خارج ذاكرته، مثل محتوى ملف أو رد من web request، تنقل .NET هذه الـ data عبر stream، chunk من الـ bytes في كل مرة. كل حرف يمكن رؤيته على الشاشة، حتى حرف واحد، مخزن على الـ disk كـ byte واحد أو أكثر: الحرف A مخزن كقيمة byte تساوي 65، والـ stream هو الذي يحمل هذا الـ byte من الـ disk إلى داخل البرنامج، أو من البرنامج رجوعا إلى الـ disk.
كل أنواع الـ stream في .NET ترث من الـ abstract class System.IO.Stream. هذا الـ base class يعرف العمليات التي يدعمها كل stream، مثل Read، Write، Seek، Close، وخصائص مثل CanRead، CanWrite، CanSeek، و Position. وبسبب أن كل concrete stream type يرث من Stream، فإن الكود الذي يعمل مع الـ base class يمكن أن يعمل مع ملف، أو block في الذاكرة، أو network connection، بالتبادل.
تصنيفات الـ Stream: Backing Stores، Adapters، و Decorators
تنظم .NET الأنواع المرتبطة بالـ stream في ثلاثة تصنيفات، ويستخدم هذا الدرس نوعا واحدا على الأقل من كل تصنيف:
- Backing stores تتصل مباشرة بمصدر خارجي.
FileStreamهو backing store للملفات؛ يقرأ ويكتب raw bytes. - Adapters تغلف backing store وتغير شكل الـ data دون أن تغير المصدر.
StreamReader/StreamWriterتحول byte stream إلى نص، وBinaryReader/BinaryWriterتحوله إلى قيم primitive مثلint،double، أوbool. - Decorators تحول المحتوى نفسه، مثلا تضغطه أو تشفره، وتعرض في الوقت نفسه نفس stream API.
DeflateStreamهو decorator يضغط الـ data وقت الكتابة ويفك ضغطها وقت القراءة؛ يعرض قسم "الـ Decorators وDeflateStream" أدناه مثالا كاملا عليه.
تقسيم الأنواع بهذا الشكل يجعل كل class مركزة على مهمة واحدة: backing store لا تعرف إلا كيف تحرك bytes داخل وخارج مصدرها، adapter لا تعرف إلا كيف تترجم bytes إلى شكل أغنى، وdecorator لا يعرف إلا كيف يحول المحتوى. هذا الفصل يعني أنه يمكن تبديل backing store (ملف اليوم، network connection غدا) دون إعادة كتابة كود الـ adapter المبني فوقه، وتبديل الـ adapter دون لمس backing store، وحتى إلحاق أكثر من decorator بنفس الـ stream في الوقت نفسه، لأن الـ decorator لا يحتاج إلا إلى stream آخر ليغلفه.
flowchart LR
Ext[External source: file, network, memory] --> BS[Backing store: FileStream]
BS --> AD1[Adapter: StreamReader / StreamWriter - text]
BS --> AD2[Adapter: BinaryReader / BinaryWriter - primitives]
BS --> DEC[Decorator: DeflateStream - compress / decompress]
AD1 --> App[Your application]
AD2 --> App
DEC --> AppFileStream، FileMode، FileAccess، و FileShare
FileStream هو backing store الذي يستخدم لفتح ملف على مستوى الـ byte. الـ constructor الخاص به يأخذ path مضافا إليه تركيبة من ثلاثة enumerators:
FileStream fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);FileModeتحدد ما يحصل للملف عند الفتح:Createتنشئ ملفا جديدا أو تكتب فوق الملف الموجود،CreateNewتفشل إذا كان الملف موجودا،Openتفشل إذا لم يكن الملف موجودا،OpenOrCreateتفتح الملف إذا كان موجودا وتنشئه إذا لم يكن،Appendتفتح الملف وتتحرك إلى آخره، وTruncateتفتح ملفا موجودا وتفرغه.FileAccessتحدد ما يسمح للـ stream بفعله بعد الفتح:Read،Write، أوReadWrite.FileShareتحدد ما يسمح لـ file handles أخرى، في نفس الـ process أو في process مختلفة، بفعله على نفس الملف بينما ملفك لا يزال مفتوحا.FileShare.Noneتحجز الملف بالكامل للـ stream الخاصة بك،FileShare.Readتسمح لغيرها بقراءته في الوقت نفسه، وFileShare.ReadWriteتسمح لغيرها بقراءته والكتابة فيه. اختيارFileShare.Noneعند كتابة data حساسة يمنع process آخر من قراءة ملف نصفه مكتوب.
بعد الفتح، تعرض FileStream خصائص CanRead، CanWrite، و CanSeek للتأكد من ما يدعمه الـ stream المفتوح فعلا، وتتبع الخاصية Position أين ستحدث عملية الـ read أو write التالية. تحرك Seek(offset, origin) هذا الموضع مباشرة، حيث origin قيمة من SeekOrigin؛ يقيس SeekOrigin.Begin الـ offset من بداية الـ stream، فـ Seek(0, SeekOrigin.Begin) هي الطريقة التي يعود بها البرنامج إلى بداية ملف لإعادة قراءة ما كتبه فعلا، دون إغلاق وفتح الـ stream من جديد. لا توجد أي مهمة في هذا الدرس تنادي Seek مباشرة.
تنظيف الـ Resources: IDisposable وجملة using
قبل أن يعمل برنامج C#، يترجم الـ compiler الخاص به الكود المصدري إلى intermediate language (IL) بدلا من ترجمته مباشرة إلى machine code؛ ينتج compiler كل لغة من لغات .NET نفس الـ IL، وهذا سبب أن الكود المكتوب بلغات .NET مختلفة يمكن أن ينادي بعضه بعضا. طبقة تسمى Common Language Runtime (CLR) تأخذ هذا الـ IL وتديره بالكامل: تحجز الذاكرة التي تحتاجها، تحجز مساحة الـ disk التي تحتاجها، وتحرر الذاكرة حين تنتهي الحاجة إليها. الكود الذي يعمل بهذه الطريقة، تحت سيطرة الـ CLR بالكامل، يسمى managed code، وcompiler من نوع just-in-time (JIT) هو الذي تستخدمه الـ CLR لترجمة الـ IL إلى machine code الذي يشغله نظام التشغيل فعلا، لحظة قبل تشغيل هذا الكود.
وبسبب أن الـ managed code يبقى تحت سيطرة الـ CLR طوال الوقت، يستطيع الـ garbage collector (GC) الخاص بالـ CLR تتبعه وتنظيفه أوتوماتيكيا: من الداخل، يشغل الـ GC مرورين على الأقل، الأول يمر على الـ objects لتحديد ما لم يعد accessible، والثاني يمر مرة أخرى ليجمعها ويحررها فعلا. بعض الكود، مع ذلك، ينادي خارج الـ managed pipeline بالكامل، مثل call يذهب مباشرة من الـ compiler إلى machine code دون أن يمر بالـ CLR. يسمى هذا الكود unmanaged code، ولا يستطيع garbage collector تنظيفه، لأنه لم يكن أصلا تحت إدارة الـ CLR. هذا بالضبط الموقف الذي وجد IDisposable من أجله: في أي وقت تغلف فيه class managed مثل FileStream resource unmanaged مثل file handle خاص بنظام التشغيل، تكون الـ class نفسها managed ويستطيع الـ GC استعادة ذاكرتها في نهاية المطاف، لكن الـ file handle الذي بداخلها ليس شيئا يمكن للـ GC تركه لوحده، فيجب على الـ class أن تعرض طريقة لتحريره بشكل deterministic.
فتح FileStream، أو أي adapter مبني فوق واحد منه مثل StreamReader، StreamWriter، BinaryReader، أو BinaryWriter، يقوم بأكثر من حجز ذاكرة: فهو يحجز resource خاصا بنظام التشغيل، وهو file handle، طالما بقي الـ object مفتوحا. إذا لم يحرر برنامج هذا الـ handle أبدا، فقد يبقى الملف مغلقا أمام processes أخرى، ويبقى الـ handle نفسه محجوزا إلى أن يستعيده نظام التشغيل في نهاية المطاف. هذا بالضبط سبب أن methods قراءة وكتابة الملفات الخاصة بالدرس السابق تنتهي بزوج من الـ calls الصريحة مثل sr.Close(); fs.Close(); أو w.Close(); fs.Close();: إغلاق الـ reader أو الـ writer أولا، ثم الـ stream التي تحته، يحرر الـ handle حين تنتهي الـ method من عملها معه.
تعطي .NET كل type يحمل هذا النوع من الـ resource عقد تنظيف standard يسمى IDisposable. كل class تطبق IDisposable تعرض method اسمها Dispose()، و FileStream، StreamReader، StreamWriter، BinaryReader، و BinaryWriter تطبقها جميعا، فنداء Dispose() على أي منها يقوم بنفس تنظيف Close(). جملة using هي feature في الـ compiler مبنية على هذا العقد: تغليف object في بلوك using، أو declaration بـ using، يخبر الـ compiler بأن ينادي Dispose() الخاصة بهذا الـ object أوتوماتيكيا حين يخرج الـ object من الـ scope، سواء أكمل الكود إلى النهاية أو خرج مبكرا بسبب exception. من الداخل، يعيد الـ compiler كتابة using كـ try/finally block مع نداء Dispose() موضوع في finally، وهذا ما يضمن حدوث التنظيف حتى إذا حدث خطأ في الوسط.
الشكلان الخاصان بـ using يظهران لاحقا في هذا الدرس. بلوك using يغلف scope الـ object بأقواس:
using (FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read))
{
// fs.Dispose() runs automatically here, at the closing brace
}Declaration بـ using، دون أقواس، ينظف الـ object في نهاية الـ block المحيط به بدلا من فعل ذلك داخل أقواس:
using StreamReader sr = new StreamReader("data.txt");
// sr.Dispose() runs automatically at the end of this methodأي شكل من الشكلين يستبدل نمط sr.Close(); fs.Close(); اليدوي الخاص بالدرس السابق بنفس التنظيف، يحدث أوتوماتيكيا وبأمان حتى إذا حدث exception في الوسط، فمن هذا الدرس فصاعدا يفتح كل object مرتبط بـ stream بـ using بدلا من إغلاقه باليد. إذا نسي using بالكامل، فإن الـ object لا يبقى دون تنظيف إلى الأبد: سيلاحظ garbage collector الخاص بـ .NET في نهاية المطاف أن الـ object غير accessible وسيستعيده عبر finalizer الخاص به، وهي method خاصة ينادي عليها الـ garbage collector على الـ object، وفق جدوله الخاص، لحظة قبل استعادة ذاكرة هذا الـ object، وهي method مختلفة عن Dispose() ولا ينادى عليها أبدا مباشرة من الكود الخاص بك. يحدث هذا التنظيف في أي وقت يصل فيه الـ garbage collector إليه بدلا من أن يحدث فورا، وهذا سبب أن using هي الطريقة الموصى بها لتحرير resource حين ينتهي الكود من العمل معه، بدلا من تركه للصدفة.
تطبيق IDisposable على الـ Class الخاصة بك
كل type تطبق IDisposable استخدمناها حتى الآن هي type كتبتها .NET مسبقا (FileStream، StreamReader، StreamWriter، BinaryReader، BinaryWriter)؛ هذا الدرس فقط يستخدمها عن طريق using. الـ class التي تكتبها أنت تحتاج نفس العقد مطبقا باليد في أي وقت تحمل فيه resource من المفترض أن تتحرر بشكل deterministic بدلا من تركها للـ garbage collector.
النمط الـ standard يقسم التنظيف إلى Dispose() public، وهي التي ينادي عليها الـ caller، و protected virtual void Dispose(bool disposing)، وهي التي تقوم بالعمل الفعلي وتكون مشتركة مع الـ finalizer:
public class ResourceHolder : IDisposable
{
private FileStream resource;
private bool disposed = false;
public ResourceHolder(string path)
{
resource = new FileStream(path, FileMode.OpenOrCreate);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposed)
{
return;
}
if (disposing)
{
resource.Dispose();
}
disposed = true;
}
~ResourceHolder()
{
Dispose(false);
}
}- الـ constructor،
public ResourceHolder(string path)، يفتح الـ resource التي تكون هذه الـ class مسؤولة عنها ويخزنها في field الـ privateresource، فهذا الـ field هو الذي تحررهDisposeبعد ذلك. Dispose()هي method يشغلها الـ caller، مباشرة أو عن طريق بلوكusing. تناديDispose(true)لتقوم بالتنظيف الفعلي، ثم تناديGC.SuppressFinalize(this)لتخبر garbage collector بأن الـ finalizer أدناه لا يحتاج إلى العمل مرة أخرى، لأن الـ resource قد تحررت بالفعل.- الـ field
disposedيحمي من التنظيف مرتين: إذا نودي علىDispose()مرة أخرى بأي طريقة، أو إذا كانت قد نودي عليها من قبل وعمل الـ finalizer بعدها أيضا، فإن المحاولة الثانية ترجع فورا بدلا من تنظيف resource قد تم تنظيفها فعلا. Dispose(bool disposing)تحتوي منطق التنظيف المشترك ويصل إليها من مسارين مختلفين، وهذا ما يميز بينهما الـ parameterdisposing. حين تنادى منDispose()، تكونdisposingبقيمةtrue، ويكون آمنا تنظيف objects managed أخرى مثلresource. حين تنادى من الـ finalizer، تكونdisposingبقيمةfalse، لأنه حين يعمل الـ finalizer، قد تكون objects managed أخرى قد جمعت بالفعل، فلا يكون آمنا هنا إلا تحرير resources خارج إدارة ذاكرة .NET.~ResourceHolder()هي الـ finalizer (تسمى أيضا destructor): ينادي عليها garbage collector وفق جدوله الخاص كـ safety net أخير، في حالة أن شيئا يحملResourceHolderلم ينادDispose()أبدا. تناديDispose(false)، فيحدث التنظيف مع ذلك، لكن في وقت أبعد ومحدودا فقط بما يبقى آمنا لمسه في ذلك الوقت.
توجد أربع طرق يمكن بها تشغيل هذا التنظيف، من الأقل إلى الأكثر ثقة: نداء Dispose() باليد والأمل في أن كل مسار في الكود يصل إليه؛ تغليف الكود في try/catch/finally مع وضع نداء Dispose() في بلوك finally ليعمل حتى بعد exception؛ تغليف الـ object في بلوك using؛ أو declaration بـ using. الطريقتان الأخيرتان مفضلتان لأن الـ compiler يضمن أن النداء يحدث مرة واحدة بالضبط، في المكان الصحيح، دون الاعتماد على أن يتذكر المبرمج كتابته في كل مسار.
خصائص FileInfo و DirectoryInfo
FileInfo و DirectoryInfo كلاهما يرثان من FileSystemInfo ويصفان ملفا أو مجلدا واحدا دون فتح stream عليه. هما الأداة الصحيحة في أي وقت تحتاج فيه إلى metadata بدلا من المحتوى.
تعرض FileInfo خصائص FullName (الـ path الكامل)، Name (اسم الملف فقط)، Extension (شاملة النقطة التي تسبقها، مثلا .txt)، Length (الحجم بالـ bytes)، LastWriteTime (وقت آخر تغيير في محتوى الملف)، LastAccessTime، CreationTime، و Directory (نسخة DirectoryInfo للمجلد الذي يحتويه).
تعرض DirectoryInfo خصائص FullName، Name، Parent (نسخة DirectoryInfo للمجلد الذي يحتويه، أو null عند جذر الـ drive)، CreationTime، LastAccessTime، وmethods مثل GetFiles() و GetDirectories() تعيدان الـ entries الموجودة داخل المجلد مباشرة. Directory.GetDirectoryRoot(path) هي method static ذات صلة تعيد جذر الـ drive الذي يوجد عليه path معين (مثلا C:\).
FileInfo fi = new FileInfo("data.txt");
Console.WriteLine("Extension : {0}", fi.Extension);
Console.WriteLine("Size : {0} bytes", fi.Length);
Console.WriteLine("Modified : {0}", fi.LastWriteTime);ينشئ هذا القسم FileInfo لـ data.txt دون فتح الملف، ثم يقرأ ثلاثا من خصائصه: Extension تعطي امتداد الملف، Length تعطي حجمه، و LastWriteTime تعطي timestamp آخر كتابة. لا يلمس أي سطر من هذه الأسطر الثلاثة محتوى الملف الفعلي.
يمكن أيضا استخدام DirectoryInfo لقراءة access control list الخاصة بالمجلد، وهي مجموعة القواعد التي تحدد أي users أو groups مسموح لهم بالقراءة أو الكتابة أو التشغيل فيه، عن طريق GetAccessControl(). إدارة هذه الـ permissions خارج نطاق هذا الدرس ولن تغطى أكثر من ذلك.
الـ Static Methods المختصرة في File و Directory
إلى جانب classes الـ stream، تعرض File و Directory static methods تقوم بعملية كاملة بنداء واحد، وتفتح وتغلق من الداخل أي stream تحتاجها.
في File: File.Exists(path)، File.Delete(path)، File.Move(source, destination) (التي تنقل وتغير الاسم معا، لأن تغيير الاسم هو نقل داخل نفس المجلد فقط)، File.WriteAllText(path, text) و File.ReadAllText(path)، File.WriteAllLines(path, lines) و File.ReadAllLines(path)، و File.WriteAllBytes(path, bytes) و File.ReadAllBytes(path). تقوم الأزواج الثلاثة WriteAll*/ReadAll* بنفس العمل، فتحا للـ stream وكتابة أو قراءة كل شيء وإغلاقها، لكن لشكل مختلف من الـ data: block واحد من النص، أو array من السطور، أو array من الـ raw bytes.
في Directory: Directory.CreateDirectory(path)، Directory.Exists(path)، Directory.Move(source, destination)، Directory.Delete(path) (التي ترمي exception إذا لم يكن المجلد فارغا)، Directory.Delete(path, true) (التي تحذف المجلد وكل ما بداخله بشكل recursive)، Directory.GetLogicalDrives() (حروف الـ drives، أو الـ roots المحمولة على نظام غير Windows)، Directory.GetCreationTime(path)، Directory.GetLastAccessTime(path)، و Directory.GetLastWriteTime(path). Directory.GetFileSystemEntries(path, pattern) هي method ذات صلة تعيد الملفات والمجلدات الفرعية المطابقة لـ pattern بنداء واحد، لوقت تحتاج فيه task إلى كل entry معا بدلا من سرد الملفات والمجلدات كل على حدة.
تقبل Directory.GetFiles و Directory.GetDirectories كلاهما search pattern اختياريا و SearchOption:
string[] textFiles = Directory.GetFiles(path, "*.txt", SearchOption.AllDirectories);الـ argument الأول هو المجلد الذي سيتم البحث فيه، والثاني هو wildcard pattern الذي يجب أن يطابقه اسم الـ entry، والثالث يحدد إذا كان البحث في المجلد المعطى فقط (SearchOption.TopDirectoryOnly) أو في كل subdirectory متداخل أيضا (SearchOption.AllDirectories).
تعيد Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments) الـ path الكامل لمجلد معروف من النظام، مثل مجلد Documents الخاص بالمستخدم الحالي، دون الحاجة إلى كتابة path ثابت سيكون خاطئا على جهاز آخر أو نظام تشغيل آخر.
Adapters النص: StreamWriter و StreamReader
تحول StreamWriter و StreamReader byte stream إلى نص: تحول StreamWriter.Write/WriteLine الـ string إلى bytes وترسلها إلى الـ stream التي تحتها، وتسحب StreamReader.ReadLine/ReadToEnd bytes من الـ stream وتعيدها كـ string. تقوم كل من الـ classes بعمل buffer للـ data من الداخل، وهذا سبب أهمية StreamWriter.Flush(): فهي تفرض إخراج أي نص لا يزال في الـ buffer إلى الـ stream التي تحتها فورا، بدلا من الانتظار حتى يمتلئ الـ buffer أو يغلق الـ writer.
يمكن لـ StreamReader أيضا قراءة حرف واحد في كل مرة بدلا من سطر كامل: تنظر sr.Peek() إلى الحرف التالي دون استهلاكه، وتعيد -1 حين لا يبقى شيء في الـ stream، وتستهلك sr.Read() وتعيد الحرف التالي كـ int، وهذا سبب أنها تحول عادة إلى char قبل الطباعة. loop مثل while (sr.Peek() > -1) { Console.Write((char)sr.Read()); } يطبع محتوى ملف كامل حرفا بحرف، وهو بديل أقل level من نداء ReadLine() داخل loop.
Adapters الـ Binary: BinaryWriter و BinaryReader
النص ليس الشكل الوحيد للـ data. تكتب BinaryWriter قيم primitive، مثل int، double، و bool، مباشرة كـ raw bytes خاصة بها بدلا من حروف مقروءة، وتعيدها BinaryReader بنفس الترتيب الذي كتبت به.
using (FileStream fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write))
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Write(20);
writer.Write(101.5);
writer.Write(true);
}تفتح FileStream ملف data.bin للكتابة، وتغلفها BinaryWriter. كل نداء writer.Write(...) يكتب الـ byte representation الدقيق للـ argument الخاصة به: 4 bytes للـ int، 8 bytes للـ double، وbyte واحد للـ bool. يجب أن تحدث قراءة القيم رجوعا بنفس الترتيب، باستخدام الـ method المطابقة لنوعها، عن طريق BinaryReader تغلف FileStream مفتوحة بالطريقة نفسها لكن بـ FileMode.Open و FileAccess.Read بدلا من الأولى:
using (FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read))
using (BinaryReader reader = new BinaryReader(fs))
{
int i = reader.ReadInt32();
double d = reader.ReadDouble();
bool b = reader.ReadBoolean();
}هنا تفتح FileStream ملف data.bin الموجود للقراءة بدلا من إنشائه، وتغلفها BinaryReader بالطريقة نفسها التي غلفتها بها BinaryWriter في جهة الكتابة. كل من reader.ReadInt32()، و reader.ReadDouble()، و reader.ReadBoolean() تعيد نفس النوع الذي كتب، بنفس ترتيب الكتابة، وتعيد قيم الـ int، double، و bool الأصلية.
بسبب أن ملف binary يخزن raw bytes بدلا من حروف، فإن فتحه في text editor يظهر رموزا غير مقروءة بدلا من الأرقام التي كتبت؛ ولا يكون للـ data معنى إلا حين تقرأ رجوعا عن طريق BinaryReader تعرف الترتيب الدقيق للأنواع المتوقعة.
الـ Decorators و DeflateStream
على عكس الـ adapter، الذي يغير فقط شكل الـ data (bytes إلى نص، bytes إلى primitives)، يغير الـ decorator المحتوى نفسه ويعرض في الوقت نفسه نفس API القراءة والكتابة الخاصة بأي stream آخر. DeflateStream هو decorator يضغط الـ data وقت الكتابة ويفك ضغطها وقت القراءة، ويغلف أي stream يعطى له، عادة FileStream، بالطريقة نفسها التي تغلف بها StreamWriter أو BinaryWriter واحدة منها. وبسبب أن الـ decorator لا يحتاج إلا إلى stream آخر ليغلفه، يمكن إلحاق أكثر من decorator بنفس الـ stream في الوقت نفسه، مثل decorator ضغط يغلف decorator تشفير يغلف FileStream، وهذا أمر لم يبن الـ adapter لفعله.
تقرأ DeflateStream وتكتب bytes فقط. يكتب المثال أدناه قيمتين byte حرفيتين مباشرة بـ WriteByte، وهي نفس قيم الـ bytes الخاصة بحرفي A و B، لكي يبقى التركيز على الـ decorator نفسه. تتوسع المهمة 5 أدناه في هذه الفكرة أكثر من المثال: بدلا من قيمتين byte ثابتتين، تضغط سطرا كاملا من النص الذي يدخله المستخدم. وكما أثبت قسم "ما هو الـ Stream" أعلاه فعلا، كل حرف هو في الحقيقة قيمة byte رقمية تحت السطح (الحرف A هو قيمة byte تساوي 65)، فتلف المهمة 5 على كل حرف في النص المدخل وتكتب قيمة الـ byte الخاصة به بـ ds.WriteByte((byte)c)، حرفا واحدا في كل مرة، بدلا من كتابة قيمتين ثابتتين. وتطبق قراءتها رجوعا نفس الفكرة بالعكس: يحول كل byte تم فك ضغطه رجوعا إلى حرف بـ (char)b ويلحق بالنتيجة.
using (FileStream fs = new FileStream("data.bin", FileMode.Create, FileAccess.Write))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
{
ds.WriteByte(65);
ds.WriteByte(66);
}تنشئ FileStream ملف data.bin، وتغلفها DeflateStream بـ CompressionMode.Compress، فيضغط كل byte يكتب عبر ds قبل أن يصل إلى الملف. تكتب WriteByte(65) و WriteByte(66) قيم الـ byte الخام لحرفي A و B. فتح data.bin بعد ذلك بأمر cat يظهر bytes غير مقروءة بدلا من حرفي A و B، لأن محتوى الملف الفعلي على الـ disk مضغوط.
تحتاج قراءة الـ data رجوعا إلى DeflateStream أخرى، مفتوحة بـ CompressionMode.Decompress، فوق FileStream تفتح نفس الملف للقراءة:
using (FileStream fs = new FileStream("data.bin", FileMode.Open, FileAccess.Read))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
{
int b;
while ((b = ds.ReadByte()) != -1)
{
Console.WriteLine(b);
}
}تعيد ds.ReadByte() الـ byte التالي بعد فك ضغطه كـ int، أو -1 حين لا يبقى شيء يقرأ. الطول بعد فك الضغط لـ stream معينة غير معروف مسبقا، فيستمر هذا الـ loop في قراءة byte بعد byte إلى أن تشير -1 إلى نهايته، بدلا من محاولة قراءة عدد ثابت من الـ bytes من البداية. تشغيل هذا الكود على الملف المكتوب أعلاه يطبع 65 ثم 66، وهما نفس قيمتي الـ byte التي كتبت قبل الضغط، وهذا يؤكد أن الـ data تعود صحيحة عبر الـ decorator حتى إذا لم يكن الملف على الـ disk أبدا في شكل مقروء.
خوارزميات Recursive على المجلدات
ثلاث مهام شائعة (قياس حجم مجلد بالكامل، ونسخ directory tree، والبحث في كل ملف في tree عن نص معين) تتبع جميعها نفس الشكل: النظر إلى كل ما هو داخل المجلد الحالي مباشرة، ثم تكرار العملية لكل subdirectory.
يجمع قياس الحجم Length الخاصة بكل ملف موجود بـ SearchOption.AllDirectories، فيستبدل نداء واحد walk recursive باليد:
long size = 0;
foreach (FileInfo fi in dirInfo.GetFiles("*", SearchOption.AllDirectories))
{
size += fi.Length;
}تعيد dirInfo.GetFiles("*", SearchOption.AllDirectories) كل ملف تحت dirInfo في أي عمق، كـ array واحد flat من objects FileInfo، فلا تحتاج الـ method أبدا إلى السير داخل الـ subdirectories بنفسها. يضيف الـ loop بعد ذلك Length الخاصة بكل ملف إلى size، فحين ينتهي، تحمل size حجم الـ byte الكلي للـ tree بالكامل.
لا يمكن أن يكون نسخ tree نداء واحد، لأن نسخ ملف واحد فقط ينسخ هذا الملف ولا يعرف شيئا عن الـ subdirectories، فيجب كتابة الـ recursion بشكل صريح: نسخ كل ملف في المجلد الحالي، ثم نداء نفس الـ method مرة أخرى لكل subdirectory. يستخدم نسخ ملف واحد FileInfo.CopyTo:
string destPath = Path.Combine(destDir, file.Name);
file.CopyTo(destPath, false);تنسخ file.CopyTo(destPath, overwrite) الملف الذي تشير إليه file، إلى destPath. الـ argument الثاني يحدد ما يحصل إذا كان ملف موجودا هناك بالفعل: false تجعل CopyTo ترمي exception بدلا من لمسه، و true تكتب فوقه بصمت. تنادي DirectoryCopy (تبنى في المهمة 6، تظهر كاملة في القائمة الكاملة للبرنامج أدناه) CopyTo بـ false لكل ملف تجده، وهذا سبب أن تشغيل cp -r مرة أخرى في نفس الوجهة يفشل عند أول ملف مكرر بدلا من الكتابة فوق النسخة الأولى بصمت.
تأخذ DirectoryCopy parameter ثالثا، bool copySubDirs = true، إلى جانب source و dest. هذا الـ flag هو ما تفحصه الخطوة الـ recursive أدناه قبل نسخ الـ subdirectories أصلا: حين تكون true، تنادي DirectoryCopy نفسها مرة أخرى لكل subdirectory تجده، وحين تكون false، تنسخ فقط الملفات داخل source مباشرة وتتوقف. الـ default الخاصة بها true يعني أن نداء عاديا مثل DirectoryCopy(source, dest) ينسخ الـ tree بالكامل دون الحاجة إلى ذكر الـ flag؛ يحتاج الـ caller فقط إلى تمرير false صريحة للحصول على نسخة shallow بالملفات فقط. أمر cp -r الخاص بهذا الدرس ينسخ دائما الـ tree بالكامل، فيستخدم دائما الـ default true، لكن الـ parameter موجودة لكي تدعم نفس الـ method بعد ذلك نسخا غير recursive دون إعادة كتابتها.
flowchart TD
A[DirectoryCopy source, dest, copySubDirs = true] --> B{Source exists?}
B -->|No| C[Throw DirectoryNotFoundException]
B -->|Yes| D[Create dest if missing]
D --> E[Copy every file in source to dest]
E --> F{copySubDirs is true?}
F -->|No| G[Done]
F -->|Yes| H[For each subdirectory: call DirectoryCopy again]
H --> Gيتبع البحث عن نص في محتوى الملفات نفس الشكل الـ recursive، لكن بدلا من نسخ bytes يفتح كل ملف بـ StreamReader، ويقرأ نصه بالكامل بـ ReadToEnd()، ويفحص إذا كان هذا النص Contains نص البحث.
مهام المعمل
توسع المهام أدناه command loop الخاصة بـ shell emulator من الدرس السابق، أمرا واحدا في كل مرة. نتيجة تطبيق كل مهمة، تتوفر command loop الكاملة مع كل method تنادى منها، دفعة واحدة، في قسم "القائمة الكاملة للبرنامج" في آخر هذه الصفحة؛ استخدمها كـ reference implementation وأنت تبني تطبيقك بنفسك.
المهمة 1: معلومات أوسع عن الملفات والمجلدات
- حدث
pwdبحيث تطبع، بالإضافة إلى اسم المجلد الكامل، جذر الـ drive أيضا باستخدامDirectory.GetDirectoryRoot(currentDir). - حدث
lsبحيث يطبع كل سطر ملفfile.Extensionأيضا إلى جانبfile.Lengthوالـ timestamp الموجودين بالفعل، باستخدامfile.LastWriteTimeبدلا منLastAccessTime. - أضف أمرا جديدا
stat. يجب أن يطلب اسما، ثم يفحصFile.Exists(name)أولا وDirectory.Exists(name)ثانيا لتحديد أي فرع يشغل، ويطبع error ويتوقف إذا لم يكن أي منهما موجودا. إذا كان ملفا، اطبع، بشكلLabel : valueفي كل سطر:Full name(FullName)،Extension،Size(Length، متبوعة بـbytes)،Last write time(LastWriteTime)، وDirectory(المجلد الحاوي منfi.Directory، وهيDirectoryInfo، علىFileInfo). إذا كان مجلدا، اطبع، بنفس شكلLabel : value:Full name(FullName)،Parent(منDirectoryInfo)،Drive root(منDirectory.GetDirectoryRoot)، والـ3 timestampsCreated،Last access time، وLast write time(منDirectory.GetCreationTime،Directory.GetLastAccessTime، وDirectory.GetLastWriteTime). - شغل البرنامج، أنشئ ملفا تجريبيا بـ
touch demo.txt، ثم شغلpwdوتأكد من أن سطر جذر الـ drive ظاهر، ثم شغلlsوتأكد من أن عمود الامتداد ظاهر إلى جانبdemo.txt. - شغل
statوأدخلdemo.txtلتجربة فرع الملف، ثم شغلstatمرة أخرى وأدخل.لتجربة فرع المجلد على المجلد الحالي.
الناتج المتوقع للخطوة 4، pwd:
Full name : <full path of the current directory>
Drive root : <drive root, for example / on Linux>الناتج المتوقع للخطوة 4، ls:
demo.txt .txt 0 bytes <today's date and time>الناتج المتوقع للخطوة 5، فرع الملف:
Full name : <full path to demo.txt>
Extension : .txt
Size : 0 bytes
Last write time : <today's date and time>
Directory : <full path to the containing directory>الناتج المتوقع للخطوة 5، فرع المجلد:
Full name : <full path of the current directory>
Parent : <full path of the parent directory>
Drive root : <drive root, for example / on Linux>
Created : <today's date and time>
Last access time : <today's date and time>
Last write time : <today's date and time>المهمة 2: إعادة تسمية وحذف Trees كاملة
- أضف أمرا
mv. يفترض أن يعيش منطقه في method اسمهاMv()، بالطريقة نفسها التي تسمي بها المهمة 6 method الـ recursive الخاصة بهاDirectoryCopy. يجب أن تطلب الاسم الحالي والاسم الجديد، ثم تفحص إذا كان المصدر ملفا (استخدمFile.Move) أو مجلدا (استخدمDirectory.Move)، وتطبعFile moved to <new name>أوDirectory moved to <new name>حسب الحالة، أو تعرض error إذا لم يكن أي منهما موجودا. - أضف أمرا
rm -r، مطابقا كسطر كاملrm -r. يجب أن يطلب اسم مجلد، ويتأكد من وجوده، ثم يناديDirectory.Delete(name, true)لحذفه مع كل ما بداخله، ويطبعDirectory and everything inside it was deleted. - جرب
mv: شغلmkdir olddir، ثمmv، وأدخلolddirوnewdir. شغلlsللتأكد من أنolddirقد اختفى وأنnewdirموجود. - جرب
rm -r: شغلmkdir tree، ثم، خارج الـ emulator (في file manager الخاص بنظام التشغيل الخاص بك أو terminal آخر)، ضع ملفا واحدا داخل مجلدtreeالجديد لكي لا يبقى فارغا. عد إلى الـ emulator، وشغلrm -rوأدخلtree. هذا يؤكد أنDirectory.Delete(name, true)تحذف مجلدا غير فارغ، وهو أمر ترفضDirectory.Delete(name)العادية، غير الـ recursive، فعله.
الناتج المتوقع بعد الخطوة 3 (نقل olddir، وهو مجلد، فيشغل فرع المجلد الخاص بـ Mv()):
Directory moved to newdirالناتج المتوقع بعد الخطوة 4، بينما لا يزال المجلد يحتوي على محتوى:
Directory and everything inside it was deleted.المهمة 3: عمليات نص مختصرة بـ File.WriteAllLines و File.WriteAllBytes
- أضف أمرا
writelines. يجب أن يطلب اسم ملف، ثم يقرأ سطورا من الـ console إلى أن يدخل سطر فارغ، وأخيرا يناديFile.WriteAllLines(name, lines). - أضف أمرا
readlines. يجب أن يتأكد من وجود الملف، ويناديFile.ReadAllLines(name)، ويطبع كل سطر مع رقمه بترتيب يبدأ من 1. - شغل
writelines، سم الملفnotes.txt، وأدخل ثلاثة سطور متبوعة بسطر فارغ لتتوقف. - شغل
readlinesعلىnotes.txt.
الناتج المتوقع للخطوة 4:
1: first line you typed
2: second line you typed
3: third line you typedلاحظ أن File.WriteAllText (string واحدة)، و File.WriteAllLines (array أو list من الـ strings، سطر في كل عنصر)، و File.WriteAllBytes (byte array خام) تتبع جميعها النمط نفسه بالضبط: تمرير path والـ data، وتقوم الـ method بفتح الـ stream التي تحتها وكتابتها وإغلاقها من أجلك. أي واحدة تستخدم تعتمد فقط على شكل الـ data الموجودة عندك بالفعل.
المهمة 4: بيانات Binary ومشاركة الملفات
- أضف أمرا
binwrite. يجب أن يطلب اسم ملف، ثم يطلب integer، ورقما decimal، وقيمةtrue/falseكنص ويحول كل واحدة بـint.Parse،double.Parse، وbool.Parseعلى الترتيب، ثم يفتحFileStreamبـFileMode.Create،FileAccess.Write، وFileShare.None، ويغلفها بـBinaryWriter، ويكتب القيم الثلاث المحولة بهذا الترتيب. - أضف أمرا
binread. يجب أن يتأكد من وجود الملف، ويفتحFileStreamبـFileMode.Open،FileAccess.Read، وFileShare.Read، ويغلفها بـBinaryReader، ويقرأ رجوعا الـ integer، والرقم decimal، والـ boolean بنفس ترتيب كتابتهم، ويطبعها واحدة في كل سطر بشكلLabel : value:Integer،Decimal، وBoolean. - شغل
binwrite، سم الملفnumbers.bin، وأدخل20،101.5، وtrue. - شغل
binreadعلىnumbers.bin. - جرب فتح
numbers.binبـcat. لاحظ أن المحتوى ليس نصا مقروءا.
الناتج المتوقع للخطوة 4:
Integer : 20
Decimal : 101.5
Boolean : Trueاشرح بكلامك الخاص، بناء على المفاهيم أعلاه، لماذا تفتح binwrite الملف بـ FileShare.None بدلا من FileShare.ReadWrite.
المهمة 5: الـ Decorators: الضغط وفك الضغط بـ DeflateStream
- أضف أمرا
compress. يجب أن يطلب اسم ملف وسطر نص، ويفتحFileStreamبـFileMode.Create، ويغلفها بـDeflateStreamبـCompressionMode.Compress، ثم يلف على كل حرفcفي النص المدخل ويكتبه بـds.WriteByte((byte)c). - أضف أمرا
decompress. يجب أن يتأكد من وجود الملف، ويفتحFileStreamبـFileMode.Open، ويغلفها بـDeflateStreamبـCompressionMode.Decompress، ويقرأ bytes واحدا في كل مرة بـReadByte()إلى أن تعيد-1، ويحول كل واحدة رجوعا بـ(char)bويلحقها بـ result string، ثم يطبع النتيجة. - شغل
compress، سم الملفmessage.bin، وأدخلhello world. - شغل
catعلىmessage.binولاحظ أن المحتوى ليس نصا مقروءا. - شغل
decompressعلىmessage.bin.
الناتج المتوقع للخطوة 5:
hello worldالمهمة 6: البحث والقياس ونسخ Trees المجلدات
- أضف أمرا
drivesيطبع كل entry تعيدهاDirectory.GetLogicalDrives(). - أضف أمرا
ls -r، مطابقا كسطر كاملls -r. يجب أن يطلب search pattern (بـ default*إذا كان الإدخال فارغا)، ثم يسرد كل ملف مطابق تحت المجلد الحالي باستخدامDirectory.GetFiles(currentDir, pattern, SearchOption.AllDirectories)، متبوعا بكل subdirectory باستخدامDirectory.GetDirectories(currentDir, "*", SearchOption.AllDirectories). اطبعMatching files:في سطر منفرد، ثم كل path كامل لملف مطابق في سطر منفرد؛ ثم اطبعSubdirectories:في سطر منفرد، ثم كل path كامل لـ subdirectory في سطر منفرد. - أضف أمرا
find. يجب أن يطلب مجلدا ونص بحث، ثم، لكل ملف تعيدهDirectory.GetFiles(dir, "*", SearchOption.AllDirectories)، يفتحه بـStreamReader، ويقرأ نصه الكامل، ويطبع path الملف إذا كان النص يحتوي نص البحث، وفي النهاية يطبع<count> file(s) matched.بعدد الملفات المطابقة. - أضف أمرا
du. يجب أن يطلب مجلدا، ثم يجمعfi.LengthلكلFileInfoتعيدهاdirInfo.GetFiles("*", SearchOption.AllDirectories)ويطبع الإجمالي بشكلTotal size: <n> bytes. - أضف أمرا
cp -r، مطابقا كسطر كاملcp -r. يجب أن يطلب مجلد مصدر ومجلد وجهة، ثم ينادي method الـDirectoryCopyالـ recursive الموصوفة أعلاه (مع تركcopySubDirsعلى الـ default الخاصة بهاtrue)، التي تنشئ الوجهة إذا لم تكن موجودة، وتنسخ كل ملف في المجلد الحالي، وبسبب أنcopySubDirsبقيمةtrue، تنادي نفسها مرة أخرى لكل subdirectory. اطبعCopy finished.حين تعود. - أضف أمرا
homeيطبعEnvironment.GetFolderPath(Environment.SpecialFolder.MyDocuments). - خارج الـ emulator (لا يوجد في الـ emulator أمر
cd، فبناء محتوى متداخل أسهل من file manager الخاص بنظام التشغيل الخاص بك أو terminal آخر)، أنشئ مجلدا اسمهtree، يحتوي على ملفين نصيين وسبفولدر واحد اسمهnotes، وهذا السبفولدر نفسه يحتوي على ملف نص ثالث اسمهgreeting.txtتحتوي على كلمة "hello" بداخله. عد إلى الـ emulator، شغلduعلى path مجلدtree، ثم شغلcp -r، وأدخل path مجلدtreeكمصدر وpath وجهة ينتهي بـtree-copy(مثلا، نفس المجلد الأب متبوعا بـtree-copy)، ثم شغلfindعلى مجلدtree-copyبحثا عن كلمة "hello".
الناتج المتوقع للخطوة 7، du على tree (الرقم بالضبط يعتمد على حجم الملفين النصيين اللذين أنشأتهما):
Total size: <n> bytesالناتج المتوقع للخطوة 7، cp -r:
Copy finished.الناتج المتوقع للخطوة 7، find (على افتراض أن كلمة "hello" ظاهرة في ملف واحد بالضبط):
<full path to tree-copy/notes/greeting.txt>
1 file(s) matched.الخلاصة
- الـ stream هو قناة byte-oriented بين تطبيق ومصدر خارجي؛ كل نوع stream فعلي في .NET يرث من الـ abstract class
System.IO.Stream. - تفصل .NET أنواع الـ stream إلى backing stores تلمس المصدر الخارجي مباشرة (
FileStream)، وadapters تعيد تشكيل الـ bytes إلى شيء أكثر فائدة دون الحاجة إلى معرفة أو الاهتمام بأي backing store تحتها (StreamReader/StreamWriterللنص،BinaryReader/BinaryWriterللـ primitives)، وdecorators (DeflateStream) تحول المحتوى نفسه وتعرض في الوقت نفسه نفس API الـ stream. - تحدد
FileMode،FileAccess، وFileShareمعا كيف تفتحFileStreamالملف، وما يسمح بفعله به، وما يسمح لـ handles أخرى بفعله به في الوقت نفسه. - الـ objects المرتبطة بالـ stream تحمل resource خاصة بنظام التشغيل يجب أن تتحرر؛ تنادي جملة
usingDispose()الخاصة بـ object من نوعIDisposableأوتوماتيكيا في نهاية الـ scope الخاصة بها (تتحول إلىtry/finally)، وتستبدل نداءاتClose()اليدوية الخاصة بالدرس السابق بنمط يعمل بأمان أيضا إذا حدث exception. - الـ class التي تكتبها أنت تطبق
IDisposableبـDispose()public تناديprotected virtual Dispose(bool disposing)مشتركة ثمGC.SuppressFinalize(this)، محمية بـ flagdisposedمن التنظيف مرتين؛ تنادي finalizer (~ClassName()) نفس الـ method كـ safety net أخير لـ resource لم ينظفها أحد صريحا. - تصف
FileInfoوDirectoryInfoملفا أو مجلدا (الحجم، الامتداد، الـ timestamps، الأب، الـ path الكامل) دون فتح stream لقراءة محتواه. - تغطي الـ static methods في
FileوDirectory(WriteAllText/WriteAllLines/WriteAllBytes،Move،Delete،Delete(path, true)،GetFiles/GetDirectoriesمع pattern وSearchOption) أغلبية مهام الملفات والمجلدات اليومية بنداء واحد لكل منها. - الخوارزميات الـ recursive على directory tree (قياس الحجم الكلي، ونسخ tree، والبحث في محتوى الملفات) تتشارك جميعها نفس الشكل: التعامل مع ما هو داخل المجلد الحالي مباشرة، ثم التكرار لكل subdirectory.
- الحفاظ على نواة الـ Linux-shell emulator (
pwd،ls،mkdir،touch،rm،cat،nano) وتوسيعها بـmv،rm -r،stat،writelines،readlines،binwrite،binread،compress،decompress،drives،ls -r،find،du،cp -r، وhomeيوضح إلى أي مدى يمكن أن تصل building blocks الصغيرة الخاصة بـ namespaceSystem.IO.
القائمة الكاملة للبرنامج
القائمة أدناه هي تطبيق console الكامل بكل أمر من هذا الدرس موصولا في command loop واحدة، شاملة الأوامر الموروثة من الدرس السابق (pwd، ls، mkdir، touch، rm، cat، nano) والأوامر المضافة في المهام أعلاه.
namespace LapExample;
public class Program
{
public static void Main(string[] args)
{
bool exit = false;
while (!exit)
{
Console.Write("student@SDOps:~$ ");
string input = Console.ReadLine() ?? "";
switch (input.Trim())
{
case "exit":
exit = true;
break;
case "pwd":
Pwd();
break;
case "ls":
Ls();
break;
case "ls -r":
LsRecursive();
break;
case "mkdir":
MkDir();
break;
case "touch":
Touch();
break;
case "rm":
Rm();
break;
case "rm -r":
RmRecursive();
break;
case "mv":
Mv();
break;
case "cat":
Cat();
break;
case "nano":
Nano();
break;
case "nano-append":
NanoAppend();
break;
case "stat":
Stat();
break;
case "writelines":
WriteLines();
break;
case "readlines":
ReadLines();
break;
case "binwrite":
BinWrite();
break;
case "binread":
BinRead();
break;
case "strdemo":
StrDemo();
break;
case "drives":
Drives();
break;
case "find":
Find();
break;
case "du":
Du();
break;
case "cp -r":
CpDir();
break;
case "home":
Home();
break;
default:
Console.WriteLine("Unknown command.");
break;
}
}
}
// ----- Directory metadata -----
public static void Pwd()
{
var currentDir = Directory.GetCurrentDirectory();
DirectoryInfo dirinfo = new DirectoryInfo(currentDir);
Console.WriteLine("Full name : {0}", dirinfo.FullName);
Console.WriteLine("Drive root : {0}", Directory.GetDirectoryRoot(currentDir));
}
public static void Ls()
{
var currentDir = Directory.GetCurrentDirectory();
DirectoryInfo dirinfo = new DirectoryInfo(currentDir);
FileInfo[] filesInDir = dirinfo.GetFiles();
DirectoryInfo[] dirsInDir = dirinfo.GetDirectories();
foreach (var directoryInfo in dirsInDir)
{
Console.WriteLine("{0} ----- {1}",
directoryInfo.Name, directoryInfo.LastWriteTime);
}
foreach (FileInfo file in filesInDir)
{
Console.WriteLine("{0} {1} {2} bytes {3}",
file.Name, file.Extension, file.Length, file.LastWriteTime);
}
}
private static void LsRecursive()
{
Console.Write("Enter a search pattern (for example *.txt), or * for every file: ");
var pattern = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(pattern)) pattern = "*";
var currentDir = Directory.GetCurrentDirectory();
string[] files = Directory.GetFiles(currentDir, pattern, SearchOption.AllDirectories);
Console.WriteLine("Matching files:");
foreach (var file in files)
{
Console.WriteLine(file);
}
string[] dirs = Directory.GetDirectories(currentDir, "*", SearchOption.AllDirectories);
Console.WriteLine("Subdirectories:");
foreach (var dir in dirs)
{
Console.WriteLine(dir);
}
}
public static void MkDir()
{
Console.Write("Enter the name of the directory: ");
var name = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("Directory name cannot be empty.");
return;
}
if (Directory.Exists(name))
{
Console.WriteLine("Directory already exists.");
return;
}
Directory.CreateDirectory(name);
Console.WriteLine("Directory created.");
}
// ----- File creation, deletion and moving -----
public static void Touch()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("File name cannot be empty.");
return;
}
if (File.Exists(name))
{
Console.WriteLine("File already exists.");
return;
}
var fs = File.Create(name);
fs.Close(); // Always close the stream after use
Console.WriteLine("File created.");
}
private static void Rm()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
File.Delete(name);
Console.WriteLine("File deleted.");
}
private static void RmRecursive()
{
Console.Write("Enter the name of the directory: ");
var name = Console.ReadLine() ?? "";
if (!ValidateDirExists(name)) return;
Directory.Delete(name, true);
Console.WriteLine("Directory and everything inside it was deleted.");
}
private static void Mv()
{
Console.Write("Enter the current name: ");
var oldName = Console.ReadLine() ?? "";
Console.Write("Enter the new name: ");
var newName = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(oldName) || string.IsNullOrEmpty(newName))
{
Console.WriteLine("Names cannot be empty.");
return;
}
if (File.Exists(oldName))
{
File.Move(oldName, newName);
Console.WriteLine("File moved to {0}", newName);
}
else if (Directory.Exists(oldName))
{
Directory.Move(oldName, newName);
Console.WriteLine("Directory moved to {0}", newName);
}
else
{
Console.WriteLine("Source file or directory does not exist.");
}
}
// ----- Reading and displaying content -----
private static void Cat()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
var content = File.ReadAllText(name);
Console.WriteLine(content);
}
private static void NanoAppend()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
Console.Write("Enter the text to append: ");
var text = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(text))
{
Console.WriteLine("Text cannot be empty.");
return;
}
File.AppendAllText(name, text);
}
private static void Nano()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
Console.WriteLine("----- ------ ----- [Old Content] ----- ------ -----");
ReadFileData(name);
Console.WriteLine("----- ------ ----- [Please write the new content] ----- ------ -----");
WriteFileData(name);
}
private static void ReadFileData(string? name)
{
FileStream fs = new FileStream(name!, FileMode.OpenOrCreate, FileAccess.ReadWrite);
StreamReader sr = new StreamReader(fs);
sr.BaseStream.Seek(0, SeekOrigin.Begin); // Move to the start of the file
string? str = sr.ReadLine();
while (str != null)
{
Console.WriteLine("{0}", str);
str = sr.ReadLine();
}
sr.Close();
fs.Close();
}
private static void WriteFileData(string? name)
{
FileStream fs = new FileStream(name!, FileMode.Create, FileAccess.ReadWrite);
StreamWriter w = new StreamWriter(fs);
var text = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(text))
{
Console.WriteLine("Text cannot be empty.");
fs.Close();
return;
}
w.Write(text);
w.Flush(); // Forces the buffered data onto disk
w.Close();
fs.Close();
}
// ----- FileInfo / DirectoryInfo metadata -----
private static void Stat()
{
Console.Write("Enter the name of the file or directory: ");
var name = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("Name cannot be empty.");
return;
}
if (File.Exists(name))
{
FileInfo fi = new FileInfo(name);
Console.WriteLine("Full name : {0}", fi.FullName);
Console.WriteLine("Extension : {0}", fi.Extension);
Console.WriteLine("Size : {0} bytes", fi.Length);
Console.WriteLine("Last write time : {0}", fi.LastWriteTime);
Console.WriteLine("Directory : {0}", fi.DirectoryName);
}
else if (Directory.Exists(name))
{
DirectoryInfo di = new DirectoryInfo(name);
Console.WriteLine("Full name : {0}", di.FullName);
Console.WriteLine("Parent : {0}", di.Parent);
Console.WriteLine("Drive root : {0}", Directory.GetDirectoryRoot(di.FullName));
Console.WriteLine("Created : {0}", Directory.GetCreationTime(name));
Console.WriteLine("Last access time : {0}", Directory.GetLastAccessTime(name));
Console.WriteLine("Last write time : {0}", Directory.GetLastWriteTime(name));
}
else
{
Console.WriteLine("File or directory does not exist.");
}
}
// ----- Convenience File.WriteAllLines / ReadAllLines -----
private static void WriteLines()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("File name cannot be empty.");
return;
}
Console.WriteLine("Enter lines of text. Type an empty line to stop:");
var lines = new List<string>();
string? line;
while (!string.IsNullOrEmpty(line = Console.ReadLine()))
{
lines.Add(line);
}
File.WriteAllLines(name, lines);
Console.WriteLine("{0} line(s) written to {1}", lines.Count, name);
}
private static void ReadLines()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
string[] lines = File.ReadAllLines(name);
for (int i = 0; i < lines.Length; i++)
{
Console.WriteLine("{0}: {1}", i + 1, lines[i]);
}
}
// ----- Binary data with BinaryReader / BinaryWriter -----
private static void BinWrite()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("File name cannot be empty.");
return;
}
Console.Write("Enter an integer: ");
int i = int.Parse(Console.ReadLine() ?? "0");
Console.Write("Enter a decimal number: ");
double d = double.Parse(Console.ReadLine() ?? "0");
Console.Write("Enter true or false: ");
bool b = bool.Parse(Console.ReadLine() ?? "false");
using (FileStream fs = new FileStream(name, FileMode.Create, FileAccess.Write, FileShare.None))
using (BinaryWriter writer = new BinaryWriter(fs))
{
writer.Write(i);
writer.Write(d);
writer.Write(b);
}
Console.WriteLine("Binary data written to {0}", name);
}
private static void BinRead()
{
Console.Write("Enter the name of the file: ");
var name = Console.ReadLine() ?? "";
if (!ValidateExists(name)) return;
using (FileStream fs = new FileStream(name, FileMode.Open, FileAccess.Read, FileShare.Read))
using (BinaryReader reader = new BinaryReader(fs))
{
int i = reader.ReadInt32();
double d = reader.ReadDouble();
bool b = reader.ReadBoolean();
Console.WriteLine("Integer : {0}", i);
Console.WriteLine("Decimal : {0}", d);
Console.WriteLine("Boolean : {0}", b);
}
}
// ----- In-memory text with StringReader / StringWriter -----
private static void StrDemo()
{
using StringWriter sw = new StringWriter();
sw.WriteLine("Line one from StringWriter");
sw.WriteLine("Line two from StringWriter");
string builtText = sw.ToString();
Console.WriteLine("----- Built in memory -----");
using (StringReader sr = new StringReader(builtText))
{
string? line;
while ((line = sr.ReadLine()) != null)
{
Console.WriteLine("> {0}", line);
}
}
Console.Write("Save this text to a file? Enter a file name or leave empty to skip: ");
var name = Console.ReadLine() ?? "";
if (!string.IsNullOrEmpty(name))
{
File.WriteAllText(name, builtText);
Console.WriteLine("Saved to {0}", name);
}
}
// ----- Drives, search and recursive copy -----
private static void Drives()
{
string[] drives = Directory.GetLogicalDrives();
foreach (var drive in drives)
{
Console.WriteLine(drive);
}
}
private static void Find()
{
Console.Write("Enter the directory to search: ");
var dir = Console.ReadLine() ?? "";
if (!ValidateDirExists(dir)) return;
Console.Write("Enter the text to search for: ");
var searchText = Console.ReadLine() ?? "";
string[] files = Directory.GetFiles(dir, "*", SearchOption.AllDirectories);
int matches = 0;
foreach (var file in files)
{
try
{
using StreamReader sr = new StreamReader(file);
string contents = sr.ReadToEnd();
if (contents.Contains(searchText))
{
Console.WriteLine(file);
matches++;
}
}
catch (IOException)
{
// Skip files that cannot be opened as text (locked, or not text)
}
}
Console.WriteLine("{0} file(s) matched.", matches);
}
private static void Du()
{
Console.Write("Enter the directory: ");
var dir = Console.ReadLine() ?? "";
if (!ValidateDirExists(dir)) return;
DirectoryInfo dirInfo = new DirectoryInfo(dir);
long size = 0;
foreach (FileInfo fi in dirInfo.GetFiles("*", SearchOption.AllDirectories))
{
size += fi.Length;
}
Console.WriteLine("Total size: {0} bytes", size);
}
private static void CpDir()
{
Console.Write("Enter the source directory: ");
var source = Console.ReadLine() ?? "";
if (!ValidateDirExists(source)) return;
Console.Write("Enter the destination directory: ");
var dest = Console.ReadLine() ?? "";
if (string.IsNullOrEmpty(dest))
{
Console.WriteLine("Destination cannot be empty.");
return;
}
DirectoryCopy(source, dest, true);
Console.WriteLine("Copy finished.");
}
private static void DirectoryCopy(string source, string dest, bool copySubDirs = true)
{
DirectoryInfo dir = new DirectoryInfo(source);
if (!dir.Exists)
{
throw new DirectoryNotFoundException(
$"Source directory does not exist or could not be found: {source}");
}
DirectoryInfo[] dirs = dir.GetDirectories();
if (!Directory.Exists(dest))
{
Directory.CreateDirectory(dest);
}
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(dest, file.Name);
file.CopyTo(tempPath, false);
}
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(dest, subdir.Name);
DirectoryCopy(subdir.FullName, tempPath, copySubDirs);
}
}
}
private static void Home()
{
var docPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
Console.WriteLine("Documents folder: {0}", docPath);
}
// ----- Shared validation helpers -----
private static bool ValidateExists(string? name)
{
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("File name cannot be empty.");
return false;
}
if (!File.Exists(name))
{
Console.WriteLine("File does not exist.");
return false;
}
return true;
}
private static bool ValidateDirExists(string? name)
{
if (string.IsNullOrEmpty(name))
{
Console.WriteLine("Directory name cannot be empty.");
return false;
}
if (!Directory.Exists(name))
{
Console.WriteLine("Directory does not exist.");
return false;
}
return true;
}
}