Skip to content

On Windows, usage's settings and its parsed args share one namespace #783

Description

@JamBalaya56562

USAGE_DEBUG, USAGE_TRACE and USAGE_SHELL_<SHELL> are settings usage reads. usage_<name> is how usage exports a parsed arg. Windows environment variables are case-insensitive, so these are one namespace there, and two things follow from it.

A script cannot read its own arg

#!/usr/bin/env -S usage bash
#USAGE flag "--debug" help="ordinary flag name"
echo "lower usage_debug=[$usage_debug]"
echo "upper USAGE_DEBUG=[$USAGE_DEBUG]"
$ usage bash debug.sh --debug
lower usage_debug=[true]     # fine
upper USAGE_DEBUG=[]

$ USAGE_DEBUG=0 usage bash debug.sh --debug
lower usage_debug=[]         # the script's own flag is gone
upper USAGE_DEBUG=[true]

Windows keeps the existing key's spelling and replaces only the value, so writing usage_debug lands in USAGE_DEBUG. The script then reads $usage_debug — the name the docs give it — and finds nothing. The value is not lost, just under a spelling the script does not look for.

It needs the variable to be set already, which makes it intermittent: it works on a clean machine and fails for whoever has USAGE_DEBUG in their profile. Colliding names are debug, trace, shell_bash, shell_zsh, shell_fish, shell_pwsh.

I do not think this half is fixable without moving the namespaces apart. Command::env_remove followed by Command::env keeps the inherited spelling, and Windows cannot hold usage_debug and USAGE_DEBUG as separate variables at all:

env_remove("usage_debug") + env("usage_debug", "true")  ->  child sees USAGE_DEBUG=true
env("usage_debug", "true")                              ->  child sees USAGE_DEBUG=true

Settings cannot reach a mise task

mise clears usage_* before running a task, because those variables are its own argument parser's output and must not influence spec discovery. On Windows the match is case-insensitive, which is correct for a platform where usage_foo and USAGE_FOO are one variable — but it takes the settings with it:

mise run <task>  ->  USAGE_SHELL_BASH=[] USAGE_FOO=[] usage_lower=[] MYAPP_SHELL_BASH=[d]
mise exec -- ... ->  USAGE_SHELL_BASH=[a] USAGE_FOO=[b] usage_lower=[c] MYAPP_SHELL_BASH=[d]

So USAGE_SHELL_BASH cannot be delivered to a mise task, which is how I ran into this: usage's own Windows CI has to call cargo test rather than mise r test.

mise is not doing anything wrong here. It avoids the same trap for itself by keeping its settings under MISE_* — 301 settings, none prefixed USAGE_ — disjoint from the usage_* its parser writes.

If the settings move

Any name beginning USAGE_ is out: mise compares the first six characters ("usage_".len()) case-insensitively, so USAGE_SETTING_*, USAGE_CLI_* and USAGE__* are all still cleared. The underscore right after USAGE is what makes it unavoidable.

USAGECLI_* is the least-bad spelling I found — not pretty, but accurate: all three settings are read only in cli/src (env.rs, main.rs), so they are CLI settings rather than library ones. That would make the collision structurally impossible and let mise tasks receive them again.

It is a breaking change, and 5.0.0 has just shipped, so it may be worth parking until the next major rather than spending one on it. Documenting the reserved names would cover the "walked into it unknowingly" case in the meantime — happy to send that as a PR, and the rename too whenever you want it.

Measured on Windows 11 with usage 5.0.0.


This issue was generated by Claude Code.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions