diff --git a/scripts/odysseus b/scripts/odysseus index b5ab6b9..5d92238 100755 --- a/scripts/odysseus +++ b/scripts/odysseus @@ -68,6 +68,10 @@ def _short_help(path: Path) -> str: return first +def _is_runnable_subcommand(path: Path) -> bool: + return path.exists() and path.is_file() and os.access(path, os.X_OK) + + def _print_listing() -> None: """`odysseus` with no args (or `odysseus help`) — print the table.""" sys.stdout.write(f"odysseus {VERSION} — every feature, on the shell.\n\n") @@ -101,7 +105,7 @@ def main(argv: list[str] | None = None) -> int: _print_listing() return 0 sub = SCRIPTS_DIR / f"odysseus-{argv[1]}" - if not sub.exists(): + if not _is_runnable_subcommand(sub): sys.stderr.write(f"odysseus: unknown subcommand {argv[1]!r}\n") return 1 return subprocess.call([str(sub), "--help"]) @@ -109,7 +113,7 @@ def main(argv: list[str] | None = None) -> int: # `odysseus foo ...` → exec `odysseus-foo ...` under the project venv. name = argv[0] sub = SCRIPTS_DIR / f"odysseus-{name}" - if not sub.exists(): + if not _is_runnable_subcommand(sub): sys.stderr.write( f"odysseus: unknown subcommand {name!r}. " f"Try `odysseus help` to see available ones.\n" diff --git a/tests/test_odysseus_dispatcher.py b/tests/test_odysseus_dispatcher.py new file mode 100644 index 0000000..96637e7 --- /dev/null +++ b/tests/test_odysseus_dispatcher.py @@ -0,0 +1,24 @@ +import importlib.machinery +import importlib.util +from pathlib import Path + + +def _load_dispatcher(): + path = Path(__file__).resolve().parent.parent / "scripts" / "odysseus" + loader = importlib.machinery.SourceFileLoader("odysseus_dispatcher_under_test", str(path)) + spec = importlib.util.spec_from_loader(loader.name, loader) + module = importlib.util.module_from_spec(spec) + loader.exec_module(module) + return module + + +def test_is_runnable_subcommand_requires_executable_file(tmp_path): + cli = _load_dispatcher() + sub = tmp_path / "odysseus-demo" + sub.write_text("#!/bin/sh\n") + sub.chmod(0o644) + + assert cli._is_runnable_subcommand(sub) is False + + sub.chmod(0o755) + assert cli._is_runnable_subcommand(sub) is True