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
674 lines
21 KiB
Python
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."
|