Files, Folders, Paths, Environment and Processes
On this page
- Paths: the path package
- Making a Path
- Windows' path syntax
- Taking a path apart
- Joining and normalizing
- Example
- Files and folders: the fs package
- Failure: Result and FsError
- Whole files
- Asking about a file
- Renaming and removing a file
- Folders
- DirEntry
- Links
- The current folder
- UTF-8 names
- Environment variables: the env package
- Running programs: the process package
- run, and what it answers
- Options
- The working folder, the environment, and finding the program
- The time limit
- A crash
- Arguments and the command line
- ProcessError
- This program: args and currentExe
- The posix package beneath
Note: Not built: copying a file, reading or writing a file a piece at a time (fs reads and writes a file whole), a file's permissions or any time of it but its last write, and giving a program that process starts an environment or a standard input of its own.
Four packages let a program work with its surroundings, all synchronously (each call returns when the work is done):
- path: file paths taken apart and put together, as text. It never asks the file system anything.
- fs: files and folders. A file is read or written whole; a folder is made, listed, walked and removed; a file's size, time and kind are asked; and the current folder is read and changed.
- env: the program's environment variables, read, set and removed.
- process: running another program, waiting for it, and capturing what it writes; and this program's own arguments and path.
A program reaches them with import fs;, import path use Path;, import env; and import process;. They run on Windows only, for now. The interfaces belong to no one system, but what is beneath them does: path reads Windows' path syntax (drive letters, shares, both separators); fs and env are written over ISO C and the posix package, which maps the POSIX names onto the Windows C runtime; and process is written directly over kernel32. A version for another system changes the insides, not what a program calls.
Every example below is a complete program that was compiled and run. Those that write to disk work in a folder of their own beside the program and remove it again. Their output is shown after them.
Paths: the path package#
A path is text, UTF-8 by Cone's convention, held by a Path. A Path is a type of its own, so a function that takes a file name says so in its signature, and the operations on a name are its methods. Nothing in the package looks beyond ASCII (a separator, a dot and a drive letter are all ASCII), and nothing validates a path against the rules of a system: any text is a Path. Whether a path names a file, what is there, and where the current folder is are questions for fs.
Making a Path#
A string literal written where a Path is wanted is made one. Any other text is copied in.
- Path.from(s) holds a copy of the text s (a &str, such as a String's view()).
- Path.fromString(s) holds the String it is given, and takes it.
- p.copy() is another path with the same text.
- p.view() is the text, as written, a &str; p.isEmpty() asks whether there is none.
Two paths are equal when their text is the same bytes, and a Path compares equal to a &str likewise: p == &q, p == "a/b". C:\a and C:/a are not equal until both are normalized. A Path is hashable, so it can be a Dict key.
Windows' path syntax#
The paths read are Windows' paths.
- Both / and \ separate names, and either may be written anywhere. path.isSeparator(b) asks it of one byte.
- A path may start with a volume: a drive, C:, or a share, \\server\share (or //server/share), which needs both its server and its share.
- After its volume a path may have a root, a separator. A path is absolute when it has both, C:\x or \\server\share\x; a share always has its root. C:x (a drive's own current folder) and \x (the current drive's root) are not absolute, as Windows reads them.
- Windows' extended prefixes (\\?\C:\x, \\.\device) are not understood: \\?\C:\x reads as the share \\?\C: and the name x.
In Cone source a backslash in a string is written twice: "C:\\work".
Taking a path apart#
These methods answer a piece of the path's own text (a &str into it, nothing copied), except parent and parts. Every one keeps the bytes as written.
- p.isAbsolute(): whether it is absolute, as above.
- p.volume(): the volume as written, C: or \\server\share, or "".
- p.parent(): a new Path, the folder it is in: the path without its last name and the separators before it. a/b/c.txt is in a/b, C:/x in C:/, and x in "", the current folder. A path that is only a volume and a root is its own parent. Separators after the last name are left off first, so a/b/ is in a. Nothing is normalized: a/.. is in a.
- p.name(): the last name, c.txt of a/b/c.txt, b of a/b/, and "" of a path that is only a volume and a root.
- p.extension(): the extension of the last name, from its last dot, the dot included: .gz of a/b.tar.gz. A name that starts with its only dot (.profile), . and .. have none, and b. has ..
- p.stem(): the name without its extension, b.tar. Stem and extension together are always the name.
- p.parts(): a List[String] of its pieces: its volume and root, as written, when it has either (C:\, /, \\server\share\, C:), then each name, as written, . and .. included. Empty names are left out, and "" has no pieces.
Joining and normalizing#
a.join(b) is the path a then the path b, with a / between them unless a already ends in a separator or is a drive alone (C:). The other may be a Path or any &str, so dir.join("src/main.cone") makes no Path for the literal. Where b cannot be read from where a is, b wins: a b with a volume (D:\x, D:x, a share) is b itself, and a b with a root but no volume (\x) is the volume of a and then b. An empty a gives b, and an empty b gives a. Nothing is normalized: a/ and ../b join to a/../b.
p.normalize() is the same path spelled one way, read from its text alone:
- every separator is /, and two or more together are one (a//b is a/b), but for the two that start a share;
- each . name is dropped;
- each .. name drops the name before it (a/b/../c is a/c); a .. at the start of a relative path is kept, and one just after a root is dropped, since nothing is above a root;
- separators after the last name are dropped, but a root is kept (C:\ is C:/);
- a path that comes to nothing is ., the current folder, but a volume alone stays (C: stays C:).
The drive letter is kept as written. This reads text, not the file system: a .. after a symbolic link is dropped with the link, which on the disk may not be where the link's folder is. Windows takes / everywhere a path goes, and a normalized path is spelled one way, so two of them compare as bytes. A program handing a path to something that reads only \ (some of cmd.exe's commands) turns it round with the String's replaceByte('/', '\\').
Example#
imm work Path = "C:\\work"; // a literal is made a Path
imm p = work.join("src/main.cone");
show("join", p.view());
imm n = p.normalize();
show("normalize", n.view());
imm dir = n.parent();
show("parent", dir.view());
show("name", n.name());
show("stem", n.stem());
show("extension", n.extension());
show("volume", n.volume());
imm tar = Path.from("backup/a.tar.gz");
show("tar stem", tar.stem());
show("tar extension", tar.extension());
imm hidden = Path.from(".profile");
show("hidden extension", hidden.extension());
imm messy = Path.from("a\\b/./c//d/../e/");
imm tidy = messy.normalize();
show("tidy", tidy.view());
imm parts = Path.from("C:\\a\\b.txt").parts();
show("parts", String.join(parts.view(), "|").view());
show("join absolute", work.join("D:\\other").view());
show("join rooted", work.join("\\x").view());
if Path.from("C:\\x").isAbsolute() { printStr("C:\\x is absolute\n"); }
if not Path.from("C:x").isAbsolute() { printStr("C:x is not\n"); }
imm same = Path.from("a/b");
imm other = Path.from("a\\b");
if not (same == &other) { printStr("a/b and a\\b differ\n"); }
if same == &other.normalize() { printStr("normalized they meet\n"); }
This is the body of main; show(label, value) prints label = value. It prints:
join = C:\work/src/main.cone normalize = C:/work/src/main.cone parent = C:/work/src name = main.cone stem = main extension = .cone volume = C: tar stem = a.tar tar extension = .gz hidden extension = tidy = a/b/c/e parts = C:\|a|b.txt join absolute = D:\other join rooted = C:\x C:\x is absolute C:x is not a/b and a\b differ normalized they meet
Files and folders: the fs package#
A file is read or written whole; a folder is made, listed, walked and removed. Every function that names a file or folder takes a &Path, and nothing else. A string literal written where one is wanted is made a Path for the call (fs.readFile("notes.txt")), and a Path in a variable is lent, not given away: fs.readFile(&p) leaves p as it was. Any other text, such as a &str held in a variable, is made a Path first with Path.from(s). A relative path is read from the current folder.
A path the package makes (a listed entry's path, and each one walk gives) is the folder's path as given joined to the name by Path.join. The paths currentDir and absolute answer are normalized, every separator /: C:/work/notes.txt, which the C runtime and Windows take everywhere.
Failure: Result and FsError#
Everything that can fail answers a Result: Ok with the value (Result[void, FsError] where there is none), or Error with an FsError. A caller matches each Result; the value is moved out of a matched Ok by swapping an empty one in (text <=> got, below). An FsError says what was being done, to which path, and why:
- op: the function that failed, a String: "readFile", "makeDir", ...
- path: the Path it was given; and other, the second path of rename, empty for the others.
- code: the C library's error number, an i32, one of the posix package's E... constants (posix.ENOENT, posix.EACCES, ...).
- message(): one line to show a person, a new String: readFile notes.txt: No such file or directory, or rename a.txt to b.txt: File exists.
- notFound() asks whether nothing has that name (ENOENT), and alreadyExists() whether something does (EEXIST).
A path holding a NUL byte, which C would read as its end, fails with EINVAL before anything is asked of the system. Asking whether something is there is not a failure: exists, isDir and isFile answer a bool, false for anything they cannot see.
// Move a file's text out of the Result: an empty String is swapped in
fn textOf(p &Path) String {
mut text = String.empty();
match fs.readFile(p) {
case mut got Ok { text <=> got; }
case is Error { text.append("<unreadable>"); }
}
text;
}
match fs.readFile(&dir.join("missing.txt")) {
case imm e Error {
printStr("op: ");
printStr(e.op.view());
printStr("\npath: ");
printStr(e.path.view());
printStr("\ncode: ");
printInt(i64.from(e.code));
printStr("\nnotFound: ");
printStr(if e.notFound() {"yes";} else {"no";});
printStr("\nmessage: ");
imm m = e.message();
printStr(m.view());
printStr("\n");
}
case is Ok { return 1i32; }
}
With dir the folder scratch-errors, that prints:
op: readFile path: scratch-errors/missing.txt code: 2 notFound: yes message: readFile scratch-errors/missing.txt: No such file or directory
Whole files#
- fs.readFile(path) answers the whole of a file as a String, its bytes as they are: text or not, NULs included. A folder is refused (EISDIR).
- fs.writeFile(path, data) makes the file hold exactly data, making it if it is not there and replacing what it held if it is.
- fs.appendFile(path, data) adds data to the end, making the file if it is not there.
data is a &Array[u8]: a string literal, or a String's view(). A failure anywhere (the folder is not there, the disk is full, the file is open elsewhere) answers an Error naming the function.
Asking about a file#
fs.stat(path) answers a FileInfo, following a symbolic link to what it names:
- size: its size in bytes, an i64; 0 for a folder.
- modified: when it was last written, a Timestamp of the time package (UTC), in whole seconds: the C runtime's stat counts no finer.
- isDir, and isFile for a regular file: not a folder, a device or anything else.
fs.exists(path) asks whether anything has that name, fs.isDir(path) whether a folder does and fs.isFile(path) whether a regular file does. None of the three can fail.
imm name = Path.from("scratch-modtime.txt");
fs.writeFile(&name, "x");
match fs.stat(&name) {
case imm info Ok {
// modified is a Timestamp, whole seconds
imm age = Timestamp.now() - info.modified;
printStr("written less than 10 s ago: ");
printStr(if age < Duration.fromSeconds(10) {"yes";} else {"no";});
printStr("\nin a Timestamp of whole seconds: ");
imm whole = Timestamp.fromUnixSeconds(info.modified.unixSeconds());
printStr(if whole == info.modified {"yes";} else {"no";});
printStr("\n");
}
case is Error { return 1i32; }
}
fs.removeFile(&name);
It prints written less than 10 s ago: yes and in a Timestamp of whole seconds: yes, each on a line of its own.
Renaming and removing a file#
- fs.rename(from, to) gives a file or folder another name, which may be in another folder on the same volume. Where to is already there, POSIX replaces it; Windows refuses (EEXIST for a file, EACCES for a folder) and nothing changes.
- fs.removeFile(path) removes a file. A folder is refused (EISDIR): use removeDir or removeAll.
This program writes, appends, reads, asks, renames and removes in a folder of its own. yn turns a bool into "yes" or "no", and report prints "ok" or the message of a failed Result.
imm dir Path = "scratch-wholefiles";
fs.removeAll(&dir);
report("makeDir", fs.makeDir(&dir));
imm file = dir.join("notes.txt");
report("writeFile", fs.writeFile(&file, "first line\n"));
report("appendFile", fs.appendFile(&file, "second line\n"));
match fs.readFile(&file) {
case imm ok Ok {
printStr("read back:\n");
printStr(ok.view());
}
case is Error { return 1i32; }
}
match fs.stat(&file) {
case imm ok Ok {
printStr("size ");
printInt(ok.size);
printStr(", file ");
printStr(yn(ok.isFile));
printStr(", folder ");
printStr(yn(ok.isDir));
printStr("\n");
}
case is Error { return 1i32; }
}
imm moved = dir.join("renamed.txt");
report("rename", fs.rename(&file, &moved));
report("rename again", fs.rename(&file, &moved));
report("removeFile", fs.removeFile(&moved));
report("removeFile again", fs.removeFile(&moved));
report("readFile of a folder", fs.readFile(&dir));
report("removeDir", fs.removeDir(&dir));
(A few exists, isFile and isDir lines between are left out here.) It prints:
makeDir: ok writeFile: ok appendFile: ok read back: first line second line size 23, file yes, folder no exists yes, isFile yes, isDir of the folder yes rename: ok rename again: rename scratch-wholefiles/notes.txt to scratch-wholefiles/renamed.txt: No such file or directory removeFile: ok removeFile again: removeFile scratch-wholefiles/renamed.txt: No such file or directory readFile of a folder: readFile scratch-wholefiles: Is a directory removeDir: ok exists after: no
Where Windows refuses a rename because the name is taken, the message names both paths and the file already there is untouched:
rename scratch-errors/a.txt to scratch-errors/b.txt: File exists
Folders#
- fs.makeDir(path) makes a folder whose parent must be there. One already there is refused (EEXIST).
- fs.makeDirAll(path) makes a folder and every folder above it that is not there yet. A folder already there is success; a file in the way is refused (ENOTDIR), and so is a parent that cannot be made, whose error names it.
- fs.removeDir(path) removes an empty folder. One with anything in it is refused (ENOTEMPTY).
- fs.removeAll(path) removes what a path names and, for a folder, everything in it. Nothing there is success. The first thing that will not go (a read-only file, a file another program has open) ends it with its error, and what was removed before stays removed. As a guard against a path made wrongly, a path that is empty, is a root or a volume (C:/, /, C:, a share), or ends in . or .. is refused (EINVAL).
- fs.readDir(path) answers a List[DirEntry]: what a folder holds, . and .. left out, sorted by name, byte by byte (so "B" before "a", and a UTF-8 name by its bytes). A path that is not a folder is refused (ENOTDIR).
- fs.walk(root) answers a List[DirEntry] of every entry below a folder, depth first: each folder's entries in name order, each folder's own entries straight after it. The folder itself is not among them. The first folder that cannot be read ends the walk with its error.
DirEntry#
One entry of a folder, as readDir and walk give it:
- name: a String, its name in the folder.
- path: a Path, the folder's path as given joined to the name: src/main.cone. For walk, the root as given joined with the names down to it.
- isDir, isFile (a regular file) and isLink: a link, which is neither a folder nor a file here; stat its path to learn what it leads to.
imm root Path = "scratch-folders";
fs.removeAll(&root);
// makeDirAll makes every folder on the way; makeDir would need the parent
imm deep = root.join("src/util");
if fs.makeDirAll(&deep) is Error { return 1i32; }
fs.writeFile(&root.join("README.txt"), "read me");
fs.writeFile(&root.join("src/main.cone"), "mod main;");
fs.writeFile(&root.join("src/util/Strings.cone"), "mod strings;");
fs.writeFile(&root.join("src/util/arrays.cone"), "mod arrays;");
printStr("readDir of the root:\n");
match fs.readDir(&root) {
case imm listed Ok { list(&listed); }
case is Error { return 1i32; }
}
printStr("walk of the root:\n");
match fs.walk(&root) {
case imm all Ok { list(&all); }
case is Error { return 1i32; }
}
list prints each entry as its kind, its name and its path, one to a line; the kind comes from isDir, isFile and isLink. The program goes on to ask for a folder that is not there, a file as a folder, removeDir of a folder with things in it, and removeAll. It prints:
readDir of the root: file README.txt (scratch-folders/README.txt) folder src (scratch-folders/src) walk of the root: file README.txt (scratch-folders/README.txt) folder src (scratch-folders/src) file main.cone (scratch-folders/src/main.cone) folder util (scratch-folders/src/util) file Strings.cone (scratch-folders/src/util/Strings.cone) file arrays.cone (scratch-folders/src/util/arrays.cone) readDir scratch-folders/nowhere: No such file or directory readDir scratch-folders/README.txt: Not a directory removeDir scratch-folders: Directory not empty removed removed again, nothing there
Note the order: README.txt comes before src because the names are compared byte by byte, and Strings.cone before arrays.cone for the same reason.
The package's own examples/sizes.cone walks a folder given on the command line and prints each file's size with fs.stat.
Links#
stat, isDir and isFile follow a symbolic link to what it names, as POSIX's stat does. A listing does not: an entry that is a link says so (isLink) and is then neither a folder nor a file. walk never goes down into a linked folder, and removeAll removes a link itself, never what it leads to. On Windows a link is any reparse point: a symbolic link or a directory junction, and also some cloud-storage placeholders, which are then left whole.
The current folder#
- fs.currentDir() answers the folder relative paths are read from, a normalized Path (C:/work).
- fs.setCurrentDir(path) makes path that folder, for the whole program (every thread).
- fs.absolute(path) answers the absolute, normalized path of path, read from the current folder: a.txt in C:/work is C:/work/a.txt. Only its text is read: whatever is or is not there, links are not followed, and .. drops the name before it. "" is the current folder.
// The last name of the current folder
fn here() String {
match fs.currentDir() {
case imm p Ok { String.from(p.name()); }
case is Error { String.from("<error>"); }
}
}
// absolute reads only the text: nothing need be there, '..' drops a name
match fs.absolute("scratch-cwd/sub/../y.txt") {
case imm p Ok {
printStr("absolute is absolute: ");
printStr(if p.isAbsolute() {"yes";} else {"no";});
printStr(", ends in ");
printStr(p.name());
printStr(", in ");
imm up = p.parent();
printStr(up.name());
printStr("\n");
}
case is Error { return 1i32; }
}
fs.removeAll("scratch-cwd");
if fs.makeDir("scratch-cwd") is Error { return 1i32; }
// Relative paths are read from the current folder, so moving it moves them
if fs.setCurrentDir("scratch-cwd") is Error { return 1i32; }
printStr("now in ");
imm inside = here();
printStr(inside.view());
printStr("\n");
fs.writeFile("inner.txt", "written inside");
printStr("inner.txt exists here: ");
printStr(if fs.exists("inner.txt") {"yes";} else {"no";});
printStr("\n");
if fs.setCurrentDir("..") is Error { return 1i32; }
printStr("back in ");
imm back = here();
printStr(back.view());
printStr("\n");
printStr("scratch-cwd/inner.txt exists: ");
printStr(if fs.exists("scratch-cwd/inner.txt") {"yes";} else {"no";});
printStr("\n");
match fs.setCurrentDir("no such folder") {
case imm e Error {
imm m = e.message();
printStr(m.view());
printStr("\n");
}
case is Ok { return 1i32; }
}
fs.removeAll("scratch-cwd");
The program first prints start in and here(). Its output, run from the folder cone-probes-docs4-2026-10-11:
start in cone-probes-docs4-2026-10-11 absolute is absolute: yes, ends in y.txt, in scratch-cwd now in scratch-cwd inner.txt exists here: yes back in cone-probes-docs4-2026-10-11 scratch-cwd/inner.txt exists: yes setCurrentDir no such folder: No such file or directory
UTF-8 names#
A name is UTF-8. The first call that hands a path to the C runtime sets the C library's character type (LC_CTYPE) to UTF-8 when the program has left it at C's default, "C". On Windows that is what makes the runtime read and answer file names as UTF-8 (without it, an "é" names some other file). It changes nothing else: numbers, dates and sorting keep C's own forms, and a program that has chosen a character type of its own keeps it.
Environment variables: the env package#
The program's environment variables, read, set and removed. A program it starts inherits them as they are when it starts. Names and values are text (&str), UTF-8, whatever locale the program has set: a variable inherited beyond ASCII reads as the UTF-8 of what Windows holds, and one set reaches a child as that.
- env.get(name) answers an Option[String]: the value, or None when the variable is not set. A variable not being set is not a failure. A name that is no name (empty, or holding = or a NUL byte) is simply never set, so get answers None for it.
- env.set(name, value) sets the variable, replacing any value it had.
- env.unset(name) removes it. One not set is left so.
Setting and removing cannot fail but for a program error: a name that is no name (empty, or holding = or a NUL byte), a value holding a NUL, or either not UTF-8. That is a panic at the caller's line, as Rust's set_var panics. Changing the environment while another thread reads it is not safe in C, and so not here: set variables before starting threads.
Windows differs from POSIX in two ways the package does not hide:
- names are compared without regard to ASCII case: Path and PATH are one variable;
- a variable cannot be set to the empty value: env.set(name, "") removes it, and get then answers None.
imm name = "CONE_DOC_EXAMPLE";
env.unset(name);
show(name);
env.set(name, "first");
show(name);
env.set(name, "second value, with spaces");
show(name);
// Windows reads a name without regard to ASCII case
show("Cone_Doc_Example");
// Windows cannot hold an empty value: setting one removes the variable
env.set(name, "");
show(name);
// UTF-8 in the name and the value
env.set("CONE_DOC_É", "日本 é");
show("CONE_DOC_É");
env.unset("CONE_DOC_É");
// A name that is no name is simply never set
show("");
show("A=B");
show(name) prints the name, a colon, and the value in square brackets or "not set". It prints:
CONE_DOC_EXAMPLE: not set CONE_DOC_EXAMPLE: [first] CONE_DOC_EXAMPLE: [second value, with spaces] Cone_Doc_Example: [second value, with spaces] CONE_DOC_EXAMPLE: not set CONE_DOC_É: [日本 é] : not set A=B: not set
Setting a variable named A=B stops the program:
setting a variable named A=B panic at envpanic.cone:8: env: a variable's name may not hold '='
Running programs: the process package#
The process package runs another program and waits for it, with what it writes captured, and answers this program's own arguments and path. It is the same interface Go, Rust, Python, Zig and Odin each give: a list of arguments, a folder, a time limit; an exit code and two captured outputs.
run, and what it answers#
process.run(args, opts, dir) starts the program args[0] with the rest of args as its arguments, waits until it ends, and answers a Result[Output, ProcessError]. args is a &Array[String] (a List[String]'s view()); the program and its arguments are text, not paths. Both opts and dir have defaults, so process.run(list.view()) is a whole call.
An Output holds:
- exitCode: the exit code, all 32 bits of it, a u32: 0, 1, 255, or a status such as 3221225477 (0xC0000005, an access violation) or 3221226505 (0xC0000409), what a Cone panic ends with on Windows.
- timedOut: whether it was stopped because its time ran out; its exit code is then 1.
- stdout and stderr: what it wrote to its standard output and its standard error, two Strings, kept apart, byte for byte; empty when it inherited them. A C program's text mode writes \n as \r\n, and that is what comes back.
- success(): exit code 0 within the time.
The exit code is a value, not an error. A nonzero exit is the answer, which the caller reads, as in Python's run and Rust's output. An Error is only a program that could not be started: none of that name, a folder that is not there, an argument Windows cannot be given.
By default the child reads its standard input from NUL (it finds nothing there, never this program's keyboard). Both captured streams are read at once, so a child that writes a great deal to both never stalls on a full pipe. A child shares this program's console, where it has one, so Ctrl+C reaches both; from a program with no console, a console child is started so that no window opens for it.
This program is its own child: started with "child" as its first argument it writes to both streams and exits with code 3, and the parent finds its own executable with currentExe.
fn child() i32 {
printStr("to standard output\n");
eprintStr("to standard error\n");
3i32;
}
fn main() i32 {
imm mine = process.args();
if mine.len() > 1usize and mine[1usize].equals("child") { return child(); }
// The program to run is this one: its own path, from currentExe
mut args = List[String].empty();
match process.currentExe() {
case imm exe Ok { args.push(String.from(exe.view())); }
case is Error { return 1i32; }
}
args.push(String.from("child"));
match process.run(args.view()) {
case imm o Ok {
printStr("exit code ");
printUInt(u64.from(o.exitCode));
printStr(", success ");
printStr(if o.success() {"yes";} else {"no";});
printStr(", timed out ");
printStr(if o.timedOut {"yes";} else {"no";});
printStr("\nstdout: ");
imm out = o.stdout.trim();
printStr(out.view());
printStr("\nstderr: ");
imm err = o.stderr.trim();
printStr(err.view());
printStr("\n");
}
case imm e Error {
imm m = e.message();
printStr(m.view());
printStr("\n");
return 1i32;
}
}
// A program that cannot be started is the Error
mut missing = List[String].empty();
missing.push(String.from("no-such-program-anywhere"));
match process.run(missing.view()) {
case imm e Error {
imm m = e.message();
printStr(m.view());
printStr("\nnotFound: ");
printStr(if e.notFound() {"yes";} else {"no";});
printStr(", code ");
printUInt(u64.from(e.code));
printStr("\n");
}
case is Ok { printStr("started?\n"); }
}
0i32;
}
It prints:
exit code 3, success no, timed out no stdout: to standard output stderr: to standard error run no-such-program-anywhere: The system cannot find the file specified notFound: yes, code 2
Options#
An Options is built with named fields, each with a default: new process.Options(timeout: Duration.fromSeconds(60), inherit: true).
- timeout: a Duration of the time package. Above zero, the child is stopped when that long has passed (see The time limit). Zero, the default, waits as long as it runs.
- inherit: false by default. With true the child shares this program's standard input, output and error instead, and nothing is captured: what congo run wants. Flush what this program has printed first (printFlush()), or the child's lines come before it.
- crashReport: true by default. See A crash.
The working folder, the environment, and finding the program#
The last parameter of run and runLine is the folder the child starts in, a &Path that defaults to "", this program's current folder. Like the paths of fs, a string literal there is made a Path for the call, and a Path in a variable is lent, not given away: process.run(args.view(), new process.Options(), "build").
The child inherits this program's environment as it is when run is called: set a variable with the env package first. There is no environment of the child's own to pass.
The program is found as Windows' CreateProcessW finds it: a name with no folder is looked for in the folder this program's executable is in, then the current folder, then Windows' system folders, then each folder on PATH, with .exe added when it has no extension. A relative path is read from this program's current folder, never from the working folder given. A batch file (.bat, .cmd) is run by cmd.exe, which reads its line by its own rules, not the ones below: an argument from outside the program is not safe on a batch file's line.
The time limit#
With a timeout above zero, the child is stopped when that long has passed since it started (measured on the time package's steady clock, and waited for in whole milliseconds, rounded up), and so is every program it has started and not yet seen end: it runs in a Windows job object of its own, and the whole job is ended. The Output then says timedOut, its exit code is 1, and what it wrote until then is kept. With no limit nothing is put in a job, and a program it starts that is still running when it ends is left running. A child that ends, though, while a program it started still holds its output open has not finished writing: run waits for that too, which only a time limit cuts short.
fn child(mode &String) i32 {
if mode.equals("where") {
match fs.currentDir() {
case imm d Ok { printStr(d.name()); }
case is Error { printStr("<error>"); }
}
printStr(" ");
match env.get("GREETING") {
case imm g Some { printStr(g.view()); }
case is None { printStr("<none>"); }
}
printStr("\n");
return 0i32;
}
printStr("going to sleep\n");
printFlush();
thread.sleep(Duration.fromSeconds(10));
printStr("woke up\n");
0i32;
}
The child says which folder it is in and what GREETING holds, or writes a line and sleeps ten seconds. command(mode) builds the argument list of this program's own executable followed by the mode. The parent runs the first in a folder of its own with the variable set, then without either, then in a folder that is not there, then runs the sleeper under a time limit, then with inherit:
fs.removeAll("scratch-options");
fs.makeDir("scratch-options");
// In a folder of the child's own, with a variable set first
env.set("GREETING", "hello from the parent");
imm inFolder = command("where");
match process.run(inFolder.view(), new process.Options(), "scratch-options") {
case imm o Ok {
imm text = o.stdout.trim();
printStr("child says: ");
printStr(text.view());
printStr("\n");
}
case is Error { return 1i32; }
}
env.unset("GREETING");
// Without a folder it starts where this program is; with the variable unset
match process.run(inFolder.view()) { ... prints the same way ... }
// A folder that is not there is a failure to start
match process.run(inFolder.view(), new process.Options(), "scratch-options/missing") {
case imm e Error {
imm m = e.message();
printStr(m.view());
printStr("\n");
}
case is Ok { printStr("started?\n"); }
}
// A time limit: stopped after a third of a second, not ten seconds
imm sleeper = command("sleep");
match process.run(sleeper.view(), new process.Options(timeout: Duration.fromMillis(300))) {
case imm o Ok {
printStr("timed out ");
printStr(if o.timedOut {"yes";} else {"no";});
printStr(", exit code ");
printUInt(u64.from(o.exitCode));
printStr(", success ");
printStr(if o.success() {"yes";} else {"no";});
imm text = o.stdout.trim();
printStr(", wrote: ");
printStr(text.view());
printStr("\n");
}
case is Error { return 1i32; }
}
// inherit: the child writes straight into this program's output
printStr("child, sharing this output:\n");
printFlush();
imm shared = command("where");
match process.run(shared.view(), new process.Options(inherit: true)) {
case imm o Ok {
printStr("captured nothing: ");
printStr(if o.stdout.len() == 0usize {"yes";} else {"no";});
printStr("\n");
}
case is Error { return 1i32; }
}
It prints the following (the third line starts with the path of the program's own executable, which is shortened here):
child says: scratch-options hello from the parent child says: cone-probes-docs4-2026-10-11 <none> run C:/.../runoptions.exe in scratch-options/missing: The directory name is invalid timed out yes, exit code 1, success no, wrote: going to sleep child, sharing this output: cone-probes-docs4-2026-10-11 <none> captured nothing: yes
A crash#
A child that crashes (a Cone panic ends in a crash on Windows) is handed by Windows to its crash reporting, Windows Error Reporting, as any program's crash is. That holds the dying child, and its executable locked, while it writes a report: a fifth of a second on a quiet machine, and many seconds when many crash at once. A test runner, whose children panic on purpose, wants none of that. With crashReport: false the child runs as this program's debuggee, so its crash comes here first, and ends the child at once, with the exit code Windows would have given it (the exception's own code: 3221226505 for a panic), and nothing is reported. Nothing else changes for it: the exceptions it handles itself are its own, the programs it starts are no debugger's, and its heap is the one it would have had (Windows gives a program started under a debugger its checking debug heap, so its environment has _NO_DEBUG_HEAP=1 added, where that is not set already). No other debugger can attach to such a child.
Arguments and the command line#
Windows hands a program one command line, and the program splits it. run writes args as the line that Windows' own splitter, CommandLineToArgvW, and the C runtime's both read back as they were: an argument with a space, a tab, a line break or a ", or an empty one, is put in double quotes, each " escaped with a backslash, and the backslashes before a " doubled. The program's name, first, is read by a simpler rule, so it is quoted only when it holds a space or a tab, and may not hold a " at all. Everything is UTF-8, and goes to Windows as UTF-16.
- process.commandLine(args) answers that line as a String, so a program can show what run will hand Windows.
- process.runLine(line, opts, dir) hands Windows a line as it is written, for a program with rules of its own: cmd /c and what follows it. It takes the same Options and folder, and answers the same.
An argument that is not UTF-8 is refused (error 1113, ERROR_NO_UNICODE_TRANSLATION), as is one holding a NUL (error 87, ERROR_INVALID_PARAMETER); an empty args or a program name holding a " is refused too.
// The line run hands Windows for some awkward arguments
mut args = List[String].empty();
args.push(String.from("tool"));
args.push(String.from("plain"));
args.push(String.from("two words"));
args.push(String.from("say \"hi\""));
args.push(String.from(""));
args.push(String.from("C:\\dir with space\\"));
printStr("line: ");
imm line = process.commandLine(args.view());
printStr(line.view());
printStr("\n");
// ... run this program again with those arguments, as in the first example,
// and print each one it reads back in square brackets ...
// A line written by hand, for a program with rules of its own
match process.runLine("cmd /c echo hello from cmd") {
case imm o Ok {
printStr("runLine: ");
imm text = o.stdout.trim();
printStr(text.view());
printStr("\n");
}
case is Error { return 1i32; }
}
It prints:
line: tool plain "two words" "say \"hi\"" "" "C:\dir with space\\" child read: [plain][two words][say "hi"][][C:\dir with space\] runLine: hello from cmd
ProcessError#
An Error from run, runLine or currentExe holds a ProcessError:
- op: the function that failed, a String: "run", "runLine", "currentExe".
- program: the program, as run was given it (runLine's whole line).
- dir: the Path of the working folder it was to start in, or "".
- code: Windows' error number (GetLastError's), a u32. The package names the ones it gives or reads: process.errorFileNotFound (2), errorPathNotFound (3), errorInvalidParameter (87), errorInvalidName (123), errorDirectory (267) and errorNoUnicodeTranslation (1113).
- message(): one line for a person, in Windows' own words (in English where the system has them): run nosuch: The system cannot find the file specified, or run conec in build/x: The directory name is invalid.
- notFound(): whether there was no such program (or no folder on its path), ERROR_FILE_NOT_FOUND or ERROR_PATH_NOT_FOUND.
This program: args and currentExe#
- process.args() answers a List[String]: this program's own arguments, its name first, in UTF-8 whatever the system's code page, as Windows' parser splits them. main's argv says the same for ASCII, and loses whatever the code page cannot hold.
- process.currentExe() answers a Result[Path, ProcessError]: the path of this program's executable, normalized, every separator /.
imm all = process.args();
printStr("count: ");
printUInt(u64.from(all.len()));
printStr("\n");
mut i = 1usize;
while i < all.len() {
printStr("argument ");
printUInt(u64.from(i));
printStr(": ");
printStr(all[i].view());
printStr("\n");
i = i + 1usize;
}
match process.currentExe() {
case imm exe Ok {
printStr("running ");
printStr(exe.name());
printStr(" (absolute: ");
printStr(if exe.isAbsolute() {"yes";} else {"no";});
printStr(")\n");
}
case imm e Error {
imm m = e.message();
printStr(m.view());
printStr("\n");
}
}
Run with first "second arg" é after the program's name, it prints:
count: 4 argument 1: first argument 2: second arg argument 3: é running myargs.exe (absolute: yes)
The posix package beneath#
The posix package is not a package a program usually calls. It is raw bindings to the POSIX functions a program needs beyond the ISO C library, for files, directories, processes and the environment, built on the libc package: C names, C types, C strings, errors as C reports them. The Cone-flavoured layer over it is fs (files and folders) and env (the environment). Its constants are the ones an FsError's code is compared with (posix.ENOENT, posix.EEXIST, posix.EISDIR, ...).
Cone cannot yet choose declarations by target, so this version binds the Windows C runtime, which is not a POSIX system. Where the runtime's function takes POSIX's arguments in POSIX's order, the POSIX name binds straight to the runtime's symbol (stat to _stat64); where the arguments differ (mkdir's mode, setenv's overwrite flag, realpath's order), the POSIX name is a small function that calls the runtime's; and where the runtime has no equivalent, the function is written in Cone over what it does have: opendir, readdir and lstat over the runtime's _findfirst64. This is why fs and env run on Windows only for now.