From 66c952916d510f7d21dd95fbeede12e0ea221622 Mon Sep 17 00:00:00 2001 From: Wu Zhenyu Date: Thu, 4 Aug 2022 22:42:06 +0800 Subject: [PATCH] Rewrite zsh completion script, fix #30900 1. `devscripts/zsh-completion.py` 2. `sudo mv youtube-dl.zsh /usr/share/zsh/site-functions/_youtube-dl` 3. `compinit` --- devscripts/zsh-completion.in | 42 +++---- devscripts/zsh-completion.py | 216 ++++++++++++++++++++++++++++++----- youtube_dl/options.py | 24 ++-- 3 files changed, 220 insertions(+), 62 deletions(-) diff --git a/devscripts/zsh-completion.in b/devscripts/zsh-completion.in index b394a1ae7..cc47f8234 100644 --- a/devscripts/zsh-completion.in +++ b/devscripts/zsh-completion.in @@ -1,28 +1,22 @@ -#compdef youtube-dl +#compdef {{programs}} +# https://github.com/zsh-users/zsh/blob/master/Etc/completion-style-guide -__youtube_dl() { - local curcontext="$curcontext" fileopts diropts cur prev - typeset -A opt_args - fileopts="{{fileopts}}" - diropts="{{diropts}}" - cur=$words[CURRENT] - case $cur in - :) - _arguments '*: :(::ytfavorites ::ytrecommended ::ytsubscriptions ::ytwatchlater ::ythistory)' +typeset -A opt_args +_arguments -S -s \ + {{flags}} \ + '*:URL:_urls' + +case $state in + external-downloader-args) + case "${opt_args[--external-downloader]}" in + curl) _curl ;; - *) - prev=$words[CURRENT-1] - if [[ ${prev} =~ ${fileopts} ]]; then - _path_files - elif [[ ${prev} =~ ${diropts} ]]; then - _path_files -/ - elif [[ ${prev} == "--recode-video" ]]; then - _arguments '*: :(mp4 flv ogg webm mkv)' - else - _arguments '*: :({{flags}})' - fi + ffmpeg) _ffmpeg + ;; + httpie) _httpie + ;; + wget) _wget ;; esac -} - -__youtube_dl \ No newline at end of file + ;; +esac diff --git a/devscripts/zsh-completion.py b/devscripts/zsh-completion.py index 60aaf76cc..ca17b85f0 100755 --- a/devscripts/zsh-completion.py +++ b/devscripts/zsh-completion.py @@ -1,49 +1,203 @@ #!/usr/bin/env python -from __future__ import unicode_literals +"""Generate zsh completion script. +Usage +----- +.. code-block:: zsh + devscripts/zsh-completion.py + sudo mv youtube-dl.zsh /usr/share/zsh/site-functions/_youtube-dl + rm -f ~/.zcompdump # optional + compinit # regenerate ~/.zcompdump + +Debug +----- +.. code-block:: zsh + devscripts/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 -- + option + --help show this help message and exit + % foo --help + 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 + 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 + 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 + config file + a.toml b/ ... + ... + +.. code-block:: + {1,2}'::_command_names -e' + |<2->|4|<-------5------->| + +.. code-block:: console + % foo help + _command_names -e + help2man generate a simple manual page + helpviewer + ... + % foo hello hello + no more arguments + +.. code-block:: + '*: :_command_names -e' + 2|4||<-------5------->| + +.. code-block:: console + % foo help + external command + help2man generate a simple manual page + helpviewer + ... + % foo hello hello help + 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 __future__ import unicode_literals +from optparse import SUPPRESS_HELP import os from os.path import dirname as dirn import sys +from typing import Final -sys.path.insert(0, dirn(dirn((os.path.abspath(__file__))))) -import youtube_dl +from setuptools import find_packages -ZSH_COMPLETION_FILE = "youtube-dl.zsh" +rootpath = dirn(dirn(os.path.abspath(__file__))) +path = os.path.join(rootpath, "src") +packages = find_packages(path) +if packages == []: + path = rootpath +sys.path.insert(0, path) +PACKAGE: Final = "youtube_dl" if sys.argv[1:2] == [] else sys.argv[1] +parser = __import__(PACKAGE).parseOpts()[0] +BINNAME: Final = PACKAGE.replace("_", "-") +BINNAMES: Final = [BINNAME] +ZSH_COMPLETION_FILE: Final = ( + "youtube-dl.zsh" if sys.argv[2:3] == [] else sys.argv[2] +) ZSH_COMPLETION_TEMPLATE = "devscripts/zsh-completion.in" +opts = parser._get_all_options() -def build_completion(opt_parser): - opts = [opt for group in opt_parser.option_groups - for opt in group.option_list] - opts_file = [opt for opt in opts if opt.metavar == "FILE"] - opts_dir = [opt for opt in opts if opt.metavar == "DIR"] +flags = [] +for opt in opts: + optionstrs = opt._long_opts + opt._short_opts + if len(optionstrs) == 1: + optionstr = optionstrs[0] + else: + optionstr = "'{" + ",".join(optionstrs) + "}'" - fileopts = [] - for opt in opts_file: - if opt._short_opts: - fileopts.extend(opt._short_opts) - if opt._long_opts: - fileopts.extend(opt._long_opts) + if opt.action in ["help", "version"]: + prefix = "- *" + else: + prefix = " ".join(optionstrs) + prefix = "'(" + prefix + ")" - diropts = [] - for opt in opts_dir: - if opt._short_opts: - diropts.extend(opt._short_opts) - if opt._long_opts: - diropts.extend(opt._long_opts) + if opt.help == SUPPRESS_HELP: + helpstr = "" + else: + helpstr = opt.help.replace("'", "'\\''").replace("]", "\\]") + helpstr = "[" + helpstr + "]" - flags = [opt.get_opt_string() for opt in opts] + if isinstance(opt.metavar, str): + metavar = opt.metavar + elif optionstr == "--external-downloader-args": + metavar = " " # use default + else: # opt.metavar is None + metavar = "" + if metavar != "": + # use lowcase conventionally + metavar = metavar.lower().replace(":", "\\:") - with open(ZSH_COMPLETION_TEMPLATE) as f: - template = f.read() + if opt.choices: + completion = "(" + " ".join(opt.choices) + ")" + elif optionstr == "--external-downloader-args": + completion = "->external-downloader-args" + elif metavar == "file": + completion = "_files" + metavar = " " + elif metavar == "dir": + completion = "_dirs" + metavar = " " + elif metavar == "url": + completion = "_urls" + metavar = " " + elif metavar == "command": + completion = "_command_names -e" + metavar = " " + else: + completion = "" - template = template.replace("{{fileopts}}", "|".join(fileopts)) - template = template.replace("{{diropts}}", "|".join(diropts)) - template = template.replace("{{flags}}", " ".join(flags)) + if metavar != "": + metavar = ":" + metavar + if completion != "": + completion = ":" + completion - with open(ZSH_COMPLETION_FILE, "w") as f: - f.write(template) + flag = "{0}{1}{2}{3}{4}'".format( + prefix, optionstr, helpstr, metavar, completion + ) + flags += [flag] -parser = youtube_dl.parseOpts()[0] -build_completion(parser) +with open(ZSH_COMPLETION_TEMPLATE) as f: + template = f.read() + +template = template.replace("{{programs}}", " ".join(BINNAMES)) +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) diff --git a/youtube_dl/options.py b/youtube_dl/options.py index f6621ef91..c685343f5 100644 --- a/youtube_dl/options.py +++ b/youtube_dl/options.py @@ -178,7 +178,7 @@ def parseOpts(overrideArguments=None): '(%APPDATA%/youtube-dl/config.txt on Windows)') general.add_option( '--config-location', - dest='config_location', metavar='PATH', + dest='config_location', metavar='FILE', help='Location of the configuration file; either the path to the config or its containing directory.') general.add_option( '--flat-playlist', @@ -414,13 +414,15 @@ def parseOpts(overrideArguments=None): '--youtube-skip-dash-manifest', action='store_false', dest='youtube_include_dash_manifest', help='Do not download the DASH manifests and related data on YouTube videos') + choices = ['mkv', 'mp4', 'ogg', 'webm', 'flv'] video_format.add_option( '--merge-output-format', action='store', dest='merge_output_format', metavar='FORMAT', default=None, + choices=choices, help=( 'If a merge is required (e.g. bestvideo+bestaudio), ' - 'output to given container format. One of mkv, mp4, ogg, webm, flv. ' - 'Ignored if no merge is required')) + 'output to given container format. (currently supported: {0})' + 'Ignored if no merge is required'.format(' '.join(choices)))) subtitles = optparse.OptionGroup(parser, 'Subtitle Options') subtitles.add_option( @@ -519,8 +521,9 @@ def parseOpts(overrideArguments=None): downloader.add_option( '--external-downloader', dest='external_downloader', metavar='COMMAND', + choices=list_external_downloaders(), help='Use the specified external downloader. ' - 'Currently supports %s' % ','.join(list_external_downloaders())) + '(currently supported: %s)' % ' '.join(list_external_downloaders())) downloader.add_option( '--external-downloader-args', dest='external_downloader_args', metavar='ARGS', @@ -787,17 +790,22 @@ def parseOpts(overrideArguments=None): '-x', '--extract-audio', action='store_true', dest='extractaudio', default=False, help='Convert video files to audio-only files (requires ffmpeg/avconv and ffprobe/avprobe)') + choices = ["best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", "wav"] postproc.add_option( '--audio-format', metavar='FORMAT', dest='audioformat', default='best', - help='Specify audio format: "best", "aac", "flac", "mp3", "m4a", "opus", "vorbis", or "wav"; "%default" by default; No effect without -x') + choices=choices, + help='Specify audio format. (currently supported: {0}); %default by default; No effect without -x'.format(" ".join(choices))) postproc.add_option( '--audio-quality', metavar='QUALITY', dest='audioquality', default='5', + choices=list(map(str, range(10))), help='Specify ffmpeg/avconv audio quality, insert a value between 0 (better) and 9 (worse) for VBR or a specific bitrate like 128K (default %default)') + choices = ["mp4", "flv", "ogg", "webm", "mkv", "avi"] postproc.add_option( '--recode-video', metavar='FORMAT', dest='recodevideo', default=None, - help='Encode the video to another format if necessary (currently supported: mp4|flv|ogg|webm|mkv|avi)') + choices=choices, + help='Encode the video to another format if necessary (currently supported: {0})'.format(" ".join(choices))) postproc.add_option( '--postprocessor-args', dest='postprocessor_args', metavar='ARGS', @@ -858,10 +866,12 @@ def parseOpts(overrideArguments=None): '--exec', metavar='CMD', dest='exec_cmd', help='Execute a command on the file after downloading and post-processing, similar to find\'s -exec syntax. Example: --exec \'adb push {} /sdcard/Music/ && rm {}\'') + choices = ["srt", "ass", "vtt", "lrc"] postproc.add_option( '--convert-subs', '--convert-subtitles', metavar='FORMAT', dest='convertsubtitles', default=None, - help='Convert the subtitles to other format (currently supported: srt|ass|vtt|lrc)') + choices=choices, + help='Convert the subtitles to other format (currently supported: {0})'.format(" ".join(choices))) parser.add_option_group(general) parser.add_option_group(network)