mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-08-24 00:15:26 -07:00
test(release): split shell lines outside quotes and comments
Splitting on separators anywhere in a line broke quoted strings apart, so echo 'note; bun run changelog:check-prerelease-notes ...' produced a bare command position and satisfied the ordering assertion. Track quote state while scanning, stop at an unquoted inline comment, and keep redirects from being read as pipes. Cover quoted separators, inline comments, trailing comments, and pipe-vs-redirect.
This commit is contained in:
@@ -26,6 +26,18 @@ test('stepRunsCommand rejects commands that are only mentioned, not run', () =>
|
|||||||
assert.equal(runs('bun run verify'), false);
|
assert.equal(runs('bun run verify'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('stepRunsCommand ignores separators inside quotes and inline comments', () => {
|
||||||
|
assert.equal(runs('echo \'note; bun run verify --flag "$VALUE"\''), false);
|
||||||
|
assert.equal(runs('echo "note && bun run verify --flag \\"$VALUE\\""'), false);
|
||||||
|
assert.equal(runs("printf '%s\\n' 'a | bun run verify --flag \"$VALUE\"'"), false);
|
||||||
|
assert.equal(runs('if false; then # bun run verify --flag "$VALUE"'), false);
|
||||||
|
// A trailing comment does not hide the command in front of it.
|
||||||
|
assert.equal(runs('bun run verify --flag "$VALUE" # keep this'), true);
|
||||||
|
// A pipe is a real separator; a redirect is not.
|
||||||
|
assert.equal(runs('cat notes | bun run verify --flag "$VALUE"'), true);
|
||||||
|
assert.equal(stepRunsCommand({ run: 'gh release view "$V" 2>&1 | tee log' }, /^tee\b/), true);
|
||||||
|
});
|
||||||
|
|
||||||
test('commandPositions splits on separators and strips control-flow prefixes', () => {
|
test('commandPositions splits on separators and strips control-flow prefixes', () => {
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
commandPositions({ run: 'if gh release view "$V"; then\ngh release edit "$V"\nfi' }),
|
commandPositions({ run: 'if gh release view "$V"; then\ngh release edit "$V"\nfi' }),
|
||||||
|
|||||||
@@ -53,14 +53,72 @@ export function executableRunLines(step: WorkflowStep): string[] {
|
|||||||
// Leading shell keywords and operators that can precede a real command.
|
// Leading shell keywords and operators that can precede a real command.
|
||||||
const COMMAND_PREFIX = /^(?:if|elif|while|until|then|else|do|!|&&|\|\||\(|\{)\s+/;
|
const COMMAND_PREFIX = /^(?:if|elif|while|until|then|else|do|!|&&|\|\||\(|\{)\s+/;
|
||||||
|
|
||||||
|
// Splits one shell line on command separators, tracking quotes so a separator
|
||||||
|
// inside a string is not treated as a command break, and stopping at an
|
||||||
|
// unquoted inline comment.
|
||||||
|
function splitCommandSeparators(line: string): string[] {
|
||||||
|
const segments: string[] = [];
|
||||||
|
let current = '';
|
||||||
|
let quote: "'" | '"' | null = null;
|
||||||
|
|
||||||
|
for (let index = 0; index < line.length; index += 1) {
|
||||||
|
const char = line[index]!;
|
||||||
|
|
||||||
|
if (quote) {
|
||||||
|
current += char;
|
||||||
|
if (char === '\\' && quote === '"' && index + 1 < line.length) {
|
||||||
|
current += line[index + 1]!;
|
||||||
|
index += 1;
|
||||||
|
} else if (char === quote) {
|
||||||
|
quote = null;
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (char === "'" || char === '"') {
|
||||||
|
quote = char;
|
||||||
|
current += char;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// An unquoted # starts a comment when it opens a word; the rest is inert.
|
||||||
|
if (char === '#' && (current === '' || /\s$/.test(current))) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = line[index + 1];
|
||||||
|
if (char === ';') {
|
||||||
|
segments.push(current);
|
||||||
|
current = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if ((char === '&' || char === '|') && next === char) {
|
||||||
|
segments.push(current);
|
||||||
|
current = '';
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// A lone pipe separates commands; a redirect such as 2>&1 does not.
|
||||||
|
if (char === '|' && !/[0-9<>&]$/.test(current)) {
|
||||||
|
segments.push(current);
|
||||||
|
current = '';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
current += char;
|
||||||
|
}
|
||||||
|
|
||||||
|
segments.push(current);
|
||||||
|
return segments;
|
||||||
|
}
|
||||||
|
|
||||||
// Command positions within a step's shell body: each line split on separators,
|
// Command positions within a step's shell body: each line split on separators,
|
||||||
// with control-flow prefixes stripped. A pattern anchored with ^ therefore
|
// with control-flow prefixes stripped. A pattern anchored with ^ therefore
|
||||||
// matches only where a command actually starts, so text quoted inside an
|
// matches only where a command actually starts, so text quoted inside an
|
||||||
// `echo`/`printf` argument is not mistaken for the command running.
|
// `echo`/`printf` argument is not mistaken for the command running.
|
||||||
export function commandPositions(step: WorkflowStep): string[] {
|
export function commandPositions(step: WorkflowStep): string[] {
|
||||||
return executableRunLines(step).flatMap((line) =>
|
return executableRunLines(step).flatMap((line) =>
|
||||||
line
|
splitCommandSeparators(line)
|
||||||
.split(/;|&&|\|\||(?<![0-9<>])\|(?!\|)/)
|
|
||||||
.map((segment) => {
|
.map((segment) => {
|
||||||
let candidate = segment.trim();
|
let candidate = segment.trim();
|
||||||
let stripped = candidate.replace(COMMAND_PREFIX, '');
|
let stripped = candidate.replace(COMMAND_PREFIX, '');
|
||||||
|
|||||||
Reference in New Issue
Block a user