12. Input/Output
This chapter is the I/O how-to: console, files, paths, environment variables, and host process facts. Prefer the io.* namespace. Flat aliases such as print and readFile still run, but the editor warns malda-style. Signatures, git helpers, glob / grep, document extractors, and Spectre.Console live in 13. Built-in Functions.
File I/O is a host capability. The interpreter and C# transpile support it; the JavaScript backend does not (file-io is no on the backend capability matrix).
12.1 Console Output
io.print(value) takes exactly one argument, converts it with string(value), and writes a line (a trailing newline). There is no println. Concatenate or interpolate before the call if you need several pieces on one line.
io.print("Hello, World!");
var name = "Alice";
io.print($"Hello, {name}!"); // interpolation — the $ is required
io.print("n is {n}"); // literal braces: a plain string does not interpolate
- Use
$"…"or"Value of x: " + string(x). A plain"n is {n}"prints the braces. - Numbers, booleans, arrays, and objects print their string form. For JSON text, call
toJSON(value)first. - Rich colour, tables, panels, and progress bars are
AnsiConsole.*— see 13.12 Spectre.Console.AnsiConsole.markupdoes not add a newline; usemarkupLinefor a standalone line. - When stdout is not a terminal, Spectre.Console strips ANSI and Unicode panel borders fall back to square corners. That is expected for pipes and redirected files.
12.2 Console Input
io.input(prompt?) writes an optional prompt (no extra newline) and reads one line from stdin. The result is always a string. Convert it if you need a number.
var name = io.input("Enter your name: ");
var age = int(io.input("Enter your age: "));
io.print($"Hello, {name}. Next year you will be {age + 1}.");
null. At EOF every later io.input also returns "". A loop that only advances on non-empty input never terminates. Treat blank as quit:
while (true) {
var raw = io.input("? ");
if (str.trim(raw) == "") {
break;
}
io.print(raw);
}
The call is asynchronous: execution pauses until a line arrives, then resumes at the same statement. It works inside loops, functions, and nested control structures.
For malformed numbers prefer toIntOr(raw, fallback) or toIntOrNull(raw) over bare int(...), which throws. See 13.1 Type Conversion.
12.3 Files
Paths are strings (or a matching capability token). Ordinary failures return null or false; they do not throw. Check the result.
12.3.1 Read and write
io.writeFile(path, content) overwrites the file and returns true on success, false on I/O failure. Strings are written as UTF-8. Objects and arrays are serialized with toJSON first. io.readFile(path) returns the whole file as a string, or null when the path is missing or unreadable.
io.writeFile("_rm_io_notes.txt", "hello");
io.print(io.hasFile("_rm_io_notes.txt"));
io.print(io.readFile("_rm_io_notes.txt"));
io.deleteFile("_rm_io_notes.txt");
io.print(io.hasFile("_rm_io_notes.txt"));
io.writeFile("_rm_io_json.txt", dict { "ok": true });
var obj = parseJSON(io.readFile("_rm_io_json.txt"));
io.print(obj.ok);
io.deleteFile("_rm_io_json.txt");
io.print(io.readFile("_rm_io_missing.txt") == null);
12.3.2 Line ranges and line arrays
Line numbers are 1-based.
io.readFile(path, start)— from that line to the end. A negativestartmeans the last N lines (-30is the last 30).io.readFile(path, start, end)— inclusive range. A negativeendcounts from the end of the file.- If
startis past the last line, the result is""(empty string), notnull. io.readTextFileLines(path)— UTF-8 lines as an array, ornullif the file cannot be read. Prefer this overstr.split(io.readFile(path), "\n")for large files.
io.writeFile("_rm_io_lines.txt", "a\nb\nc\nd");
io.print(io.readFile("_rm_io_lines.txt", 2, 3));
io.deleteFile("_rm_io_lines.txt");
12.3.3 Directories, delete, and binary
| Call | Result |
|---|---|
io.hasFile(path) | true when a file exists (disk or embed:) |
io.hasDirectory(path) | true when a directory exists |
io.ensureDir(path) | Creates the directory and missing parents; no-op if it already exists. Throws on failure. Rejects embed: |
io.listDirectory(path) | Array of { name, type, path }. type is "file" or "directory". Missing directories yield []. Empty path means "." |
io.deleteFile(path) | true if the file was deleted or was already absent; false on an empty path or I/O failure |
io.readFileBase64(path) / io.writeFileBase64(path, b64) | Raw bytes as base64. Read returns null when missing; write returns false on invalid base64 |
io.writeFile("_rm_io_list.txt", "x");
var items = io.listDirectory(".");
var found = false;
for (var item in items) {
if (item.name == "_rm_io_list.txt") {
found = true;
}
}
io.print(found);
io.deleteFile("_rm_io_list.txt");
insertAtLine, replaceInFile, and editFile edit text in place — catalog and failure style in 13.8. insertAtLine is interpreter-only.
Digital text from documents: pdf.extractText(path, password?) (PdfPig text layer, no OCR) and doc.extractText(path) (.docx only). Those do throw on a missing file. See 13.8.1.
12.3.4 Embedded folders (embed:)
Compile with malda compile … --embed-folder <dir[=alias]> to pack a directory into the executable. At runtime, read with the virtual scheme embed:<alias>/<relative>. Nothing is extracted to disk. Writes (io.writeFile, io.ensureDir, io.deleteFile, …) reject embed: paths. io.hasEmbeddedFolder(alias) / io.embeddedFolderRoot(alias) inspect what was packed. Full example in 13.8.0.
12.4 Paths
Join and inspect paths instead of concatenating slashes. io.pathJoin uses the host separator. io.pathNormalize returns an absolute path.
io.print(io.getFileName("docs/notes.md"));
io.print(io.pathGetExtension("docs/notes.md"));
io.print(io.isPathUnder("brain", "brain/notes/a.md"));
io.getDirectoryName(path)— parent directory, or""for a bare filename.io.pathExists(path)— file or directory.io.isPathUnder(root, path)—truewhenpathresolves torootor a descendant. Uses a directory-separator boundary (demodoes not matchdemo-evil). Works for disk paths andembed:roots. Traversal such asbrain/../secret.txtisfalse.getProgramDirectory()— folder of the running.maldaunder the interpreter, or the compiled executable’s directory aftermalda compile. Not on theioobject.
12.5 Environment and Host
These read the process environment and the machine the program is running on. They are not file I/O, but they are how programs find keys, args, and a place to load sidecar files.
io.print(io.getEnvOr("MALDA_RM_IO_MISSING_VAR_7D05", "fallback"));
var host = getHostPlatform();
io.print(str.length(host.pathSeparator));
io.getEnv(name)— string, ornullwhen unset. Do not pass thatnulltostr.trim; useio.getEnvOr(name, default?)(empty string when the default is omitted) orstr.trimText.io.hasEnv(name)— whether the variable is set.getHostPlatform()—{ os, arch, pathSeparator, description }. No arguments.getCommandLineArgs()— array of strings passed to the program, excluding the program name. Same in interpreted and transpiled runs.
Assistant config under ~/.malda (getMaldaHome, getMaldaConfig) is documented with the personal assistant.
12.6 Capability Tokens
@effects("io") is a name allow-list. A tool that takes a path string can still be given "/etc/passwd". Mint an unforgeable token and pass that instead. Tokens are sealed host objects — a dict { kind, path } is not a token.
var notes = cap.fileRead("notes.md");
io.print(notes.kind);
io.print(cap.is(notes, "fileRead"));
- Mint:
cap.fileRead(path),cap.fileWrite(path),cap.dirList(path). - Consume:
cap.read/cap.write/cap.list, or pass the token toio.readFile/io.writeFile/io.listDirectory. Ordinary string paths still work for non-tool code. cap.confine(token, relativePath)narrows a token; paths outside the parent throw.
Full contract and a forged-dict test: 13.19.3 and Examples/Tools/capability_tokens.malda.
12.7 Searching Files
io.glob(pattern, dirPath?) finds paths. io.grep(pattern, filePath) searches contents. Both take a required first argument and then optional positional arguments. On a missing path or a bad regex they return an error string, not a collection — guard with typeOf(result) == "string".
io.writeFile("_rm_io_glob.txt", "needle");
var found = io.glob("_rm_io_glob.txt", ".");
io.print(found.count);
io.deleteFile("_rm_io_glob.txt");
Default glob cap is 200 results (hard cap 500); the result is { items, count, truncated } with each item { name, type, path }. grep defaults to recursive search with line numbers. Argument lists and the count-only shape are in 13.8.2.
12.8 Sleep
sleep(milliseconds) pauses without spinning the CPU. The argument must be a non-negative integer. It is a built-in, not an io.* member. Useful for keep-alive loops around a server.
sleep(0);
io.print("awake");
while (server.isRunning) {
sleep(1000);
}
sleep, io.writeFile, and other deny-listed effects raise WF1001 / WF1002 when they run in a deterministic workflow section — including when nested in a helper. Put clock, sleep, and I/O inside a step. See 22. Durable Workflows.
12.9 What This Chapter Is Not
| Need | Go to |
|---|---|
| HTTP client / REST calls | 28. REST Web Client |
HTTP / HTML / ui.* servers | 23. Web UI Overview |
| SQL clients | 16. Database Support |
| Shell commands | runCommand in 13.10 (blocked dangerous names, 1-hour timeout cap) |
| Git | 13.9 |
| Agent file tools | createReadFileTool and siblings in 18. Agent Orchestration |
See Also
- 7. Expressions — string interpolation
- 13. Built-in Functions — I/O catalog, Spectre.Console, git,
runCommand - 9. Functions —
@pure/@effects("io") - 22. Durable Workflows — I/O must live inside
step Examples/Tools/file_operations.malda,Examples/Tools/capability_tokens.malda