Repository: pre-commit/pre-commit-hooks

Issue:
Check-yaml exception for Ansible Vault
Hi,

I've recently used the [embed encrypt variables](https://docs.ansible.com/ansible/latest/vault.html#encrypt-string-for-use-in-yaml) in my Ansible repos, and the check-yaml hook is failing because of the specific `!vault` usage.

```
Check Yaml...............................................................Failed
hookid: check-yaml

could not determine a constructor for the tag '!vault'
  in "inventories/group_vars/all.yml", line 62, column 13
```

Do you think it's possible to add this as an exception inside the hook (and make it pass for everyone who's using encypted variables in their playbooks) ?

Or should I add explicit exception inside my `.pre-commit-config.yaml`, for each files where I use those encrypted variables ?

Discussion:

Patch:
diff --git a/README.md b/README.md
index 8d21a68..41e9103 100644
--- a/README.md
+++ b/README.md
@@ -52,6 +52,11 @@ Add this to your `.pre-commit-config.yaml`
 - `check-yaml` - Attempts to load all yaml files to verify syntax.
     - `--allow-multiple-documents` - allow yaml files which use the
       [multi-document syntax](http://www.yaml.org/spec/1.2/spec.html#YAML)
+    - `--unsafe` - Instaed of loading the files, simply parse them for syntax.
+      A syntax-only check enables extensions and unsafe constructs which would
+      otherwise be forbidden.  Using this option removes all guarantees of
+      portability to other yaml implementations.
+      Implies `--allow-multiple-documents`.
 - `debug-statements` - Check for pdb / ipdb / pudb statements in code.
 - `detect-aws-credentials` - Checks for the existence of AWS secrets that you
   have set up with the AWS CLI.
diff --git a/pre_commit_hooks/check_yaml.py b/pre_commit_hooks/check_yaml.py
index e9bb8f0..9fbbd88 100644
--- a/pre_commit_hooks/check_yaml.py
+++ b/pre_commit_hooks/check_yaml.py
@@ -1,6 +1,7 @@
 from __future__ import print_function
 
 import argparse
+import collections
 import sys
 
 import yaml
@@ -11,24 +12,52 @@ except ImportError:  # pragma: no cover (no libyaml-dev / pypy)
     Loader = yaml.SafeLoader
 
 
+def _exhaust(gen):
+    for _ in gen:
+        pass
+
+
+def _parse_unsafe(*args, **kwargs):
+    _exhaust(yaml.parse(*args, **kwargs))
+
+
 def _load_all(*args, **kwargs):
-    # need to exhaust the generator
-    return tuple(yaml.load_all(*args, **kwargs))
+    _exhaust(yaml.load_all(*args, **kwargs))
+
+
+Key = collections.namedtuple('Key', ('multi', 'unsafe'))
+LOAD_FNS = {
+    Key(multi=False, unsafe=False): yaml.load,
+    Key(multi=False, unsafe=True): _parse_unsafe,
+    Key(multi=True, unsafe=False): _load_all,
+    Key(multi=True, unsafe=True): _parse_unsafe,
+}
 
 
 def check_yaml(argv=None):
     parser = argparse.ArgumentParser()
     parser.add_argument(
-        '-m', '--allow-multiple-documents', dest='yaml_load_fn',
-        action='store_const', const=_load_all, default=yaml.load,
+        '-m', '--multi', '--allow-multiple-documents', action='store_true',
+    )
+    parser.add_argument(
+        '--unsafe', action='store_true',
+        help=(
+            'Instead of loading the files, simply parse them for syntax.  '
+            'A syntax-only check enables extensions and unsafe contstructs '
+            'which would otherwise be forbidden.  Using this option removes '
+            'all guarantees of portability to other yaml implementations.  '
+            'Implies --allow-multiple-documents'
+        ),
     )
     parser.add_argument('filenames', nargs='*', help='Yaml filenames to check.')
     args = parser.parse_args(argv)
 
+    load_fn = LOAD_FNS[Key(multi=args.multi, unsafe=args.unsafe)]
+
     retval = 0
     for filename in args.filenames:
         try:
-            args.yaml_load_fn(open(filename), Loader=Loader)
+            load_fn(open(filename), Loader=Loader)
         except yaml.YAMLError as exc:
             print(exc)
             retval = 1


Test patch:
diff --git a/tests/check_yaml_test.py b/tests/check_yaml_test.py
index de3b383..aa357f1 100644
--- a/tests/check_yaml_test.py
+++ b/tests/check_yaml_test.py
@@ -22,7 +22,7 @@ def test_check_yaml_allow_multiple_documents(tmpdir):
     f = tmpdir.join('test.yaml')
     f.write('---\nfoo\n---\nbar\n')
 
-    # should failw without the setting
+    # should fail without the setting
     assert check_yaml((f.strpath,))
 
     # should pass when we allow multiple documents
@@ -33,3 +33,22 @@ def test_fails_even_with_allow_multiple_documents(tmpdir):
     f = tmpdir.join('test.yaml')
     f.write('[')
     assert check_yaml(('--allow-multiple-documents', f.strpath))
+
+
+def test_check_yaml_unsafe(tmpdir):
+    f = tmpdir.join('test.yaml')
+    f.write(
+        'some_foo: !vault |\n'
+        '    $ANSIBLE_VAULT;1.1;AES256\n'
+        '    deadbeefdeadbeefdeadbeef\n',
+    )
+    # should fail "safe" check
+    assert check_yaml((f.strpath,))
+    # should pass when we allow unsafe documents
+    assert not check_yaml(('--unsafe', f.strpath))
+
+
+def test_check_yaml_unsafe_still_fails_on_syntax_errors(tmpdir):
+    f = tmpdir.join('test.yaml')
+    f.write('[')
+    assert check_yaml(('--unsafe', f.strpath))