diff --git a/debian/install b/debian/install index 5169d0b62..56f174ecc 100644 --- a/debian/install +++ b/debian/install @@ -6,5 +6,6 @@ conf/* /usr/share/yunohost/conf/ locales/* /usr/share/yunohost/locales/ doc/yunohost.8.gz /usr/share/man/man8/ doc/bash_completion.d/* /etc/bash_completion.d/ +doc/yunohost_zsh_completion /usr/share/zsh/vendor-completions/_yunohost conf/metronome/modules/* /usr/lib/metronome/modules/ src/* /usr/lib/python3/dist-packages/yunohost/ diff --git a/debian/rules b/debian/rules index 5cf1d9bee..9d5935fb1 100755 --- a/debian/rules +++ b/debian/rules @@ -7,4 +7,5 @@ override_dh_auto_build: # Generate bash completion file python3 doc/generate_bash_completion.py + python3 doc/generate_zsh_completion.py python3 doc/generate_manpages.py --gzip --output doc/yunohost.8.gz diff --git a/doc/generate_zsh_completion.py b/doc/generate_zsh_completion.py new file mode 100644 index 000000000..18c987f62 --- /dev/null +++ b/doc/generate_zsh_completion.py @@ -0,0 +1,720 @@ +#!/usr/bin/python3 + +""" +INSTALL: + This script creates a `yunohost_zsh_completion` file in the same folder as this script. + To install, copy (and rename) the created file to: + - (Debian) `/usr/share/zsh/vendor-completions/_yunohost` + - (Fedora) `/usr/share/zsh/site-functions/_yunohost` + - (other distribution) `/usr/local/share/zsh/site-functions/_yunohost` + +DOCS: +- https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide +- http://zsh.sourceforge.net/Doc/Release/Completion-System.html#Completion-System + or `man zshcompsys` +- http://zsh.sourceforge.net/Guide/zshguide06.html + +Misc: +- http://zsh.sourceforge.net/Doc/Release/Parameters.html#Array-Parameters + +TODO: +- use the extra:required:True pattern (similar to `nargs`?) +- Have the global options listed in a separate category +- Allow multiple arguments per option + (e.g.: `yunohost user info alice --fields uid cn mail`) +- In `yunohost.yml`, consider merging: + - metavar + - pattern + - autocomplete +- Make use of `type`, maybe using `_guard` +- Use `pattern`, maybe with `_guard`. This seems hard though, as ZSH has +its own globbing language... +Link about this globbing system: +http://zsh.sourceforge.net/Doc/Release/Expansion.html#Filename-Generation + +NOTES: +- Command for debugging zsh: `unfunction _yunohost; autoload -U _yunohost` + +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Optimization: + - caching mecanism: invalidate the cache afer some commands? Hard, the + cache is local to user + - implement a zstyle switch, to change the cache validity period? + +AUTHORS: + - buzuck (Fol) + - kayou + - getzze +""" + +import os +import re +import yaml + + +THIS_SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) +ACTIONSMAP_FILE = THIS_SCRIPT_DIR + "/../share/actionsmap.yml" +ZSH_COMPLETION_FILE = THIS_SCRIPT_DIR + "/yunohost_zsh_completion" + +# This dict will contain the completion function data +COMPLETION_FUNCTIONS = {} + + +def main(): + yunohost_map = yaml.safe_load(open(ACTIONSMAP_FILE, "r")) + output = CONST_HEADER + # + # Creation of the entry function of the completion script + output += entry_point(yunohost_map) + + # + # The main loop, going through all (sub)commands + for key, value in yunohost_map.items(): + output += make_category(key, value) + + # + # Building the auxilliary completion functions, they have been added + # to this dict during the main loop + output += build_completion_functions(COMPLETION_FUNCTIONS) + output += CONST_END_OF_FILE + + with open(ZSH_COMPLETION_FILE, "w") as _yunohost: + _yunohost.write(output) + + +def entry_point(yunohost_map: dict) -> str: + """ + Provides the entry point of the completion script: a function called _yunohost, along with the routing function. + :param dict yunohost_map: The while YAML + :return str: The resulting string + """ + categories = "" + + # Remember the amount of spaces to get a human readable completion file + spaces = re.search(r"\n(\s*)YNH_GLOBAL_OPTIONS", ENTRY_POINT).group(1) + if "_global" in yunohost_map and "arguments" in yunohost_map["_global"]: + global_arguments = make_argument_list(yunohost_map["_global"], spaces) + else: + global_arguments = "\n".join( + "{}{}".format(spaces, global_arg) for global_arg in GLOBAL_ARGUMENTS + ) + # Remove spaces and newlines before and after + global_arguments = global_arguments.strip() + + # Remember the amount of spaces to get a human readable completion file + spaces = re.search(r"\n(\s*)YNH_COMMANDS_DESCRIPTION", ENTRY_POINT).group(1) + # + # Going through the main ynh commands + for category, content in yunohost_map.items(): + # We only consider a command an item that has a "category_help" field + if "category_help" not in content: + continue + + # Creation of the category line, with the right amount of spaces + categories += "{}'{}:{}'\n".format( + spaces, category, _escape(content["category_help"]) + ) + # Remove spaces and newlines before and after + categories = categories.strip() + + return ENTRY_POINT.replace("YNH_GLOBAL_OPTIONS", global_arguments).replace( + "YNH_COMMANDS_DESCRIPTION", categories + ) + + +def make_category(category_name: str, category_map: dict) -> str: + """ + Generates the main function of a given category. + Reminder: + yunohost monitor info --cpu --ram + ^ ^ ^ ^ + (script) | category | action | parameters + :param str category_name: The name of the category + :param dict category_map: The mapping associated to the category + :return str: The entry point of the given category + """ + # No need to go further if a category does not have an `action` field + if "actions" not in category_map: + return "" + + output = TEMPLATES["command"] + subcategories = "" + + # Memorizing the spaces, to get a human readable file + spaces = re.search(r"\n(\s*)YNH_ACTIONS_DESCRIPTION", output).group(1) + + # First, complete the main action map + for action, action_details in category_map["actions"].items(): + # Empty element failsafe + if "action_help" not in action_details and "arguments" not in action_details: + continue + + # Adding the new action to the map of the category + new_action = "'{}:{}'".format(action, _escape(action_details["action_help"])) + output = output.replace( + "YNH_ACTIONS_DESCRIPTION", + "{} \\\n{}YNH_ACTIONS_DESCRIPTION".format(new_action, spaces), + ) + # Generation of this action completion function + output += make_action(action, action_details).replace("COMMAND", category_name) + + # Removing the remaining tag, uneeded now + output = re.sub(r"\n(\s*)YNH_ACTIONS_DESCRIPTION", "", output) + + # + # Going through the subcategories if any + # + if "subcategories" in category_map: + # Getting the template, and saving the spaces + subcategories = TEMPLATES["subcategory"] + spaces = re.search( + r"\n(\s*)YNH_SUBCACTEGORIES_DESCRIPTION", subcategories + ).group(1) + + # Looping through the subcategories + for subcategory, subcategory_details in category_map["subcategories"].items(): + # Append new subcategory + new_subcategory = "'{}:{}'".format( + subcategory, _escape(subcategory_details["subcategory_help"]) + ) + subcategories = subcategories.replace( + "YNH_SUBCACTEGORIES_DESCRIPTION", + "{}\\\n{}YNH_SUBCACTEGORIES_DESCRIPTION".format( + new_subcategory, spaces + ), + ) + # Creation of the subcategory + output += make_category( + category_name + "_" + subcategory, subcategory_details + ) + + # Removing the remaining tag, uneeded now + subcategories = re.sub( + r"\n(\s*)YNH_SUBCACTEGORIES_DESCRIPTION", "", subcategories + ) + + # Adding the subcategories to the final output. `subcategories` may be + # empty, if no subcategory exists for the current command. + return output.replace("YNH_SUBCATEGORY", subcategories).replace( + "COMMAND", category_name + ) + + +def make_action(action_name: str, action_map: dict) -> str: + """ + Generates the completion function for a given action + Reminder: + yunohost monitor info --cpu --ram + ^ ^ ^ ^ + (script) | category | action | parameters + :param str action_name: The name of the action + :param dict action_map: The mapping associated to the action + :return str: The entry point of the given category + """ + # Return immediately if no argument is expected for this action + if "arguments" not in action_map: + return TEMPLATES["action_without_arguments"].replace("ACTION", action_name) + + action = TEMPLATES["action"] + # Memorizing the spaces, to get a human readable file + spaces = re.search(r"\n(\s*)YNH_ACTION", action).group(1) + + # Creation of the arguments list + args = make_argument_list(action_map, spaces) + + # Insertion of the action's name + return action.replace("YNH_ACTION", args).replace("ACTION", action_name) + + +def make_argument_list(action_map: dict, spaces: str = 4 * " ") -> str: + """ + Builds the actions list. + :param dict action_map: The list of possible arguments + :param str spaces: [optional] The amount of spaces used for the indentation + :return str: The arguments list, ready to use + """ + action_list = "YNH_ACTION" + # This is a counter, in case of position dependent paremeters (the ones not + # beginning with a `-`) + position = 0 + + # Early return if no arguments key found + if "arguments" not in action_map: + return "" + + for argument_name, argument_details in action_map["arguments"].items(): + # + # Initializing the argument dict to make sure all fields are defined + # - id: identifier (`-n` or `--name`). If none (e.g. `ynh app install + # APP_NAME`), this field is the arguments position or cardinality (from + # `nargs`) + # - excludes: usually the argument itself. Only used for optional args + # - desc: the argument description + # - completion: the completion function name + # + arg = {"excludes": "", "spec": "", "desc": "", "mess": "", "action": ""} + + # + # Forcing to str, as the yaml parser inteprets numbers as integers + # (eg.: `firewall allow... -4`) + argument_name = str(argument_name) + + # + # Generation of the completion hints + # + arg["action"] = make_argument_completion(argument_details) + + # + # A parameter not beginning with `-` is considered mandatory. + if argument_name[0] != "-": + position += 1 + # This parameter may be used more than once, else we use the position counter + if argument_details.get("nargs", "") in "+*": + if argument_details["nargs"] == "+": + arg["spec"] = "'{{{},*}}'".format(str(position)) + else: # argument_details["nargs"] == "*": + arg["spec"] = "*" + else: + arg["spec"] = str(position) + arg["mess"] = argument_details.get("help", argument_name) + + # + # If defined, add the default value as a hint + if "default" in argument_details: + arg["mess"] += " (default: {})".format(argument_details["default"]) + # Escape special character in the description + arg["mess"] = _escape(arg["mess"]) + + # ---- + # NOTE: a double colon marks for an optional argument: + # '::Username to update:__ynh_user_list' + # ---- + placeholder = "'{}{}{}:{}:{}'" + + # + # This is an optional parameter, beginning with a `-` + else: + # `full` is the extended form of the argument (e.g.: -n is short for --number) + if "full" in argument_details: + full_name = argument_details["full"] + arg["mess"] = str(full_name).lstrip("--") + arg["spec"] = "'{{{},{}}}'".format(argument_name, full_name) + arg["excludes"] = "({} {})".format(argument_name, full_name) + else: + arg["mess"] = str(argument_name).lstrip("--") + arg["spec"] = argument_name + # Escape special character in the description + arg["mess"] = _escape(arg["mess"]) + + # The description of the parameter + # Getting the `help` field if any, else simply by using it's name + arg["desc"] = "[{}]".format(argument_details.get("help", arg["mess"])) + + has_action = True + # Add a pattern field to match multiple arguments + if argument_details.get("nargs", "") in "+*": + if arg["excludes"]: + # suppose that `arg["excludes"] = (-f --foo)` + arg["excludes"] = "(* " + arg["excludes"][1:] + else: + arg["excludes"] = "(*)" + arg["mess"] = "*:" + arg["mess"] + has_action = True + + # Options without arguments should skip the message and action fields + elif argument_details.get("action", "").startswith("store_"): + has_action = False + + # Place holder for the parameters + if has_action: + placeholder = "'{}{}{}:{}:{}'" + else: + placeholder = "'{}{}{}'" + + # + # Putting it all together + # Escape special character in the description + arg["desc"] = _escape(arg["desc"]) + action_list = action_list.replace( + "YNH_ACTION", + "{} \\\n{}YNH_ACTION".format(placeholder, spaces).format( + arg["excludes"], arg["spec"], arg["desc"], arg["mess"], arg["action"] + ), + ) + # Removing the extra tag and backslash, + return re.sub(r"\s*\\\n(\s*)YNH_ACTION", "", action_list) + + +def make_argument_completion(argument_details: dict) -> str: + """ + Finds the completion function for the given argument, if defined. + :param dict argument_details: The mapping of the argument + :return str: The name of the completion function, or an empty string + """ + # `COMPLETION_FUNCTIONS` hold the elements needed to generate it. The + # actual creation of this function will be done by build_completion_functions(), + # called near the end of this script. + # `choices` and `autocomplete` should not be present at the same time (`choices` takes precedence) + + # + # A list of choices is defined + if "choices" in argument_details: + return "({})".format(" ".join(argument_details["choices"])) + + # + # An autocompletion function is defined + if "extra" in argument_details and "autocomplete" in argument_details["extra"]: + autocomplete = argument_details["extra"]["autocomplete"] + + # + # This is a combinaision of YunoHost and jq commands + # + if "ynh_selector" in autocomplete and "jq_selector" in autocomplete: + # Create this function's name + function_name = _remove_special_chars( + "__ynh_" + _norm_name(autocomplete["ynh_selector"]) + ) + # + # Add it to the function's dict, if it has not been created yet + if function_name not in COMPLETION_FUNCTIONS: + # First, build the shell command that returns the completions + call = "sudo yunohost {} --output-as json | jq -cr '{}'".format( + autocomplete["ynh_selector"], autocomplete["jq_selector"] + ) + # If a cache is needed, wrap the call in the caching function + if "use_cache" in autocomplete: + call = "__get_ynh_cache 'YNH_{}' \"{}\"".format( + _norm_name(autocomplete["ynh_selector"]), call + ) + # Lastly, save the content + COMPLETION_FUNCTIONS[function_name] = {"shell_call": call} + return function_name + + # + # The autocompletion is done by a grep + # + elif "shell_call" in autocomplete: + # Create this function's name + function_name = _remove_special_chars( + "__ynh_" + _norm_name(autocomplete["shell_call"]) + ) + # + # Add it to the function's dict, if it has not been created yet + if function_name not in COMPLETION_FUNCTIONS: + # First, build the shell command that returns the completions + call = autocomplete["shell_call"] + + # If a cache is needed, wrap the call in the caching function + # Note: not tested with grep, only with YunoHost's commands + if "use_cache" in autocomplete: + call = "__get_ynh_cache 'YNH_{}' \"{}\"".format( + _remove_special_chars(autocomplete["shell_call"]), call + ) + # Lastly, save the content + COMPLETION_FUNCTIONS[function_name] = {"shell_call": call} + return function_name + + # + # This is a combinaision of two other completion functions + # + elif "aggregate" in autocomplete: + # Create this function's name + function = "__ynh" + for subcall in autocomplete["aggregate"]: + function += "_" + _norm_name(subcall["ynh_selector"]) + + # If this function is yet undefined, create it + if function not in COMPLETION_FUNCTIONS: + aggregation = "" + for subcall in autocomplete["aggregate"]: + aggregation += "\n'{}:{}:{}' \\".format( + subcall["name"], + subcall["name"], + _norm_name("__ynh_" + subcall["ynh_selector"]), + ) + # Saving the result, and removing the extra backslash + COMPLETION_FUNCTIONS[function] = {"aggregated": aggregation} + return function + + # + # The autocompletion is done by a ZSH function + # + elif "zsh_completion" in autocomplete: + return autocomplete["zsh_completion"] + + return "" + + +def build_completion_functions(functions: dict) -> str: + """ + Generates the custom completion functions for YunoHost, from the "metavar" + object of the YAML mapping. + :param dict metavars: The "metavar" object from the YAML + :return str: All custom completion functions, beginning by "__ynh_" + """ + output = "" + + # + # This basically consists in associating the type of the completion + # function to it's template + for function, completion in functions.items(): + if "aggregated" in completion: + output += ( + TEMPLATES["completion_function_aggregate"] + .replace("FUNCTION", function) + .replace("AGGREGATED", completion["aggregated"]) + ) + else: + output += ( + TEMPLATES["completion_shell_call"] + .replace("FUNCTION", function) + .replace("SHELL_CALL", completion["shell_call"]) + ) + + return output + + +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# +# Utility functions, mainly string manipulation +# + + +def _norm_name(string: str) -> str: + """ + Normalizies a string to make it look like a function name: + - lowercase + - spaces replaced by underscores + - no dashs + :param str string: the string to norm. + :return str: The normed string + """ + return ( + string.lower() + .replace(" ", "_") + .replace("-", "") + .replace("/", "_") + .replace(".", "_") + ) + + +def _escape(string: str) -> str: + r""" + Escapes any special character: + - single quotes (') are put in a separate double quoted string ('"'"') + - colons (:) and other characters are preceded by a backslash (\:) + :param str string: The string to escape + :return str: `string` escaped + """ + return string.replace("'", "'\"'\"'").replace(":", r"\:") + + +def _remove_special_chars(string: str) -> str: + """ + Removes any character with a special meaning in ZSH, such as `$`, `{` ... + :param str string: The string to clean + :return str: `string` cleaned + """ + # NOTE: this list may not be comprehensive and should be extended if needed + return re.sub(r'[- =\^+:\?\'"$(){}\[\]/\\\\]', "", string).replace(".", "") + + +# ----------------------------------------------------------------------------- +# ----------------------------------------------------------------------------- +# +# The ZSH templates are defined below +# + +GLOBAL_ARGUMENTS = [ + r"""'(-h --help)'{-h,--help}'[Show help message and exit]'""", + r"""'--output-as[Output result in another format]:output:(json plain none)'""", + r"""'--debug[Log and print debug messages]'""", + r"""'--quiet[Don't produce any output]'""", + r"""'--version[Display YunoHost packages versions (alias to `yunohost tools versions`)]'""", + r"""'--timeout[Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock]'""", +] + +CONST_HEADER = r"""#compdef yunohost +# ----------------------------------------------------------------------------- +# Description +# ----------- +# Completion script for yunohost, automatically generated from the action map +# decribed by `yunohost.yml` +# ----------------------------------------------------------------------------- + +local state line curcontext + +# For debug purposes only +__log() { + echo $@ >> '/tmp/zsh-completion.log' +} + +# First argument: The name of the completion list +# 2nd argument: The command to get it +# (( $+functions[__get_ynh_cache] )) || +function __get_ynh_cache() { + # Checking a global cache policy is defined, + # and linkage to ynh-cache-policy + local update_policy completion_items + zstyle -s ":completion:${curcontext}:" cache-policy update_policy + if [[ -z "$update_policy" ]]; then + zstyle ":completion:${curcontext}:" cache-policy __yunohost_cache_policy + fi + # If the cache is invalid (too old), regenerate it + if _cache_invalid $1 || ! _retrieve_cache $1; then + completion_items=(`eval $2`) + _store_cache $1 completion_items + else + _retrieve_cache $1 + fi + echo $completion_items +} +# (( $+functions[__yunohost_cache_policy] )) || +__yunohost_cache_policy(){ + local cache_file="$1" + # Rebuild if the yunohost executable is newer than cache + [[ "${commands[yunohost]}" -nt "${cache_file}" ]] && return + + # Rebuild if cache is more than a week old + local -a oldp + # oldp=( "$1"(mM+1) ) # month + # oldp=( "$1"(Nm+7) ) # 1 week + oldp=( "$1"(Nmm+1) ) # 1 min + (( $#oldp )) && return + return 1 +} + + +# +# Routing function, used to go through $words and find the correct subfunction +# (Suggestions welcome to improve that design... =/ ) +# (( $+functions[__jump] )) || +function __jump() { + local cmd + + # Remember the subcommand name + if (( ${#@} == 0 )); then + local cmd=${words[2]} + else + cmd=$1 # < no more used? + fi + + # Set the context for the subcommand + ynhcommand="${ynhcommand}_${cmd}" + # Narrow the range of words we are looking at to exclude `yunohost` + (( CURRENT-- )) + shift words + # Run the completion for the subcommand + if ! _call_function ret ${ynhcommand#:*:}; then + _default && ret=0 + fi + return ret +} + +""" + +# --------------- Main entry point ----------------------------------- +ENTRY_POINT = r""" +# (( $+functions[_yunohost] )) || +function _yunohost() { + local curcontext="${curcontext}" state line ret=1 + local mode + # `ynhcommand` is where `__jump` builds the name of the completion function + ynhcommand='_yunohost' + + typeset -ag common_options; common_options=( + YNH_GLOBAL_OPTIONS + ) + + if (( CURRENT > 2 )); then + __jump + else + local -a yunohost_categories; yunohost_categories=( + YNH_COMMANDS_DESCRIPTION + ) + _describe -V -t yunohost-commands 'yunohost category' yunohost_categories "$@" + fi + + _arguments -s -C $common_options + # unset common_option +} +""" + + +CONST_END_OF_FILE = r""" +_yunohost "$@" +""" + + +TEMPLATES = { + "command": r""" + +#----------------------------------------- +# COMMAND +#----------------------------------------- + +# (( $+functions[_yunohost_COMMAND] )) || +function _yunohost_COMMAND() { + if (( CURRENT > 2 )); then + __jump + else + local -a yunohost_COMMAND; yunohost_COMMAND=( + YNH_ACTIONS_DESCRIPTION + ) + _describe -V -t yunohost-COMMAND 'yunohost COMMAND category' yunohost_COMMAND "$@" + YNH_SUBCATEGORY + fi +} +''', + + # -------------------------------------------------------------------- + 'subcategory': + r''' + local -a yunohost_COMMAND_subcategories; yunohost_COMMAND_subcategories=( + YNH_SUBCACTEGORIES_DESCRIPTION + ) + _describe -V -t yunohost-COMMAND-subcategories 'yunohost COMMAND subcategories' yunohost_COMMAND_subcategories "$@" +""", + # -------------------------------------------------------------------- + # Note: The common_options are not added until we find a way to have them + # in a separate category. It is too confusing to have them mixed with + # the command's options. + # Memo: + # YNH_ACTION \ + # $common_options + # + "action": r""" +# (( $+functions[_yunohost_COMMAND_ACTION] )) || +function _yunohost_COMMAND_ACTION() { + _arguments -s -C \ + YNH_ACTION +} + +""", + # -------------------------------------------------------------------- + "action_without_arguments": r""" +# (( $+functions[_yunohost_COMMAND_ACTION] )) || +function _yunohost_COMMAND_ACTION() { } + +""", + # -------------------------------------------------------------------- + "completion_shell_call": r""" +# (( $+functions[FUNCTION] )) || +function FUNCTION() { + compadd "$@" -- ${(@)$(SHELL_CALL)} +} +""", + # -------------------------------------------------------------------- + # The newline is included in `AGGREGATED` + "completion_function_aggregate": r""" +# (( $+functions[FUNCTION] )) || +function FUNCTION() { + _alternative \AGGREGATED +} +""", +} + + +if __name__ == "__main__": + main() diff --git a/share/actionsmap.yml b/share/actionsmap.yml index e44a72125..e52a0f1a6 100644 --- a/share/actionsmap.yml +++ b/share/actionsmap.yml @@ -37,6 +37,32 @@ _global: authentication: api: ldap_admin cli: null + arguments: + -h: + full: --help + help: Show this help message and exit + --version: + help: Display YunoHost packages versions + action: callback + callback: + method: yunohost.utils.packages.ynh_packages_version + return: true + --output-as: + help: Output result in another format + choices: + - json + - plain + - none + --debug: + help: Log and print debug messages + action: store_true + --quiet: + help: Don't produce any output + action: store_true + --timeout: + help: Number of seconds before this command will timeout because it can't acquire the lock (meaning that another command is currently running), by default there is no timeout and the command will wait until it can get the lock + type: int + ############################# # User # @@ -53,6 +79,16 @@ user: --fields: help: fields to fetch (username, fullname, mail, mail-alias, mail-forward, mailbox-quota, groups, shell, home-path) nargs: "+" + choices: + - username + - fullname + - mail + - mail-alias + - mail-forward + - mailbox-quota + - groups + - shell + - home-path ### user_create() create: @@ -107,6 +143,10 @@ user: pattern: &pattern_domain - !!str ^([^\W_A-Z]+([-]*[^\W_A-Z]+)*\.)+((xn--)?[^\W_]{2,})$ - "pattern_domain" + autocomplete: &domains_list + ynh_selector: domain list + jq_selector: '.domains[]' + use_cache: false -q: full: --mailbox-quota help: Mailbox size quota @@ -120,7 +160,7 @@ user: full: --loginShell help: The login shell used default: "/bin/bash" - + ### user_delete() delete: @@ -131,6 +171,10 @@ user: help: Username to delete extra: pattern: *pattern_username + autocomplete: &usernames_and_mails_list + ynh_selector: user list + jq_selector: '.users[].username, .users[].mail' + use_cache: false --purge: help: Purge user's home and mail directories action: store_true @@ -142,6 +186,11 @@ user: arguments: username: help: Username to update + extra: + autocomplete: &users_list + ynh_selector: user list --fields username + jq_selector: '.users[].username' + use_cache: false -F: full: --fullname help: The full name of the user. For example 'Camille Dupont' @@ -212,12 +261,14 @@ user: arguments: username: help: Username or email to get information - + extra: + autocomplete: *usernames_and_mails_list + ### user_export() export: action_help: Export users into CSV api: GET /users/export - + ### user_import() import: action_help: Import several users from CSV @@ -226,6 +277,9 @@ user: csvfile: help: "CSV file with columns username, firstname, lastname, password, mail, mailbox-quota, mail-alias, mail-forward, groups (separated by coma)" type: open + extra: + autocomplete: + zsh_completion: _file -u: full: --update help: Update all existing users contained in the CSV file (by default existing users are ignored) @@ -279,6 +333,10 @@ user: help: Name of the group to be deleted extra: pattern: *pattern_groupname + autocomplete: &user_groups_list + ynh_selector: user group list -s + jq_selector: '.groups[]' + use_cache: false ### user_group_info() info: @@ -288,7 +346,8 @@ user: groupname: help: Name of the group to fetch info about extra: - pattern: *pattern_username + pattern: *pattern_groupname + autocomplete: *user_groups_list ### user_group_add() add: @@ -299,12 +358,14 @@ user: help: Name of the group to add user(s) to extra: pattern: *pattern_groupname + autocomplete: *user_groups_list usernames: help: User(s) to add in the group nargs: "*" metavar: USERNAME extra: pattern: *pattern_username + autocomplete: *users_list ### user_group_remove() remove: @@ -315,12 +376,14 @@ user: help: Name of the group to remove user(s) from extra: pattern: *pattern_groupname + autocomplete: *user_groups_list usernames: help: User(s) to remove from the group nargs: "*" metavar: USERNAME extra: pattern: *pattern_username + autocomplete: *users_list add-mailalias: action_help: Add mail aliases to group @@ -330,6 +393,7 @@ user: help: Name of the group to add user(s) to extra: pattern: *pattern_groupname + autocomplete: *user_groups_list aliases: help: Mail aliases to add nargs: "+" @@ -344,13 +408,13 @@ user: help: Name of the group to add user(s) to extra: pattern: *pattern_groupname + autocomplete: *user_groups_list aliases: help: Mail aliases to remove nargs: "+" metavar: MAIL - permission: subcategory_help: Manage permissions actions: @@ -363,6 +427,11 @@ user: apps: help: Apps to list permission for (all by default) nargs: "*" + extra: + autocomplete: &apps_list + ynh_selector: app list + jq_selector: '.apps[].id' + use_cache: false -s: full: --short help: Only list permission names @@ -379,6 +448,11 @@ user: arguments: permission: help: Name of the permission to fetch info about (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: &permissions_list + ynh_selector: user permission list + jq_selector: '.permissions | keys[]' + use_cache: false ### user_permission_update() update: @@ -387,6 +461,8 @@ user: arguments: permission: help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list -l: full: --label help: Label for this permission. This label will be shown on the SSO and in the admin @@ -394,8 +470,8 @@ user: full: --show_tile help: Define if a tile will be shown in the SSO choices: - - 'True' - - 'False' + - "True" + - "False" ## user_permission_add() add: @@ -404,12 +480,18 @@ user: arguments: permission: help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list names: help: Group or usernames to grant this permission to nargs: "*" metavar: GROUP_OR_USER extra: pattern: *pattern_username + autocomplete: &user_and_groups_list + ynh_selector: user group list + jq_selector: '[(.groups | keys), ([.groups[] | select(.members).members[]] | unique)] | add[]' + use_cache: false ## user_permission_remove() remove: @@ -418,12 +500,15 @@ user: arguments: permission: help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list names: help: Group or usernames to revoke this permission to nargs: "*" metavar: GROUP_OR_USER extra: pattern: *pattern_username + autocomplete: *user_and_groups_list ## user_permission_reset() reset: @@ -432,6 +517,8 @@ user: arguments: permission: help: Permission to manage (e.g. mail or nextcloud or wordpress.editors) (use "yunohost user permission list" and "yunohost user permission -f" to see all the current permissions) + extra: + autocomplete: *permissions_list ssh: subcategory_help: Manage ssh access @@ -446,6 +533,7 @@ user: help: Username of the user extra: pattern: *pattern_username + autocomplete: *users_list ### user_ssh_keys_add() add-key: @@ -456,6 +544,7 @@ user: help: Username of the user extra: pattern: *pattern_username + autocomplete: *users_list key: help: The key to be added -c: @@ -471,6 +560,7 @@ user: help: Username of the user extra: pattern: *pattern_username + autocomplete: *users_list key: help: The key to be removed @@ -505,6 +595,7 @@ domain: help: Domain to check extra: pattern: *pattern_domain + autocomplete: *domains_list ### domain_add() add: @@ -535,6 +626,7 @@ domain: help: Domain to delete extra: pattern: *pattern_domain + autocomplete: *domains_list -r: full: --remove-apps help: Remove apps installed on the domain @@ -564,7 +656,8 @@ domain: help: Target domain extra: pattern: *pattern_domain - + autocomplete: *domains_list + ### domain_maindomain() main-domain: action_help: Check the current main domain, or change it @@ -577,6 +670,7 @@ domain: help: Change the current main domain extra: pattern: *pattern_domain + autocomplete: *domains_list ### certificate_status() cert-status: @@ -586,6 +680,8 @@ domain: domain_list: help: Domains to check nargs: "*" + extra: + autocomplete: *domains_list --full: help: Show more details action: store_true @@ -598,6 +694,8 @@ domain: domain_list: help: Domains for which to install the certificates nargs: "*" + extra: + autocomplete: *domains_list --force: help: Install even if current certificate is not self-signed action: store_true @@ -616,6 +714,8 @@ domain: domain_list: help: Domains for which to renew the certificates nargs: "*" + extra: + autocomplete: *domains_list --force: help: Ignore the validity threshold (30 days) action: store_true @@ -636,9 +736,10 @@ domain: help: The domain for the web path (e.g. your.domain.tld) extra: pattern: *pattern_domain + autocomplete: *domains_list path: help: The path to check (e.g. /coffee) - + ### domain_action_run() action-run: @@ -648,6 +749,8 @@ domain: arguments: domain: help: Domain name + extra: + autocomplete: *domains_list action: help: action id -a: @@ -666,6 +769,7 @@ domain: help: Domain to subscribe to the DynDNS service extra: pattern: *pattern_domain + autocomplete: *domains_list -p: full: --recovery-password nargs: "?" @@ -673,7 +777,7 @@ domain: help: Password used to later recover the domain if needed extra: pattern: *pattern_password - + ### domain_dyndns_unsubscribe() unsubscribe: action_help: Unsubscribe from a DynDNS service @@ -682,6 +786,7 @@ domain: help: Domain to unsubscribe from the DynDNS service extra: pattern: *pattern_domain + autocomplete: *domains_list required: True -p: full: --recovery-password @@ -699,6 +804,7 @@ domain: help: Domain to set recovery password for extra: pattern: *pattern_domain + autocomplete: *domains_list required: True -p: full: --recovery-password @@ -719,6 +825,8 @@ domain: arguments: domain: help: Domain name + extra: + autocomplete: *domains_list key: help: A specific panel, section or a question identifier nargs: '?' @@ -738,8 +846,10 @@ domain: arguments: domain: help: Domain name + extra: + autocomplete: *domains_list key: - help: The question or form key + help: The question or form key nargs: '?' -v: full: --value @@ -754,7 +864,7 @@ domain: ### domain_dns_conf() suggest: action_help: Generate sample DNS configuration for a domain - api: + api: - GET /domains//dns - GET /domains//dns/suggest arguments: @@ -762,7 +872,8 @@ domain: help: Target domain extra: pattern: *pattern_domain - + autocomplete: *domains_list + ### domain_dns_push() push: action_help: Push DNS records to registrar @@ -772,6 +883,7 @@ domain: help: Domain name to push DNS conf for extra: pattern: *pattern_domain + autocomplete: *domains_list -d: full: --dry-run help: Only display what's to be pushed @@ -794,6 +906,9 @@ domain: domain_list: help: Domains to check nargs: "*" + extra: + autocomplete: *domains_list + --full: help: Show more details action: store_true @@ -806,6 +921,8 @@ domain: domain_list: help: Domains for which to install the certificates nargs: "*" + extra: + autocomplete: *domains_list --force: help: Install even if current certificate is not self-signed action: store_true @@ -824,6 +941,8 @@ domain: domain_list: help: Domains for which to renew the certificates nargs: "*" + extra: + autocomplete: *domains_list --force: help: Ignore the validity threshold (30 days) action: store_true @@ -873,6 +992,8 @@ app: arguments: app: help: Name, local path or git URL of the app to fetch the manifest of + extra: + autocomplete: *apps_list -s: full: --with-screenshot help: Also return a base64 screenshot if any (API only) @@ -899,6 +1020,8 @@ app: arguments: app: help: Specific app ID + extra: + autocomplete: *apps_list -f: full: --full help: Display all details, including the app manifest and various other infos @@ -912,6 +1035,8 @@ app: -a: full: --app help: Specific app to map + extra: + autocomplete: *apps_list -r: full: --raw help: Return complete dict @@ -921,6 +1046,7 @@ app: help: Allowed app map for a user extra: pattern: *pattern_username + autocomplete: *users_list ### app_install() install: @@ -929,6 +1055,8 @@ app: arguments: app: help: Name, local path or git URL of the app to install + extra: + autocomplete: *apps_list -l: full: --label help: Custom name for the app @@ -951,6 +1079,8 @@ app: arguments: app: help: App to remove + extra: + autocomplete: *apps_list -p: full: --purge help: Also remove all application data @@ -964,6 +1094,8 @@ app: app: help: App(s) to upgrade (default all) nargs: "*" + extra: + autocomplete: *apps_list -u: full: --url help: Git url to fetch for upgrade @@ -990,6 +1122,8 @@ app: arguments: app: help: Target app instance name + extra: + autocomplete: *apps_list -d: full: --domain help: New app domain on which the application will be moved @@ -997,6 +1131,7 @@ app: ask: ask_new_domain pattern: *pattern_domain required: True + autocomplete: *domains_list -p: full: --path help: New path at which the application will be moved @@ -1011,6 +1146,8 @@ app: arguments: app: help: App ID + extra: + autocomplete: *apps_list key: help: Key to get/set -v: @@ -1029,6 +1166,8 @@ app: arguments: app: help: App ID + extra: + autocomplete: *apps_list ### app_register_url() register-url: @@ -1037,8 +1176,12 @@ app: arguments: app: help: App which will use the web path + extra: + autocomplete: *apps_list domain: help: The domain on which the app should be registered (e.g. your.domain.tld) + extra: + autocomplete: *domains_list path: help: The path to be registered (e.g. /coffee) @@ -1051,9 +1194,13 @@ app: arguments: app: help: App name to put on domain root + extra: + autocomplete: *apps_list -d: full: --domain help: Specific domain to put app on (the app domain by default) + extra: + autocomplete: *domains_list -u: full: --undo help: Undo redirection @@ -1067,8 +1214,13 @@ app: arguments: app: help: App ID to dismiss notification for + extra: + autocomplete: *apps_list name: help: Notification name, either post_install or post_upgrade + choices: + - post_install + - post_upgrade ### app_ssowatconf() ssowatconf: @@ -1081,6 +1233,8 @@ app: arguments: app: help: App ID + extra: + autocomplete: *apps_list new_label: help: New app label @@ -1097,6 +1251,8 @@ app: arguments: app: help: App name + extra: + autocomplete: *apps_list ### app_action_run() run: @@ -1105,6 +1261,8 @@ app: arguments: app: help: App name + extra: + autocomplete: *apps_list action: help: action id -a: @@ -1124,6 +1282,8 @@ app: arguments: app: help: App name + extra: + autocomplete: *apps_list key: help: A specific panel, section or a question identifier nargs: '?' @@ -1143,8 +1303,10 @@ app: arguments: app: help: App name + extra: + autocomplete: *apps_list key: - help: The question or panel key + help: The question or panel key nargs: '?' -v: full: --value @@ -1191,6 +1353,8 @@ backup: --apps: help: List of application names to backup (or all if none given) nargs: "*" + extra: + autocomplete: *apps_list --dry-run: help: "'Simulate' the backup and return the size details per item to backup" action: store_true @@ -1202,12 +1366,19 @@ backup: arguments: name: help: Name of the local backup archive + extra: + autocomplete: &backups_list + ynh_selector: backup list + jq_selector: '.archives[]' + use_cache: false --system: help: List of system parts to restore (or all if none is given) nargs: "*" --apps: help: List of application names to restore (or all if none is given) nargs: "*" + extra: + autocomplete: *apps_list --force: help: Force restauration on an already installed system action: store_true @@ -1233,6 +1404,8 @@ backup: arguments: name: help: Name of the local backup archive + extra: + autocomplete: *backups_list -d: full: --with-details help: Show additional backup information @@ -1250,6 +1423,8 @@ backup: arguments: name: help: Name of the local backup archive + extra: + autocomplete: *backups_list ### backup_delete() delete: @@ -1260,6 +1435,7 @@ backup: help: Name of the archive to delete extra: pattern: *pattern_backup_archive_name + autocomplete: *backups_list ############################# @@ -1286,6 +1462,11 @@ settings: arguments: key: help: Settings key + extra: + autocomplete: &settings_list + ynh_selector: settings list + jq_selector: '. | keys[]' + use_cache: false -f: full: --full help: Display all details (meant to be used by the API) @@ -1301,8 +1482,10 @@ settings: api: PUT /settings/ arguments: key: - help: The question or form key + help: The question or form key nargs: '?' + extra: + autocomplete: *settings_list -v: full: --value help: new value @@ -1322,6 +1505,8 @@ settings: arguments: key: help: Settings key + extra: + autocomplete: *settings_list ############################# # Service # @@ -1343,6 +1528,9 @@ service: full: --log help: Absolute path to log file to display nargs: "+" + extra: + autocomplete: + zsh_completion: _files --test_status: help: Specify a custom bash command to check the status of the service. Note that it only makes sense to specify this if the corresponding systemd service does not return the proper information already. --test_conf: @@ -1363,6 +1551,11 @@ service: arguments: name: help: Service name to remove + extra: + autocomplete: &services_list + ynh_selector: service status + jq_selector: '. | keys[]' + use_cache: false ### service_start() start: @@ -1373,6 +1566,8 @@ service: help: Service name to start nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_stop() stop: @@ -1383,6 +1578,8 @@ service: help: Service name to stop nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_reload() reload: @@ -1392,6 +1589,8 @@ service: help: Service name to reload nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_restart() restart: @@ -1402,6 +1601,8 @@ service: help: Service name to restart nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_reload_or_restart() reload_or_restart: @@ -1411,6 +1612,8 @@ service: help: Service name to reload or restart nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_enable() enable: @@ -1421,6 +1624,8 @@ service: help: Service name to enable nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_disable() disable: @@ -1431,6 +1636,8 @@ service: help: Service name to disable nargs: "+" metavar: NAME + extra: + autocomplete: *services_list ### service_status() status: @@ -1443,6 +1650,8 @@ service: help: Service name to show nargs: "*" metavar: NAME + extra: + autocomplete: *services_list ### service_log() log: @@ -1451,6 +1660,8 @@ service: arguments: name: help: Service name to log + extra: + autocomplete: *services_list -n: full: --number help: Number of lines to display @@ -1596,6 +1807,7 @@ dyndns: help: Full domain to subscribe with ( deprecated, use 'yunohost domain dyndns subscribe' instead ) extra: pattern: *pattern_domain + autocomplete: *domains_list -p: full: --recovery-password nargs: "?" @@ -1603,7 +1815,7 @@ dyndns: help: Password used to later recover the domain if needed extra: pattern: *pattern_password - + ### dyndns_update() update: action_help: Update IP on DynDNS platform @@ -1613,6 +1825,7 @@ dyndns: help: Full domain to update extra: pattern: *pattern_domain + autocomplete: *domains_list -f: full: --force help: Force the update (for debugging only) @@ -1668,6 +1881,7 @@ tools: ask: ask_main_domain pattern: *pattern_domain required: True + autocomplete: *domains_list -u: full: --username help: Username for the first (admin) user. For example 'camille' @@ -1675,6 +1889,7 @@ tools: ask: ask_admin_username pattern: *pattern_username required: True + autocomplete: *users_list -F: full: --fullname help: The full name for the first (admin) user. For example 'Camille Dupont' @@ -1822,6 +2037,11 @@ tools: targets: help: Migrations to run (all pendings by default) nargs: "*" + extra: + autocomplete: &migrations_list + ynh_selector: tools migrations list + jq_selector: '.migrations[].id' + use_cache: false --skip: help: Skip specified migrations (to be used only if you know what you are doing) action: store_true @@ -1853,8 +2073,13 @@ hook: arguments: app: help: App to link with + extra: + autocomplete: *apps_list file: help: Script to add + extra: + autocomplete: + zsh_completion: _files ### hook_remove() remove: @@ -1862,6 +2087,8 @@ hook: arguments: app: help: Scripts related to app will be removed + extra: + autocomplete: *apps_list ### hook_info() info: @@ -1870,6 +2097,25 @@ hook: arguments: action: help: Action name + choices: &hook_action_choices + - post_user_create + - post_user_delete + - post_user_update + - post_app_addaccess + - post_app_removeaccess + - post_domain_add + - post_domain_remove + - post_cert_update + - custom_dns_rules + - post_app_change_url + - post_app_upgrade + - post_app_install + - post_app_remove + - backup + - restore + - backup_method + - post_iptable_rules + - conf_regen name: help: Hook name @@ -1880,6 +2126,7 @@ hook: arguments: action: help: Action name + choices: *hook_action_choices -l: full: --list-by help: Property to list hook by @@ -1900,6 +2147,7 @@ hook: arguments: action: help: Action name + choices: *hook_action_choices -n: full: --hooks help: List of hooks names to execute @@ -1923,6 +2171,10 @@ hook: arguments: path: help: Path of the script to execute + extra: + autocomplete: + zsh_completion: _files + -a: full: --args help: Ordered list of arguments to pass to the script @@ -1973,6 +2225,11 @@ log: arguments: path: help: Log file which to display the content + extra: + autocomplete: &logs_list + ynh_selector: log list + jq_selector: '.operation[].name' + use_cache: false -n: full: --number help: Number of lines to display @@ -1997,6 +2254,8 @@ log: arguments: path: help: Log file to share + extra: + autocomplete: *logs_list ############################# @@ -2017,6 +2276,12 @@ diagnosis: categories: help: Diagnosis categories to display (all by default) nargs: "*" + extra: + autocomplete: &diagnosis_list + ynh_selector: diagnosis list + jq_selector: '.categories[]' + use_cache: false + --full: help: Display additional information action: store_true @@ -2036,6 +2301,8 @@ diagnosis: arguments: category: help: Diagnosis category to fetch results from + extra: + autocomplete: *diagnosis_list item: help: "List of criteria describing the test. Must correspond exactly to the 'meta' infos in 'yunohost diagnosis show'" metavar: CRITERIA @@ -2048,6 +2315,8 @@ diagnosis: categories: help: Diagnosis categories to run (all by default) nargs: "*" + extra: + autocomplete: *diagnosis_list --force: help: Ignore the cached report even if it is still 'fresh' action: store_true