command-stream 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,6 +22,119 @@ const TokenType = {
22
22
  EOF: 'eof',
23
23
  };
24
24
 
25
+ /**
26
+ * Parse a word token from the command string, handling quotes and escapes
27
+ * @param {string} command - The command string
28
+ * @param {number} startIndex - Starting position
29
+ * @returns {{word: string, endIndex: number}} Parsed word and end position
30
+ */
31
+ function parseWord(command, startIndex) {
32
+ let word = '';
33
+ let i = startIndex;
34
+ let inQuote = false;
35
+ let quoteChar = '';
36
+
37
+ while (i < command.length) {
38
+ const char = command[i];
39
+
40
+ if (!inQuote) {
41
+ const result = parseUnquotedChar(command, i, char, word);
42
+ if (result.done) {
43
+ return { word: result.word, endIndex: i };
44
+ }
45
+ word = result.word;
46
+ i = result.index;
47
+ if (result.startQuote) {
48
+ inQuote = true;
49
+ quoteChar = result.startQuote;
50
+ }
51
+ } else {
52
+ const result = parseQuotedChar(command, i, char, word, quoteChar);
53
+ word = result.word;
54
+ i = result.index;
55
+ if (result.endQuote) {
56
+ inQuote = false;
57
+ quoteChar = '';
58
+ }
59
+ }
60
+ }
61
+
62
+ return { word, endIndex: i };
63
+ }
64
+
65
+ /**
66
+ * Parse a character when not inside quotes
67
+ */
68
+ function parseUnquotedChar(command, i, char, word) {
69
+ if (char === '"' || char === "'") {
70
+ return { word: word + char, index: i + 1, startQuote: char };
71
+ }
72
+ if (/\s/.test(char) || '&|;()<>'.includes(char)) {
73
+ return { word, done: true };
74
+ }
75
+ if (char === '\\' && i + 1 < command.length) {
76
+ const escaped = command[i + 1];
77
+ return { word: word + char + escaped, index: i + 2 };
78
+ }
79
+ return { word: word + char, index: i + 1 };
80
+ }
81
+
82
+ /**
83
+ * Parse a character when inside quotes
84
+ */
85
+ function parseQuotedChar(command, i, char, word, quoteChar) {
86
+ if (char === quoteChar && command[i - 1] !== '\\') {
87
+ return { word: word + char, index: i + 1, endQuote: true };
88
+ }
89
+ if (
90
+ char === '\\' &&
91
+ i + 1 < command.length &&
92
+ (command[i + 1] === quoteChar || command[i + 1] === '\\')
93
+ ) {
94
+ const escaped = command[i + 1];
95
+ return { word: word + char + escaped, index: i + 2 };
96
+ }
97
+ return { word: word + char, index: i + 1 };
98
+ }
99
+
100
+ /**
101
+ * Try to match an operator at the current position
102
+ * @returns {{type: string, value: string, length: number} | null}
103
+ */
104
+ function matchOperator(command, i) {
105
+ const twoChar = command.slice(i, i + 2);
106
+ const oneChar = command[i];
107
+
108
+ if (twoChar === '&&') {
109
+ return { type: TokenType.AND, value: '&&', length: 2 };
110
+ }
111
+ if (twoChar === '||') {
112
+ return { type: TokenType.OR, value: '||', length: 2 };
113
+ }
114
+ if (twoChar === '>>') {
115
+ return { type: TokenType.REDIRECT_APPEND, value: '>>', length: 2 };
116
+ }
117
+ if (oneChar === '|') {
118
+ return { type: TokenType.PIPE, value: '|', length: 1 };
119
+ }
120
+ if (oneChar === ';') {
121
+ return { type: TokenType.SEMICOLON, value: ';', length: 1 };
122
+ }
123
+ if (oneChar === '(') {
124
+ return { type: TokenType.LPAREN, value: '(', length: 1 };
125
+ }
126
+ if (oneChar === ')') {
127
+ return { type: TokenType.RPAREN, value: ')', length: 1 };
128
+ }
129
+ if (oneChar === '>') {
130
+ return { type: TokenType.REDIRECT_OUT, value: '>', length: 1 };
131
+ }
132
+ if (oneChar === '<') {
133
+ return { type: TokenType.REDIRECT_IN, value: '<', length: 1 };
134
+ }
135
+ return null;
136
+ }
137
+
25
138
  /**
26
139
  * Tokenize a shell command string
27
140
  */
@@ -40,90 +153,19 @@ function tokenize(command) {
40
153
  }
41
154
 
42
155
  // Check for operators
43
- if (command[i] === '&' && command[i + 1] === '&') {
44
- tokens.push({ type: TokenType.AND, value: '&&' });
45
- i += 2;
46
- } else if (command[i] === '|' && command[i + 1] === '|') {
47
- tokens.push({ type: TokenType.OR, value: '||' });
48
- i += 2;
49
- } else if (command[i] === '|') {
50
- tokens.push({ type: TokenType.PIPE, value: '|' });
51
- i++;
52
- } else if (command[i] === ';') {
53
- tokens.push({ type: TokenType.SEMICOLON, value: ';' });
54
- i++;
55
- } else if (command[i] === '(') {
56
- tokens.push({ type: TokenType.LPAREN, value: '(' });
57
- i++;
58
- } else if (command[i] === ')') {
59
- tokens.push({ type: TokenType.RPAREN, value: ')' });
60
- i++;
61
- } else if (command[i] === '>' && command[i + 1] === '>') {
62
- tokens.push({ type: TokenType.REDIRECT_APPEND, value: '>>' });
63
- i += 2;
64
- } else if (command[i] === '>') {
65
- tokens.push({ type: TokenType.REDIRECT_OUT, value: '>' });
66
- i++;
67
- } else if (command[i] === '<') {
68
- tokens.push({ type: TokenType.REDIRECT_IN, value: '<' });
69
- i++;
70
- } else {
71
- // Parse word (respecting quotes)
72
- let word = '';
73
- let inQuote = false;
74
- let quoteChar = '';
75
-
76
- while (i < command.length) {
77
- const char = command[i];
78
-
79
- if (!inQuote) {
80
- if (char === '"' || char === "'") {
81
- inQuote = true;
82
- quoteChar = char;
83
- word += char;
84
- i++;
85
- } else if (/\s/.test(char) || '&|;()<>'.includes(char)) {
86
- break;
87
- } else if (char === '\\' && i + 1 < command.length) {
88
- // Handle escape sequences
89
- word += char;
90
- i++;
91
- if (i < command.length) {
92
- word += command[i];
93
- i++;
94
- }
95
- } else {
96
- word += char;
97
- i++;
98
- }
99
- } else {
100
- if (char === quoteChar && command[i - 1] !== '\\') {
101
- inQuote = false;
102
- quoteChar = '';
103
- word += char;
104
- i++;
105
- } else if (
106
- char === '\\' &&
107
- i + 1 < command.length &&
108
- (command[i + 1] === quoteChar || command[i + 1] === '\\')
109
- ) {
110
- // Handle escaped quotes and backslashes inside quotes
111
- word += char;
112
- i++;
113
- if (i < command.length) {
114
- word += command[i];
115
- i++;
116
- }
117
- } else {
118
- word += char;
119
- i++;
120
- }
121
- }
122
- }
156
+ const operator = matchOperator(command, i);
157
+ if (operator) {
158
+ tokens.push({ type: operator.type, value: operator.value });
159
+ i += operator.length;
160
+ continue;
161
+ }
123
162
 
124
- if (word) {
125
- tokens.push({ type: TokenType.WORD, value: word });
126
- }
163
+ // Parse word (respecting quotes)
164
+ const { word, endIndex } = parseWord(command, i);
165
+ i = endIndex;
166
+
167
+ if (word) {
168
+ tokens.push({ type: TokenType.WORD, value: word });
127
169
  }
128
170
  }
129
171
 
@@ -8,19 +8,27 @@ import { fileURLToPath } from 'url';
8
8
  const __filename = fileURLToPath(import.meta.url);
9
9
  const __dirname = path.dirname(__filename);
10
10
 
11
+ // Helper to check if a listener is a command-stream SIGINT handler
12
+ function isCommandStreamListener(l) {
13
+ const str = l.toString();
14
+ return (
15
+ str.includes('findActiveRunners') ||
16
+ str.includes('forwardSigintToRunners') ||
17
+ str.includes('handleSigintExit') ||
18
+ str.includes('activeProcessRunners') ||
19
+ str.includes('ProcessRunner') ||
20
+ str.includes('activeChildren')
21
+ );
22
+ }
23
+
11
24
  // Helper to access internal state for testing
12
25
  // This is a testing-only approach to verify cleanup
13
26
  function getInternalState() {
14
27
  // We'll use process listeners as a proxy for internal state
15
28
  const sigintListeners = process.listeners('SIGINT');
16
- const commandStreamListeners = sigintListeners.filter((l) => {
17
- const str = l.toString();
18
- return (
19
- str.includes('activeProcessRunners') ||
20
- str.includes('ProcessRunner') ||
21
- str.includes('activeChildren')
22
- );
23
- });
29
+ const commandStreamListeners = sigintListeners.filter(
30
+ isCommandStreamListener
31
+ );
24
32
 
25
33
  return {
26
34
  sigintHandlerCount: commandStreamListeners.length,
@@ -50,14 +58,9 @@ describe('Resource Cleanup Internal Verification', () => {
50
58
 
51
59
  // Force remove any command-stream SIGINT listeners
52
60
  const sigintListeners = process.listeners('SIGINT');
53
- const commandStreamListeners = sigintListeners.filter((l) => {
54
- const str = l.toString();
55
- return (
56
- str.includes('activeProcessRunners') ||
57
- str.includes('ProcessRunner') ||
58
- str.includes('activeChildren')
59
- );
60
- });
61
+ const commandStreamListeners = sigintListeners.filter(
62
+ isCommandStreamListener
63
+ );
61
64
 
62
65
  commandStreamListeners.forEach((listener) => {
63
66
  process.removeListener('SIGINT', listener);
@@ -431,14 +434,9 @@ describe('Resource Cleanup Internal Verification', () => {
431
434
  `Pipeline error test left behind ${state.sigintHandlerCount - initialState.sigintHandlerCount} handlers, forcing cleanup...`
432
435
  );
433
436
  const sigintListeners = process.listeners('SIGINT');
434
- const commandStreamListeners = sigintListeners.filter((l) => {
435
- const str = l.toString();
436
- return (
437
- str.includes('activeProcessRunners') ||
438
- str.includes('ProcessRunner') ||
439
- str.includes('activeChildren')
440
- );
441
- });
437
+ const commandStreamListeners = sigintListeners.filter(
438
+ isCommandStreamListener
439
+ );
442
440
 
443
441
  commandStreamListeners.forEach((listener) => {
444
442
  process.removeListener('SIGINT', listener);
@@ -82,6 +82,9 @@ describe.skipIf(isWindows)('SIGINT Handler Cleanup Tests', () => {
82
82
  const ourListeners = process.listeners('SIGINT').filter((l) => {
83
83
  const str = l.toString();
84
84
  return (
85
+ str.includes('findActiveRunners') ||
86
+ str.includes('forwardSigintToRunners') ||
87
+ str.includes('handleSigintExit') ||
85
88
  str.includes('activeProcessRunners') ||
86
89
  str.includes('ProcessRunner') ||
87
90
  str.includes('activeChildren')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "command-stream",
3
- "version": "0.9.0",
3
+ "version": "0.9.1",
4
4
  "description": "Modern $ shell utility library with streaming, async iteration, and EventEmitter support, optimized for Bun runtime",
5
5
  "type": "module",
6
6
  "main": "js/src/$.mjs",