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
739 lines
22 KiB
Python
Executable File
739 lines
22 KiB
Python
Executable File
#!/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()
|