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 converts a file path to a web-accessible URL with cache-busting:

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

Key Differences from Other Languages

See Also