Repository: aws-cloudformation/cfn-lint

Issue:
Flag to lint a directory hierarchy recursively
*cfn-lint version: (`cfn-lint --version`)*
0.8.2

*Description of issue.*
I would like to point the linter at a directory (/template) and have it scan files recursively, including sub-directories.

Please provide as much information as possible:
Currently you can only lint the contents of a directory (/template/*) -- you need to script linting recursively via an external tool.  I'd like to scan all templates at once.


Discussion:

Resolving patch:
diff --git a/setup.py b/setup.py
--- a/setup.py
+++ b/setup.py
@@ -48,10 +48,11 @@ def get_version(filename):
         'data/CloudSpecs/*.json',
         'data/AdditionalSpecs/*.json',
         'data/Serverless/*.json',
+        'data/CfnLintCli/config/schema.json'
     ]},
     packages=find_packages('src'),
     zip_safe=False,
-    install_requires=['pyyaml', 'six', 'requests', 'aws-sam-translator>=1.6.0', 'jsonpatch'],
+    install_requires=['pyyaml', 'six', 'requests', 'aws-sam-translator>=1.6.0', 'jsonpatch', 'jsonschema~=2.6.0', 'pathlib2'],
     python_requires='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*',
     entry_points={
         'console_scripts': [
diff --git a/src/cfnlint/config.py b/src/cfnlint/config.py
new file mode 100644
--- /dev/null
+++ b/src/cfnlint/config.py
@@ -0,0 +1,469 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+import sys
+import argparse
+import logging
+import glob
+import json
+import yaml
+import six
+import jsonschema
+from cfnlint.version import __version__
+try:  # pragma: no cover
+    from pathlib import Path
+except ImportError:  # pragma: no cover
+    from pathlib2 import Path
+
+LOGGER = logging.getLogger('cfnlint')
+
+
+def configure_logging(debug_logging):
+    """Setup Logging"""
+    ch = logging.StreamHandler()
+    ch.setLevel(logging.DEBUG)
+
+    if debug_logging:
+        LOGGER.setLevel(logging.DEBUG)
+    else:
+        LOGGER.setLevel(logging.INFO)
+    log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
+    ch.setFormatter(log_formatter)
+
+    # make sure all other log handlers are removed before adding it back
+    for handler in LOGGER.handlers:
+        LOGGER.removeHandler(handler)
+    LOGGER.addHandler(ch)
+
+
+class ConfigFileArgs(object):
+    """
+        Config File arguments.
+        Parses .cfnlintrc in the Home and Project folder.
+    """
+    file_args = {}
+
+    def __init__(self, schema=None):
+        # self.file_args = self.get_config_file_defaults()
+        self.__user_config_file = None
+        self.__project_config_file = None
+        self.file_args = {}
+        self.default_schema_file = Path(__file__).parent.joinpath('data/CfnLintCli/config/schema.json')
+        with self.default_schema_file.open() as f:
+            self.default_schema = json.load(f)
+        self.schema = self.default_schema if not schema else schema
+        self.load()
+
+    def _find_config(self):
+        """Looks up for user and project level config
+        Returns
+        -------
+        Tuple
+            (Path, Path)
+            Tuple with both configs and whether they were found
+        Example
+        -------
+            > user_config, project_config = self._find_config()
+        """
+        config_file_name = '.cfnlintrc'
+        self.__user_config_file = Path.home().joinpath(config_file_name)
+        self.__project_config_file = Path.cwd().joinpath(config_file_name)
+
+        user_config_path = ''
+        project_config_path = ''
+
+        if self._has_file(self.__user_config_file):
+            LOGGER.debug('Found User CFNLINTRC')
+            user_config_path = self.__user_config_file
+
+        if self._has_file(self.__project_config_file):
+            LOGGER.debug('Found Project level CFNLINTRC')
+            project_config_path = self.__project_config_file
+
+        return user_config_path, project_config_path
+
+    def _has_file(self, filename):
+        """Confirm whether file exists
+        Parameters
+        ----------
+        filename : str
+            Path to a file
+        Returns
+        -------
+        Boolean
+        """
+
+        return Path(filename).is_file()
+
+    def load(self):
+        """Load configuration file and expose as a dictionary
+        Returns
+        -------
+        Dict
+            CFLINTRC configuration
+        """
+
+        LOGGER.debug('Looking for CFLINTRC before attempting to load')
+        user_config, project_config = self._find_config()
+
+        user_config = self._read_config(user_config)
+        LOGGER.debug('Validating User CFNLINTRC')
+        self.validate_config(user_config, self.schema)
+
+        project_config = self._read_config(project_config)
+        LOGGER.debug('Validating Project CFNLINTRC')
+        self.validate_config(project_config, self.schema)
+
+        LOGGER.debug('User configuration loaded as')
+        LOGGER.debug('%s', user_config)
+        LOGGER.debug('Project configuration loaded as')
+        LOGGER.debug('%s', project_config)
+
+        LOGGER.debug('Merging configurations...')
+        self.file_args = self.merge_config(user_config, project_config)
+
+    def validate_config(self, config, schema):
+        """Validate configuration against schema
+        Parameters
+        ----------
+        config : dict
+            CFNLINTRC configuration
+        schema : dict
+            JSONSchema to validate against
+        Raises
+        -------
+        jsonschema.exceptions.ValidationError
+            Returned when cfnlintrc doesn't match schema provided
+        """
+        LOGGER.debug('Validating CFNLINTRC config with given JSONSchema')
+        LOGGER.debug('Schema used: %s', schema)
+        LOGGER.debug('Config used: %s', config)
+
+        jsonschema.validate(config, schema)
+        LOGGER.debug('CFNLINTRC looks valid!')
+
+    def merge_config(self, user_config, project_config):
+        """Merge project and user configuration into a single dictionary
+        Creates a new configuration with both configuration merged
+        it favours project level over user configuration if keys are duplicated
+        NOTE
+        ----
+            It takes any number of nested dicts
+            It overrides lists found in user_config with project_config
+        Parameters
+        ----------
+        user_config : Dict
+            User configuration (~/.cfnlintrc) found at user's home directory
+        project_config : Dict
+            Project configuration (.cfnlintrc) found at current directory
+        Returns
+        -------
+        Dict
+            Merged configuration
+        """
+        # Recursively override User config with Project config
+        for key in user_config:
+            if key in project_config:
+                # If both keys are the same, let's check whether they have nested keys
+                if isinstance(user_config[key], dict) and isinstance(project_config[key], dict):
+                    self.merge_config(user_config[key], project_config[key])
+                else:
+                    user_config[key] = project_config[key]
+                    LOGGER.debug('Overriding User\'s key %s with Project\'s specific value %s.', key, project_config[key])
+
+        # Project may have unique config we need to copy over too
+        # so that we can have user+project config available as one
+        for key in project_config:
+            if key not in user_config:
+                user_config[key] = project_config[key]
+
+        return user_config
+
+    def _read_config(self, config):
+        """Parse given YAML configuration
+        Returns
+        -------
+        Dict
+            Parsed YAML configuration as dictionary
+        """
+        config = Path(config)
+        config_template = None
+
+        if self._has_file(config):
+            LOGGER.debug('Parsing CFNLINTRC')
+            config_template = yaml.safe_load(config.read_text())
+
+        if not config_template:
+            config_template = {}
+
+        return config_template
+
+
+def comma_separated_arg(string):
+    """ Split a comma separated string """
+    return string.split(',')
+
+
+class CliArgs(object):
+    """ Base Args class"""
+    cli_args = {}
+
+    def __init__(self, cli_args):
+        self.parser = self.create_parser()
+        self.cli_args, _ = self.parser.parse_known_args(cli_args)
+
+    def create_parser(self):
+        """Do first round of parsing parameters to set options"""
+        class ArgumentParser(argparse.ArgumentParser):
+            """ Override Argument Parser so we can control the exit code"""
+            def error(self, message):
+                self.print_help(sys.stderr)
+                self.exit(32, '%s: error: %s\n' % (self.prog, message))
+
+        class ExtendAction(argparse.Action):
+            """Support argument types that are lists and can be specified multiple times."""
+            def __call__(self, parser, namespace, values, option_string=None):
+                items = getattr(namespace, self.dest)
+                items = [] if items is None else items
+                for value in values:
+                    if isinstance(value, list):
+                        items.extend(value)
+                    else:
+                        items.append(value)
+                setattr(namespace, self.dest, items)
+
+        parser = ArgumentParser(description='CloudFormation Linter')
+        parser.register('action', 'extend', ExtendAction)
+
+        standard = parser.add_argument_group('Standard')
+        advanced = parser.add_argument_group('Advanced / Debugging')
+
+        # Alllow the template to be passes as an optional or a positional argument
+        standard.add_argument(
+            'templates', metavar='TEMPLATE', nargs='*', help='The CloudFormation template to be linted')
+        standard.add_argument(
+            '-t', '--template', metavar='TEMPLATE', dest='template_alt',
+            help='The CloudFormation template to be linted', nargs='+', default=[], action='extend')
+        standard.add_argument(
+            '-b', '--ignore-bad-template', help='Ignore failures with Bad template',
+            action='store_true'
+        )
+        advanced.add_argument(
+            '-d', '--debug', help='Enable debug logging', action='store_true'
+        )
+        standard.add_argument(
+            '-f', '--format', help='Output Format', choices=['quiet', 'parseable', 'json']
+        )
+
+        standard.add_argument(
+            '-l', '--list-rules', dest='listrules', default=False,
+            action='store_true', help='list all the rules'
+        )
+        standard.add_argument(
+            '-r', '--regions', dest='regions', nargs='+', default=[],
+            type=comma_separated_arg, action='extend',
+            help='list the regions to validate against.'
+        )
+        advanced.add_argument(
+            '-a', '--append-rules', dest='append_rules', nargs='+', default=[],
+            type=comma_separated_arg, action='extend',
+            help='specify one or more rules directories using '
+                 'one or more --append-rules arguments. '
+        )
+        standard.add_argument(
+            '-i', '--ignore-checks', dest='ignore_checks', nargs='+', default=[],
+            type=comma_separated_arg, action='extend',
+            help='only check rules whose id do not match these values'
+        )
+        standard.add_argument(
+            '-c', '--include-checks', dest='include_checks', nargs='+', default=[],
+            type=comma_separated_arg, action='extend',
+            help='include rules whose id match these values'
+        )
+
+        advanced.add_argument(
+            '-o', '--override-spec', dest='override_spec',
+            help='A CloudFormation Spec override file that allows customization'
+        )
+
+        standard.add_argument(
+            '-v', '--version', help='Version of cfn-lint', action='version',
+            version='%(prog)s {version}'.format(version=__version__)
+        )
+        advanced.add_argument(
+            '-u', '--update-specs', help='Update the CloudFormation Specs',
+            action='store_true'
+        )
+        advanced.add_argument(
+            '--update-documentation', help=argparse.SUPPRESS,
+            action='store_true'
+        )
+
+        return parser
+
+
+class TemplateArgs(object):
+    """ Per Template Args """
+    def __init__(self, template_args):
+        self.set_template_args(template_args)
+
+    def get_template_args(self):
+        """ Get Template Args"""
+        return self._template_args
+
+    def set_template_args(self, template):
+        """ Set Template Args"""
+        defaults = {}
+        if isinstance(template, dict):
+            configs = template.get('Metadata', {}).get('cfn-lint', {}).get('config', {})
+
+            if isinstance(configs, dict):
+                for config_name, config_value in configs.items():
+                    if config_name == 'ignore_checks':
+                        if isinstance(config_value, list):
+                            defaults['ignore_checks'] = config_value
+                    if config_name == 'regions':
+                        if isinstance(config_value, list):
+                            defaults['regions'] = config_value
+                    if config_name == 'append_rules':
+                        if isinstance(config_value, list):
+                            defaults['override_spec'] = config_value
+                    if config_name == 'override_spec':
+                        if isinstance(config_value, (six.string_types, six.text_type)):
+                            defaults['override_spec'] = config_value
+                    if config_name == 'ignore_bad_template':
+                        if isinstance(config_value, bool):
+                            defaults['ignore_bad_template'] = config_value
+                    if config_name == 'include_checks':
+                        if isinstance(config_value, list):
+                            defaults['include_checks'] = config_value
+
+        self._template_args = defaults
+
+    template_args = property(get_template_args, set_template_args)
+
+
+class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs, object):
+    """ Mixin for the Configs """
+
+    def __init__(self, cli_args):
+        CliArgs.__init__(self, cli_args)
+        # configure debug as soon as we can
+        configure_logging(self.cli_args.debug)
+        ConfigFileArgs.__init__(self)
+        TemplateArgs.__init__(self, {})
+
+    def _get_argument_value(self, arg_name, is_template, is_config_file):
+        """ Get Argument value """
+        cli_value = getattr(self.cli_args, arg_name)
+        template_value = self.template_args.get(arg_name)
+        file_value = self.file_args.get(arg_name)
+        if cli_value:
+            return cli_value
+        if template_value and is_template:
+            return template_value
+        if file_value and is_config_file:
+            return file_value
+
+        return cli_value
+
+    @property
+    def ignore_checks(self):
+        """ ignore_checks """
+        return self._get_argument_value('ignore_checks', True, True)
+
+    @property
+    def include_checks(self):
+        """ include_checks """
+        return self._get_argument_value('include_checks', True, True)
+
+    @property
+    def regions(self):
+        """ regions """
+        results = self._get_argument_value('regions', True, True)
+        if not results:
+            return ['us-east-1']
+        return results
+
+    @property
+    def ignore_bad_template(self):
+        """ ignore_bad_template """
+        return self._get_argument_value('ignore_bad_template', True, True)
+
+    @property
+    def debug(self):
+        """ debug """
+        return self._get_argument_value('debug', False, False)
+
+    @property
+    def format(self):
+        """ format """
+        return self._get_argument_value('format', False, True)
+
+    @property
+    def templates(self):
+        """ templates """
+        templates_args = self._get_argument_value('templates', False, True)
+        template_alt_args = self._get_argument_value('template_alt', False, False)
+        if templates_args:
+            filenames = templates_args
+        elif template_alt_args:
+            filenames = template_alt_args
+        else:
+            return None
+
+        # if only one is specified convert it to array
+        if isinstance(filenames, six.string_types):
+            filenames = [filenames]
+
+        # handle different shells and Config files
+        # some shells don't expand * and configparser won't expand wildcards
+        all_filenames = []
+        for filename in filenames:
+            add_filenames = glob.glob(filename)
+            # only way to know of the glob failed is to test it
+            # then add the filename as requested
+            if not add_filenames:
+                all_filenames.append(filename)
+            else:
+                all_filenames.extend(add_filenames)
+
+        return all_filenames
+
+    @property
+    def append_rules(self):
+        """ append_rules """
+        return self._get_argument_value('append_rules', False, True)
+
+    @property
+    def override_spec(self):
+        """ override_spec """
+        return self._get_argument_value('override_spec', False, True)
+
+    @property
+    def update_specs(self):
+        """ update_specs """
+        return self._get_argument_value('update_specs', False, False)
+
+    @property
+    def update_documentation(self):
+        """ update_specs """
+        return self._get_argument_value('update_documentation', False, False)
+
+    @property
+    def listrules(self):
+        """ listrules """
+        return self._get_argument_value('listrules', False, False)
diff --git a/src/cfnlint/core.py b/src/cfnlint/core.py
--- a/src/cfnlint/core.py
+++ b/src/cfnlint/core.py
@@ -15,28 +15,21 @@
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
 import logging
-import sys
 import os
-import argparse
-import six
+from jsonschema.exceptions import ValidationError
 from cfnlint import RulesCollection
+import cfnlint.config
 import cfnlint.formatters
 import cfnlint.decode
 import cfnlint.maintenance
 from cfnlint.version import __version__
 from cfnlint.helpers import REGIONS
 
+
 LOGGER = logging.getLogger('cfnlint')
 DEFAULT_RULESDIR = os.path.join(os.path.dirname(__file__), 'rules')
 
 
-class ArgumentParser(argparse.ArgumentParser):
-    """ Override Argument Parser so we can control the exit code"""
-    def error(self, message):
-        self.print_help(sys.stderr)
-        self.exit(32, '%s: error: %s\n' % (self.prog, message))
-
-
 def run_cli(filename, template, rules, regions, override_spec):
     """Process args and run"""
 
@@ -60,118 +53,6 @@ def get_exit_code(matches):
     return exit_code
 
 
-def configure_logging(debug_logging):
-    """Setup Logging"""
-    ch = logging.StreamHandler()
-    ch.setLevel(logging.DEBUG)
-
-    if debug_logging:
-        LOGGER.setLevel(logging.DEBUG)
-    else:
-        LOGGER.setLevel(logging.INFO)
-    log_formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
-    ch.setFormatter(log_formatter)
-
-    # make sure all other log handlers are removed before adding it back
-    for handler in LOGGER.handlers:
-        LOGGER.removeHandler(handler)
-    LOGGER.addHandler(ch)
-
-
-def comma_separated_arg(string):
-    """ Split a comma separated string """
-    return string.split(',')
-
-
-def space_separated_arg(string):
-    """ Split a comma separated string """
-    return string.split(' ')
-
-
-class ExtendAction(argparse.Action):
-    """Support argument types that are lists and can be specified multiple times."""
-    def __call__(self, parser, namespace, values, option_string=None):
-        items = getattr(namespace, self.dest)
-        items = [] if items is None else items
-        for value in values:
-            if isinstance(value, list):
-                items.extend(value)
-            else:
-                items.append(value)
-        setattr(namespace, self.dest, items)
-
-
-def create_parser():
-    """Do first round of parsing parameters to set options"""
-    parser = ArgumentParser(description='CloudFormation Linter')
-    parser.register('action', 'extend', ExtendAction)
-
-    standard = parser.add_argument_group('Standard')
-    advanced = parser.add_argument_group('Advanced / Debugging')
-
-    # Alllow the template to be passes as an optional or a positional argument
-    standard.add_argument(
-        'templates', metavar='TEMPLATE', nargs='*', help='The CloudFormation template to be linted')
-    standard.add_argument(
-        '-t', '--template', metavar='TEMPLATE', dest='template_alt',
-        help='The CloudFormation template to be linted', nargs='+', default=[], action='extend')
-    standard.add_argument(
-        '-b', '--ignore-bad-template', help='Ignore failures with Bad template',
-        action='store_true'
-    )
-    advanced.add_argument(
-        '-d', '--debug', help='Enable debug logging', action='store_true'
-    )
-    standard.add_argument(
-        '-f', '--format', help='Output Format', choices=['quiet', 'parseable', 'json']
-    )
-
-    standard.add_argument(
-        '-l', '--list-rules', dest='listrules', default=False,
-        action='store_true', help='list all the rules'
-    )
-    standard.add_argument(
-        '-r', '--regions', dest='regions', nargs='+', default=[],
-        type=comma_separated_arg, action='extend',
-        help='list the regions to validate against.'
-    )
-    advanced.add_argument(
-        '-a', '--append-rules', dest='append_rules', nargs='+', default=[],
-        type=comma_separated_arg, action='extend',
-        help='specify one or more rules directories using '
-             'one or more --append-rules arguments. '
-    )
-    standard.add_argument(
-        '-i', '--ignore-checks', dest='ignore_checks', nargs='+', default=[],
-        type=comma_separated_arg, action='extend',
-        help='only check rules whose id do not match these values'
-    )
-    standard.add_argument(
-        '-c', '--include-checks', dest='include_checks', nargs='+', default=[],
-        type=comma_separated_arg, action='extend',
-        help='include rules whose id match these values'
-    )
-
-    advanced.add_argument(
-        '-o', '--override-spec', dest='override_spec',
-        help='A CloudFormation Spec override file that allows customization'
-    )
-
-    standard.add_argument(
-        '-v', '--version', help='Version of cfn-lint', action='version',
-        version='%(prog)s {version}'.format(version=__version__)
-    )
-    advanced.add_argument(
-        '-u', '--update-specs', help='Update the CloudFormation Specs',
-        action='store_true'
-    )
-    advanced.add_argument(
-        '--update-documentation', help=argparse.SUPPRESS,
-        action='store_true'
-    )
-    return parser
-
-
 def get_formatter(fmt):
     """ Get Formatter"""
     formatter = {}
@@ -204,53 +85,43 @@ def get_rules(rulesdir, ignore_rules, include_rules):
     return rules
 
 
+def configure_logging(debug_logging):
+    """ Backwards compatibility for integrators """
+    LOGGER.debug('Update your integrations to use "cfnlint.config.configure_logging" instead')
+    cfnlint.config.configure_logging(debug_logging)
+
+
 def get_args_filenames(cli_args):
     """ Get Template Configuration items and set them as default values"""
-    parser = create_parser()
-    args, _ = parser.parse_known_args(cli_args)
-
-    configure_logging(args.debug)
+    try:
+        config = cfnlint.config.ConfigMixIn(cli_args)
+    except ValidationError as e:
+        LOGGER.error('Error parsing config file: %s', str(e))
+        exit(1)
 
-    fmt = args.format
+    fmt = config.format
     formatter = get_formatter(fmt)
 
-    # Filename can be speficied as positional or optional argument. Positional
-    # is leading
-    if args.templates:
-        filenames = args.templates
-    elif args.template_alt:
-        filenames = args.template_alt
-    else:
-        filenames = None
+    rules = cfnlint.core.get_rules(config.append_rules, config.ignore_checks, config.include_checks)
 
-    # if only one is specified convert it to array
-    if isinstance(filenames, six.string_types):
-        filenames = [filenames]
-
-    # Set default regions if none are specified.
-    if not args.regions:
-        setattr(args, 'regions', ['us-east-1'])
-
-    if args.update_specs:
+    if config.update_specs:
         cfnlint.maintenance.update_resource_specs()
         exit(0)
 
-    if args.update_documentation:
-        # Load ALL rules when generating documentation
-        all_rules = cfnlint.core.get_rules([], [], ['E', 'W', 'I'])
-        cfnlint.maintenance.update_documentation(all_rules)
+    if config.update_documentation:
+        cfnlint.maintenance.update_documentation(rules)
         exit(0)
 
-    if args.listrules:
-        print(cfnlint.core.get_rules(args.append_rules, args.ignore_checks, args.include_checks))
+    if config.listrules:
+        print(rules)
         exit(0)
 
-    if not filenames:
+    if not config.templates:
         # Not specified, print the help
-        parser.print_help()
+        config.parser.print_help()
         exit(1)
 
-    return(args, filenames, formatter)
+    return(config, config.templates, formatter)
 
 
 def get_template_rules(filename, args):
@@ -261,46 +132,13 @@ def get_template_rules(filename, args):
     if matches:
         return(template, [], matches)
 
-    # If the template has cfn-lint Metadata but the same options are set on the command-
-    # line, ignore the template's configuration. This works because these are all appends
-    # that have default values of empty arrays or none.  The only one that really doesn't
-    # work is ignore_bad_template but you can't override that back to false at this point.
-    for section, values in get_default_args(template).items():
-        if not getattr(args, section):
-            setattr(args, section, values)
+    args.template_args = template
 
     rules = cfnlint.core.get_rules(args.append_rules, args.ignore_checks, args.include_checks)
 
     return(template, rules, [])
 
 
-def get_default_args(template):
-    """ Parse and validate default args """
-    defaults = {}
-    if isinstance(template, dict):
-        configs = template.get('Metadata', {}).get('cfn-lint', {}).get('config', {})
-
-        if isinstance(configs, dict):
-            for config_name, config_value in configs.items():
-                if config_name == 'ignore_checks':
-                    if isinstance(config_value, list):
-                        defaults['ignore_checks'] = config_value
-                if config_name == 'regions':
-                    if isinstance(config_value, list):
-                        defaults['regions'] = config_value
-                if config_name == 'append_rules':
-                    if isinstance(config_value, list):
-                        defaults['override_spec'] = config_value
-                if config_name == 'override_spec':
-                    if isinstance(config_value, (six.string_types, six.text_type)):
-                        defaults['override_spec'] = config_value
-                if config_name == 'ignore_bad_template':
-                    if isinstance(config_value, bool):
-                        defaults['ignore_bad_template'] = config_value
-
-    return defaults
-
-
 def run_checks(filename, template, rules, regions):
     """Run Checks against the template"""
     if regions:


Test patch:
diff --git a/test/module/config/__init__.py b/test/module/config/__init__.py
new file mode 100644
--- /dev/null
+++ b/test/module/config/__init__.py
@@ -0,0 +1,16 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
diff --git a/test/module/core/test_arg_parser.py b/test/module/config/test_cli_args.py
similarity index 55%
rename from test/module/core/test_arg_parser.py
rename to test/module/config/test_cli_args.py
--- a/test/module/core/test_arg_parser.py
+++ b/test/module/config/test_cli_args.py
@@ -15,7 +15,8 @@
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
 import logging
-import cfnlint.core  # pylint: disable=E0401
+from mock import patch, mock_open
+import cfnlint.config  # pylint: disable=E0401
 from testlib.testcase import BaseTestCase
 
 LOGGER = logging.getLogger('cfnlint')
@@ -31,30 +32,35 @@ def tearDown(self):
     def test_create_parser(self):
         """Test success run"""
 
-        parser = cfnlint.core.create_parser()
-        args = parser.parse_args([
+        config = cfnlint.config.CliArgs([
             '-t', 'test.yaml', '--ignore-bad-template',
             '--format', 'quiet', '--debug'])
-        self.assertEqual(args.templates, [])
-        self.assertEqual(args.template_alt, ['test.yaml'])
-        self.assertEqual(args.ignore_bad_template, True)
-        self.assertEqual(args.format, 'quiet')
-        self.assertEqual(args.debug, True)
+        self.assertEqual(config.cli_args.templates, [])
+        self.assertEqual(config.cli_args.template_alt, ['test.yaml'])
+        self.assertEqual(config.cli_args.ignore_bad_template, True)
+        self.assertEqual(config.cli_args.format, 'quiet')
+        self.assertEqual(config.cli_args.debug, True)
 
     def test_create_parser_default_param(self):
         """Test success run"""
 
-        parser = cfnlint.core.create_parser()
-        args = parser.parse_args([
+        config = cfnlint.config.CliArgs([
             '--regions', 'us-east-1', 'us-west-2', '--', 'template1.yaml', 'template2.yaml'])
-        self.assertEqual(args.templates, ['template1.yaml', 'template2.yaml'])
-        self.assertEqual(args.template_alt, [])
-        self.assertEqual(args.regions, ['us-east-1', 'us-west-2'])
+        self.assertEqual(config.cli_args.templates, ['template1.yaml', 'template2.yaml'])
+        self.assertEqual(config.cli_args.template_alt, [])
+        self.assertEqual(config.cli_args.regions, ['us-east-1', 'us-west-2'])
 
     def test_create_parser_exend(self):
         """Test success run"""
 
-        parser = cfnlint.core.create_parser()
-        args = parser.parse_args(['-t', 'template1.yaml', '-t', 'template2.yaml'])
-        self.assertEqual(args.templates, [])
-        self.assertEqual(args.template_alt, ['template1.yaml', 'template2.yaml'])
+        config = cfnlint.config.CliArgs(['-t', 'template1.yaml', '-t', 'template2.yaml'])
+        self.assertEqual(config.cli_args.templates, [])
+        self.assertEqual(config.cli_args.template_alt, ['template1.yaml', 'template2.yaml'])
+
+    def test_create_parser_config_file(self):
+        """Test success run"""
+
+        config = cfnlint.config.CliArgs(['--regions', 'us-west-1', '--include-checks', 'I1234', '--', 'template1.yaml'])
+        self.assertEqual(config.cli_args.templates, ['template1.yaml'])
+        self.assertEqual(config.cli_args.include_checks, ['I1234'])
+        self.assertEqual(config.cli_args.regions, ['us-west-1'])
diff --git a/test/module/config/test_config_file_args.py b/test/module/config/test_config_file_args.py
new file mode 100644
--- /dev/null
+++ b/test/module/config/test_config_file_args.py
@@ -0,0 +1,66 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+import logging
+from mock import patch, mock_open, Mock
+import jsonschema
+import cfnlint.config  # pylint: disable=E0401
+from testlib.testcase import BaseTestCase
+
+
+LOGGER = logging.getLogger('cfnlint')
+
+
+class TestConfigFileArgs(BaseTestCase):
+    """Test ConfigParser Arguments """
+    def tearDown(self):
+        """Setup"""
+        for handler in LOGGER.handlers:
+            LOGGER.removeHandler(handler)
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_parser_read(self, yaml_mock):
+        """ Testing one file successful """
+        # cfnlintrc_mock.return_value.side_effect = [Mock(return_value=True), Mock(return_value=False)]
+        yaml_mock.side_effect = [
+            {"regions": ["us-west-1"]},
+            {}
+        ]
+        results = cfnlint.config.ConfigFileArgs()
+        self.assertEqual(results.file_args, {'regions': ['us-west-1']})
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_parser_read_merge(self, yaml_mock):
+        """ test the merge of config """
+
+        yaml_mock.side_effect = [
+            {"regions": ["us-west-1"]},
+            {"regions": ["us-east-1"]}
+        ]
+
+        results = cfnlint.config.ConfigFileArgs()
+        self.assertEqual(results.file_args, {'regions': ['us-east-1']})
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_parser_fail_on_bad_config(self, yaml_mock):
+        """ test the read call to the config parser is reading two files """
+
+        yaml_mock.side_effect = [
+            {"regions": True}, {}
+        ]
+
+        with self.assertRaises(jsonschema.exceptions.ValidationError):
+            cfnlint.config.ConfigFileArgs()
diff --git a/test/module/config/test_config_mixin.py b/test/module/config/test_config_mixin.py
new file mode 100644
--- /dev/null
+++ b/test/module/config/test_config_mixin.py
@@ -0,0 +1,110 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+import logging
+from mock import patch, mock_open
+import cfnlint.config  # pylint: disable=E0401
+from testlib.testcase import BaseTestCase
+
+
+LOGGER = logging.getLogger('cfnlint')
+
+
+class TestConfigMixIn(BaseTestCase):
+    """Test ConfigParser Arguments """
+    def tearDown(self):
+        """Setup"""
+        for handler in LOGGER.handlers:
+            LOGGER.removeHandler(handler)
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_mix_in(self, yaml_mock):
+        """ Test mix in  """
+        yaml_mock.side_effect = [
+            {"include_checks": ["I", "I1111"], "regions": ["us-west-2"]},
+            {}
+        ]
+
+        config = cfnlint.config.ConfigMixIn(['--regions', 'us-west-1'])
+        self.assertEqual(config.regions, ['us-west-1'])
+        self.assertEqual(config.include_checks, ['I', 'I1111'])
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_precedence(self, yaml_mock):
+        """ Test precedence in  """
+
+        yaml_mock.side_effect = [
+            {"include_checks": ["I"], "ignore_checks": ["E3001"], "regions": ["us-west-2"]},
+            {}
+        ]
+        config = cfnlint.config.ConfigMixIn(['--include-checks', 'I1234', 'I4321'])
+        config.template_args = {
+            'Metadata': {
+                'cfn-lint': {
+                    'config': {
+                        'include_checks': ['I9876'],
+                        'ignore_checks': ['W3001']
+                    }
+                }
+            }
+        }
+        # config files wins
+        self.assertEqual(config.regions, ['us-west-2'])
+        # CLI should win
+        self.assertEqual(config.include_checks, ['I1234', 'I4321'])
+        # template file wins over config file
+        self.assertEqual(config.ignore_checks, ['W3001'])
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_default_region(self, yaml_mock):
+        """ Test precedence in  """
+
+        yaml_mock.side_effect = [
+            {},
+            {}
+        ]
+        config = cfnlint.config.ConfigMixIn([])
+
+        # test defaults
+        self.assertEqual(config.regions, ['us-east-1'])
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_expand_paths(self, yaml_mock):
+        """ Test precedence in  """
+
+        yaml_mock.side_effect = [
+            {'templates': ['fixtures/templates/public/*.yaml']},
+            {}
+        ]
+        config = cfnlint.config.ConfigMixIn([])
+
+        # test defaults
+        self.assertEqual(config.templates, [
+            'fixtures/templates/public/lambda-poller.yaml',
+            'fixtures/templates/public/rds-cluster.yaml'])
+
+    @patch('cfnlint.config.ConfigFileArgs._read_config', create=True)
+    def test_config_expand_paths_failure(self, yaml_mock):
+        """ Test precedence in  """
+
+        yaml_mock.side_effect = [
+            {'templates': ['*.yaml']},
+            {}
+        ]
+        config = cfnlint.config.ConfigMixIn([])
+
+        # test defaults
+        self.assertEqual(config.templates, ['*.yaml'])
diff --git a/test/module/core/test_logging.py b/test/module/config/test_logging.py
similarity index 91%
rename from test/module/core/test_logging.py
rename to test/module/config/test_logging.py
--- a/test/module/core/test_logging.py
+++ b/test/module/config/test_logging.py
@@ -15,7 +15,7 @@
   SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 """
 import logging
-import cfnlint.core  # pylint: disable=E0401
+import cfnlint.config  # pylint: disable=E0401
 from testlib.testcase import BaseTestCase
 
 LOGGER = logging.getLogger('cfnlint')
@@ -31,13 +31,13 @@ def tearDown(self):
     def test_logging_info(self):
         """Test success run"""
 
-        cfnlint.core.configure_logging(False)
+        cfnlint.config.configure_logging(False)
         self.assertEqual(logging.INFO, LOGGER.level)
         self.assertEqual(len(LOGGER.handlers), 1)
 
     def test_logging_debug(self):
         """Test debug level"""
 
-        cfnlint.core.configure_logging(True)
+        cfnlint.config.configure_logging(True)
         self.assertEqual(logging.DEBUG, LOGGER.level)
         self.assertEqual(len(LOGGER.handlers), 1)
diff --git a/test/module/config/test_template_args.py b/test/module/config/test_template_args.py
new file mode 100644
--- /dev/null
+++ b/test/module/config/test_template_args.py
@@ -0,0 +1,46 @@
+"""
+  Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+
+  Permission is hereby granted, free of charge, to any person obtaining a copy of this
+  software and associated documentation files (the "Software"), to deal in the Software
+  without restriction, including without limitation the rights to use, copy, modify,
+  merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
+  permit persons to whom the Software is furnished to do so.
+
+  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
+  INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
+  PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+  HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+  OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
+  SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+"""
+import logging
+from mock import patch, mock_open
+import cfnlint.config  # pylint: disable=E0401
+from testlib.testcase import BaseTestCase
+
+
+LOGGER = logging.getLogger('cfnlint')
+
+
+class TestTempalteArgs(BaseTestCase):
+    """Test ConfigParser Arguments """
+    def tearDown(self):
+        """Setup"""
+        for handler in LOGGER.handlers:
+            LOGGER.removeHandler(handler)
+
+    def test_template_args(self):
+        """ test template args """
+        config = cfnlint.config.TemplateArgs({
+            'Metadata': {
+                'cfn-lint': {
+                    'config': {
+                        'regions': ['us-east-1', 'us-east-2'],
+                        'ignore_checks': ['E2530']
+                    }
+                }
+            }
+        })
+
+        self.assertEqual(config.template_args['regions'], ['us-east-1', 'us-east-2'])
diff --git a/test/module/core/test_run_cli.py b/test/module/core/test_run_cli.py
--- a/test/module/core/test_run_cli.py
+++ b/test/module/core/test_run_cli.py
@@ -17,6 +17,8 @@
 import logging
 import cfnlint.core  # pylint: disable=E0401
 from testlib.testcase import BaseTestCase
+from mock import patch, mock_open
+import cfnlint.config  # pylint: disable=E0401
 
 LOGGER = logging.getLogger('cfnlint')
 
@@ -85,20 +87,19 @@ def test_template_config(self):
         (args, _, _,) = cfnlint.core.get_args_filenames([
             '--template', filename, '--ignore-bad-template'])
 
-        self.assertEqual(vars(args), {
-            'append_rules': [],
-            'format': None,
-            'ignore_bad_template': True,
-            'ignore_checks': [],
-            'include_checks': [],
-            'listrules': False,
-            'debug': False,
-            'override_spec': None,
-            'regions': ['us-east-1'],
-            'template_alt': ['fixtures/templates/good/core/config_parameters.yaml'],
-            'templates': [],
-            'update_documentation': False,
-            'update_specs': False})
+        self.assertEqual(args.append_rules, [])
+        self.assertEqual(args.append_rules, [])
+        self.assertEqual(args.format, None)
+        self.assertEqual(args.ignore_bad_template, True)
+        self.assertEqual(args.ignore_checks, [])
+        self.assertEqual(args.include_checks, [])
+        self.assertEqual(args.listrules, False)
+        self.assertEqual(args.debug, False)
+        self.assertEqual(args.override_spec, None)
+        self.assertEqual(args.regions, ['us-east-1'])
+        self.assertEqual(args.templates, ['fixtures/templates/good/core/config_parameters.yaml'])
+        self.assertEqual(args.update_documentation, False)
+        self.assertEqual(args.update_specs, False)
 
     def test_positional_template_parameters(self):
         """Test overriding parameters"""
@@ -107,20 +108,18 @@ def test_positional_template_parameters(self):
             filename, '--ignore-bad-template',
             '--ignore-checks', 'E0000'])
 
-        self.assertEqual(vars(args), {
-            'append_rules': [],
-            'format': None,
-            'ignore_bad_template': True,
-            'ignore_checks': ['E0000'],
-            'include_checks': [],
-            'listrules': False,
-            'debug': False,
-            'override_spec': None,
-            'regions': ['us-east-1'],
-            'template_alt': [],
-            'templates': ['fixtures/templates/good/core/config_parameters.yaml'],
-            'update_documentation': False,
-            'update_specs': False})
+        self.assertEqual(args.append_rules, [])
+        self.assertEqual(args.format, None)
+        self.assertEqual(args.ignore_bad_template, True)
+        self.assertEqual(args.ignore_checks, ['E0000'])
+        self.assertEqual(args.include_checks, [])
+        self.assertEqual(args.listrules, False)
+        self.assertEqual(args.debug, False)
+        self.assertEqual(args.override_spec, None)
+        self.assertEqual(args.regions, ['us-east-1'])
+        self.assertEqual(args.templates, ['fixtures/templates/good/core/config_parameters.yaml'])
+        self.assertEqual(args.update_documentation, False)
+        self.assertEqual(args.update_specs, False)
 
     def test_override_parameters(self):
         """Test overriding parameters"""
@@ -129,20 +128,18 @@ def test_override_parameters(self):
             '--template', filename, '--ignore-bad-template',
             '--ignore-checks', 'E0000'])
 
-        self.assertEqual(vars(args), {
-            'append_rules': [],
-            'format': None,
-            'ignore_bad_template': True,
-            'ignore_checks': ['E0000'],
-            'include_checks': [],
-            'listrules': False,
-            'debug': False,
-            'override_spec': None,
-            'regions': ['us-east-1'],
-            'template_alt': ['fixtures/templates/good/core/config_parameters.yaml'],
-            'templates': [],
-            'update_documentation': False,
-            'update_specs': False})
+        self.assertEqual(args.append_rules, [])
+        self.assertEqual(args.format, None)
+        self.assertEqual(args.ignore_bad_template, True)
+        self.assertEqual(args.ignore_checks, ['E0000'])
+        self.assertEqual(args.include_checks, [])
+        self.assertEqual(args.listrules, False)
+        self.assertEqual(args.debug, False)
+        self.assertEqual(args.override_spec, None)
+        self.assertEqual(args.regions, ['us-east-1'])
+        self.assertEqual(args.templates, ['fixtures/templates/good/core/config_parameters.yaml'])
+        self.assertEqual(args.update_documentation, False)
+        self.assertEqual(args.update_specs, False)
 
     def test_bad_config(self):
         """ Test bad formatting in config"""
@@ -151,17 +148,15 @@ def test_bad_config(self):
         (args, _, _) = cfnlint.core.get_args_filenames([
             '--template', filename, '--ignore-bad-template'])
 
-        self.assertEqual(vars(args), {
-            'append_rules': [],
-            'format': None,
-            'ignore_bad_template': True,
-            'ignore_checks': [],
-            'include_checks': [],
-            'listrules': False,
-            'debug': False,
-            'override_spec': None,
-            'regions': ['us-east-1'],
-            'template_alt': [filename],
-            'templates': [],
-            'update_documentation': False,
-            'update_specs': False})
+        self.assertEqual(args.append_rules, [])
+        self.assertEqual(args.format, None)
+        self.assertEqual(args.ignore_bad_template, True)
+        self.assertEqual(args.ignore_checks, [])
+        self.assertEqual(args.include_checks, [])
+        self.assertEqual(args.listrules, False)
+        self.assertEqual(args.debug, False)
+        self.assertEqual(args.override_spec, None)
+        self.assertEqual(args.regions, ['us-east-1'])
+        self.assertEqual(args.templates, [filename])
+        self.assertEqual(args.update_documentation, False)
+        self.assertEqual(args.update_specs, False)