Repository: pre-commit/pre-commit

Issue:
Not-found executable crashes framework
This was introduced with the new exe logic in 0.8.0

Here's a simple reproduction:

``` yaml
- repo: local
  hooks:
  - id: test
    name: test
    language: system
    entry: i-dont-exist-lol
    files: '\.py$'
```

```
$ pre-commit run --all-files
test.....................................................................An unexpected error has occurred: OSError: Executable i-dont-exist-lol not found
Check the log at ~/.pre-commit/pre-commit.log
```



Discussion:

Resolving patch:
diff --git a/pre_commit/parse_shebang.py b/pre_commit/parse_shebang.py
--- a/pre_commit/parse_shebang.py
+++ b/pre_commit/parse_shebang.py
@@ -12,6 +12,10 @@
 printable = frozenset(string.printable)
 
 
+class ExecutableNotFoundError(OSError):
+    pass
+
+
 def parse_bytesio(bytesio):
     """Parse the shebang from a file opened for reading binary."""
     if bytesio.read(2) != b'#!':
@@ -70,7 +74,9 @@ def normexe(orig_exe):
     if os.sep not in orig_exe:
         exe = find_executable(orig_exe)
         if exe is None:
-            raise OSError('Executable {0} not found'.format(orig_exe))
+            raise ExecutableNotFoundError(
+                'Executable `{0}` not found'.format(orig_exe),
+            )
         return exe
     else:
         return orig_exe
diff --git a/pre_commit/util.py b/pre_commit/util.py
--- a/pre_commit/util.py
+++ b/pre_commit/util.py
@@ -181,23 +181,26 @@ def cmd_output(*cmd, **kwargs):
         for key, value in kwargs.pop('env', {}).items()
     ) or None
 
-    cmd = parse_shebang.normalize_cmd(cmd)
-
-    popen_kwargs.update(kwargs)
-    proc = __popen(cmd, **popen_kwargs)
-    stdout, stderr = proc.communicate()
-    if encoding is not None and stdout is not None:
-        stdout = stdout.decode(encoding)
-    if encoding is not None and stderr is not None:
-        stderr = stderr.decode(encoding)
-    returncode = proc.returncode
+    try:
+        cmd = parse_shebang.normalize_cmd(cmd)
+    except parse_shebang.ExecutableNotFoundError as e:
+        returncode, stdout, stderr = (-1, e.args[0].encode('UTF-8'), b'')
+    else:
+        popen_kwargs.update(kwargs)
+        proc = __popen(cmd, **popen_kwargs)
+        stdout, stderr = proc.communicate()
+        if encoding is not None and stdout is not None:
+            stdout = stdout.decode(encoding)
+        if encoding is not None and stderr is not None:
+            stderr = stderr.decode(encoding)
+        returncode = proc.returncode
 
     if retcode is not None and retcode != returncode:
         raise CalledProcessError(
             returncode, cmd, retcode, output=(stdout, stderr),
         )
 
-    return proc.returncode, stdout, stderr
+    return returncode, stdout, stderr
 
 
 def rmtree(path):


Test patch:
diff --git a/testing/resources/not_found_exe/hooks.yaml b/testing/resources/not_found_exe/hooks.yaml
new file mode 100644
--- /dev/null
+++ b/testing/resources/not_found_exe/hooks.yaml
@@ -0,0 +1,5 @@
+-   id: not-found-exe
+    name: Not found exe
+    entry: i-dont-exist-lol
+    language: system
+    files: ''
diff --git a/tests/parse_shebang_test.py b/tests/parse_shebang_test.py
--- a/tests/parse_shebang_test.py
+++ b/tests/parse_shebang_test.py
@@ -108,7 +108,7 @@ def test_find_executable_path_ext(in_tmpdir):
 def test_normexe_does_not_exist():
     with pytest.raises(OSError) as excinfo:
         parse_shebang.normexe('i-dont-exist-lol')
-    assert excinfo.value.args == ('Executable i-dont-exist-lol not found',)
+    assert excinfo.value.args == ('Executable `i-dont-exist-lol` not found',)
 
 
 def test_normexe_already_full_path():
diff --git a/tests/repository_test.py b/tests/repository_test.py
--- a/tests/repository_test.py
+++ b/tests/repository_test.py
@@ -164,6 +164,16 @@ def test_system_hook_with_spaces(tempdir_factory, store):
     )
 
 
+@pytest.mark.integration
+def test_missing_executable(tempdir_factory, store):
+    _test_hook_repo(
+        tempdir_factory, store, 'not_found_exe',
+        'not-found-exe', ['/dev/null'],
+        b'Executable `i-dont-exist-lol` not found',
+        expected_return_code=1,
+    )
+
+
 @pytest.mark.integration
 def test_run_a_script_hook(tempdir_factory, store):
     _test_hook_repo(