Repository: aws/aws-cli

Issue:
Merge boolean options in reference documentation
Current documentation displays boolean options in a confusing way:

--force (boolean) Forces the instance to stop. The instance will not have an opportunity to flush file system caches nor file system meta data. If you use this option, you must perform file system check and repair procedures. This option is not recommended for Windows instances.

--no-force (boolean) Forces the instance to stop. The instance will not have an opportunity to flush file system caches nor file system meta data. If you use this option, you must perform file system check and repair procedures. This option is not recommended for Windows instances.



Discussion:

Resolving patch:
diff --git a/awscli/arguments.py b/awscli/arguments.py
--- a/awscli/arguments.py
+++ b/awscli/arguments.py
@@ -92,6 +92,10 @@ def add_to_params(self, parameters, value):
         """
         pass
 
+    @property
+    def name(self):
+        return self._name
+
     @property
     def cli_name(self):
         return '--' + self._name
@@ -108,18 +112,37 @@ def required(self):
     def documentation(self):
         raise NotImplementedError("documentation")
 
+    @property
+    def cli_type(self):
+        raise NotImplementedError("cli_type")
+
     @property
     def py_name(self):
         return self._name.replace('-', '_')
 
     @property
-    def name(self):
-        return self._name
+    def choices(self):
+        return None
 
     @name.setter
     def name(self, value):
         self._name = value
 
+    @property
+    def group_name(self):
+        """Get the group name associated with the argument.
+
+        An argument can be part of a group.  This property will
+        return the name of that group.
+
+        This base class has no default behavior for groups, code
+        that consumes argument objects can use them for whatever
+        purposes they like (documentation, mututally exclusive group
+        validation, etc.).
+
+        """
+        return None
+
 
 class CustomArgument(BaseCLIArgument):
     """
@@ -133,7 +156,7 @@ class CustomArgument(BaseCLIArgument):
 
     def __init__(self, name, help_text='', dest=None, default=None,
                  action=None, required=None, choices=None, nargs=None,
-                 cli_type_name=None):
+                 cli_type_name=None, group_name=None):
         self._name = name
         self._help = help_text
         self._dest = dest
@@ -142,6 +165,7 @@ def __init__(self, name, help_text='', dest=None, default=None,
         self._required = required
         self._nargs = nargs
         self._cli_type_name = cli_type_name
+        self._group_name = group_name
         if choices is None:
             choices = []
         self._choices = choices
@@ -204,6 +228,10 @@ def cli_type(self):
     def choices(self):
         return self._choices
 
+    @property
+    def group_name(self):
+        return self._group_name
+
 
 class CLIArgument(BaseCLIArgument):
     """Represents a CLI argument that maps to a service parameter.
@@ -369,7 +397,7 @@ class BooleanArgument(CLIArgument):
     """
 
     def __init__(self, name, argument_object, operation_object,
-                 action='store_true', dest=None):
+                 action='store_true', dest=None, group_name=None):
         super(BooleanArgument, self).__init__(name, argument_object,
                                               operation_object)
         self._mutex_group = None
@@ -378,6 +406,10 @@ def __init__(self, name, argument_object, operation_object,
             self._destination = self.py_name
         else:
             self._destination = dest
+        if group_name is None:
+            self._group_name = self.name
+        else:
+            self._group_name = group_name
 
     def add_to_params(self, parameters, value):
         # If a value was explicitly specified (so value is True/False
@@ -399,7 +431,8 @@ def add_to_arg_table(self, argument_table):
         negative_version = self.__class__(negative_name, self.argument_object,
                                           self.operation_object,
                                           action='store_false',
-                                          dest=self._destination)
+                                          dest=self._destination,
+                                          group_name=self.group_name)
         argument_table[negative_name] = negative_version
 
     def add_to_parser(self, parser):
@@ -408,3 +441,7 @@ def add_to_parser(self, parser):
                             action=self._action,
                             default=None,
                             dest=self._destination)
+
+    @property
+    def group_name(self):
+        return self._group_name


Test patch:
diff --git a/tests/unit/docs/test_help_output.py b/tests/unit/docs/test_help_output.py
--- a/tests/unit/docs/test_help_output.py
+++ b/tests/unit/docs/test_help_output.py
@@ -156,9 +156,8 @@ def test_boolean_param_documented(self):
         self.driver.main(['autoscaling',
                           'terminate-instance-in-auto-scaling-group', 'help'])
         self.assert_contains(
-            '``--should-decrement-desired-capacity`` (boolean)')
-        self.assert_contains(
-            '``--no-should-decrement-desired-capacity`` (boolean)')
+            ('``--should-decrement-desired-capacity`` | '
+             '``--no-should-decrement-desired-capacity`` (boolean)'))
 
     def test_streaming_output_arg(self):
         self.driver.main(['s3api', 'get-object', 'help'])
@@ -193,3 +192,15 @@ def test_builtin_paging_params_removed(self):
         self.driver.main(['s3api', 'list-objects', 'help'])
         self.assert_not_contains('``--next-token``')
         self.assert_not_contains('``--max-keys``')
+
+
+class TestMergeBooleanGroupArgs(BaseAWSHelpOutputTest):
+    def test_merge_bool_args(self):
+        # Boolean args need to be group together so rather than
+        # --foo foo docs
+        # --no-foo foo docs again
+        #
+        # We instead have:
+        # --foo | --no-foo foo docs
+        self.driver.main(['ec2', 'run-instances', 'help'])
+        self.assert_contains('``--dry-run`` | ``--no-dry-run``')