Stream: show and tell

Topic: Ultra-lightweight near-pure cli pipe transformers for files


view this post on Zulip Scott Campbell (Sep 14 2026 at 02:11):

So I needed a tool that takes a .json file, transforms it, and then produces a file... This is just some auxiliary tooling & scripting, for my main roc project.

So was thinking... I don't want to use anything heavy for this...
And I want a nice lightweight scripting language...
(NOT python, nodejs, bash, deno or bun).

And the obvious answer to this, is just more roc code, since I already have roc compiler as a dependency.

And I was looking at the various ways to achieve this...
I need ultra minimal lightweight filesystem IO.
Explicit permissions & capabilities based is preferred.
Streaming support could be nice.

Which platform can i use... hmmm...
basic-cli...?

hmmm.... I know! I don't even need filesystem apis... just stdin & stdout, and let the OS pipe the file to and from disk.

Just the roc-platform-template-zig, as a platform!

And there are 2 code examples that work great...
examples/cli args
examples/multiline echo

And the end result?

cat extension_api.json | roc main.roc --json-to-roc > my_generated_code.roc

cat extension_api.json | roc main.roc --json-to-zig > my_generated_code.zig

Output:

===ARGS===
args: ["main.roc", "--json-to-roc"]
===Prefix===
{
    "header": {}
}
===Suffix===

Source:

app [main!] { roc: "nightly-2026-09-11-793f9d8", pf: platform "https://github.com/lukewilliamboswell/roc-platform-template-zig/releases/download/1.0.0/AnZoxzoGPtSGQ15EQh6pBeeaHJ7aizP9MQhK81dES3Uq.tar.zst" }

import pf.Stdin
import pf.Stdout

# Demonstrates: Reading multiline input from stdin until EOF, while loops, for loops, List.append

main! : List(Str) => Try({}, [Exit(I32), StdinErr(Str), StdoutErr(Str), ..])
main! = |args| {
    var $lines = []
    var $continue = True

    # Read all lines from stdin until EOF (which returns empty string)
    # (Note: this will block, instead of streaming the output chunk by chunk)

    while $continue {
        line = Stdin.line!({})?

        # Empty string indicates EOF
        if line == "" {
            $continue = False
        } else {
            $lines = List.append($lines, line)
        }
    }

    # Transform the file, and then spit it back out via stdout...

    Stdout.line!("===ARGS===")?
    Stdout.line!("args: ${Str.inspect(args)}")?
    Stdout.line!("===Prefix===")?

    # Echo all lines back
    for line in $lines {
        Stdout.line!(line)?
    }

    Stdout.line!("===Suffix===")?

    Ok({})
}

Obviously, I need a pure json parser lib, and another pure "Roc/zig built-ins" lib... map the json to roc primitives (functions & types), and then can just dump the results to disk.

Or I could use two separate roc scripts, instead of cli args.

But this is just the starting point that I needed.

I think this piping method is highly overlooked and underrated imo.

And the roc script is effectively just pure code... stdin and stdout can't really do anything by themselves... can't harm or damage the system in any meaningful capacity, and the context, permissions & capabilities model are specified by the rest of the cli command & the operating system. The only way this harms the system, is if you mess up the command by adding sudo and piping it to some operating system file, like overriding the kernel or something...

Can just wrap it into a bash script or something, if needed.

I figured this was worth sharing... no idea if anyone else is using this pattern.

But this will be super useful to me.

Unfortunately, this piping method only really supports a single input and output stream.

So it's not suitable, if you want to operate across many files as input, and many files as output... Would need at concatenation tool and a splitting tool... it's possible, but could get quite messy and un-ergonomic rather fast... and then you're stuck with a single pipeline unless you do some complex bash script, so maybe not the best for performance if you need to block & parse, across many files.

But for my use-case, of a quick script to process a single file with a single pipe, works great!

No idea if anyone else is using this pattern... haven't searched.

view this post on Zulip Karl (Sep 14 2026 at 02:15):

Scott Campbell said:

Obviously, I need a pure json parser lib,

Is there a particular reason the built-in JSON decoder won't work for this?

view this post on Zulip Scott Campbell (Sep 14 2026 at 02:16):

No reason why not, I just wasn't aware there was one.

Any json decoder will work.

Thanks, i'll use the std library one!

view this post on Zulip Karl (Sep 14 2026 at 02:17):

The encoder/decoder APIs are my favorite Roc feature.

view this post on Zulip Scott Campbell (Sep 14 2026 at 02:29):

Yup... this will be useful.
https://roc-lang.org/examples/Json/README

Thanks for the tip!

Source:

DecodedExtensionApiJson : {
    header: {}
}
decoded : Try(DecodedExtensionApiJson, _)
decoded = Json.parse(Str.join_with($lines, "\n"))
Stdout.line!("decoded: ${Str.inspect(decoded)}")?

Output:

decoded: Ok({ header: {} })

Last updated: Sep 24 2026 at 15:59 UTC