mirror of
https://github.com/affaan-m/ECC.git
synced 2026-11-07 08:35:54 +00:00
fix(hooks): leave here-document bodies out of the shell segments
A body attached to the segment of its command was still searched by the dev-server check. For npm --version <<EOF && cat, the body line npm run dev made the npm segment look like a dev server. A body is data, so it is no longer part of any segment. An empty delimiter (<<'') is a valid opener again, and its body ends at the first empty line.
This commit is contained in:
3 files changed
+30
-46
No files matched your search
+12
-17
@@ -1,7 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
// A here-document opener at `index`, but not a here-string (<<<): `<<` or `<<-`, then the
|
||||
// delimiter, a shell word whose quotes are removed (`<<'EOF'`, `<<E"OF"`, `<<EOF/END`, `<<123`).
|
||||
// delimiter, a shell word whose quotes are removed (`<<'EOF'`, `<<E"OF"`, `<<EOF/END`, `<<123`,
|
||||
// and `<<''`, whose body ends at the first empty line).
|
||||
function heredocAt(command, index) {
|
||||
if (command[index - 1] === '<' || command[index + 2] === '<') return null;
|
||||
let i = index + 2;
|
||||
@@ -10,6 +11,7 @@ function heredocAt(command, index) {
|
||||
while (command[i] === ' ' || command[i] === '\t') i++;
|
||||
let delimiter = '';
|
||||
let quote = null;
|
||||
let quoted = false;
|
||||
for (; i < command.length; i++) {
|
||||
const ch = command[i];
|
||||
if (quote) {
|
||||
@@ -17,6 +19,7 @@ function heredocAt(command, index) {
|
||||
else delimiter += ch;
|
||||
} else if (ch === "'" || ch === '"') {
|
||||
quote = ch;
|
||||
quoted = true;
|
||||
} else if (ch === '\\' && i + 1 < command.length) {
|
||||
delimiter += command[++i];
|
||||
} else if (/[\s|&;()<>]/.test(ch)) {
|
||||
@@ -25,7 +28,7 @@ function heredocAt(command, index) {
|
||||
delimiter += ch;
|
||||
}
|
||||
}
|
||||
return delimiter && !quote ? { text: command.slice(index, i), delimiter, stripTabs } : null;
|
||||
return (delimiter || quoted) && !quote ? { text: command.slice(index, i), delimiter, stripTabs } : null;
|
||||
}
|
||||
|
||||
// The index after the `))` that closes the arithmetic `((` at `index`, or -1.
|
||||
@@ -64,27 +67,19 @@ function endOfHeredocBodies(command, start, heredocs) {
|
||||
return Math.min(end, command.length);
|
||||
}
|
||||
|
||||
// At the newline at `index` the command ends, after the bodies of the here-documents opened on
|
||||
// its line. The bodies are data, so they go with the command that opened the first of them, even
|
||||
// when an operator after it (`cat <<EOF &&`) already ended its segment. Returns the index after
|
||||
// the bodies.
|
||||
// At the newline at `index` the command ends. The bodies of the here-documents opened on its
|
||||
// line follow it. They are data, not commands, so they are left out of the segments. Returns the
|
||||
// index after the bodies.
|
||||
function endLine(command, index, current, heredocs, segments) {
|
||||
const end = endOfHeredocBodies(command, index + 1, heredocs);
|
||||
let bodies = command.slice(index, end);
|
||||
if (heredocs.length > 0 && heredocs[0].segment < segments.length) {
|
||||
segments[heredocs[0].segment] += bodies.trimEnd();
|
||||
bodies = '';
|
||||
}
|
||||
const rest = (current + bodies).trim();
|
||||
if (rest) segments.push(rest);
|
||||
return end;
|
||||
if (current.trim()) segments.push(current.trim());
|
||||
return endOfHeredocBodies(command, index + 1, heredocs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a shell command into segments by operators (&&, ||, ;, &) and by
|
||||
* newlines, while respecting quoting (single/double) and escaped characters.
|
||||
* Redirection operators (&>, >&, 2>&1) are NOT treated as separators, and the
|
||||
* body of a here-document stays in the segment of the command that reads it.
|
||||
* body of a here-document, which is data, is left out of the segments.
|
||||
*/
|
||||
function splitShellSegments(command) {
|
||||
const segments = [];
|
||||
@@ -134,7 +129,7 @@ function splitShellSegments(command) {
|
||||
|
||||
const heredoc = ch === '<' && next === '<' ? heredocAt(command, i) : null;
|
||||
if (heredoc) {
|
||||
heredocs.push({ ...heredoc, segment: segments.length });
|
||||
heredocs.push(heredoc);
|
||||
current += heredoc.text;
|
||||
i += heredoc.text.length - 1;
|
||||
continue;
|
||||
|
||||
@@ -234,6 +234,11 @@ function runTests() {
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
(test('allows a here-document body fed to a package manager command', () => {
|
||||
const result = runScript('npm --version <<EOF && cat\nnpm run dev\nEOF');
|
||||
assert.strictEqual(result.code, 0, `Expected exit code 0, got ${result.code}`);
|
||||
}) ? passed++ : failed++);
|
||||
|
||||
// --- Edge cases ---
|
||||
|
||||
(test('empty/invalid input passes through (exit code 0)', () => {
|
||||
|
||||
@@ -120,16 +120,10 @@ test('an escaped or quoted newline does not split', () => {
|
||||
assert.deepStrictEqual(splitShellSegments('npm run \\\ndev'), ['npm run \\\ndev']);
|
||||
assert.deepStrictEqual(splitShellSegments('echo "a\nb"'), ['echo "a\nb"']);
|
||||
});
|
||||
test('a here-document body stays with its command', () => {
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments('cat <<EOF > notes.md\na && b\nnpm run dev\nEOF\nls'),
|
||||
['cat <<EOF > notes.md\na && b\nnpm run dev\nEOF', 'ls']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments("cat <<-'END'\n\tnpm run dev\n\tEND\nls"),
|
||||
["cat <<-'END'\n\tnpm run dev\n\tEND", 'ls']
|
||||
);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<A <<B\na\nA\nb\nB\nnext'), ['cat <<A <<B\na\nA\nb\nB', 'next']);
|
||||
test('a here-document body is data, left out of the segments', () => {
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<EOF > notes.md\na && b\nnpm run dev\nEOF\nls'), ['cat <<EOF > notes.md', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<-'END'\n\tnpm run dev\n\tEND\nls"), ["cat <<-'END'", 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<A <<B\na\nA\nb\nB\nnext'), ['cat <<A <<B', 'next']);
|
||||
});
|
||||
test('a here-string or a shift is not a here-document', () => {
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<< x && y'), ['cat <<< x', 'y']);
|
||||
@@ -143,28 +137,18 @@ test('a << in a comment is not a here-document, and a comment is not split', ()
|
||||
assert.deepStrictEqual(splitShellSegments('echo a#b ${#x} && c'), ['echo a#b ${#x}', 'c']);
|
||||
});
|
||||
test('a here-document delimiter is a shell word, read without its quotes', () => {
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments('cat <<EOF/END\nnpm run dev\nEOF/END\nls'),
|
||||
['cat <<EOF/END\nnpm run dev\nEOF/END', 'ls']
|
||||
);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<E"OF"\nnpm run dev\nEOF\nls'), ['cat <<E"OF"\nnpm run dev\nEOF', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<123\nnpm run dev\n123\nls'), ['cat <<123\nnpm run dev\n123', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<EOF/END\nnpm run dev\nEOF/END\nls'), ['cat <<EOF/END', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<E"OF"\nnpm run dev\nEOF\nls'), ['cat <<E"OF"', 'ls']);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<123\nnpm run dev\n123\nls'), ['cat <<123', 'ls']);
|
||||
// An empty delimiter ends the body at the first empty line
|
||||
assert.deepStrictEqual(splitShellSegments("cat <<''\nnpm run dev\n\nls"), ["cat <<''", 'ls']);
|
||||
});
|
||||
test('a here-document body stays with its command after an operator ends the segment', () => {
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments('cat <<EOF &&\nnpm run dev\nEOF\necho done'),
|
||||
['cat <<EOF\nnpm run dev\nEOF', 'echo done']
|
||||
);
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments('cat <<EOF && npm run build\nnpm run dev\nEOF'),
|
||||
['cat <<EOF\nnpm run dev\nEOF', 'npm run build']
|
||||
);
|
||||
test('a here-document body after an operator is left out too', () => {
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<EOF &&\nnpm run dev\nEOF\necho done'), ['cat <<EOF', 'echo done']);
|
||||
assert.deepStrictEqual(splitShellSegments('npm --version <<EOF && cat\nnpm run dev\nEOF'), ['npm --version <<EOF', 'cat']);
|
||||
});
|
||||
test('a here-document delimiter on a CRLF line still ends the body', () => {
|
||||
assert.deepStrictEqual(
|
||||
splitShellSegments('cat <<EOF\r\ntext\r\nEOF\r\nnpm run dev'),
|
||||
['cat <<EOF\r\ntext\r\nEOF', 'npm run dev']
|
||||
);
|
||||
assert.deepStrictEqual(splitShellSegments('cat <<EOF\r\ntext\r\nEOF\r\nnpm run dev'), ['cat <<EOF', 'npm run dev']);
|
||||
});
|
||||
|
||||
console.log(`\n=== Results: ${passed} passed, ${failed} failed ===`);
|
||||
|
||||
Reference in new issue
Block a user