Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit db6cc60

Browse files
authored
Migrate command line parsing to argparse (ansible#50610)
* Start of migration to argparse * various fixes and improvements * Linting fixes * Test fixes * Fix vault_password_files * Add PrependAction for argparse * A bunch of additional tweak/fixes * Fix ansible-config tests * Fix man page generation * linting fix * More adhoc pattern fixes * Add changelog fragment * Add support for argcomplete * Enable argcomplete global completion * Rename PrependAction to PrependListAction to better describe what it does * Add documentation for installing and configuring argcomplete * Address rebase issues * Fix display encoding for vault * Fix line length * Address rebase issues * Handle rebase issues * Use mutually exclusive group instead of handling manually * Fix rebase issues * Address rebase issue * Update version added for argcomplete support * -e must be given a value * ci_complete
1 parent 7ee6c13 commit db6cc60

28 files changed

Lines changed: 933 additions & 917 deletions

File tree

bin/ansible

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
#!/usr/bin/env python
2-
2+
# -*- coding: utf-8 -*-
33
# (c) 2012, Michael DeHaan <[email protected]>
44
#
55
# This file is part of Ansible
@@ -17,7 +17,8 @@
1717
# You should have received a copy of the GNU General Public License
1818
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
1919

20-
########################################################
20+
# PYTHON_ARGCOMPLETE_OK
21+
2122
from __future__ import (absolute_import, division, print_function)
2223
__metaclass__ = type
2324

changelogs/fragments/argparse.yaml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
minor_changes:
2+
- Command line argument parsing - Switch from deprecated optparse to argparse

docs/bin/generate_man.py

Lines changed: 45 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env python
22

3-
import optparse
3+
import argparse
44
import os
55
import sys
66

@@ -11,15 +11,14 @@
1111

1212

1313
def generate_parser():
14-
p = optparse.OptionParser(
15-
version='%prog 1.0',
16-
usage='usage: %prog [options]',
14+
p = argparse.ArgumentParser(
1715
description='Generate cli documentation from cli docstrings',
1816
)
1917

20-
p.add_option("-t", "--template-file", action="store", dest="template_file", default="../templates/man.j2", help="path to jinja2 template")
21-
p.add_option("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
22-
p.add_option("-f", "--output-format", action="store", dest="output_format", default='man', help="Output format for docs (the default 'man' or 'rst')")
18+
p.add_argument("-t", "--template-file", action="store", dest="template_file", default="../templates/man.j2", help="path to jinja2 template")
19+
p.add_argument("-o", "--output-dir", action="store", dest="output_dir", default='/tmp/', help="Output directory for rst files")
20+
p.add_argument("-f", "--output-format", action="store", dest="output_format", default='man', help="Output format for docs (the default 'man' or 'rst')")
21+
p.add_argument('args', help='CLI module(s)', metavar='module', nargs='*')
2322
return p
2423

2524

@@ -57,34 +56,49 @@ def get_options(optlist):
5756
for opt in optlist:
5857
res = {
5958
'desc': opt.help,
60-
'options': opt._short_opts + opt._long_opts
59+
'options': opt.option_strings
6160
}
62-
if opt.action == 'store':
61+
if isinstance(opt, argparse._StoreAction):
6362
res['arg'] = opt.dest.upper()
63+
elif not res['options']:
64+
continue
6465
opts.append(res)
6566

6667
return opts
6768

6869

70+
def dedupe_groups(parser):
71+
action_groups = []
72+
for action_group in parser._action_groups:
73+
found = False
74+
for a in action_groups:
75+
if a._actions == action_group._actions:
76+
found = True
77+
break
78+
if not found:
79+
action_groups.append(action_group)
80+
return action_groups
81+
82+
6983
def get_option_groups(option_parser):
7084
groups = []
71-
for option_group in option_parser.option_groups:
85+
for action_group in dedupe_groups(option_parser)[1:]:
7286
group_info = {}
73-
group_info['desc'] = option_group.get_description()
74-
group_info['options'] = option_group.option_list
75-
group_info['group_obj'] = option_group
87+
group_info['desc'] = action_group.description
88+
group_info['options'] = action_group._actions
89+
group_info['group_obj'] = action_group
7690
groups.append(group_info)
7791
return groups
7892

7993

80-
def opt_doc_list(cli):
94+
def opt_doc_list(parser):
8195
''' iterate over options lists '''
8296

8397
results = []
84-
for option_group in cli.parser.option_groups:
85-
results.extend(get_options(option_group.option_list))
98+
for option_group in dedupe_groups(parser)[1:]:
99+
results.extend(get_options(option_group._actions))
86100

87-
results.extend(get_options(cli.parser.option_list))
101+
results.extend(get_options(parser._actions))
88102

89103
return results
90104

@@ -106,15 +120,17 @@ def opts_docs(cli_class_name, cli_module_name):
106120

107121
# parse the common options
108122
try:
109-
cli.parse()
123+
cli.init_parser()
110124
except Exception:
111125
pass
112126

127+
cli.parser.prog = cli_name
128+
113129
# base/common cli info
114130
docs = {
115131
'cli': cli_module_name,
116132
'cli_name': cli_name,
117-
'usage': cli.parser.usage,
133+
'usage': cli.parser.format_usage(),
118134
'short_desc': cli.parser.description,
119135
'long_desc': trim_docstring(cli.__doc__),
120136
'actions': {},
@@ -127,7 +143,7 @@ def opts_docs(cli_class_name, cli_module_name):
127143
if hasattr(cli, extras):
128144
docs[extras.lower()] = getattr(cli, extras)
129145

130-
common_opts = opt_doc_list(cli)
146+
common_opts = opt_doc_list(cli.parser)
131147
groups_info = get_option_groups(cli.parser)
132148
shared_opt_names = []
133149
for opt in common_opts:
@@ -144,25 +160,11 @@ def opts_docs(cli_class_name, cli_module_name):
144160
# force populate parser with per action options
145161

146162
# use class attrs not the attrs on a instance (not that it matters here...)
147-
for action in getattr(cli_klass, 'VALID_ACTIONS', ()):
148-
# instantiate each cli and ask its options
149-
action_cli_klass = getattr(__import__("ansible.cli.%s" % cli_module_name,
150-
fromlist=[cli_class_name]), cli_class_name)
151-
# init with args with action added?
152-
cli = action_cli_klass([])
153-
cli.args.append(action)
154-
155-
try:
156-
cli.parse()
157-
except Exception:
158-
pass
159-
160-
# FIXME/TODO: needed?
161-
# avoid dupe errors
162-
cli.parser.set_conflict_handler('resolve')
163-
164-
cli.set_action()
165-
163+
try:
164+
subparser = cli.parser._subparsers._group_actions[0].choices
165+
except AttributeError:
166+
subparser = {}
167+
for action, parser in subparser.items():
166168
action_info = {'option_names': [],
167169
'options': []}
168170
# docs['actions'][action] = {}
@@ -171,7 +173,7 @@ def opts_docs(cli_class_name, cli_module_name):
171173
action_info['desc'] = trim_docstring(getattr(cli, 'execute_%s' % action).__doc__)
172174

173175
# docs['actions'][action]['desc'] = getattr(cli, 'execute_%s' % action).__doc__.strip()
174-
action_doc_list = opt_doc_list(cli)
176+
action_doc_list = opt_doc_list(parser)
175177

176178
uncommon_options = []
177179
for action_doc in action_doc_list:
@@ -196,15 +198,15 @@ def opts_docs(cli_class_name, cli_module_name):
196198

197199
docs['actions'][action] = action_info
198200

199-
docs['options'] = opt_doc_list(cli)
201+
docs['options'] = opt_doc_list(cli.parser)
200202
return docs
201203

202204

203205
if __name__ == '__main__':
204206

205207
parser = generate_parser()
206208

207-
options, args = parser.parse_args()
209+
options = parser.parse_args()
208210

209211
template_file = options.template_file
210212
template_path = os.path.expanduser(template_file)
@@ -214,7 +216,7 @@ def opts_docs(cli_class_name, cli_module_name):
214216
output_dir = os.path.abspath(options.output_dir)
215217
output_format = options.output_format
216218

217-
cli_modules = args
219+
cli_modules = options.args
218220

219221
# various cli parsing things checks sys.argv if the 'args' that are passed in are []
220222
# so just remove any args so the cli modules dont try to parse them resulting in warnings

docs/docsite/rst/installation_guide/intro_installation.rst

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,91 @@ Now let's test things with a ping command:
429429
430430
You can also use "sudo make install".
431431

432+
.. _shell_completion:
433+
434+
Shell Completion
435+
````````````````
436+
437+
As of Ansible 2.9 shell completion of the ansible command line utilities is available and provided through an optional dependency
438+
called ``argcomplete``. ``argcomplete`` supports bash, and limited support for zsh and tcsh
439+
440+
``python-argcomplete`` can be installed from EPEL on Red Hat Enterprise based distributions, and is available in the standard OS repositories for many other distributions.
441+
442+
For more information about installing and configuration see the `argcomplete documentation <https://argcomplete.readthedocs.io/en/latest/>_`.
443+
444+
Installing
445+
++++++++++
446+
447+
via yum/dnf
448+
-----------
449+
450+
On Fedora:
451+
452+
.. code-block:: bash
453+
454+
$ sudo dnf install python-argcomplete
455+
456+
On RHEL and CentOS:
457+
458+
.. code-block:: bash
459+
460+
$ sudo yum install epel-release
461+
$ sudo yum install python-argcomplete
462+
463+
via apt
464+
-------
465+
466+
.. code-block:: bash
467+
468+
$ sudo apt install python-argcomplete
469+
470+
via pip
471+
-------
472+
473+
.. code-block:: bash
474+
475+
$ pip install argcomplete
476+
477+
Configuring
478+
+++++++++++
479+
480+
There are 2 ways to configure argcomplete to allow shell completion of the Ansible command line utilities. Per command, or globally.
481+
482+
Globally
483+
--------
484+
485+
Global completion requires bash 4.2
486+
487+
.. code-block:: bash
488+
489+
$ sudo activate-global-python-argcomplete
490+
491+
This will write a bash completion file to a global location, use ``--dest`` to change the location
492+
493+
Per Command
494+
-----------
495+
496+
If you do not have bash 4.2, you must register each script independently
497+
498+
.. code-block:: bash
499+
500+
$ eval $(register-python-argcomplete ansible)
501+
$ eval $(register-python-argcomplete ansible-config)
502+
$ eval $(register-python-argcomplete ansible-console)
503+
$ eval $(register-python-argcomplete ansible-doc)
504+
$ eval $(register-python-argcomplete ansible-galaxy)
505+
$ eval $(register-python-argcomplete ansible-inventory)
506+
$ eval $(register-python-argcomplete ansible-playbook)
507+
$ eval $(register-python-argcomplete ansible-pull)
508+
$ eval $(register-python-argcomplete ansible-vault)
509+
510+
It would be advisable to place the above commands, into your shells profile file such as ``~/.profile`` or ``~/.bash_profile``.
511+
512+
Zsh or tcsh
513+
-----------
514+
515+
See the `argcomplete documentation <https://argcomplete.readthedocs.io/en/latest/>_`.
516+
432517
.. _getting_ansible:
433518

434519
Ansible on GitHub

docs/docsite/rst/user_guide/playbooks_vault.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ Using Vault in playbooks
77

88
The "Vault" is a feature of Ansible that allows you to keep sensitive data such as passwords or keys in encrypted files, rather than as plaintext in playbooks or roles. These vault files can then be distributed or placed in source control.
99

10-
To enable this feature, a command line tool, :ref:`ansible-vault` is used to edit files, and a command line flag :option:`--ask-vault-pass <ansible-vault --ask-vault-pass>`, :option:`--vault-password-file <ansible-vault --vault-password-file>` or :option:`--vault-id <ansible-playbook --vault-id>` is used. You can also modify your ``ansible.cfg`` file to specify the location of a password file or configure Ansible to always prompt for the password. These options require no command line flag usage.
10+
To enable this feature, a command line tool, :ref:`ansible-vault` is used to edit files, and a command line flag :option:`--ask-vault-pass <ansible-vault-create --ask-vault-pass>`, :option:`--vault-password-file <ansible-vault-create --vault-password-file>` or :option:`--vault-id <ansible-playbook --vault-id>` is used. You can also modify your ``ansible.cfg`` file to specify the location of a password file or configure Ansible to always prompt for the password. These options require no command line flag usage.
1111

1212
For best practices advice, refer to :ref:`best_practices_for_variables_and_vaults`.
1313

docs/docsite/rst/user_guide/vault.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ passwords will be tried in the order they are specified.
344344
In the above case, the 'dev' password will be tried first, then the 'prod' password for cases
345345
where Ansible doesn't know which vault ID is used to encrypt something.
346346

347-
To add a vault ID label to the encrypted data use the :option:`--vault-id <ansible-vault --vault-id>` option
347+
To add a vault ID label to the encrypted data use the :option:`--vault-id <ansible-vault-create --vault-id>` option
348348
with a label when encrypting the data.
349349

350350
The :ref:`DEFAULT_VAULT_ID_MATCH` config option can be set so that Ansible will only use the password with

docs/templates/cli_rst.j2

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ Common Options
3838
==============
3939

4040

41-
{% for option in options|sort(attribute='options') %}
41+
{% for option in options|sort(attribute='options') if option.options %}
4242

4343
.. option:: {% for switch in option['options'] %}{{switch}}{% if option['arg'] %} <{{option['arg']}}>{% endif %}{% if not loop.last %}, {% endif %}{% endfor %}
4444

0 commit comments

Comments
 (0)