fix: refuse abbreviated flags in every script

argparse completed a prefix into the only flag starting with it:
slots.py --emulator read profiles from a directory named after the
emulator and reported nothing, and pipeline.py --all built every pack
variant. Every parser now refuses an abbreviation.
This commit is contained in:
Abdessamad Derraz committed 2026-10-18 05:30:58 +02:00
1 parent 88a3bb8953
commit e1b6aad51b
53 files changed
+79 -33

No files matched your search

+1 -1
View File
@@ -8,7 +8,7 @@ param(
$ErrorActionPreference = "Stop"
$defaultInstallUrl = "https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
$defaultInstallSha256 = "bfc3a2127689147e8a5f729a9ed668608f06dbd3b14ca6e768cdf2a7f90d5d32"
$defaultInstallSha256 = "a7c7a25573c6c96beb63eebbc079956dca4f173b9cc8aa6397935654ddc23f71"
$maximumInstallerBytes = 2MB
$installerExit = 0
$installer = if ($PSScriptRoot) { Join-Path $PSScriptRoot "install.py" } else { $null }
+1
View File
@@ -2830,6 +2830,7 @@ def _requested_narrowing(
def main() -> None:
"""Entry point."""
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Download missing BIOS files for retrogaming emulators.",
)
parser.add_argument(
+1 -1
View File
@@ -18,7 +18,7 @@ esac
TEMP_INSTALLER=""
TEMP_DIRECTORY=""
DEFAULT_INSTALL_URL="https://raw.githubusercontent.com/Abdess/retrobios/main/install.py"
DEFAULT_INSTALL_SHA256="bfc3a2127689147e8a5f729a9ed668608f06dbd3b14ca6e768cdf2a7f90d5d32"
DEFAULT_INSTALL_SHA256="a7c7a25573c6c96beb63eebbc079956dca4f173b9cc8aa6397935654ddc23f71"
MAX_INSTALLER_BYTES=2097152
cleanup() {
+1 -1
View File
@@ -415,7 +415,7 @@ def generate_issue_body(missing: list[dict], platform: str) -> str:
def main() -> None:
parser = argparse.ArgumentParser(description="Auto-fetch missing BIOS files")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Auto-fetch missing BIOS files")
parser.add_argument("--platform", "-p", help="Platform to check")
parser.add_argument("--all", action="store_true", help="Check all platforms")
parser.add_argument(
+1
View File
@@ -177,6 +177,7 @@ def update_changed(report: dict) -> list[str]:
def main() -> None:
logging.basicConfig(level=logging.INFO, format="%(message)s")
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Check buildbot system directory for changes",
)
parser.add_argument(
+1 -1
View File
@@ -1149,7 +1149,7 @@ def render(findings: list[Finding]) -> str:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0])
parser.add_argument("--only", help="comma-separated areas: " + ",".join(AREAS))
parser.add_argument("--json", action="store_true", help="machine-readable output")
parser.add_argument("--profiles", action="store_true", help="also run profile_sync (slow)")
+1 -1
View File
@@ -318,7 +318,7 @@ def fetch_assets() -> dict[str, int]:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n")[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n")[0])
parser.add_argument("--db", default="database.json")
parser.add_argument("--gitignore", default=".gitignore")
parser.add_argument("--json", action="store_true")
+1 -1
View File
@@ -748,7 +748,7 @@ def print_report(report: dict, scope: str = "all emulators") -> None:
def main() -> None:
parser = argparse.ArgumentParser(description="Emulator vs platform gap analysis")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Emulator vs platform gap analysis")
parser.add_argument("--emulators-dir", default=DEFAULT_EMULATORS_DIR)
parser.add_argument("--platforms-dir", default=DEFAULT_PLATFORMS_DIR)
parser.add_argument("--db", default=DEFAULT_DB)
+1 -1
View File
@@ -313,7 +313,7 @@ def deduplicate(bios_dir: str, dry_run: bool = False) -> dict:
def main() -> None:
parser = argparse.ArgumentParser(description="Deduplicate bios/ directory")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Deduplicate bios/ directory")
parser.add_argument(
"--dry-run", action="store_true", help="Preview without deleting"
)
+1 -1
View File
@@ -160,7 +160,7 @@ def _format_markdown(report: dict) -> str:
def main() -> None:
parser = argparse.ArgumentParser(description="Compare scraped vs truth YAMLs")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Compare scraped vs truth YAMLs")
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument(
"--all", action="store_true", help="diff all registered platforms"
+1
View File
@@ -311,6 +311,7 @@ def show_info(platform: str, release: dict) -> None:
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Download BIOS packs from GitHub Releases",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
+1
View File
@@ -502,6 +502,7 @@ def run(
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Rewrite each platform's own BIOS file, corrected.",
)
group = parser.add_mutually_exclusive_group(required=True)
+1 -1
View File
@@ -85,7 +85,7 @@ def audit(
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
parser.add_argument("emulators", nargs="+")
parser.add_argument("--emulators-dir", default="emulators")
parser.add_argument("--cache-dir", default=".cache")
+1 -1
View File
@@ -69,7 +69,7 @@ def find_revision(
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0])
parser.add_argument("--repo", required=True, help="forge URL of the project")
parser.add_argument("--path", required=True, help="path of the file in the project")
parser.add_argument("--md5", required=True, help="md5 the platform pins")
+1 -1
View File
@@ -342,7 +342,7 @@ def _preserve_large_file_entries(files: dict, db_path: str) -> int:
def main() -> int:
parser = argparse.ArgumentParser(description="Generate multi-indexed BIOS database")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate multi-indexed BIOS database")
parser.add_argument("--force", action="store_true", help="Force rehash all files")
parser.add_argument(
"--bios-dir", default=DEFAULT_BIOS_DIR, help="BIOS directory path"
+1 -1
View File
@@ -2871,7 +2871,7 @@ def main() -> None:
def _main(held: contextlib.ExitStack) -> None:
parser = argparse.ArgumentParser(description="Generate platform BIOS ZIP packs")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate platform BIOS ZIP packs")
parser.add_argument("--platform", "-p", help="Platform name (e.g., retroarch)")
parser.add_argument(
"--all", action="store_true", help="Generate packs for all active platforms"
+1 -1
View File
@@ -746,7 +746,7 @@ def generate_contributing() -> str:
def main() -> None:
parser = argparse.ArgumentParser(description="Generate slim README.md")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Generate slim README.md")
parser.add_argument("--db", default="database.json")
parser.add_argument("--platforms-dir", default="platforms")
args = parser.parse_args()
+1
View File
@@ -3749,6 +3749,7 @@ def generate_mkdocs_nav(
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Generate MkDocs site from project data"
)
parser.add_argument("--db", default="database.json")
+1
View File
@@ -36,6 +36,7 @@ DEFAULT_DB_FILE = "database.json"
def parse_args(argv: list[str] | None = None) -> argparse.Namespace:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Generate ground-truth YAML from emulator profiles",
)
group = parser.add_mutually_exclusive_group(required=True)
+1
View File
@@ -133,6 +133,7 @@ def main() -> int:
import argparse
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Report one content stored under identities that disagree.",
)
parser.add_argument("--bios-dir", default="bios")
+1 -1
View File
@@ -30,7 +30,7 @@ def list_platforms(include_archived: bool = False) -> list[str]:
def main() -> None:
parser = argparse.ArgumentParser(description="List available platforms")
parser = argparse.ArgumentParser(allow_abbrev=False, description="List available platforms")
parser.add_argument("--all", action="store_true", help="Include archived platforms")
args = parser.parse_args()
+1 -1
View File
@@ -114,7 +114,7 @@ def rewrite(name: str, emulators_dir: str, findings: list[Finding]) -> int:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.splitlines()[0])
parser.add_argument("emulators", nargs="+")
parser.add_argument("--emulators-dir", default="emulators")
parser.add_argument("--cache-dir", default=upstream.CACHE_DIR)
+1
View File
@@ -493,6 +493,7 @@ def migrate_local(source: Path, target: Path, dry_run: bool) -> tuple:
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Migrate BIOS files to Manufacturer/Console structure"
)
parser.add_argument(
+1 -1
View File
@@ -325,7 +325,7 @@ def _truth_steps(args: argparse.Namespace, results: dict) -> bool:
def main() -> None:
parser = argparse.ArgumentParser(description="Run the full retrobios pipeline")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Run the full retrobios pipeline")
parser.add_argument(
"--include-archived", action="store_true", help="Include archived platforms"
)
+1
View File
@@ -3239,6 +3239,7 @@ def emulators_dir_is_dirty(emulators_dir: str) -> bool:
def build_parser() -> argparse.ArgumentParser:
"""Command line: selection, output, detection, writes, network."""
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Confront emulator profiles with their upstream source"
)
group = parser.add_mutually_exclusive_group(required=True)
+1 -1
View File
@@ -130,7 +130,7 @@ def build_report(db: dict, snapshots: dict, members: dict | None = None) -> dict
def main() -> int:
parser = argparse.ArgumentParser(description="Dump-catalog coverage report")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Dump-catalog coverage report")
parser.add_argument("--db", default="database.json", help="Database path")
parser.add_argument(
"--provenance-dir",
+1
View File
@@ -518,6 +518,7 @@ def refresh_all(
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Refresh cached data directories from upstream"
)
parser.add_argument("--key", help="Refresh only this entry")
+1 -1
View File
@@ -377,7 +377,7 @@ def render(
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n\n")[0])
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__.split("\n\n")[0])
parser.add_argument(
"--only",
help="comma-separated areas to check: " + ",".join(AUTO_AREAS),
+1
View File
@@ -191,6 +191,7 @@ def print_report(report: dict) -> None:
def main() -> int:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Cross-check profile region: values against No-Intro provenance"
)
parser.add_argument("--db", default="database.json", help="Path to database.json")
+1 -1
View File
@@ -124,7 +124,7 @@ def _size(count: int) -> str:
def main() -> int:
parser = argparse.ArgumentParser(description="Record what a release's packs hold")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Record what a release's packs hold")
parser.add_argument("dist", type=Path, help="directory of the packs to publish")
parser.add_argument("--tag", required=True, help="release tag, e.g. v2026.10.04")
parser.add_argument("--install-dir", default="install", type=Path)
+1 -1
View File
@@ -92,7 +92,7 @@ def restore(
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__)
parser.add_argument("--cache", default=".cache/large")
parser.add_argument("--db", default="database.json")
parser.add_argument("--gitignore", default=".gitignore")
+1 -1
View File
@@ -451,7 +451,7 @@ def _resolve_modes(args: argparse.Namespace) -> None:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__)
parser.add_argument("--identify", action="store_true", help="label local archives")
parser.add_argument(
"--missing", action="store_true", help="attempt pinned archives we lack"
+1 -1
View File
@@ -320,7 +320,7 @@ def scraper_cli(
"""Shared CLI entry point for all scrapers. Eliminates main() boilerplate."""
import argparse
parser = argparse.ArgumentParser(description=description)
parser = argparse.ArgumentParser(allow_abbrev=False, description=description)
parser.add_argument("--dry-run", action="store_true", help="Show scraped data")
parser.add_argument("--output", "-o", help="Output YAML file")
parser.add_argument("--json", action="store_true", help="Output as JSON")
+1
View File
@@ -308,6 +308,7 @@ def main() -> None:
import argparse
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Scrape libretro-core-info firmware requirements"
)
parser.add_argument("--dry-run", action="store_true")
+1 -1
View File
@@ -91,7 +91,7 @@ def import_pack(source: str, pack: Path) -> tuple[dict, list[dict], int]:
def main() -> int:
parser = argparse.ArgumentParser(description="Import BIOS provenance from a DAT pack")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Import BIOS provenance from a DAT pack")
parser.add_argument("--source", required=True, choices=SOURCES)
parser.add_argument("--pack", required=True, help="DAT pack (ZIP or directory)")
parser.add_argument("--output", "-o", help="Snapshot output path")
+1
View File
@@ -303,6 +303,7 @@ def run(
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Scrape FBNeo BIOS set hashes from upstream source",
)
parser.add_argument(
+1
View File
@@ -421,6 +421,7 @@ def _write_release(args: argparse.Namespace) -> None:
def build_parser() -> argparse.ArgumentParser:
parser = argparse.ArgumentParser(
allow_abbrev=False,
prog="mame_hash_scraper",
description="Fetch MAME BIOS hashes from source and merge into profiles.",
)
+1 -1
View File
@@ -83,7 +83,7 @@ def fetch_snapshot() -> tuple[dict, list[dict]]:
def main() -> int:
parser = argparse.ArgumentParser(description="Fetch Redump BIOS DAT provenance")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Fetch Redump BIOS DAT provenance")
parser.add_argument("--dry-run", action="store_true", help="Show fetched entries")
parser.add_argument(
"--output", "-o", default="provenance/redump.json", help="Snapshot output path"
+1 -1
View File
@@ -443,7 +443,7 @@ def _listxml_opener(pack: Path) -> Callable[[], io.BufferedIOBase]:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__)
parser.add_argument("--source", choices=SOURCES, required=True)
parser.add_argument("--pack", help="DAT file, ZIP or directory")
parser.add_argument(
@@ -430,6 +430,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Scrape Batocera per-board emulator targets"
)
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
@@ -166,7 +166,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(description="Scrape EmuDeck emulator targets")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape EmuDeck emulator targets")
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
parser.add_argument("--output", "-o", help="Output YAML file")
args = parser.parse_args()
@@ -104,7 +104,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(description="Scrape Recalbox per-board cores")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape Recalbox per-board cores")
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
parser.add_argument("--output", "-o", help="Output YAML file")
args = parser.parse_args()
@@ -168,6 +168,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Scrape RetroArch buildbot nightly targets"
)
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
@@ -177,6 +177,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Scrape RetroPie package targets from scriptmodules"
)
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
@@ -174,7 +174,7 @@ class Scraper(BaseTargetScraper):
def main() -> None:
parser = argparse.ArgumentParser(description="Scrape ROCKNIX per-device cores")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Scrape ROCKNIX per-device cores")
parser.add_argument("--dry-run", action="store_true", help="Show target summary")
parser.add_argument("--output", "-o", help="Output YAML file")
args = parser.parse_args()
+1
View File
@@ -812,6 +812,7 @@ def main() -> int:
)
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Report destinations where an emulator profile contradicts "
"the file a platform baseline would ship.",
)
+1
View File
@@ -219,6 +219,7 @@ def _size(text: str) -> int:
def main() -> int:
parser = argparse.ArgumentParser(
allow_abbrev=False,
description="Split packs over the release asset limit into ZIP parts"
)
parser.add_argument("target", type=Path, help="a pack, or a directory of packs")
+1 -1
View File
@@ -116,7 +116,7 @@ def identify_romset(
def main() -> int:
"""Entry point: 1 when a checked archive is not TorrentZip."""
parser = argparse.ArgumentParser(description="TorrentZip builder and checker")
parser = argparse.ArgumentParser(allow_abbrev=False, description="TorrentZip builder and checker")
parser.add_argument("--check", metavar="ZIP", help="report TorrentZip conformance")
parser.add_argument("--rebuild", metavar="ZIP", help="normalize an archive")
parser.add_argument("--output", "-o", help="output path for --rebuild")
+1 -1
View File
@@ -336,7 +336,7 @@ def _load_database_if_any(path: str) -> dict | None:
def main() -> None:
parser = argparse.ArgumentParser(description="Validate BIOS file contributions")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Validate BIOS file contributions")
parser.add_argument("files", nargs="*", help="Files to validate")
parser.add_argument(
"--changed", action="store_true", help="Auto-detect changed BIOS files"
+1 -1
View File
@@ -336,7 +336,7 @@ def _format_errors() -> list[str]:
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__)
parser.add_argument(
"--source-only",
action="store_true",
+1 -1
View File
@@ -264,7 +264,7 @@ def validate_site(site: Path, config_path: Path) -> list[str]:
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser = argparse.ArgumentParser(allow_abbrev=False, description=__doc__)
parser.add_argument("--site-dir", default="site")
parser.add_argument("--config", default="mkdocs.yml")
parser.add_argument("--max-errors", type=int, default=100)
+1 -1
View File
@@ -2080,7 +2080,7 @@ def main() -> None:
def _main(held: contextlib.ExitStack) -> None:
parser = argparse.ArgumentParser(description="Platform-native BIOS verification")
parser = argparse.ArgumentParser(allow_abbrev=False, description="Platform-native BIOS verification")
parser.add_argument("--platform", "-p", help="Platform name")
parser.add_argument(
"--all", action="store_true", help="Verify all active platforms"
+27
View File
@@ -637,6 +637,33 @@ class AutoFetchReadsListsAndPlacesWithoutClobbering(unittest.TestCase):
self.assertEqual(held.read_bytes(), b"other revision")
class NoParserAcceptsAnAbbreviation(unittest.TestCase):
"""argparse completes a prefix: `slots.py --emulator dolphin` became
--emulators-dir dolphin and reported 0 contested destinations, exit 0;
`pipeline.py --all` became --all-variants and built six packs per
platform. The flags read like other scripts' and were never refused."""
def test_every_parser_refuses_prefixes(self):
import ast
lax = []
for path in sorted([*(ROOT / "scripts").rglob("*.py"), ROOT / "install.py"]):
for node in ast.walk(ast.parse(path.read_text(encoding="utf-8"))):
if not isinstance(node, ast.Call):
continue
func = node.func
name = func.attr if isinstance(func, ast.Attribute) else getattr(func, "id", "")
if name != "ArgumentParser":
continue
if not any(
kw.arg == "allow_abbrev"
and isinstance(kw.value, ast.Constant) and kw.value.value is False
for kw in node.keywords
):
lax.append(f"{path.relative_to(ROOT)}:{node.lineno}")
self.assertEqual(lax, [])
class FaqRegressions(unittest.TestCase):
"""The FAQ states facts the code owns; these tie it back to the source.