diff --git a/run.py b/run.py index 0655cf5..05610bd 100755 --- a/run.py +++ b/run.py @@ -439,6 +439,65 @@ class StorageManager: / f"{filename}.{lang.value}" ) + def list_entries( + self, + location: str | None = None, + lang: Language | None = None, + completed: bool | None = None, + ) -> list[tuple[str, str, str, bool]]: + """저장된 파일 목록을 반환합니다. + + Args: + location: location 필터 (None이면 전체) + lang: language 필터 (None이면 전체) + completed: completed 필터 (None이면 both, True이면 completed만, False이면 uncompleted만) + + Returns: + list[tuple[str, str, str, bool]]: (location, lang, filename, is_completed) + """ + result: list[tuple[str, str, str, bool]] = [] + + if not STORAGE_DIR.is_dir(): + return result + + for loc_dir in sorted(STORAGE_DIR.iterdir()): + if not loc_dir.is_dir(): + continue + loc_name = loc_dir.name + + if location and loc_name != location: + continue + + for lang_dir in sorted(loc_dir.iterdir()): + if not lang_dir.is_dir(): + continue + lang_name = lang_dir.name + + if Language.convert_ext(lang_name) is Language.UNDEFINED: + continue + + if lang and lang_name != lang.value: + continue + + for f in sorted( + lang_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem) + ): + if f.is_file(): + result.append((loc_name, lang_name, f.stem, False)) + + completed_dir = lang_dir / "completed" + if completed_dir.is_dir(): + for f in sorted( + completed_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem) + ): + if f.is_file(): + result.append((loc_name, lang_name, f.stem, True)) + + if completed is not None: + result = [e for e in result if e[3] == completed] + + return result + # ======================= # MAIN CLI LOGIC @@ -547,8 +606,7 @@ def run(lang: str, testcase: tuple[str, ...], verbose: bool): ) # 테케 분석 - testcase: list[str] = list(testcase) - tcs: list[int] = _parse_range_string_list(testcase) + tcs: list[int] = _parse_range_string_list(list(testcase)) # build try: @@ -657,7 +715,7 @@ def load(target: str, force: bool): @click.command(name="export") -@click.argument("from", "from_", type=str, nargs=-1) +@click.argument("from_", type=str, nargs=-1) @click.option( "--all", "all_", default=False, is_flag=True, help="Export all files in the space" ) @@ -810,91 +868,41 @@ def init(count: int): @click.command(name="show") -@click.argument("filter", type=str, default=None, required=False) -@click.option( - "--completed/--no-completed", - "-c/-nc", - default=False, - help="Filter by completed status (default: uncompleted only)", -) -@click.option( - "--all", - "show_all", - is_flag=True, - default=False, - help="Show all problems regardless of completed status", -) -def show(filter: str | None, completed: bool, show_all: bool): - """ - Show all stored problems in storage directory. +@click.argument("filter_", type=str, default=None, required=False) +@click.option("--completed", "completed_mode", flag_value="completed", default=True) +@click.option("--uncompleted", "completed_mode", flag_value="uncompleted") +@click.option("--both-completed", "completed_mode", flag_value="both") +def show(filter_: str | None, completed_mode: str): + """Show all stored problems in storage directory. FILTER format (optional): location e.g. zeta location/lang e.g. zeta/rs /lang e.g. /py + + FILTER MODE (default: --completed): + --completed show completed only + --uncompleted show uncompleted only + --both-completed show both completed and uncompleted """ - location = None - lang = None - if filter: - parts = filter.split("/", 1) - if len(parts) == 2: - location = parts[0] or None - lang = parts[1] or None - else: - location = parts[0] + completed = {"completed": True, "uncompleted": False, "both": None}[completed_mode] - storage = pathlib.Path(STORAGE_DIR) - if not storage.is_dir(): - raise click.ClickException(f"Storage directory '{STORAGE_DIR}' not found") + location = lang = None + if filter_: + parts = filter_.split("/", 1) + location = parts[0] if parts[0].strip() else None + lang = parts[1] if len(parts) == 2 else None - # Collect all files - entries = [] # (location, language, filename, is_completed) - - for loc_dir in sorted(storage.iterdir()): - if not loc_dir.is_dir(): - continue - loc_name = loc_dir.name - - if location and loc_name != location: - continue - - for lang_dir in sorted(loc_dir.iterdir()): - if not lang_dir.is_dir(): - continue - lang_name = lang_dir.name - - if ( - lang - and lang_name != Language.convert_name(lang).value - and lang_name != lang - ): - continue - - # uncompleted files - for f in sorted( - lang_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem) - ): - if f.is_file(): - entries.append((loc_name, lang_name, f.stem, False)) - - # completed files - completed_dir = lang_dir / "completed" - if completed_dir.is_dir(): - for f in sorted( - completed_dir.iterdir(), key=lambda p: _natural_sort_key(p.stem) - ): - if f.is_file(): - entries.append((loc_name, lang_name, f.stem, True)) - - # Filter by completed status - if not show_all: - entries = [e for e in entries if e[3] == completed] + entries = StorageManager().list_entries( + location=location, + lang=Language.convert_name(lang) if lang else None, + completed=completed, + ) if not entries: - click.echo("No problems found.") + click.echo(" - ") return - # Display total = len(entries) completed_count = sum(1 for e in entries if e[3]) uncompleted_count = total - completed_count @@ -906,7 +914,6 @@ def show(filter: str | None, completed: bool, show_all: bool): ) click.echo() - # Group by location current_loc = None current_lang = None for loc_name, lang_name, file_name, is_completed in entries: @@ -928,90 +935,64 @@ def show(filter: str | None, completed: bool, show_all: bool): @click.command(name="find") -@click.argument("keyword", type=str) +@click.argument("filter_", type=str) @click.option( "--completed/--no-completed", "-c/-nc", default=None, help="Filter by completed status (default: all)", ) -def find(keyword: str, completed: bool | None): +def find(filter_: str, completed: bool | None): """ - Find problems by keyword in storage. + Find problems by filter in storage. - KEYWORD format: - keyword e.g. 2447 - keyword.ext e.g. 2447.py (filters by language) - .ext e.g. .py (all files of a language) - location/keyword e.g. zeta/2447 - location/keyword.ext e.g. zeta/2447.py - location/.ext e.g. zeta/.py + FILTER format: + filter e.g. 2447 + filter.ext e.g. 2447.py (filters by language) + .ext e.g. .py (all files of a language) + location/filter e.g. zeta/2447 + location/filter.ext e.g. zeta/2447.py + location/.ext e.g. zeta/.py """ - location = None - lang = None - if "/" in keyword: - parts = keyword.split("/", 1) + location: str | None = None + filter_base: str | None = None + lang: Language | None = None + + if "/" in filter_: + parts = filter_.split("/", 1) location = parts[0] or None - keyword = parts[1] + filter_ = parts[1] + else: + location = None - # Extract language from extension - if "." in keyword: - keyword_base, ext = keyword.rsplit(".", 1) + if "." in filter_: + filter_base, ext = filter_.rsplit(".", 1) lang_candidate = Language.convert_ext(ext) if lang_candidate is not Language.UNDEFINED: - lang = lang_candidate.value + lang = lang_candidate + filter_base = None if filter_base == "" else filter_base else: - keyword_base = keyword + filter_base = filter_ else: - keyword_base = keyword - - storage = pathlib.Path(STORAGE_DIR) - if not storage.is_dir(): - raise click.ClickException(f"Storage directory '{STORAGE_DIR}' not found") - - entries = [] # (location, language, filename, is_completed) - - for loc_dir in sorted(storage.iterdir()): - if not loc_dir.is_dir(): - continue - loc_name = loc_dir.name - - if location and loc_name != location: - continue - - for lang_dir in sorted(loc_dir.iterdir()): - if not lang_dir.is_dir(): - continue - lang_name = lang_dir.name - - if lang and lang_name != lang: - continue - - for f in lang_dir.iterdir(): - if f.is_file() and keyword_base.lower() in f.stem.lower(): - entries.append((loc_name, lang_name, f.stem, False)) - - completed_dir = lang_dir / "completed" - if completed_dir.is_dir(): - for f in completed_dir.iterdir(): - if f.is_file() and keyword_base.lower() in f.stem.lower(): - entries.append((loc_name, lang_name, f.stem, True)) - - if completed is not None: - entries = [e for e in entries if e[3] == completed] + filter_base = filter_ + entries = StorageManager().list_entries( + location=location, lang=lang, completed=completed + ) + if filter_base is not None: + entries = [e for e in entries if filter_base.lower() in e[2].lower()] entries.sort(key=lambda e: (e[0], e[1], _natural_sort_key(e[2]))) if not entries: click.echo("No problems found.") return - total = len(entries) + total_count = len(entries) completed_count = sum(1 for e in entries if e[3]) - uncompleted_count = total - completed_count + uncompleted_count = sum(1 for e in entries if not e[3]) click.secho( - f"Found: {total} (completed: {completed_count}, uncompleted: {uncompleted_count})", + f"Found: {total_count} (completed: {completed_count}, uncompleted: {uncompleted_count})", fg="cyan", bold=True, ) @@ -1039,7 +1020,7 @@ def find(keyword: str, completed: bool | None): @click.command(name="fetchprob") @click.argument("target", type=str, nargs=1, required=True) -@click.option("--force", "-f", is_flag=True, help="Overwrite existing HTML files") +@click.option("--force", "-f", is_flag=True, help="Overwrite existing static files") def fetchprob(target: str, force: bool): """(disabled) Fetch BOJ problem HTML - BOJ is closing, keeping for future multi-location support.""" del target, force