From a0fa3ec55c531b12d7de947bfedd8ad47f0a89af Mon Sep 17 00:00:00 2001
From: James Saryerwinnie <js@jamesls.com>
Date: Mon, 25 Nov 2013 16:33:00 -0800
Subject: [PATCH 1/4] Remove unused function

---
 awscli/customizations/configure.py | 3 ---
 1 file changed, 3 deletions(-)

diff --git a/awscli/customizations/configure.py b/awscli/customizations/configure.py
index 3e97338308ca..e4b55c32c13a 100644
--- a/awscli/customizations/configure.py
+++ b/awscli/customizations/configure.py
@@ -33,9 +33,6 @@ def register_configure_cmd(cli):
     cli.register('building-command-table.main',
                  ConfigureCommand.add_command)
 
-def add_configure_cmd(command_table, session, **kwargs):
-    command_table['configure'] = ConfigureCommand(session)
-
 
 class SectionNotFoundError(Exception):
     pass

From a54f8861c93dd0b0ad4e9b14cb20434d283c6e01 Mon Sep 17 00:00:00 2001
From: James Saryerwinnie <js@jamesls.com>
Date: Mon, 25 Nov 2013 18:17:21 -0800
Subject: [PATCH 2/4] Add an 'aws configure list' command

This will show you where all the config values are coming from.
As part of this change, I also updated the commands.py module
to support subcommands.  This makes it easy to create a top
level command that supports subcommands in a declarative manner.
You can now run:

* `aws configure`
* `aws configure list`

As an example, you'll see output like this:

        Name                    Value             Type    Location
        ----                    -----             ----    --------
     profile                <not set>             None    None
  access_key     ****************ABCD      config_file    ~/.aws/config
  secret_key     ****************ABCD      config_file    ~/.aws/config
      region                us-west-2      config_file    ~/.aws/config
---
 awscli/argparser.py                         |  14 ++-
 awscli/customizations/commands.py           |  35 +++++-
 awscli/customizations/configure.py          | 110 +++++++++++++++++--
 tests/unit/customizations/test_configure.py | 115 +++++++++++++++++++-
 4 files changed, 257 insertions(+), 17 deletions(-)

diff --git a/awscli/argparser.py b/awscli/argparser.py
index cb2a09562411..751a1404bdc3 100644
--- a/awscli/argparser.py
+++ b/awscli/argparser.py
@@ -94,18 +94,26 @@ class ArgTableArgParser(CLIArgParser):
     """CLI arg parser based on an argument table."""
     Usage = ("aws [options] <command> <subcommand> [parameters]")
 
-    def __init__(self, argument_table):
+    def __init__(self, argument_table, command_table=None):
+        # command_table is an optional subcommand_table.  If it's passed
+        # in, then we'l update the argparse to parse a 'subcommand' argument
+        # and populate the choices field with the command table keys.
         super(ArgTableArgParser, self).__init__(
             formatter_class=self.Formatter,
             add_help=False,
             usage=self.Usage,
             conflict_handler='resolve')
-        self._build(argument_table)
+        if command_table is None:
+            command_table = {}
+        self._build(argument_table, command_table)
 
-    def _build(self, argument_table):
+    def _build(self, argument_table, command_table):
         for arg_name in argument_table:
             argument = argument_table[arg_name]
             argument.add_to_parser(self)
+        if command_table:
+            self.add_argument('subcommand', choices=list(command_table.keys()),
+                              nargs='?')
 
     def parse_known_args(self, args, namespace=None):
         if len(args) == 1 and args[0] == 'help':
diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py
index 3de0ffffb28f..7517c3f5b935 100644
--- a/awscli/customizations/commands.py
+++ b/awscli/customizations/commands.py
@@ -1,5 +1,7 @@
 import bcdoc.docevents
 
+from botocore.compat import OrderedDict
+
 from awscli.clidocs import CLIDocumentEventHandler
 from awscli.argparser import ArgTableArgParser
 from awscli.clidriver import CLICommand
@@ -43,6 +45,15 @@ class BasicCommand(CLICommand):
     #      'action': 'store', 'choices': ['a', 'b', 'c']},
     # ]
     ARG_TABLE = []
+    # If you want the command to have subcommands, you can provide a list of
+    # dicts.  We use a list here because we want to allow a user to provide
+    # the order they want to use for subcommands.
+    # SUBCOMMANDS = [
+    #     {'name': 'subcommand1', 'command_class': SubcommandClass},
+    #     {'name': 'subcommand2', 'command_class': SubcommandClass2},
+    # ]
+    # The command_class must subclass from ``BasicCommand``.
+    SUBCOMMANDS = []
 
     # At this point, the only other thing you have to implement is a _run_main
     # method (see the method for more information).
@@ -54,12 +65,17 @@ def __call__(self, args, parsed_globals):
         # args is the remaining unparsed args.
         # We might be able to parse these args so we need to create
         # an arg parser and parse them.
-        parser = ArgTableArgParser(self.arg_table)
-        parsed_args = parser.parse_args(args)
+        subcommand_table = self._build_subcommand_table()
+        parser = ArgTableArgParser(self.arg_table, subcommand_table)
+        parsed_args, remaining = parser.parse_known_args(args)
         if hasattr(parsed_args, 'help'):
             self._display_help(parsed_args, parsed_globals)
-        else:
+        elif getattr(parsed_args, 'subcommand', None) is None:
+            # No subcommand was specified was call the main
+            # function for this top level command.
             self._run_main(parsed_args, parsed_globals)
+        else:
+            subcommand_table[parsed_args.subcommand](remaining, parsed_globals)
 
     def _run_main(self, parsed_args, parsed_globals):
         # Subclasses should implement this method.
@@ -70,7 +86,18 @@ def _run_main(self, parsed_args, parsed_globals):
         # provided as the 'dest' key.  Otherwise they default to the
         # 'name' key.  For example: ARG_TABLE[0] = {"name": "foo-arg", ...}
         # can be accessed by ``parsed_args.foo_arg``.
-        raise NotImlementedError("_run_main")
+        raise NotImplementedError("_run_main")
+
+    def _build_subcommand_table(self):
+        subcommand_table = OrderedDict()
+        for subcommand in self.SUBCOMMANDS:
+            subcommand_name = subcommand['name']
+            subcommand_class = subcommand['command_class']
+            subcommand_table[subcommand_name] = subcommand_class(self._session)
+        self._session.emit('building-command-table.%s' % self.NAME,
+                           command_table=subcommand_table,
+                           session=self._session)
+        return subcommand_table
 
     def _display_help(self, parsed_args, parsed_globals):
         help_command = self.create_help_command()
diff --git a/awscli/customizations/configure.py b/awscli/customizations/configure.py
index e4b55c32c13a..c6f78790db7f 100644
--- a/awscli/customizations/configure.py
+++ b/awscli/customizations/configure.py
@@ -12,6 +12,7 @@
 # language governing permissions and limitations under the License.
 import os
 import re
+import sys
 import logging
 
 import six
@@ -27,6 +28,7 @@
 
 
 logger = logging.getLogger(__name__)
+NOT_SET = '<not set>'
 
 
 def register_configure_cmd(cli):
@@ -34,14 +36,33 @@ def register_configure_cmd(cli):
                  ConfigureCommand.add_command)
 
 
+class ConfigValue(object):
+    def __init__(self, value, config_type, config_variable):
+        self.value = value
+        self.config_type = config_type
+        self.config_variable = config_variable
+
+    def mask_value(self):
+        if self.value is NOT_SET:
+            return
+        self.value = _mask_value(self.value)
+
+
 class SectionNotFoundError(Exception):
     pass
 
 
+def _mask_value(current_value):
+    if current_value is None:
+        return 'None'
+    else:
+        return ('*' * 16) +  current_value[-4:]
+
+
 class InteractivePrompter(object):
     def get_value(self, current_value, config_name, prompt_text=''):
         if config_name in ('aws_access_key_id', 'aws_secret_access_key'):
-            current_value = self._mask_value(current_value)
+            current_value = _mask_value(current_value)
         response = raw_input("%s [%s]: " % (prompt_text, current_value))
         if not response:
             # If the user hits enter, we return a value of None
@@ -50,12 +71,6 @@ def get_value(self, current_value, config_name, prompt_text=''):
             response = None
         return response
 
-    def _mask_value(self, current_value):
-        if current_value is None:
-            return 'None'
-        else:
-            return ('*' * 16) +  current_value[-4:]
-
 
 class ConfigFileWriter(object):
     SECTION_REGEX = re.compile(r'\[(?P<header>[^]]+)\]')
@@ -154,6 +169,84 @@ def _matches_section(self, match, section_name):
         return unquoted_match
 
 
+class ConfigureListCommand(BasicCommand):
+    NAME = 'list'
+
+    def __init__(self, session, stream=sys.stdout):
+        super(ConfigureListCommand, self).__init__(session)
+        self._stream = stream
+
+    def _run_main(self, args, parsed_globals):
+        self._display_config_value(ConfigValue('Value', 'Type', 'Location'),
+                                   'Name')
+        self._display_config_value(ConfigValue('-----', '----', '--------'),
+                                   '----')
+
+        if self._session.profile is not None:
+            profile = ConfigValue(self._session.profile, 'manual',
+                                  '--profile')
+        else:
+            profile = self._lookup_config('profile')
+        self._display_config_value(profile, 'profile')
+
+        access_key, secret_key = self._lookup_credentials()
+        self._display_config_value(access_key, 'access_key')
+        self._display_config_value(secret_key, 'secret_key')
+
+        region = self._lookup_config('region')
+        self._display_config_value(region, 'region')
+
+    def _display_config_value(self, config_value, config_name):
+        self._stream.write('%10s %24s %16s    %s\n' % (
+            config_name, config_value.value, config_value.config_type,
+            config_value.config_variable))
+
+    def _lookup_credentials(self):
+        # First try it with _lookup_config.  It's possible
+        # that we don't find credentials this way (for example,
+        # if we're using an IAM role).
+        access_key = self._lookup_config('access_key')
+        if access_key.value is not NOT_SET:
+            secret_key = self._lookup_config('secret_key')
+            access_key.mask_value()
+            secret_key.mask_value()
+            return access_key, secret_key
+        else:
+            # Otherwise we can try to use get_credentials().
+            # This includes a few more lookup locations
+            # (IAM roles, some of the legacy configs, etc.)
+            credentials = self._session.get_credentials()
+            if credentials is None:
+                no_config = ConfigValue(NOT_SET, None, None)
+                return no_config, no_config
+            else:
+                # For the ConfigValue, we don't track down the
+                # config_variable because that info is not
+                # visible from botocore.credentials.  I think
+                # the credentials.method is sufficient to show
+                # where the credentials are coming from.
+                access_key = ConfigValue(credentials.access_key,
+                                        credentials.method, '')
+                secret_key = ConfigValue(credentials.secret_key,
+                                        credentials.method, '')
+                access_key.mask_value()
+                secret_key.mask_value()
+                return access_key, secret_key
+
+    def _lookup_config(self, name):
+        # First try to look up the variable in the env.
+        value = self._session.get_variable(name, methods=('env',))
+        if value is not None:
+            return ConfigValue(value, 'env', self._session.env_vars[name][1])
+        # Then try to look up the variable in the config file.
+        value = self._session.get_variable(name, methods=('config',))
+        if value is not None:
+            return ConfigValue(value, 'config_file',
+                               self._session.get_variable('config_file'))
+        else:
+            return ConfigValue(NOT_SET, None, None)
+
+
 class ConfigureCommand(BasicCommand):
     NAME = 'configure'
     DESCRIPTION = (
@@ -189,6 +282,9 @@ class ConfigureCommand(BasicCommand):
         '    Default region name [us-west-1]: us-west-2\n'
         '    Default output format [None]:\n'
     )
+    SUBCOMMANDS = [
+        {'name': 'list', 'command_class': ConfigureListCommand}
+    ]
 
     # If you want to add new values to prompt, update this list here.
     VALUES_TO_PROMPT = [
diff --git a/tests/unit/customizations/test_configure.py b/tests/unit/customizations/test_configure.py
index 9a4d4201d208..2c52adf0b407 100644
--- a/tests/unit/customizations/test_configure.py
+++ b/tests/unit/customizations/test_configure.py
@@ -18,6 +18,7 @@
 from tests import BaseAWSHelpOutputTest
 
 import mock
+from six import StringIO
 from botocore.exceptions import ProfileNotFound
 
 from awscli.customizations import configure
@@ -46,10 +47,23 @@ def get_value(self, current_value, config_name, prompt_text=''):
 
 
 class FakeSession(object):
-    def __init__(self, variables, profile_does_not_exist=False):
-        self.variables = variables
+    def __init__(self, all_variables, profile_does_not_exist=False,
+                 config_file_vars=None, environment_vars=None,
+                 credentials=None):
+        self.variables = all_variables
         self.profile_does_not_exist = profile_does_not_exist
         self.config = {}
+        if config_file_vars is None:
+            config_file_vars = {}
+        self.config_file_vars = config_file_vars
+        if environment_vars is None:
+            environment_vars = {}
+        self.environment_vars = environment_vars
+        self._credentials = credentials
+        self.profile = None
+
+    def get_credentials(self):
+        return self._credentials
 
     def get_config(self):
         if self.profile_does_not_exist:
@@ -59,7 +73,16 @@ def get_config(self):
     def get_variable(self, name, methods=None):
         if self.profile_does_not_exist and not name == 'config_file':
             raise ProfileNotFound(profile='foo')
-        return self.variables.get(name)
+        if methods is not None:
+            if 'env' in methods:
+                return self.environment_vars.get(name)
+            elif 'config' in methods:
+                return self.config_file_vars.get(name)
+        else:
+            return self.variables.get(name)
+
+    def emit(self, event_name, **kwargs):
+        pass
 
 
 class TestConfigureCommand(unittest.TestCase):
@@ -422,3 +445,89 @@ def test_profile_with_multiple_spaces(self):
             'foo = newvalue\n'
             'bar = 1\n'
         )
+
+
+class TestConfigureListCommand(unittest.TestCase):
+    def setUp(self):
+        pass
+
+    def test_configure_list_command_nothing_set(self):
+        # Test the case where the user only wants to change a single_value.
+        session = FakeSession(all_variables={'config_file': '/config/location'})
+        stream = StringIO()
+        self.configure_list = configure.ConfigureListCommand(session, stream)
+        self.configure_list(args=[], parsed_globals=None)
+        rendered = stream.getvalue()
+        self.assertRegexpMatches(rendered, 'profile\s+<not set>')
+        self.assertRegexpMatches(rendered, 'access_key\s+<not set>')
+        self.assertRegexpMatches(rendered, 'secret_key\s+<not set>')
+        self.assertRegexpMatches(rendered, 'region\s+<not set>')
+
+    def test_configure_from_env(self):
+        env_vars = {
+            'profile': 'myprofilename'
+        }
+        session = FakeSession(
+            all_variables={'config_file': '/config/location'},
+            environment_vars=env_vars)
+        session.env_vars = {'profile': (None, "PROFILE_ENV_VAR")}
+        stream = StringIO()
+        self.configure_list = configure.ConfigureListCommand(session, stream)
+        self.configure_list(args=[], parsed_globals=None)
+        rendered = stream.getvalue()
+        self.assertRegexpMatches(
+            rendered, 'profile\s+myprofilename\s+env\s+PROFILE_ENV_VAR')
+
+    def test_configure_from_config_file(self):
+        config_file_vars = {
+            'region': 'us-west-2'
+        }
+        session = FakeSession(
+            all_variables={'config_file': '/config/location'},
+            config_file_vars=config_file_vars)
+        session.env_vars = {'region': ('region', "AWS_REGION")}
+        stream = StringIO()
+        self.configure_list = configure.ConfigureListCommand(session, stream)
+        self.configure_list(args=[], parsed_globals=None)
+        rendered = stream.getvalue()
+        self.assertRegexpMatches(
+            rendered, 'region\s+us-west-2\s+config_file\s+/config/location')
+
+    def test_configure_from_multiple_sources(self):
+        # Here the profile is from an env var, the
+        # region is from the config file, and the credentials
+        # are from an iam-role.
+        env_vars = {
+            'profile': 'myprofilename'
+        }
+        config_file_vars = {
+            'region': 'us-west-2'
+        }
+        credentials = mock.Mock()
+        credentials.access_key = 'access_key'
+        credentials.secret_key = 'secret_key'
+        credentials.method = 'iam-role'
+        session = FakeSession(
+            all_variables={'config_file': '/config/location'},
+            environment_vars=env_vars,
+            config_file_vars=config_file_vars,
+            credentials=credentials)
+        session.env_vars = {'region': ('region', 'AWS_REGION'),
+                            'profile': ('profile', 'AWS_DEFAULT_PROFILE')}
+        stream = StringIO()
+        self.configure_list = configure.ConfigureListCommand(session, stream)
+        self.configure_list(args=[], parsed_globals=None)
+        rendered = stream.getvalue()
+        # The profile came from an env var.
+        self.assertRegexpMatches(
+            rendered, 'profile\s+myprofilename\s+env\s+AWS_DEFAULT_PROFILE')
+        # The region came from the config file.
+        self.assertRegexpMatches(
+            rendered, 'region\s+us-west-2\s+config_file\s+/config/location')
+        # The credentials came from an IAM role.  Note how we're
+        # also checking that the access_key/secret_key are masked
+        # with '*' chars except for the last 4 chars.
+        self.assertRegexpMatches(
+            rendered, r'access_key\s+\*+_key\s+iam-role')
+        self.assertRegexpMatches(
+            rendered, r'secret_key\s+\*+_key\s+iam-role')

From 83574d2369a9a9c2cd9643587c58ca56c95fa978 Mon Sep 17 00:00:00 2001
From: James Saryerwinnie <js@jamesls.com>
Date: Tue, 26 Nov 2013 10:39:11 -0800
Subject: [PATCH 3/4] Add docs for the aws configure list command

---
 awscli/customizations/configure.py | 24 ++++++++++++++++++++++++
 1 file changed, 24 insertions(+)

diff --git a/awscli/customizations/configure.py b/awscli/customizations/configure.py
index c6f78790db7f..812b09ad1956 100644
--- a/awscli/customizations/configure.py
+++ b/awscli/customizations/configure.py
@@ -171,6 +171,30 @@ def _matches_section(self, match, section_name):
 
 class ConfigureListCommand(BasicCommand):
     NAME = 'list'
+    DESCRIPTION = (
+        'List the AWS CLI configuration data.  This command will '
+        'show you the current configuration data.  For each configuration '
+        'item, it will show you the value, where the configuration value '
+        'was retrieved, and the configuration variable name.  For example, '
+        'if you provide the AWS region in an environment variable, this '
+        'command will show you the name of the region you\'ve configured, '
+        'it will tell you that this value came from an environment '
+        'variable, and it will tell you the name of the environment '
+        'variable.\n'
+    )
+    SYNOPSIS = ('aws configure list [--profile profile-name]')
+    EXAMPLES = (
+        'To show your current configuration values::\n'
+        '\n'
+        '  $ aws configure list\n'
+        '        Name                    Value             Type    Location\n'
+        '        ----                    -----             ----    --------\n'
+        '     profile                <not set>             None    None\n'
+        '  access_key     ****************ABCD      config_file    ~/.aws/config\n'
+        '  secret_key     ****************ABCD      config_file    ~/.aws/config\n'
+        '      region                us-west-2              env    AWS_DEFAULT_REGION\n'
+        '\n'
+    )
 
     def __init__(self, session, stream=sys.stdout):
         super(ConfigureListCommand, self).__init__(session)

From 89c1551d6598993b2980160ec1d3311ae84a107c Mon Sep 17 00:00:00 2001
From: James Saryerwinnie <js@jamesls.com>
Date: Tue, 26 Nov 2013 13:17:34 -0800
Subject: [PATCH 4/4] Correct grammar/typos in comments

---
 awscli/argparser.py               | 2 +-
 awscli/customizations/commands.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/awscli/argparser.py b/awscli/argparser.py
index 751a1404bdc3..389d0b1d5c16 100644
--- a/awscli/argparser.py
+++ b/awscli/argparser.py
@@ -96,7 +96,7 @@ class ArgTableArgParser(CLIArgParser):
 
     def __init__(self, argument_table, command_table=None):
         # command_table is an optional subcommand_table.  If it's passed
-        # in, then we'l update the argparse to parse a 'subcommand' argument
+        # in, then we'll update the argparse to parse a 'subcommand' argument
         # and populate the choices field with the command table keys.
         super(ArgTableArgParser, self).__init__(
             formatter_class=self.Formatter,
diff --git a/awscli/customizations/commands.py b/awscli/customizations/commands.py
index 7517c3f5b935..afddc25f1beb 100644
--- a/awscli/customizations/commands.py
+++ b/awscli/customizations/commands.py
@@ -71,7 +71,7 @@ def __call__(self, args, parsed_globals):
         if hasattr(parsed_args, 'help'):
             self._display_help(parsed_args, parsed_globals)
         elif getattr(parsed_args, 'subcommand', None) is None:
-            # No subcommand was specified was call the main
+            # No subcommand was specified so call the main
             # function for this top level command.
             self._run_main(parsed_args, parsed_globals)
         else: