diff --git a/src/cfnlint/config.py b/src/cfnlint/config.py
index 91781ce309..8f22c87ccf 100644
--- a/src/cfnlint/config.py
+++ b/src/cfnlint/config.py
@@ -12,12 +12,14 @@
 import logging
 import os
 import sys
+from copy import deepcopy
 from pathlib import Path
 from typing import Any, Dict, Sequence, TypedDict
 
 from typing_extensions import Unpack
 
 import cfnlint.decode.cfn_yaml
+from cfnlint.context.parameters import ParameterSet
 from cfnlint.helpers import REGIONS, format_json_string
 from cfnlint.jsonschema import StandardValidator
 from cfnlint.version import __version__
@@ -681,7 +683,8 @@ class ManualArgs(TypedDict, total=False):
     non_zero_exit_code: str
     output_file: str
     regions: list
-    parameters: list[dict[str, Any]]
+    parameters: list[ParameterSet]
+    templates: list[str]
 
 
 # pylint: disable=too-many-public-methods
@@ -689,7 +692,6 @@ class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs):
 
     def __init__(self, cli_args: list[str] | None = None, **kwargs: Unpack[ManualArgs]):
         self._manual_args = kwargs or ManualArgs()
-        self._templates_to_process = False
         CliArgs.__init__(self, cli_args)
         # configure debug as soon as we can
         TemplateArgs.__init__(self, {})
@@ -716,12 +718,35 @@ def __repr__(self):
                 "merge_configs": self.merge_configs,
                 "non_zero_exit_code": self.non_zero_exit_code,
                 "override_spec": self.override_spec,
-                "regions": self.regions,
                 "parameters": self.parameters,
+                "regions": self.regions,
                 "templates": self.templates,
             }
         )
 
+    def __eq__(self, value):
+        if not isinstance(value, ConfigMixIn):
+            return False
+        for key in [
+            "configure_rules",
+            "deployment_files",
+            "ignore_bad_template",
+            "ignore_checks",
+            "include_checks",
+            "include_experimental",
+            "mandatory_checks",
+            "merge_configs",
+            "non_zero_exit_code",
+            "output_file",
+            "regions",
+            "parameters",
+            "templates",
+        ]:
+            if getattr(self, key) != getattr(value, key):
+                return False
+
+        return True
+
     def _get_argument_value(self, arg_name, is_template, is_config_file):
         cli_value = getattr(self.cli_args, arg_name)
         template_value = self.template_args.get(arg_name)
@@ -816,7 +841,9 @@ def templates(self):
         file_args = self._get_argument_value("templates", False, True)
         cli_args = self._get_argument_value("templates", False, False)
 
-        if cli_alt_args:
+        if "templates" in self._manual_args:
+            filenames = self._manual_args["templates"]
+        elif cli_alt_args:
             filenames = cli_alt_args
         elif file_args:
             filenames = file_args
@@ -826,10 +853,6 @@ def templates(self):
             # No filenames found, could be piped in or be using the api.
             return None
 
-        # If we're still haven't returned, we've got templates to lint.
-        # Build up list of templates to lint.
-        self.templates_to_process = True
-
         if isinstance(filenames, str):
             filenames = [filenames]
 
@@ -880,12 +903,21 @@ def append_rules(self):
         )
 
     @property
-    def parameters(self):
-        return self._get_argument_value("parameters", True, True)
+    def parameters(self) -> list[ParameterSet]:
+        parameter_sets = self._get_argument_value("parameters", True, True)
+        results: list[ParameterSet] = []
+        for parameter_set in parameter_sets:
+            if isinstance(parameter_set, ParameterSet):
+                results.append(parameter_set)
+            else:
+                results.append(
+                    ParameterSet(
+                        source=None,
+                        parameters=parameter_set,
+                    )
+                )
 
-    @parameters.setter
-    def parameters(self, parameters: list[dict[str, Any]]):
-        self._manual_args["parameters"] = parameters
+        return results
 
     @property
     def override_spec(self):
@@ -952,10 +984,8 @@ def non_zero_exit_code(self):
     def force(self):
         return self._get_argument_value("force", False, False)
 
-    @property
-    def templates_to_process(self):
-        return self._templates_to_process
+    def evolve(self, **kwargs: Unpack[ManualArgs]) -> "ConfigMixIn":
 
-    @templates_to_process.setter
-    def templates_to_process(self, value: bool):
-        self._templates_to_process = value
+        config = deepcopy(self)
+        config._manual_args.update(kwargs)
+        return config
diff --git a/src/cfnlint/context/__init__.py b/src/cfnlint/context/__init__.py
index 0d4e0154ef..ed0e1e027a 100644
--- a/src/cfnlint/context/__init__.py
+++ b/src/cfnlint/context/__init__.py
@@ -1,3 +1,8 @@
-__all__ = ["Context", "create_context_for_template"]
+__all__ = ["Context", "create_context_for_template", "ParameterSet"]
 
-from cfnlint.context.context import Context, Path, create_context_for_template
+from cfnlint.context.context import (
+    Context,
+    ParameterSet,
+    Path,
+    create_context_for_template,
+)
diff --git a/src/cfnlint/context/context.py b/src/cfnlint/context/context.py
index 0e75023a05..dfb1c46a80 100644
--- a/src/cfnlint/context/context.py
+++ b/src/cfnlint/context/context.py
@@ -9,10 +9,11 @@
 from collections import deque
 from dataclasses import InitVar, dataclass, field, fields
 from functools import lru_cache
-from typing import Any, Deque, Iterator, Sequence, Set, Tuple
+from typing import TYPE_CHECKING, Any, Deque, Iterator, Set, Tuple
 
 from cfnlint.context._mappings import Mappings
 from cfnlint.context.conditions._conditions import Conditions
+from cfnlint.context.parameters import ParameterSet
 from cfnlint.helpers import (
     BOOLEAN_STRINGS_TRUE,
     FUNCTIONS,
@@ -22,6 +23,9 @@
 )
 from cfnlint.schema import PROVIDER_SCHEMA_MANAGER, AttributeDict
 
+if TYPE_CHECKING:
+    from cfnlint.template import Template
+
 _PSEUDOPARAMS_NON_REGION = ["AWS::AccountId", "AWS::NoValue", "AWS::StackName"]
 
 
@@ -132,12 +136,12 @@ class Context:
     """
 
     # what regions we are processing
-    regions: Sequence[str] = field(
+    regions: list[str] = field(
         init=True, default_factory=lambda: list([REGION_PRIMARY])
     )
 
     # supported functions at this point in the template
-    functions: Sequence[str] = field(init=True, default_factory=list)
+    functions: list[str] = field(init=True, default_factory=list)
 
     path: Path = field(init=True, default_factory=Path)
 
@@ -153,7 +157,7 @@ class Context:
         init=True, default_factory=lambda: set(PSEUDOPARAMS)
     )
 
-    # Combiniation of storing any resolved ref
+    # Combination of storing any resolved ref
     # and adds in any Refs available from things like Fn::Sub
     ref_values: dict[str, Any] = field(init=True, default_factory=dict)
 
@@ -163,6 +167,9 @@ class Context:
     is_resolved_value: bool = field(init=True, default=False)
     resolve_pseudo_parameters: bool = field(init=True, default=True)
 
+    # Deployment parameters
+    parameter_sets: list[ParameterSet] | None = field(init=True, default_factory=list)
+
     def evolve(self, **kwargs) -> "Context":
         """
         Create a new context without merging together attributes
@@ -451,7 +458,9 @@ def _init_transforms(transforms: Any) -> Transforms:
     return Transforms([])
 
 
-def create_context_for_template(cfn):
+def create_context_for_template(
+    cfn: Template,
+) -> "Context":
     parameters = {}
     try:
         parameters = _init_parameters(cfn.template.get("Parameters", {}))
@@ -486,5 +495,6 @@ def create_context_for_template(cfn):
         regions=cfn.regions,
         path=Path(),
         functions=["Fn::Transform"],
-        ref_values=cfn.parameters or {},
+        ref_values={},
+        parameter_sets=[],
     )
diff --git a/src/cfnlint/context/parameters.py b/src/cfnlint/context/parameters.py
new file mode 100644
index 0000000000..916b84e1ba
--- /dev/null
+++ b/src/cfnlint/context/parameters.py
@@ -0,0 +1,16 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass, field
+from typing import Any
+
+
+@dataclass
+class ParameterSet:
+
+    source: str | None = field(default=None)
+    parameters: dict[str, Any] = field(default_factory=dict)
diff --git a/src/cfnlint/core.py b/src/cfnlint/core.py
index 3c5db7abb7..fae5fc379c 100644
--- a/src/cfnlint/core.py
+++ b/src/cfnlint/core.py
@@ -12,12 +12,9 @@
 from cfnlint.exceptions import UnexpectedRuleException
 from cfnlint.match import Match
 from cfnlint.rules import RulesCollection
-
-from cfnlint.runner.exceptions import UnexpectedRuleException
 from cfnlint.runner.template.runner import _run_template
 
 
-
 def get_rules(
     append_rules: list[str],
     ignore_rules: list[str],
diff --git a/src/cfnlint/decode/__init__.py b/src/cfnlint/decode/__init__.py
index fd51552922..fda9f7ac6a 100644
--- a/src/cfnlint/decode/__init__.py
+++ b/src/cfnlint/decode/__init__.py
@@ -3,6 +3,15 @@
 SPDX-License-Identifier: MIT-0
 """
 
+__all__ = [
+    "create_match_file_error",
+    "create_match_json_parser_error",
+    "create_match_yaml_parser_error",
+    "decode",
+    "decode_str",
+    "convert_dict",
+]
+
 from cfnlint.decode.decode import (
     create_match_file_error,
     create_match_json_parser_error,
diff --git a/src/cfnlint/decode/decode.py b/src/cfnlint/decode/decode.py
index ef8ebe8ee0..eadc400628 100644
--- a/src/cfnlint/decode/decode.py
+++ b/src/cfnlint/decode/decode.py
@@ -37,7 +37,7 @@ def _decode(
 ) -> Decode:
     """Decode payload using yaml_f and json_f, using filename for log output."""
     template = None
-    matches = []
+    matches: Matches = []
     try:
         template = yaml_f(payload)
     except IOError as e:
@@ -145,7 +145,10 @@ def _decode(
                 message="Template needs to be an object.",
             )
         ]
-    return (template, matches)
+
+    if matches:
+        return None, matches
+    return template, matches
 
 
 def create_match_yaml_parser_error(parser_error, filename):
diff --git a/src/cfnlint/exceptions.py b/src/cfnlint/exceptions.py
index 993106ab01..f4629dac38 100644
--- a/src/cfnlint/exceptions.py
+++ b/src/cfnlint/exceptions.py
@@ -1,3 +1,11 @@
+"""
+Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+from __future__ import annotations
+
+
 class CfnLintExitException(Exception):
     """
     An exception that is raised to indicate that the CloudFormation linter should exit.
@@ -10,11 +18,11 @@ class CfnLintExitException(Exception):
         exit_code (int): The exit code to be used when the linter exits.
 
     Methods:
-        __init__(self, exit_code: int) -> None:
+        __init__(self, msg: str | None=None, exit_code: int=1) -> None:
             Initialize a new CfnLintExitException instance with the specified exit code.
     """
 
-    def __init__(self, msg=None, exit_code=1):
+    def __init__(self, msg: str | None = None, exit_code: int = 1):
         """
         Initialize a new CfnLintExitException instance with the specified exit code.
 
@@ -49,9 +57,17 @@ class UnexpectedRuleException(CfnLintExitException):
 
 class DuplicateRuleError(CfnLintExitException):
     """
-    The data associated with a particular path could not be loaded.
-    :ivar data_path: The data path that the user attempted to load.
+    An exception that is raised when an unexpected error occurs while loading rules.
+
+    This exception is raised when the CloudFormation linter encounters a rule with a
+    duplicate ID.
     """
 
     def __init__(self, rule_id: str):
+        """
+        Initialize a new CfnLintExitException instance with the specified exit code.
+
+        Args:
+            rule_id (str): The rule ID that a duplicate was found for.
+        """
         super().__init__(f"Rule already included: {rule_id}")
diff --git a/src/cfnlint/helpers.py b/src/cfnlint/helpers.py
index 298f51cc85..e60d0ee0b6 100644
--- a/src/cfnlint/helpers.py
+++ b/src/cfnlint/helpers.py
@@ -7,6 +7,7 @@
 
 from __future__ import annotations
 
+import dataclasses
 import datetime
 import fnmatch
 import gzip
@@ -627,6 +628,8 @@ def converter(o):  # pylint: disable=R1710
         """Help convert date/time into strings"""
         if isinstance(o, datetime.datetime):
             return o.__str__()  # pylint: disable=unnecessary-dunder-call
+        elif dataclasses.is_dataclass(o):
+            return dataclasses.asdict(o)
 
     return json.dumps(
         json_string, indent=1, sort_keys=True, separators=(",", ": "), default=converter
diff --git a/src/cfnlint/jsonschema/validators.py b/src/cfnlint/jsonschema/validators.py
index c9450b1695..76f5bbcf3e 100644
--- a/src/cfnlint/jsonschema/validators.py
+++ b/src/cfnlint/jsonschema/validators.py
@@ -25,7 +25,7 @@
 from typing import Any, Callable
 
 from cfnlint.conditions import UnknownSatisfisfaction
-from cfnlint.context import Context, create_context_for_template
+from cfnlint.context import Context
 from cfnlint.helpers import is_function
 from cfnlint.jsonschema import _keywords, _keywords_cfn, _resolvers_cfn
 from cfnlint.jsonschema._filter import FunctionFilter
@@ -92,7 +92,7 @@ class Validator:
 
         def __post_init__(self):
             if self.context is None:
-                self.context = create_context_for_template(self.cfn)
+                self.context = self.cfn.context.evolve()
             if self.resolver is None:
                 self.resolver = RefResolver.from_schema(
                     schema=self.schema,
diff --git a/src/cfnlint/match.py b/src/cfnlint/match.py
index 1f28e58f4d..8dc204b62f 100644
--- a/src/cfnlint/match.py
+++ b/src/cfnlint/match.py
@@ -118,6 +118,8 @@ def create(
         if linenumberend is None:
             linenumberend = linenumber
 
+        filename = getattr(rulematch_obj, "filename", filename)
+
         return cls(
             linenumber=linenumber,
             columnnumber=columnnumber,
diff --git a/src/cfnlint/rules/deployment_files/Parameters.py b/src/cfnlint/rules/deployment_files/Parameters.py
index 596c52a9f1..229c6a5330 100644
--- a/src/cfnlint/rules/deployment_files/Parameters.py
+++ b/src/cfnlint/rules/deployment_files/Parameters.py
@@ -79,18 +79,23 @@ def _build_schema(self, instance: Any) -> dict[str, Any]:
         return schema
 
     def validate(self, validator: Validator, _: Any, instance: Any, schema: Any):
-        if validator.cfn.parameters is None:
+        if validator.context.parameter_sets is None:
             return
 
-        cfn_validator = self.extend_validator(
-            validator=validator,
-            schema=self._build_schema(instance),
-            context=validator.context,
-        ).evolve(
-            context=validator.context.evolve(strict_types=False),
-            function_filter=validator.function_filter.evolve(
-                add_cfn_lint_keyword=False,
-            ),
-        )
-
-        yield from super()._iter_errors(cfn_validator, validator.cfn.parameters)
+        for parameter_set in validator.context.parameter_sets:
+
+            cfn_validator = self.extend_validator(
+                validator=validator,
+                schema=self._build_schema(instance),
+                context=validator.context,
+            ).evolve(
+                context=validator.context.evolve(strict_types=False),
+                function_filter=validator.function_filter.evolve(
+                    add_cfn_lint_keyword=False,
+                ),
+            )
+
+            for err in super()._iter_errors(cfn_validator, parameter_set.parameters):
+                if parameter_set.source:
+                    err.extra_args["filename"] = parameter_set.source
+                yield err
diff --git a/src/cfnlint/runner/__init__.py b/src/cfnlint/runner/__init__.py
index ea1a996f4c..82ee8a6cba 100644
--- a/src/cfnlint/runner/__init__.py
+++ b/src/cfnlint/runner/__init__.py
@@ -8,17 +8,17 @@
     "Runner",
     "run_template_by_data",
     "run_template_by_file_path",
-    "run_deployment_file",
+    "expand_deployment_files",
     "CfnLintExitException",
     "InvalidRegionException",
     "UnexpectedRuleException",
 ]
 
-from cfnlint.runner.cli import Runner, main
-from cfnlint.runner.deployment_file import run_deployment_files
-from cfnlint.runner.exceptions import (
+from cfnlint.exceptions import (
     CfnLintExitException,
     InvalidRegionException,
     UnexpectedRuleException,
 )
+from cfnlint.runner.cli import Runner, main
+from cfnlint.runner.deployment_file import expand_deployment_files
 from cfnlint.runner.template import run_template_by_data, run_template_by_file_path
diff --git a/src/cfnlint/runner/cli.py b/src/cfnlint/runner/cli.py
index 65bd1332b5..b52afc827a 100644
--- a/src/cfnlint/runner/cli.py
+++ b/src/cfnlint/runner/cli.py
@@ -8,17 +8,20 @@
 import logging
 import os
 import sys
-from typing import Any, Iterator, Sequence
+from typing import Any, Iterator
 
 import cfnlint.formatters
 import cfnlint.maintenance
 from cfnlint.config import ConfigMixIn, configure_logging
 from cfnlint.exceptions import CfnLintExitException, UnexpectedRuleException
 from cfnlint.rules import Match, Rules
-from cfnlint.rules.errors import ConfigError, ParseError
-from cfnlint.runner.deployment_file.runner import run_deployment_files
-from cfnlint.runner.exceptions import CfnLintExitException, UnexpectedRuleException
-from cfnlint.runner.template import run_template_by_data, run_template_by_file_path
+from cfnlint.rules.errors import ConfigError
+from cfnlint.runner.deployment_file.runner import expand_deployment_files
+from cfnlint.runner.template import (
+    run_template_by_data,
+    run_template_by_file_paths,
+    run_template_by_pipe,
+)
 from cfnlint.schema import PROVIDER_SCHEMA_MANAGER
 
 LOGGER = logging.getLogger(__name__)
@@ -129,41 +132,6 @@ def _get_rules(self) -> None:
                 f"Tried to append rules but got an error: {str(e)}", 1
             ) from e
 
-    def _validate_filenames(self, filenames: Sequence[str | None]) -> Iterator[Match]:
-        """
-        Validate the specified filenames and yield any matches found.
-
-        This function processes each filename in the provided sequence, decoding the
-        template and validating it against the configured rules. Any matches found
-        are yielded as an iterator.
-
-        Args:
-            filenames (Sequence[str | None]): The sequence of filenames to be validated.
-
-        Yields:
-            Match: The matches found during the validation process.
-
-        Raises:
-            None: This function does not raise any exceptions.
-        """
-        ignore_bad_template: bool = False
-        if self.config.ignore_bad_template:
-            ignore_bad_template = True
-        else:
-            # There is no collection at this point so we need to handle this
-            # check directly
-            if not ParseError().is_enabled(
-                include_experimental=False,
-                ignore_rules=self.config.ignore_checks,
-                include_rules=self.config.include_checks,
-                mandatory_rules=self.config.mandatory_checks,
-            ):
-                ignore_bad_template = True
-        for filename in filenames:
-            yield from run_template_by_file_path(
-                filename, self.config, self.rules, ignore_bad_template
-            )
-
     def validate_template(self, template: dict[str, Any]) -> Iterator[Match]:
         """
         Validate a single CloudFormation template and yield any matches found.
@@ -251,15 +219,24 @@ def run(self) -> Iterator[Match]:
             None: This function does not raise any exceptions.
         """
 
-        if (not sys.stdin.isatty()) and (not self.config.templates_to_process):
-            yield from self._validate_filenames([None])
+        if (
+            not sys.stdin.isatty()
+            and not self.config.templates
+            and not self.config.deployment_files
+        ):
+            yield from run_template_by_pipe(self.config, self.rules)
             return
 
-        if self.config.templates:
-            yield from self._validate_filenames(self.config.templates)
+        if self.config.deployment_files:
+            for template_config, matches in expand_deployment_files(self.config):
+                if not template_config:
+                    yield from matches
+                    continue
+                yield from run_template_by_file_paths(template_config, self.rules)
+
             return
 
-        yield from run_deployment_files(self.config, self.rules)
+        yield from run_template_by_file_paths(self.config, self.rules)
 
     def cli(self) -> None:
         """
@@ -301,14 +278,14 @@ def cli(self) -> None:
             print(self.rules)
             sys.exit(0)
 
-        if not self.config.templates_to_process and not self.config.deployment_files:
+        if not self.config.templates and not self.config.deployment_files:
             if sys.stdin.isatty():
                 self.config.parser.print_help()
                 sys.exit(1)
 
         if self.config.templates and self.config.deployment_files:
             self.config.parser.print_help()
-            sys.exit(32)
+            sys.exit(1)
 
         try:
             self._cli_output(list(self.run()))
diff --git a/src/cfnlint/runner/deployment_file/__init__.py b/src/cfnlint/runner/deployment_file/__init__.py
index e9d82a7326..d5fc4ec8ca 100644
--- a/src/cfnlint/runner/deployment_file/__init__.py
+++ b/src/cfnlint/runner/deployment_file/__init__.py
@@ -3,6 +3,6 @@
 SPDX-License-Identifier: MIT-0
 """
 
-__all__ = ["run_deployment_files"]
+__all__ = ["expand_deployment_files"]
 
-from cfnlint.runner.deployment_file.runner import run_deployment_files
+from cfnlint.runner.deployment_file.runner import expand_deployment_files
diff --git a/src/cfnlint/runner/deployment_file/deployment.py b/src/cfnlint/runner/deployment_file/deployment.py
index ce67716b4f..b2a45d4a16 100644
--- a/src/cfnlint/runner/deployment_file/deployment.py
+++ b/src/cfnlint/runner/deployment_file/deployment.py
@@ -10,7 +10,7 @@
 
 
 @dataclass(frozen=True)
-class Deployment:
+class DeploymentFileData:
 
     template_file_path: str = field()
     parameters: dict[str, Any] = field(default_factory=dict)
diff --git a/src/cfnlint/runner/deployment_file/deployment_types/git_sync.py b/src/cfnlint/runner/deployment_file/deployment_types/git_sync.py
index 6af58ebd92..b2e5fa6fb2 100644
--- a/src/cfnlint/runner/deployment_file/deployment_types/git_sync.py
+++ b/src/cfnlint/runner/deployment_file/deployment_types/git_sync.py
@@ -11,12 +11,12 @@
 from cfnlint._typing import RuleMatches
 from cfnlint.helpers import load_resource
 from cfnlint.rules.deployment_files.Configuration import Configuration
-from cfnlint.runner.deployment_file.deployment import Deployment
+from cfnlint.runner.deployment_file.deployment import DeploymentFileData
 
 
 def create_deployment_from_git_sync(
     data: dict[str, Any]
-) -> tuple[Deployment | None, RuleMatches | None]:
+) -> tuple[DeploymentFileData | None, RuleMatches | None]:
 
     schema = load_resource(cfnlint.data.schemas.other.deployment_files, "git_sync.json")
     matches = Configuration().validate_deployment_file(data, schema)
@@ -27,7 +27,7 @@ def create_deployment_from_git_sync(
     parameters: dict[str, Any] = data.get("parameters", {})
     tags: dict[str, Any] = data.get("tags", {})
     return (
-        Deployment(
+        DeploymentFileData(
             template_file_path=template_file_path, parameters=parameters, tags=tags
         ),
         None,
diff --git a/src/cfnlint/runner/deployment_file/runner.py b/src/cfnlint/runner/deployment_file/runner.py
index a65101c56b..2938273a5a 100644
--- a/src/cfnlint/runner/deployment_file/runner.py
+++ b/src/cfnlint/runner/deployment_file/runner.py
@@ -6,44 +6,28 @@
 from __future__ import annotations
 
 import logging
-from copy import deepcopy
 from pathlib import Path
 from typing import Iterator
 
 import cfnlint.runner.deployment_file.deployment_types
 from cfnlint.config import ConfigMixIn
+from cfnlint.context.parameters import ParameterSet
 from cfnlint.decode import decode
-from cfnlint.rules import Match, RuleMatch, Rules
+from cfnlint.rules import Match, RuleMatch
 from cfnlint.rules.deployment_files.Configuration import Configuration
-from cfnlint.runner.template import run_template_by_file_path
+from cfnlint.runner.deployment_file.deployment import DeploymentFileData
 
 LOGGER = logging.getLogger(__name__)
 
 
-def run_deployment_file(
-    filename: str, config: ConfigMixIn, rules: Rules
-) -> Iterator[Match]:
-    """
-    Run a single deployment file specified in the configuration.
-
-    Args:
-        filename (str): The filename of the deployment file to be run.
-        config (ConfigMixIn): The configuration object containing
-        settings for the deployment file scan.
-
-    Yields:
-
-    """
+def _parse_deployment_file(
+    filename: str,
+) -> DeploymentFileData | list[Match]:
 
     data, matches = decode(filename)
 
-    if matches:
-        yield from iter(matches)
-        return
-
-    ignore_bad_template: bool = False
-    if config.ignore_bad_template:
-        ignore_bad_template = True
+    if data is None:
+        return matches
 
     all_matches: list[RuleMatch] = []
     for plugin in cfnlint.runner.deployment_file.deployment_types.__all__:
@@ -53,48 +37,30 @@ def run_deployment_file(
         if deployment_matches:
             all_matches.extend(deployment_matches)
             continue
-        try:
-            template_path = (
-                (Path(filename).parent / deployment_data.template_file_path)
-                .resolve()
-                .relative_to(Path.cwd())
-            )
-        except ValueError:
-            LOGGER.debug(
-                (
-                    f"Template file path {deployment_data.template_file_path!r} "
-                    "is not relative to the current working directory"
-                )
-            )
-            template_path = Path(filename).parent / deployment_data.template_file_path
-        template_config = deepcopy(config)
-        template_config.parameters = [deployment_data.parameters]
-
-        yield from run_template_by_file_path(
-            filename=template_path,
-            config=template_config,
-            rules=rules,
-            ignore_bad_template=ignore_bad_template,
-        )
-        return
+
+        return deployment_data  # type: ignore
 
     for match in all_matches:
         LOGGER.debug(
             f"While tring to process deployment file got error: {match.message}"
         )
 
-    yield Match(
-        linenumber=1,
-        columnnumber=1,
-        linenumberend=1,
-        columnnumberend=1,
-        filename=filename,
-        message=f"Deployment file {filename!r} is not supported",
-        rule=Configuration(),
-    )
+    return [
+        Match(
+            linenumber=1,
+            columnnumber=1,
+            linenumberend=1,
+            columnnumberend=1,
+            filename=filename,
+            message=f"Deployment file {filename!r} is not supported",
+            rule=Configuration(),
+        )
+    ]
 
 
-def run_deployment_files(config: ConfigMixIn, rules: Rules) -> Iterator[Match]:
+def expand_deployment_files(
+    config: ConfigMixIn,
+) -> Iterator[tuple[ConfigMixIn | None, list[Match]]]:
     """
     Run the deployment files specified in the configuration.
 
@@ -106,5 +72,44 @@ def run_deployment_files(config: ConfigMixIn, rules: Rules) -> Iterator[Match]:
 
     """
 
+    deployments: dict[str, list[ParameterSet]] = {}
     for deployment_file in config.deployment_files:
-        yield from run_deployment_file(deployment_file, deepcopy(config), rules)
+        deployment_data = _parse_deployment_file(deployment_file)
+
+        if not isinstance(deployment_data, DeploymentFileData):
+            yield None, deployment_data
+            continue
+
+        try:
+            template_path = (
+                (Path(deployment_file).parent / deployment_data.template_file_path)
+                .resolve()
+                .relative_to(Path.cwd())
+            )
+        except ValueError:
+            LOGGER.debug(
+                (
+                    f"Template file path {deployment_data.template_file_path!r} "
+                    "is not relative to the current working directory"
+                )
+            )
+            template_path = (
+                Path(deployment_file).parent / deployment_data.template_file_path
+            )
+
+        if str(template_path) in deployments:
+            deployments[str(template_path)].append(
+                ParameterSet(deployment_file, deployment_data.parameters)
+            )
+        else:
+            deployments[str(template_path)] = [
+                ParameterSet(deployment_file, deployment_data.parameters)
+            ]
+
+    for path, parameter_sets in deployments.items():
+        template_config = config.evolve(
+            templates=[path],
+            parameters=parameter_sets,
+            deployment_files=[],
+        )
+        yield template_config, []
diff --git a/src/cfnlint/runner/exceptions.py b/src/cfnlint/runner/exceptions.py
deleted file mode 100644
index f99d1d7e34..0000000000
--- a/src/cfnlint/runner/exceptions.py
+++ /dev/null
@@ -1,55 +0,0 @@
-"""
-Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved.
-SPDX-License-Identifier: MIT-0
-"""
-
-from __future__ import annotations
-
-
-class CfnLintExitException(Exception):
-    """
-    An exception that is raised to indicate that the CloudFormation linter should exit.
-
-    This exception is used to signal that the linter should exit
-    with a specific exit code, typically indicating the severity
-    of the issues found in the CloudFormation template.
-
-    Attributes:
-        exit_code (int): The exit code to be used when the linter exits.
-
-    Methods:
-        __init__(self, exit_code: int) -> None:
-            Initialize a new CfnLintExitException instance with the specified exit code.
-    """
-
-    def __init__(self, msg=None, exit_code=1):
-        """
-        Initialize a new CfnLintExitException instance with the specified exit code.
-
-        Args:
-            exit_code (int): The exit code to be used when the linter exits.
-        """
-        if msg is None:
-            msg = f"process failed with exit code {exit_code}"
-        super().__init__(msg)
-        self.exit_code = exit_code
-
-
-class InvalidRegionException(CfnLintExitException):
-    """
-    An exception that is raised when an invalid AWS region is encountered.
-
-    This exception is raised when the CloudFormation linter encounters a resource
-    or parameter that references an AWS region that is not valid or supported.
-    """
-
-
-class UnexpectedRuleException(CfnLintExitException):
-    """
-    An exception that is raised when an unexpected error occurs while loading rules.
-
-    This exception is raised when the CloudFormation linter encounters an error
-    while attempting to load custom rules or rules from a specified directory or
-    module. This could be due to a variety of reasons, such as a missing file,
-    a syntax error in the rule code, or an issue with the rule implementation.
-    """
diff --git a/src/cfnlint/runner/template/__init__.py b/src/cfnlint/runner/template/__init__.py
index 3ec5ca82fa..0144158fb7 100644
--- a/src/cfnlint/runner/template/__init__.py
+++ b/src/cfnlint/runner/template/__init__.py
@@ -3,9 +3,16 @@
 SPDX-License-Identifier: MIT-0
 """
 
-__all__ = ["run_template_by_file_path", "run_template_by_data"]
+__all__ = [
+    "run_template_by_file_path",
+    "run_template_by_data",
+    "run_template_by_pipe",
+    "run_template_by_file_paths",
+]
 
 from cfnlint.runner.template.runner import (
     run_template_by_data,
     run_template_by_file_path,
+    run_template_by_file_paths,
+    run_template_by_pipe,
 )
diff --git a/src/cfnlint/runner/template/runner.py b/src/cfnlint/runner/template/runner.py
index 1155bb886b..9c570c0999 100644
--- a/src/cfnlint/runner/template/runner.py
+++ b/src/cfnlint/runner/template/runner.py
@@ -10,10 +10,10 @@
 
 from cfnlint.config import ConfigMixIn
 from cfnlint.decode import decode
+from cfnlint.exceptions import InvalidRegionException
 from cfnlint.helpers import REGIONS
 from cfnlint.rules import Match, Rules
-from cfnlint.rules.errors import TransformError
-from cfnlint.runner.exceptions import InvalidRegionException
+from cfnlint.rules.errors import ParseError, TransformError
 from cfnlint.template.template import Template
 
 LOGGER = logging.getLogger(__name__)
@@ -107,15 +107,8 @@ def _run_template(
 ) -> Iterator[Match]:
 
     config.set_template_args(template)
-    if config.parameters:
-        matches: list[Match] = []
-        for parameters in config.parameters:
-            cfn = Template(filename, template, config.regions, parameters)
-            matches.extend(list(_run_template_per_config(cfn, config, rules)))
-        yield from _dedup(iter(matches))
-    else:
-        cfn = Template(filename, template, config.regions)
-        yield from _dedup(_run_template_per_config(cfn, config, rules))
+    cfn = Template(filename, template, config.regions, config.parameters)
+    yield from _dedup(_run_template_per_config(cfn, config, rules))
 
 
 def run_template_by_file_path(
@@ -157,3 +150,57 @@ def run_template_by_data(
     """
 
     yield from _run_template(None, template, config, rules)
+
+
+def run_template_by_pipe(config: ConfigMixIn, rules: Rules) -> Iterator[Match]:
+    """
+    Runs a set of rules against a CloudFormation template.
+
+    Attributes:
+        config (ConfigMixIn): The configuration object containing
+        settings for the template scan.
+        cfn (Template): The CloudFormation template object.
+        rules (Rules): The set of rules to be applied to the template.
+    """
+
+    (template, matches) = decode(None)  # type: ignore
+    if matches:
+        yield from iter(matches)
+        return
+    yield from run_template_by_data(template, config, rules)  # type: ignore
+
+
+def run_template_by_file_paths(config: ConfigMixIn, rules: Rules) -> Iterator[Match]:
+    """
+    Validate the specified filenames and yield any matches found.
+
+    This function processes each filename in the provided sequence, decoding the
+    template and validating it against the configured rules. Any matches found
+    are yielded as an iterator.
+
+    Args:
+        filenames (Sequence[str | None]): The sequence of filenames to be validated.
+
+    Yields:
+        Match: The matches found during the validation process.
+
+    Raises:
+        None: This function does not raise any exceptions.
+    """
+    ignore_bad_template: bool = False
+    if config.ignore_bad_template:
+        ignore_bad_template = True
+    else:
+        # There is no collection at this point so we need to handle this
+        # check directly
+        if not ParseError().is_enabled(
+            include_experimental=False,
+            ignore_rules=config.ignore_checks,
+            include_rules=config.include_checks,
+            mandatory_rules=config.mandatory_checks,
+        ):
+            ignore_bad_template = True
+    for filename in config.templates:
+        yield from run_template_by_file_path(
+            filename, config, rules, ignore_bad_template
+        )
diff --git a/src/cfnlint/template/template.py b/src/cfnlint/template/template.py
index 325a4e5331..e8678d162f 100644
--- a/src/cfnlint/template/template.py
+++ b/src/cfnlint/template/template.py
@@ -15,7 +15,7 @@
 import cfnlint.conditions
 import cfnlint.helpers
 from cfnlint._typing import CheckValueFn, Path
-from cfnlint.context import Context, create_context_for_template
+from cfnlint.context import Context, ParameterSet, create_context_for_template
 from cfnlint.context.conditions.exceptions import Unsatisfiable
 from cfnlint.decode.node import dict_node, list_node
 from cfnlint.graph import Graph
@@ -52,7 +52,7 @@ def __init__(
         filename: str | None,
         template: dict[str, Any],
         regions: list[str] | None = None,
-        parameters: dict[str, Any] | None = None,
+        parameter_sets: list[ParameterSet] | None = None,
     ):
         """Initialize a Template instance.
 
@@ -61,13 +61,9 @@ def __init__(
             template (dict[str, Any]): The dictionary representing the CloudFormation template.
             regions (list[str] | None): A list of AWS regions associated with the template.
         """
-        if regions is None:
-            self.regions = [cfnlint.helpers.REGION_PRIMARY]
-        else:
-            self.regions = regions
-        self.parameters = (
-            parameters  # None represents no parameters are provided at all
-        )
+
+        self.regions = regions or [cfnlint.helpers.REGION_PRIMARY]
+
         self.filename = filename
         self.template = template
         self.transform_pre: dict[str, Any] = {}
@@ -95,6 +91,8 @@ def __init__(
             LOGGER.info("Encountered unknown error while building graph: %s", err)
 
         self.context = create_context_for_template(self)
+        if parameter_sets:
+            self.context = self.context.evolve(parameter_sets=parameter_sets)
         self.search_deep_keys = functools.lru_cache()(self.search_deep_keys)  # type: ignore
 
     def __deepcopy__(self, memo):
diff --git a/test/fixtures/deployment_files/dev.yaml b/test/fixtures/deployment_files/dev.yaml
new file mode 100644
index 0000000000..792a7f1c69
--- /dev/null
+++ b/test/fixtures/deployment_files/dev.yaml
@@ -0,0 +1,5 @@
+template-file-path: ../templates/integration/deployment-file-template.yaml
+parameters:
+  Environment: Dev
+tags:
+  Environment: Dev
diff --git a/test/fixtures/deployment_files/prod.yaml b/test/fixtures/deployment_files/prod.yaml
new file mode 100644
index 0000000000..546eb113d5
--- /dev/null
+++ b/test/fixtures/deployment_files/prod.yaml
@@ -0,0 +1,6 @@
+template-file-path: ../templates/integration/deployment-file-template.yaml
+parameters:
+  AvailabilityZone: us-east-1a
+  ImageId: ami-12345678
+tags:
+  Environment: Prod
diff --git a/test/fixtures/templates/integration/deployment-file-template.yaml b/test/fixtures/templates/integration/deployment-file-template.yaml
new file mode 100644
index 0000000000..8b0aa76876
--- /dev/null
+++ b/test/fixtures/templates/integration/deployment-file-template.yaml
@@ -0,0 +1,25 @@
+Parameters:
+  AvailabilityZone:
+    Type: String
+    Default: us-east-1a
+    Description: The availability zone in which to deploy
+  ImageId:
+    Type: AWS::EC2::Image::Id
+    Description: The AMI to use for the instance
+Resources:
+  Vpc:
+    Type: AWS::EC2::VPC
+    Properties:
+      CidrBlock: "10.0.0.0/16"
+  Subnet1:
+    Type: AWS::EC2::Subnet
+    Properties:
+      AvailabilityZone: !Ref AvailabilityZone
+      CidrBlock: "10.0.0.0/16"
+      VpcId: !Ref Vpc
+  MyInstance:
+    Type: AWS::EC2::Instance
+    Properties:
+      ImageId: !Ref ImageId
+      InstanceType: t2.micro
+      SubnetId: !GetAtt Subnet1.SubnetId
diff --git a/test/integration/test_deployment_files.py b/test/integration/test_deployment_files.py
new file mode 100644
index 0000000000..4e75d08a67
--- /dev/null
+++ b/test/integration/test_deployment_files.py
@@ -0,0 +1,70 @@
+"""
+Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
+SPDX-License-Identifier: MIT-0
+"""
+
+import pytest
+
+from cfnlint.config import ConfigMixIn
+from cfnlint.rules import Match
+from cfnlint.rules.deployment_files.Parameters import Parameters
+from cfnlint.runner import Runner
+
+
+@pytest.mark.parametrize(
+    ("name,deployment_files,expected"),
+    [
+        (
+            "Correctly configured deployment file",
+            ["test/fixtures/deployment_files/prod.yaml"],
+            [],
+        ),
+        (
+            "Incorrectly configured deployment file",
+            ["test/fixtures/deployment_files/dev.yaml"],
+            [
+                Match(
+                    message="'ImageId' is a required property",
+                    rule=Parameters(),
+                    filename="test/fixtures/deployment_files/dev.yaml",
+                    linenumber=1,
+                    linenumberend=1,
+                    columnnumber=1,
+                    columnnumberend=11,
+                ),
+                Match(
+                    message=(
+                        "Additional properties are not allowed "
+                        "('Environment' was unexpected)"
+                    ),
+                    rule=Parameters(),
+                    filename="test/fixtures/deployment_files/dev.yaml",
+                    linenumber=3,
+                    linenumberend=3,
+                    columnnumber=3,
+                    columnnumberend=14,
+                ),
+            ],
+        ),
+        (
+            "Multiple deployment files",
+            ["test/fixtures/deployment_files/prod.yaml"],
+            [],
+        ),
+    ],
+)
+def test_deployment_files(
+    name,
+    deployment_files,
+    expected,
+):
+
+    config = ConfigMixIn(
+        cli_args=[],
+        deployment_files=deployment_files,
+    )
+
+    runner = Runner(config)
+    results = list(runner.run())
+
+    assert results == expected, f"{name}: {results} != {expected}"
diff --git a/test/integration/test_run_deployment_files.py b/test/integration/test_run_deployment_files.py
deleted file mode 100644
index e69de29bb2..0000000000
diff --git a/test/unit/module/config/test_cli_args.py b/test/unit/module/config/test_cli_args.py
index 43f6b953be..b38b3ddbe5 100644
--- a/test/unit/module/config/test_cli_args.py
+++ b/test/unit/module/config/test_cli_args.py
@@ -126,7 +126,7 @@ def test_bad_rule_configuration(self, mock_print_help):
         with self.assertRaises(SystemExit) as e:
             cfnlint.config.CliArgs(["-x", "E3012:key;value"])
 
-        self.assertEqual(e.exception.code, 32)
+        self.assertEqual(e.exception.code, 1)
         mock_print_help.assert_called_once()
 
     def test_exit_code_parameter(self):
diff --git a/test/unit/module/config/test_config_mixin.py b/test/unit/module/config/test_config_mixin.py
index 83babcd86f..ae41e3c6ec 100644
--- a/test/unit/module/config/test_config_mixin.py
+++ b/test/unit/module/config/test_config_mixin.py
@@ -9,7 +9,8 @@
 from test.testlib.testcase import BaseTestCase
 from unittest.mock import patch
 
-import cfnlint.config  # pylint: disable=E0401
+import cfnlint.config
+from cfnlint.context import ParameterSet
 from cfnlint.helpers import REGIONS
 
 LOGGER = logging.getLogger("cfnlint")
@@ -306,7 +307,7 @@ def test_parameters(self, yaml_mock):
         config = cfnlint.config.ConfigMixIn(["--parameters", "Foo=Bar"])
 
         # test defaults
-        self.assertEqual(config.parameters, [{"Foo": "Bar"}])
+        self.assertEqual(getattr(config.cli_args, "parameters"), [{"Foo": "Bar"}])
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
     def test_parameters_lists(self, yaml_mock):
@@ -314,7 +315,7 @@ def test_parameters_lists(self, yaml_mock):
         config = cfnlint.config.ConfigMixIn(["--parameters", "A=1", "B=2"])
 
         # test defaults
-        self.assertEqual(config.parameters, [{"A": "1", "B": "2"}])
+        self.assertEqual(getattr(config.cli_args, "parameters"), [{"A": "1", "B": "2"}])
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
     def test_parameters_lists_bad_value(self, yaml_mock):
@@ -350,5 +351,13 @@ def test_templates_with_deployment_files(self, mock_print_help):
                 ]
             )
 
-        self.assertEqual(e.exception.code, 32)
+        self.assertEqual(e.exception.code, 1)
         mock_print_help.assert_called_once()
+
+    def test_conversion_of_template_parameters(self):
+
+        config = cfnlint.ConfigMixIn(["--parameters", "Foo=Bar"])
+
+        self.assertEqual(
+            config.parameters, [ParameterSet(source=None, parameters={"Foo": "Bar"})]
+        )
diff --git a/test/unit/module/jsonschema/test_resolvers_cfn.py b/test/unit/module/jsonschema/test_resolvers_cfn.py
index a64ff6dd08..d5c84fbc1c 100644
--- a/test/unit/module/jsonschema/test_resolvers_cfn.py
+++ b/test/unit/module/jsonschema/test_resolvers_cfn.py
@@ -15,7 +15,6 @@
 
 def _resolve(name, instance, expected_results, **kwargs):
     validator = CfnTemplateValidator().evolve(**kwargs)
-
     resolutions = list(validator.resolve_value(instance))
 
     assert len(resolutions) == len(
diff --git a/test/unit/module/runner/deployment_file/deployment_types/test_git_sync.py b/test/unit/module/runner/deployment_file/deployment_types/test_git_sync.py
index e35b18f123..f74c2c2bbc 100644
--- a/test/unit/module/runner/deployment_file/deployment_types/test_git_sync.py
+++ b/test/unit/module/runner/deployment_file/deployment_types/test_git_sync.py
@@ -6,7 +6,7 @@
 import pytest
 
 from cfnlint.rules import RuleMatch
-from cfnlint.runner.deployment_file.deployment import Deployment
+from cfnlint.runner.deployment_file.deployment import DeploymentFileData
 from cfnlint.runner.deployment_file.deployment_types import (
     create_deployment_from_git_sync,
 )
@@ -27,7 +27,7 @@
                 },
             },
             (
-                Deployment(
+                DeploymentFileData(
                     template_file_path="../a/path",
                     parameters={
                         "Foo": "Bar",
diff --git a/test/unit/module/runner/deployment_file/test_runner.py b/test/unit/module/runner/deployment_file/test_runner.py
index 6f317c16b6..c37ed33e89 100644
--- a/test/unit/module/runner/deployment_file/test_runner.py
+++ b/test/unit/module/runner/deployment_file/test_runner.py
@@ -3,29 +3,31 @@
 SPDX-License-Identifier: MIT-0
 """
 
-from pathlib import Path
-from unittest.mock import patch
+from unittest.mock import MagicMock, PropertyMock, patch
 
 import pytest
 
 from cfnlint.config import ConfigMixIn
+from cfnlint.context import ParameterSet
 from cfnlint.rules import Match
 from cfnlint.rules.deployment_files.Configuration import Configuration
-from cfnlint.runner.deployment_file import run_deployment_files
+from cfnlint.runner.deployment_file import expand_deployment_files
 
-_filename = "deployment-file.yaml"
+_filename_dev = "deployment-dev.yaml"
+_filename_prod = "deployment-prod.yaml"
+
+
+def mock_glob(value, recursive):
+    return [value]
 
 
 @pytest.mark.parametrize(
-    (
-        "name,deployment_files,validate_template_parameters,"
-        "validate_template_return,expected"
-    ),
+    ("name,deployment_files,expected"),
     [
         (
             "A standard git sync file",
             {
-                _filename: (
+                _filename_dev: (
                     {
                         "template-file-path": "../a/path",
                         "parameters": {
@@ -38,19 +40,30 @@
                     [],
                 ),
             },
-            {
-                "filename": Path("../a/path"),
-                "parameters": [{"Foo": "Bar"}],
-            },
-            [],
-            [],
+            [
+                (
+                    ConfigMixIn(
+                        [],
+                        deployment_files=[_filename_dev],
+                        parameters=[
+                            ParameterSet(
+                                source=_filename_dev, parameters={"Foo": "Bar"}
+                            )
+                        ],
+                        templates=[
+                            "../a/path",
+                        ],
+                    ),
+                    [],
+                )
+            ],
         ),
         (
-            "Bad template-file-path type",
+            "Multiple standard git sync file with same template path",
             {
-                _filename: (
+                _filename_dev: (
                     {
-                        "template-file-path": ["../a/path"],
+                        "template-file-path": "../a/path",
                         "parameters": {
                             "Foo": "Bar",
                         },
@@ -59,26 +72,45 @@
                         },
                     },
                     [],
-                )
+                ),
+                _filename_prod: (
+                    {
+                        "template-file-path": "../a/path",
+                        "parameters": {
+                            "Foo": "Foo",
+                        },
+                        "tags": {
+                            "Key": "Value",
+                        },
+                    },
+                    [],
+                ),
             },
-            {},
-            [],
             [
-                Match(
-                    linenumber=1,
-                    columnnumber=1,
-                    linenumberend=1,
-                    columnnumberend=1,
-                    filename=_filename,
-                    message=f"Deployment file {_filename!r} is not supported",
-                    rule=Configuration(),
-                )
+                (
+                    ConfigMixIn(
+                        [],
+                        deployment_files=[_filename_dev, _filename_prod],
+                        parameters=[
+                            ParameterSet(
+                                source=_filename_dev, parameters={"Foo": "Bar"}
+                            ),
+                            ParameterSet(
+                                source=_filename_prod, parameters={"Foo": "Foo"}
+                            ),
+                        ],
+                        templates=[
+                            "../a/path",
+                        ],
+                    ),
+                    [],
+                ),
             ],
         ),
         (
-            "Bad template-file-path type",
+            "Multiple standard git sync file with a different template path",
             {
-                _filename: (
+                _filename_dev: (
                     {
                         "template-file-path": "../a/path",
                         "parameters": {
@@ -90,66 +122,120 @@
                     },
                     [],
                 ),
+                _filename_prod: (
+                    {
+                        "template-file-path": "../b/path",
+                        "parameters": {
+                            "Foo": "Foo",
+                        },
+                        "tags": {
+                            "Key": "Value",
+                        },
+                    },
+                    [],
+                ),
             },
+            [
+                (
+                    ConfigMixIn(
+                        [],
+                        deployment_files=[_filename_dev],
+                        parameters=[
+                            ParameterSet(
+                                source=_filename_dev, parameters={"Foo": "Bar"}
+                            ),
+                        ],
+                        templates=[
+                            "../a/path",
+                        ],
+                    ),
+                    [],
+                ),
+                (
+                    ConfigMixIn(
+                        [],
+                        deployment_files=[_filename_prod],
+                        parameters=[
+                            ParameterSet(
+                                source=_filename_prod, parameters={"Foo": "Foo"}
+                            ),
+                        ],
+                        templates=[
+                            "../b/path",
+                        ],
+                    ),
+                    [],
+                ),
+            ],
+        ),
+        (
+            "Bad template-file-path type",
             {
-                "filename": Path("../a/path"),
-                "parameters": [{"Foo": "Bar"}],
+                _filename_dev: (
+                    {
+                        "template-file-path": ["../a/path"],
+                        "parameters": {
+                            "Foo": "Bar",
+                        },
+                        "tags": {
+                            "Key": "Value",
+                        },
+                    },
+                    [],
+                )
             },
-            iter(
-                [
-                    Match(
-                        linenumber=1,
-                        columnnumber=1,
-                        linenumberend=1,
-                        columnnumberend=1,
-                        filename=_filename,
-                        message=f"Deployment file {_filename!r} is not supported",
-                        rule=Configuration(),
-                    )
-                ]
-            ),
             [
-                Match(
-                    linenumber=1,
-                    columnnumber=1,
-                    linenumberend=1,
-                    columnnumberend=1,
-                    filename=_filename,
-                    message=f"Deployment file {_filename!r} is not supported",
-                    rule=Configuration(),
+                (
+                    None,
+                    [
+                        Match(
+                            linenumber=1,
+                            columnnumber=1,
+                            linenumberend=1,
+                            columnnumberend=1,
+                            filename=_filename_dev,
+                            message=(
+                                f"Deployment file {_filename_dev!r} " "is not supported"
+                            ),
+                            rule=Configuration(),
+                        )
+                    ],
                 )
             ],
         ),
         (
             "Bad decode",
             {
-                _filename: (
-                    {},
+                _filename_dev: (
+                    None,
                     [
                         Match(
                             linenumber=1,
                             columnnumber=1,
                             linenumberend=1,
                             columnnumberend=1,
-                            filename=_filename,
-                            message=f"Deployment file {_filename!r} is not supported",
+                            filename=_filename_dev,
+                            message="Random failure",
                             rule=Configuration(),
                         )
                     ],
                 ),
             },
-            {},
-            None,
             [
-                Match(
-                    linenumber=1,
-                    columnnumber=1,
-                    linenumberend=1,
-                    columnnumberend=1,
-                    filename=_filename,
-                    message=f"Deployment file {_filename!r} is not supported",
-                    rule=Configuration(),
-                )
+                (
+                    None,
+                    [
+                        Match(
+                            linenumber=1,
+                            columnnumber=1,
+                            linenumberend=1,
+                            columnnumberend=1,
+                            filename=_filename_dev,
+                            message="Random failure",
+                            rule=Configuration(),
+                        ),
+                    ],
+                ),
             ],
         ),
     ],
@@ -157,32 +243,24 @@
 def test_runner(
     name,
     deployment_files,
-    validate_template_parameters,
-    validate_template_return,
     expected,
 ):
 
     decode_results = [v for _, v in deployment_files.items()]
-    deployment_files = {k for k, _ in deployment_files.items()}
-    with patch("cfnlint.runner.deployment_file.runner.decode") as mock_decode:
-        mock_decode.side_effect = decode_results
+    deployment_files = [k for k, _ in deployment_files.items()]
+    with patch("glob.glob", MagicMock(side_effect=mock_glob)):
+
         with patch(
-            "cfnlint.runner.deployment_file.runner.run_template_by_file_path",
-            return_value=validate_template_return,
-        ) as mock_run_template_by_file_path:
-            config = ConfigMixIn([], deployment_files=deployment_files)
-            deployment = list(run_deployment_files(config, None))
+            "cfnlint.config.ConfigMixIn.deployment_files", new_callable=PropertyMock
+        ) as mock_deployment_files:
+            with patch("cfnlint.runner.deployment_file.runner.decode") as mock_decode:
+                mock_deployment_files.return_value = deployment_files
+                mock_decode.side_effect = decode_results
+                config = ConfigMixIn([], deployment_files=deployment_files)
+
+                deployments = list(expand_deployment_files(config))
 
-            for deployment_file in deployment_files:
-                mock_decode.assert_called_with(deployment_file)
-            if validate_template_parameters:
-                mock_run_template_by_file_path.assert_called_once()
-                config = mock_run_template_by_file_path.call_args_list
-                assert config[0].kwargs.get(
-                    "config"
-                ).parameters == validate_template_parameters.get("parameters")
-                assert config[0].kwargs.get(
-                    "filename"
-                ) == validate_template_parameters.get("filename")
+                for deployment_file in deployment_files:
+                    mock_decode.assert_any_call(deployment_file)
 
-    assert deployment == expected, f"{name}: {deployment} != {expected}"
+                assert deployments == expected, f"{name}: {deployments} != {expected}"
diff --git a/test/unit/module/runner/template/test_run_template_by_data.py b/test/unit/module/runner/template/test_run_template_by_data.py
index 8c2c8664bb..d0bdf69242 100644
--- a/test/unit/module/runner/template/test_run_template_by_data.py
+++ b/test/unit/module/runner/template/test_run_template_by_data.py
@@ -8,6 +8,7 @@
 import pytest
 
 from cfnlint.config import ConfigMixIn
+from cfnlint.context import ParameterSet
 from cfnlint.rules import RulesCollection
 from cfnlint.runner.template import run_template_by_data
 
@@ -21,26 +22,39 @@
             [
                 iter([]),
             ],
-            [None],
+            [],
         ),
         (
             "One set of parameters",
-            ConfigMixIn(parameters=[{"Foo": "Bar"}]),
+            ConfigMixIn(
+                parameters=[ParameterSet(source=None, parameters={"Foo": "Bar"})]
+            ),
             [
                 iter([]),
             ],
-            [{"Foo": "Bar"}],
+            [ParameterSet(source=None, parameters={"Foo": "Bar"})],
         ),
         (
             "Multiple parameters",
-            ConfigMixIn(parameters=[{"A": "B"}, {"C": "D"}]),
+            ConfigMixIn(
+                parameters=[
+                    ParameterSet(source=None, parameters={"A": "B"}),
+                    ParameterSet(source=None, parameters={"C": "D"}),
+                ]
+            ),
             [
                 iter([]),
                 iter([]),
             ],
             [
-                {"A": "B"},
-                {"C": "D"},
+                ParameterSet(
+                    source=None,
+                    parameters={"A": "B"},
+                ),
+                ParameterSet(
+                    source=None,
+                    parameters={"C": "D"},
+                ),
             ],
         ),
     ],
@@ -57,9 +71,9 @@ def test_runner(
     ) as mock_run:
         list(run_template_by_data({}, config, RulesCollection()))
 
-        calls = mock_run.call_args_list
-        for index, call in enumerate(calls):
-            assert call.kwargs["cfn"].parameters == expected_parameters[index], (
-                f"{name}: {call.kwargs['cfn'].parameters} "
-                f"!= {expected_parameters[index]}"
-            )
+        assert mock_run.call_count == 1
+        call = mock_run.call_args
+        assert call.kwargs["cfn"].context.parameter_sets == expected_parameters, (
+            f"{name}: {call.kwargs['cfn'].context.parameter_sets} "
+            f"!= {expected_parameters}"
+        )
diff --git a/test/unit/module/runner/test_cli.py b/test/unit/module/runner/test_cli.py
index 7451f27bc9..f8635780ed 100644
--- a/test/unit/module/runner/test_cli.py
+++ b/test/unit/module/runner/test_cli.py
@@ -4,10 +4,12 @@
 """
 
 import logging
+from io import StringIO
 from test.testlib.testcase import BaseTestCase
 from unittest.mock import patch
 
 from cfnlint import ConfigMixIn
+from cfnlint.helpers import format_json_string
 from cfnlint.runner import Runner
 
 LOGGER = logging.getLogger("cfnlint")
@@ -45,6 +47,18 @@ def test_update_specs(self, mock_maintenance):
         self.assertEqual(e.exception.code, 0)
         mock_maintenance.assert_called_once()
 
+    @patch("cfnlint.maintenance.patch_resource_specs")
+    def test_patch_specs(self, mock_maintenance):
+        config = ConfigMixIn(["--patch-specs"])
+
+        runner = Runner(config)
+
+        with self.assertRaises(SystemExit) as e:
+            runner.cli()
+
+        self.assertEqual(e.exception.code, 0)
+        mock_maintenance.assert_called_once()
+
     @patch("cfnlint.maintenance.update_iam_policies")
     def test_update_iam_policies(self, mock_maintenance):
         config = ConfigMixIn(["--update-iam-policies"])
@@ -116,5 +130,45 @@ def test_templates_with_deployment_files(self, mock_print_help):
         with self.assertRaises(SystemExit) as e:
             runner.cli()
 
-        self.assertEqual(e.exception.code, 32)
+        self.assertEqual(e.exception.code, 1)
         mock_print_help.assert_called_once()
+
+    @patch("fileinput.input")
+    @patch("sys.stdin.isatty")
+    def test_templates_with_stdin(self, mock_isatty, mock_fileinput):
+        template = {
+            "Resources": {
+                "Bucket": {
+                    "Type": "AWS::S3::Bucket",
+                }
+            }
+        }
+
+        mock_fileinput.return_value = StringIO(format_json_string(template))
+        mock_isatty.return_value = False
+
+        config = ConfigMixIn()
+
+        runner = Runner(config)
+
+        with self.assertRaises(SystemExit) as e:
+            runner.cli()
+
+        self.assertEqual(e.exception.code, 0)
+
+    @patch("fileinput.input")
+    @patch("sys.stdin.isatty")
+    def test_templates_with_stdin_with_bad_syntax(self, mock_isatty, mock_fileinput):
+        template = "{"
+
+        mock_fileinput.return_value = StringIO(template)
+        mock_isatty.return_value = False
+
+        config = ConfigMixIn()
+
+        runner = Runner(config)
+
+        with self.assertRaises(SystemExit) as e:
+            runner.cli()
+
+        self.assertEqual(e.exception.code, 2)
diff --git a/test/unit/module/runner/test_rule_configuration.py b/test/unit/module/runner/test_rule_configuration.py
index 54c5b11cac..444ad341e1 100644
--- a/test/unit/module/runner/test_rule_configuration.py
+++ b/test/unit/module/runner/test_rule_configuration.py
@@ -10,7 +10,6 @@
 from cfnlint.runner import Runner, UnexpectedRuleException
 
 
-
 class TestGetRules(BaseTestCase):
     """Test Run Checks"""
 
diff --git a/test/unit/rules/conftest.py b/test/unit/rules/conftest.py
index 3598af672d..910421b381 100644
--- a/test/unit/rules/conftest.py
+++ b/test/unit/rules/conftest.py
@@ -31,7 +31,7 @@ def regions():
 def parameters(request):
     if hasattr(request, "param"):
         return request.param
-    return None
+    return []
 
 
 @pytest.fixture
@@ -75,11 +75,12 @@ def strict_types(strict_types=True):
 
 
 @pytest.fixture
-def context(cfn, path, functions, strict_types):
+def context(cfn, path, functions, strict_types, parameters):
     return create_context_for_template(cfn).evolve(
         path=path,
         functions=functions,
         strict_types=strict_types,
+        parameter_sets=parameters,
     )
 
 
diff --git a/test/unit/rules/deployment_files/test_parameters.py b/test/unit/rules/deployment_files/test_parameters.py
index 27b2677bed..f00883907e 100644
--- a/test/unit/rules/deployment_files/test_parameters.py
+++ b/test/unit/rules/deployment_files/test_parameters.py
@@ -7,6 +7,7 @@
 
 import pytest
 
+from cfnlint.context import ParameterSet
 from cfnlint.jsonschema import ValidationError
 from cfnlint.rules.deployment_files.Parameters import Parameters
 
@@ -29,9 +30,14 @@ def rule():
         (
             "Parameter provided with no default",
             {"Foo": {"Type": "String"}},
-            {
-                "Foo": "Bar",
-            },
+            [
+                ParameterSet(
+                    source=None,
+                    parameters={
+                        "Foo": "Bar",
+                    },
+                )
+            ],
             [],
         ),
         (
@@ -43,7 +49,7 @@ def rule():
         (
             "Empty with no default should have an error",
             {"Foo": {"Type": "String"}},
-            {},
+            [ParameterSet(source=None, parameters={})],
             [
                 ValidationError(
                     "'Foo' is a required property",
@@ -57,7 +63,7 @@ def rule():
         (
             "Failure on bad enum",
             {"Foo": {"Type": "String", "AllowedValues": ["A", "B", "C"]}},
-            {"Foo": "D"},
+            [ParameterSet(source=None, parameters={"Foo": "D"})],
             [
                 ValidationError(
                     "'D' is not one of ['A', 'B', 'C']",
@@ -71,7 +77,7 @@ def rule():
         (
             "Failure on bad pattern",
             {"Foo": {"Type": "String", "Pattern": "^Bar$"}},
-            {"Foo": "D"},
+            [ParameterSet(source=None, parameters={"Foo": "D"})],
             [
                 ValidationError(
                     "'D' does not match '^Bar$'",
@@ -85,13 +91,13 @@ def rule():
         (
             "Okay with a list",
             {"Foo": {"Type": "CommaDelimitedList"}},
-            {"Foo": ["D"]},
+            [ParameterSet(source=None, parameters={"Foo": ["D"]})],
             [],
         ),
         (
             "Not okay with a list and a bad pattern",
             {"Foo": {"Type": "CommaDelimitedList", "Pattern": "^Bar$"}},
-            {"Foo": ["Bar", "D"]},
+            [ParameterSet(source=None, parameters={"Foo": ["Bar", "D"]})],
             [
                 ValidationError(
                     "'D' does not match '^Bar$'",
@@ -105,7 +111,7 @@ def rule():
         (
             "Not okay with a list and an enum",
             {"Foo": {"Type": "CommaDelimitedList", "AllowedValues": ["Bar"]}},
-            {"Foo": ["Bar", "D"]},
+            [ParameterSet(source=None, parameters={"Foo": ["Bar", "D"]})],
             [
                 ValidationError(
                     "'D' is not one of ['Bar']",
@@ -117,21 +123,21 @@ def rule():
             ],
         ),
         (
-            "Issues when a bad properties type",
+            "No issues when a bad property type",
             [{"Foo": {"Type": "CommaDelimitedList", "AllowedValues": ["Bar"]}}],
-            {"Foo": "Bar"},
+            [ParameterSet(source=None, parameters={"Foo": "Bar"})],
             [],
         ),
         (
-            "Issues when a bad property type",
+            "No issues when a bad property type",
             {"Foo": [{"Type": "CommaDelimitedList", "AllowedValues": ["Bar"]}]},
-            {"Foo": "Bar"},
+            [ParameterSet(source=None, parameters={"Foo": "Bar"})],
             [],
         ),
         (
-            "Issues when a bad type",
+            "No issues when a bad type",
             {"Foo": {"Type": ["String"], "AllowedValues": ["Bar"]}},
-            {"Foo": "Foo"},
+            [ParameterSet(source=None, parameters={"Foo": "Foo"})],
             [],
         ),
     ],