Chapter 14

Accessing the file system

Accessing the file system is an integral part of any programming language. X# itself contains no commands or functions for that. It's all part of the .Net runtime and the VO library.

Since the file system can be a huge topic and this is intended to be a small book, I only will cover it briefly and show some examples about

  • How to read and write text files
  • How to read and write binary files
  • Check for the existence of a file or directory
  • Create and delete files and directories

For all other file system-related topics, like modifying attributes or the access control lists (ACL) for the file system security rules, please refer to the huge selection of know-how and examples on the internet and the very good Microsoft documentation that contains many examples (in C#). As mentioned already several times before, even if all the code is for C#, it can be "translated" to X# with little effort.

I will cover the .Net runtime first and foremost, but I will also resort to the VO functions if it's appropriate.

Overview of System.IO

All the file system classes and constants (always as part of enums) are part of the System.IO namespace. The namespace is not as big as you would expect. It contains "only" 28 classes (with .Net Framework 4.x). << Tab:TabNum << contains the most important classes.


TIP: If you prefer PowerShell as your second language, as I do, the following command will print out all the classes in the System.IO namespace in the Windows PowerShell console:

[AppDomain]::CurrentDomain.GetAssemblies().Where{$_.Location -match "mscorlib.dll"}[0].GetTypes().Where{$_.Namespace -eq "System.IO"}.Where{$_.IsClass -and -$_.IsPublic}   

Of course, you could write a small X# console application, but for ad hoc queries like the above, an interactive shell like PowerShell is more practical. Let alone for not having to write an output function every time.


class meaning
FileStream The most convenient class for read and write operations with a single file.
File Contains several static methods for basic file I/O.
FileInfo Provides the attributes of a file.
Directory Contains several static methods for the basic directory I/O.
DirectoryInfo Provides the attributes of a directory.
StreamReader Provides methods for read access to a file.
StreamWriter Provide methods for write access to a file.
Path Contains static methods for path operations like concatenating several subdirectory names to a single path.

Tab 14.1: A few important classes in the System.IO namespace

Concatenating path fragments

Concatenating the path and names of subdirectories to a complete and valid path is easy with the static Combine() method of the Path class.

var path := Path.Combine(Environment.CurrentDirectory, "Sub1", "Sub2", "Sub3")

The result is a string.

Creating a directory in the temp directory

If an application should save temporary files the best place is a directory inside the temp directory of the user file. And where exactly is that location? Easy, that's where the GetTempPath() method of the Path class is for.

var dir1 := Path.Combine(Path.GetTempPath(), "sub1")
if !Directory.Exists(dir1)
   Directory.CreateDirectory(dir1)
endif

Getting files from a directory

There is no dir function in X# (at least not that I know of). Getting all the files from a specific directory is the job of the GetFiles() method of the DirectoryInfo class. Since it's not a static method, DirectoryInfo has to be instantiated with the path of the directory.

var prgFiles := DirectoryInfo{prgPath}:GetFiles("*.prg")

The filter is optional of course. If GetFiles() should also get the files not only from the top directory but from every subdirectory, all you have to do is use SearchOption.AllDirectories as the second argument.

var prgFiles := DirectoryInfo{prgPath}:GetFiles("*.prg", SearchOption.AllDirectories)


TIP: The X# runtime has a Directory() function that returns an array of file names, sizes, etc


Reading and writing text files

There are several choices when using the .Net runtime, the simplest is using the File class and its static method ReadAllText().

**Example 14.1: (XS_ReadTextFile.prg) **

// Reading an existing text file with the file class

using System.IO

Function Start() as void
   var filePath := "C:\Windows\Win.ini"
   var textContent := File.ReadAllText(filePath)
   ? textContent
   // no need to close anything since there is no file handle available

Using the File class is not a big difference to the VO function that reduces any file system-related activity to a single call.

It is important to note, that there is no need to open the file first and close it later because there is no file handle involved.

Writing a text into a file is also simple thanks to the WriteAllText() method of the File class.

**Example 14.2: (XS_WriteTextfile.prg) **

// Example for writing a text file with the file class

using System.IO
using System.Collections.Generic

Function Start() as void
    var monate := List<String>{}
    for Local m := 1 UpTo 12
        monate.add(DateTime{2022,m,1}:ToString("MMMM"))
    next
    var filePath := "Monate.txt"
    File.WriteAllLines(filePath, monate)A

When encoding matters

If encoding matters, methods like ReadAllLines and WriteAllLines offer an overloaded definition that accepts an Encoding object (namespace System.Text) as an optional argument.

**Example 14.3: (XS_ReadTextWithEncoding.prg) **

// Example for reading a text file with enconding

using System.IO
using System.Text

Function Start() As Void
  var filePath := Path.Combine(Environment.CurrentDirectory, "umlaute.txt")
  If File.Exists(filePath)
    // No umlauts with this
    // var lines := File.ReadAllLines(filePath)
    var lines := File.ReadAllLines(filePath, Encoding.Default)
    ForEach var line in lines
      ? line
    Next
  EndIf

I am always baffled by the fact that "Utf-8" for reading "Umlaute" does not work, it has to be "Default" as the encoding. But this has to do of course with the file and the encoding the containing text is based on. If it's ANSI, then Encoding.Default is the correct value, otherwise Encoding.Utf8.

Another little confusing detail is the fact, that something like Encoding.Utf8 has an Encoding object as its value. So you write something like System.Text.Encoding.Utf8.GetBytes(strText).

Reading and writing binary files

A binary file is just a file where each byte is not interpreted as a text character and special characters like line feed or carriage return have no special meaning.

**Example 14.4: (XS_WriteBinaryFile.prg) **

// Example for writing a binary file - an byte array will be written to a file
// hint: if you get error XS9008: Untyped arrays are not available in the selected dialect 'Core'
// something is wrong with your LINQ query

using System.IO
using System.Linq

Function Start() As Void
   // Getting 10 numbers first
   var r := Random{}
   // Lets use a LINQ query to make the example less boring - the type casting (Byte) is important
   var numbers := Enumerable.Range(1,10):Select({z => (Byte)r:Next(1,100)}):OrderBy({z => z}):ToArray()
   // No store all the numbers in a file
   var filePath := Path.Combine(Environment.CurrentDirectory, "Numbers.dat")
   Begin Using var fs := FileStream{filePath, FileMode.Create}
     Begin Using var bWrite := BinaryWriter{fs}
        // use the overload write(byte[], start, end) method
        bWrite:Write(numbers, 0, numbers:Length)
     End Using
   End Using
   var numBytes := FileInfo{filePath}:Length
   ? i"{numBytes} Bytes written to {filePath}"

**Example 14.5: (XS_ReadBinaryFile.prg) **

// Example for reading a binary file - that had been written with an byte array before

using System.IO

Function Start() As Void
   var filePath := Path.Combine(Environment.CurrentDirectory, "Numbers.dat")
   Begin Using var fs := FileStream{filePath, FileMode.Open}
     Begin Using var bReader := BinaryReader{fs}
       // there is no EndOfFile property
       While bReader:PeekChar() != -1
         // Read the type that had been written
         ? bReader:ReadByte()
       End While
     End Using
   End Using

Working with files and directories

The previous sections were all about reading and writing files. This section is about handling files and directories as entities where the content does not matter.

Check for the existence of a file or directory

Both the Directory and the File class offer an Exists() method that simply returns a True if a directory or file exists.

using System.IO

function Start() As Void var configPath := Path.Combine(Environment.CurrentDirectory, "config") If !Directory.Exists(configPath) Directory.CreateDirectory(configPath) EndIf

Create and delete files and directories

The universal File and Directory classes offer both a Create()/CreateDirectory() and a Delete() method for creating and deleting files and directories. For deleting a directory there is always a distinction between a directory that is empty and one that is not empty.

Deleting a directory (and its content too)

Deleting a directory is very simple thanks to the static Delete() method of the Directory class. Beware, for deleting a directory that is not empty, a True value has to be passed as the second argument.

**Example 14.6: (XS_DirectoryDelete.prg) **

// Example for deleting a directory with its content
using System.IO

Function CreateDirectory(Path As String) As Void
  Directory.CreateDirectory(Path)
  For Local i:= 1 UpTo 10
      Begin Using var fi := File.CreateText(Path.Combine(Path, i"File{i}.txt"))
       // nothing to do here
      End Using
  Next
  ? i"{Path} created and filled with files"

Function Start() As Void
   var dir1 := Path.Combine(Path.GetTempPath(), "sub1")
   // Check if the directory exists
   If !Directory.Exists(dir1)
      CreateDirectory(dir1)
   EndIf
   // List all files from dir1
   ForEach var fi in DirectoryInfo{dir1}:GetFiles("*.txt")
     ? fi
   Next
   // Now, delete the directory recursevly
   Directory.Delete(dir1, True)
   ? i"{dir1} was successfully deleted for good!"

Moving a directory and its content

Although the Directory class offers a Move() method as well, I would prefer moving each file separately because this approach offers more control over each single move operation.

**Example 14.7: (XS_DirectoryMove.prg) **

// Example for moving a directory with its content

Using System
Using System.IO
Using System.Linq

/// <summary>
/// Move a directory with subdirectories and files by moving each item
/// </summary>
/// <param name="sourcePath"></param>
/// <param name="targetPath"></param>
Function MoveDirectory(sourcePath As String, targetPath As String) As Void
    Var files := Directory.EnumerateFiles(sourcePath, "*", SearchOption.AllDirectories).GroupBy({s => Path.GetDirectoryName(s)})
    Foreach Var folder In files
        Var targetFolder := folder:Key:Replace(sourcePath, targetPath)
        Directory.CreateDirectory(targetFolder)
        Foreach Var file In folder
            Var targetFile := Path.Combine(targetFolder, Path.GetFileName(file))
            If File.Exists(targetFile)
                File.Delete(targetFile)
            End If
            File.Move(file, targetFile)
        Next
    Next

    Directory.Delete(sourcePath, True)


// Move a directory from A to B
Function Start() As Void
    // Creating tmpDirectory1
    var dir1 := Path.Combine(Path.GetTempPath(), "sub1")
    var dir2 := Path.Combine(Path.GetTempPath(), "sub2")
    if !Directory.Exists(dir1)
       Directory.CreateDirectory(dir1)
    endif
    if !Directory.Exists(dir2)
       Directory.CreateDirectory(dir2)
    endif
    // Create a couple of files in dir1
    For Local i := 1 UpTo 10
       var txtPath := Path.Combine(dir1, i"File{i}.txt")
       Begin Using var fi := File:CreateText(txtPath)

       End Using
    Next
    MoveDirectory(dir1, dir2)

Working with files and directory metadata

Another topic is dealing with the metadata like the creation time, the last write time, and the attributes like read-only or hidden or a file or a directory. This is all accomplished with a FileInfo or a DirectoryInfo object respectively.

The following example outputs the creation time of each file in the current directory.

ForEach var fi in DirectoryInfo{dir1}:GetFiles()
  ? i"Creation time of {fi.Name}: {fi.CreationTime}"
Next

Example 14.8: (XS_FileCreationTime.prg) ** You will find the source code in the repository

Querying the extended file properties through COM

What about metadata like the name of the author of an Office document, the word count, or the height, width, or dpi of an image? For mp3 files, there are metadata like the length of the song, the name of the artist (if it has a name that consists of ANSI characters), and even a rating. This additional metadata has nothing to do with the file system because. Therefore, there are no classes in System.IO for querying this kind of metadata. That does not mean of course, that there is no way to query or update the metadata of an mp3 file for example. Since the explorer shows all that information, we have to access the explorer through its COM interface.

To make it short, what is needed is instantiating a COM object with the ProgId "Shell.Application". X# offers no function for this but it's not difficult to achieve this goal with the universal Type class and its static GetTypeFromProgId() method:

Local progId := "Shell.Application" As String
Local shellType := Type.GetTypeFromProgId(progId) As Type

The next step is to use the CreateInstance() method of the Activator class:

Local shell := Activator.CreateInstance(shellType) As Dynamic

Because we are using late binding, the type has to be Dynamic. This small improvement makes late binding calls easy in X# (and any other .Net language too).

The next steps are specific to the vocabulary of the Windows Explorer. The idea is to first create a "Shell folder" by calling the Namespace() method with the path of a specific directory. The next step would be to enumerate all the files of that directory and call the ParseName() method of the ShellFolder object for each file. Since a file can contain any kind of metadata, each property has to be queried with an integer index that starts with 0. The GetDetails() method either returns the name or the value of a property.

The example program returns the extended file properties for each PNG file in the pictures directory in the user profile and its sub-directories. Setting the extended file properties is possible too.

Example 14.9: (XS_GetExtendedFileInfo.prg) ** You will find the source code in the repository

Alt Querying the extended file properties through COM and Shell.Application Fig 14.1: Querying the extended file properties through COM and Shell.Application