0003 — prototext CLI design

Status: implemented App: prototext Implemented in: 2026-03-10

Problem

A standalone Rust binary (prototext) is needed that exercises prototext-core end-to-end. Before implementation can start, the CLI surface must be precisely specified.

Goals

Non-goals


Specification

Flags

Mode

ShortLongDescription
-d--decodeDecode: treat input as binary protobuf, emit text
-e--encodeEncode: treat input as textual prototext, emit binary

-d and -e are mandatory and mutually exclusive: exactly one must be given. Omitting both or supplying both is an error.

Schema

ShortLongDescription
-D PATH--descriptor PATHPath to a compiled .pb descriptor file
-t NAME--type NAMEFully-qualified root message type name (e.g. pkg.MyMessage)

Rules:

Summary:

-D-tSchema used
absentabsentschemaless
absentgivenembedded descriptor.pb
givenabsenterror
givengivendescriptor at -D path

Output

ShortLongDescription
--annotations / --no-annotationsEmit inline comments with wire type and field number (default: on). Required for lossless round-trip.
-o PATH--output PATHWrite output to PATH (single input only; exclusive with -O)
-O DIR--output-root DIRRoot directory for output files (batch mode; exclusive with -o and -i)

Input

ShortLongDescription
-I DIR--input-root DIRRoot directory: positional paths and glob expansion are relative to DIR
-i--in-placeRewrite each input file in place (exclusive with -O)

Other

ShortLongDescription
-q--quietSuppress warnings on stderr (errors are always printed)
-h--helpPrint help and exit

Positional arguments

prototext -d|-e [OPTIONS] [PATH...]

Each positional PATH may be:

When -I DIR is given:

When -I is absent, all of the above are relative to cwd.

No positional arguments — read from stdin. -i and -O are errors when reading from stdin (no path to write back to).


Output routing

Exclusive flag constraints (enforced at startup, before any I/O)

Single input (stdin or exactly one resolved file)

Output goes to stdout unless -o PATH is given, in which case output goes to PATH. -O with a single input is an error.

Batch mode (two or more resolved files)

Batch mode requires either -i or -O. Neither with multiple inputs is an error.

Each input file has a relative path:

The output path for each file is:

Extension is never changed in batch mode.


Error handling

In batch mode, a failure on one file does not abort processing of subsequent files. All errors are collected and printed to stderr at the end. Exit code is 1 if any file failed, 0 if all succeeded.


Shell completion

Dynamic completion using clap + clap_complete with the unstable-dynamic feature enabled — the same model used by Cargo today (CARGO_COMPLETE=bash cargo).

Activation:

# bash (with workaround for path-completion bugs in clap_complete)
source <(PROTOTEXT_COMPLETE=bash prototext | sed \
  -e '/^\s*) )$/a\    compopt -o filenames 2>/dev/null' \
  -e 's|words\[COMP_CWORD\]="$2"|local _cur="${COMP_LINE:0:$COMP_POINT}"; _cur="${_cur##* }"; words[COMP_CWORD]="$_cur"|')

Completion behaviour:

Bash word-split fix for dotted type names. Bash uses . as a COMP_WORDBREAKS character, so google.protobuf.F is split and the binary only receives F as the current token. The generated completion script is patched with two sed substitutions at load time (see activation command above):

  1. compopt -o filenames — tells readline candidates are filesystem paths, suppressing bash's tendency to strip the dotted prefix.
  2. Word reassembly from COMP_LINE/COMP_POINT — reconstructs the full token (e.g. google.protobuf.F) so the prefix filter in complete_type_names works correctly.

Implementation notes


Examples

# Decode foo.pb to stdout (schemaless)
prototext -d foo.pb

# Decode foo.pb with schema (annotations on by default)
prototext -d -D knife.pb -t SwissArmyKnife foo.pb

# Decode foo.pb with schema, suppress annotations (protoc-compatible output)
prototext -d -D knife.pb -t SwissArmyKnife --no-annotations foo.pb

# Decode a FileDescriptorProto using the embedded descriptor
prototext -d -t google.protobuf.FileDescriptorProto foo.pb

# Encode textproto back to binary
prototext -e foo.txtpb -o foo.pb

# Batch decode all .pb files under src/, write results under out/
prototext -d -D knife.pb -t SwissArmyKnife -I src/ -O out/ '**/*.pb'

# In-place batch encode (read fully, then overwrite)
prototext -e -i '**/*.txtpb'

# Read from stdin, decode schemalessly
cat foo.pb | prototext -d

# Enable bash completion
source <(PROTOTEXT_COMPLETE=bash prototext | sed \
  -e '/^\s*) )$/a\    compopt -o filenames 2>/dev/null' \
  -e 's|words\[COMP_CWORD\]="$2"|local _cur="${COMP_LINE:0:$COMP_POINT}"; _cur="${_cur##* }"; words[COMP_CWORD]="$_cur"|')

Open questions

References