Files
strukto-ai--mirage/python/tests/commands/test_spec.py
T
wehub-resource-sync bcbd1bdb22
Integ / changes (push) Has been skipped
Pre-commit / pre-commit (push) Failing after 1s
CLI exit codes / changes (push) Has been skipped
Test (Install) / changes (push) Has been skipped
Test (Python) / changes (push) Has been skipped
Test (TypeScript) / changes (push) Has been skipped
CLI exit codes / cli-gate (push) Has been cancelled
Test (Install) / test-install-gate (push) Has been cancelled
Integ / integ-gate (push) Has been cancelled
Test (Python) / test-python-gate (push) Has been cancelled
Test (TypeScript) / test-typescript-gate (push) Has been cancelled
Test (Install) / python-minimal (3.12) (push) Has been cancelled
Test (Install) / python-minimal (3.11) (push) Has been cancelled
Test (Install) / python-extra (agno, mirage.agents.agno) (push) Has been cancelled
Test (Install) / python-extra (chroma, mirage.resource.chroma) (push) Has been cancelled
Test (Install) / python-extra (pdf, mirage.core.filetype.pdf) (push) Has been cancelled
Integ / integ (push) Has been cancelled
Integ / integ-database (push) Has been cancelled
Integ / integ-database-ts (push) Has been cancelled
Integ / integ-data (push) Has been cancelled
Integ / integ-ssh (push) Has been cancelled
Integ / integ-ssh-ts (push) Has been cancelled
Test (Python) / audit (push) Has been cancelled
Test (TypeScript) / test (push) Has been cancelled
Test (TypeScript) / python-fs-shim (push) Has been cancelled
CLI exit codes / Python CLI (push) Has been cancelled
CLI exit codes / TypeScript CLI (push) Has been cancelled
CLI exit codes / Cross-language snapshot interop (push) Has been cancelled
Test (Python) / test (push) Has been cancelled
Test (Python) / import-isolation (deepagents, openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Python) / import-isolation (deepagents, pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Integ / integ-ts (push) Has been cancelled
Integ / integ-fuse (push) Has been cancelled
Test (Install) / python-extra (databricks, mirage.resource.databricks_volume) (push) Has been cancelled
Test (Install) / python-extra (deepagents, mirage.agents.langchain) (push) Has been cancelled
Test (Install) / python-extra (email, mirage.resource.email) (push) Has been cancelled
Test (Install) / python-extra (fuse, mirage.fuse.mount) (push) Has been cancelled
Test (Install) / python-extra (hdf5, mirage.core.filetype.hdf5) (push) Has been cancelled
Test (Install) / python-extra (hf, mirage.resource.hf_buckets) (push) Has been cancelled
Test (Install) / python-extra (lancedb, mirage.resource.lancedb) (push) Has been cancelled
Test (Install) / python-extra (langfuse, mirage.resource.langfuse) (push) Has been cancelled
Test (Install) / python-extra (mongodb, mirage.resource.mongodb) (push) Has been cancelled
Test (Install) / python-extra (nextcloud, mirage.resource.nextcloud) (push) Has been cancelled
Test (Install) / python-extra (openai, mirage.agents.openai_agents) (push) Has been cancelled
Test (Install) / python-extra (openhands, mirage.agents.openhands, 3.12) (push) Has been cancelled
Test (Install) / python-extra (parquet, mirage.core.filetype.parquet) (push) Has been cancelled
Test (Install) / python-extra (postgres, mirage.resource.postgres) (push) Has been cancelled
Test (Install) / python-extra (pydantic-ai, mirage.agents.pydantic_ai) (push) Has been cancelled
Test (Install) / python-extra (qdrant, mirage.resource.qdrant) (push) Has been cancelled
Test (Install) / python-extra (redis, mirage.resource.redis) (push) Has been cancelled
Test (Install) / python-extra (s3, mirage.resource.s3) (push) Has been cancelled
Test (Install) / python-extra (ssh, mirage.resource.ssh) (push) Has been cancelled
Test (Install) / ts-minimal (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:30:44 +08:00

674 lines
21 KiB
Python

# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# ========= Copyright 2026 @ Strukto.AI All Rights Reserved. =========
from mirage.commands.spec import (SPECS, CommandSpec, Operand, OperandKind,
Option, ParsedArgs, parse_command,
parse_to_kwargs)
def test_parse_simple_path_args():
spec = CommandSpec(rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["a.txt", "b.txt"], cwd="/home")
assert parsed.args == [("/home/a.txt", OperandKind.PATH),
("/home/b.txt", OperandKind.PATH)]
assert parsed.flags == {}
def test_parse_absolute_path():
spec = CommandSpec(rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["/data/file.csv"], cwd="/home")
assert parsed.args == [("/data/file.csv", OperandKind.PATH)]
def test_parse_bool_flags():
spec = CommandSpec(options=(Option(short="-r"), Option(short="-f"),
Option(short="-v")),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["-rf", "file.txt"], cwd="/")
assert parsed.flags == {"-r": True, "-f": True}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_parse_value_flag_space():
spec = CommandSpec(options=(Option(short="-n",
value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["-n", "10", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "10"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_parse_value_flag_joined():
spec = CommandSpec(options=(Option(short="-n",
value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["-n10", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "10"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_parse_long_bool_flag():
spec = CommandSpec(options=(Option(long="--hidden"), ),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["--hidden", "dir/"], cwd="/")
assert parsed.flags == {"--hidden": True}
assert parsed.args == [("/dir", OperandKind.PATH)]
def test_parse_long_value_flag():
spec = CommandSpec(options=(Option(long="--type",
value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["--type", "py", "src/"], cwd="/")
assert parsed.flags == {"--type": "py"}
assert parsed.args == [("/src", OperandKind.PATH)]
def test_parse_positional_text_then_path():
spec = CommandSpec(
options=(Option(short="-i"), Option(short="-v")),
positional=(Operand(kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-i", "pattern", "file1.txt", "file2.txt"],
cwd="/data")
assert parsed.flags == {"-i": True}
assert parsed.args == [
("pattern", OperandKind.TEXT),
("/data/file1.txt", OperandKind.PATH),
("/data/file2.txt", OperandKind.PATH),
]
def test_search_spec_parses_query_paths_and_options():
parsed = parse_command(
SPECS["search"],
[
"--method", "hybrid", "--top-k", "5", "--threshold", "0.4",
"login docs", "guides/*.md"
],
cwd="/knowledge",
)
assert parsed.flag("--method") == "hybrid"
assert parsed.flag("--top-k") == "5"
assert parsed.flag("--threshold") == "0.4"
assert parsed.args == [("login docs", OperandKind.TEXT),
("/knowledge/guides/*.md", OperandKind.PATH)]
def test_parse_double_dash_stops_flags():
spec = CommandSpec(options=(Option(short="-r"), ),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["--", "-r"], cwd="/")
assert parsed.flags == {}
assert parsed.args == [("/-r", OperandKind.PATH)]
def test_parse_text_rest():
spec = CommandSpec(rest=Operand(kind=OperandKind.TEXT))
parsed = parse_command(spec, ["hello", "world"], cwd="/")
assert parsed.args == [("hello", OperandKind.TEXT),
("world", OperandKind.TEXT)]
def test_parsed_args_paths():
p = ParsedArgs(flags={},
args=[("/a", OperandKind.PATH), ("text", OperandKind.TEXT),
("/b", OperandKind.PATH)])
assert p.paths() == ["/a", "/b"]
def test_parsed_args_texts():
p = ParsedArgs(flags={},
args=[("/a", OperandKind.PATH), ("text", OperandKind.TEXT)])
assert p.texts() == ["text"]
def test_parsed_args_flag():
p = ParsedArgs(flags={"-n": "10", "-r": True}, args=[])
assert p.flag("-n") == "10"
assert p.flag("-r") is True
assert p.flag("-x") is None
assert p.flag("-x", "default") == "default"
def test_parse_combined_bool_and_value():
spec = CommandSpec(options=(Option(short="-r"), Option(short="-i"),
Option(short="-m",
value_kind=OperandKind.TEXT)),
rest=Operand(kind=OperandKind.PATH))
parsed = parse_command(spec, ["-ri", "-m", "5", "file.txt"], cwd="/")
assert parsed.flags == {"-r": True, "-i": True, "-m": "5"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_clustered_flags_with_unknown_short_reported_as_invalid():
# Regression: a real user ran `grep -RIl "Base3\|base3" /r2/Review` and
# the pattern + path got swapped because `-I` was missing from the spec.
# The offending character is reported as invalid instead of the token
# becoming the pattern and shifting the real pattern into the paths.
spec_missing_I = CommandSpec(
options=(Option(short="-R"),
Option(short="-l")), # -I deliberately missing
positional=(Operand(kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec_missing_I,
["-RIl", "Base3\\|base3", "/r2/Review"],
cwd="/")
assert parsed.texts() == ["Base3\\|base3"]
assert parsed.paths() == ["/r2/Review"]
assert parsed.invalid_options == ["I"]
def test_clustered_flags_with_all_known_short_classifies_correctly():
spec_full = CommandSpec(
options=(Option(short="-R"), Option(short="-I"), Option(short="-l")),
positional=(Operand(kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec_full, ["-RIl", "Base3\\|base3", "/r2/Review"],
cwd="/")
assert parsed.flags == {"-R": True, "-I": True, "-l": True}
assert parsed.texts() == ["Base3\\|base3"]
assert parsed.paths() == ["/r2/Review"]
def test_grep_spec_recognizes_capital_I_for_ignore_binary():
grep_spec = SPECS["grep"]
parsed = parse_command(grep_spec, ["-RIl", "Base3\\|base3", "/r2/Review"],
cwd="/")
assert parsed.flags.get("-I") is True
assert parsed.flags.get("-R") is True
assert parsed.flags.get("-l") is True
assert parsed.texts() == ["Base3\\|base3"]
assert parsed.paths() == ["/r2/Review"]
def test_all_commands_have_specs():
expected = {
"ls",
"stat",
"pwd",
"find",
"tree",
"du",
"cat",
"head",
"tail",
"wc",
"md5",
"diff",
"file",
"nl",
"grep",
"search",
"rg",
"sort",
"uniq",
"cut",
"mkdir",
"touch",
"cp",
"mv",
"rm",
"sed",
"echo",
"tee",
"tr",
"curl",
"wget",
"jq",
"awk",
"base64",
"bash",
"tar",
"gzip",
"gunzip",
"zip",
"unzip",
"sha256sum",
"tac",
"paste",
"ln",
"readlink",
"basename",
"dirname",
"realpath",
"printf",
"seq",
"split",
"xxd",
"patch",
"shuf",
"comm",
"column",
"fold",
"fmt",
"cmp",
"iconv",
"strings",
"rev",
"zcat",
"zgrep",
"mktemp",
"bc",
"expr",
"date",
"csplit",
"expand",
"unexpand",
"tsort",
"look",
"sleep",
"join",
"python",
"python3",
"history",
}
assert set(SPECS.keys()) == expected
def test_grep_spec_parses_correctly():
spec = SPECS["grep"]
parsed = parse_command(spec, ["-rni", "pattern", "/data/file.txt"],
cwd="/")
assert parsed.flags["-r"] is True
assert parsed.flags["-n"] is True
assert parsed.flags["-i"] is True
assert parsed.args[0] == ("pattern", OperandKind.TEXT)
assert parsed.args[1] == ("/data/file.txt", OperandKind.PATH)
def test_head_spec_parses_n_flag():
spec = SPECS["head"]
parsed = parse_command(spec, ["-n", "20", "file.txt"], cwd="/data")
assert parsed.flag("-n") == "20"
assert parsed.paths() == ["/data/file.txt"]
def test_head_spec_parses_joined_n():
spec = SPECS["head"]
parsed = parse_command(spec, ["-n20", "file.txt"], cwd="/")
assert parsed.flag("-n") == "20"
assert parsed.paths() == ["/file.txt"]
def test_echo_spec_text_args():
spec = SPECS["echo"]
parsed = parse_command(spec, ["hello", "world"], cwd="/")
assert parsed.texts() == ["hello", "world"]
def test_rm_spec_combined_flags():
spec = SPECS["rm"]
parsed = parse_command(spec, ["-rf", "a", "b", "c"], cwd="/data")
assert parsed.flags["-r"] is True
assert parsed.flags["-f"] is True
assert parsed.paths() == ["/data/a", "/data/b", "/data/c"]
def test_cp_spec():
spec = SPECS["cp"]
parsed = parse_command(spec, ["-r", "src/", "dst/"], cwd="/data")
assert parsed.flags["-r"] is True
assert parsed.paths() == ["/data/src", "/data/dst"]
def test_rg_spec_with_long_flags():
spec = SPECS["rg"]
parsed = parse_command(spec,
["--type", "py", "--hidden", "pattern", "src/"],
cwd="/")
assert parsed.flag("--type") == "py"
assert parsed.flag("--hidden") is True
assert parsed.args[0] == ("pattern", OperandKind.TEXT)
assert parsed.args[1] == ("/src", OperandKind.PATH)
def test_find_spec():
spec = SPECS["find"]
parsed = parse_command(spec, ["/data", "-name", "*.py", "-type", "f"],
cwd="/")
assert parsed.paths() == ["/data"]
assert parsed.flag("-name") == ["*.py"]
assert parsed.flag("-type") == ["f"]
def test_curl_spec():
spec = SPECS["curl"]
parsed = parse_command(spec, ["-H", "Accept: json", "http://example.com"],
cwd="/")
assert parsed.flag("-H") == "Accept: json"
assert parsed.texts() == ["http://example.com"]
def test_wget_spec():
spec = SPECS["wget"]
parsed = parse_command(spec, ["http://example.com/file.zip", "out.zip"],
cwd="/data")
assert parsed.args[0] == ("http://example.com/file.zip", OperandKind.TEXT)
assert parsed.args[1] == ("/data/out.zip", OperandKind.PATH)
def test_sed_spec():
spec = SPECS["sed"]
parsed = parse_command(spec, ["-i", "s/foo/bar/", "file.txt"], cwd="/data")
assert parsed.flag("-i") is True
assert parsed.args[0] == ("s/foo/bar/", OperandKind.TEXT)
assert parsed.args[1] == ("/data/file.txt", OperandKind.PATH)
def test_sort_spec():
spec = SPECS["sort"]
parsed = parse_command(spec, ["-k", "2", "-t", ",", "-rn", "data.csv"],
cwd="/")
assert parsed.flag("-k") == "2"
assert parsed.flag("-t") == ","
assert parsed.flag("-r") is True
assert parsed.flag("-n") is True
assert parsed.paths() == ["/data.csv"]
def test_parse_to_kwargs_short_bool():
parsed = ParsedArgs(flags={"-l": True, "-a": True}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"args_l": True, "a": True}
def test_parse_to_kwargs_short_value():
parsed = ParsedArgs(flags={"-n": "5"}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"n": "5"}
def test_parse_to_kwargs_long_flags():
parsed = ParsedArgs(flags={"--hidden": True, "--type": "py"}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"hidden": True, "type": "py"}
def test_parse_to_kwargs_mixed():
parsed = ParsedArgs(flags={
"-i": True,
"--glob": "*.py",
"-m": "10"
},
args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"i": True, "glob": "*.py", "m": "10"}
def test_parse_to_kwargs_ambiguous_names():
parsed = ParsedArgs(flags={"-l": True, "-O": True, "-I": True}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"args_l": True, "args_O": True, "args_I": True}
def test_parse_to_kwargs_dash_one_maps_to_args_1():
parsed = ParsedArgs(flags={"-1": True}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {"args_1": True}
def test_ls_spec_parses_dash_one_and_dash_l_together():
parsed = parse_command(SPECS["ls"], ["-1", "-l", "/data"], cwd="/")
kw = parse_to_kwargs(parsed)
assert kw["args_1"] is True
assert kw["args_l"] is True
def test_parse_to_kwargs_empty():
parsed = ParsedArgs(flags={}, args=[])
kw = parse_to_kwargs(parsed)
assert kw == {}
def test_cache_flag_single_path():
spec = SPECS["grep"]
parsed = parse_command(spec,
["pattern", "file.txt", "--cache", "file.txt"], "/")
assert parsed.cache_paths == ["/file.txt"]
def test_cache_flag_multiple_paths():
spec = SPECS["grep"]
parsed = parse_command(
spec, ["pattern", "f1.txt", "--cache", "f1.txt", "f2.txt"], "/")
assert parsed.cache_paths == ["/f1.txt", "/f2.txt"]
def test_cache_flag_empty():
spec = SPECS["cat"]
parsed = parse_command(spec, ["file.txt"], "/")
assert parsed.cache_paths == []
def test_cache_flag_with_other_flags():
spec = SPECS["grep"]
parsed = parse_command(
spec, ["-i", "pattern", "file.txt", "--cache", "file.txt"], "/")
assert parsed.cache_paths == ["/file.txt"]
assert parsed.flags.get("-i") is True
def test_option_bool_flag():
opt = Option(short="-v")
assert opt.short == "-v"
assert opt.long is None
assert opt.value_kind == OperandKind.NONE
def test_option_value_flag():
opt = Option(short="-n", value_kind=OperandKind.TEXT)
assert opt.value_kind == OperandKind.TEXT
def test_option_long_with_alias():
opt = Option(short="-r", long="--recursive")
assert opt.short == "-r"
assert opt.long == "--recursive"
def test_option_path_value():
opt = Option(short="-f", value_kind=OperandKind.PATH)
assert opt.value_kind == OperandKind.PATH
def test_operand_default():
op = Operand()
assert op.kind == OperandKind.PATH
def test_operand_text():
op = Operand(kind=OperandKind.TEXT)
assert op.kind == OperandKind.TEXT
def test_command_spec_new_style():
spec = CommandSpec(
options=(
Option(short="-r"),
Option(short="-n", value_kind=OperandKind.TEXT),
Option(long="--hidden"),
),
positional=(Operand(kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
assert len(spec.options) == 3
assert len(spec.positional) == 1
assert spec.rest.kind == OperandKind.PATH
def test_parse_new_spec_bool_flag():
spec = CommandSpec(
options=(Option(short="-r"), Option(short="-f")),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-rf", "file.txt"], cwd="/")
assert parsed.flags == {"-r": True, "-f": True}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_parse_new_spec_value_flag():
spec = CommandSpec(
options=(Option(short="-n", value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-n", "10", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "10"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_parse_new_spec_long_flags():
spec = CommandSpec(
options=(
Option(long="--hidden"),
Option(long="--type", value_kind=OperandKind.TEXT),
),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["--hidden", "--type", "py", "src/"], cwd="/")
assert parsed.flags == {"--hidden": True, "--type": "py"}
assert parsed.args == [("/src", OperandKind.PATH)]
def test_parse_new_spec_aliased_option():
spec = CommandSpec(
options=(Option(short="-r", long="--recursive"), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["--recursive", "dir/"], cwd="/")
assert parsed.flags == {"--recursive": True}
parsed2 = parse_command(spec, ["-r", "dir/"], cwd="/")
assert parsed2.flags == {"-r": True}
def test_parse_new_spec_path_value_flag():
spec = CommandSpec(
options=(
Option(short="-c"),
Option(short="-f", value_kind=OperandKind.PATH),
),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-c", "-f", "archive.tar", "file.txt"],
cwd="/data")
assert parsed.flags == {"-c": True, "-f": "/data/archive.tar"}
assert parsed.paths() == ["/data/file.txt"]
assert parsed.path_flag_values == ["/data/archive.tar"]
def test_parse_new_spec_positional_text_then_path():
spec = CommandSpec(
options=(Option(short="-i"), ),
positional=(Operand(kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-i", "pattern", "file1.txt"], cwd="/data")
assert parsed.flags == {"-i": True}
assert parsed.args == [
("pattern", OperandKind.TEXT),
("/data/file1.txt", OperandKind.PATH),
]
def test_parse_new_spec_no_rest():
# Overflow past a fixed arity is classified like the last positional
# slot and passed through; the command owns the refusal (#452).
spec = CommandSpec(positional=(Operand(kind=OperandKind.TEXT),
Operand(kind=OperandKind.TEXT)), )
parsed = parse_command(spec, ["hello", "world", "extra"], cwd="/")
assert parsed.args == [
("hello", OperandKind.TEXT),
("world", OperandKind.TEXT),
("extra", OperandKind.TEXT),
]
def test_parse_new_spec_joined_value_flag():
spec = CommandSpec(
options=(Option(short="-n", value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-n10", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "10"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_numeric_shorthand_treats_dash_n_as_flag():
spec = CommandSpec(
options=(Option(short="-n",
value_kind=OperandKind.TEXT,
numeric_shorthand=True), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-3", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "3"}
assert parsed.args == [("/file.txt", OperandKind.PATH)]
def test_numeric_shorthand_keeps_dash_n_value_form():
spec = CommandSpec(
options=(Option(short="-n",
value_kind=OperandKind.TEXT,
numeric_shorthand=True), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-n", "3", "file.txt"], cwd="/")
assert parsed.flags == {"-n": "3"}
def test_numeric_shorthand_opt_in_only():
spec = CommandSpec(
options=(Option(short="-n", value_kind=OperandKind.TEXT), ),
rest=Operand(kind=OperandKind.PATH),
)
parsed = parse_command(spec, ["-3", "file.txt"], cwd="/")
assert "-n" not in parsed.flags
def test_head_spec_supports_numeric_shorthand():
parsed = parse_command(SPECS["head"], ["-3", "/file.txt"], cwd="/")
assert parsed.flags.get("-n") == "3"
assert parsed.paths() == ["/file.txt"]
def test_tail_spec_supports_numeric_shorthand():
parsed = parse_command(SPECS["tail"], ["-5", "/file.txt"], cwd="/")
assert parsed.flags.get("-n") == "5"
assert parsed.paths() == ["/file.txt"]
def test_option_description_default_none():
opt = Option(short="-v")
assert opt.description is None
def test_option_description_round_trip():
opt = Option(short="-v", description="Verbose output.")
assert opt.description == "Verbose output."
def test_command_spec_description_default_none():
spec = CommandSpec()
assert spec.description is None
def test_command_spec_description_round_trip():
spec = CommandSpec(description="Do a thing.")
assert spec.description == "Do a thing."