Menu

File I/O

Parsley uses file handles and I/O operators for reading and writing files. You create a handle that describes the file and its format, then use <== to read or ==> to write. The handle determines how data is serialized and deserialized.

I/O Operators

Operator Direction Description
<== Read Read file contents into a variable
==> Write Write data to a file (overwrites)
==>> Append Append data to a file
=/=> Remote Write Write data to a network target (HTTP/SFTP)
=/=>> Remote Append Append data to a network target (SFTP)
let config <== JSON(@./config.json)       // read
{name: "Alice"} ==> JSON(@./output.json)  // write
"log entry\n" ==>> text(@./app.log)       // append

// Network targets use =/=> instead of ==>
{name: "Alice"} =/=> JSON(@https://api.example.com/users)  // remote write

Note: For network targets (HTTP URLs and SFTP connections), use =/=> and =/=>> instead of ==> and ==>>. The / in the operator visually signals that data crosses a network boundary. Both fetch (<=/=) and remote write (=/=>, =/=>>) can also be used as expressions — see HTTP & Networking for details.

File Handles

File handles are created by calling a format function with a path. They don't read or write anything on their own — the I/O operators do the actual work.

Function Format Read type Description
JSON(path) JSON dictionary/array JSON file
YAML(path) YAML dictionary/array YAML file
CSV(path) CSV table CSV file (returns a table)
PLN(path) PLN any Parsley Literal Notation
text(path) Plain text string Raw text content
lines(path) Lines array Array of strings (one per line)
raw(path) Binary array Raw byte array
MD(path) Markdown dictionary {html, md, raw} — rendered HTML, frontmatter, and source
SVG(path) SVG string SVG content (strips XML prolog)
file(path) Auto varies Auto-detect format from extension
dir(path) Directory array Directory listing

All file handles accept a path literal as the first argument:

let handle = JSON(@./data.json)
let data <== handle

Or inline:

let data <== JSON(@./data.json)

Reading Files

JSON

let config <== JSON(@./config.json)
config.database.host             // "localhost"

Tip: For configuration files with Parsley-specific types (dates, money, paths), use PLN instead: let config <== PLN(@./config.pln). PLN preserves all Parsley types—see Data Formats.

CSV

CSV reads return a table (not a raw array):

let sales <== CSV(@./sales.csv)
sales.count()                    // number of rows
for (row in sales) {
    row.name + ": " + row.amount
}

Plain Text

let readme <== text(@./README.md)
readme.length()                  // character count

Lines

let items <== lines(@./todo.txt)
items.length()                   // number of lines
items[0]                         // first line

Markdown with Frontmatter

MD() reads a Markdown file, parses YAML frontmatter, and renders the content to HTML:

let doc <== MD(@./post.md)
doc.md.title                     // frontmatter field
doc.html                         // rendered HTML string
doc.raw                          // original Markdown (frontmatter stripped)

To parse Markdown already in a string, use the string method instead:

let result = source.parseMarkdown({ids: true})
// result.html, result.md, result.raw

Auto-detect

file() picks the format based on the file extension:

let data <== file(@./config.json)  // reads as JSON
let text <== file(@./notes.txt)    // reads as text

Writing Files

Use ==> to write (overwrite) and ==>> to append:

// Write PLN (preserves Parsley types like dates, money, paths)
{name: "Alice", joined: @2024-01-15, balance: $100.00} ==> PLN(@./user.pln)

// Write JSON (for external systems)
{name: "Alice", age: 30} ==> JSON(@./user.json)

// Write plain text
"Hello, world!" ==> text(@./greeting.txt)

// Append to a log
"New entry\n" ==>> text(@./app.log)

The write operator serializes the data according to the file handle's format. Writing a dictionary to a JSON handle produces formatted JSON; writing a string to a text handle writes it verbatim.

When to use PLN vs JSON: Use PLN for internal Parsley data (configs, caches, data files)—it preserves dates, money, paths, and other Parsley types. Use JSON for external interoperability (APIs, other languages). See Data Formats for details.

Directory Operations

dir()

Create a directory handle and read its contents:

let files <== dir(@./uploads)
for (f in files) {
    f.name                       // filename
}

fileList()

List files matching a glob pattern. Takes a single path or string pattern and returns an array of file handles — read one with <==, or get its path via .path:

let sources = fileList("./src/*.pars")
sources.length()                 // number of matching files

for (f in sources) {
    f.path.filename              // "main.pars"
    let content <== f            // read the file
}

⚠️ ** matches files in subdirectories but not in the root of the pattern: fileList("./docs/**/*.md") misses ./docs/index.md. List both patterns and join them with ++ if you need the whole tree.

Error Handling with Read

Use destructured read with {data, error} for safe file operations:

let {data, error} <== JSON(@./config.json)
let config = if (error) {
    defaults                     // use defaults on error
} else {
    data
}

When using the {data, error} pattern, read errors are captured instead of halting execution. Without it, a missing file or parse error produces an IO-class error.

Assets

The asset() builtin maps a file path under the public directory to its web-root-relative URL by stripping the public_dir prefix:

<img src={asset(@./public/images/logo.png)} alt="Logo"/>
// Produces: <img src="/images/logo.png" alt="Logo" />

It is a pure path-to-URL transform — it does not hash file contents or add a cache-busting suffix, and it also accepts the file dictionaries returned by fileList().

Cache-busting is a separate, Basil-only feature. For content-hashed URLs like /assets/logo-a1b2c3d4.png, use publicUrl(), which registers and hashes the file. It is only available inside Basil server handlers — see publicUrl() in the Basil manual.

Key Differences from Other Languages

  • Operators instead of functions<== and ==> replace readFile()/writeFile(). The operator syntax makes the data flow direction visually clear.
  • Format-aware handles — the handle knows the file format, so you don't manually parse JSON or serialize CSV. let data <== CSV(@./file.csv) returns a ready-to-use table.
  • PLN format — Parsley has its own serialization format (Parsley Literal Notation) that round-trips all Parsley types, including dates, money, and paths.
  • Markdown is a first-class formatMD() handles frontmatter parsing and HTML rendering in one step.
  • No streams — file I/O is synchronous and reads/writes the entire file at once.

See Also

  • Paths — path literals and path manipulation
  • URLs — URL literals used as file handle sources
  • HTTP & Networking<=/= fetch, =/=> remote write, and =/=>> remote append operators
  • Error Handling{data, error} destructuring pattern
  • Data Formats — CSV and Markdown parsing details