🍽 Fork YunoHost — snapshot mangé par la machine à tsoins
Upstream: https://github.com/YunoHost/yunohost @ 3a5f8bac8301c450897b96cbd43a4c7d3ba750fb But (José) : transformer tout le code en bions + ploxions du xerboxion. La carte de digestion vit au labo : /yunohost-digest.json
This commit is contained in:
738
doc/generate_zsh_completion.py
Executable file
738
doc/generate_zsh_completion.py
Executable file
@@ -0,0 +1,738 @@
|
||||
#!/usr/bin/env python3
|
||||
#
|
||||
# Copyright (c) 2024 YunoHost Contributors
|
||||
#
|
||||
# This file is part of YunoHost (see https://yunohost.org)
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
|
||||
"""Automated generation of a zsh_completion file for yunohost.
|
||||
|
||||
Using the actionsmap yaml file and a jinja template.
|
||||
|
||||
INSTALL:
|
||||
This script creates a zsh completion file for yunohost.
|
||||
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
|
||||
|
||||
MISSING:
|
||||
- use the extra:required:True pattern (similar to `nargs`?)
|
||||
- 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
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, cast
|
||||
|
||||
import yaml
|
||||
from jinja2 import Template
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from typing import NotRequired, TypedDict
|
||||
|
||||
class Function(TypedDict):
|
||||
"""Details of a helper function."""
|
||||
|
||||
name: str
|
||||
shell_call: NotRequired[str]
|
||||
aggregated: NotRequired[str]
|
||||
|
||||
class Case(TypedDict):
|
||||
"""Details of dynamic argument completion function."""
|
||||
|
||||
name: str
|
||||
shell_call: str
|
||||
|
||||
class Action(TypedDict):
|
||||
"""Command line action."""
|
||||
|
||||
name: str
|
||||
help: str
|
||||
arguments: list[str]
|
||||
cases: list[Case]
|
||||
|
||||
class Category(TypedDict):
|
||||
"""Command line category.
|
||||
|
||||
Categories have different level:
|
||||
- level 1: main category, e.g. `yunohost user`
|
||||
- level 2: sub-category, e.g. `yunohost user group`
|
||||
|
||||
Only categories of level 1 have a `subs` key.
|
||||
|
||||
Reminder:
|
||||
yunohost user group list --full --short
|
||||
^ ^ ^ ^
|
||||
(script) | category | subcategory | action | parameters
|
||||
|
||||
"""
|
||||
|
||||
name: str
|
||||
help: str
|
||||
level: int
|
||||
actions: list[Action]
|
||||
subs: NotRequired[dict[str, str]]
|
||||
|
||||
|
||||
YUNOHOST_SRCDIR = Path(__file__).resolve().parent.parent
|
||||
|
||||
|
||||
def get_actions_zsh(
|
||||
ynh_map: dict[str, dict[str, Any]],
|
||||
) -> tuple[list[Category], list[Function]]:
|
||||
"""Parse categories, subcategories and actions from an actionsmap yml file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ynh_map: dict[str, dict[str, Any]]
|
||||
A dict loaded from an actionsmap yml file.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[list[Category], list[Function]]
|
||||
A tuple of categories dict and helper functions dict.
|
||||
|
||||
"""
|
||||
categories: list[Category] = []
|
||||
functions: list[Function] = []
|
||||
|
||||
for category, cat_info in ynh_map.items():
|
||||
if category.startswith("_") or cat_info.get("hide_in_help", False):
|
||||
continue
|
||||
|
||||
cats, funcs = parse_category(category, cat_info)
|
||||
|
||||
categories.extend(cats)
|
||||
functions.extend(funcs)
|
||||
|
||||
# Remove duplicates in functions
|
||||
functions = [
|
||||
cast("Function", dict(t)) for t in {tuple(d.items()) for d in functions}
|
||||
]
|
||||
|
||||
return categories, functions
|
||||
|
||||
|
||||
def parse_category(
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
) -> tuple[list[Category], list[Function]]:
|
||||
"""Parse a Category (level 1) for its actions and subcategories.
|
||||
|
||||
Reminder:
|
||||
yunohost monitor info --cpu --ram
|
||||
^ ^ ^ ^
|
||||
(script) | category | action | parameters
|
||||
|
||||
A Category may contain subcategories (of level 2), so a list of categories
|
||||
(of level 1 and 2) is returned.
|
||||
A category may need to define help functions that are needed to build
|
||||
the list of options. The list of help functions is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
name: str
|
||||
the category name
|
||||
info: dict[str, Any]
|
||||
the information dict about the category
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[list[Category], list[Function]]
|
||||
A tuple of the list of category and subcategories dicts
|
||||
and the list of category and subcategories helper functions.
|
||||
|
||||
"""
|
||||
cat: Category = {
|
||||
"name": name,
|
||||
"level": 1,
|
||||
"help": _escape(info.get("category_help", "")),
|
||||
"actions": [],
|
||||
"subs": {},
|
||||
}
|
||||
# Add the category first, the subcategories will be appended later
|
||||
categories: list[Category] = [cat]
|
||||
functions: list[Function] = []
|
||||
|
||||
# Parse actions (before subcategories)
|
||||
actions = []
|
||||
for action, action_info in info.get("actions", {}).items():
|
||||
if action_info.get("hide_in_help", False):
|
||||
continue
|
||||
|
||||
act, funcs = parse_actions(action, action_info)
|
||||
actions.append(act)
|
||||
functions.extend(funcs)
|
||||
cat["actions"] = actions
|
||||
|
||||
# Parse subcategories
|
||||
subs = {}
|
||||
for subcategory, subcategory_info in info.get("subcategories", {}).items():
|
||||
if subcategory.startswith("_") or subcategory_info.get("hide_in_help", False):
|
||||
continue
|
||||
|
||||
help, subcategory_dict, funcs = parse_subcategory( # noqa: A001
|
||||
name,
|
||||
subcategory,
|
||||
subcategory_info,
|
||||
)
|
||||
subs[subcategory] = help
|
||||
functions.extend(funcs)
|
||||
# Append subcategory below the category
|
||||
categories.append(subcategory_dict)
|
||||
# Add the list of subcategories to the category
|
||||
cat["subs"] = subs
|
||||
|
||||
return categories, functions
|
||||
|
||||
|
||||
def parse_subcategory(
|
||||
category: str,
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
) -> tuple[str, Category, list[Function]]:
|
||||
"""Parse a sub-category (level 2) for its actions.
|
||||
|
||||
Reminder:
|
||||
yunohost user group list --full --short
|
||||
^ ^ ^ ^
|
||||
(script) | category | subcategory | action | parameters
|
||||
|
||||
A subcategory is treated as a Category (of level 2), with an 'actions' key,
|
||||
but no 'subs' key.
|
||||
The help text of the subcategory is needed to construct the 'subs' dict
|
||||
of the parent category.
|
||||
Like a level-1 category, subcategories may need to define help functions.
|
||||
The list of help functions is returned.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
category: str
|
||||
the name of the parent category
|
||||
name: str
|
||||
the subcategory name
|
||||
info: dict[str, Any]
|
||||
the information dict about the subcategory
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[str, Category, list[Function]]
|
||||
A tuple of the subcategory help text, the subcategory dict
|
||||
and the list of subcategory helper functions.
|
||||
|
||||
"""
|
||||
full_name = f"{category}_{name}"
|
||||
help = _escape(info.get("subcategory_help", "")) # noqa: A001
|
||||
|
||||
subcat: Category = {"name": full_name, "level": 2, "help": help, "actions": []}
|
||||
functions: list[Function] = []
|
||||
|
||||
# Parse actions (before subcategories)
|
||||
actions = []
|
||||
for action, action_info in info.get("actions", {}).items():
|
||||
if action_info.get("hide_in_help", False):
|
||||
continue
|
||||
|
||||
act, funcs = parse_actions(action, action_info)
|
||||
actions.append(act)
|
||||
functions.extend(funcs)
|
||||
subcat["actions"] = actions
|
||||
|
||||
return help, subcat, functions
|
||||
|
||||
|
||||
def parse_actions(
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
) -> tuple[Action, list[Function]]:
|
||||
"""Parse an Action for it's help text and arguments.
|
||||
|
||||
Returns
|
||||
-------
|
||||
tuple[Action, list[Function]]
|
||||
A tuple of the action dict and the list of action helper functions.
|
||||
|
||||
"""
|
||||
functions: list[Function] = []
|
||||
|
||||
# This is a counter, in case of position dependent paremeters (the ones not
|
||||
# beginning with a `-`)
|
||||
position = 0
|
||||
|
||||
arguments: list[str] = []
|
||||
cases: list[Case] = []
|
||||
|
||||
for _argument_name, argument_info in info.get("arguments", {}).items():
|
||||
#
|
||||
# Forcing to str, as the yaml parser inteprets numbers as integers
|
||||
# (eg.: `firewall allow... -4`)
|
||||
argument_name = str(_argument_name)
|
||||
case: Case | None = None
|
||||
funcs: list[Function] = []
|
||||
|
||||
#
|
||||
# This is an optional parameter, beginning with a `-`
|
||||
if argument_name.startswith("-"):
|
||||
full_argument, case, funcs = parse_argument_optional(
|
||||
argument_name,
|
||||
argument_info,
|
||||
)
|
||||
#
|
||||
# A parameter not beginning with `-` is considered mandatory.
|
||||
else:
|
||||
position += 1
|
||||
full_argument, case, funcs = parse_argument_mandatory(
|
||||
argument_name,
|
||||
argument_info,
|
||||
position,
|
||||
)
|
||||
|
||||
# If action is None, do not display the parameter
|
||||
if not full_argument:
|
||||
continue
|
||||
|
||||
# If case is not None, add a case below the arguments list
|
||||
if case:
|
||||
cases.append(case)
|
||||
|
||||
# Add helper functions
|
||||
functions.extend(funcs)
|
||||
|
||||
# Append argument
|
||||
arguments.append(full_argument)
|
||||
|
||||
help = _escape(info.get("action_help", "")) # noqa: A001
|
||||
action_dict: Action = {
|
||||
"name": name,
|
||||
"help": help,
|
||||
"arguments": arguments,
|
||||
"cases": cases,
|
||||
}
|
||||
return action_dict, functions
|
||||
|
||||
|
||||
def parse_argument_mandatory(
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
position: int = 0,
|
||||
) -> tuple[str, Case | None, list[Function]]:
|
||||
"""Parse a mandatory argument."""
|
||||
#
|
||||
# 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": "", "func": ""}
|
||||
|
||||
#
|
||||
# Generation of the completion hints
|
||||
#
|
||||
arg["action"], case, functions = parse_argument_action(name, info)
|
||||
|
||||
# Hidden argument
|
||||
if arg["action"] is None:
|
||||
return ("", None, [])
|
||||
|
||||
# This parameter may be used more than once, else we use the position counter
|
||||
if info.get("nargs", "") in ["+", "*"]:
|
||||
if info["nargs"] == "+":
|
||||
arg["spec"] = f"'{{{position!s},*}}'"
|
||||
else: # argument_details["nargs"] == "*":
|
||||
arg["spec"] = "*"
|
||||
else:
|
||||
arg["spec"] = str(position)
|
||||
arg["mess"] = info.get("help", name)
|
||||
|
||||
#
|
||||
# If defined, add the default value as a hint
|
||||
if "default" in info:
|
||||
arg["mess"] += f" (default: {info['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 = "'{}{}{}:{}:{}'"
|
||||
|
||||
# Escape special character in the description
|
||||
arg["desc"] = _escape(arg["desc"])
|
||||
argument = placeholder.format(
|
||||
arg["excludes"],
|
||||
arg["spec"],
|
||||
arg["desc"],
|
||||
arg["mess"],
|
||||
arg["action"],
|
||||
)
|
||||
return (argument, case, functions)
|
||||
|
||||
|
||||
def parse_argument_optional(
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
) -> tuple[str, Case | None, list[Function]]:
|
||||
"""Parse an optional argument."""
|
||||
#
|
||||
# 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": "", "func": ""}
|
||||
|
||||
#
|
||||
# Generation of the completion hints
|
||||
#
|
||||
arg["action"], case, functions = parse_argument_action(name, info)
|
||||
|
||||
# Hidden argument
|
||||
if arg["action"] is None:
|
||||
return ("", None, [])
|
||||
|
||||
# `full` is the extended form of the argument (e.g.: -n is short for --number)
|
||||
if "full" in info:
|
||||
full_name = info["full"]
|
||||
arg["mess"] = str(full_name).lstrip("-")
|
||||
arg["spec"] = f"'{{{name},{full_name}}}'"
|
||||
arg["excludes"] = f"({name} {full_name})"
|
||||
else:
|
||||
arg["mess"] = str(name).lstrip("-")
|
||||
arg["spec"] = 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
|
||||
help = info.get("help", arg["mess"]) # noqa: A001
|
||||
arg["desc"] = f"[{help}]"
|
||||
|
||||
has_action = True
|
||||
# Add a pattern field to match multiple arguments
|
||||
if info.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 info.get("action", "").startswith("store_"):
|
||||
has_action = False
|
||||
|
||||
# Place holder for the parameters
|
||||
placeholder = "'{}{}{}:{}:{}'" if has_action else "'{}{}{}'"
|
||||
# Escape special character in the description
|
||||
arg["desc"] = _escape(arg["desc"])
|
||||
argument = placeholder.format(
|
||||
arg["excludes"],
|
||||
arg["spec"],
|
||||
arg["desc"],
|
||||
arg["mess"],
|
||||
arg["action"],
|
||||
)
|
||||
return (argument, case, functions)
|
||||
|
||||
|
||||
def parse_argument_action( # noqa: C901, PLR0911, PLR0912
|
||||
name: str,
|
||||
info: dict[str, Any],
|
||||
) -> tuple[str, Case | None, list[Function]]:
|
||||
"""Parse an argument action."""
|
||||
functions: list[Function] = []
|
||||
#
|
||||
# Finds the completion function for the given argument, if defined.
|
||||
#
|
||||
# `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 info:
|
||||
all_choices = " ".join(info["choices"])
|
||||
action = f"({all_choices})"
|
||||
return (action, None, functions)
|
||||
|
||||
#
|
||||
# Look for an autocompletion function, but it is not defined
|
||||
if "extra" not in info or "autocomplete" not in info["extra"]:
|
||||
return ("", None, functions)
|
||||
|
||||
#
|
||||
# An autocompletion function is defined
|
||||
autocomplete = info["extra"]["autocomplete"]
|
||||
|
||||
#
|
||||
# Check if the argument should be hidden (API only)
|
||||
if autocomplete.get("hide_in_help", False):
|
||||
return ("", None, functions)
|
||||
|
||||
#
|
||||
# This is a combinaision of YunoHost and jq commands
|
||||
#
|
||||
if "ynh_selector" in autocomplete and "jq_selector" in autocomplete:
|
||||
#
|
||||
# Function dependent on previous arguments
|
||||
#
|
||||
if "depends" in autocomplete and autocomplete["depends"] == "previous":
|
||||
# Create cases that depend on the previous argument.
|
||||
#
|
||||
# First, build the shell command that returns the completions
|
||||
call = (
|
||||
f"sudo yunohost {autocomplete['ynh_selector']} "
|
||||
f'"${{previous}}" --output-as json '
|
||||
f"| jq -cr '{autocomplete['jq_selector']}' | xargs"
|
||||
)
|
||||
# If a cache is needed, wrap the call in the caching function
|
||||
if autocomplete.get("use_cache", False):
|
||||
call = '__get_ynh_cache YNH_{}_"${{previous}}" "{}"'.format(
|
||||
_norm_name(autocomplete["ynh_selector"]),
|
||||
# Remove the double-quote in "{previous}"
|
||||
# because the whole cmd will be encased in quotes.
|
||||
call.replace('"', ""),
|
||||
)
|
||||
|
||||
function_name = f"->{name}"
|
||||
case: Case = {"name": name, "shell_call": call}
|
||||
return (function_name, case, functions)
|
||||
|
||||
# Create this function's name
|
||||
function_name = _remove_special_chars(
|
||||
"__ynh_" + _norm_name(autocomplete["ynh_selector"]),
|
||||
)
|
||||
#
|
||||
# Add a helper function
|
||||
#
|
||||
# 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 autocomplete.get("use_cache", False):
|
||||
call = "__get_ynh_cache 'YNH_{}' \"{}\"".format(
|
||||
_norm_name(autocomplete["ynh_selector"]),
|
||||
call,
|
||||
)
|
||||
# Lastly, save the content
|
||||
func: Function = {"name": function_name, "shell_call": call}
|
||||
functions.append(func)
|
||||
return (function_name, None, functions)
|
||||
|
||||
#
|
||||
# The autocompletion is done by a grep
|
||||
#
|
||||
if "shell_call" in autocomplete:
|
||||
# Create this function's name
|
||||
function_name = _remove_special_chars(
|
||||
"__ynh_" + _norm_name(autocomplete["shell_call"]),
|
||||
)
|
||||
#
|
||||
# Add a helper function
|
||||
#
|
||||
# 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 autocomplete.get("use_cache", False):
|
||||
call = "__get_ynh_cache 'YNH_{}' \"{}\"".format(
|
||||
_remove_special_chars(autocomplete["shell_call"]),
|
||||
call,
|
||||
)
|
||||
# Lastly, save the content
|
||||
func = {"name": function_name, "shell_call": call}
|
||||
functions.append(func)
|
||||
return (function_name, None, functions)
|
||||
|
||||
#
|
||||
# This is a combinaision of two other completion functions
|
||||
#
|
||||
if "aggregate" in autocomplete:
|
||||
# Create this function's name
|
||||
function_name = "__ynh"
|
||||
for subcall in autocomplete["aggregate"]:
|
||||
if "ynh_selector" in subcall:
|
||||
function_name += "_" + _norm_name(subcall["ynh_selector"])
|
||||
|
||||
#
|
||||
# Add a helper function
|
||||
aggregation = ""
|
||||
for subcall in autocomplete["aggregate"]:
|
||||
if "name" in subcall and "ynh_selector" in subcall:
|
||||
aggregation += "\n'{}:{}:{}' \\".format(
|
||||
subcall["name"],
|
||||
subcall["name"],
|
||||
_norm_name("__ynh_" + subcall["ynh_selector"]),
|
||||
)
|
||||
# Lastly, save the content
|
||||
func = {"name": function_name, "aggregated": aggregation}
|
||||
functions.append(func)
|
||||
return (function_name, None, functions)
|
||||
|
||||
#
|
||||
# The autocompletion is done by a ZSH function
|
||||
#
|
||||
if "zsh_completion" in autocomplete:
|
||||
return (autocomplete["zsh_completion"], None, functions)
|
||||
|
||||
#
|
||||
# No autocompletion schema was defined
|
||||
#
|
||||
return ("", None, functions)
|
||||
|
||||
|
||||
def render_zsh(categories: list[Category], functions: list[Function]) -> str:
|
||||
"""Render the jinja template with the parsed categories and helper functions."""
|
||||
template_file = YUNOHOST_SRCDIR / "doc" / "zsh_completion.j2"
|
||||
template = Template(
|
||||
template_file.read_text(),
|
||||
keep_trailing_newline=True,
|
||||
comment_start_string="disabled because bash contains {#",
|
||||
)
|
||||
|
||||
return template.render(
|
||||
categories=categories,
|
||||
functions=functions,
|
||||
)
|
||||
|
||||
|
||||
#
|
||||
# Utility functions, mainly string manipulation
|
||||
#
|
||||
|
||||
|
||||
def _norm_name(string: str) -> str:
|
||||
"""Normalize a string to make it look like a function name.
|
||||
|
||||
Apply the transformations:
|
||||
- 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"""Escape any special character.
|
||||
|
||||
Escape the characters:
|
||||
- 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: The escaped string
|
||||
"""
|
||||
return string.replace("'", "'\"'\"'").replace(":", r"\:")
|
||||
|
||||
|
||||
def _remove_special_chars(string: str) -> str:
|
||||
"""Remove any character with a special meaning in ZSH.
|
||||
|
||||
Example of characters to remove:
|
||||
`$`, `{`, `(`, `[`, ...
|
||||
|
||||
:param str string: The string to clean
|
||||
:return str: The cleaned string
|
||||
"""
|
||||
# NOTE: this list may not be comprehensive and should be extended if needed
|
||||
return re.sub(r'[- =\^+:\?\'"$(){}\[\]/\\\\]', "", string).replace(".", "")
|
||||
|
||||
|
||||
#
|
||||
# Get action map
|
||||
#
|
||||
|
||||
|
||||
def get_action_map() -> dict[str, Any]:
|
||||
"""Load the actionmap from a YAML file."""
|
||||
actionsmap = YUNOHOST_SRCDIR / "share" / "actionsmap.yml"
|
||||
return cast("dict[str, Any]", yaml.safe_load(actionsmap.open()))
|
||||
|
||||
|
||||
def main() -> None:
|
||||
"""Generate the completion file for Zsh."""
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--output", "-o", type=Path, required=True)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
yunohost_map = get_action_map()
|
||||
|
||||
categories, functions = get_actions_zsh(yunohost_map)
|
||||
result = render_zsh(categories, functions)
|
||||
|
||||
args.output.write_text(result)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user