Skip to content

Commit d3265d7

Browse files
committed
Add support for input from stdin via "-" glob (fixes #414).
1 parent 212db1f commit d3265d7

16 files changed

+237
-23
lines changed

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@ Glob expressions (from the globby library):
7676
- {} allows for a comma-separated list of "or" expressions
7777
- ! or # at the beginning of a pattern negate the match
7878
- : at the beginning identifies a literal file path
79+
- - as a glob represents standard input (stdin)
7980
8081
Dot-only glob:
8182
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended

markdownlint-cli2.js

+24-9
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,7 @@ Glob expressions (from the globby library):
263263
- {} allows for a comma-separated list of "or" expressions
264264
- ! or # at the beginning of a pattern negate the match
265265
- : at the beginning identifies a literal file path
266+
- - as a glob represents standard input (stdin)
266267
267268
Dot-only glob:
268269
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended
@@ -807,7 +808,8 @@ const lintFiles = (fs, dirInfos, fileContents) => {
807808
task = task.then((results) => {
808809
options.files = [];
809810
const subTasks = [];
810-
const errorFiles = Object.keys(results);
811+
const errorFiles = Object.keys(results).
812+
filter((result) => filteredFiles.includes(result));
811813
for (const fileName of errorFiles) {
812814
const errorInfos = results[fileName].
813815
filter((errorInfo) => errorInfo.fixInfo);
@@ -909,11 +911,12 @@ const main = async (params) => {
909911
optionsDefault,
910912
optionsOverride,
911913
fileContents,
912-
nonFileContents,
913-
noRequire
914+
noRequire,
915+
allowStdin
914916
} = params;
915917
let {
916-
noGlobs
918+
noGlobs,
919+
nonFileContents
917920
} = params;
918921
const logMessage = params.logMessage || noop;
919922
const logError = params.logError || noop;
@@ -926,13 +929,16 @@ const main = async (params) => {
926929
let fixDefault = false;
927930
// eslint-disable-next-line unicorn/no-useless-undefined
928931
let configPath = undefined;
932+
let useStdin = false;
929933
let sawDashDash = false;
930934
let shouldShowHelp = false;
931935
const argvFiltered = (argv || []).filter((arg) => {
932936
if (sawDashDash) {
933937
return true;
934938
} else if (configPath === null) {
935939
configPath = arg;
940+
} else if ((arg === "-") && allowStdin) {
941+
useStdin = true;
936942
// eslint-disable-next-line unicorn/prefer-switch
937943
} else if (arg === "--") {
938944
sawDashDash = true;
@@ -983,22 +989,30 @@ const main = async (params) => {
983989
}
984990
}
985991
if (
986-
((globPatterns.length === 0) && !nonFileContents) ||
992+
((globPatterns.length === 0) && !useStdin && !nonFileContents) ||
987993
(configPath === null)
988994
) {
989995
return showHelp(logMessage, false);
990996
}
997+
// Add stdin as a non-file input if necessary
998+
if (useStdin) {
999+
const key = pathPosix.join(baseDir, "stdin");
1000+
const { text } = require("node:stream/consumers");
1001+
nonFileContents = {
1002+
...nonFileContents,
1003+
[key]: await text(process.stdin)
1004+
};
1005+
}
9911006
// Include any file overrides or non-file content
992-
const { baseMarkdownlintOptions, dirToDirInfo } = baseOptions;
9931007
const resolvedFileContents = {};
9941008
for (const file in fileContents) {
9951009
const resolvedFile = posixPath(pathDefault.resolve(baseDirSystem, file));
996-
resolvedFileContents[resolvedFile] =
997-
fileContents[file];
1010+
resolvedFileContents[resolvedFile] = fileContents[file];
9981011
}
9991012
for (const nonFile in nonFileContents) {
10001013
resolvedFileContents[nonFile] = nonFileContents[nonFile];
10011014
}
1015+
const { baseMarkdownlintOptions, dirToDirInfo } = baseOptions;
10021016
appendToArray(
10031017
dirToDirInfo[baseDir].files,
10041018
Object.keys(nonFileContents || {})
@@ -1079,7 +1093,8 @@ const run = (overrides, args) => {
10791093
const defaultParams = {
10801094
"argv": argsAndArgv,
10811095
"logMessage": console.log,
1082-
"logError": console.error
1096+
"logError": console.error,
1097+
"allowStdin": true
10831098
};
10841099
const params = {
10851100
...defaultParams,

test/markdownlint-cli2-test-exec.js

+153-3
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,19 @@
44

55
const fs = require("node:fs/promises");
66
const path = require("node:path");
7+
const test = require("ava").default;
78
const testCases = require("./markdownlint-cli2-test-cases");
89

10+
const absolute = (rootDir, file) => path.join(rootDir, file);
11+
const repositoryPath = (name) => path.join(__dirname, "..", name);
12+
913
const invoke = (directory, args, noRequire, env, script) => async () => {
1014
await fs.access(directory);
1115
const { "default": spawn } = await import("nano-spawn");
1216
return spawn(
1317
"node",
1418
[
15-
path.join(__dirname, "..", script || "markdownlint-cli2.js"),
19+
repositoryPath(script || "markdownlint-cli2.js"),
1620
...args
1721
],
1822
{
@@ -27,8 +31,6 @@ const invoke = (directory, args, noRequire, env, script) => async () => {
2731
catch((error) => error);
2832
};
2933

30-
const absolute = (rootDir, file) => path.join(rootDir, file);
31-
3234
testCases({
3335
"host": "exec",
3436
invoke,
@@ -39,3 +41,151 @@ testCases({
3941
"includeRequire": true,
4042
"includeAbsolute": true
4143
});
44+
45+
const invokeStdin = async (args, stdin, cwd) => {
46+
const { "default": spawn } = await import("nano-spawn");
47+
return spawn(
48+
"node",
49+
[
50+
repositoryPath("markdownlint-cli2.js"),
51+
...args
52+
],
53+
{
54+
cwd,
55+
"stdin": { "string": stdin }
56+
}
57+
);
58+
};
59+
60+
const validInput = "# Heading\n\nText\n";
61+
const invalidInput = "# Heading\n\nText";
62+
63+
test("- parameter with empty input from stdin", (t) => {
64+
t.plan(1);
65+
return invokeStdin(
66+
[ "-" ],
67+
""
68+
).
69+
then(() => t.pass()).
70+
catch(() => t.fail());
71+
});
72+
73+
test("- parameter with valid input from stdin", (t) => {
74+
t.plan(1);
75+
return invokeStdin(
76+
[ "-" ],
77+
validInput
78+
).
79+
then(() => t.pass()).
80+
catch(() => t.fail());
81+
});
82+
83+
test("- parameter with invalid input from stdin", (t) => {
84+
t.plan(2);
85+
return invokeStdin(
86+
[ "-" ],
87+
invalidInput
88+
).
89+
then(() => t.fail()).
90+
catch((error) => {
91+
t.is(error.exitCode, 1);
92+
t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, ""));
93+
});
94+
});
95+
96+
test("- parameter with invalid input from stdin and --fix", (t) => {
97+
t.plan(2);
98+
return invokeStdin(
99+
[ "-", "--fix" ],
100+
invalidInput
101+
).
102+
then(() => t.fail()).
103+
catch((error) => {
104+
t.is(error.exitCode, 1);
105+
t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, ""));
106+
});
107+
});
108+
109+
test("- parameter multiple times with invalid input", (t) => {
110+
t.plan(2);
111+
return invokeStdin(
112+
[ "-", "-" ],
113+
invalidInput
114+
).
115+
then(() => t.fail()).
116+
catch((error) => {
117+
t.is(error.exitCode, 1);
118+
t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, ""));
119+
});
120+
});
121+
122+
test("- parameter with valid input combined with valid globs", (t) => {
123+
t.plan(1);
124+
return invokeStdin(
125+
[ repositoryPath("CONTRIBUTING.md"), "-", repositoryPath("README.md") ],
126+
validInput
127+
).
128+
then(() => t.pass()).
129+
catch(() => t.fail());
130+
});
131+
132+
test("- parameter with invalid input combined with valid globs", (t) => {
133+
t.plan(2);
134+
return invokeStdin(
135+
[ repositoryPath("CONTRIBUTING.md"), repositoryPath("README.md"), "-" ],
136+
invalidInput
137+
).
138+
then(() => t.fail()).
139+
catch((error) => {
140+
t.is(error.exitCode, 1);
141+
t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, ""));
142+
});
143+
});
144+
145+
test("- parameter with invalid input combined with invalid glob", (t) => {
146+
t.plan(2);
147+
return invokeStdin(
148+
[ "-", repositoryPath("LICENSE") ],
149+
invalidInput
150+
).
151+
then(() => t.fail()).
152+
catch((error) => {
153+
t.is(error.exitCode, 1);
154+
t.is("", error.stderr.replace(/^LICENSE:1 MD041\/.*$[\n\r]+^stdin:1:3 MD019\/.*$[\n\r]+^stdin:3:4 MD047\/.*$/mu, ""));
155+
});
156+
});
157+
158+
test("- parameter uses base directory configuration", (t) => {
159+
t.plan(2);
160+
return invokeStdin(
161+
[ "-" ],
162+
invalidInput,
163+
path.join(__dirname, "stdin")
164+
).
165+
then(() => t.fail()).
166+
catch((error) => {
167+
t.is(error.exitCode, 1);
168+
t.is("", error.stderr.replace(/^stdin:1:3 MD019\/.*$/mu, ""));
169+
});
170+
});
171+
172+
test("- parameter not treated as stdin in configuration file globs", (t) => {
173+
t.plan(1);
174+
return invokeStdin(
175+
[],
176+
invalidInput,
177+
path.join(__dirname, "stdin-globs")
178+
).
179+
then(() => t.pass()).
180+
catch(() => t.fail());
181+
});
182+
183+
test("- parameter ignored after --", (t) => {
184+
t.plan(1);
185+
return invokeStdin(
186+
[ "--", "-" ],
187+
invalidInput
188+
).
189+
then(() => t.pass()).
190+
catch(() => t.fail());
191+
});

test/markdownlint-cli2-test.js

+15-2
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ test("README files", (t) => {
6767
});
6868

6969
test("validateMarkdownlintConfigSchema", async (t) => {
70-
t.plan(26);
70+
t.plan(27);
7171

7272
// Validate schema
7373
// @ts-ignore
@@ -111,7 +111,7 @@ test("validateMarkdownlintConfigSchema", async (t) => {
111111
});
112112

113113
test("validateMarkdownlintCli2ConfigSchema", async (t) => {
114-
t.plan(90);
114+
t.plan(91);
115115

116116
// Validate schema
117117
// @ts-ignore
@@ -696,3 +696,16 @@ test("-- stops matching parameters per POSIX Utility Conventions 12.2 Guideline
696696
files.push([ "/--", "# Title" ]);
697697
await scenario([ "--", "--" ], 1);
698698
});
699+
700+
test ("- not supported by main entry point", (t) => {
701+
t.plan(2);
702+
return markdownlintCli2({
703+
"argv": [ "-" ],
704+
"optionsOverride": {
705+
"outputFormatters": [ [ outputFormatterLengthIs(t, 0) ] ]
706+
}
707+
}).
708+
then((exitCode) => {
709+
t.is(exitCode, 0);
710+
});
711+
});

test/snapshots/markdownlint-cli2-test-exec.js.md

+5
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Generated by [AVA](https://avajs.dev).
2727
- {} allows for a comma-separated list of "or" expressions␊
2828
- ! or # at the beginning of a pattern negate the match␊
2929
- : at the beginning identifies a literal file path␊
30+
- - as a glob represents standard input (stdin)␊
3031
3132
Dot-only glob:␊
3233
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊
@@ -83,6 +84,7 @@ Generated by [AVA](https://avajs.dev).
8384
- {} allows for a comma-separated list of "or" expressions␊
8485
- ! or # at the beginning of a pattern negate the match␊
8586
- : at the beginning identifies a literal file path␊
87+
- - as a glob represents standard input (stdin)␊
8688
8789
Dot-only glob:␊
8890
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊
@@ -139,6 +141,7 @@ Generated by [AVA](https://avajs.dev).
139141
- {} allows for a comma-separated list of "or" expressions␊
140142
- ! or # at the beginning of a pattern negate the match␊
141143
- : at the beginning identifies a literal file path␊
144+
- - as a glob represents standard input (stdin)␊
142145
143146
Dot-only glob:␊
144147
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊
@@ -195,6 +198,7 @@ Generated by [AVA](https://avajs.dev).
195198
- {} allows for a comma-separated list of "or" expressions␊
196199
- ! or # at the beginning of a pattern negate the match␊
197200
- : at the beginning identifies a literal file path␊
201+
- - as a glob represents standard input (stdin)␊
198202
199203
Dot-only glob:␊
200204
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊
@@ -681,6 +685,7 @@ Generated by [AVA](https://avajs.dev).
681685
- {} allows for a comma-separated list of "or" expressions␊
682686
- ! or # at the beginning of a pattern negate the match␊
683687
- : at the beginning identifies a literal file path␊
688+
- - as a glob represents standard input (stdin)␊
684689
685690
Dot-only glob:␊
686691
- The command "markdownlint-cli2 ." would lint every file in the current directory tree which is probably not intended␊
Binary file not shown.

0 commit comments

Comments
 (0)