595 lines
20 KiB
Python
595 lines
20 KiB
Python
# -*- coding: utf-8 -*- #
|
|
# Copyright 2015 Google LLC. All Rights Reserved.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
"""A collection of CLI walkers."""
|
|
|
|
from __future__ import absolute_import
|
|
from __future__ import division
|
|
from __future__ import unicode_literals
|
|
|
|
import io
|
|
import os
|
|
|
|
from googlecloudsdk.calliope import actions
|
|
from googlecloudsdk.calliope import arg_parsers
|
|
from googlecloudsdk.calliope import cli_tree
|
|
from googlecloudsdk.calliope import markdown
|
|
from googlecloudsdk.calliope import walker
|
|
from googlecloudsdk.core import properties
|
|
from googlecloudsdk.core.document_renderers import render_document
|
|
from googlecloudsdk.core.util import files
|
|
from googlecloudsdk.core.util import pkg_resources
|
|
import six
|
|
|
|
|
|
_HELP_HTML_DATA_FILES = [
|
|
'favicon.ico',
|
|
'index.html',
|
|
'_menu_.css',
|
|
'_menu_.js',
|
|
'_title_.html',
|
|
]
|
|
|
|
|
|
class DevSiteGenerator(walker.Walker):
|
|
"""Generates DevSite reference HTML in a directory hierarchy.
|
|
|
|
This implements gcloud meta generate-help-docs --manpage-dir=DIRECTORY.
|
|
|
|
Attributes:
|
|
_directory: The DevSite reference output directory. _need_section_tag[]:
|
|
_need_section_tag[i] is True if there are section subitems at depth i.
|
|
This prevents the creation of empty 'section:' tags in the '_toc' files.
|
|
_toc_root: The root TOC output stream.
|
|
_toc_main: The current main (just under root) TOC output stream.
|
|
"""
|
|
|
|
_REFERENCE = '/sdk/gcloud/reference' # TOC reference directory offset.
|
|
_TOC = '_toc.yaml'
|
|
|
|
def __init__(
|
|
self, cli, directory, hidden=False, progress_callback=None, restrict=None
|
|
):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The devsite output directory path name.
|
|
hidden: Boolean indicating whether to consider the hidden CLI.
|
|
progress_callback: f(float), The function to call to update the progress
|
|
bar or None for no progress bar.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
super(DevSiteGenerator, self).__init__(cli, restrict=restrict)
|
|
self._directory = directory
|
|
files.MakeDir(self._directory)
|
|
self._need_section_tag = []
|
|
toc_path = os.path.join(self._directory, self._TOC)
|
|
self._toc_root = files.FileWriter(toc_path)
|
|
self._toc_root.write('toc:\n')
|
|
self._toc_root.write('- title: "gcloud Reference"\n')
|
|
self._toc_root.write(' path: %s\n' % self._REFERENCE)
|
|
self._toc_root.write(' section:\n')
|
|
self._toc_main = None
|
|
|
|
def Visit(self, node, parent, is_group):
|
|
"""Updates the TOC and Renders a DevSite doc for each node in the CLI tree.
|
|
|
|
Args:
|
|
node: group/command CommandCommon info.
|
|
parent: The parent Visit() return value, None at the top level.
|
|
is_group: True if node is a group, otherwise its is a command.
|
|
|
|
Returns:
|
|
The parent value, ignored here.
|
|
"""
|
|
|
|
def _UpdateTOC():
|
|
"""Updates the DevSIte TOC."""
|
|
depth = len(command) - 1
|
|
if not depth:
|
|
return
|
|
title = ' '.join(command)
|
|
while depth >= len(self._need_section_tag):
|
|
self._need_section_tag.append(False)
|
|
if depth == 1:
|
|
if is_group:
|
|
if self._toc_main:
|
|
# Close the current main group toc if needed.
|
|
self._toc_main.close()
|
|
# Create a new main group toc.
|
|
toc_path = os.path.join(directory, self._TOC)
|
|
toc = files.FileWriter(toc_path)
|
|
self._toc_main = toc
|
|
toc.write('toc:\n')
|
|
toc.write('- title: "%s"\n' % title)
|
|
toc.write(' path: %s\n' % '/'.join([self._REFERENCE] + command[1:]))
|
|
self._need_section_tag[depth] = True
|
|
|
|
toc = self._toc_root
|
|
indent = ' '
|
|
if is_group:
|
|
toc.write(
|
|
'%s- include: %s\n'
|
|
% (
|
|
indent,
|
|
'/'.join([self._REFERENCE] + command[1:] + [self._TOC]),
|
|
)
|
|
)
|
|
return
|
|
else:
|
|
toc = self._toc_main
|
|
indent = ' ' * (depth - 1)
|
|
if self._need_section_tag[depth - 1]:
|
|
self._need_section_tag[depth - 1] = False
|
|
toc.write('%ssection:\n' % indent)
|
|
title = command[-1]
|
|
toc.write('%s- title: "%s"\n' % (indent, title))
|
|
toc.write(
|
|
'%s path: %s\n' % (indent, '/'.join([self._REFERENCE] + command[1:]))
|
|
)
|
|
self._need_section_tag[depth] = is_group
|
|
|
|
# Set up the destination dir for this level.
|
|
command = node.GetPath()
|
|
if is_group:
|
|
directory = os.path.join(self._directory, *command[1:])
|
|
files.MakeDir(directory, mode=0o755)
|
|
else:
|
|
directory = os.path.join(self._directory, *command[1:-1])
|
|
|
|
# Render the DevSite document.
|
|
path = (
|
|
os.path.join(directory, 'index' if is_group else command[-1]) + '.html'
|
|
)
|
|
|
|
# Currently, devsite pages from GDU are automatically mirrored to all other
|
|
# universes. To display Universe Disclaimer Information section correctly on
|
|
# all universes after mirroring, temporarily override universe_domain
|
|
# property to force the info section generated in devsite pages.
|
|
universe_domain = None
|
|
if properties.VALUES.core.universe_domain.IsExplicitlySet():
|
|
universe_domain = properties.VALUES.core.universe_domain.Get()
|
|
properties.VALUES.core.universe_domain.Set('universe')
|
|
|
|
with files.FileWriter(path) as f:
|
|
md = markdown.Markdown(node)
|
|
render_document.RenderDocument(
|
|
style='devsite',
|
|
title=' '.join(command),
|
|
fin=io.StringIO(md),
|
|
out=f,
|
|
command_node=node,
|
|
)
|
|
|
|
# reset universe_domain
|
|
properties.VALUES.core.universe_domain.Set(universe_domain)
|
|
_UpdateTOC()
|
|
return parent
|
|
|
|
def Done(self):
|
|
"""Closes the TOC files after the CLI tree walk is done."""
|
|
self._toc_root.close()
|
|
if self._toc_main:
|
|
self._toc_main.close()
|
|
|
|
|
|
class HelpTextGenerator(walker.Walker):
|
|
"""Generates help text files in a directory hierarchy.
|
|
|
|
Attributes:
|
|
_directory: The help text output directory.
|
|
"""
|
|
|
|
def __init__(
|
|
self, cli, directory, hidden=False, progress_callback=None, restrict=None
|
|
):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The Help Text output directory path name.
|
|
hidden: Boolean indicating whether to consider the hidden CLI.
|
|
progress_callback: f(float), The function to call to update the progress
|
|
bar or None for no progress bar.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
super(HelpTextGenerator, self).__init__(
|
|
cli, progress_callback=progress_callback, restrict=restrict
|
|
)
|
|
self._directory = directory
|
|
files.MakeDir(self._directory)
|
|
|
|
def Visit(self, node, parent, is_group):
|
|
"""Renders a help text doc for each node in the CLI tree.
|
|
|
|
Args:
|
|
node: group/command CommandCommon info.
|
|
parent: The parent Visit() return value, None at the top level.
|
|
is_group: True if node is a group, otherwise its is a command.
|
|
|
|
Returns:
|
|
The parent value, ignored here.
|
|
"""
|
|
# Set up the destination dir for this level.
|
|
command = node.GetPath()
|
|
|
|
if is_group:
|
|
directory = os.path.join(self._directory, *command[1:])
|
|
else:
|
|
directory = os.path.join(self._directory, *command[1:-1])
|
|
|
|
files.MakeDir(directory, mode=0o755)
|
|
# Render the help text document.
|
|
path = os.path.join(directory, 'GROUP' if is_group else command[-1])
|
|
with files.FileWriter(path) as f:
|
|
md = markdown.Markdown(node)
|
|
render_document.RenderDocument(style='text', fin=io.StringIO(md), out=f)
|
|
return parent
|
|
|
|
|
|
class DocumentGenerator(walker.Walker):
|
|
"""Generates style manpage files with suffix in an output directory.
|
|
|
|
All files will be generated in one directory.
|
|
|
|
Attributes:
|
|
_directory: The document output directory.
|
|
_style: The document style.
|
|
_suffix: The output file suffix.
|
|
"""
|
|
|
|
def __init__(self, cli, directory, style, suffix, restrict=None):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The manpage output directory path name.
|
|
style: The document style.
|
|
suffix: The generate document file suffix. None for .<SECTION>.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
super(DocumentGenerator, self).__init__(cli, restrict=restrict)
|
|
self._directory = directory
|
|
self._style = style
|
|
self._suffix = suffix
|
|
files.MakeDir(self._directory)
|
|
|
|
def Visit(self, node, parent, is_group):
|
|
"""Renders document file for each node in the CLI tree.
|
|
|
|
Args:
|
|
node: group/command CommandCommon info.
|
|
parent: The parent Visit() return value, None at the top level.
|
|
is_group: True if node is a group, otherwise its is a command.
|
|
|
|
Returns:
|
|
The parent value, ignored here.
|
|
"""
|
|
|
|
if self._style == 'linter':
|
|
meta_data = actions.GetCommandMetaData(node)
|
|
else:
|
|
meta_data = None
|
|
command = node.GetPath()
|
|
path = os.path.join(self._directory, '_'.join(command)) + self._suffix
|
|
with files.FileWriter(path) as f:
|
|
md = markdown.Markdown(node)
|
|
render_document.RenderDocument(
|
|
style=self._style,
|
|
title=' '.join(command),
|
|
fin=io.StringIO(md),
|
|
out=f,
|
|
command_metadata=meta_data,
|
|
)
|
|
return parent
|
|
|
|
|
|
class HtmlGenerator(DocumentGenerator):
|
|
"""Generates HTML manpage files with suffix .html in an output directory.
|
|
|
|
The output directory will contain a man1 subdirectory containing all of the
|
|
HTML manpage files.
|
|
"""
|
|
|
|
def WriteHtmlMenu(self, command, out):
|
|
"""Writes the command menu tree HTML on out.
|
|
|
|
Args:
|
|
command: dict, The tree (nested dict) of command/group names.
|
|
out: stream, The output stream.
|
|
"""
|
|
|
|
def ConvertPathToIdentifier(path):
|
|
return '_'.join(path)
|
|
|
|
def WalkCommandTree(command, prefix):
|
|
"""Visit each command and group in the CLI command tree.
|
|
|
|
Args:
|
|
command: dict, The tree (nested dict) of command/group names.
|
|
prefix: [str], The subcommand arg prefix.
|
|
"""
|
|
level = len(prefix)
|
|
visibility = 'visible' if level <= 1 else 'hidden'
|
|
indent = level * 2 + 2
|
|
name = command.get('_name_')
|
|
args = prefix + [name]
|
|
out.write(
|
|
'{indent}<li class="{visibility}" id="{item}" '
|
|
'onclick="select(event, this.id)">{name}'.format(
|
|
indent=' ' * indent,
|
|
visibility=visibility,
|
|
name=name,
|
|
item=ConvertPathToIdentifier(args),
|
|
)
|
|
)
|
|
commands = command.get('commands', []) + command.get('groups', [])
|
|
if commands:
|
|
out.write('<ul>\n')
|
|
for c in sorted(commands, key=lambda x: x['_name_']):
|
|
WalkCommandTree(c, args)
|
|
out.write('{indent}</ul>\n'.format(indent=' ' * (indent + 1)))
|
|
out.write('{indent}</li>\n'.format(indent=' ' * indent))
|
|
else:
|
|
out.write('</li>\n')
|
|
|
|
out.write("""\
|
|
<html>
|
|
<head>
|
|
<meta name="description" content="man page tree navigation">
|
|
<meta name="generator" content="gcloud meta generate-help-docs --html-dir=.">
|
|
<title> man page tree navigation </title>
|
|
<base href="." target="_blank">
|
|
<link rel="stylesheet" type="text/css" href="_menu_.css">
|
|
<script type="text/javascript" src="_menu_.js"></script>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="menu">
|
|
<ul>
|
|
""")
|
|
WalkCommandTree(command, [])
|
|
out.write("""\
|
|
</ul>
|
|
</div>
|
|
|
|
</body>
|
|
</html>
|
|
""")
|
|
|
|
def _GenerateHtmlNav(self, directory, cli, hidden, restrict):
|
|
"""Generates html nav files in directory."""
|
|
tree = CommandTreeGenerator(cli).Walk(hidden, restrict)
|
|
with files.FileWriter(os.path.join(directory, '_menu_.html')) as out:
|
|
self.WriteHtmlMenu(tree, out)
|
|
for file_name in _HELP_HTML_DATA_FILES:
|
|
file_contents = pkg_resources.GetResource(
|
|
'googlecloudsdk.api_lib.meta.help_html_data.', file_name
|
|
)
|
|
files.WriteBinaryFileContents(
|
|
os.path.join(directory, file_name), file_contents
|
|
)
|
|
|
|
def __init__(
|
|
self, cli, directory, hidden=False, progress_callback=None, restrict=None
|
|
):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The HTML output directory path name.
|
|
hidden: Boolean indicating whether to consider the hidden CLI.
|
|
progress_callback: f(float), The function to call to update the progress
|
|
bar or None for no progress bar.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
super(HtmlGenerator, self).__init__(
|
|
cli,
|
|
directory=directory,
|
|
style='html',
|
|
suffix='.html',
|
|
restrict=restrict,
|
|
)
|
|
self._GenerateHtmlNav(directory, cli, hidden, restrict)
|
|
|
|
|
|
class ManPageGenerator(DocumentGenerator):
|
|
"""Generates manpage files with suffix .1 in an output directory.
|
|
|
|
The output directory will contain a man1 subdirectory containing all of the
|
|
manpage files.
|
|
"""
|
|
|
|
_SECTION_FORMAT = 'man{section}'
|
|
|
|
def __init__(
|
|
self, cli, directory, hidden=False, progress_callback=None, restrict=None
|
|
):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The manpage output directory path name.
|
|
hidden: Boolean indicating whether to consider the hidden CLI.
|
|
progress_callback: f(float), The function to call to update the progress
|
|
bar or None for no progress bar.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
|
|
# Currently all gcloud manpages are in section 1.
|
|
section_subdir = self._SECTION_FORMAT.format(section=1)
|
|
section_dir = os.path.join(directory, section_subdir)
|
|
super(ManPageGenerator, self).__init__(
|
|
cli, directory=section_dir, style='man', suffix='.1', restrict=restrict
|
|
)
|
|
|
|
|
|
class LinterGenerator(DocumentGenerator):
|
|
"""Generates linter files with suffix .json in an output directory."""
|
|
|
|
def __init__(
|
|
self, cli, directory, hidden=False, progress_callback=None, restrict=None
|
|
):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
directory: The linter output directory path name.
|
|
hidden: Boolean indicating whether to consider the hidden CLI.
|
|
progress_callback: f(float), The function to call to update the progress
|
|
bar or None for no progress bar.
|
|
restrict: Restricts the walk to the command/group dotted paths in this
|
|
list. For example, restrict=['gcloud.alpha.test', 'gcloud.topic']
|
|
restricts the walk to the 'gcloud topic' and 'gcloud alpha test'
|
|
commands/groups.
|
|
"""
|
|
|
|
super(LinterGenerator, self).__init__(
|
|
cli,
|
|
directory=directory,
|
|
style='linter',
|
|
suffix='.json',
|
|
restrict=restrict,
|
|
)
|
|
|
|
|
|
class CommandTreeGenerator(walker.Walker):
|
|
"""Constructs a CLI command dict tree.
|
|
|
|
This implements the resource generator for gcloud meta list-commands.
|
|
|
|
Attributes:
|
|
_with_flags: Include the non-global flags for each command/group if True.
|
|
_with_flag_values: Include flag value choices or :type: if True.
|
|
_global_flags: The set of global flags, only listed for the root command.
|
|
"""
|
|
|
|
def __init__(self, cli, with_flags=False, with_flag_values=False, **kwargs):
|
|
"""Constructor.
|
|
|
|
Args:
|
|
cli: The Cloud SDK CLI object.
|
|
with_flags: Include the non-global flags for each command/group if True.
|
|
with_flag_values: Include flags and flag value choices or :type: if True.
|
|
**kwargs: Other keyword arguments to pass to Walker constructor.
|
|
"""
|
|
super(CommandTreeGenerator, self).__init__(cli, **kwargs)
|
|
self._with_flags = with_flags or with_flag_values
|
|
self._with_flag_values = with_flag_values
|
|
self._global_flags = set()
|
|
|
|
def Visit(self, node, parent, is_group):
|
|
"""Visits each node in the CLI command tree to construct the dict tree.
|
|
|
|
Args:
|
|
node: group/command CommandCommon info.
|
|
parent: The parent Visit() return value, None at the top level.
|
|
is_group: True if node is a group, otherwise its is a command.
|
|
|
|
Returns:
|
|
The subtree parent value, used here to construct a dict tree.
|
|
"""
|
|
name = node.name.replace('_', '-')
|
|
info = {'_name_': name}
|
|
if self._with_flags:
|
|
all_flags = []
|
|
for arg in node.GetAllAvailableFlags():
|
|
value = None
|
|
if self._with_flag_values:
|
|
if arg.choices:
|
|
choices = sorted(arg.choices)
|
|
if choices != ['false', 'true']:
|
|
value = ','.join([six.text_type(choice) for choice in choices])
|
|
elif isinstance(arg.type, int):
|
|
value = ':int:'
|
|
elif isinstance(arg.type, float):
|
|
value = ':float:'
|
|
elif isinstance(arg.type, arg_parsers.ArgDict):
|
|
value = ':dict:'
|
|
elif isinstance(arg.type, arg_parsers.ArgList):
|
|
value = ':list:'
|
|
elif arg.nargs != 0:
|
|
metavar = arg.metavar or arg.dest.upper()
|
|
value = ':' + metavar + ':'
|
|
for f in arg.option_strings:
|
|
if value:
|
|
f += '=' + value
|
|
all_flags.append(f)
|
|
no_prefix = '--no-'
|
|
flags = []
|
|
for flag in all_flags:
|
|
if flag in self._global_flags:
|
|
continue
|
|
if flag.startswith(no_prefix):
|
|
positive = '--' + flag[len(no_prefix) :]
|
|
if positive in all_flags:
|
|
continue
|
|
flags.append(flag)
|
|
if flags:
|
|
info['_flags_'] = sorted(flags)
|
|
if not self._global_flags:
|
|
# Most command flags are global (defined by the root command) or
|
|
# command-specific. Group-specific flags are rare. Separating out
|
|
# the global flags streamlines command descriptions and prevents
|
|
# global flag changes (we already have too many!) from making it
|
|
# look like every command has changed.
|
|
self._global_flags.update(flags)
|
|
if is_group:
|
|
if parent:
|
|
if cli_tree.LOOKUP_GROUPS not in parent:
|
|
parent[cli_tree.LOOKUP_GROUPS] = []
|
|
parent[cli_tree.LOOKUP_GROUPS].append(info)
|
|
return info
|
|
if cli_tree.LOOKUP_COMMANDS not in parent:
|
|
parent[cli_tree.LOOKUP_COMMANDS] = []
|
|
parent[cli_tree.LOOKUP_COMMANDS].append(info)
|
|
return None
|
|
|
|
|
|
class GCloudTreeGenerator(walker.Walker):
|
|
"""Generates an external representation of the gcloud CLI tree.
|
|
|
|
This implements the resource generator for gcloud meta list-gcloud.
|
|
"""
|
|
|
|
def Visit(self, node, parent, is_group):
|
|
"""Visits each node in the CLI command tree to construct the external rep.
|
|
|
|
Args:
|
|
node: group/command CommandCommon info.
|
|
parent: The parent Visit() return value, None at the top level.
|
|
is_group: True if node is a group, otherwise its is a command.
|
|
|
|
Returns:
|
|
The subtree parent value, used here to construct an external rep node.
|
|
"""
|
|
return cli_tree.Command(node, parent)
|