diff --git a/cloudinary_cli/cli_group.py b/cloudinary_cli/cli_group.py index 210d0ac..9cd4147 100644 --- a/cloudinary_cli/cli_group.py +++ b/cloudinary_cli/cli_group.py @@ -14,7 +14,7 @@ CONTEXT_SETTINGS = dict(max_content_width=shutil.get_terminal_size()[0], terminal_width=shutil.get_terminal_size()[0]) -@click.group(context_settings=CONTEXT_SETTINGS) +@click.group(context_settings=CONTEXT_SETTINGS, invoke_without_command=True) @click.help_option() @click.version_option(cli_version, prog_name="Cloudinary CLI", message=f"%(prog)s, version %(version)s\n" @@ -24,10 +24,11 @@ help="""Tell the CLI which account to run the command on by specifying an account environment variable.""" ) @click.option("-C", "--config_saved", - help="""Tell the CLI which account to run the command on by specifying a saved configuration - see + help="""Tell the CLI which account to run the command on by specifying a saved configuration - see `config` command.""") @click_log.simple_verbosity_option(logger) -def cli(config, config_saved): +@click.pass_context +def cli(ctx, config, config_saved): if config: refresh_cloudinary_config(config) elif config_saved: @@ -40,4 +41,9 @@ def cli(config, config_saved): if not is_valid_cloudinary_config(): logger.warning("No Cloudinary configuration found.") + # If no subcommand was invoked, show help and exit with code 0 + if ctx.invoked_subcommand is None: + click.echo(ctx.get_help()) + ctx.exit(0) + return True diff --git a/cloudinary_cli/core/__init__.py b/cloudinary_cli/core/__init__.py index 5df20ac..7646e1c 100644 --- a/cloudinary_cli/core/__init__.py +++ b/cloudinary_cli/core/__init__.py @@ -8,7 +8,7 @@ from cloudinary_cli.core.utils import url, utils from cloudinary_cli.core.overrides import resolve_command -setattr(click.MultiCommand, "resolve_command", resolve_command) +setattr(click.Group, "resolve_command", resolve_command) commands = [ config, diff --git a/cloudinary_cli/core/overrides.py b/cloudinary_cli/core/overrides.py index 650f9c5..8eda507 100644 --- a/cloudinary_cli/core/overrides.py +++ b/cloudinary_cli/core/overrides.py @@ -1,14 +1,19 @@ -from click.parser import split_opt from click.utils import make_str from cloudinary import api, uploader from cloudinary.uploader import upload as original_upload from cloudinary.utils import cloudinary_url as original_cloudinary_url +from cloudinary_cli.utils.utils import split_opt # overrides click.MultiCommand.resolve_command def resolve_command(self, ctx, args): # Patch the `resolve_command` function to enable simple commands (eg. cld resource) # Only core commands from API and modules are registered (eg. cld admin) + + # Handle empty args (when CLI is invoked with no arguments) + if not args: + return None, None, [] + cmd_name = make_str(args[0]) original_cmd_name = cmd_name diff --git a/cloudinary_cli/modules/sync.py b/cloudinary_cli/modules/sync.py index d0acb65..8e82c82 100644 --- a/cloudinary_cli/modules/sync.py +++ b/cloudinary_cli/modules/sync.py @@ -300,7 +300,7 @@ def _normalize_remote_file_names(self, remote_files, local_files): def _local_candidates(self, candidate_path): filename, extension = path.splitext(candidate_path) - r = re.compile(f"({candidate_path}|{filename} \(\d+\){extension})") + r = re.compile(f"({candidate_path}|{filename} \\(\\d+\\){extension})") # sort local files by base name (without ext) for accurate results. return dict(sorted({f: self.local_files[f]["etag"] for f in filter(r.match, self.local_files.keys())}.items(), key=lambda f: path.splitext(f[0])[0])) diff --git a/cloudinary_cli/utils/utils.py b/cloudinary_cli/utils/utils.py index 1e1ed3d..e0c69a4 100644 --- a/cloudinary_cli/utils/utils.py +++ b/cloudinary_cli/utils/utils.py @@ -379,3 +379,25 @@ def duplicate_values(items, value_key, key_of_interest=None): rev_multidict.setdefault(value[value_key], set()).add(value[key_of_interest] if key_of_interest is not None else key) return {key: values for key, values in rev_multidict.items() if len(values) > 1} + + +def split_opt(opt): + """ + Splits an option string into prefix and value parts. + + This function replaces the deprecated click.parser.split_opt import. + Returns a tuple of (prefix, value) where prefix is the option prefix + (like '-' or '--') and value is the remaining part, or ('', opt) + if it doesn't look like an option. + + :param opt: The option string to parse. + :type opt: str + :return: Tuple of (prefix, value) + :rtype: tuple + """ + first = opt[:1] + if first.isalnum(): + return '', opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:]