Standard library · System and tools

fs/dir

Creating, listing, copying, moving and removing directories.

import fs.dir

The fs.dir package covers everything you do with directories: create them, list them, walk a tree, measure it, copy, move and delete it. It is built on the folder functions of os and adds two things: when a function fails it records the detail (an IOError with the operation and the path) in os.error, and Move works across disks. For files use fs.file, and to build the paths fs.path.

Listing functions return bare names; Walk returns full paths. Functions that change the disk return 0 or -1.

What people get wrong:

  • Using Exists for files. Here it is true only for a directory; for files use fs.file.Exists.
  • Expecting Copy and Move to refuse an existing target. Both merge into it: files with the same name are replaced and other files stay.
  • Pointing RemoveAll, Walk, Size or Copy at a tree with symbolic links. They follow a link to a folder as if it were part of the tree, so RemoveAll deletes the contents of the link's target and a link loop never ends.

Create a project layout

MkdirAll creates every missing parent and succeeds when the folder is already there, so it is the safe choice for setting up a layout that may partly exist. List, ListDirs and ListFiles return bare names, not full paths, in whatever order the file system keeps them, and that order differs between systems. Sort them with sort.Sort before you show or compare them.

fs-dir-guide-skeleton.salam
import fs.dir
import fs.file
import sort

func main:
    each d in ["shop/src/models", "shop/src/views", "shop/tests", "shop/docs"]:
        dir.MkdirAll(d)
    end
    file.WriteText("shop/main.salam", "func main:\nend\n")
    file.WriteText("shop/README.md", "# Shop\n")
    file.WriteText("shop/src/models/order.salam", "")
    top := dir.ListDirs("shop")
    sort.Sort(top)
    println "folders:", top
    files := dir.ListFiles("shop")
    sort.Sort(files)
    println "files:  ", files
    println "src has", dir.ListDirs("shop/src").len(), "folders"
    dir.RemoveAll("shop")
end
Outputfolders: ["docs", "src", "tests"] files: ["README.md", "main.salam"] src has 2 folders

Walk a whole tree

Walk returns the path of every file below a directory, already joined to the directory you passed in, so each one can go straight to an fs.file function. Directories themselves are not in the list, and like the listing functions it has no fixed order. This example counts files by extension and adds up their sizes, which is what Size does in one call.

fs-dir-guide-walk.salam
import fs.dir
import fs.file
import fs.path

func main:
    dir.MkdirAll("site/pages/blog")
    dir.MkdirAll("site/assets/img")
    file.WriteText("site/pages/index.html", "<h1>Home</h1>")
    file.WriteText("site/pages/blog/first.html", "<p>First post</p>")
    file.WriteText("site/assets/style.css", "body { margin: 0 }")
    file.WriteText("site/assets/img/logo.svg", "<svg/>")
    counts := HashMap {} as HashMap<str, int>
    mut bytes := 0 as i64
    each p in dir.Walk("site"):
        ext := path.Ext(p)
        if counts.has(ext): counts.put(ext, counts.get(ext) + 1) else: counts.put(ext, 1) end
        bytes += file.Size(p)
    end
    each ext in [".html", ".css", ".svg"]:
        println ext, counts.get(ext)
    end
    println "total:", bytes, "bytes, same as dir.Size:", bytes == dir.Size("site")
    dir.RemoveAll("site")
end
Output.html 2 .css 1 .svg 1 total: 54 bytes, same as dir.Size: true

Keep rotating backups

Copy copies a tree with everything in it (file contents only; permissions are not kept), and Move renames one, falling back to copy and delete when the rename fails, for example because the target is on another disk. Together they make a simple rotation: drop the oldest backup, shift the others up by one, and copy the live folder to .bak1. The oldest is removed first so that its name is free before anything moves onto it.

fs-dir-guide-backup.salam
import fs.dir
import fs.file
import str

func backup(src: str, keep: int):
    dir.RemoveAll(src.concat(".bak").concat(str.FromInt(keep)))
    mut n := keep - 1
    until n >= 1:
        from := src.concat(".bak").concat(str.FromInt(n))
        if dir.Exists(from): dir.Move(from, src.concat(".bak").concat(str.FromInt(n + 1))) end
        n -= 1
    end
    dir.Copy(src, src.concat(".bak1"))
end

func main:
    dir.MkdirAll("notes")
    repeat 1 to 4 in day:
        file.WriteText("notes/today.txt", "day ".concat(str.FromInt(day)))
        backup("notes", 3)
    end
    repeat 1 to 3 in n:
        name := "notes.bak".concat(str.FromInt(n))
        println name, file.ReadText(name.concat("/today.txt"))
    end
    println "notes.bak4 exists:", dir.Exists("notes.bak4")
    each d in ["notes", "notes.bak1", "notes.bak2", "notes.bak3"]: dir.RemoveAll(d) end
end
Outputnotes.bak1 day 4 notes.bak2 day 3 notes.bak3 day 2 notes.bak4 exists: false

Errors and pitfalls

Every function that changes the disk returns 0 or -1, and on failure it leaves a short message in os.error. The message names the operation that failed, not the reason, so when the reason matters, check the likely causes yourself: the folder already exists, its parent is missing, or it is not empty.

fs-dir-guide-errors.salam
import fs.dir
import fs.file
import os.error

func report(what: str, rc: int):
    if rc == 0:
        println what, "ok"
    else:
        println what, "failed:", error.LastMessage()
    end
end

func main:
    report("mkdir data", dir.Mkdir("data"))
    report("mkdir data again", dir.Mkdir("data"))
    report("mkdir a/b/c", dir.Mkdir("a/b/c"))
    file.WriteText("data/x.txt", "x")
    report("remove data", dir.Remove("data"))
    println "data/x.txt is a dir:", dir.Exists("data/x.txt"), "is a file:", file.Exists("data/x.txt")
    report("remove all data", dir.RemoveAll("data"))
    println "listing a missing dir:", dir.List("data").len()
end
Outputmkdir data ok mkdir data again failed: mkdir failed mkdir a/b/c failed: mkdir failed remove data failed: rmdir failed data/x.txt is a dir: false is a file: true remove all data ok listing a missing dir: 0
  • Mkdir fails when the folder exists or its parent is missing; MkdirAll handles both.
  • Remove only removes an empty folder. RemoveAll deletes everything inside without asking, so build its argument carefully and never pass it a path straight from user input; path.IsSafe checks that a path stays inside a base folder.
  • Exists is false for a file, and List on a missing folder gives an empty vector rather than an error.

Which function for which job

JobFunction
Create one folder in an existing parentMkdir
Create a whole path of foldersMkdirAll
Names inside one folderList, or ListDirs and ListFiles for one kind only
Every file below a folder, with pathsWalk
Total size of a treeSize
Duplicate a treeCopy
Rename or move a treeMove
Delete an empty folderRemove
Delete a folder and its contentsRemoveAll

Functions

Mkdir

func Mkdir(p: str): int
Creates one directory; fails (-1, with an os.error record) when it exists or its parent is missing.
fs-dir-List.salam
import fs.dir
import fs.file

func main:
    dir.Mkdir("library")
    dir.MkdirAll("library/poetry/hafez")
    file.WriteText("library/index.txt", "3 books")
    file.WriteText("library/poetry/hafez/ghazal.txt", "a line of verse")
    println dir.Exists("library"), dir.IsDir("library/index.txt")
    println dir.List("library").len(), dir.ListDirs("library"), dir.ListFiles("library")
    println dir.Walk("library").len(), dir.Size("library") > 0
    println dir.Permissions("library") > 0, dir.ModTime("library") > 0
    dir.Copy("library", "backup")
    dir.Move("backup", "archive")
    println dir.Exists("backup"), dir.Exists("archive/poetry/hafez"), file.Exists("archive/poetry/hafez/ghazal.txt")
    dir.Mkdir("empty")
    dir.Remove("empty")
    dir.RemoveAll("library")
    dir.RemoveAll("archive")
    println dir.Exists("library"), dir.Exists("empty")
end
Outputtrue false 2 ["poetry"] ["index.txt"] 2 true true true false true true false false

MkdirAll

func MkdirAll(p: str): int
Creates a directory along with every parent it needs; succeeds when it already exists.

The example for this item is shown with the ones above: fs-dir-List.salam

Remove

func Remove(p: str): int
Removes an empty directory; a directory with anything in it fails.

The example for this item is shown with the ones above: fs-dir-List.salam

RemoveAll

func RemoveAll(p: str): int
Removes a directory with everything in it; a missing path counts as success. It follows symbolic links to folders and deletes what is inside their targets.

The example for this item is shown with the ones above: fs-dir-List.salam

List

func List(p: str): Vector<str>
The names of everything inside a directory, hidden entries included, without . and .., in no fixed order. A missing directory gives an empty vector.

The example for this item is shown with the ones above: fs-dir-List.salam

ListDirs

func ListDirs(p: str): Vector<str>
Only the names of the subdirectories.

The example for this item is shown with the ones above: fs-dir-List.salam

ListFiles

func ListFiles(p: str): Vector<str>
Only the names of the files.

The example for this item is shown with the ones above: fs-dir-List.salam

Walk

func Walk(p: str): Vector<str>
The paths of every file in a directory and its subdirectories, each starting with the path you passed; directories are not listed. The same as os.Walk.

The example for this item is shown with the ones above: fs-dir-List.salam

Copy

func Copy(src: str, dst: str): int
Copies a directory with all its contents into dst, creating it if needed and merging with what is already there. Permissions are not copied.

The example for this item is shown with the ones above: fs-dir-List.salam

Move

func Move(src: str, dst: str): int
Moves a directory. When the rename fails, across two disks for instance, it copies into the target (merging with an existing one) and then removes the original.

The example for this item is shown with the ones above: fs-dir-List.salam

Exists

func Exists(p: str): bool
True when the path exists and is a directory.

The example for this item is shown with the ones above: fs-dir-List.salam

IsDir

func IsDir(p: str): bool
True when the path is a directory.

The example for this item is shown with the ones above: fs-dir-List.salam

Size

func Size(p: str): i64
The total size of every file in the directory and its subdirectories, in bytes; 0 for a missing directory.

The example for this item is shown with the ones above: fs-dir-List.salam

Permissions

func Permissions(p: str): int
The directory's permissions, as octal digits.

The example for this item is shown with the ones above: fs-dir-List.salam

ModTime

func ModTime(p: str): i64
When the directory's list of entries last changed (a file added, removed or renamed), in Unix seconds; editing a file inside does not change it.

The example for this item is shown with the ones above: fs-dir-List.salam