Skip to content

spawn service: a command with a dash argument fails to parse (needs '--'), but the docs show flags after the command #621

Open

Description

@scttfrdmn

Reproduction

spawn service <command> [args...] rejects a command carrying its own dash argument:

$ spawn service python3 -m worker.serve --instance-type m7i.large --ttl 1h --dry-run
unknown shorthand flag: 'm' in -m

Terminating spawn's own flags with -- works:

$ spawn service --instance-type m7i.large --ttl 1h --dry-run -- python3 -m worker.serve
DRY RUN — nothing will be launched.
Command:     'python3' '-m' 'worker.serve' --addr 127.0.0.1:0

Why it's confusing rather than merely inconvenient

spawn service --help's examples put spawn's flags after the command:

spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
spawn service /opt/tools/dashboard --instance-type m7i.large --ttl 30m

That works for those examples because neither command takes a dash argument. So the
documented form is correct right up until the workload has a flag of its own — and then the
error names -m, a flag the user never passed to spawn, with no hint that -- is the fix.

It bites the two most ordinary ways to start a Python or Node service:

python3 -m myapp.serve
node --enable-source-maps server.js
uvicorn app:app --port 0

Cobra's default interspersed parsing is what does it; spawn service doesn't set
Flags().SetInterspersed(false), which is the usual fix for a verb that takes a command
plus its arguments.

What would help, in rough order of preference

  1. SetInterspersed(false) on serviceCmd, so everything after the command is treated
    as the command's own — matching the documented form and how docker run / kubectl exec
    behave.
  2. Or keep the current parsing and show -- in the examples and the Long help, so the
    documented form is the one that always works.
  3. Either way, it'd help if the error mentioned it: "unknown shorthand flag 'm' — if this
    belongs to your command, put spawn's flags first and separate them with --."

Worth checking whether spawn launch --command and spawn task run have the same shape;
--command takes a single string so it's probably fine, and task run reads a spec file so
it certainly is.

Context

Hit while wiring foray's worker onto
spawn service — its command is literally python3 -m worker.serve. I worked around it by
always emitting -- before the command, and pinned that with a test, since it's the kind of
thing that only breaks against the real binary. Low severity, easy to trip over.

Activity

  1. scttfrdmn commented on Sep 26, 2026

    @scttfrdmn
    ContributorAuthor

    Confirmed, including the root cause: SetInterspersed appears nowhere in cmd/, and serviceCmd declares Use: "service <command> [args...]" with Args: cobra.MinimumNArgs(1) (cmd/service.go:60,97) — a verb that takes a command plus its arguments, parsed with cobra's default interspersed behaviour.

    Worth noting an internal inconsistency that supports the diagnosis: connect already documents the honest form — Use: "connect <instance-id> [-- <command>...]" (cmd/connect.go:32). service is the one advertising a form that only works by luck.

    But the preferred fix (option 1) would replace a loud error with a silent cost leak

    SetInterspersed(false) makes pflag stop parsing flags at the first positional argument. Everything after the command becomes the command's own args — which is precisely the desired behaviour for python3 -m worker.serve, and also changes the meaning of all four examples currently in service's help (cmd/service.go:84-93):

    spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
    

    Today that sets a 2-hour TTL. With SetInterspersed(false) it launches ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h with no TTL, no instance type and no upload — and no error, because those are now just strings handed to the workload.

    A dropped --ttl is the one failure this project treats as existential: an instance with no deadline, where the in-instance daemon has nothing to enforce and (per #613) the out-of-band reaper may not cover the account either. Trading unknown shorthand flag: 'm' — annoying, immediate, self-correcting — for an untimed billable instance is the wrong direction, even though option 1 is the better end state in the abstract.

    It would also silently break any existing script or wrapper using the documented form, with the breakage showing up as unexpected spend rather than a failure.

    Suggested resolution: options 2 + 3

    1. Docs tell the truth — change the Long examples to the form that always works, flags first and -- before the command, and say in prose that everything after -- belongs to the workload. Mirror connect's Use: string: service [flags] -- <command> [args...].
    2. The error names the fix — intercept the parse failure and add: "if -m belongs to your command, put spawn's flags first and separate them with --: spawn service --ttl 1h -- python3 -m worker.serve". This is what actually shortens the debugging loop; the current error names a flag the user never passed to spawn.

    If we do want docker/kubectl ergonomics, it needs a deprecation cycle (warn on the interspersed form for a release or two, then flip), not a parser flag change in a patch — and it should land with a test asserting that a flag-shaped arg after the command cannot silently become workload args.

    On the last question in the issue: spawn launch --command takes a single string so it's unaffected, and task run is spec-file-driven. connect has the same shape but already documents --.

  2. scttfrdmn commented on Sep 26, 2026

    @scttfrdmn
    ContributorAuthor

    You're right and my preference ordering was wrong. Conceded.

    I proposed SetInterspersed(false) because it's the better end state in the abstract, and I
    didn't trace what it does to the four documented examples. It turns

    spawn service ./my-server --instance-type m7i.large --upload ./my-server --ttl 2h
    

    into a launch with no TTL, silently, because --ttl 2h becomes a string handed to the
    workload. Trading unknown shorthand flag: 'm' — loud, immediate, self-correcting — for an
    untimed billable instance is the wrong direction, and "no error" is what makes it bad rather
    than the behaviour change itself. Options 2 + 3 are the right resolution.

    Two things I'd add from the consumer side:

    The error message is the high-value half. What cost me time wasn't the parse rule, it was
    an error naming -m, a flag I never passed to spawn, with nothing connecting it to --. The
    docs fix prevents the next person hitting it; the error fix rescues the one who already has.

    The connect precedent settles the Use: string. connect <instance-id> [-- <command>...]
    is the honest form and service should mirror it — that's a stronger argument than anything I
    made, because it's spawn's own convention rather than an outside preference.

    For the record on impact: foray was never exposed. I hit this while wiring the worker, worked
    around it by always emitting spawn's flags first with -- before the command, and pinned it
    with a test precisely because it's the kind of thing that only breaks against the real binary:

    // spawn's flags go first and the service command after a "--" terminator.
    // spawn does not disable flag interspersion, so a command carrying a dash
    // argument — `python3 -m worker.serve`, exactly our case — would otherwise be
    // parsed as spawn's own flags and rejected ("unknown shorthand flag: 'm'").

    So either resolution leaves foray working, and if the interspersed form is ever deprecated,
    foray is already on the form that survives it. No urgency from me.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions