Repository: conan-io/conan

Issue:
[feature] cookiecutter style templates with conan new command
I would like to extend the concept introduced with  #5192 to include more than just the conanfile.py file. 

Basically, I would like to be able to call cookiecutter (or similar) from within Conan, so that I can generate package templates and include them in my conan config repo that I distribute to the rest of my team. 

As an example,  I could use https://github.com/AnticliMaxtic/conan_config 

``` 
$ conan config install --type git https://github.com/AnticliMaxtic/conan_config.git
$ conan new cpp_proj/0.0.1@AnticliMaxtic/stable -t cpp 
```

I obviously could just call 

```
$ cookiecutter ~/.conan/templates/cpp 
```
but the fewer tools I ask my developers to install and use explicitly, the better, even if I am directly calling cookiecutter from within conan. As a first step, the  

- [x] I've read the [CONTRIBUTING guide](https://github.com/conan-io/conan/blob/develop/.github/CONTRIBUTING.md).



Discussion:
Would it be just an extension of the file template to a folder? Or is there something more in the cookicutter besides jinja replacements? In that case it should be relatively easy to implement. If you know some python, would you like to give it a try? 
@memsharded , I would like to implement this feature. I.e. ability to specify directory with multiple files as a template.

The main question is about CLI (since naming things is hard). Currently I'm thinking of smth like this:

`conan new package_name [-md, --template-directory TEMPLATE_DIR]`. (since currently it's `-m, --template` for template file)

TEMPLATE_DIR must contain `conanfile.py` and might have any other arbitrary files.
jinja is applied to all files in that directory.


Another thing that I want to do, (don't how to implement in this feature tho) is to somehow replace dir names in template. E.g.
- template
  - conanfile.py
  - Makefile
  - src
  - include
    - `{{package_name}}`
Further improvements for `--template-dir`:
`--overwrite-policy {overwrite, skip, ask}` - this would allow to convert existing non-conan projects.


Resolving patch:
diff --git a/conans/client/cmd/new.py b/conans/client/cmd/new.py
--- a/conans/client/cmd/new.py
+++ b/conans/client/cmd/new.py
@@ -3,6 +3,7 @@
 
 from jinja2 import Template
 
+from conans import __version__ as client_version
 from conans.client.cmd.new_ci import ci_get_files
 from conans.errors import ConanException
 from conans.model.ref import ConanFileReference
@@ -236,6 +237,34 @@ def test(self):
 
 """
 
+def _render_template(text, name, version, package_name):
+    context = {'name': name, 
+               'version': version, 
+               'package_name': package_name,
+               'conan_version': client_version}
+    t = Template(text, keep_trailing_newline=True)
+    return t.render(**context)
+
+def _get_files_from_template_dir(template_dir, name, version, package_name):
+    if (not os.path.isfile(os.path.join(template_dir, "conanfile.py")) 
+        and not os.path.isfile(os.path.join(template_dir, "conanfile.txt"))):
+        raise ConanException("Template is missing a recipe file: {}".format(template_dir))
+
+    files = []
+    for d, _, fs in os.walk(template_dir):
+        for f in fs:
+            rel_d = os.path.relpath(d, template_dir)
+            rel_f = os.path.join(rel_d, f)
+            files.append(rel_f)
+
+    out_files = dict()
+    for f in files:
+        f_path = os.path.join(template_dir, f)
+        rendered_path = _render_template(f, name=name, version=version, package_name=package_name)
+        rendered_file = _render_template(load(f_path), name=name, version=version, package_name=package_name)
+        out_files[rendered_path] = rendered_file
+
+    return out_files
 
 def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False, bare=False,
             visual_versions=None, linux_gcc_versions=None, linux_clang_versions=None,
@@ -264,9 +293,9 @@ def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False,
         raise ConanException("'pure_c' is incompatible with 'header' and 'sources'")
     if bare and (header or exports_sources):
         raise ConanException("'bare' is incompatible with 'header' and 'sources'")
-    if template and (header or exports_sources or bare):
-        raise ConanException("'template' argument incompatible with 'header', "
-                             "'sources', and 'bare'")
+    if template and (header or exports_sources or bare or pure_c):
+        raise ConanException("'template' is incompatible with 'header', "
+                             "'sources', 'pure-c' and 'bare'")
 
     if header:
         files = {"conanfile.py": conanfile_header.format(name=name, version=version,
@@ -281,14 +310,30 @@ def cmd_new(ref, header=False, pure_c=False, test=False, exports_sources=False,
         files = {"conanfile.py": conanfile_bare.format(name=name, version=version,
                                                        package_name=package_name)}
     elif template:
-        if not os.path.isabs(template):
-            template = os.path.join(cache.cache_folder, "templates", template)
-        if not os.path.isfile(template):
-            raise ConanException("Template doesn't exist: %s" % template)
-        conanfile_template = load(template)
-        t = Template(conanfile_template)
-        replaced = t.render(name=name, version=version, package_name=package_name)
-        files = {"conanfile.py": replaced}
+        is_file_template = os.path.basename(template).endswith('.py')
+        if is_file_template:
+            if not os.path.isabs(template):
+                # FIXME: Conan 2.0. The old path should be removed
+                old_path = os.path.join(cache.cache_folder, "templates", template)
+                new_path = os.path.join(cache.cache_folder, "templates", "command/new", template)
+                template = new_path if os.path.isfile(new_path) else old_path
+            if not os.path.isfile(template):
+                raise ConanException("Template doesn't exist: %s" % template)
+            replaced = _render_template(load(template), 
+                                        name=name, 
+                                        version=version, 
+                                        package_name=package_name)
+            files = {"conanfile.py": replaced}
+        else:
+            if not os.path.isabs(template):
+                template = os.path.join(cache.cache_folder, "templates", "command/new", template)
+            if not os.path.isdir(template):
+                raise ConanException("Template doesn't exist: {}".format(template))
+            template = os.path.normpath(template)
+            files = _get_files_from_template_dir(template_dir=template,
+                                                 name=name,
+                                                 version=version,
+                                                 package_name=package_name)
     else:
         files = {"conanfile.py": conanfile.format(name=name, version=version,
                                                   package_name=package_name)}
diff --git a/conans/client/command.py b/conans/client/command.py
--- a/conans/client/command.py
+++ b/conans/client/command.py
@@ -173,7 +173,7 @@ def new(self, *args):
                             help='Create the minimum package recipe, without build() method. '
                             'Useful in combination with "export-pkg" command')
         parser.add_argument("-m", "--template",
-                            help='Use the given template from the local cache for conanfile.py')
+                            help='Use the given template to generate a conan project')
         parser.add_argument("-cis", "--ci-shared", action='store_true',
                             default=False,
                             help='Package will have a "shared" option to be used in CI')


Test patch:
diff --git a/conans/test/functional/command/new_test.py b/conans/test/functional/command/new_test.py
--- a/conans/test/functional/command/new_test.py
+++ b/conans/test/functional/command/new_test.py
@@ -4,6 +4,7 @@
 
 from parameterized import parameterized
 
+from conans import __version__ as client_version
 from conans.test.utils.test_files import temp_folder
 from conans.test.utils.tools import TestClient
 from conans.tools import save
@@ -18,6 +19,7 @@ def template_test(self):
             class {{package_name}}Conan(ConanFile):
                 name = "{{name}}"
                 version = "{{version}}"
+                conan_version = "{{conan_version}}"
         """)
         save(os.path.join(client.cache_folder, "templates/mytemplate.py"), template1)
         client.run("new hello/0.1 --template=mytemplate.py")
@@ -25,6 +27,39 @@ class {{package_name}}Conan(ConanFile):
         self.assertIn("class HelloConan(ConanFile):", conanfile)
         self.assertIn('name = "hello"', conanfile)
         self.assertIn('version = "0.1"', conanfile)
+        self.assertIn('conan_version = "{}"'.format(client_version), conanfile)
+
+    def template_dir_test(self):
+        client = TestClient()
+        template_dir = "templates/command/new/t_dir"
+        template_recipe = textwrap.dedent("""
+            class {{package_name}}Conan(ConanFile):
+                name = "{{name}}"
+                version = "{{version}}"
+                conan_version = "{{conan_version}}"
+        """)
+        save(os.path.join(client.cache_folder, template_dir + "/conanfile.py"), template_recipe)
+
+        template_txt = textwrap.dedent("""
+            package_name={{package_name}}
+            name={{name}}
+            version={{version}}
+            conan_version={{conan_version}}
+        """)
+        save(os.path.join(client.cache_folder, template_dir + "/{{name}}/hello.txt"), template_txt)
+
+        client.run("new hello/0.1 --template=t_dir")
+        conanfile = client.load("conanfile.py")
+        self.assertIn("class HelloConan(ConanFile):", conanfile)
+        self.assertIn('name = "hello"', conanfile)
+        self.assertIn('version = "0.1"', conanfile)
+        self.assertIn('conan_version = "{}"'.format(client_version), conanfile)
+
+        hellotxt = client.load("hello/hello.txt")
+        self.assertIn("package_name=Hello", hellotxt)
+        self.assertIn("name=hello", hellotxt)
+        self.assertIn('version=0.1', hellotxt)
+        self.assertIn("conan_version={}".format(client_version), hellotxt)
 
     def template_test_package_test(self):
         client = TestClient()
@@ -57,8 +92,10 @@ def template_errors_test(self):
         client = TestClient()
         client.run("new hello/0.1 -m=mytemplate.py", assert_error=True)
         self.assertIn("ERROR: Template doesn't exist", client.out)
+        client.run("new hello/0.1 -m=mytemplate", assert_error=True)
+        self.assertIn("ERROR: Template doesn't exist", client.out)
         client.run("new hello/0.1 --template=mytemplate.py --bare", assert_error=True)
-        self.assertIn("ERROR: 'template' argument incompatible", client.out)
+        self.assertIn("ERROR: 'template' is incompatible", client.out)
         client.run("new hello/0.1 --template", assert_error=True)
         self.assertIn("ERROR: Exiting with code: 2", client.out)