SCUA

News

SCUA 0.24.0 is out

September 8, 2026

A SCUA tool's output is now safe for another program to parse. A log line can no longer land in the middle of a result and break whatever is reading it: standard output is the answer, standard error is everything else. info, warn and severe write to stderr; print and sys.write stay on stdout. That is one changed default, and it is a breaking change, so read the upgrade section before you install. Alongside it: --output=json for a stdout that carries the result and nothing else, sys.write_err for raw bytes on stderr, print that reaches a pipe as it happens, a sys.parse_args that no longer eats a shell loop's list, and a scua fmt fix.

#Log lines now go to standard error

warn("cache is cold")
print("42")
$ scua tool.scua 2>/dev/null
42

The caller of a SCUA tool is increasingly a program: a shell pipeline, a CI step, an AI agent. All of them parse what comes out of stdout, and to a parser a log line in the middle of the payload is not a warning, it is a syntax error. Diagnostics belong on the stream that has always existed for them.

#A result channel for tools

--output=json makes the guarantee explicit. sys.emit writes the result, and nothing else reaches stdout: print, sys.write and log lines all divert to stderr. A tool can print progress through a long job and its caller still gets one clean JSON document.

import sys

print("scanning")
warn("cache is cold")
sys.emit({ answer = 42 })
$ scua tool.scua --output=json 2>/dev/null
{"answer":42}

The progress line and the warning are not lost, they are on stderr:

$ scua tool.scua --output=json 2>&1 >/dev/null
scanning
[warn] cache is cold

It is a flag on the run, not a declaration in the script, because the caller is the one who knows it intends to parse the output. Run it by hand and it reads as before.

#Raw bytes on standard error

sys.write_err(s) is sys.write for stderr: raw bytes, no trailing newline, no [level] prefix. Until now the only ways onto stderr were a log level, which stamps [warn] on the front, or sys.fail, which exits. A progress bar or a status line fitted neither.

A script that printed a line and then computed for ten seconds used to show nothing until it paused or exited, so a long job with progress lines looked hung. Output is now handed over once it is about a millisecond old, or sooner once enough builds up. A tight loop printing thousands of lines still batches, so nothing got slower. Terminal, pipe or file, it is the same. SCUA has no equivalent of Python's -u.

#A tool in a loop keeps the loop

while read f; do mytool "$f"; done < files.txt

sys.parse_args used to read stdin to end-of-file the moment it was called, before looking at the arguments. In that loop the first run swallowed the rest of the list. It then usually exited 2 complaining about bad JSON on stdin for a payload you never supplied; if the leftover happened to parse as a JSON object, it exited 0 and processed one file. Neither told you the list was gone.

It now reads stdin only when a JSON body could still supply something: nothing on the command line at all, or a field with no default still unfilled after flags and positional arguments. --help and an unrecognised option answer without touching stdin.

#Upgrading: the one breaking change

Log lines move from stdout to stderr. Two things to check.

If you pass --log-stderr, remove it. It described what is now the default, so it is gone. A run that still passes it fails with scua: --log-stderr: cannot read file and exit status 2: the flag is no longer recognised, so it is read as the name of the script to run. If you see that message, this is why.

If anything reads log lines off stdout, point it at stderr. A wrapper grepping for [warn], a CI step, a log collector. The new --log-stdout puts them back on stdout while you migrate, and 2>&1 merges the streams in written order.

#Also in this release

scua fmt no longer mis-indents everything after a table key or record field named like a keyword: on = 2, then = 3, a field do: int, a member access like spec.then. Each was counted as opening a block nothing closed, so every following line gained two spaces. As a backstop, the formatter now refuses and writes nothing if it reaches end of file with a block still open. Its indentation rule is in the manual for the first time.

The full changelog has the detail.