Repository: ansible/ansible-lint

Issue:
How do you exclude third party roles in requirements.yml
How do you exclude a third party roles, or any role for that matter. I have tried

I have tried
```exclude_paths:
  - ./roles/nexus
  - requirements.yml
  - compliance-playbook.yml
  - RHEL7-CIS
  - ./roles/RHEL7-CIS
  - ./.ansible/roles/RHEL7-CIS
  - ~/.ansible/roles/RHEL7-CIS
  - hardened-core.yml
  - site.yml
  - meta
  - ./roles/java```

I just want to exclude RHEL7-CIS and nothing seems to prevent it from running. I have even excluded files referencing it. I am running this as a pre-commit run -a with an exclude: .*RHEL.* even. I can not get RHEL7-CIS to stop being checked. And even some of the roles i have excluded like java continue to have files picked up. 


Discussion:
Yea, I am running into this as well, trying to throw this into CI and running into the situation where each playbook book is checking associated roles. Was hoping to lint them separately.
This seems to be a dupe of https://github.com/ansible/ansible-lint/issues/322

Resolving patch:
diff --git a/lib/ansiblelint/__init__.py b/lib/ansiblelint/__init__.py
--- a/lib/ansiblelint/__init__.py
+++ b/lib/ansiblelint/__init__.py
@@ -140,8 +140,13 @@ def matchyaml(self, file, text):
 
 class RulesCollection(object):
 
-    def __init__(self):
+    def __init__(self, rulesdirs=None):
+        if rulesdirs is None:
+            rulesdirs = []
+        self.rulesdirs = ansiblelint.utils.expand_paths_vars(rulesdirs)
         self.rules = []
+        for rulesdir in self.rulesdirs:
+            self.extend(ansiblelint.utils.load_plugins(rulesdir))
 
     def register(self, obj):
         self.rules.append(obj)
@@ -193,12 +198,6 @@ def listtags(self):
             results.append("{0} {1}".format(tag, tags[tag]))
         return "\n".join(results)
 
-    @classmethod
-    def create_from_directory(cls, rulesdir):
-        result = cls()
-        result.rules = ansiblelint.utils.load_plugins(os.path.expanduser(rulesdir))
-        return result
-
 
 class Match(object):
 
@@ -239,7 +238,7 @@ def __init__(self, rules, playbook, tags, skip_list, exclude_paths,
     def _update_exclude_paths(self, exclude_paths):
         if exclude_paths:
             # These will be (potentially) relative paths
-            paths = [s.strip() for s in exclude_paths]
+            paths = ansiblelint.utils.expand_paths_vars(exclude_paths)
             # Since ansiblelint.utils.find_children returns absolute paths,
             # and the list of files we create in `Runner.run` can contain both
             # relative and absolute paths, we need to cover both bases.
diff --git a/lib/ansiblelint/__main__.py b/lib/ansiblelint/__main__.py
--- a/lib/ansiblelint/__main__.py
+++ b/lib/ansiblelint/__main__.py
@@ -157,10 +157,7 @@ def main():
         rulesdirs = options.rulesdir + [default_rulesdir]
     else:
         rulesdirs = options.rulesdir or [default_rulesdir]
-
-    rules = RulesCollection()
-    for rulesdir in rulesdirs:
-        rules.extend(RulesCollection.create_from_directory(rulesdir))
+    rules = RulesCollection(rulesdirs)
 
     if options.listrules:
         print(rules)
diff --git a/lib/ansiblelint/utils.py b/lib/ansiblelint/utils.py
--- a/lib/ansiblelint/utils.py
+++ b/lib/ansiblelint/utils.py
@@ -861,3 +861,17 @@ def get_playbooks_and_roles(options=None):
         print('Found playbooks: ' + ' '.join(playbooks))
 
     return role_dirs + playbooks
+
+
+def expand_path_vars(path):
+    """Expand the environment or ~ variables in a path string."""
+    path = path.strip()
+    path = os.path.expanduser(path)
+    path = os.path.expandvars(path)
+    return path
+
+
+def expand_paths_vars(paths):
+    """Expand the environment or ~ variables in a list."""
+    paths = [expand_path_vars(p) for p in paths]
+    return paths


Test patch:
diff --git a/test/TestDependenciesInMeta.py b/test/TestDependenciesInMeta.py
--- a/test/TestDependenciesInMeta.py
+++ b/test/TestDependenciesInMeta.py
@@ -8,7 +8,7 @@ class TestDependenciesInMeta(unittest.TestCase):
 
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
     def test_bitbucket_in_meta_dependency_is_ok(self):
         filename = 'test/dependency-in-meta/bitbucket.yml'
diff --git a/test/TestImportIncludeRole.py b/test/TestImportIncludeRole.py
--- a/test/TestImportIncludeRole.py
+++ b/test/TestImportIncludeRole.py
@@ -49,7 +49,7 @@
 class TestImportIncludeRole(unittest.TestCase):
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
         # make dir and write role tasks to import or include
         self.play_root = tempfile.mkdtemp()
diff --git a/test/TestPretaskIncludePlaybook.py b/test/TestPretaskIncludePlaybook.py
--- a/test/TestPretaskIncludePlaybook.py
+++ b/test/TestPretaskIncludePlaybook.py
@@ -8,7 +8,7 @@ class TestTaskIncludes(unittest.TestCase):
 
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
     def test_pre_task_include_playbook(self):
         filename = 'test/playbook-include/playbook_pre.yml'
diff --git a/test/TestRuleProperties.py b/test/TestRuleProperties.py
--- a/test/TestRuleProperties.py
+++ b/test/TestRuleProperties.py
@@ -8,7 +8,7 @@ class TestAlwaysRun(unittest.TestCase):
 
     def setUp(self):
         self.collection.extend(
-            RulesCollection.create_from_directory(default_rulesdir)
+            RulesCollection([default_rulesdir])
         )
 
     def test_serverity_valid(self):
diff --git a/test/TestRulesCollection.py b/test/TestRulesCollection.py
--- a/test/TestRulesCollection.py
+++ b/test/TestRulesCollection.py
@@ -20,6 +20,7 @@
 
 import collections
 import unittest
+import os
 
 from ansiblelint import RulesCollection
 
@@ -31,7 +32,7 @@ class TestRulesCollection(unittest.TestCase):
     bracketsmatchtestfile = dict(path='test/bracketsmatchtest.yml', type='playbook')
 
     def setUp(self):
-        self.rules = RulesCollection.create_from_directory('./test/rules')
+        self.rules = RulesCollection(['./test/rules'])
 
     def test_load_collection_from_directory(self):
         self.assertEqual(len(self.rules), 2)
@@ -75,6 +76,19 @@ def test_skip_non_existent_id(self):
         self.assertEqual(len(matches), 3)
 
     def test_no_duplicate_rule_ids(self):
-        real_rules = RulesCollection.create_from_directory('./lib/ansiblelint/rules')
+        real_rules = RulesCollection(['./lib/ansiblelint/rules'])
         rule_ids = [rule.id for rule in real_rules]
         self.assertEqual([x for x, y in collections.Counter(rule_ids).items() if y > 1], [])
+
+
+def test_rulesdir_var_expansion(monkeypatch):
+    test_path = '/test/path'
+    monkeypatch.setenv('TEST_PATH', test_path)
+    test_rulesdirs = ['$TEST_PATH']
+    expansion_rules = RulesCollection(test_rulesdirs)
+    assert test_path in expansion_rules.rulesdirs
+
+
+def test_rulesdir_user_expansion():
+    expansion_rules = RulesCollection(['~'])
+    assert os.path.expanduser('~') in expansion_rules.rulesdirs
diff --git a/test/TestRunner.py b/test/TestRunner.py
--- a/test/TestRunner.py
+++ b/test/TestRunner.py
@@ -22,7 +22,7 @@
 import unittest
 
 import ansiblelint
-from ansiblelint import Runner, RulesCollection
+from ansiblelint import default_rulesdir, Runner, RulesCollection
 import ansiblelint.formatters
 
 
@@ -30,7 +30,7 @@ class TestRule(unittest.TestCase):
 
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
     def test_runner_count(self):
         filename = 'test/nomatchestest.yml'
@@ -118,3 +118,20 @@ def test_files_not_scanned_twice(self):
         run2 = runner.run()
 
         assert ((len(run1) + len(run2)) == 1)
+
+
+def test_runner_exclude_var_expansion(monkeypatch):
+    rules = RulesCollection([default_rulesdir])
+    filename = 'example/lots_of_warnings.yml'
+    monkeypatch.setenv('EXCLUDE_PATH', filename)
+    excludes = ['$EXCLUDE_PATH']
+    runner = Runner(rules, filename, [], [], excludes)
+    assert filename in runner.exclude_paths
+
+
+def test_runner_exclude_user_expansion():
+    rules = RulesCollection([default_rulesdir])
+    filename = 'example/lots_of_warnings.yml'
+    excludes = ['~']
+    runner = Runner(rules, filename, [], [], excludes)
+    assert os.path.expanduser('~') in runner.exclude_paths
diff --git a/test/TestSkipImportPlaybook.py b/test/TestSkipImportPlaybook.py
--- a/test/TestSkipImportPlaybook.py
+++ b/test/TestSkipImportPlaybook.py
@@ -27,7 +27,7 @@
 class TestSkipBeforeImport(unittest.TestCase):
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
         # make dir and write role tasks to import or include
         self.play_root = tempfile.mkdtemp()
diff --git a/test/TestSkipInsideYaml.py b/test/TestSkipInsideYaml.py
--- a/test/TestSkipInsideYaml.py
+++ b/test/TestSkipInsideYaml.py
@@ -90,7 +90,7 @@
 
 class TestSkipInsideYaml(unittest.TestCase):
     rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-    collection = RulesCollection.create_from_directory(rulesdir)
+    collection = RulesCollection([rulesdir])
 
     def setUp(self):
         self.runner = RunFromText(self.collection)
diff --git a/test/TestSkipPlaybookItems.py b/test/TestSkipPlaybookItems.py
--- a/test/TestSkipPlaybookItems.py
+++ b/test/TestSkipPlaybookItems.py
@@ -91,7 +91,7 @@
 
 class TestSkipPlaybookItems(unittest.TestCase):
     rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-    collection = RulesCollection.create_from_directory(rulesdir)
+    collection = RulesCollection([rulesdir])
 
     def setUp(self):
         self.runner = RunFromText(self.collection)
diff --git a/test/TestTaskIncludes.py b/test/TestTaskIncludes.py
--- a/test/TestTaskIncludes.py
+++ b/test/TestTaskIncludes.py
@@ -8,7 +8,7 @@ class TestTaskIncludes(unittest.TestCase):
 
     def setUp(self):
         rulesdir = os.path.join('lib', 'ansiblelint', 'rules')
-        self.rules = RulesCollection.create_from_directory(rulesdir)
+        self.rules = RulesCollection([rulesdir])
 
     def test_block_included_tasks(self):
         filename = 'test/blockincludes.yml'
diff --git a/test/TestUtils.py b/test/TestUtils.py
--- a/test/TestUtils.py
+++ b/test/TestUtils.py
@@ -21,6 +21,7 @@
 # THE SOFTWARE.
 
 import unittest
+import os
 try:
     from pathlib import Path
 except ImportError:
@@ -154,3 +155,17 @@ def test_normpath_with_string(self):
         self.assertEqual(
             utils.normpath("a/b/../"),
             "a")
+
+
+def test_expand_path_vars(monkeypatch):
+    test_path = '/test/path'
+    monkeypatch.setenv('TEST_PATH', test_path)
+    assert utils.expand_path_vars('~') == os.path.expanduser('~')
+    assert utils.expand_path_vars('$TEST_PATH') == test_path
+
+
+def test_expand_paths_vars(monkeypatch):
+    test_path = '/test/path'
+    monkeypatch.setenv('TEST_PATH', test_path)
+    assert utils.expand_paths_vars(['~']) == [os.path.expanduser('~')]
+    assert utils.expand_paths_vars(['$TEST_PATH']) == [test_path]