Repository: audreyr/cookiecutter (python)

Issue:
Empty hook file causes cryptic error
If you have a pre_gen_project.sh or a post_gen_project.sh file with no data in it, cookiecutter fails with an unhelpful traceback.

```
Traceback (most recent call last):
  File "/usr/local/bin/cookiecutter", line 11, in <module>
    sys.exit(main())
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 716, in __call__
    return self.main(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 696, in main
    rv = self.invoke(ctx)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 889, in invoke
    return ctx.invoke(self.callback, **ctx.params)
  File "/usr/local/lib/python2.7/site-packages/click/core.py", line 534, in invoke
    return callback(*args, **kwargs)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/cli.py", line 100, in main
    config_file=user_config
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/main.py", line 140, in cookiecutter
    output_dir=output_dir
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/generate.py", line 273, in generate_files
    _run_hook_from_repo_dir(repo_dir, 'pre_gen_project', project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/generate.py", line 232, in _run_hook_from_repo_dir
    run_hook(hook_name, project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 116, in run_hook
    run_script_with_context(script, project_dir, context)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 101, in run_script_with_context
    run_script(temp.name, cwd)
  File "/usr/local/lib/python2.7/site-packages/cookiecutter/hooks.py", line 73, in run_script
    cwd=cwd
  File "/usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 656, in __init__
    _cleanup()
  File "/usr/local/Cellar/python/2.7.10_2/Frameworks/Python.framework/Versions/2.7/lib/python2.7/subprocess.py", line 1335, in _execute_child
    raise child_exception
OSError: [Errno 8] Exec format error
```


Pull request description:
Handle empty hook file or other OSErrors
Took a guess at the right way to test based on the existing tests. Let me know if anything needs to be changed!

Fixes #632.


Patch:
diff --git a/.travis.yml b/.travis.yml
index 6f80a54..ecd23db 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -2,30 +2,20 @@
 
 sudo: false
 language: python
-
-matrix:
-    include:
-      - python: 2.7
-        env: TOX_ENV=py27
-      - python: 3.3
-        env: TOX_ENV=py33
-      - python: 3.4
-        env: TOX_ENV=py34
-      - python: 3.5
-        env: TOX_ENV=py35
-      - python: nightly
-        env: TOX_ENV=py36
-      - python: pypy
-        env: TOX_ENV=pypy
-      - python: 3.5
-        env: TOX_ENV=flake8
+python: 3.5
+env:
+    - TOX_ENV=py27
+    - TOX_ENV=py33
+    - TOX_ENV=py34
+    - TOX_ENV=py35
+    - TOX_ENV=pypy
+    - TOX_ENV=flake8
 
 script: tox -e $TOX_ENV
 
 install:
     - pip install tox
 
-
 after_success:
     # Report coverage results to codecov.io
     # and export tox environment variables
diff --git a/README.rst b/README.rst
index a2d2cc2..0f98004 100644
--- a/README.rst
+++ b/README.rst
@@ -32,7 +32,7 @@ Cookiecutter
 A command-line utility that creates projects from **cookiecutters** (project
 templates), e.g. creating a Python package project from a Python package project template.
 
-* Documentation: https://cookiecutter.readthedocs.io
+* Documentation: http://cookiecutter.rtfd.org
 * GitHub: https://github.com/audreyr/cookiecutter
 * Free software: BSD license
 * PyPI: https://pypi.python.org/pypi/cookiecutter
@@ -349,7 +349,7 @@ Python
 .. _`cookiecutter-python-app`: https://github.com/mdklatt/cookiecutter-python-app
 .. _`morepath-cookiecutter`: https://github.com/morepath/morepath-cookiecutter
 .. _`Springerle/hovercraft-slides`: https://github.com/Springerle/hovercraft-slides
-.. _`Hovercraft!`: https://hovercraft.readthedocs.io/
+.. _`Hovercraft!`: https://hovercraft.readthedocs.org/
 .. _`cookiecutter-snakemake-analysis-pipeline`: https://github.com/xguse/cookiecutter-snakemake-analysis-pipeline
 .. _`Snakemake`: https://bitbucket.org/snakemake/snakemake/wiki/Home
 
diff --git a/cookiecutter/hooks.py b/cookiecutter/hooks.py
index 254bc2e..630d7b1 100644
--- a/cookiecutter/hooks.py
+++ b/cookiecutter/hooks.py
@@ -8,6 +8,7 @@ cookiecutter.hooks
 Functions for discovering and executing various cookiecutter hooks.
 """
 
+import errno
 import io
 import logging
 import os
@@ -65,15 +66,23 @@ def run_script(script_path, cwd='.'):
 
     utils.make_executable(script_path)
 
-    proc = subprocess.Popen(
-        script_command,
-        shell=run_thru_shell,
-        cwd=cwd
-    )
-    exit_status = proc.wait()
-    if exit_status != EXIT_SUCCESS:
+    try:
+        proc = subprocess.Popen(
+            script_command,
+            shell=run_thru_shell,
+            cwd=cwd
+        )
+        exit_status = proc.wait()
+        if exit_status != EXIT_SUCCESS:
+            raise FailedHookException(
+                "Hook script failed (exit status: %d)" % exit_status)
+    except OSError as oe:
+        if oe.errno == errno.ENOEXEC:
+            raise FailedHookException(
+                "Hook script failed, might be an "
+                "empty file or missing a shebang")
         raise FailedHookException(
-            "Hook script failed (exit status: %d)" % exit_status)
+            "Hook script failed (error: %s)" % oe)
 
 
 def run_script_with_context(script_path, cwd, context):
diff --git a/tox.ini b/tox.ini
index b984fb1..c02c519 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,9 +1,6 @@
 [tox]
 envlist = py27, py33, py34, py35, pypy, flake8
 
-[tox:travis]
-nightly = py36
-
 [testenv]
 passenv = LC_ALL, LANG, HOME
 commands = py.test --cov=cookiecutter {posargs:tests}


Test patch:
diff --git a/tests/test-shellhooks-empty/hooks/pre_gen_project.sh b/tests/test-shellhooks-empty/hooks/pre_gen_project.sh
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst b/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst
new file mode 100644
index 0000000..8c02abb
--- /dev/null
+++ b/tests/test-shellhooks-empty/input{{cookiecutter.shellhooks}}/README.rst
@@ -0,0 +1,1 @@
+Empty, just like the hooks we are testing.
diff --git a/tests/test_generate_hooks.py b/tests/test_generate_hooks.py
index 7586c78..4d966ab 100644
--- a/tests/test_generate_hooks.py
+++ b/tests/test_generate_hooks.py
@@ -13,6 +13,7 @@ TestHooks.test_run_shell_hooks
 """
 
 from __future__ import unicode_literals
+import errno
 import os
 import sys
 import stat
@@ -20,6 +21,7 @@ import pytest
 
 from cookiecutter import generate
 from cookiecutter import utils
+from cookiecutter.exceptions import FailedHookException
 
 
 @pytest.fixture(scope='function')
@@ -74,6 +76,46 @@ def test_run_python_hooks_cwd():
     assert os.path.exists('inputpyhooks/python_post.txt')
 
 
+@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
+def test_empty_hooks():
+
+    # OSError.errno=8 is not thrown on Windows when the script is emtpy
+    # because it always runs through shell instead of needing a shebang.
+
+    if not sys.platform.startswith('win'):
+        with pytest.raises(FailedHookException) as excinfo:
+            generate.generate_files(
+                context={
+                    'cookiecutter': {'shellhooks': 'shellhooks'}
+                },
+                repo_dir='tests/test-shellhooks-empty/',
+                overwrite_if_exists=True
+            )
+        assert 'shebang' in str(excinfo.value)
+
+
+@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
+def test_oserror_hooks(mocker):
+
+    message = 'Out of memory'
+
+    err = OSError(message)
+    err.errno = errno.ENOMEM
+
+    prompt = mocker.patch('subprocess.Popen')
+    prompt.side_effect = err
+
+    with pytest.raises(FailedHookException) as excinfo:
+        generate.generate_files(
+            context={
+                'cookiecutter': {'shellhooks': 'shellhooks'}
+            },
+            repo_dir='tests/test-shellhooks-empty/',
+            overwrite_if_exists=True
+        )
+    assert message in str(excinfo.value)
+
+
 def make_test_repo(name):
     hooks = os.path.join(name, 'hooks')
     template = os.path.join(name, 'input{{cookiecutter.shellhooks}}')