Repository: audreyr/cookiecutter (python)

Issue:
Idea: have a way to specify context via command line
Something like repeat arguments:
```
cookiecutter mytemplate -Cname=my-project -Cgithub-user=ionelmc
```

Or maybe the whole json?
```
cookiecutter mytemplate --context='{"name": "my-project", "github-user": "ionelmc"}'
```

Or variable arguments?
```
cookiecutter mytemplate --context-name=my-project --context-github-user=ionelmc
```





Pull request description:
This allows specifying context as arguments
Master rebased #400.
Fixes #389.

Allows specifying or overriding variables by adding `key=value` arguments after the template.

E.g.:

    [marca@marca-mac2 test-cookiecutter-supervisor]$ cookiecutter --no-input https://github.com/msabramo/cookiecutter-supervisor.git program_name=foobar
    Cloning into 'cookiecutter-supervisor'...
    remote: Counting objects: 16, done.
    remote: Compressing objects: 100% (13/13), done.
    remote: Total 16 (delta 5), reused 8 (delta 2), pack-reused 0
    Unpacking objects: 100% (16/16), done.
    Checking connectivity... done.

    [marca@marca-mac2 test-cookiecutter-supervisor]$ cat foobar/supervisor.conf
    [program:foobar]
    numprocs                 = 1
    process_name             = %(program_name)s%(process_num)02d
    user                     = foobaruser
    command                  = gunicorn --bind 0.0.0.0:$PORT --paste development.ini
    startsecs                = 30
    priority                 = 12
    redirect_stderr          = true
    autorestart              = true
    autostart                = true
    stopwaitsecs             = 10
    stopsignal               = QUIT
    directory                = /opt/webapp/foobar
    stdout_logfile           = /var/log/sm/foobar-app-out.log
    stdout_logfile_maxbytes  = 10MB
    stdout_logfile_backups   = 0
    stdout_capture_maxbytes  = 1MB
    stderr_logfile           = /var/log/sm/foobar-app-err.log
    stderr_logfile_maxbytes  = 10MB
    stderr_logfile_backups   = 0
    stderr_capture_maxbytes  = 1MB
    environment              = NEW_RELIC_CONFIG_FILE="/opt/webapp/foobar/etc/newrelic.ini"

Patch:
diff --git a/README.rst b/README.rst
index 15d86ad..114a352 100644
--- a/README.rst
+++ b/README.rst
@@ -136,6 +136,12 @@ Did someone say features?
 * Default context: specify key/value pairs that you want used as defaults
   whenever you generate a project
 
+* Inject extra context with command-line arguments:
+
+    .. code-block:: bash
+
+        $ cookiecutter --no-input gh:msabramo/cookiecutter-supervisor program_name=foobar startsecs=10
+
 * Direct access to the Cookiecutter API allows for injection of extra context.
 
 * Pre- and post-generate hooks: Python or shell scripts to run before or after
diff --git a/cookiecutter/cli.py b/cookiecutter/cli.py
index 3740769..237f5c4 100644
--- a/cookiecutter/cli.py
+++ b/cookiecutter/cli.py
@@ -38,9 +38,23 @@ def version_msg():
     return message.format(location, python_version)
 
 
+def validate_extra_context(ctx, param, value):
+    for s in value:
+        if '=' not in s:
+            raise click.BadParameter(
+                'EXTRA_CONTEXT should contain items of the form key=value; '
+                "'{}' doesn't match that form".format(s)
+            )
+
+    # Convert tuple -- e.g.: (u'program_name=foobar', u'startsecs=66')
+    # to dict -- e.g.: {'program_name': 'foobar', 'startsecs': '66'}
+    return dict(s.split('=', 1) for s in value) or None
+
+
 @click.command(context_settings=dict(help_option_names=[u'-h', u'--help']))
 @click.version_option(__version__, u'-V', u'--version', message=version_msg())
 @click.argument(u'template')
+@click.argument(u'extra_context', nargs=-1, callback=validate_extra_context)
 @click.option(
     u'--no-input', is_flag=True,
     help=u'Do not prompt for parameters and only use cookiecutter.json '
@@ -75,8 +89,8 @@ def version_msg():
     u'--default-config', is_flag=True,
     help=u'Do not load a config file. Use the defaults instead'
 )
-def main(template, no_input, checkout, verbose, replay, overwrite_if_exists,
-         output_dir, config_file, default_config):
+def main(template, extra_context, no_input, checkout, verbose, replay,
+         overwrite_if_exists, output_dir, config_file, default_config):
     """Create a project from a Cookiecutter project template (TEMPLATE)."""
     if verbose:
         logging.basicConfig(
@@ -101,6 +115,7 @@ def main(template, no_input, checkout, verbose, replay, overwrite_if_exists,
 
         cookiecutter(
             template, checkout, no_input,
+            extra_context=extra_context,
             replay=replay,
             overwrite_if_exists=overwrite_if_exists,
             output_dir=output_dir,


Test patch:
diff --git a/tests/fake-repo-pre/{{cookiecutter.repo_name}}/README.rst b/tests/fake-repo-pre/{{cookiecutter.repo_name}}/README.rst
index d97975c..0abd26e 100644
--- a/tests/fake-repo-pre/{{cookiecutter.repo_name}}/README.rst
+++ b/tests/fake-repo-pre/{{cookiecutter.repo_name}}/README.rst
@@ -2,4 +2,6 @@
 Fake Project
 ============
 
+Project name: **{{ cookiecutter.project_name }}**
+
 Blah!!!!
diff --git a/tests/test_cli.py b/tests/test_cli.py
index 301774e..b3dc767 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -52,6 +52,8 @@ def test_cli():
     result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input'])
     assert result.exit_code == 0
     assert os.path.isdir('fake-project')
+    with open(os.path.join('fake-project', 'README.rst')) as f:
+        assert 'Project name: **Fake Project**' in f.read()
 
 
 @pytest.mark.usefixtures('remove_fake_project_dir')
@@ -59,6 +61,8 @@ def test_cli_verbose():
     result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input', '-v'])
     assert result.exit_code == 0
     assert os.path.isdir('fake-project')
+    with open(os.path.join('fake-project', 'README.rst')) as f:
+        assert 'Project name: **Fake Project**' in f.read()
 
 
 @pytest.mark.usefixtures('remove_fake_project_dir')
@@ -82,7 +86,8 @@ def test_cli_replay(mocker):
         replay=True,
         overwrite_if_exists=False,
         output_dir='.',
-        config_file=config.USER_CONFIG_PATH
+        config_file=config.USER_CONFIG_PATH,
+        extra_context=None
     )
 
 
@@ -117,7 +122,8 @@ def test_cli_exit_on_noinput_and_replay(mocker):
         replay=True,
         overwrite_if_exists=False,
         output_dir='.',
-        config_file=config.USER_CONFIG_PATH
+        config_file=config.USER_CONFIG_PATH,
+        extra_context=None
     )
 
 
@@ -151,7 +157,8 @@ def test_run_cookiecutter_on_overwrite_if_exists_and_replay(
         replay=True,
         overwrite_if_exists=True,
         output_dir='.',
-        config_file=config.USER_CONFIG_PATH
+        config_file=config.USER_CONFIG_PATH,
+        extra_context=None
     )
 
 
@@ -206,7 +213,8 @@ def test_cli_output_dir(mocker, output_dir_flag, output_dir):
         replay=False,
         overwrite_if_exists=False,
         output_dir=output_dir,
-        config_file=config.USER_CONFIG_PATH
+        config_file=config.USER_CONFIG_PATH,
+        extra_context=None
     )
 
 
@@ -246,7 +254,8 @@ def test_user_config(mocker, user_config_path):
         replay=False,
         overwrite_if_exists=False,
         output_dir='.',
-        config_file=user_config_path
+        config_file=user_config_path,
+        extra_context=None
     )
 
 
@@ -271,7 +280,8 @@ def test_default_user_config_overwrite(mocker, user_config_path):
         replay=False,
         overwrite_if_exists=False,
         output_dir='.',
-        config_file=None
+        config_file=None,
+        extra_context=None
     )
 
 
@@ -294,7 +304,8 @@ def test_default_user_config(mocker):
         replay=False,
         overwrite_if_exists=False,
         output_dir='.',
-        config_file=None
+        config_file=None,
+        extra_context=None
     )
 
 
@@ -343,3 +354,22 @@ def test_echo_unknown_extension_error(tmpdir):
     assert result.exit_code == 1
 
     assert 'Unable to load extension: ' in result.output
+
+
+@pytest.mark.usefixtures('remove_fake_project_dir')
+def test_cli_extra_context():
+    result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input', '-v',
+                                  'project_name=Awesomez'])
+    assert result.exit_code == 0
+    assert os.path.isdir('fake-project')
+    with open(os.path.join('fake-project', 'README.rst')) as f:
+        assert 'Project name: **Awesomez**' in f.read()
+
+
+@pytest.mark.usefixtures('remove_fake_project_dir')
+def test_cli_extra_context_invalid_format():
+    result = runner.invoke(main, ['tests/fake-repo-pre/', '--no-input', '-v',
+                                  'ExtraContextWithNoEqualsSoInvalid'])
+    assert result.exit_code == 2
+    assert 'Error: Invalid value for "extra_context"' in result.output
+    assert 'should contain items of the form key=value' in result.output