Created
April 24, 2025 18:25
-
-
Save tos-kamiya/7abac530a6a1e46a82d88368db2e13d4 to your computer and use it in GitHub Desktop.
Select files by modification time with slicing semantics.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Select files by modification time with slicing semantics. | |
Usage: | |
latest.py [-n SLICE] [-q] <file>... | |
Examples: | |
# The single most recent file | |
latest.py ~/Pictures/* | |
# The 4 most recent files | |
latest.py -n 4 ~/Pictures/* | |
# Skip the most recent, then take files 1–3 | |
latest.py -n 1,4 ~/Pictures/* | |
# The single oldest file | |
latest.py -n -1 ~/Pictures/* | |
Options: | |
-q, --quiet Suppress all log output to stderr | |
""" | |
import sys | |
import os | |
import argparse | |
from typing import Union, List | |
def parse_slice(arg: str) -> Union[slice, int]: | |
""" | |
Parse a slice specification string into a slice or index. | |
- "4" -> slice(0, 4) (newest 4 files) | |
- "-1" -> -1 (oldest file) | |
- "1,4" -> slice(1, 4) (skip newest, take files 1–3) | |
""" | |
parts = arg.split(',') | |
if len(parts) == 1: | |
n = int(parts[0]) | |
if n >= 0: | |
return slice(0, n) | |
return n | |
if len(parts) == 2: | |
start, stop = map(int, parts) | |
return slice(start, stop) | |
raise argparse.ArgumentTypeError(f"Invalid slice specification: {arg}") | |
def main() -> None: | |
script_name = os.path.basename(sys.argv[0]) | |
parser = argparse.ArgumentParser( | |
description="Select files by modification time with slicing semantics.", | |
formatter_class=argparse.RawDescriptionHelpFormatter, | |
epilog=__doc__, | |
) | |
parser.add_argument( | |
'-n', '--slice', | |
dest='slice_spec', | |
type=parse_slice, | |
default=slice(0, 1), | |
help=( | |
"Which files to select (Python slice semantics):\n" | |
" 4 -> newest 4 files\n" | |
" 1,4 -> skip newest, take files 1–3\n" | |
" -1 -> the oldest file" | |
) | |
) | |
parser.add_argument( | |
'-q', '--quiet', | |
action='store_true', | |
help='Suppress all log output to stderr' | |
) | |
parser.add_argument( | |
'files', | |
nargs='+', | |
help='Files or glob patterns to consider' | |
) | |
args = parser.parse_args() | |
# Determine whether to emit logs: | |
# only if stdout is not a tty and not in quiet mode | |
log_enabled = (not sys.stdout.isatty()) and (not args.quiet) | |
try: | |
# Sort by modification time descending (newest first) | |
all_files: List[str] = sorted( | |
args.files, | |
key=lambda f: os.stat(f).st_mtime, | |
reverse=True | |
) | |
except OSError as e: | |
if log_enabled: | |
sys.stderr.write(f"{script_name}: Error: {e}\n") | |
sys.exit(1) | |
sel = args.slice_spec | |
if isinstance(sel, slice): | |
selected: List[str] = all_files[sel] | |
else: | |
selected = [all_files[sel]] | |
if not selected: | |
if log_enabled: | |
sys.stderr.write(f"{script_name}: No files selected.\n") | |
sys.exit(1) | |
if log_enabled: | |
# Write confirmation list to stderr with script name prefix | |
for path in selected: | |
sys.stderr.write(f"{script_name}: {path}\n") | |
# Output one file per line to stdout | |
for path in selected: | |
print(path) | |
if __name__ == "__main__": | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment