summaryrefslogtreecommitdiff
path: root/patternutils
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--patternutils/commands/patterncfg.py40
-rw-r--r--patternutils/commands/pmatch.py100
-rw-r--r--patternutils/commands/pmediainfo.py2
-rw-r--r--patternutils/commands/targs.py122
-rw-r--r--patternutils/commands/tmv.py85
5 files changed, 226 insertions, 123 deletions
diff --git a/patternutils/commands/patterncfg.py b/patternutils/commands/patterncfg.py
index f30a39a..07c815a 100644
--- a/patternutils/commands/patterncfg.py
+++ b/patternutils/commands/patterncfg.py
@@ -4,6 +4,7 @@ import os.path
from typing import Literal, get_args
import click
+from click_aliases import ClickAliasedGroup
try:
from pygments import highlight # type: ignore
@@ -24,27 +25,46 @@ from ..types import (
CURRENT_SUPPORTED_PROGRAMS: tuple[PredefinedCommands] = get_args(PredefinedCommands)
-@click.group(context_settings={"help_option_names": ("-h", "--help", "-?")})
+@click.group(
+ context_settings={"help_option_names": ("-h", "--help", "-?")},
+ cls=ClickAliasedGroup,
+)
def patterncfg() -> None:
"""CLI tool for managing patternutils templates."""
pass
@patterncfg.command(name="init", help="Initialize config folders")
-def init() -> None:
+@click.option(
+ "-v",
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Explain what is being done.",
+)
+def init(verbose: bool) -> None:
"""Creates necessary config directories if they do not exist."""
for directory in itertools.chain(CURRENT_SUPPORTED_PROGRAMS, ("globals",)):
- os.makedirs(os.path.join(config.CONFIG_PATH, directory), exist_ok=True)
+ path = config.CONFIG_PATH / directory
+ try:
+ path.mkdir(parents=True)
+ except FileExistsError:
+ if verbose:
+ click.secho(f"{path} already exists!", fg="red", err=True)
+ else:
+ if verbose:
+ click.echo(f"Created {path}")
+ click.secho("Initialized patternutils!", fg="green")
@patterncfg.command(name="list", help="List all patternutils template files")
def list() -> None:
"""Lists all available template files in the config directory."""
try:
- for directory in os.listdir(config.CONFIG_PATH):
- click.secho(directory, fg="blue")
- for filename in os.listdir(os.path.join(config.CONFIG_PATH, directory)):
- click.echo(f"└── {filename}")
+ for directory in config.CONFIG_PATH.iterdir():
+ click.secho(directory.name, fg="blue")
+ for filename in directory.iterdir():
+ click.echo(f"└── {filename.name}")
except FileNotFoundError:
click.secho(
f"Config directory '{config.CONFIG_PATH}' not found! Run 'patterncfg init' first.",
@@ -75,8 +95,8 @@ def add(
if command == "pmatch":
fileext = "regex"
- filename = os.path.join(config.CONFIG_PATH, command, f"{name}.{fileext}")
- click.edit(filename=filename)
+ filename = config.CONFIG_PATH / command / f"{name}.{fileext}"
+ click.edit(filename=str(filename))
@patterncfg.command(name="remove", help="Remove a template file")
@@ -112,7 +132,7 @@ def remove(command: PredefinedCommands | None, name: str) -> None:
os.remove(files[0])
-@patterncfg.command(name="view", help="View a template file")
+@patterncfg.command(name="view", help="View a template file", aliases=["show"])
@click.option(
"-c",
"--command",
diff --git a/patternutils/commands/pmatch.py b/patternutils/commands/pmatch.py
index 7d02ac3..68b166a 100644
--- a/patternutils/commands/pmatch.py
+++ b/patternutils/commands/pmatch.py
@@ -6,6 +6,9 @@ import re
from typing import Generator, Iterator, Pattern
import click
+from click_option_group import MutuallyExclusiveOptionGroup, optgroup
+
+from patternutils.types import RegexFlags, TemplatePrefer
from .. import config, utils
from .. import match as mat
@@ -49,64 +52,101 @@ def apply_regex(
@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
-@click.option(
+@optgroup.group("General options")
+@optgroup.option(
+ "-v",
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Explain what is being done.",
+)
+@optgroup.group("Output Options")
+@optgroup.option(
+ "--stream",
+ is_flag=True,
+ default=False,
+ help="Output a stream of objects instead a list.",
+)
+@optgroup.option(
+ "--human-readable",
+ is_flag=True,
+ default=False,
+ help="Output the JSON in a human readable format.",
+)
+@optgroup.group("Match options")
+@optgroup.option(
"-d",
"--directory",
multiple=True,
default=["./"],
help="directories to search. Can be specified multiple times. Default: ./",
)
-@click.option(
+@optgroup.option(
"-m",
"--match-directories",
is_flag=True,
default=False,
help="Match on directories as well.",
)
-@click.option(
+@optgroup.option(
"-r",
"--recursive",
is_flag=True,
default=False,
help="Search directories recursively.",
)
-@click.option(
+@optgroup.option(
"--abort-on-no-match",
is_flag=True,
default=False,
help="Exit if a name does not match the INPUT_PATTERN.",
)
-@click.option(
+@optgroup.option(
"--match-full-path",
is_flag=True,
default=False,
help="Match on full absolute path instead of only the filename.",
)
-@click.option(
+@optgroup.group("Regex Options")
+@optgroup.option(
"-p",
"--use-predefined-pattern",
is_flag=True,
default=False,
help="Use predefined regex pattern.",
)
-@click.option(
- "--stream",
- is_flag=True,
- default=False,
- help="Output a stream of objects instead a list.",
+@optgroup.option(
+ "--flag",
+ type=click.Choice(
+ [
+ "ascii",
+ "a",
+ "ignorecase",
+ "i",
+ "multiline",
+ "m",
+ "dotall",
+ "s",
+ "verbose",
+ "x",
+ ],
+ case_sensitive=False,
+ ),
+ multiple=True,
+ help="Specify the flags for the regex",
)
-@click.option(
- "--human-readable",
- is_flag=True,
- default=False,
- help="Output the JSON in a human readable format.",
+@optgroup.group("Pattern Loading Options", cls=MutuallyExclusiveOptionGroup)
+@optgroup.option(
+ "--prefer-global",
+ "prefer",
+ flag_value="global",
+ help="Prefer loading predefined pattern from the globals directory",
)
-@click.option(
- "-v",
- "--verbose",
- is_flag=True,
- default=False,
- help="Explain what is being done.",
+@optgroup.option(
+ "--prefer-command",
+ "prefer",
+ flag_value="command",
+ help="Prefer loading predefined pattern from the pmatch directory",
)
@click.version_option()
@click.argument("regex-pattern")
@@ -121,12 +161,18 @@ def pmatch(
stream: bool,
human_readable: bool,
verbose: bool,
+ flags: list[RegexFlags],
+ prefer: TemplatePrefer,
) -> None:
"""Match the REGEX_PATTERN against files in specified directories and output JSON."""
if use_predefined_pattern:
try:
- regex_pattern, _ = config.load_template(regex_pattern, "pmatch")
+ regex_pattern, _ = config.load_template(
+ regex_pattern,
+ "pmatch",
+ prefer,
+ )
except FileNotFoundError:
click.secho(
f"Regex pattern '{regex_pattern}' not found!", fg="red", err=True
@@ -140,8 +186,10 @@ def pmatch(
)
raise SystemExit(1)
+ concrete_flags = utils.parse_regex_flags(flags)
+
try:
- regex_pattern_c = re.compile(regex_pattern)
+ regex_pattern_c = re.compile(regex_pattern, flags=concrete_flags)
except re.error as e:
if verbose:
click.secho("Regular expression could not be compiled!", fg="red", err=True)
@@ -150,7 +198,9 @@ def pmatch(
# Monkey patch walk function to include commandline options
walk_function = functools.partial(
- utils.walk, recursive=recursive, match_directories=match_directories
+ utils.walk,
+ recursive=recursive,
+ match_directories=match_directories,
)
new_walk = itertools.chain.from_iterable(map(walk_function, directory))
diff --git a/patternutils/commands/pmediainfo.py b/patternutils/commands/pmediainfo.py
index 2cc0f8f..b782ab5 100644
--- a/patternutils/commands/pmediainfo.py
+++ b/patternutils/commands/pmediainfo.py
@@ -1,7 +1,7 @@
import functools
import itertools
import os.path
-from typing import Any, Generator, Iterator,
+from typing import Any, Generator, Iterator
import click
from pymediainfo import MediaInfo
diff --git a/patternutils/commands/targs.py b/patternutils/commands/targs.py
index a4f91e0..e7c85cd 100644
--- a/patternutils/commands/targs.py
+++ b/patternutils/commands/targs.py
@@ -1,11 +1,12 @@
+import concurrent.futures
import functools
-import multiprocessing
import shlex
import subprocess
from collections.abc import Iterable
from typing import Any, Callable, TextIO
import click
+from click_option_group import optgroup
from .. import config, template, utils
from ..types import TemplateExtensions
@@ -27,78 +28,89 @@ def run_command(
@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
-@click.option(
+@optgroup.group("General options")
+@optgroup.option(
+ "-i",
+ "--interactive",
+ is_flag=True,
+ default=False,
+ help="Prompt before running command.",
+)
+@optgroup.option(
+ "-e",
+ "--editor",
+ is_flag=True,
+ default=False,
+ help="Open an editor before running commands to allow manual changes.",
+)
+@optgroup.option(
+ "-n",
+ "--dry-run",
+ is_flag=True,
+ default=False,
+ help="Perform a trial run without executing commands.",
+)
+@optgroup.option(
+ "-v",
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Explain what is being done.",
+)
+@optgroup.group("Input options")
+@optgroup.option(
"--read-from",
default="-",
type=click.File("r"),
- help="Read input from file or stdin.",
+ help="Input file (default: stdin)",
)
-@click.option(
+@optgroup.option(
"-s",
- "--shell",
+ "--stream",
is_flag=True,
default=False,
- help="Run command in a shell.",
+ help="Read input as stream of JSON objects.",
)
-@click.option(
+@optgroup.group("Template options")
+@optgroup.option(
"-t",
"--template-engine",
default="python",
type=click.Choice(["python", "jinja2"]),
help="Select the template engine to use.",
)
-@click.option(
- "-v",
- "--verbose",
- is_flag=True,
- default=False,
- help="Explain what is being done.",
-)
-@click.option(
- "-P",
- "--max-procs",
- default=1,
- type=click.IntRange(min=0, clamp=True),
- help="Max parallel processes.",
-)
-@click.option(
+@optgroup.option(
"-p",
"--use-predefined-template",
is_flag=True,
default=False,
help="Use a predefined template.",
)
-@click.option(
+@optgroup.group("Command options")
+@optgroup.option(
+ "--shell",
+ is_flag=True,
+ default=False,
+ help="Run command in a shell.",
+)
+@optgroup.option(
"--redirect-stdout",
is_flag=True,
default=False,
help="Redirect stdout to /dev/null.",
)
-@click.option(
+@optgroup.option(
"--redirect-stderr",
is_flag=True,
default=False,
help="Redirect stderr to /dev/null.",
)
-@click.option(
- "--stream",
- is_flag=True,
- default=False,
- help="Input is a stream of JSON objects.",
-)
-@click.option(
- "-e",
- "--editor",
- is_flag=True,
- default=False,
- help="Open an editor before running commands to allow manual changes.",
-)
-@click.option(
- "-n",
- "--dry-run",
- is_flag=True,
- default=False,
- help="Perform a trial run without executing commands.",
+@optgroup.option(
+ "-P",
+ "--max-procs",
+ default=1,
+ type=click.IntRange(min=0, clamp=True),
+ help="Max parallel processes.",
)
@click.version_option()
@click.argument("command")
@@ -123,6 +135,9 @@ def targs(
--stream option is used), which are then supplied to the templating engine,
to generate the command to run.
"""
+ if interactive and max_procs != 1:
+ pass
+
data: Iterable[dict[str, Any]]
if stream:
data = map(utils.json_loads, read_from)
@@ -176,12 +191,25 @@ def targs(
max_procs = utils.max_procs() if max_procs == 0 else max_procs
- if max_procs == 1:
+ if max_procs == 1 or interactive:
for cmd in commands:
- execute_command(cmd)
+ if verbose or interactive:
+ click.secho(cmd)
+
+ if not (interactive and not click.confirm("Run the command?")):
+ execute_command(cmd)
else:
- with multiprocessing.Pool(max_procs) as pool:
- pool.map(execute_command, commands)
+ with concurrent.futures.ProcessPoolExecutor(max_workers=max_procs) as executor:
+ futures = [
+ executor.submit(execute_command, command) for command in commands
+ ]
+ for future in concurrent.futures.as_completed(futures):
+ try:
+ result = future.result()
+ if result is not None and result.returncode != 0:
+ pass
+ except Exception:
+ pass
if __name__ == "__main__":
diff --git a/patternutils/commands/tmv.py b/patternutils/commands/tmv.py
index 58a75e5..ecc680b 100644
--- a/patternutils/commands/tmv.py
+++ b/patternutils/commands/tmv.py
@@ -4,6 +4,7 @@ from collections.abc import Iterable
from typing import Any, TextIO
import click
+from click_option_group import optgroup
from .. import config, template, utils
from ..types import TemplateExtensions
@@ -19,86 +20,90 @@ def move_file(old_filename: str, new_filename: str) -> None:
@click.command(context_settings={"help_option_names": ("-h", "--help", "-?")})
-@click.option(
+@optgroup.group("General options")
+@optgroup.option(
"-i",
"--interactive",
is_flag=True,
default=False,
help="Prompt before renaming files.",
)
-@click.option(
+@optgroup.option(
"-e",
"--editor",
is_flag=True,
default=False,
help="Open an editor before renaming files to allow manual changes.",
)
-@click.option(
- "-f",
- "--force",
+@optgroup.option(
+ "-n",
+ "--dry-run",
is_flag=True,
default=False,
- help="Force rename, even if a file exists.",
+ help="Perform a trial run without making changes.",
)
-@click.option(
- "-t",
- "--template-engine",
- default="python",
- type=click.Choice(["python", "jinja2"]),
- help="Select the template engine to use.",
+@optgroup.option(
+ "-v",
+ "--verbose",
+ is_flag=True,
+ default=False,
+ help="Explain what is being done.",
)
-@click.option(
- "-k",
- "--key",
- default="_abspath",
- help="Select the key to use as the source file.",
+@optgroup.group("Input options")
+@optgroup.option(
+ "--read-from",
+ default="-",
+ type=click.File("r"),
+ help="Input file (default: stdin)",
)
-@click.option(
+@optgroup.option(
"-s",
"--stream",
is_flag=True,
default=False,
help="Read input as stream of JSON objects.",
)
-@click.option(
+@optgroup.group("Template options")
+@optgroup.option(
+ "-t",
+ "--template-engine",
+ default="python",
+ type=click.Choice(["python", "jinja2"]),
+ help="Select the template engine to use.",
+)
+@optgroup.option(
"-p",
"--use-predefined-template",
is_flag=True,
default=False,
help="Use a predefined template.",
)
-@click.option(
- "--read-from",
- default="-",
- type=click.File("r"),
- help="Input file (default: stdin)",
+@optgroup.group("Renaming options")
+@optgroup.option(
+ "-k",
+ "--key",
+ default="_abspath",
+ help="Select the key to use as the source file.",
+)
+@optgroup.option(
+ "-f",
+ "--force",
+ is_flag=True,
+ default=False,
+ help="Force rename, even if a file exists.",
)
-@click.option(
+@optgroup.option(
"--abort-on-path-exist",
is_flag=True,
default=False,
help="Exit if a renamed path already exists.",
)
-@click.option(
+@optgroup.option(
"--abort-on-key-not-found",
is_flag=True,
default=False,
help="Exit if the specified key (specified by the --key option) is not found in the input JSON.",
)
-@click.option(
- "-n",
- "--dry-run",
- is_flag=True,
- default=False,
- help="Perform a trial run without making changes.",
-)
-@click.option(
- "-v",
- "--verbose",
- is_flag=True,
- default=False,
- help="Explain what is being done.",
-)
@click.version_option()
@click.argument("output-pattern")
def tmv(