Skip to content

Commit e2a6c83

Browse files
Fail gracefully if no templates match wildcard (#3603)
*Description of changes:* `cfn-lint` currently fails if there are no Cloudformation templates that match the pattern specified either via CLI arguments or via config file. ``` $ cfn-lint "cfn/**/*.y*ml" 2024-08-16 13:41:38,381 - cfnlint.decode.decode - ERROR - Template file not found: cfn/**/*.y*ml E0000 Template file not found: cfn/**/*.y*ml cfn/**/*.y*ml:1:1 $ echo $? 2 ``` It appears that when the glob pattern matching does not find any match, the actual string is appended as a template file. This PR improves the handling of wildcard templates by ensuring only matched templates are added to be linted by `cfn-lint` and would gracefully exit without an output message. If run with debug switch, it lists the Cloudformation templates found by the glob. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. Co-authored-by: Kevin DeJong <[email protected]>
1 parent 67e39ee commit e2a6c83

File tree

5 files changed

+68
-38
lines changed

5 files changed

+68
-38
lines changed

src/cfnlint/config.py

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,7 @@ class ConfigMixIn(TemplateArgs, CliArgs, ConfigFileArgs):
624624

625625
def __init__(self, cli_args: list[str] | None = None, **kwargs: Unpack[ManualArgs]):
626626
self._manual_args = kwargs or ManualArgs()
627+
self._templates_to_process = False
627628
CliArgs.__init__(self, cli_args)
628629
# configure debug as soon as we can
629630
TemplateArgs.__init__(self, {})
@@ -732,23 +733,47 @@ def format(self):
732733

733734
@property
734735
def templates(self):
735-
templates_args = self._get_argument_value("templates", False, True)
736-
template_alt_args = self._get_argument_value("template_alt", False, False)
737-
if template_alt_args:
738-
filenames = template_alt_args
739-
elif templates_args:
740-
filenames = templates_args
736+
"""
737+
738+
Returns a list of Cloudformation templates to lint.
739+
740+
Order of precedence:
741+
- Filenames provided via `-t` CLI
742+
- Filenames specified in the config file.
743+
- Arguments provided via `cfn-lint` CLI.
744+
"""
745+
746+
all_filenames = []
747+
748+
cli_alt_args = self._get_argument_value("template_alt", False, False)
749+
file_args = self._get_argument_value("templates", False, True)
750+
cli_args = self._get_argument_value("templates", False, False)
751+
752+
if cli_alt_args:
753+
filenames = cli_alt_args
754+
elif file_args:
755+
filenames = file_args
756+
elif cli_args:
757+
filenames = cli_args
741758
else:
759+
# No filenames found, could be piped in or be using the api.
742760
return None
743761

744-
# if only one is specified convert it to array
762+
# If we're still haven't returned, we've got templates to lint.
763+
# Build up list of templates to lint.
764+
self.templates_to_process = True
765+
745766
if isinstance(filenames, str):
746767
filenames = [filenames]
747768

748769
ignore_templates = self._ignore_templates()
749-
all_filenames = self._glob_filenames(filenames)
770+
all_filenames.extend(self._glob_filenames(filenames))
750771

751-
return [i for i in all_filenames if i not in ignore_templates]
772+
found_files = [i for i in all_filenames if i not in ignore_templates]
773+
LOGGER.debug(
774+
f"List of Cloudformation Templates to lint: {found_files} from {filenames}"
775+
)
776+
return found_files
752777

753778
def _ignore_templates(self):
754779
ignore_template_args = self._get_argument_value("ignore_templates", False, True)
@@ -770,12 +795,11 @@ def _glob_filenames(self, filenames: Sequence[str]) -> list[str]:
770795

771796
for filename in filenames:
772797
add_filenames = glob.glob(filename, recursive=True)
773-
# only way to know of the glob failed is to test it
774-
# then add the filename as requested
775-
if not add_filenames:
776-
all_filenames.append(filename)
777-
else:
798+
799+
if isinstance(add_filenames, list):
778800
all_filenames.extend(add_filenames)
801+
else:
802+
LOGGER.error(f"{filename} could not be processed by glob.glob")
779803

780804
return sorted(list(map(str, map(Path, all_filenames))))
781805

@@ -845,3 +869,11 @@ def non_zero_exit_code(self):
845869
@property
846870
def force(self):
847871
return self._get_argument_value("force", False, False)
872+
873+
@property
874+
def templates_to_process(self):
875+
return self._templates_to_process
876+
877+
@templates_to_process.setter
878+
def templates_to_process(self, value: bool):
879+
self._templates_to_process = value

src/cfnlint/decode/cfn_json.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -356,15 +356,13 @@ def load(filename):
356356

357357
content = ""
358358

359-
if not sys.stdin.isatty():
360-
filename = "-" if filename is None else filename
361-
if sys.version_info.major <= 3 and sys.version_info.minor <= 9:
362-
for line in fileinput.input(files=filename):
363-
content = content + line
364-
else:
365-
for line in fileinput.input( # pylint: disable=unexpected-keyword-arg
366-
files=filename, encoding="utf-8"
367-
):
359+
if (filename is None) and (not sys.stdin.isatty()):
360+
filename = "-" # no filename provided, it's stdin
361+
fileinput_args = {"files": filename}
362+
if sys.version_info.major <= 3 and sys.version_info.minor >= 10:
363+
fileinput_args["encoding"] = "utf-8"
364+
with fileinput.input(**fileinput_args) as f:
365+
for line in f:
368366
content = content + line
369367
else:
370368
with open(filename, encoding="utf-8") as fp:

src/cfnlint/decode/cfn_yaml.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -304,15 +304,13 @@ def load(filename):
304304

305305
content = ""
306306

307-
if not sys.stdin.isatty():
308-
filename = "-" if filename is None else filename
309-
if sys.version_info.major <= 3 and sys.version_info.minor <= 9:
310-
for line in fileinput.input(files=filename):
311-
content = content + line
312-
else:
313-
for line in fileinput.input( # pylint: disable=unexpected-keyword-arg
314-
files=filename, encoding="utf-8"
315-
):
307+
if (filename is None) and (not sys.stdin.isatty()):
308+
filename = "-" # no filename provided, it's stdin
309+
fileinput_args = {"files": filename}
310+
if sys.version_info.major <= 3 and sys.version_info.minor >= 10:
311+
fileinput_args["encoding"] = "utf-8"
312+
with fileinput.input(**fileinput_args) as f:
313+
for line in f:
316314
content = content + line
317315
else:
318316
with open(filename, encoding="utf-8") as fp:

src/cfnlint/runner.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ def __init__(self, config: ConfigMixIn) -> None:
223223
settings for the template scan.
224224
"""
225225
self.config = config
226+
self.config.templates
226227
self.formatter = get_formatter(self.config)
227228
self.rules: Rules = Rules()
228229
self._get_rules()
@@ -388,7 +389,8 @@ def run(self) -> Iterator[Match]:
388389
Raises:
389390
None: This function does not raise any exceptions.
390391
"""
391-
if not sys.stdin.isatty() and not self.config.templates:
392+
393+
if (not sys.stdin.isatty()) and (not self.config.templates_to_process):
392394
yield from self._validate_filenames([None])
393395
return
394396

@@ -434,7 +436,7 @@ def cli(self) -> None:
434436
print(self.rules)
435437
sys.exit(0)
436438

437-
if not self.config.templates:
439+
if not self.config.templates_to_process:
438440
if sys.stdin.isatty():
439441
self.config.parser.print_help()
440442
sys.exit(1)

test/unit/module/config/test_config_mixin.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -198,18 +198,18 @@ def test_config_expand_paths(self, yaml_mock):
198198
)
199199

200200
@patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
201-
def test_config_expand_paths_failure(self, yaml_mock):
201+
def test_config_expand_paths_nomatch(self, yaml_mock):
202202
"""Test precedence in"""
203203

204-
filename = "test/fixtures/templates/badpath/*.yaml"
204+
filename = "test/fixtures/templates/nonexistant/*.yaml"
205205
yaml_mock.side_effect = [
206-
{"templates": ["test/fixtures/templates/badpath/*.yaml"]},
206+
{"templates": [filename]},
207207
{},
208208
]
209209
config = cfnlint.config.ConfigMixIn([])
210210

211211
# test defaults
212-
self.assertEqual(config.templates, [str(Path(filename))])
212+
self.assertEqual(config.templates, [])
213213

214214
@patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
215215
def test_config_expand_ignore_templates(self, yaml_mock):

0 commit comments

Comments
 (0)