Help us shape the future of Go! Take the 2026 Go Developer Survey

cmdwire

package module
v0.1.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 3, 2026 License: BSD-3-Clause Imports: 8 Imported by: 0

README

cmdwire

cmdwire is a bounded, line-oriented request and reply protocol for command consoles. It uses one readable wire format across serial consoles, automated qualification, remote command adapters, and other ordered text transports.

The protocol, Go implementation, and Rust implementation are drafts. Expect incompatible changes before the first stable release.

Contents

Quick start

Wire example
request object.observe count=2 timeout=30
event object.observe phase=collecting
event object.observe alpha/telemetry phase=sampled
ok object.observe schema=1 count=2 stop=count

A client starts a command with request.

See cmdwire Protocol below for the normative protocol.

Go package
record, err := cmdwire.ParseLine(
    `item object.status alpha state=ready`,
)
if err != nil {
    return err
}

line, err := cmdwire.Format(record)

ParseLine enforces the grammar, token-only ASCII values, exact spacing, duplicate-field rejection, and the 80-byte limit. Format emits canonical ASCII and refuses an oversized result.

Use a Collector to extract one reply from interleaved console output:

collector, err := cmdwire.NewCollector("object.status")
if err != nil {
    return err
}
for _, line := range consoleLines {
    _, complete, err := collector.AddLine(line)
    if err != nil {
        return err
    }
    if complete {
        break
    }
}
report, err := collector.Result()

Collector.Result returns a terminal err as *cmdwire.RemoteError and discards earlier data records. NewCollector buffers at most 1,024 data records; use NewCollectorWithLimit to choose a smaller application limit.

Command-line validator

Install the cmdwire executable:

go install github.com/sartura/cmdwire/cmd/cmdwire@latest

Validate protocol records from stdin or files:

cmdwire check transcript.txt

Canonicalize valid records:

cmdwire format transcript.txt

check is quiet on success. Both commands report the first invalid file, line, and byte column.

Rust crate

The Rust crate under rust/cmdwire provides allocation-free parsing and encoding for no_std firmware. It targets Rust 1.97.1 and compiles for aarch64-unknown-none.

let record = cmdwire::parse("request object.status")?;
assert_eq!(record.command(), "object.status");

Line stores an encoded record in a fixed 80-byte buffer, so an oversized record fails before reaching the transport.

Scope and security

cmdwire defines framing, lexical rules, generic observation limits, and the ordered record model. Command schemas define resources, fields, types, units, ordering, identity, and completeness. Parsing never authorizes or executes a command.

cmdwire does not authenticate records or protect their integrity. Use a trusted or authenticated transport across security boundaries.


cmdwire Protocol

cmdwire is a line-oriented request and reply protocol for command consoles. It combines readable wire records with deterministic parsing, bounded records, command-scoped events, and explicit completion.

1. Conformance language

The key words MUST, MUST NOT, REQUIRED, SHOULD, SHOULD NOT, and MAY are to be interpreted as described by RFC 2119 and RFC 8174.

Implementations conform by following the lexical and lifecycle rules in this specification. Command schemas add resources, fields, types, units, ordering, and completeness rules.

2. Transport

cmdwire runs over an ordered, reliable byte stream. A physical record:

  • occupies one line;
  • contains printable ASCII bytes 0x20 through 0x7e only;
  • is at most 80 bytes, excluding its line ending;
  • uses one ASCII space between tokens;
  • has no leading or trailing space;
  • ends in LF or CRLF when carried in a stream.

A sender MUST NOT wrap or truncate a record. A schema MUST divide a wide object into semantic resources or bounded chunks.

Clients send request records. Servers send event, item, chunk, ok, and err records. Other console output may share the transport. A producer MUST prefix non-protocol output that would otherwise parse as a cmdwire record.

3. Request records

3.1 Request

request starts a command and carries its name, optional resource path, and ordered fields.

request object.status
request object.action alpha mode=fast
request object.observe count=2 timeout=30

A client MUST NOT start a second request with the same command name while the first remains active. A server rejects a request it cannot start with err, normally using BUSY, BAD_REQUEST, or UNSUPPORTED.

4. Reply records

A reply contains zero or more data records followed by exactly one terminal record. Data records are event, item, and chunk. Terminal records are ok and err. Replies do not nest.

4.1 Successful terminal

ok terminates every successful reply. Its first field is a positive schema; its second field is a nonnegative count. Additional result fields may follow. A reply without data records uses count=0.

ok object.action schema=1 count=0 state=ready

count equals the number of matching event, item, and chunk records before the terminal. It excludes requests, unrelated lines, and the terminal.

4.2 Error

err terminates a failed reply. Its first field is code. Structured diagnostic fields may follow.

err object.action code=BAD_VALUE field=mode

An error invalidates earlier data records for that reply. Codes are stable uppercase API identifiers.

4.3 Event

event reports progress or an observation without terminating the reply. It has an optional resource path and at least one field.

event object.observe state=waiting
event object.observe alpha/telemetry phase=sampled

Events are command-scoped. Unsolicited background events are outside the protocol.

4.4 Item

item reports one object or semantic facet. It has an optional resource path and zero or more fields.

item object.status alpha state=ready mode=automatic
4.5 Chunk

chunk carries bounded bulk data. It has an optional resource path and at least one field.

chunk object.read alpha/data offset=0 data=00112233

5. Observation

Commands may support observation. Consumers MUST parse event records even if they never request observation.

An observation request carries one or both of these fields:

  • count=N: stop after N matching events;
  • timeout=N: stop after N seconds.

Each value is a positive unsigned decimal integer. When both fields are present, the first limit reached ends observation. An observation without either field is invalid.

A successful observation terminal includes stop=count or stop=timeout.

request object.observe count=2 timeout=30
event object.observe phase=collecting
event object.observe alpha phase=sampled
ok object.observe schema=1 count=2 stop=count

The server reports source closure or any failure to observe or stop with err.

6. Normative wire grammar

The following normative ABNF uses RFC 5234 notation. SP is one ASCII space.

record          = request / success / error / event / item / chunk

request         = "request" SP command [SP resource] *(SP field)
success         = "ok" SP command SP schema-field SP count-field
                  *(SP field)
error           = "err" SP command SP code-field *(SP field)
event           = "event" SP command [SP resource] 1*(SP field)
item            = "item" SP command [SP resource] *(SP field)
chunk           = "chunk" SP command [SP resource] 1*(SP field)

schema-field    = "schema=" positive-decimal
count-field     = "count=" unsigned-decimal
code-field      = "code=" error-code
field           = key "=" value

command         = command-segment *("." command-segment)
command-segment = LCALPHA *(LCALPHA / DIGIT / "_" / "-")
resource        = resource-segment *("/" resource-segment)
resource-segment = resource-part ["." resource-part]
resource-part   = ALNUM / (ALNUM *resource-char ALNUM)
resource-char   = ALNUM / "_" / ":" / "-"
key             = LCALPHA *(LCALPHA / DIGIT / "_")
error-code      = UCALPHA *(UCALPHA / DIGIT / "_")

value           = 1*value-char
value-char      = %x21 / %x23-5B / %x5D-7E
positive-decimal = %x31-39 *DIGIT
unsigned-decimal = "0" / (%x31-39 *DIGIT)

ALNUM           = ALPHA / DIGIT
LCALPHA         = %x61-7A
UCALPHA         = %x41-5A

Values are nonempty printable ASCII tokens. Space, double quote, and backslash are forbidden. The protocol has no quoting or escaping. A command schema uses a textual encoding such as hexadecimal for arbitrary text or bytes.

7. Semantic rules

  • A record MUST contain one known kind and one valid command.
  • A record MUST NOT repeat a field key.
  • Field order is deterministic and schema-defined where it matters.
  • Wire values are nonempty ASCII strings. A command schema defines their types and units.
  • Every ok begins with schema and count; those keys are reserved on ok.
  • Every err begins with code; that key is reserved on err.
  • Only request, event, item, and chunk may carry a resource path.
  • A resource path is opaque unless its command schema defines segment meaning.
  • Each resource part MUST begin and end with an ASCII letter or digit.
  • A parser MUST reject malformed records for the command it is observing.
  • A parser MAY ignore unrelated lines and valid records for other commands.
  • A command schema decides whether resource paths are forbidden, optional, or required for each applicable record kind.

8. Command schemas

A command schema defines:

  • request resources and fields;
  • required and optional reply resources and fields;
  • field types, units, and allowed values;
  • deterministic field and record ordering where order matters;
  • terminal result fields;
  • valid empty replies;
  • item identity and uniqueness;
  • chunk offsets and completeness;
  • observation support and stop conditions;
  • supported schema versions.

Consumers MUST reject unsupported schema versions, mismatched counts, duplicate identities, missing fields, invalid values, and incomplete chunks as defined by the command schema.

cmdwire schema documents provide a machine-readable subset for fixed request and reply shapes. Format version 1 is JSON with format, command, version, request, reply, and errors members. Each error declares its stable code and ordered diagnostic fields. Request and terminal fields are ordered. Reply records are ordered and identify their kind, resource, and fields. Field types are token, bool, uint, enum, fixed-width hex, and lowercase hexadecimal bytes; fields may add enum values, inclusive integer bounds, or the unavailable sentinel. Rust reply bindings accept bytes as borrowed byte slices and encode them without allocation. Request bindings and generated Go do not yet support bytes.

Validate schemas and generate checked-in Go or Rust bindings with:

cmdwire schema check schema/cmdwire/*.json
cmdwire schema generate-go cmdschema internal/cmdschema/generated.go \
  schema/cmdwire/*.json
cmdwire schema generate-rust src/cmdschema/generated.rs \
  schema/cmdwire/*.json

Generated Rust bindings depend only on the public no_std cmdwire crate. Schema files and generated product bindings remain in the consuming repository. Generated request decoders and reply types own command names, resources, field order, counts, value encodings, and declared error constants. Implementations supply typed operational values. Independent consumers validate replies from the same schema documents. Product profiles carry exact hardware values and do not redefine protocol structure.

Format version 1 represents exact record sequences. Format version 2 adds an optional occurs object to reply records. minimum sets the required number of ordered occurrences; an omitted maximum permits an unbounded stream. Generated Rust bindings expose one bounded push_<resource> method per record group and a finish method that verifies cardinality and emits the terminal with the actual record count. Adjacent variable groups must have distinct kinds or resources so consumers can classify their wire records without backtracking. Observation lifecycles require a later schema extension; the wire protocol already supports them.

9. Collection behavior

Before sending request, a consumer marks its position in the input stream. It then collects matching data records, ignores unrelated lines, and stops at the first matching terminal.

After ok, the consumer validates the count and command schema before exposing buffered data. After err, it discards buffered data and exposes the stable code and diagnostic fields. Consumers MUST bound buffered records or process them incrementally. A timeout without a terminal is a local collection failure, not an implicit protocol terminal.

10. Security and resource limits

Implementations MUST enforce the 80-byte limit before parsing fields. They MUST bound buffered records, value lengths, and chunk totals. They SHOULD bound observation duration when the command permits it. Applications MUST validate resource paths before mapping them to filesystems, URLs, or other external namespaces.

cmdwire provides no authentication, integrity, or confidentiality. A parser cannot distinguish a protocol record from identical non-protocol text. Use a trusted or authenticated transport when records cross a security boundary. Parsing MUST NOT execute commands; schema validation, authorization, and dispatch remain separate operations.

11. Conformance data

testdata/conformance.json is the language-neutral parser corpus. Implementations should accept every valid case, produce the stated ordered record model, and reject every invalid case. The corpus supplements this specification, which remains authoritative.

Implementation architecture

The Go implementation has five layers:

  1. lexer.go enforces wire-level line and token rules, then classifies tokens.
  2. grammar.y defines the Go record grammar over those tokens.
  3. validate.go enforces record semantics that the grammar cannot express.
  4. Collector validates counts across multiple records.
  5. Command schemas validate request and reply resources, fields, and values.

The Rust crate implements the same normative ABNF as a bounded, borrowing parser. It uses no allocator or runtime dependencies. Both parsers run the language-neutral conformance corpus. The schema generator emits typed Rust request decoders, aggregate command dispatch, bounded fixed replies, streaming format-2 replies, and declared error encoders.

The record grammar does not parse individual wire bytes. It receives tokens only after the lexer has validated and classified them. Semantic validation then checks constraints such as duplicate fields and canonical numbers.

The repository includes generated parser_gen.go. Only maintainers need goyacc. Regenerate the parser with the pinned Go tool:

go generate ./...

The normative wire grammar is the ABNF above. grammar.y, lexer.go, and validate.go together implement that contract for Go.

Verification

testdata/conformance.json provides a language-neutral parser corpus. Unit and fuzz tests cover lexical boundaries and malformed input.

Run the standard checks with:

go generate ./...
git diff --exit-code -- parser_gen.go
go test ./...
go vet ./...
cargo test --locked --manifest-path rust/cmdwire/Cargo.toml
cargo clippy --locked --manifest-path rust/cmdwire/Cargo.toml --all-targets -- -D warnings
cargo check --locked --manifest-path rust/cmdwire/Cargo.toml --target aarch64-unknown-none

Run the parser fuzzer, then summarize its cached corpus:

go test -fuzz=FuzzParseLine -fuzztime=60s .
./scripts/fuzz-report

FuzzFormat, FuzzDecoder, FuzzCollector, and FuzzParseSchema cover the remaining layers. The report summarizes the parser corpus, including rejection paths, boundary inputs, and canonicalization examples.

Run a two-hour campaign for each fuzz target:

./scripts/fuzz-campaign

The campaign uses seven fuzz workers by default. Override its defaults with FUZZ_TIME, FUZZ_TIMEOUT, and FUZZ_PARALLEL. On macOS, prevent sleep with caffeinate -i ./scripts/fuzz-campaign.

The optional Gherkin suite tests public parsing and collection behavior. Its independent module keeps Godog out of the package dependency graph.

(cd features && go test ./...)

Run mutation testing with:

./scripts/mutation-check

The script downloads its pinned Gremlins version on demand and uses seven workers by default. Configure it through MUTATION_* environment variables:

MUTATION_WORKERS=4 MUTATION_TEST_CPU=1 ./scripts/mutation-check
MUTATION_DRY_RUN=1 ./scripts/mutation-check
MUTATION_INTEGRATION=1 MUTATION_OUTPUT=/tmp/mutation.json \
  ./scripts/mutation-check

Resource controls are MUTATION_WORKERS, MUTATION_TEST_CPU, and MUTATION_TIMEOUT_COEFFICIENT. Quality gates are MUTATION_EFFICACY and MUTATION_COVERAGE. Scope and reporting controls include MUTATION_DIFF, MUTATION_OUTPUT, MUTATION_OUTPUT_STATUSES, MUTATION_TAGS, and MUTATION_COVERPKG. MUTATION_OPERATORS accepts a comma-separated list of additional operators.

.gremlins.yaml excludes generated code. CI runs the standard, Gherkin, 10-second fuzz, and mutation suites separately.


Contributing

Before submitting a change:

gofmt -w *.go cmd/cmdwire/*.go features/*.go internal/schemagen/*.go
go generate ./...
git diff --exit-code -- parser_gen.go
go test -race ./...
go vet ./...
(cd features && go test ./...)
rustfmt --edition 2024 --check rust/cmdwire/src/lib.rs \
  rust/cmdwire/tests/{conformance,encoding}.rs
rustfmt --edition 2024 --config skip_children=true --check \
  rust/cmdwire/tests/generated_bindings.rs
cargo test --locked --manifest-path rust/cmdwire/Cargo.toml
cargo clippy --locked --manifest-path rust/cmdwire/Cargo.toml --all-targets -- -D warnings
cargo check --locked --manifest-path rust/cmdwire/Cargo.toml --target aarch64-unknown-none
./scripts/mutation-check

For wire-syntax changes, update the protocol specification in this README, grammar.y, the generated parser, and the language-neutral conformance corpus in one commit. For schema-format changes, update schema validation, generation, tests, and this README together.

For lifecycle changes, update the specification and its Gherkin scenario. Regenerate generated code instead of editing it.

Use lowercase imperative commit subjects such as:

parser: reject repeated fields

Protocol behavior remains draft until the first stable release. Explain the compatibility impact of each proposed wire change.

Documentation

Overview

Code generated by goyacc -l -o parser_gen.go -p cw grammar.y. DO NOT EDIT.

Package cmdwire parses and formats the cmdwire line protocol.

Index

Examples

Constants

View Source
const CHUNK_TOKEN = 57351
View Source
const CODE_FIELD = 57355
View Source
const COMMAND_TOKEN = 57352
View Source
const COUNT_FIELD = 57357
View Source
const DefaultMaxDataRecords = 1024

DefaultMaxDataRecords bounds data buffered by NewCollector.

View Source
const ERR = 57348
View Source
const EVENT_TOKEN = 57349
View Source
const FIELD_TOKEN = 57354
View Source
const ITEM_TOKEN = 57350
View Source
const MaxLineBytes = 80

MaxLineBytes is the maximum record width, excluding the line ending.

View Source
const OK_TOKEN = 57347
View Source
const PATH_TOKEN = 57353
View Source
const REQUEST = 57346
View Source
const SCHEMA_FIELD = 57356

Variables

View Source
var (
	// ErrIncomplete means that no matching terminal record has arrived.
	ErrIncomplete = errors.New("cmdwire: reply is incomplete")
	// ErrComplete means that a matching record arrived after the terminal.
	ErrComplete = errors.New("cmdwire: reply is already complete")
	// ErrDataLimit means that a reply exceeded its collector's record limit.
	ErrDataLimit = errors.New("cmdwire: reply data record limit exceeded")
)

Functions

func Format

func Format(record Record) (string, error)

Format returns the canonical wire representation of record without a line ending.

Types

type Collector

type Collector struct {
	// contains filtered or unexported fields
}

Collector extracts one command-scoped reply from interleaved console lines.

Example
package main

import (
	"fmt"

	"github.com/sartura/cmdwire"
)

func main() {
	collector, err := cmdwire.NewCollector("object.status")
	if err != nil {
		panic(err)
	}
	for _, line := range []string{
		"unrelated console output",
		"item object.status alpha state=ready",
		"ok object.status schema=1 count=1",
	} {
		_, complete, err := collector.AddLine(line)
		if err != nil {
			panic(err)
		}
		if complete {
			break
		}
	}
	report, err := collector.Result()
	if err != nil {
		panic(err)
	}
	fmt.Println(report.Command, report.Schema, len(report.Data))
}
Output:
object.status 1 1

func NewCollector

func NewCollector(command string) (*Collector, error)

NewCollector starts bounded collection for command.

func NewCollectorWithLimit

func NewCollectorWithLimit(command string, maxDataRecords int) (*Collector, error)

NewCollectorWithLimit starts collection with a positive data-record limit.

func (*Collector) Add

func (collector *Collector) Add(record Record) error

Add adds an already parsed record for the observed command.

func (*Collector) AddLine

func (collector *Collector) AddLine(line string) (matched bool, complete bool, err error)

AddLine parses a physical line. Unrelated and other-command lines are ignored. matched reports whether the line belongs to the observed command.

func (*Collector) Result

func (collector *Collector) Result() (Report, error)

Result validates and returns the completed reply.

type Decoder

type Decoder struct {
	// contains filtered or unexported fields
}

Decoder reads cmdwire records from a line-oriented stream.

func NewDecoder

func NewDecoder(input io.Reader) *Decoder

NewDecoder returns a bounded line decoder for input.

func (*Decoder) Decode

func (decoder *Decoder) Decode() (Record, error)

Decode reads and parses the next record.

type ErrorSchema added in v0.1.1

type ErrorSchema struct {
	Code   string        `json:"code"`
	Fields []FieldSchema `json:"fields,omitempty"`
}

ErrorSchema describes one stable error code and its diagnostic fields.

type Field

type Field struct {
	Name  string `json:"name"`
	Value string `json:"value"`
}

Field is one ordered key-value pair.

type FieldSchema

type FieldSchema struct {
	Name        string   `json:"name"`
	Type        string   `json:"type"`
	Required    bool     `json:"required,omitempty"`
	Values      []string `json:"values,omitempty"`
	Width       int      `json:"width,omitempty"`
	Minimum     *uint64  `json:"minimum,omitempty"`
	Maximum     *uint64  `json:"maximum,omitempty"`
	Unavailable bool     `json:"unavailable,omitempty"`
}

FieldSchema describes one ordered field and its wire value constraints.

type Kind

type Kind string

Kind identifies a cmdwire record.

const (
	Request Kind = "request"
	OK      Kind = "ok"
	Error   Kind = "err"
	Event   Kind = "event"
	Item    Kind = "item"
	Chunk   Kind = "chunk"
)

type MessageSchema

type MessageSchema struct {
	Resource string        `json:"resource,omitempty"`
	Fields   []FieldSchema `json:"fields,omitempty"`
}

MessageSchema describes one request record.

type OccursSchema added in v0.1.1

type OccursSchema struct {
	Minimum uint64  `json:"minimum"`
	Maximum *uint64 `json:"maximum,omitempty"`
}

OccursSchema describes the permitted cardinality of one ordered reply record group. An absent maximum means the group is unbounded.

type Record

type Record struct {
	Kind     Kind    `json:"kind"`
	Command  string  `json:"command"`
	Resource string  `json:"resource,omitempty"`
	Fields   []Field `json:"fields,omitempty"`
}

Record is one parsed cmdwire line.

func ParseLine

func ParseLine(input string) (Record, error)

ParseLine parses one cmdwire record. The input may omit its line ending or end in LF or CRLF.

Example
package main

import (
	"fmt"

	"github.com/sartura/cmdwire"
)

func main() {
	record, err := cmdwire.ParseLine(
		"item object.status alpha state=ready",
	)
	if err != nil {
		panic(err)
	}
	state, _ := record.Field("state")
	fmt.Println(record.Kind, record.Command, record.Resource, state)
}
Output:
item object.status alpha ready

func (Record) Field

func (record Record) Field(name string) (value string, ok bool)

Field returns the unique field with name. Parsed records always have unique names, but constructed records are checked defensively.

type RecordSchema

type RecordSchema struct {
	Kind     Kind          `json:"kind"`
	Resource string        `json:"resource,omitempty"`
	Fields   []FieldSchema `json:"fields,omitempty"`
	Occurs   *OccursSchema `json:"occurs,omitempty"`
}

RecordSchema describes one ordered reply data record.

type RemoteError

type RemoteError struct {
	Command string
	Code    string
	Fields  []Field
}

RemoteError is a terminal err record. Data emitted before it is discarded.

func (*RemoteError) Error

func (err *RemoteError) Error() string

type ReplySchema

type ReplySchema struct {
	Records  []RecordSchema `json:"records,omitempty"`
	Terminal []FieldSchema  `json:"terminal,omitempty"`
}

ReplySchema describes an ordered successful reply.

type Report

type Report struct {
	Command  string
	Schema   uint64
	Data     []Record
	Terminal []Field
}

Report is one successful command reply.

type Schema

type Schema struct {
	Format  uint64        `json:"format"`
	Command string        `json:"command"`
	Version uint64        `json:"version"`
	Request MessageSchema `json:"request"`
	Reply   ReplySchema   `json:"reply"`
	Errors  []ErrorSchema `json:"errors,omitempty"`
}

Schema describes one command's request and successful reply contract.

func DecodeSchema

func DecodeSchema(input io.Reader) (schema Schema, err error)

DecodeSchema decodes and validates one schema document.

func ParseSchema

func ParseSchema(input []byte) (Schema, error)

ParseSchema parses and validates one schema document.

func (Schema) Validate

func (schema Schema) Validate() error

Validate validates a schema document independently of any record.

func (Schema) ValidateError

func (schema Schema) ValidateError(record Record) error

ValidateError checks one error terminal against the command schema.

func (Schema) ValidateReply

func (schema Schema) ValidateReply(report Report) error

ValidateReply checks one collected successful reply against the schema.

func (Schema) ValidateRequest

func (schema Schema) ValidateRequest(record Record) error

ValidateRequest checks one request against the command schema.

type SyntaxError

type SyntaxError struct {
	Column  int
	Message string
}

SyntaxError reports a one-based byte column in a physical record.

func (*SyntaxError) Error

func (err *SyntaxError) Error() string

Directories

Path Synopsis
cmd
cmdwire command
internal
fuzzreport command
schemagen
Package schemagen generates typed Go bindings for cmdwire command schemas.
Package schemagen generates typed Go bindings for cmdwire command schemas.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL