Repository: aws/aws-cli

Issue:
Profile does not return region or output format if it contains a SPACE character
The AWS CLI tool allows you to create a profile name with a space character in it, and writes the values to the credentials and config file as expected. However when getting that config using the quoted profile name, it doesn't return the region or default output format.

Setting the profile credentials and config:

    $ aws configure --profile 'Foo Bar'
    AWS Access Key ID [None]: foo
    AWS Secret Access Key [None]: bar
    Default region name [None]: eu-west-1
    Default output format [None]: json

    $ cat .aws/config
    [profile Foo Bar]
    output = json
    region = eu-west-1

    $ cat .aws/credentials
    [Foo Bar]
    aws_access_key_id = foo
    aws_secret_access_key = bar


Checking the profile credentials and config:

    $ aws configure --profile 'Foo Bar'
    AWS Access Key ID [****************foo]:
    AWS Secret Access Key [****************bar]:
    Default region name [None]:
    Default output format [None]:

CLI commands then prompt for a region when using the profile name. The **credentials** do still work, and are retrieved from the profile, but the **config** ,e.g. the region, has to be specified on the command line still:

    $ aws autoscaling describe-auto-scaling-groups --auto-scaling-group-names group_name --profile 'Foo Bar'
    You must specify a region. You can also configure your region by running "aws configure".

When performing the same steps above with a profile name without a space character (Foo_Bar), everything works as expected.


Discussion:
So after some debugging. Essentially the `configure` function for profile needs to check if the profile name has spaces and if so add single quotes around them. First off actual config loading happens in botocore and uses `shlex.split` on the profile section name:

https://github.com/boto/botocore/blob/develop/botocore/configloader.py#L254

To get the name of the profile. Unfortunately, this splits on whitespace which leads to your issue:

```
>>> import shlex
>>> test = 'profile Foo Bar'
>>> shlex.split(test)
['profile', 'Foo', 'Bar']
```

 Now given the following profile:

```
[profile 'Foo Bar']
output = json
region = eu-west-1
```

If I run this through the python interactive shell (botocore parses config files through `configparser`):

```
>>> config = configparser.RawConfigParser()
>>> config.read('config.ini')
['config.ini']
>>> config.sections()
["profile 'Foo Bar'"]
>>> shlex.split(config.sections()[0])
['profile', 'Foo Bar']
```

The quoting ensure that the profile name even with spaces comes up properly.
Thanks for looking into this.  So I think a fix for this would be when the CLI is writing out a new profile, check if the profile name has spaces and if so, surround the name in quotes.

I suppose we could always quote the profile name, but I'd prefer a smaller change.
@jamesls I'm thinking of doing the quoting route. It's a mostly small fix with the main part to deal with being testing. This week might end up a bit busy, but at the worst case I might be able to pull together a pull request over the weekend if no one gets to it by then. 

Resolving patch:
diff --git a/awscli/customizations/configure/__init__.py b/awscli/customizations/configure/__init__.py
--- a/awscli/customizations/configure/__init__.py
+++ b/awscli/customizations/configure/__init__.py
@@ -10,8 +10,12 @@
 # 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.
+import string
+from botocore.vendored.six.moves import shlex_quote
+
 NOT_SET = '<not set>'
 PREDEFINED_SECTION_NAMES = ('preview', 'plugins')
+_WHITESPACE = ' \t'
 
 
 class ConfigValue(object):
@@ -36,3 +40,10 @@ def mask_value(current_value):
         return 'None'
     else:
         return ('*' * 16) + current_value[-4:]
+
+
+def profile_to_section(profile_name):
+    """Converts a profile name to a section header to be used in the config."""
+    if any(c in _WHITESPACE for c in profile_name):
+        profile_name = shlex_quote(profile_name)
+    return 'profile %s' % profile_name
diff --git a/awscli/customizations/configure/configure.py b/awscli/customizations/configure/configure.py
--- a/awscli/customizations/configure/configure.py
+++ b/awscli/customizations/configure/configure.py
@@ -23,7 +23,7 @@
 from awscli.customizations.configure.list import ConfigureListCommand
 from awscli.customizations.configure.writer import ConfigFileWriter
 
-from . import mask_value
+from . import mask_value, profile_to_section
 
 
 logger = logging.getLogger(__name__)
@@ -115,8 +115,8 @@ def _run_main(self, parsed_args, parsed_globals):
             self._write_out_creds_file_values(new_values,
                                               parsed_globals.profile)
             if parsed_globals.profile is not None:
-                new_values['__section__'] = (
-                    'profile %s' % parsed_globals.profile)
+                section = profile_to_section(parsed_globals.profile)
+                new_values['__section__'] = section
             self._config_writer.update_config(new_values, config_filename)
 
     def _write_out_creds_file_values(self, new_values, profile_name):
diff --git a/awscli/customizations/configure/set.py b/awscli/customizations/configure/set.py
--- a/awscli/customizations/configure/set.py
+++ b/awscli/customizations/configure/set.py
@@ -15,7 +15,7 @@
 from awscli.customizations.commands import BasicCommand
 from awscli.customizations.configure.writer import ConfigFileWriter
 
-from . import PREDEFINED_SECTION_NAMES
+from . import PREDEFINED_SECTION_NAMES, profile_to_section
 
 
 class ConfigureSetCommand(BasicCommand):
@@ -60,7 +60,7 @@ def _run_main(self, args, parsed_globals):
             # profile (or leave it as the 'default' section if
             # no profile is set).
             if self._session.profile is not None:
-                section = 'profile %s' % self._session.profile
+                section = profile_to_section(self._session.profile)
         else:
             # First figure out if it's been scoped to a profile.
             parts = varname.split('.')
@@ -71,14 +71,14 @@ def _run_main(self, args, parsed_globals):
                     remaining = parts[1:]
                 else:
                     # [profile, profile_name, ...]
-                    section = "profile %s" % parts[1]
+                    section = profile_to_section(parts[1])
                     remaining = parts[2:]
                 varname = remaining[0]
                 if len(remaining) == 2:
                     value = {remaining[1]: value}
             elif parts[0] not in PREDEFINED_SECTION_NAMES:
                 if self._session.profile is not None:
-                    section = 'profile %s' % self._session.profile
+                    section = profile_to_section(self._session.profile)
                 else:
                     profile_name = self._session.get_config_variable('profile')
                     if profile_name is not None:


Test patch:
diff --git a/tests/integration/customizations/test_configure.py b/tests/integration/customizations/test_configure.py
--- a/tests/integration/customizations/test_configure.py
+++ b/tests/integration/customizations/test_configure.py
@@ -188,6 +188,30 @@ def test_set_with_updating_value(self):
             '[default]\n'
             'region = us-west-1\n', self.get_config_file_contents())
 
+    def test_set_with_profile_spaces(self):
+        p = aws('configure set region us-west-1 --profile "test with spaces"',
+                env_vars=self.env_vars)
+        self.assert_no_errors(p)
+        self.assertEqual(
+            '[profile \'test with spaces\']\n'
+            'region = us-west-1\n', self.get_config_file_contents())
+
+    def test_set_with_profile_unknown_nested_key(self):
+        p = aws('configure set un.known us-west-1 --profile "space test"',
+                env_vars=self.env_vars)
+        self.assert_no_errors(p)
+        self.assertEqual(
+            '[profile \'space test\']\n'
+            'un =\n    known = us-west-1\n', self.get_config_file_contents())
+
+    def test_set_with_profile_spaces_scoped(self):
+        p = aws('configure set profile."test with spaces".region us-west-1',
+                env_vars=self.env_vars)
+        self.assert_no_errors(p)
+        self.assertEqual(
+            '[profile \'test with spaces\']\n'
+            'region = us-west-1\n', self.get_config_file_contents())
+
     def test_set_with_profile(self):
         p = aws('configure set region us-west-1 --profile testing',
                 env_vars=self.env_vars)
diff --git a/tests/unit/customizations/configure/test_configure.py b/tests/unit/customizations/configure/test_configure.py
--- a/tests/unit/customizations/configure/test_configure.py
+++ b/tests/unit/customizations/configure/test_configure.py
@@ -14,6 +14,7 @@
 import mock
 
 from awscli.customizations.configure import configure, ConfigValue, NOT_SET
+from awscli.customizations.configure import profile_to_section
 from awscli.testutils import unittest
 from awscli.compat import six
 
@@ -238,6 +239,29 @@ def test_dont_mask_unset_value(self):
         self.assertEqual(no_config.value, NOT_SET)
 
 
+class TestProfileToSection(unittest.TestCase):
+
+    def test_normal_profile(self):
+        profile = 'my-profile'
+        section = profile_to_section(profile)
+        self.assertEqual('profile my-profile', section)
+
+    def test_profile_with_spaces(self):
+        profile = 'my spaced profile'
+        section = profile_to_section(profile)
+        self.assertEqual('profile \'my spaced profile\'', section)
+
+    def test_profile_with_tab(self):
+        profile = 'tab\ts'
+        section = profile_to_section(profile)
+        self.assertEqual('profile \'tab\ts\'', section)
+
+    def test_profile_with_consecutive_spaces(self):
+        profile = '    '
+        section = profile_to_section(profile)
+        self.assertEqual('profile \'    \'', section)
+
+
 class PrecannedPrompter(object):
 
     def __init__(self, value):