diff --git a/README.rst b/README.rst
index 5393adf..1f5dac1 100644
--- a/README.rst
+++ b/README.rst
@@ -119,6 +119,27 @@ or for a whole block:
consectetur : adipiscing elit
# yamllint enable
+Specific files can be ignored (totally or for some rules only) using a
+``.gitignore``-style pattern:
+
+.. code:: yaml
+
+ # For all rules
+ ignore: |
+ *.dont-lint-me.yaml
+ /bin/
+ !/bin/*.lint-me-anyway.yaml
+
+ rules:
+ key-duplicates:
+ ignore: |
+ generated
+ *.template.yaml
+ trailing-spaces:
+ ignore: |
+ *.ignore-trailing-spaces.yaml
+ /ascii-art/*
+
`Read more in the complete documentation! `_
License
diff --git a/docs/configuration.rst b/docs/configuration.rst
index 44aad82..0815789 100644
--- a/docs/configuration.rst
+++ b/docs/configuration.rst
@@ -114,3 +114,57 @@ return code will be:
* ``0`` if no errors or warnings occur
* ``1`` if one or more errors occur
* ``2`` if no errors occur, but one or more warnings occur
+
+Ignoring paths
+--------------
+
+It is possible to exclude specific files or directories, so that the linter
+doesn't process them.
+
+You can either totally ignore files (they won't be looked at):
+
+.. code-block:: yaml
+
+ extends: default
+
+ ignore: |
+ /this/specific/file.yaml
+ /all/this/directory/
+ *.template.yaml
+
+or ignore paths only for specific rules:
+
+.. code-block:: yaml
+
+ extends: default
+
+ rules:
+ trailing-spaces:
+ ignore: |
+ /this-file-has-trailing-spaces-but-it-is-OK.yaml
+ /generated/*.yaml
+
+Note that this ``.gitignore``-style path pattern allows complex path
+exclusion/inclusion, see the `pathspec README file
+`_ for more details.
+Here is a more complex example:
+
+.. code-block:: yaml
+
+ # For all rules
+ ignore: |
+ *.dont-lint-me.yaml
+ /bin/
+ !/bin/*.lint-me-anyway.yaml
+
+ extends: default
+
+ rules:
+ key-duplicates:
+ ignore: |
+ generated
+ *.template.yaml
+ trailing-spaces:
+ ignore: |
+ *.ignore-trailing-spaces.yaml
+ /ascii-art/*
diff --git a/setup.py b/setup.py
index 8a865f4..9c54fcd 100644
--- a/setup.py
+++ b/setup.py
@@ -46,7 +46,7 @@ setup(
entry_points={'console_scripts': ['yamllint=yamllint.cli:run']},
package_data={'yamllint': ['conf/*.yaml'],
'tests': ['yaml-1.2-spec-examples/*']},
- install_requires=['pyyaml'],
+ install_requires=['pathspec', 'pyyaml'],
tests_require=['nose'],
test_suite='nose.collector',
)
diff --git a/tests/test_config.py b/tests/test_config.py
index 5f06ef1..d6485cc 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -14,10 +14,20 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
+try:
+ from cStringIO import StringIO
+except ImportError:
+ from io import StringIO
+import os
+import shutil
+import sys
import unittest
+from yamllint import cli
from yamllint import config
+from tests.common import build_temp_workspace
+
class SimpleConfigTestCase(unittest.TestCase):
def test_parse_config(self):
@@ -30,7 +40,7 @@ class SimpleConfigTestCase(unittest.TestCase):
self.assertEqual(new.rules['colons']['max-spaces-before'], 0)
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
- self.assertEqual(len(new.enabled_rules()), 1)
+ self.assertEqual(len(new.enabled_rules(None)), 1)
def test_invalid_conf(self):
with self.assertRaises(config.YamlLintConfigError):
@@ -170,7 +180,7 @@ class ExtendedConfigTestCase(unittest.TestCase):
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
- self.assertEqual(len(new.enabled_rules()), 2)
+ self.assertEqual(len(new.enabled_rules(None)), 2)
def test_extend_remove_rule(self):
old = config.YamlLintConfig('rules:\n'
@@ -187,7 +197,7 @@ class ExtendedConfigTestCase(unittest.TestCase):
self.assertEqual(new.rules['colons'], False)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
- self.assertEqual(len(new.enabled_rules()), 1)
+ self.assertEqual(len(new.enabled_rules(None)), 1)
def test_extend_edit_rule(self):
old = config.YamlLintConfig('rules:\n'
@@ -207,7 +217,7 @@ class ExtendedConfigTestCase(unittest.TestCase):
self.assertEqual(new.rules['colons']['max-spaces-after'], 4)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
- self.assertEqual(len(new.enabled_rules()), 2)
+ self.assertEqual(len(new.enabled_rules(None)), 2)
def test_extend_reenable_rule(self):
old = config.YamlLintConfig('rules:\n'
@@ -225,7 +235,7 @@ class ExtendedConfigTestCase(unittest.TestCase):
self.assertEqual(new.rules['colons']['max-spaces-after'], 1)
self.assertEqual(new.rules['hyphens']['max-spaces-after'], 2)
- self.assertEqual(len(new.enabled_rules()), 2)
+ self.assertEqual(len(new.enabled_rules(None)), 2)
class ExtendedLibraryConfigTestCase(unittest.TestCase):
@@ -270,3 +280,93 @@ class ExtendedLibraryConfigTestCase(unittest.TestCase):
self.assertEqual(sorted(new.rules.keys()), sorted(old.rules.keys()))
for rule in new.rules:
self.assertEqual(new.rules[rule], old.rules[rule])
+
+
+class IgnorePathConfigTestCase(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ super(IgnorePathConfigTestCase, cls).setUpClass()
+
+ bad_yaml = ('---\n'
+ '- key: val1\n'
+ ' key: val2\n'
+ '- trailing space \n'
+ '- lonely hyphen\n')
+
+ cls.wd = build_temp_workspace({
+ 'bin/file.lint-me-anyway.yaml': bad_yaml,
+ 'bin/file.yaml': bad_yaml,
+ 'file-at-root.yaml': bad_yaml,
+ 'file.dont-lint-me.yaml': bad_yaml,
+ 'ign-dup/file.yaml': bad_yaml,
+ 'ign-dup/sub/dir/file.yaml': bad_yaml,
+ 'ign-trail/file.yaml': bad_yaml,
+ 'include/ign-dup/sub/dir/file.yaml': bad_yaml,
+ 's/s/ign-trail/file.yaml': bad_yaml,
+ 's/s/ign-trail/s/s/file.yaml': bad_yaml,
+ 's/s/ign-trail/s/s/file2.lint-me-anyway.yaml': bad_yaml,
+
+ '.yamllint': 'ignore: |\n'
+ ' *.dont-lint-me.yaml\n'
+ ' /bin/\n'
+ ' !/bin/*.lint-me-anyway.yaml\n'
+ '\n'
+ 'extends: default\n'
+ '\n'
+ 'rules:\n'
+ ' key-duplicates:\n'
+ ' ignore: |\n'
+ ' /ign-dup\n'
+ ' trailing-spaces:\n'
+ ' ignore: |\n'
+ ' ign-trail\n'
+ ' !*.lint-me-anyway.yaml\n',
+ })
+
+ cls.backup_wd = os.getcwd()
+ os.chdir(cls.wd)
+
+ @classmethod
+ def tearDownClass(cls):
+ super(IgnorePathConfigTestCase, cls).tearDownClass()
+
+ os.chdir(cls.backup_wd)
+
+ shutil.rmtree(cls.wd)
+
+ def test_run_with_ignored_path(self):
+ sys.stdout = StringIO()
+ with self.assertRaises(SystemExit):
+ cli.run(('-f', 'parsable', '.'))
+
+ out = sys.stdout.getvalue()
+ out = '\n'.join(sorted(out.splitlines()))
+
+ keydup = '[error] duplication of key "key" in mapping (key-duplicates)'
+ trailing = '[error] trailing spaces (trailing-spaces)'
+ hyphen = '[error] too many spaces after hyphen (hyphens)'
+
+ self.assertEqual(out, '\n'.join((
+ './bin/file.lint-me-anyway.yaml:3:3: ' + keydup,
+ './bin/file.lint-me-anyway.yaml:4:17: ' + trailing,
+ './bin/file.lint-me-anyway.yaml:5:5: ' + hyphen,
+ './file-at-root.yaml:3:3: ' + keydup,
+ './file-at-root.yaml:4:17: ' + trailing,
+ './file-at-root.yaml:5:5: ' + hyphen,
+ './ign-dup/file.yaml:4:17: ' + trailing,
+ './ign-dup/file.yaml:5:5: ' + hyphen,
+ './ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './ign-trail/file.yaml:3:3: ' + keydup,
+ './ign-trail/file.yaml:5:5: ' + hyphen,
+ './include/ign-dup/sub/dir/file.yaml:3:3: ' + keydup,
+ './include/ign-dup/sub/dir/file.yaml:4:17: ' + trailing,
+ './include/ign-dup/sub/dir/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file.yaml:5:5: ' + hyphen,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:3:3: ' + keydup,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:4:17: ' + trailing,
+ './s/s/ign-trail/s/s/file2.lint-me-anyway.yaml:5:5: ' + hyphen,
+ )))
diff --git a/yamllint/cli.py b/yamllint/cli.py
index f207b88..418cf35 100644
--- a/yamllint/cli.py
+++ b/yamllint/cli.py
@@ -126,10 +126,11 @@ def run(argv=None):
max_level = 0
for file in find_files_recursively(args.files):
+ filepath = file[2:] if file.startswith('./') else file
try:
first = True
with open(file) as f:
- for problem in linter.run(f, conf):
+ for problem in linter.run(f, conf, filepath):
if args.format == 'parsable':
print(Format.parsable(problem, file))
elif sys.stdout.isatty():
diff --git a/yamllint/config.py b/yamllint/config.py
index 48ea380..fb5a161 100644
--- a/yamllint/config.py
+++ b/yamllint/config.py
@@ -16,6 +16,7 @@
import os.path
+import pathspec
import yaml
import yamllint.rules
@@ -29,6 +30,8 @@ class YamlLintConfig(object):
def __init__(self, content=None, file=None):
assert (content is None) ^ (file is None)
+ self.ignore = None
+
if file is not None:
with open(file) as f:
content = f.read()
@@ -36,9 +39,14 @@ class YamlLintConfig(object):
self.parse(content)
self.validate()
- def enabled_rules(self):
+ def is_file_ignored(self, filepath):
+ return self.ignore and self.ignore.match_file(filepath)
+
+ def enabled_rules(self, filepath):
return [yamllint.rules.get(id) for id, val in self.rules.items()
- if val is not False]
+ if val is not False and (
+ filepath is None or 'ignore' not in val or
+ not val['ignore'].match_file(filepath))]
def extend(self, base_config):
assert isinstance(base_config, YamlLintConfig)
@@ -53,6 +61,9 @@ class YamlLintConfig(object):
self.rules = base_config.rules
+ if base_config.ignore is not None:
+ self.ignore = base_config.ignore
+
def parse(self, raw_content):
try:
conf = yaml.safe_load(raw_content)
@@ -73,6 +84,13 @@ class YamlLintConfig(object):
except Exception as e:
raise YamlLintConfigError('invalid config: %s' % e)
+ if 'ignore' in conf:
+ if type(conf['ignore']) != str:
+ raise YamlLintConfigError(
+ 'invalid config: ignore should be a list of patterns')
+ self.ignore = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'].splitlines())
+
def validate(self):
for id in self.rules:
try:
@@ -90,6 +108,14 @@ def validate_rule_conf(rule, conf):
conf = {}
if type(conf) == dict:
+ if ('ignore' in conf and
+ type(conf['ignore']) != pathspec.pathspec.PathSpec):
+ if type(conf['ignore']) != str:
+ raise YamlLintConfigError(
+ 'invalid config: ignore should be a list of patterns')
+ conf['ignore'] = pathspec.PathSpec.from_lines(
+ 'gitwildmatch', conf['ignore'].splitlines())
+
if 'level' not in conf:
conf['level'] = 'error'
elif conf['level'] not in ('error', 'warning'):
@@ -98,7 +124,7 @@ def validate_rule_conf(rule, conf):
options = getattr(rule, 'CONF', {})
for optkey in conf:
- if optkey == 'level':
+ if optkey in ('ignore', 'level'):
continue
if optkey not in options:
raise YamlLintConfigError(
diff --git a/yamllint/linter.py b/yamllint/linter.py
index 012bcbd..c8eff8d 100644
--- a/yamllint/linter.py
+++ b/yamllint/linter.py
@@ -63,8 +63,8 @@ class LintProblem(object):
return '%d:%d: %s' % (self.line, self.column, self.message)
-def get_cosmetic_problems(buffer, conf):
- rules = conf.enabled_rules()
+def get_cosmetic_problems(buffer, conf, filepath):
+ rules = conf.enabled_rules(filepath)
# Split token rules from line rules
token_rules = [r for r in rules if r.TYPE == 'token']
@@ -185,7 +185,7 @@ def get_syntax_error(buffer):
return problem
-def _run(buffer, conf):
+def _run(buffer, conf, filepath):
assert hasattr(buffer, '__getitem__'), \
'_run() argument must be a buffer, not a stream'
@@ -193,7 +193,7 @@ def _run(buffer, conf):
# right line
syntax_error = get_syntax_error(buffer)
- for problem in get_cosmetic_problems(buffer, conf):
+ for problem in get_cosmetic_problems(buffer, conf, filepath):
# Insert the syntax error (if any) at the right place...
if (syntax_error and syntax_error.line <= problem.line and
syntax_error.column <= problem.column):
@@ -215,7 +215,7 @@ def _run(buffer, conf):
yield syntax_error
-def run(input, conf):
+def run(input, conf, filepath=None):
"""Lints a YAML source.
Returns a generator of LintProblem objects.
@@ -223,11 +223,14 @@ def run(input, conf):
:param input: buffer, string or stream to read from
:param conf: yamllint configuration object
"""
+ if conf.is_file_ignored(filepath):
+ return ()
+
if type(input) in (type(b''), type(u'')): # compat with Python 2 & 3
- return _run(input, conf)
+ return _run(input, conf, filepath)
elif hasattr(input, 'read'): # Python 2's file or Python 3's io.IOBase
# We need to have everything in memory to parse correctly
content = input.read()
- return _run(content, conf)
+ return _run(content, conf, filepath)
else:
raise TypeError('input should be a string or a stream')