Repository: aws-cloudformation/cfn-lint

Issue:
cfnlint via stdin ignored when templates config defined in .cfnlintrc
### CloudFormation Lint Version

1.34.2

### What operating system are you using?

Mac

### Describe the bug

When `templates` config is not present in `.cfnlintrc`, stdin works as expected:
```sh
echo 'Resources: {}' | cfn-lint
```

When `templates` config is present in `.cfnlintrc`, stdin is ignored, which causes issues for linting plugins via stdin in IDE:
```sh
echo -e 'templates:\n  - abc.yaml' > .cfnlintrc
echo 'Resources: {}' | cfn-lint
E0003 abc.yaml could not be processed by glob.glob
None:1:1
```

### Expected behavior

`stdin` should take precedence before `templates` config in `.cfnlintrc`:

```sh
echo -e 'templates:\n  - abc.yaml' > .cfnlintrc
echo 'Resources: {}' | cfn-lint
```

### Reproduction template

```yaml
Resources: {}
```

```yaml
# .cfnlintrc
templates:
   - abc.yaml
```


Discussion:
Because of this line: https://github.com/aws-cloudformation/cfn-lint/blob/bd0c0ce80d581281cf14e0d93099f5c1a5f50b5b/src/cfnlint/runner.py#L399

We can't just switch it to `if not sys.stdin.isatty():` as templates passed via a parameter should still take precedence.  The best way to do this would be to validate if we are getting templates from a .cfnlintrc file or from parameters.



Patch:
diff --git a/src/cfnlint/config.py b/src/cfnlint/config.py
index f15573462a..005cedfaa1 100644
--- a/src/cfnlint/config.py
+++ b/src/cfnlint/config.py
@@ -775,10 +775,12 @@ def templates(self):
 
         if cli_alt_args:
             filenames = cli_alt_args
-        elif file_args:
-            filenames = file_args
         elif cli_args:
             filenames = cli_args
+        elif not sys.stdin.isatty():
+            return []
+        elif file_args:
+            filenames = file_args
         else:
             # No filenames found, could be piped in or be using the api.
             return None


Test patch:
diff --git a/test/unit/module/config/test_config_mixin.py b/test/unit/module/config/test_config_mixin.py
index cbed4b458e..049352ecb1 100644
--- a/test/unit/module/config/test_config_mixin.py
+++ b/test/unit/module/config/test_config_mixin.py
@@ -167,9 +167,10 @@ def test_config_force_with_update_specs(self, yaml_mock):
         self.assertEqual(config.update_specs, True)
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_expand_paths(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_expand_paths(self, isatty_mock, yaml_mock):
         """Test precedence in"""
-
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {"templates": ["test/fixtures/templates/public/*.yaml"]},
             {},
@@ -198,10 +199,12 @@ def test_config_expand_paths(self, yaml_mock):
         )
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_expand_paths_nomatch(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_expand_paths_nomatch(self, isatty_mock, yaml_mock):
         """Test precedence in"""
 
         filename = "test/fixtures/templates/nonexistant/*.yaml"
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {"templates": [filename]},
             {},
@@ -212,10 +215,14 @@ def test_config_expand_paths_nomatch(self, yaml_mock):
             self.assertEqual(len(config.templates), 1)
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_expand_paths_nomatch_ignore_bad_template(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_expand_paths_nomatch_ignore_bad_template(
+        self, isatty_mock, yaml_mock
+    ):
         """Test precedence in"""
 
         filename = "test/fixtures/templates/nonexistant/*.yaml"
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {"templates": [filename], "ignore_bad_template": True},
             {},
@@ -226,9 +233,10 @@ def test_config_expand_paths_nomatch_ignore_bad_template(self, yaml_mock):
         self.assertEqual(config.templates, [])
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_expand_ignore_templates(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_expand_ignore_templates(self, isatty_mock, yaml_mock):
         """Test ignore templates"""
-
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {
                 "templates": ["test/fixtures/templates/bad/resources/iam/*.yaml"],
@@ -248,9 +256,13 @@ def test_config_expand_ignore_templates(self, yaml_mock):
         self.assertEqual(len(config.templates), 3)
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_expand_and_ignore_templates_with_bad_path(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_expand_and_ignore_templates_with_bad_path(
+        self, isatty_mock, yaml_mock
+    ):
         """Test ignore templates"""
 
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {
                 "templates": ["test/fixtures/templates/bad/resources/iam/*.yaml"],
@@ -270,9 +282,11 @@ def test_config_expand_and_ignore_templates_with_bad_path(self, yaml_mock):
         self.assertEqual(len(config.templates), 4)
 
     @patch("cfnlint.config.ConfigFileArgs._read_config", create=True)
-    def test_config_merge(self, yaml_mock):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_config_merge(self, isatty_mock, yaml_mock):
         """Test merging lists"""
 
+        isatty_mock.return_value = True
         yaml_mock.side_effect = [
             {
                 "include_checks": ["I"],
diff --git a/test/unit/module/formatters/test_formatters.py b/test/unit/module/formatters/test_formatters.py
index 6e89b49579..0d61ff8576 100644
--- a/test/unit/module/formatters/test_formatters.py
+++ b/test/unit/module/formatters/test_formatters.py
@@ -7,6 +7,7 @@
 import sys
 from pathlib import Path
 from test.testlib.testcase import BaseTestCase
+from unittest.mock import patch
 
 import defusedxml.ElementTree as ET
 
@@ -214,8 +215,10 @@ def test_pretty_formatter(self):
                 ),
             )
 
-    def test_pretty_formatter_pipe(self):
+    @patch("sys.stdin.isatty", return_va=True)
+    def test_pretty_formatter_pipe(self, isatty_mock):
         """Test pretty formatter"""
+        isatty_mock.return_value = True
         formatter = PrettyFormatter()
         self.config.cli_args.templates = None
         results = formatter.print_matches(
diff --git a/test/unit/module/runner/test_cli.py b/test/unit/module/runner/test_cli.py
index 318067520b..7d843dafb7 100644
--- a/test/unit/module/runner/test_cli.py
+++ b/test/unit/module/runner/test_cli.py
@@ -79,7 +79,7 @@ def test_print_help(self, mock_isatty, mock_print_help):
 
         self.assertEqual(e.exception.code, 1)
         mock_print_help.assert_called_once()
-        mock_isatty.assert_called_once()
+        self.assertEqual(mock_isatty.call_count, 2)
 
     def test_bad_regions(self):
         config = ConfigMixIn(