Streams and Directory Operations in C#
Every program that works with files does two things: it moves bytes in and out of files, and it finds its way around the folders that hold them. In .NET the first job belongs to streams and the second to the Directory, DirectoryInfo and FileInfo classes, all in the System.IO namespace. This section builds both skills with small programs whose output you predict before you run them. Every program on this page was built and run on Windows with .NET 8, and every output shown is the real one.
Objectives
By the end of the section you should be able to:
- Explain what a stream is, and the three kinds of stream types .NET provides: backing stores, adapters and decorators.
- Release a file correctly with
IDisposable,Dispose()and theusingstatement, and explain whatusingturns into. - Open a
FileStreamwith the rightFileMode,FileAccessandFileShare, and predict itsPositionandLengthafter every read, write and seek. - Read and write text with
StreamReaderandStreamWriter, and primitive values withBinaryReaderandBinaryWriter. - Use the one-call methods of the
Fileclass, and compress data with theDeflateStreamdecorator. - Create, list, measure, copy and search a directory tree with
Directory,DirectoryInfoandFileInfo, using recursion.
All programs need these two lines at the top:
using System;
using System.IO;Part 1: what a stream is
A stream is a sequence of bytes
A program accepts input, processes it and produces output. A file is a collection of data stored on a disk under a name and a directory path. When a file is opened for reading or writing it becomes a stream: a sequence of bytes travelling from a source to a destination.
- An input stream is used to read; an output stream is used to write.
- The data can be stored in, or forwarded through, a file, the network or the internet. These are the backing stores of the streamed data. The program sees the same flow of bytes whichever it is.
- A stream can be readable, writable, or both. Some streams are seekable: they keep a current position that can be queried and changed.
- All stream types derive from the abstract base class
System.IO.Stream, which is whyRead,WriteandSeeklook the same on every kind of stream.
Worked example: one character, one byte
A new text file holds only the letter A. How big is it? Then it holds A, four spaces and z. How big now? Predict, then read the program and its output.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("sample.txt", "A");
Console.WriteLine("Size: " + new FileInfo("sample.txt").Length + " byte(s)");
File.WriteAllText("sample.txt", "A z");
Console.WriteLine("Size: " + new FileInfo("sample.txt").Length + " byte(s)");
byte[] data = File.ReadAllBytes("sample.txt");
foreach (byte b in data)
{
Console.WriteLine(b + " = " + Convert.ToString(b, 2).PadLeft(8, '0') + " = '" + (char)b + "'");
}
}
}Output (Windows):
Size: 1 byte(s)
Size: 6 byte(s)
65 = 01000001 = 'A'
32 = 00100000 = ' '
32 = 00100000 = ' '
32 = 00100000 = ' '
32 = 00100000 = ' '
122 = 01111010 = 'z'The editor shows letters, but the disk stores numbers. Here every character takes one byte (8 bits): A is 65, which is 64 + 1, so its bits are 01000001. A space is 32 and z is 122. Six characters, six bytes.
Three kinds of stream types
.NET has a very large number of stream types, so it groups them into three kinds. This section takes one type from each kind.
| Kind | Job | Example used here |
|---|---|---|
| Backing store | Talks directly to the external source (a file, the network) and moves raw bytes | FileStream |
| Decorator | Transforms the bytes, for example compresses them, and is itself still a stream | DeflateStream |
| Adapter | Presents the bytes to the application in a useful shape: text, lines, numbers | StreamReader, StreamWriter, BinaryReader, BinaryWriter |
The bytes flow from the external source into the backing store, may pass through one or more decorators, and reach the program through an adapter. Why three kinds and not one big class?
- Separation of concerns. A class that does everything is "polluted": hard to read and hard to maintain.
- Adapters are plugs. Like a travel adapter between a socket and a device, an adapter can be swapped without touching the implementation underneath.
- Decorators can be stacked. More than one decorator can be attached to the same stream at run time.
The System.IO classes
| Class | Used to |
|---|---|
FileStream | Read and write from any location within a file |
BinaryReader | Read primitive data types from a binary stream |
BinaryWriter | Write primitive data types in binary format to a binary stream |
StreamReader | Read characters from a byte stream |
StreamWriter | Write characters to a stream |
StringReader | Read from a string buffer |
StringWriter | Write into a string buffer |
DirectoryInfo | Perform operations on directories |
FileInfo | Perform operations on files |
Part 2: cleaning up with IDisposable and using
Managed and unmanaged resources
C# code is compiled to an intermediate language and run by the common language runtime (CLR). Code under the CLR is managed: the garbage collector finds objects that are no longer needed and frees their memory. An open file, however, is an operating system handle. The garbage collector does not know when you have finished with it, so it cannot release it at the right moment, and until it is released the file stays open and locked.
A class that holds such a resource implements the IDisposable interface and offers a Dispose() method. The rule for the user of the class is simple: when you are done, call Dispose(), or let a using statement call it. FileStream, StreamReader, StreamWriter, BinaryReader and BinaryWriter all implement IDisposable.
Worked example: the dispose pattern
When your own class holds a disposable object, it should implement the recommended dispose pattern. LogFile below owns a FileStream, and prints a line whenever something happens so you can follow the order.
using System;
using System.IO;
class LogFile : IDisposable
{
private readonly FileStream fs; // holds an operating system file handle
private bool disposed = false;
public LogFile(string path)
{
fs = new FileStream(path, FileMode.Create, FileAccess.Write);
Console.WriteLine("LogFile: file opened");
}
public void Write(byte b)
{
fs.WriteByte(b);
}
public void Dispose() // step 2: the public method clients call
{
Dispose(true);
GC.SuppressFinalize(this); // cleanup done, the finalizer is not needed
}
protected virtual void Dispose(bool disposing) // step 3
{
if (disposed)
{
Console.WriteLine("LogFile: already disposed, nothing to do");
return;
}
if (disposing)
{
fs.Dispose(); // release the managed object we own
Console.WriteLine("LogFile: file closed");
}
disposed = true;
}
~LogFile() // safety net: the garbage collector calls it
{
Dispose(false);
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("1) using block");
using (LogFile log = new LogFile("log.txt"))
{
log.Write(65);
Console.WriteLine("inside the block");
}
Console.WriteLine("after the block");
Console.WriteLine("2) try / finally");
LogFile log2 = null;
try
{
log2 = new LogFile("log.txt");
log2.Write(66);
}
finally
{
log2?.Dispose();
}
Console.WriteLine("3) Dispose called twice");
LogFile log3 = new LogFile("log.txt");
log3.Dispose();
log3.Dispose();
Console.WriteLine("4) using declaration");
using LogFile log4 = new LogFile("log.txt");
log4.Write(67);
Console.WriteLine("end of Main");
}
}The pattern has five parts:
- The class implements
IDisposable. - A public
Dispose()is what users of the class call. - A protected virtual
Dispose(bool disposing)does the work.protectedlets subclasses see it,virtuallets them extend it. Thedisposedflag makes sure the cleanup runs only once, however many timesDispose()is called. Whendisposingis true, the managed objects the class owns (here theFileStream) are released too. GC.SuppressFinalize(this)tells the garbage collector that cleanup is done, so it need not call the finalizer.- The finalizer
~LogFile()is a safety net: if a user forgets to callDispose(), the garbage collector calls it, and it callsDispose(false).
Predict the output before you read it. In particular: what does the second log3.Dispose() print, and where does file closed appear for log4?
Output (Windows):
1) using block
LogFile: file opened
inside the block
LogFile: file closed
after the block
2) try / finally
LogFile: file opened
LogFile: file closed
3) Dispose called twice
LogFile: file opened
LogFile: file closed
LogFile: already disposed, nothing to do
4) using declaration
LogFile: file opened
end of Main
LogFile: file closedFour ways to release a resource
| Way | Shape | Verdict |
|---|---|---|
Call Dispose() yourself | at the end of the code | Not recommended: if an exception is thrown first, the call is skipped |
| try / finally | Dispose() in the finally block, with ?. | Always runs; ?. avoids a null reference when the constructor failed |
| using block | using (var r = new ...) followed by a block | Disposed at the closing brace of the block |
| using declaration (C# 8) | using var r = new ...; | Disposed at the end of the enclosing scope |
A using statement is not magic: the compiler turns it into exactly the try / finally of the second row. This block
using (LogFile log = new LogFile("log.txt"))
{
log.Write(65);
}is compiled as
LogFile log = new LogFile("log.txt");
try
{
log.Write(65);
}
finally
{
log?.Dispose();
}Use the block form when everything that follows belongs to the resource; use the declaration form when the resource should live until the end of the method, as log4 does above: end of Main is printed before file closed.
Part 3: FileStream, the backing store
Opening a FileStream
FileStream fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read, FileShare.Read);The three enumerations decide how the file is opened.
FileMode decides whether the file is created, opened, truncated or appended to:
| Member | File missing | File exists |
|---|---|---|
Append | Creates it | Opens it and places the position at the end |
Create | Creates it | Overwrites it: the old content is gone |
CreateNew | Creates it | Throws IOException |
Open | Throws FileNotFoundException | Opens it |
OpenOrCreate | Creates it | Opens it |
Truncate | Throws FileNotFoundException | Opens it and cuts it to 0 bytes |
FileAccess says what you will do with the stream: Read, Write or ReadWrite.
FileShare says what other streams may do with the file while yours is open: None (refuse sharing), Read, Write, ReadWrite, and Inheritable (a child process may inherit the handle).
Worked example: HELLO meets every FileMode
The program below tries every FileMode, first on a missing file and then on a file holding HELLO. It opens with FileAccess.Write, prints the position and length right after opening, writes the two bytes a and b, and prints the file. Predict the twelve lines before reading the output.
using System;
using System.IO;
class Program
{
static void Try(FileMode mode, bool exists)
{
string path = "test.txt";
if (File.Exists(path)) File.Delete(path);
if (exists) File.WriteAllText(path, "HELLO");
string label = mode + (exists ? " (file has HELLO)" : " (no file)");
try
{
using (var fs = new FileStream(path, mode, FileAccess.Write))
{
long pos = fs.Position, len = fs.Length;
fs.WriteByte((byte)'a');
fs.WriteByte((byte)'b');
fs.Close();
Console.WriteLine("{0,-32} Position={1} Length={2} -> after writing ab: {3}",
label, pos, len, File.ReadAllText(path));
}
}
catch (Exception ex)
{
Console.WriteLine("{0,-32} {1}", label, ex.GetType().Name);
}
}
static void Main(string[] args)
{
FileMode[] modes = { FileMode.Append, FileMode.Create, FileMode.CreateNew,
FileMode.Open, FileMode.OpenOrCreate, FileMode.Truncate };
foreach (FileMode m in modes) Try(m, false);
Console.WriteLine();
foreach (FileMode m in modes) Try(m, true);
}
}Output (Windows):
Append (no file) Position=0 Length=0 -> after writing ab: ab
Create (no file) Position=0 Length=0 -> after writing ab: ab
CreateNew (no file) Position=0 Length=0 -> after writing ab: ab
Open (no file) FileNotFoundException
OpenOrCreate (no file) Position=0 Length=0 -> after writing ab: ab
Truncate (no file) FileNotFoundException
Append (file has HELLO) Position=5 Length=5 -> after writing ab: HELLOab
Create (file has HELLO) Position=0 Length=0 -> after writing ab: ab
CreateNew (file has HELLO) IOException
Open (file has HELLO) Position=0 Length=5 -> after writing ab: abLLO
OpenOrCreate (file has HELLO) Position=0 Length=5 -> after writing ab: abLLO
Truncate (file has HELLO) Position=0 Length=0 -> after writing ab: abAppendkeeps the old data and writes after it:HELLOab.CreateandTruncatethrow the old data away: the file is justab.OpenandOpenOrCreatedo not clear the file. The position starts at 0, so the two bytes overwriteHandEand the rest stays:abLLO.CreateNewrefuses an existing file, andOpenandTruncaterefuse a missing one.
Worked example: which operations throw
data.txt holds HELLO. Each case opens its own stream and does one thing. Which ones throw, and which exception?
using System;
using System.IO;
class Program
{
static void Run(string title, Action action)
{
try
{
action();
Console.WriteLine(title + ": OK");
}
catch (Exception ex)
{
Console.WriteLine(title + ": " + ex.GetType().Name);
}
}
static void Main(string[] args)
{
File.WriteAllText("data.txt", "HELLO");
Run("ReadByte on a Write-only stream", () =>
{
using var fs = new FileStream("data.txt", FileMode.Open, FileAccess.Write);
fs.ReadByte();
});
Run("WriteByte on a Read-only stream", () =>
{
using var fs = new FileStream("data.txt", FileMode.Open, FileAccess.Read);
fs.WriteByte(65);
});
Run("Append with FileAccess.Read", () =>
{
using var fs = new FileStream("data.txt", FileMode.Append, FileAccess.Read);
});
Run("Append, then Seek back to 0", () =>
{
using var fs = new FileStream("data.txt", FileMode.Append, FileAccess.Write);
fs.Seek(0, SeekOrigin.Begin);
});
Console.WriteLine("File still holds: " + File.ReadAllText("data.txt"));
}
}Output (Windows):
ReadByte on a Write-only stream: NotSupportedException
WriteByte on a Read-only stream: NotSupportedException
Append with FileAccess.Read: ArgumentException
Append, then Seek back to 0: IOException
File still holds: HELLO- A stream opened for
Writecannot read, and one opened forReadcannot write:NotSupportedException. Appendis allowed only together withFileAccess.Write: any other combination is anArgumentExceptionbefore the file is even touched.- A stream opened with
Appendrefuses to seek back over the data that was already in the file:IOException. The old data is protected.
Worked example: FileShare
The first stream keeps the file open while a second stream tries to open it.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("shared.txt", "HELLO");
using (var first = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
using (var second = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read))
{
Console.WriteLine("FileShare.Read: second reader opened, first byte = " + second.ReadByte());
}
try
{
using var writer = new FileStream("shared.txt", FileMode.Open, FileAccess.Write, FileShare.Read);
Console.WriteLine("FileShare.Read: a writer opened");
}
catch (IOException ex)
{
Console.WriteLine("FileShare.Read: a writer was refused (" + ex.GetType().Name + ")");
}
}
using (var locked = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.None))
{
try
{
using var second = new FileStream("shared.txt", FileMode.Open, FileAccess.Read, FileShare.Read);
Console.WriteLine("FileShare.None: second reader opened");
}
catch (IOException ex)
{
Console.WriteLine("FileShare.None: second reader was refused (" + ex.GetType().Name + ")");
}
}
}
}Output (Windows):
FileShare.Read: second reader opened, first byte = 72
FileShare.Read: a writer was refused (IOException)
FileShare.None: second reader was refused (IOException)With FileShare.Read the first stream lets other readers in but refuses a writer. With FileShare.None it refuses everyone. This is how two programs, or two parts of one program, are kept from corrupting a file.
The Can properties and Position
Stream defines properties that every stream fills in: CanRead, CanWrite, CanSeek, Length, Position and ReadTimeout. The position is where the next byte will be read or written, like the cursor in an editor. It starts at 0, and WriteByte writes one byte there and moves it forward by one.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
string path = "sample.txt";
File.WriteAllText(path, ""); // start with an empty file
using (var fs = new FileStream(path, FileMode.Open, FileAccess.Read))
{
Console.WriteLine("Read only : CanRead={0} CanWrite={1} CanSeek={2}",
fs.CanRead, fs.CanWrite, fs.CanSeek);
}
using (var fs = new FileStream(path, FileMode.Open, FileAccess.ReadWrite))
{
Console.WriteLine("Read write: CanRead={0} CanWrite={1} CanSeek={2}",
fs.CanRead, fs.CanWrite, fs.CanSeek);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.WriteByte(65);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.WriteByte(66);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
}
Console.WriteLine("File now holds: " + File.ReadAllText(path));
}
}Output (Windows):
Read only : CanRead=True CanWrite=False CanSeek=True
Read write: CanRead=True CanWrite=True CanSeek=True
Position=0 Length=0
Position=1 Length=1
Position=2 Length=2
File now holds: ABWorked example: writing bytes 65 to 122
A loop writes every byte from 65 up to 122. What appears between Z and a?
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var fs = new FileStream("letters.txt", FileMode.Create, FileAccess.Write))
{
for (int i = 65; i < 123; i++)
{
fs.WriteByte((byte)i);
}
Console.WriteLine("Bytes written: " + fs.Length);
}
Console.WriteLine(File.ReadAllText("letters.txt"));
}
}Output (Windows):
Bytes written: 58
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyzZ is 90 and a is 97, so the six codes 91 to 96 are written too: they are the symbols between the capital and the small letters.
Worked example: reading into a buffer and copying
Read(buffer, offset, count) reads up to count bytes into the array starting at offset, and returns how many it actually read. It may return fewer than you asked for, and it returns 0 at the end of the file, so the reading is done in a loop. The bytes are then written into a second file with Write(buffer, offset, count).
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("sample.txt", "C# streams");
byte[] data;
using (var fs = new FileStream("sample.txt", FileMode.Open, FileAccess.Read))
{
data = new byte[fs.Length];
int numBytesToRead = (int)fs.Length;
int numBytesRead = 0;
while (numBytesToRead > 0)
{
int n = fs.Read(data, numBytesRead, numBytesToRead);
if (n == 0) break; // end of the file
numBytesRead += n;
numBytesToRead -= n;
}
Console.WriteLine("Read " + numBytesRead + " bytes:");
foreach (byte b in data) Console.Write(b + " ");
Console.WriteLine();
}
using (var fsw = new FileStream("copy.txt", FileMode.OpenOrCreate, FileAccess.Write))
{
fsw.Write(data, 0, data.Length);
}
Console.WriteLine("copy.txt holds: " + File.ReadAllText("copy.txt"));
}
}Output (Windows):
Read 10 bytes:
67 35 32 115 116 114 101 97 109 115
copy.txt holds: C# streamsC is 67, # is 35 and the space is 32. A FileStream works only with bytes; the adapters in Part 4 turn them into text for you.
Worked example: Seek past the end
Seek(offset, origin) moves the position. The new position is
p = b + owhere p is the new position, o is the offset you pass, and b is 0 for SeekOrigin.Begin, the current position for SeekOrigin.Current, and the length for SeekOrigin.End. The new position may not be negative, but it may be past the end of the file.
A new, empty file. The program seeks to 20 and writes the byte 65, then reads every byte from the start. Predict the length and the bytes.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var fs = new FileStream("gap.txt", FileMode.Create, FileAccess.ReadWrite))
{
fs.Seek(20, SeekOrigin.Begin);
fs.WriteByte(65);
Console.WriteLine("Position={0} Length={1}", fs.Position, fs.Length);
fs.Position = 0;
for (int i = 0; i < fs.Length; i++)
{
Console.Write(fs.ReadByte() + " ");
}
Console.WriteLine();
fs.Seek(-3, SeekOrigin.End);
Console.WriteLine("Seek(-3, End) -> Position=" + fs.Position);
fs.Seek(1, SeekOrigin.Current);
Console.WriteLine("Seek(1, Current) -> Position=" + fs.Position);
}
}
}Output (Windows):
Position=21 Length=21
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 65
Seek(-3, End) -> Position=18
Seek(1, Current) -> Position=19The file is 21 bytes long. The 20 bytes before A are the byte 0, not spaces: a space would be 32. Many people expect spaces here; the file simply holds nothing (zero) in the gap.
Interactive: move the file pointer
The widget shows a file as a row of byte cells with the position as an arrow. Choose the file on disk, a FileMode and a FileAccess, open it, then press WriteByte, ReadByte and Seek and watch the position and length change. The examples replay the cases above one statement at a time, including the exceptions. Its rules were checked against a C# program that tried every FileMode and FileAccess combination.
Part 4: adapters, shortcuts and a decorator
StreamWriter
StreamWriter inherits from the abstract class TextWriter and writes characters to a stream.
| Method | What it does |
|---|---|
Write | Writes to the stream and stays on the same line |
WriteLine | Writes the data followed by an end of line |
Flush | Clears the writer's buffers and makes the buffered data be written to the stream |
Close | Flushes, then closes the writer and its stream |
Worked example: when does the text reach the file?
Predict the size of note.txt after each step.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
var fs = new FileStream("note.txt", FileMode.Create, FileAccess.Write);
var w = new StreamWriter(fs);
w.Write("Hello");
Console.WriteLine("After Write : file size = " + new FileInfo("note.txt").Length);
w.Flush();
Console.WriteLine("After Flush : file size = " + new FileInfo("note.txt").Length);
w.WriteLine(" OS");
Console.WriteLine("After WriteLine: file size = " + new FileInfo("note.txt").Length);
w.Close(); // flushes, then closes the stream too
Console.WriteLine("After Close : file size = " + new FileInfo("note.txt").Length);
}
}Output (Windows):
After Write : file size = 0
After Flush : file size = 5
After WriteLine: file size = 5
After Close : file size = 10A writer collects text in a buffer in memory and writes it to the stream in large pieces. Until Flush or Close, the file on disk does not change. On Windows the end of a line is two bytes, 13 and 10, so WriteLine(" OS") adds three characters plus two bytes: 5 more bytes, and the file ends at 10.
Worked example: appending user input
This program asks for a string and appends it to data.txt. WriteData is called twice, and the input was the two lines first run and second run.
using System;
using System.IO;
class Program
{
public void WriteData()
{
FileStream fs = new FileStream("data.txt", FileMode.Append, FileAccess.Write);
StreamWriter w = new StreamWriter(fs);
Console.WriteLine("Enter the string");
string str = Console.ReadLine();
w.WriteLine(str);
w.Flush();
fs.Close();
}
static void Main(string[] args)
{
if (File.Exists("data.txt")) File.Delete("data.txt");
Program p = new Program();
p.WriteData();
p.WriteData();
Console.WriteLine("data.txt now holds:");
Console.Write(File.ReadAllText("data.txt"));
}
}Output (Windows):
Enter the string
Enter the string
data.txt now holds:
first run
second runFileMode.Append with FileAccess.Write adds to the end, so each run keeps the lines already there.
StreamReader
StreamReader inherits from the abstract class TextReader and reads characters.
| Member | What it does |
|---|---|
Peek | Returns the next available character but does not consume it; -1 at the end |
Read | Reads the next character (or set of characters) from the stream |
ReadLine | Reads a line of characters and returns it as a string; returns null at the end |
BaseStream.Seek | Moves the position of the underlying stream, for example back to the beginning |
Close | Closes the reader and its stream and releases the system resources |
Worked example: Peek, Read and ReadLine
lines.txt holds three lines: Streams, and, Directories. Predict the first four lines of output.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var sw = new StreamWriter("lines.txt"))
{
sw.WriteLine("Streams");
sw.WriteLine("and");
sw.Write("Directories");
}
using (var sr = new StreamReader("lines.txt"))
{
Console.WriteLine("Peek: " + sr.Peek() + " = '" + (char)sr.Peek() + "'");
Console.WriteLine("Read: " + (char)sr.Read());
Console.WriteLine("Peek: " + (char)sr.Peek());
Console.WriteLine("ReadLine: " + sr.ReadLine());
}
Console.WriteLine("-- ReadLine loop --");
FileStream fs = new FileStream("lines.txt", FileMode.Open, FileAccess.Read);
StreamReader reader = new StreamReader(fs);
reader.BaseStream.Seek(0, SeekOrigin.Begin);
string str = reader.ReadLine();
int count = 0;
while (str != null)
{
count++;
Console.WriteLine("{0}: {1}", count, str);
str = reader.ReadLine();
}
Console.WriteLine("ReadLine returned null after line " + count);
reader.Close();
fs.Close();
Console.WriteLine("-- Peek / Read loop --");
using (var sr = new StreamReader("lines.txt"))
{
int chars = 0;
while (sr.Peek() >= 0)
{
char c = (char)sr.Read();
chars++;
}
Console.WriteLine("Characters read: " + chars);
}
Console.WriteLine("File size: " + new FileInfo("lines.txt").Length + " bytes");
}
}Output (Windows):
Peek: 83 = 'S'
Read: S
Peek: t
ReadLine: treams
-- ReadLine loop --
1: Streams
2: and
3: Directories
ReadLine returned null after line 3
-- Peek / Read loop --
Characters read: 25
File size: 25 bytesPeekreturns 83, the code ofS, and leaves it in place: the second call sees the sameS.Readconsumes theS, so the nextPeekseestandReadLinereturns only the rest of the line,treams.- The
ReadLineloop stops whenReadLinereturns null. - The file has 21 letters plus two line ends of 2 bytes each: 25 characters, 25 bytes.
Worked example: BinaryWriter and BinaryReader
Text stores a number as its digits. BinaryWriter stores it in its binary form instead: an int always takes 4 bytes, a double 8 and a bool 1. BinaryReader reads the values back, and they must be read in the same order with the matching method (ReadInt32, ReadDouble, ReadBoolean). Predict the file size.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
BinaryReader datain;
BinaryWriter dataout;
int i = 20;
double j = 101.50;
bool b = true;
dataout = new BinaryWriter(new FileStream("data", FileMode.Create));
Console.WriteLine("Writing " + i);
dataout.Write(i);
Console.WriteLine("Writing " + j);
dataout.Write(j);
Console.WriteLine("Writing " + b);
dataout.Write(b);
Console.WriteLine("Writing " + 12.2 * 15.5);
dataout.Write(12.2 * 15.5);
dataout.Close();
Console.WriteLine("File size: " + new FileInfo("data").Length + " bytes");
datain = new BinaryReader(new FileStream("data", FileMode.Open));
i = datain.ReadInt32();
Console.WriteLine("Reading " + i);
j = datain.ReadDouble();
Console.WriteLine("Reading " + j);
b = datain.ReadBoolean();
Console.WriteLine("Reading " + b);
j = datain.ReadDouble();
Console.WriteLine("Reading " + j);
datain.Close();
byte[] raw = File.ReadAllBytes("data");
Console.WriteLine("First 4 bytes (the int 20): {0} {1} {2} {3}", raw[0], raw[1], raw[2], raw[3]);
Console.WriteLine("Byte 12 (the bool true): " + raw[12]);
}
}Output (Windows):
Writing 20
Writing 101.5
Writing True
Writing 189.1
File size: 21 bytes
Reading 20
Reading 101.5
Reading True
Reading 189.1
First 4 bytes (the int 20): 20 0 0 0
Byte 12 (the bool true): 1The size is 4 + 8 + 1 + 8 = 21 bytes. The int 20 is stored as the four bytes 20 0 0 0, lowest byte first, and true is the single byte 1 at position 4 + 8 = 12.
The File class
The static File class does a whole job in one call: it opens the file, reads or writes, and closes it again. There are three pairs of read and write methods, plus Exists, Delete and Move. There is no Rename method: moving and renaming are the same operation.
| Method | What it does |
|---|---|
WriteAllText / ReadAllText | Write / read the whole file as one string |
WriteAllLines / ReadAllLines | Write / read an array of lines |
WriteAllBytes / ReadAllBytes | Write / read an array of bytes |
Exists / Delete | Check whether a file exists / delete it |
Move | Move or rename a file |
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
string[] lines = { "Linux", "Windows", "macOS" };
File.WriteAllLines("os.txt", lines);
string[] back = File.ReadAllLines("os.txt");
Console.WriteLine("ReadAllLines returned " + back.Length + " lines, the second is " + back[1]);
File.WriteAllText("test.txt", "Streams are sequences of bytes");
Console.WriteLine("ReadAllText: " + File.ReadAllText("test.txt"));
File.WriteAllBytes("raw.bin", new byte[] { 72, 105 });
Console.WriteLine("WriteAllBytes wrote: " + File.ReadAllText("raw.bin"));
if (File.Exists("renamed.txt")) File.Delete("renamed.txt");
File.Move("test.txt", "renamed.txt");
Console.WriteLine("test.txt exists: " + File.Exists("test.txt"));
Console.WriteLine("renamed.txt exists: " + File.Exists("renamed.txt"));
File.Delete("renamed.txt");
if (File.Exists("renamed.txt") == false)
Console.WriteLine("File deleted...");
}
}Output (Windows):
ReadAllLines returned 3 lines, the second is Windows
ReadAllText: Streams are sequences of bytes
WriteAllBytes wrote: Hi
test.txt exists: False
renamed.txt exists: True
File deleted...Worked example: the DeflateStream decorator
A decorator wraps another stream and transforms the bytes that pass through it. DeflateStream (namespace System.IO.Compression) compresses on the way in with CompressionMode.Compress and decompresses on the way out with CompressionMode.Decompress. The program writes 1000 bytes of ABCABC... through it. Predict whether the file is larger or smaller than 1000 bytes.
using System;
using System.IO;
using System.IO.Compression;
class Program
{
static void Main(string[] args)
{
byte[] original = new byte[1000];
for (int i = 0; i < original.Length; i++)
original[i] = (byte)(65 + i % 3); // ABCABCABC...
using (FileStream fs = File.Create("data.bin"))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Compress))
{
ds.Write(original, 0, original.Length);
}
Console.WriteLine("Original bytes : " + original.Length);
Console.WriteLine("data.bin on disk: " + new FileInfo("data.bin").Length + " bytes");
int count = 0, first = -1, last = -1;
using (FileStream fs = File.OpenRead("data.bin"))
using (DeflateStream ds = new DeflateStream(fs, CompressionMode.Decompress))
{
int b;
while ((b = ds.ReadByte()) != -1)
{
if (first == -1) first = b;
last = b;
count++;
}
}
Console.WriteLine("Bytes after decompress: " + count + " (first " + first + ", last " + last + ")");
}
}Output (Windows):
Original bytes : 1000
data.bin on disk: 15 bytes
Bytes after decompress: 1000 (first 65, last 65)Repetitive data compresses extremely well: 1000 bytes became 15 on disk, and decompressing gives all 1000 back. The compressed file is not readable as text; only the decorator can turn it back. Notice that the reading loop stops when ReadByte returns -1, the end of the stream.
Part 5: directories
Directory or DirectoryInfo
A directory (a folder) is a location that stores files and other directories. C# offers two classes for it: Directory is a static class whose methods take a path, while a DirectoryInfo object represents one specific folder. In the same way, File is static and a FileInfo object represents one file. Both DirectoryInfo and FileInfo derive from FileSystemInfo.
Properties of DirectoryInfo:
| Property | Description |
|---|---|
Attributes | Gets or sets the attributes (inherited from FileSystemInfo) |
CreationTime | Gets or sets the creation time |
Exists | True when the directory exists |
FullName | The full path of the directory |
Name | The name of the directory |
Other members used below: Parent, LastAccessTime, and the methods Create, CreateSubdirectory, Delete, GetDirectories and GetFiles.
Properties of FileInfo:
| Property | Description |
|---|---|
Directory | The directory the file belongs to |
Extension | The file extension, such as .txt |
FullName | The full path of the file |
Length | The size of the file in bytes |
Name | The name of the file |
FileInfo also has CreationTime, LastAccessTime, LastWriteTime, Attributes and Exists, and the methods Create, AppendText, Delete, Open, OpenRead and CopyTo. The static Directory class adds GetCurrentDirectory, GetDirectoryRoot, GetLogicalDrives, GetFileSystemEntries, and the time methods GetCreationTime, GetLastAccessTime and GetLastWriteTime.
Worked example: directory basics
The program creates lab3/test, prints information about it, tries to delete it while it holds a file, renames it and finally deletes it recursively. Predict what Directory.Delete("lab3/test") does.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
if (Directory.Exists("lab3")) Directory.Delete("lab3", true);
DirectoryInfo di = Directory.CreateDirectory("lab3/test");
Console.WriteLine($"Name: {di.Name}, Parent: {di.Parent.Name}");
Console.WriteLine("Exists: " + Directory.Exists("lab3/test"));
var curDir = Directory.GetCurrentDirectory();
Console.WriteLine("Current directory: " + curDir);
Console.WriteLine("Root: " + Directory.GetDirectoryRoot(curDir));
Console.WriteLine("Drives: " + string.Join(" ", Directory.GetLogicalDrives()));
File.WriteAllText("lab3/test/a.txt", "abc");
try
{
Directory.Delete("lab3/test");
}
catch (IOException ex)
{
Console.WriteLine("Delete(test) failed: " + ex.GetType().Name);
}
Directory.Move("lab3/test", "lab3/test2");
Console.WriteLine("After Move: test={0} test2={1}",
Directory.Exists("lab3/test"), Directory.Exists("lab3/test2"));
Directory.Delete("lab3/test2", true);
Console.WriteLine("After Delete(test2, true): " + Directory.Exists("lab3/test2"));
}
}Output (Windows):
Name: test, Parent: lab3
Exists: True
Current directory: D:\a\os1-lab-runner\os1-lab-runner\labs\os1-03-p16-dirbasics
Root: D:\
Drives: C:\ D:\
Delete(test) failed: IOException
After Move: test=False test2=True
After Delete(test2, true): FalseThis output was recorded on the build machine that runs the lesson programs, which works on drive D:. On a lab PC the program usually prints Root: C:\ and a folder inside your own project, and the list of drives depends on the PC.
CreateDirectoryreturns aDirectoryInfofor the new folder and creates any missing parent folders on the way.- The current directory is where the program was started;
GetDirectoryRootgives its drive, andGetLogicalDriveslists every drive in the formC:\. Delete(path)works only on an empty directory, so it throwsIOExceptionhere.Delete(path, true)is recursive: it deletes the files and subdirectories first.Directory.Moverenames a directory, just asFile.Moverenames a file.
The sample tree
The rest of the directory programs work on the same small tree, created by this method (each program contains a copy of it):
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}site
about.txt 48 bytes "Streams move bytes between a program and a file."
work.txt 13 bytes "Lab work list"
images
logo.bin 300 bytes (300 zero bytes)
wiki
home.txt 19 bytes "Welcome to the wiki"
week1.txt 25 bytes "Week 1: what is a stream?"
old
week0.txt 13 bytes "Week 0: setup"Windows returns the names in alphabetical order.
Worked example: listing with patterns
GetDirectories, GetFiles and GetFileSystemEntries (both files and folders) take an optional search pattern, such as w* for names starting with w, and a SearchOption: TopDirectoryOnly searches only the given folder, AllDirectories searches every subfolder too. Predict each list.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Show(string title, string[] items)
{
Console.WriteLine(title);
foreach (var item in items) Console.WriteLine(" " + item);
}
static void Main(string[] args)
{
MakeSampleTree();
Show("GetDirectories(site):", Directory.GetDirectories("site"));
Show("GetDirectories(site, w*, TopDirectoryOnly):",
Directory.GetDirectories("site", "w*", SearchOption.TopDirectoryOnly));
Show("GetDirectories(site, *, AllDirectories):",
Directory.GetDirectories("site", "*", SearchOption.AllDirectories));
Show("GetFiles(site):", Directory.GetFiles("site"));
Show("GetFileSystemEntries(site, w*):", Directory.GetFileSystemEntries("site", "w*"));
Show("GetFiles(site, w*, AllDirectories):",
Directory.GetFiles("site", "w*", SearchOption.AllDirectories));
Console.WriteLine("FileInfo for every file in site/wiki:");
DirectoryInfo dirinfo = new DirectoryInfo("site/wiki");
FileInfo[] filesInDir = dirinfo.GetFiles();
foreach (FileInfo file in filesInDir)
{
Console.WriteLine(" File Name {0} Extension {1} Size: {2} bytes",
file.Name, file.Extension, file.Length);
}
}
}Output (Windows):
GetDirectories(site):
site\images
site\wiki
GetDirectories(site, w*, TopDirectoryOnly):
site\wiki
GetDirectories(site, *, AllDirectories):
site\images
site\wiki
site\wiki\old
GetFiles(site):
site\about.txt
site\work.txt
GetFileSystemEntries(site, w*):
site\wiki
site\work.txt
GetFiles(site, w*, AllDirectories):
site\work.txt
site\wiki\week1.txt
site\wiki\old\week0.txt
FileInfo for every file in site/wiki:
File Name home.txt Extension .txt Size: 19 bytes
File Name week1.txt Extension .txt Size: 25 bytesThe paths come back relative because the program passed the relative path site. GetFiles("site", "w*", SearchOption.AllDirectories) finds week1.txt and week0.txt deep inside wiki, while GetFileSystemEntries("site", "w*") stays at the top and returns one folder and one file.
Worked example: the size of a directory
The size of a folder is the sum of the lengths of all the files below it. GetFiles("*", SearchOption.AllDirectories) returns them all as FileInfo objects. Predict the total, then the total of the top folder alone.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Main(string[] args)
{
MakeSampleTree();
long size = 0;
var dirInfo = new DirectoryInfo("site");
foreach (FileInfo fi in dirInfo.GetFiles("*", SearchOption.AllDirectories))
{
size += fi.Length;
Console.WriteLine("{0,-10} {1,4} bytes running total {2,4}", fi.Name, fi.Length, size);
}
Console.WriteLine($"The directory size: {size} bytes");
long top = 0;
foreach (FileInfo fi in dirInfo.GetFiles()) top += fi.Length;
Console.WriteLine($"Top level files only: {top} bytes");
}
}Output (Windows):
about.txt 48 bytes running total 48
work.txt 13 bytes running total 61
logo.bin 300 bytes running total 361
home.txt 19 bytes running total 380
week1.txt 25 bytes running total 405
week0.txt 13 bytes running total 418
The directory size: 418 bytes
Top level files only: 61 bytes48 + 13 + 300 + 19 + 25 + 13 = 418 bytes. The top folder alone holds only about.txt and work.txt: 48 + 13 = 61 bytes.
Worked example: copying a directory recursively
DirectoryCopy creates the destination folder, copies the files of the source folder into it, and then calls itself once for each subfolder. That is recursion: each call handles one folder and hands its subfolders to new calls. Predict the order of the output lines.
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static void Main(string[] args)
{
MakeSampleTree();
if (Directory.Exists("site-2")) Directory.Delete("site-2", true);
DirectoryCopy("site", "site-2", true);
Console.WriteLine("Copying finished");
Console.WriteLine("Files in site-2: " +
Directory.GetFiles("site-2", "*", SearchOption.AllDirectories).Length);
}
private static void DirectoryCopy(string source, string dest, bool copySubDirs = true)
{
var 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);
Console.WriteLine("create " + dest);
}
FileInfo[] files = dir.GetFiles();
foreach (FileInfo file in files)
{
string tempPath = Path.Combine(dest, file.Name);
file.CopyTo(tempPath, false);
Console.WriteLine(" copy " + tempPath);
}
if (copySubDirs)
{
foreach (DirectoryInfo subdir in dirs)
{
string tempPath = Path.Combine(dest, subdir.Name);
DirectoryCopy(subdir.FullName, tempPath, copySubDirs);
}
}
}
}Output (Windows):
create site-2
copy site-2\about.txt
copy site-2\work.txt
create site-2\images
copy site-2\images\logo.bin
create site-2\wiki
copy site-2\wiki\home.txt
copy site-2\wiki\week1.txt
create site-2\wiki\old
copy site-2\wiki\old\week0.txt
Copying finished
Files in site-2: 6The first call handles site: it creates site-2 and copies the two top files. Then it calls itself for images (create, copy logo.bin) and for wiki, and the call for wiki copies its two files before calling itself for old.
Worked example: searching the files for a text
The source gives a recursive routine that searches every file for a piece of text. Here it looks for stream in the sample tree, reading each file with StreamReader.ReadToEnd and comparing in lower case. The routine, cut from the full program in the answer to Practice 6:
private static void DirSearch(string sDir)
{
try
{
foreach (var directory in Directory.GetDirectories(sDir))
{
foreach (var filename in Directory.GetFiles(directory))
{
using (var streamReader = new StreamReader(filename))
{
var contents = streamReader.ReadToEnd().ToLower();
if (contents.Contains(SearchText))
{
FilesFound.Add(filename);
}
}
}
DirSearch(directory);
}
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
}
}Which files does DirSearch("site") report? about.txt contains "Streams" and week1.txt contains "stream".
DirSearch: files containing the word stream
site\wiki\week1.txtabout.txt is missed. The outer loop goes over the subdirectories of site, and the inner loop opens the files of each subdirectory, so the files of the folder the method was called on are never opened. For site that means about.txt and work.txt. Practice 6 fixes it.
Interactive: watch the recursion
The widget walks the sample tree one step at a time. Choose the directory size, the recursive copy, the source's DirSearch, or a search that also opens the top folder's files; press Step or Play and watch the current node, the call stack of recursive calls, the running total, and the console output grow. The visiting order and every number match the Windows runs above.
Worked example: access control lists (Windows only)
An access control list (ACL) is a list of access control entries (ACEs). Each entry names a trustee (a user or a group) and the access rights allowed, denied or audited for it. DirectoryInfo.GetAccessControl() returns the ACL of a directory. This example works only on Windows, and on .NET 8 it needs no extra package.
using System;
using System.IO;
using System.Security.AccessControl;
class Program
{
static void Main(string[] args)
{
var myDir = "test";
Directory.CreateDirectory(myDir);
var dirInfo = new DirectoryInfo(myDir);
DirectorySecurity dSecurity = dirInfo.GetAccessControl();
AuthorizationRuleCollection acl = dSecurity.GetAccessRules(true, true,
typeof(System.Security.Principal.NTAccount));
foreach (FileSystemAccessRule ace in acl)
{
Console.WriteLine("Account: {0}", ace.IdentityReference.Value);
Console.WriteLine("Type: {0}", ace.AccessControlType);
Console.WriteLine("Rights: {0}", ace.FileSystemRights);
Console.WriteLine("Inherited: {0}", ace.IsInherited);
Console.WriteLine("------------------------");
}
}
}Output (Windows):
Account: BUILTIN\Administrators
Type: Allow
Rights: FullControl
Inherited: False
------------------------
Account: BUILTIN\Administrators
Type: Allow
Rights: FullControl
Inherited: True
------------------------
Account: NT AUTHORITY\SYSTEM
Type: Allow
Rights: FullControl
Inherited: True
------------------------
Account: CREATOR OWNER
Type: Allow
Rights: 268435456
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: ReadAndExecute, Synchronize
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: CreateDirectories
Inherited: True
------------------------
Account: BUILTIN\Users
Type: Allow
Rights: CreateFiles
Inherited: True
------------------------Each block is one entry: the account, whether the rights are allowed or denied, the rights, and whether the entry was inherited from the parent folder. Only the first entry has Inherited: False; the other six come from the parent folder.
Practice
About 30 minutes. Predict every answer on paper first, then write and run the program to check yourself. The answers are at the end of the page.
Practice 1: trace the pointer (about 6 minutes)
File.WriteAllText("data.txt", "STREAM");
using (var fs = new FileStream("data.txt", FileMode.Open, FileAccess.ReadWrite))
{
fs.ReadByte(); // 1
fs.ReadByte(); // 2
fs.Seek(2, SeekOrigin.Current); // 3
fs.WriteByte(33); // 4
fs.Seek(-1, SeekOrigin.End); // 5
fs.ReadByte(); // 6
fs.ReadByte(); // 7
fs.Seek(8, SeekOrigin.Begin); // 8
fs.WriteByte(90);
}For each numbered line give the value ReadByte returns (if any) and the position after the line. Then give the final length and all the bytes of the file. The codes you need: S 83, T 84, R 82, E 69, A 65, M 77.
Practice 2: choose and predict the mode (about 5 minutes)
- (a) A log file must gain one line on every run and keep the old lines. Which
FileModeandFileAccessdo you use? After three runs, how many lines does it have? - (b)
new FileStream("new.txt", FileMode.CreateNew, FileAccess.Write)runs twice in a row, starting with no file. What happens each time? - (c)
hello.txtholdsHELLO. It is opened withFileMode.OpenandFileAccess.Write, andWriteByte((byte)'J')is called. What does the file hold after closing? - (d) Then
hello.txtis opened withFileMode.TruncateandFileAccess.Read. What happens, and what does the file hold afterwards?
Practice 3: the size of a binary record (about 4 minutes)
using (var bw = new BinaryWriter(new FileStream("rec.bin", FileMode.Create)))
{
bw.Write(7);
bw.Write(true);
bw.Write(3.5);
bw.Write(false);
bw.Write(1000);
}How many bytes is rec.bin? Write the reading code that prints the five values back. What are the last four bytes of the file? (Hint: 1000 = 3 x 256 + 232.)
Practice 4: count the lines (about 5 minutes)
Write a program that writes the five lines Windows, Linux, macOS, FreeBSD and Android to os.txt with a StreamWriter, then reads the file back with StreamReader.ReadLine and prints the number of lines and the longest line with its length. How big is the file on Windows?
Practice 5: print the tree (about 6 minutes)
Write a recursive method PrintTree(DirectoryInfo dir, string indent) that prints the folder name followed by / (for example site/), then its files with their sizes, then calls itself for each subfolder with a deeper indent. It returns the total size of all the files below the folder. Run it on the sample tree and print the total, then print the size of site/wiki alone.
Practice 6: fix the search (about 5 minutes)
DirSearch never opens the files of the folder it is called on. Write FileSearch(string sDir) that first searches the files of sDir itself and then calls itself for each subdirectory. Run both on site with the search text stream. Which files must FileSearch report?
Answers
Practice 1
| Line | ReadByte returns | Position after |
|---|---|---|
| 1 | 83 (S) | 1 |
| 2 | 84 (T) | 2 |
| 3 | 4 | |
| 4 | 5 (byte 4 is overwritten, length stays 6) | |
| 5 | 5 (length 6 minus 1) | |
| 6 | 77 (M) | 6 |
| 7 | -1 (end of the file) | 6 |
| 8 | 9 after writing 90 (Z) at 8; length 9 |
The final bytes are 83 84 82 69 33 77 0 0 90: STRE!M, two zero bytes filling the gap, then Z. The program that checks it:
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
File.WriteAllText("data.txt", "STREAM");
using (var fs = new FileStream("data.txt", FileMode.Open, FileAccess.ReadWrite))
{
Console.WriteLine("1) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
Console.WriteLine("2) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
fs.Seek(2, SeekOrigin.Current);
Console.WriteLine("3) Seek(2, Current): Position = {0}", fs.Position);
fs.WriteByte(33);
Console.WriteLine("4) WriteByte(33): Position = {0}, Length = {1}", fs.Position, fs.Length);
fs.Seek(-1, SeekOrigin.End);
Console.WriteLine("5) Seek(-1, End): Position = {0}", fs.Position);
Console.WriteLine("6) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
Console.WriteLine("7) ReadByte() = {0}, Position = {1}", fs.ReadByte(), fs.Position);
fs.Seek(8, SeekOrigin.Begin);
fs.WriteByte(90);
Console.WriteLine("8) Seek(8, Begin), WriteByte(90): Position = {0}, Length = {1}", fs.Position, fs.Length);
}
Console.WriteLine("Bytes: " + string.Join(" ", File.ReadAllBytes("data.txt")));
}
}Output (Windows):
1) ReadByte() = 83, Position = 1
2) ReadByte() = 84, Position = 2
3) Seek(2, Current): Position = 4
4) WriteByte(33): Position = 5, Length = 6
5) Seek(-1, End): Position = 5
6) ReadByte() = 77, Position = 6
7) ReadByte() = -1, Position = 6
8) Seek(8, Begin), WriteByte(90): Position = 9, Length = 9
Bytes: 83 84 82 69 33 77 0 0 90Practice 2
- (a)
FileMode.AppendwithFileAccess.Write: every run adds to the end, so three runs give 3 lines. - (b) The first attempt creates the file; the second throws
IOExceptionbecause the file already exists. - (c)
JELLO:Opendoes not clear the file, and the byte overwrites theHat position 0. - (d)
ArgumentException:Truncateneeds write access. The check happens before the file is touched, so it still holdsJELLO.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
// (a) a log that grows by one line on every run
if (File.Exists("log.txt")) File.Delete("log.txt");
for (int run = 1; run <= 3; run++)
{
using var w = new StreamWriter(new FileStream("log.txt", FileMode.Append, FileAccess.Write));
w.WriteLine("run " + run);
}
Console.WriteLine("(a) log.txt has " + File.ReadAllLines("log.txt").Length + " lines");
// (b) CreateNew twice
if (File.Exists("new.txt")) File.Delete("new.txt");
for (int i = 1; i <= 2; i++)
{
try
{
using var fs = new FileStream("new.txt", FileMode.CreateNew, FileAccess.Write);
Console.WriteLine("(b) attempt " + i + ": created");
}
catch (Exception ex)
{
Console.WriteLine("(b) attempt " + i + ": " + ex.GetType().Name);
}
}
// (c) Open + Write over HELLO
File.WriteAllText("hello.txt", "HELLO");
using (var fs = new FileStream("hello.txt", FileMode.Open, FileAccess.Write))
{
fs.WriteByte((byte)'J');
}
Console.WriteLine("(c) hello.txt holds " + File.ReadAllText("hello.txt"));
// (d) Truncate + Read
try
{
using var fs = new FileStream("hello.txt", FileMode.Truncate, FileAccess.Read);
Console.WriteLine("(d) opened");
}
catch (Exception ex)
{
Console.WriteLine("(d) " + ex.GetType().Name);
}
Console.WriteLine("(d) hello.txt still holds " + File.ReadAllText("hello.txt"));
}
}Output (Windows):
(a) log.txt has 3 lines
(b) attempt 1: created
(b) attempt 2: IOException
(c) hello.txt holds JELLO
(d) ArgumentException
(d) hello.txt still holds JELLOPractice 3
4 + 1 + 8 + 1 + 4 = 18 bytes. The reading code must use the same order: ReadInt32, ReadBoolean, ReadDouble, ReadBoolean, ReadInt32. The last four bytes are the int 1000, lowest byte first: 232 3 0 0, because 1000 = 3 x 256 + 232.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (var bw = new BinaryWriter(new FileStream("rec.bin", FileMode.Create)))
{
bw.Write(7);
bw.Write(true);
bw.Write(3.5);
bw.Write(false);
bw.Write(1000);
}
Console.WriteLine("Size: " + new FileInfo("rec.bin").Length + " bytes");
using (var br = new BinaryReader(new FileStream("rec.bin", FileMode.Open)))
{
Console.WriteLine(br.ReadInt32() + " " + br.ReadBoolean() + " " + br.ReadDouble() + " "
+ br.ReadBoolean() + " " + br.ReadInt32());
}
byte[] raw = File.ReadAllBytes("rec.bin");
Console.WriteLine("Last 4 bytes: {0} {1} {2} {3}", raw[14], raw[15], raw[16], raw[17]);
}
}Output (Windows):
Size: 18 bytes
7 True 3.5 False 1000
Last 4 bytes: 232 3 0 0Practice 4
Windows, FreeBSD and Android all have 7 characters; the loop keeps the first one it finds because it replaces the longest line only when a line is strictly longer. The file is 31 letters plus five line ends of 2 bytes on Windows: 41 bytes.
using System;
using System.IO;
class Program
{
static void Main(string[] args)
{
using (StreamWriter sw = new StreamWriter("os.txt"))
{
sw.WriteLine("Windows");
sw.WriteLine("Linux");
sw.WriteLine("macOS");
sw.WriteLine("FreeBSD");
sw.WriteLine("Android");
}
int count = 0;
string longest = "";
using (StreamReader sr = new StreamReader("os.txt"))
{
string line = sr.ReadLine();
while (line != null)
{
count++;
if (line.Length > longest.Length) longest = line;
line = sr.ReadLine();
}
}
Console.WriteLine("Lines: " + count);
Console.WriteLine("Longest: " + longest + " (" + longest.Length + " characters)");
Console.WriteLine("File size: " + new FileInfo("os.txt").Length + " bytes");
}
}Output (Windows):
Lines: 5
Longest: Windows (7 characters)
File size: 41 bytesPractice 5
using System;
using System.IO;
class Program
{
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
static long PrintTree(DirectoryInfo dir, string indent)
{
long total = 0;
Console.WriteLine(indent + dir.Name + "/");
foreach (FileInfo f in dir.GetFiles())
{
Console.WriteLine(indent + " " + f.Name + " (" + f.Length + ")");
total += f.Length;
}
foreach (DirectoryInfo sub in dir.GetDirectories())
{
total += PrintTree(sub, indent + " ");
}
return total;
}
static void Main(string[] args)
{
MakeSampleTree();
long total = PrintTree(new DirectoryInfo("site"), "");
Console.WriteLine("Total: " + total + " bytes");
long wiki = 0;
foreach (FileInfo fi in new DirectoryInfo("site/wiki").GetFiles("*", SearchOption.AllDirectories))
wiki += fi.Length;
Console.WriteLine("wiki: " + wiki + " bytes");
}
}Output (Windows):
site/
about.txt (48)
work.txt (13)
images/
logo.bin (300)
wiki/
home.txt (19)
week1.txt (25)
old/
week0.txt (13)
Total: 418 bytes
wiki: 57 bytesThe total matches the directory size from the worked example, 418 bytes, and wiki holds 19 + 25 + 13 = 57 bytes.
Practice 6
FileSearch must report site\about.txt and site\wiki\week1.txt. The full program runs both methods:
using System;
using System.Collections.Generic;
using System.IO;
class Program
{
private static List<string> FilesFound { get; } = new List<string>();
private const string SearchText = "stream";
static void MakeSampleTree()
{
if (Directory.Exists("site")) Directory.Delete("site", true);
Directory.CreateDirectory("site/images");
Directory.CreateDirectory("site/wiki/old");
File.WriteAllText("site/about.txt", "Streams move bytes between a program and a file.");
File.WriteAllText("site/work.txt", "Lab work list");
File.WriteAllBytes("site/images/logo.bin", new byte[300]);
File.WriteAllText("site/wiki/home.txt", "Welcome to the wiki");
File.WriteAllText("site/wiki/week1.txt", "Week 1: what is a stream?");
File.WriteAllText("site/wiki/old/week0.txt", "Week 0: setup");
}
private static void DirSearch(string sDir)
{
try
{
foreach (var directory in Directory.GetDirectories(sDir))
{
foreach (var filename in Directory.GetFiles(directory))
{
using (var streamReader = new StreamReader(filename))
{
var contents = streamReader.ReadToEnd().ToLower();
if (contents.Contains(SearchText))
{
FilesFound.Add(filename);
}
}
}
DirSearch(directory);
}
}
catch (System.Exception ex)
{
Console.WriteLine(ex.Message);
}
}
private static void FileSearch(string sDir)
{
foreach (var filename in Directory.GetFiles(sDir))
{
using (var streamReader = new StreamReader(filename))
{
if (streamReader.ReadToEnd().ToLower().Contains(SearchText))
FilesFound.Add(filename);
}
}
foreach (var directory in Directory.GetDirectories(sDir))
{
FileSearch(directory);
}
}
private static void Main(string[] args)
{
MakeSampleTree();
DirSearch("site");
Console.WriteLine("DirSearch: files containing the word " + SearchText);
foreach (var file in FilesFound) Console.WriteLine(" " + file);
FilesFound.Clear();
FileSearch("site");
Console.WriteLine("FileSearch: files containing the word " + SearchText);
foreach (var file in FilesFound) Console.WriteLine(" " + file);
}
}Output (Windows):
DirSearch: files containing the word stream
site\wiki\week1.txt
FileSearch: files containing the word stream
site\about.txt
site\wiki\week1.txt