Skip to content

Generate zsh completion script automatically #504

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions ptpython/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .repl import embed
from .entry_points.run_ptpython import create_parser

__all__ = ["embed"]
6 changes: 4 additions & 2 deletions ptpython/entry_points/run_ptpython.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,9 +77,11 @@ def create_parser() -> _Parser:
help="Run on a dark background (use light colors for text).",
),
parser.add_argument(
"--config-file", type=str, help="Location of configuration file."
"--config-file", type=str, metavar="file", help="Location of configuration file."
)
parser.add_argument(
"--history-file", type=str, metavar="file", help="Location of history file."
)
parser.add_argument("--history-file", type=str, help="Location of history file.")
parser.add_argument(
"-V",
"--version",
Expand Down
6 changes: 6 additions & 0 deletions scripts/zsh_completion.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#compdef -P ptpython[0-9.]# ptipython[0-9.]#
# https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide

_arguments -s -S \
{{flags}} \
'*:file:_files'
240 changes: 240 additions & 0 deletions scripts/zsh_completion.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,240 @@
#!/usr/bin/env python
"""Generate zsh completion script.

Usage
-----
.. code-block:: zsh
scripts/zsh_completion.py
sudo mv _[^_]* /usr/share/zsh/site-functions # don't mv __pycache__
rm -f ~/.zcompdump # optional
compinit # regenerate ~/.zcompdump

Debug
-----
.. code-block:: zsh
scripts/zsh_completion.py MODULE_NAME - # will output to stdout

Refer
-----
- https://github.com/ytdl-org/youtube-dl/blob/master/devscripts/zsh-completion.py
- https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide

Examples
--------
.. code-block::
'(- *)'{-h,--help}'[show this help message and exit]'
|<-1->||<---2--->||<---------------3--------------->|

.. code-block:: console
% foo --<TAB>
option
--help show this help message and exit
% foo --help <TAB>
no more arguments

.. code-block::
--color'[When to show color. Default: auto. Support: auto, always, never]:when:(auto always never)'
|<-2->||<------------------------------3------------------------------->||<4>||<--------5-------->|

.. code-block:: console
% foo --color <TAB>
when
always
auto
never

.. code-block::
--color'[When to show color. Default: auto. Support: auto, always, never]:when:((auto\:"only when output is stdout" always\:always never\:never))'
|<-2->||<------------------------------3------------------------------->||<4>||<--------------------------------5------------------------------->|

.. code-block:: console
% foo --color <TAB>
when
always always
auto only when output is stdout
never never

.. code-block::
--config='[Config file. Default: ~/.config/foo/foo.toml]:config file:_files -g toml'
|<--2-->||<---------------------3--------------------->||<---4---->||<-----5------>|

.. code-block:: console
% foo --config <TAB>
config file
a.toml b/
...

.. code-block::
{1,2}'::_command_names -e'
|<2->|4|<-------5------->|

.. code-block:: console
% foo help<TAB>
_command_names -e
help2man generate a simple manual page
helpviewer
...
% foo hello hello <TAB>
no more arguments

.. code-block::
'*: :_command_names -e'
2|4||<-------5------->|

.. code-block:: console
% foo help<TAB>
external command
help2man generate a simple manual page
helpviewer
...
% foo hello hello help<TAB>
external command
help2man generate a simple manual page
helpviewer
...

+----+------------+----------+------+
| id | variable | required | expr |
+====+============+==========+======+
| 1 | prefix | F | (.*) |
| 2 | optionstr | T | .* |
| 3 | helpstr | F | [.*] |
| 4 | metavar | F | :.* |
| 5 | completion | F | :.* |
+----+------------+----------+------+
"""
from argparse import (
FileType,
SUPPRESS,
_HelpAction,
_SubParsersAction,
_VersionAction,
)
import os
from os.path import dirname as dirn
import sys
from typing import Final, Tuple

from setuptools import find_packages

rootpath = dirn(dirn(os.path.abspath(__file__)))
path = os.path.join(rootpath, "src")
packages = find_packages(path)
if packages == []:
path = rootpath
packages = find_packages(path)
sys.path.insert(0, path)
PACKAGES: Final = packages
PACKAGE: Final = PACKAGES[0] if sys.argv[1:2] == [] else sys.argv[1]
parser = __import__(PACKAGE).create_parser()
actions = parser._actions
BINNAME: Final = PACKAGE.replace("_", "-")
BINNAMES: Final = [BINNAME]
ZSH_COMPLETION_FILE: Final = (
"_" + BINNAME if sys.argv[2:3] == [] else sys.argv[2]
)
ZSH_COMPLETION_TEMPLATE: Final = os.path.join(
dirn(os.path.abspath(__file__)), "zsh_completion.in"
)
SUPPRESS_HELP = SUPPRESS
SUPPRESS_USAGE = SUPPRESS

flags = []
position = 1
for action in actions:
if action.__class__ in [_HelpAction, _VersionAction]:
prefix = "'(- *)'"
elif isinstance(action, _SubParsersAction): # TODO
raise NotImplementedError
elif action.option_strings == []:
prefix = "'(-)"
else:
prefix = ""

if len(action.option_strings) > 1: # {} cannot be quoted
optionstr = "{" + ",".join(action.option_strings) + "}'"
elif len(action.option_strings) == 1:
optionstr = action.option_strings[0] + "'"
else: # action.option_strings == [], positional argument
if action.nargs in ["*", "+"]:
optionstr = "1" # change in template file
else:
if isinstance(action.nargs, int) and action.nargs > 1:
old_position = position
position += action.nargs
optionstr = ",".join(map(str, range(old_position, position)))
optionstr = "{" + optionstr + "}'"
else: # action.nargs in [1, None, "?"]:
optionstr = str(position) + "'"
position += 1

if (
action.help
and action.help != SUPPRESS_HELP
and action.option_strings != []
):
helpstr = action.help.replace("]", "\\]").replace("'", "'\\''")
helpstr = "[" + helpstr + "]"
else:
helpstr = ""

if isinstance(action.metavar, str):
metavar = action.metavar
elif isinstance(action.metavar, Tuple):
metavar = " ".join(map(str, action.metavar))
# need some changes in template file
else: # action.metavar is None
if action.nargs == 0:
metavar = ""
elif action.option_strings == []:
metavar = action.dest
elif isinstance(action.type, FileType):
metavar = "file"
elif action.type:
metavar = action.type.__name__
else:
metavar = action.default.__class__.__name__
if metavar != "":
# use lowcase conventionally
metavar = metavar.lower().replace(":", "\\:")

if action.choices:
completion = "(" + " ".join(map(str, action.choices)) + ")"
elif metavar == "file":
completion = "_files"
metavar = " "
elif metavar == "args":
completion = "_files -g *.py"
elif metavar == "dir":
completion = "_dirs"
metavar = " "
elif metavar == "url":
completion = "_urls"
metavar = " "
elif metavar == "command":
completion = "_command_names -e"
metavar = " "
else:
completion = ""

if metavar != "":
metavar = ":" + metavar
if completion != "":
completion = ":" + completion

flag = "{0}{1}{2}{3}{4}'".format(
prefix, optionstr, helpstr, metavar, completion
)
flags += [flag]

with open(ZSH_COMPLETION_TEMPLATE) as f:
template = f.read()

template = template.replace("{{flags}}", " \\\n ".join(flags))

with (
open(ZSH_COMPLETION_FILE, "w")
if ZSH_COMPLETION_FILE != "-"
else sys.stdout
) as f:
f.write(template)