Repository: aws/aws-cli

Issue:
cloudformation describe-stack-events --output table omits ResourceStatusReason
aws-cli/0.13.2 Python/2.7.5 Darwin/12.4.1

This may be an RFE. With a command like `aws cloudformation describe-stack-events --output table ...` the table output does not include the `ResourceStatusReason` data. This is useful data when failures occur. For example here is some sample output with an interesting `ResourceStatusReason`.

```
{
    "StackId": "arn:aws:cloudformation:us-west-2:317324027142:stack/spot-3x-m1large/e1fa9ac0-f985-11e2-aa7f-507bfc8736d2", 
    "EventId": "LaunchConfig1-CREATE_FAILED-1375236694000", 
    "ResourceStatus": "CREATE_FAILED", 
    "ResourceType": "AWS::AutoScaling::LaunchConfiguration", 
    "Timestamp": "2013-07-31T02:11:34Z", 
    "ResourceStatusReason": "AMI cannot be described", 
    "StackName": "spot-3x-m1large", 
    "PhysicalResourceId": "spot-3x-m1large-LaunchConfig1-EFTX7ICLP050", 
    "LogicalResourceId": "LaunchConfig1"
}
```



Discussion:

Resolving patch:
diff --git a/awscli/formatter.py b/awscli/formatter.py
--- a/awscli/formatter.py
+++ b/awscli/formatter.py
@@ -140,7 +140,7 @@ def _build_sub_table_from_dict(self, current, indent_level):
                               indent_level=indent_level + 1)
 
     def _build_sub_table_from_list(self, current, indent_level, title):
-        headers, more = self._group_scalar_keys(current[0])
+        headers, more = self._group_scalar_keys_from_list(current)
         self.table.add_row_header(headers)
         first = True
         for element in current:
@@ -149,7 +149,9 @@ def _build_sub_table_from_list(self, current, indent_level, title):
                                        indent_level=indent_level)
                 self.table.add_row_header(headers)
             first = False
-            self.table.add_row([element[header] for header in headers])
+            # Use .get() to account for the fact that sometimes an element
+            # may not have all the keys from the header.
+            self.table.add_row([element.get(header, '') for header in headers])
             for remaining in more:
                 # Some of the non scalar attributes may not necessarily
                 # be in every single element of the list, so we need to
@@ -161,6 +163,20 @@ def _build_sub_table_from_list(self, current, indent_level, title):
     def _scalar_type(self, element):
         return not isinstance(element, (list, dict))
 
+    def _group_scalar_keys_from_list(self, list_of_dicts):
+        # We want to make sure we catch all the keys in the list of dicts.
+        # Most of the time each list element has the same keys, but sometimes
+        # a list element will have keys not defined in other elements.
+        headers = set()
+        more = set()
+        for item in list_of_dicts:
+            current_headers, current_more = self._group_scalar_keys(item)
+            headers.update(current_headers)
+            more.update(current_more)
+        headers = list(sorted(headers))
+        more = list(sorted(more))
+        return headers, more
+
     def _group_scalar_keys(self, current):
         # Given a dict, separate the keys into those whose values are
         # scalar, and those whose values aren't.  Return two lists,


Test patch:
diff --git a/tests/unit/output/test_table_formatter.py b/tests/unit/output/test_table_formatter.py
--- a/tests/unit/output/test_table_formatter.py
+++ b/tests/unit/output/test_table_formatter.py
@@ -166,16 +166,12 @@
 """
 
 
+# First record has "Tags" scalar, second record does not.
 INNER_LIST = {
     "Snapshots": [
         {
             "Description": "TestVolume1",
-            "Tags": [
-                {
-                    "Value": "TestVolume",
-                    "Key": "Name"
-                }
-            ],
+            "Tags": [{"Value": "TestVolume", "Key": "Name"}],
             "VolumeId": "vol-12345",
             "State": "completed",
             "VolumeSize": 8,
@@ -221,6 +217,84 @@
 |+------------------------------------------------------------+----------+-----------+-------------+---------------------------+------------+---------------+--------------+|
 """
 
+LIST_WITH_MISSING_KEYS = {
+    "Snapshots": [
+        {
+            "Description": "TestVolume1",
+            "Tags": "foo",
+            "VolumeId": "vol-12345",
+            "State": "completed",
+            "VolumeSize": 8,
+            "Progress": "100%",
+            "StartTime": "2012-05-23T21:46:41.000Z",
+            "SnapshotId": "snap-1234567",
+            "OwnerId": "12345"
+        },
+        {
+            "Description": "description",
+            "VolumeId": "vol-e543b98b",
+            "State": "completed",
+            "VolumeSize": 8,
+            "Progress": "100%",
+            "StartTime": "2012-05-25T00:07:20.000Z",
+            "SnapshotId": "snap-23456",
+            "OwnerId": "12345"
+        }
+    ]
+}
+
+LIST_WITH_MISSING_KEYS_TABLE = """\
+-----------------------------------------------------------------------------------------------------------------------------------------
+|                                                             OperationName                                                             |
++---------------------------------------------------------------------------------------------------------------------------------------+
+||                                                              Snapshots                                                              ||
+|+-------------+----------+-----------+---------------+---------------------------+------------+-------+----------------+--------------+|
+|| Description | OwnerId  | Progress  |  SnapshotId   |         StartTime         |   State    | Tags  |   VolumeId     | VolumeSize   ||
+|+-------------+----------+-----------+---------------+---------------------------+------------+-------+----------------+--------------+|
+||  TestVolume1|  12345   |  100%     |  snap-1234567 |  2012-05-23T21:46:41.000Z |  completed |  foo  |  vol-12345     |  8           ||
+||  description|  12345   |  100%     |  snap-23456   |  2012-05-25T00:07:20.000Z |  completed |       |  vol-e543b98b  |  8           ||
+|+-------------+----------+-----------+---------------+---------------------------+------------+-------+----------------+--------------+|
+"""
+
+KEYS_NOT_FROM_FIRST_ROW = {
+    "Snapshots": [
+        {
+            "Description": "TestVolume1",
+            "Tags": "foo",
+            "VolumeId": "vol-12345",
+            "State": "completed",
+            "VolumeSize": 8,
+            "Progress": "100%",
+            "StartTime": "start_time",
+            # Missing EndTime.
+            "SnapshotId": "snap-1234567",
+            "OwnerId": "12345"
+        },
+        {
+            "Description": "description",
+            "State": "completed",
+            "VolumeSize": 8,
+            "Progress": "100%",
+            # Missing StartTime
+            "EndTime": "end_time",
+            "SnapshotId": "snap-23456",
+            "OwnerId": "12345"
+        }
+    ]
+}
+
+KEYS_NOT_FROM_FIRST_ROW_TABLE = """\
+------------------------------------------------------------------------------------------------------------------------------------
+|                                                           OperationName                                                          |
++----------------------------------------------------------------------------------------------------------------------------------+
+||                                                            Snapshots                                                           ||
+|+-------------+-----------+----------+-----------+---------------+-------------+------------+-------+-------------+--------------+|
+|| Description |  EndTime  | OwnerId  | Progress  |  SnapshotId   |  StartTime  |   State    | Tags  |  VolumeId   | VolumeSize   ||
+|+-------------+-----------+----------+-----------+---------------+-------------+------------+-------+-------------+--------------+|
+||  TestVolume1|           |  12345   |  100%     |  snap-1234567 |  start_time |  completed |  foo  |  vol-12345  |  8           ||
+||  description|  end_time |  12345   |  100%     |  snap-23456   |             |  completed |       |             |  8           ||
+|+-------------+-----------+----------+-----------+---------------+-------------+------------+-------+-------------+--------------+|
+"""
 
 class Object(object):
     def __init__(self, **kwargs):
@@ -243,6 +317,7 @@ def assert_data_renders_to(self, data, table):
         self.formatter(Object(name='OperationName', can_paginate=False),
                               data, self.stream)
         rendered = self.stream.getvalue()
+        print(rendered)
         self.assertEqual(rendered, table)
 
     def test_list_table(self):
@@ -262,3 +337,11 @@ def test_inner_table(self):
     def test_empty_table(self):
         self.assert_data_renders_to(data={},
                                     table='')
+
+    def test_missing_keys(self):
+        self.assert_data_renders_to(data=LIST_WITH_MISSING_KEYS,
+                                    table=LIST_WITH_MISSING_KEYS_TABLE)
+
+    def test_new_keys_after_first_row(self):
+        self.assert_data_renders_to(data=KEYS_NOT_FROM_FIRST_ROW,
+                                    table=KEYS_NOT_FROM_FIRST_ROW_TABLE)