dsh-file-activity 0.5.7 → 0.5.9

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.
package/lib/bash-parse.js CHANGED
@@ -18,34 +18,32 @@
18
18
  * ('write' is classified create/modify by the known-file registry, 'delete'
19
19
  * removes the file from stats), so the view stays consistent with the disk.
20
20
  */
21
- import { pushCommandOps, positionalArgs, resolveSafe, pushOp } from './bash-ops.js'
22
-
21
+ import { pushCommandOps, positionalArgs, resolveSafe, pushOp } from './bash-ops.js';
23
22
  /** Prefix wrappers that do not touch files themselves. */
24
- const PREFIX_CMDS = new Set(['sudo', 'nohup', 'command', 'time', 'env'])
25
-
23
+ const PREFIX_CMDS = new Set(['sudo', 'nohup', 'command', 'time', 'env']);
26
24
  /** Parse one bash command into [{ op: 'write'|'delete', path }] (deduped). */
27
25
  export function parseBashFileOps(command, baseDir) {
28
- const ops = []
29
- let cwd = baseDir
30
- for (const segment of splitSegments(command)) {
31
- const tokens = tokenize(segment)
32
- const start = commandStartOf(tokens)
33
- if (start < 0) continue
34
- const cmd = basenameOf(tokens[start].text)
35
- const args = tokens.slice(start + 1)
36
- if (cmd === 'cd') {
37
- const target = cdTargetOf(args)
38
- if (target !== '') cwd = resolveSafe({ text: target, safe: true }, cwd)
39
- continue
26
+ const ops = [];
27
+ let cwd = baseDir;
28
+ for (const segment of splitSegments(command)) {
29
+ const tokens = tokenize(segment);
30
+ const start = commandStartOf(tokens);
31
+ if (start < 0)
32
+ continue;
33
+ const cmd = basenameOf(tokens[start].text);
34
+ const args = tokens.slice(start + 1);
35
+ if (cmd === 'cd') {
36
+ const target = cdTargetOf(args);
37
+ if (target !== '')
38
+ cwd = resolveSafe({ text: target, safe: true }, cwd);
39
+ continue;
40
+ }
41
+ pushCommandOps(ops, cmd, args, cwd);
42
+ pushRedirectOps(ops, tokens, cwd);
40
43
  }
41
- pushCommandOps(ops, cmd, args, cwd)
42
- pushRedirectOps(ops, tokens, cwd)
43
- }
44
- return dedupeOps(ops)
44
+ return dedupeOps(ops);
45
45
  }
46
-
47
46
  // ── quoting pre-pass ───────────────────────────────────────────────────────
48
-
49
47
  /**
50
48
  * Per-char quote state: 0 = unquoted, 1 = single-quoted, 2 = double-quoted,
51
49
  * -1 = the quote character itself (kept in segment text, stripped by
@@ -54,209 +52,203 @@ export function parseBashFileOps(command, baseDir) {
54
52
  * literal (bash). The output array has one entry per input character.
55
53
  */
56
54
  function quoteMarks(text) {
57
- const marks = []
58
- let quote = 0
59
- let i = 0
60
- while (i < text.length) {
61
- const step = quoteStep(text, i, quote)
62
- marks.push(...step.marks)
63
- quote = step.quote
64
- i += step.advance
65
- }
66
- return marks
55
+ const marks = [];
56
+ let quote = 0;
57
+ let i = 0;
58
+ while (i < text.length) {
59
+ const step = quoteStep(text, i, quote);
60
+ marks.push(...step.marks);
61
+ quote = step.quote;
62
+ i += step.advance;
63
+ }
64
+ return marks;
67
65
  }
68
-
69
66
  /** One character's quote transition: { marks, quote, advance }. */
70
67
  function quoteStep(text, i, quote) {
71
- const ch = text[i]
72
- if (ch === '\\' && quote !== 1 && i + 1 < text.length) return { marks: [quote, quote], quote, advance: 2 }
73
- if (quote !== 0 && ch === quoteCharOf(quote)) return { marks: [-1], quote: 0, advance: 1 }
74
- if (quote === 0 && isQuoteChar(ch)) return { marks: [-1], quote: quoteOf(ch), advance: 1 }
75
- return { marks: [quote], quote, advance: 1 }
68
+ const ch = text[i];
69
+ if (ch === '\\' && quote !== 1 && i + 1 < text.length)
70
+ return { marks: [quote, quote], quote, advance: 2 };
71
+ if (quote !== 0 && ch === quoteCharOf(quote))
72
+ return { marks: [-1], quote: 0, advance: 1 };
73
+ if (quote === 0 && isQuoteChar(ch))
74
+ return { marks: [-1], quote: quoteOf(ch), advance: 1 };
75
+ return { marks: [quote], quote, advance: 1 };
76
76
  }
77
-
78
77
  function quoteCharOf(quote) {
79
- return quote === 1 ? "'" : '"'
78
+ return quote === 1 ? "'" : '"';
80
79
  }
81
-
82
80
  function quoteOf(ch) {
83
- return ch === "'" ? 1 : 2
81
+ return ch === "'" ? 1 : 2;
84
82
  }
85
-
86
83
  function isQuoteChar(ch) {
87
- return ch === "'" || ch === '"'
84
+ return ch === "'" || ch === '"';
88
85
  }
89
-
90
86
  // ── segment splitting ──────────────────────────────────────────────────────
91
-
92
87
  /**
93
88
  * Split a command on the top-level separators && || ; | & and newlines.
94
89
  * Quoted separators never split; a bare `&` (background marker) splits only
95
90
  * when followed by whitespace/end — `2>&1` stays one word.
96
91
  */
97
92
  export function splitSegments(command) {
98
- const marks = quoteMarks(command)
99
- const segments = []
100
- let current = ''
101
- for (let i = 0; i < command.length; i += 1) {
102
- if (marks[i] !== 0 || !isSeparator(command, i)) {
103
- current += command[i]
104
- continue
93
+ const marks = quoteMarks(command);
94
+ const segments = [];
95
+ let current = '';
96
+ for (let i = 0; i < command.length; i += 1) {
97
+ if (marks[i] !== 0 || !isSeparator(command, i)) {
98
+ current += command[i];
99
+ continue;
100
+ }
101
+ flushSegment(segments, current);
102
+ current = '';
103
+ i += separatorLength(command, i) - 1;
105
104
  }
106
- flushSegment(segments, current)
107
- current = ''
108
- i += separatorLength(command, i) - 1
109
- }
110
- flushSegment(segments, current)
111
- return segments
105
+ flushSegment(segments, current);
106
+ return segments;
112
107
  }
113
-
114
108
  function isSeparator(command, i) {
115
- const ch = command[i]
116
- if (ch === '&') return command[i + 1] === '&' || i + 1 >= command.length || /\s/.test(command[i + 1])
117
- return ch === '|' || ch === ';' || ch === '\n' || ch === '\r'
109
+ const ch = command[i];
110
+ if (ch === '&')
111
+ return command[i + 1] === '&' || i + 1 >= command.length || /\s/.test(command[i + 1]);
112
+ return ch === '|' || ch === ';' || ch === '\n' || ch === '\r';
118
113
  }
119
-
120
114
  function separatorLength(command, i) {
121
- return command[i] === '&' && command[i + 1] === '&' ? 2 : 1
115
+ return command[i] === '&' && command[i + 1] === '&' ? 2 : 1;
122
116
  }
123
-
124
117
  function flushSegment(segments, current) {
125
- const trimmed = current.trim()
126
- if (trimmed !== '') segments.push(trimmed)
118
+ const trimmed = current.trim();
119
+ if (trimmed !== '')
120
+ segments.push(trimmed);
127
121
  }
128
-
129
122
  // ── tokenizing ─────────────────────────────────────────────────────────────
130
-
131
123
  /**
132
124
  * Split one segment into words. Tokens keep their quoted content (quotes
133
125
  * stripped) and a `safe` flag: false when the word contains variables /
134
126
  * command substitution / globs — such paths must never be recorded.
135
127
  */
136
128
  export function tokenize(segment) {
137
- const marks = quoteMarks(segment)
138
- const tokens = []
139
- let current = ''
140
- let safe = true
141
- const push = () => {
142
- if (current !== '') {
143
- tokens.push({ text: current, safe })
144
- current = ''
145
- safe = true
146
- }
147
- }
148
- for (let i = 0; i < segment.length; i += 1) {
149
- const ch = segment[i]
150
- if (marks[i] === -1) continue // quote characters are stripped
151
- if (marks[i] === 0 && /\s/.test(ch)) {
152
- push()
153
- continue
154
- }
155
- if (isEscape(segment, i, marks[i])) {
156
- current += segment[i + 1]
157
- i += 1
158
- continue
129
+ const marks = quoteMarks(segment);
130
+ const tokens = [];
131
+ let current = '';
132
+ let safe = true;
133
+ const push = () => {
134
+ if (current !== '') {
135
+ tokens.push({ text: current, safe });
136
+ current = '';
137
+ safe = true;
138
+ }
139
+ };
140
+ for (let i = 0; i < segment.length; i += 1) {
141
+ const ch = segment[i];
142
+ if (marks[i] === -1)
143
+ continue; // quote characters are stripped
144
+ if (marks[i] === 0 && /\s/.test(ch)) {
145
+ push();
146
+ continue;
147
+ }
148
+ if (isEscape(segment, i, marks[i])) {
149
+ current += segment[i + 1];
150
+ i += 1;
151
+ continue;
152
+ }
153
+ if (isUnsafeChar(ch, marks[i])) {
154
+ safe = false;
155
+ current += ch;
156
+ continue;
157
+ }
158
+ current += ch;
159
159
  }
160
- if (isUnsafeChar(ch, marks[i])) {
161
- safe = false
162
- current += ch
163
- continue
164
- }
165
- current += ch
166
- }
167
- push()
168
- return tokens
160
+ push();
161
+ return tokens;
169
162
  }
170
-
171
163
  /** Backslash escapes the next char outside quotes and inside double quotes. */
172
164
  function isEscape(segment, i, quote) {
173
- if (segment[i] !== '\\' || quote === 1) return false
174
- return i + 1 < segment.length
165
+ if (segment[i] !== '\\' || quote === 1)
166
+ return false;
167
+ return i + 1 < segment.length;
175
168
  }
176
-
177
169
  /** `$` expands everywhere except single quotes; globs only outside quotes. */
178
170
  function isUnsafeChar(ch, quote) {
179
- if (ch === '$') return quote !== 1
180
- if (quote !== 0) return false
181
- return ch === '`' || ch === '*' || ch === '?' || ch === '[' || ch === '{'
171
+ if (ch === '$')
172
+ return quote !== 1;
173
+ if (quote !== 0)
174
+ return false;
175
+ return ch === '`' || ch === '*' || ch === '?' || ch === '[' || ch === '{';
182
176
  }
183
-
184
177
  // ── command head ───────────────────────────────────────────────────────────
185
-
186
178
  /** Index of the first real command token (skipping VAR=x and wrappers). */
187
179
  function findStartOf(tokens, index) {
188
- while (index < tokens.length) {
189
- const token = tokens[index]
190
- if (isAssignment(token.text) || PREFIX_CMDS.has(basenameOf(token.text))) {
191
- index += 1
192
- continue
180
+ while (index < tokens.length) {
181
+ const token = tokens[index];
182
+ if (isAssignment(token.text) || PREFIX_CMDS.has(basenameOf(token.text))) {
183
+ index += 1;
184
+ continue;
185
+ }
186
+ return index;
193
187
  }
194
- return index
195
- }
196
- return -1
188
+ return -1;
197
189
  }
198
-
199
190
  function commandStartOf(tokens) {
200
- const start = findStartOf(tokens, 0)
201
- if (start < 0) return -1
202
- if (basenameOf(tokens[start].text) === 'env') {
203
- // env VAR=value cmd: assignments after env are skipped too.
204
- return findStartOf(tokens, start + 1)
205
- }
206
- return start
191
+ const start = findStartOf(tokens, 0);
192
+ if (start < 0)
193
+ return -1;
194
+ if (basenameOf(tokens[start].text) === 'env') {
195
+ // env VAR=value cmd: assignments after env are skipped too.
196
+ return findStartOf(tokens, start + 1);
197
+ }
198
+ return start;
207
199
  }
208
-
209
200
  function isAssignment(text) {
210
- const eq = text.indexOf('=')
211
- if (eq <= 0) return false
212
- return /^[A-Za-z_][A-Za-z0-9_]*=/.test(text)
201
+ const eq = text.indexOf('=');
202
+ if (eq <= 0)
203
+ return false;
204
+ return /^[A-Za-z_][A-Za-z0-9_]*=/.test(text);
213
205
  }
214
-
215
206
  function basenameOf(text) {
216
- const slash = text.lastIndexOf('/')
217
- return slash === -1 ? text : text.slice(slash + 1)
207
+ const slash = text.lastIndexOf('/');
208
+ return slash === -1 ? text : text.slice(slash + 1);
218
209
  }
219
-
220
210
  // ── paths & redirects ──────────────────────────────────────────────────────
221
-
222
211
  /** `> file` / `>> file` / `2> file` redirects write the target file. */
223
212
  function pushRedirectOps(ops, tokens, cwd) {
224
- for (let i = 0; i < tokens.length; i += 1) {
225
- const match = /^(\d*)>{1,2}(.*)$/.exec(tokens[i].text)
226
- if (match === null) continue
227
- const target = redirectTarget(tokens, i, match[2])
228
- if (target === null) continue
229
- pushOp(ops, 'write', { text: target.text, safe: target.safe }, cwd)
230
- }
213
+ for (let i = 0; i < tokens.length; i += 1) {
214
+ const match = /^(\d*)>{1,2}(.*)$/.exec(tokens[i].text);
215
+ if (match === null)
216
+ continue;
217
+ const target = redirectTarget(tokens, i, match[2]);
218
+ if (target === null)
219
+ continue;
220
+ pushOp(ops, 'write', { text: target.text, safe: target.safe }, cwd);
221
+ }
231
222
  }
232
-
233
223
  /** The file a redirect writes; null when it is an fd / /dev/null / missing. */
234
224
  function redirectTarget(tokens, i, inline) {
235
- if (inline !== '') return usable(inline, tokens[i].safe) ? { text: inline, safe: tokens[i].safe } : null
236
- const next = tokens[i + 1]
237
- if (next === undefined || !next.safe) return null
238
- return usable(next.text, true) ? { text: next.text, safe: true } : null
225
+ if (inline !== '')
226
+ return usable(inline, tokens[i].safe) ? { text: inline, safe: tokens[i].safe } : null;
227
+ const next = tokens[i + 1];
228
+ if (next === undefined || !next.safe)
229
+ return null;
230
+ return usable(next.text, true) ? { text: next.text, safe: true } : null;
239
231
  }
240
-
241
232
  function usable(text, safe) {
242
- if (!safe || text === '') return false
243
- return !text.startsWith('&') && !text.startsWith('/dev/')
233
+ if (!safe || text === '')
234
+ return false;
235
+ return !text.startsWith('&') && !text.startsWith('/dev/');
244
236
  }
245
-
246
237
  /** `cd` target: the first positional argument ('' when missing/unsafe). */
247
238
  function cdTargetOf(args) {
248
- const paths = positionalArgs(args)
249
- if (paths.length === 0) return ''
250
- return paths[0].safe ? paths[0].text : ''
239
+ const paths = positionalArgs(args);
240
+ if (paths.length === 0)
241
+ return '';
242
+ return paths[0].safe ? paths[0].text : '';
251
243
  }
252
-
253
244
  /** Dedupe by op+path, keeping the first occurrence. */
254
245
  function dedupeOps(ops) {
255
- const seen = new Set()
256
- return ops.filter((op) => {
257
- const key = `${op.op}\u0000${op.path}`
258
- if (seen.has(key)) return false
259
- seen.add(key)
260
- return true
261
- })
246
+ const seen = new Set();
247
+ return ops.filter((op) => {
248
+ const key = `${op.op}\u0000${op.path}`;
249
+ if (seen.has(key))
250
+ return false;
251
+ seen.add(key);
252
+ return true;
253
+ });
262
254
  }