@qodalis/cli-files 2.0.0-beta.16 → 2.0.0-beta.18

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/public-api.mjs CHANGED
@@ -1,11 +1,14 @@
1
- import { ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, CliAccessDeniedError, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, resolveHeaders, cliCanAccess, ICliUserSessionService_TOKEN, ICliUserEventService_TOKEN, ICliDragDropService_TOKEN, isCliAccessDeniedError, ICliAuthorizationService_TOKEN } from '@qodalis/cli-core';
1
+ import { createCliToken, ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, CliAccessDeniedError, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, writeWrapped, writeUsage, writeExamples, joinShellWords, quoteShellWord, resolveHeaders, cliCanAccess, ICliUserSessionService_TOKEN, ICliUserEventService_TOKEN, ICliDragDropService_TOKEN, splitShellWords, isCliAccessDeniedError, ICliAuthorizationService_TOKEN } from '@qodalis/cli-core';
2
2
 
3
3
  // src/lib/interfaces/i-file-system-service.ts
4
- var IFileSystemService_TOKEN = "cli-file-system-service";
4
+ var IFileSystemService_TOKEN = createCliToken("cli-file-system-service");
5
5
 
6
6
  // src/lib/version.ts
7
- var LIBRARY_VERSION = "2.0.0-beta.16";
7
+ var LIBRARY_VERSION = "2.0.0-beta.18";
8
8
  var API_VERSION = 2;
9
+ function splitArgs(raw) {
10
+ return splitShellWords(raw);
11
+ }
9
12
 
10
13
  // src/lib/processors/cli-ls-command-processor.ts
11
14
  var CliLsCommandProcessor = class {
@@ -30,41 +33,63 @@ var CliLsCommandProcessor = class {
30
33
  description: "Use long listing format",
31
34
  required: false,
32
35
  type: "boolean"
36
+ },
37
+ {
38
+ name: "human-readable",
39
+ aliases: ["h"],
40
+ description: "With -l, print sizes like 1.5K and 2M",
41
+ required: false,
42
+ type: "boolean"
33
43
  }
34
44
  ];
45
+ this.humanReadable = false;
35
46
  }
36
47
  async processCommand(command, context) {
37
48
  const fs = context.services.getRequired(
38
49
  IFileSystemService_TOKEN
39
50
  );
40
- const targetPath = command.value || fs.getCurrentDirectory();
51
+ const paths = splitArgs(command.value || "");
41
52
  const showAll = command.args["all"] || command.args["a"];
42
53
  const longFormat = command.args["long"] || command.args["l"];
43
- try {
44
- const entries = fs.listDirectory(targetPath);
45
- const filtered = showAll ? entries : entries.filter((e) => !e.name.startsWith("."));
46
- if (filtered.length === 0) {
47
- return;
48
- }
49
- filtered.sort((a, b) => {
50
- if (a.type !== b.type) {
51
- return a.type === "directory" ? -1 : 1;
54
+ this.humanReadable = !!(command.args["human-readable"] || command.args["h"]);
55
+ const targets = paths.length > 0 ? paths : [fs.getCurrentDirectory()];
56
+ const piped = [];
57
+ targets.forEach((targetPath, index) => {
58
+ try {
59
+ const node = fs.getNode(targetPath);
60
+ const entries = node && node.type !== "directory" ? [{ ...node, name: targetPath }] : fs.listDirectory(targetPath);
61
+ const filtered = showAll ? entries : entries.filter((e) => !e.name.startsWith("."));
62
+ if (targets.length > 1) {
63
+ if (index > 0) context.writer.writeln();
64
+ context.writer.writeln(`${targetPath}:`);
52
65
  }
53
- return a.name.localeCompare(b.name);
54
- });
55
- if (longFormat) {
56
- this.writeLongFormat(filtered, context);
57
- } else {
58
- this.writeShortFormat(filtered, context);
66
+ if (filtered.length === 0) {
67
+ return;
68
+ }
69
+ filtered.sort((a, b) => {
70
+ if (a.type !== b.type) {
71
+ return a.type === "directory" ? -1 : 1;
72
+ }
73
+ return a.name.localeCompare(b.name);
74
+ });
75
+ if (longFormat) {
76
+ this.writeLongFormat(filtered, context);
77
+ } else {
78
+ this.writeShortFormat(filtered, context);
79
+ piped.push(...filtered.map((e) => e.name));
80
+ }
81
+ } catch (e) {
82
+ context.writer.writeError(e.message);
59
83
  }
60
- } catch (e) {
61
- context.writer.writeError(e.message);
84
+ });
85
+ if (!longFormat && piped.length > 0) {
86
+ context.process.output(piped.join("\n"));
62
87
  }
63
88
  }
64
89
  writeDescription(context) {
65
90
  context.writer.writeln(this.description);
66
91
  context.writer.writeln();
67
- context.writer.writeln("Usage: ls [path] [--all] [--long]");
92
+ context.writer.writeln("Usage: ls [path...] [-a|--all] [-l|--long] [-h|--human-readable]");
68
93
  }
69
94
  writeLongFormat(entries, context) {
70
95
  const { writer } = context;
@@ -73,7 +98,7 @@ var CliLsCommandProcessor = class {
73
98
  const perms = entry.permissions || (entry.type === "directory" ? "rwxr-xr-x" : "rw-r--r--");
74
99
  const owner = entry.ownership?.uid || "-";
75
100
  const group = entry.ownership?.gid || "-";
76
- const size = entry.type === "file" ? entry.size.toString() : "-";
101
+ const size = entry.type !== "file" ? "-" : this.humanReadable ? this.formatSize(entry.size) : entry.size.toString();
77
102
  const date = new Date(entry.modifiedAt);
78
103
  const dateStr = date.toLocaleDateString("en-US", {
79
104
  month: "short",
@@ -87,6 +112,17 @@ var CliLsCommandProcessor = class {
87
112
  );
88
113
  }
89
114
  }
115
+ formatSize(bytes) {
116
+ const units = ["K", "M", "G", "T"];
117
+ let size = bytes;
118
+ let unit = "";
119
+ for (const next of units) {
120
+ if (size < 1024) break;
121
+ size /= 1024;
122
+ unit = next;
123
+ }
124
+ return unit ? `${size < 10 ? size.toFixed(1) : Math.round(size)}${unit}` : String(bytes);
125
+ }
90
126
  writeShortFormat(entries, context) {
91
127
  const { writer } = context;
92
128
  const names = entries.map(
@@ -104,11 +140,30 @@ var CliCdCommandProcessor = class {
104
140
  this.acceptsRawInput = true;
105
141
  this.metadata = { icon: "\u{1F4C1}", module: "file management" };
106
142
  }
143
+ writeDescription(context) {
144
+ const t = context.translator;
145
+ writeWrapped(
146
+ context,
147
+ t.t("cli.cd.help.change_current_directory_without", "Change the current directory. Without a path it goes to your home directory. Paths can be absolute (/...), relative to where you are, or start with ~ for home.")
148
+ );
149
+ context.writer.writeln();
150
+ writeUsage(context, [
151
+ ["cd", t.t("cli.cd.help.go_home_directory", "Go to your home directory")],
152
+ ["cd <dir>", t.t("cli.cd.help.go_directory", "Go into a directory")],
153
+ ["cd ..", t.t("cli.cd.help.go_up_one_level", "Go up one level")]
154
+ ]);
155
+ context.writer.writeln();
156
+ writeExamples(context, [
157
+ ["cd ~/docs", t.t("cli.cd.help.go_docs_home", "Go to docs in your home")],
158
+ ["cd ../projects", t.t("cli.cd.help.go_sibling_directory", "Go to a sibling directory")],
159
+ ["cd /", t.t("cli.cd.help.go_root", "Go to the root")]
160
+ ]);
161
+ }
107
162
  async processCommand(command, context) {
108
163
  const fs = context.services.getRequired(
109
164
  IFileSystemService_TOKEN
110
165
  );
111
- const target = command.value || "~";
166
+ const target = splitArgs(command.value || "").join(" ") || "~";
112
167
  try {
113
168
  fs.setCurrentDirectory(target);
114
169
  await fs.persist();
@@ -125,6 +180,22 @@ var CliPwdCommandProcessor = class {
125
180
  this.version = LIBRARY_VERSION;
126
181
  this.metadata = { icon: "\u{1F4C2}", module: "file management" };
127
182
  }
183
+ writeDescription(context) {
184
+ const t = context.translator;
185
+ writeWrapped(
186
+ context,
187
+ t.t("cli.pwd.help.print_full_path_directory", "Print the full path of the directory you are in. Relative paths in other commands start from here.")
188
+ );
189
+ context.writer.writeln();
190
+ writeUsage(context, [
191
+ ["pwd", t.t("cli.pwd.help.show_current_directory", "Show the current directory")]
192
+ ]);
193
+ context.writer.writeln();
194
+ writeExamples(context, [
195
+ ["pwd", t.t("cli.pwd.help.example_home_user", "For example /home/user")],
196
+ ["cd ~/docs; pwd", t.t("cli.pwd.help.prints_home_user_docs", "Prints /home/user/docs")]
197
+ ]);
198
+ }
128
199
  async processCommand(command, context) {
129
200
  const fs = context.services.getRequired(
130
201
  IFileSystemService_TOKEN
@@ -151,22 +222,44 @@ var CliMkdirCommandProcessor = class {
151
222
  }
152
223
  ];
153
224
  }
225
+ writeDescription(context) {
226
+ const t = context.translator;
227
+ writeWrapped(
228
+ context,
229
+ t.t("cli.mkdir.help.create_new_directory_p", "Create a new directory. With -p, any missing parent directories are created too.")
230
+ );
231
+ context.writer.writeln();
232
+ writeUsage(context, [
233
+ ["mkdir <dir...>", t.t("cli.mkdir.help.create_one_more_directories", "Create one or more directories")],
234
+ ["mkdir -p <path>", t.t("cli.mkdir.help.create_directory_missing_parents", "Create a directory and its missing parents")]
235
+ ]);
236
+ context.writer.writeln();
237
+ writeExamples(context, [
238
+ ["mkdir projects", t.t("cli.mkdir.help.create_projects_current_directory", "Create projects in the current directory")],
239
+ ["mkdir -p ~/docs/2026/notes", t.t("cli.mkdir.help.create_whole_path_once", "Create the whole path at once")]
240
+ ]);
241
+ }
154
242
  async processCommand(command, context) {
155
243
  const fs = context.services.getRequired(
156
244
  IFileSystemService_TOKEN
157
245
  );
158
- const path = command.value;
246
+ const paths = splitArgs(command.value || "");
159
247
  const recursive = command.args["parents"] || command.args["p"];
160
- if (!path) {
248
+ if (paths.length === 0) {
161
249
  context.writer.writeError("mkdir: missing operand");
162
250
  return;
163
251
  }
164
- try {
165
- fs.createDirectory(path, recursive);
166
- await fs.persist();
167
- } catch (e) {
168
- context.writer.writeError(e.message);
252
+ let changed = false;
253
+ for (const path of paths) {
254
+ try {
255
+ if (recursive && fs.isDirectory(path)) continue;
256
+ fs.createDirectory(path, recursive);
257
+ changed = true;
258
+ } catch (e) {
259
+ context.writer.writeError(e.message);
260
+ }
169
261
  }
262
+ if (changed) await fs.persist();
170
263
  }
171
264
  };
172
265
  var CliRmdirCommandProcessor = class {
@@ -179,38 +272,58 @@ var CliRmdirCommandProcessor = class {
179
272
  this.valueRequired = true;
180
273
  this.metadata = { icon: "\u{1F5D1}", module: "file management" };
181
274
  }
275
+ writeDescription(context) {
276
+ const t = context.translator;
277
+ writeWrapped(
278
+ context,
279
+ t.t("cli.rmdir.help.remove_empty_directory_refuses", "Remove an empty directory. It refuses to remove a directory that still contains files; use rm -r for that.")
280
+ );
281
+ context.writer.writeln();
282
+ writeUsage(context, [
283
+ ["rmdir <dir...>", t.t("cli.rmdir.help.remove_empty_directories", "Remove empty directories")]
284
+ ]);
285
+ context.writer.writeln();
286
+ writeExamples(context, [
287
+ ["rmdir empty-folder", t.t("cli.rmdir.help.remove_directory_current_one", "Remove a directory in the current one")],
288
+ ["rmdir ~/docs/old", t.t("cli.rmdir.help.remove_full_path", "Remove by full path")]
289
+ ]);
290
+ }
182
291
  async processCommand(command, context) {
183
292
  const fs = context.services.getRequired(
184
293
  IFileSystemService_TOKEN
185
294
  );
186
- const path = command.value;
187
- if (!path) {
295
+ const paths = splitArgs(command.value || "");
296
+ if (paths.length === 0) {
188
297
  context.writer.writeError("rmdir: missing operand");
189
298
  return;
190
299
  }
191
- try {
192
- const node = fs.getNode(path);
193
- if (!node) {
194
- context.writer.writeError(
195
- `rmdir: ${path}: No such file or directory`
196
- );
197
- return;
198
- }
199
- if (node.type !== "directory") {
200
- context.writer.writeError(`rmdir: ${path}: Not a directory`);
201
- return;
202
- }
203
- if (node.children && node.children.length > 0) {
204
- context.writer.writeError(
205
- `rmdir: ${path}: Directory not empty`
206
- );
207
- return;
300
+ let changed = false;
301
+ for (const path of paths) {
302
+ try {
303
+ const node = fs.getNode(path);
304
+ if (!node) {
305
+ context.writer.writeError(
306
+ `rmdir: ${path}: No such file or directory`
307
+ );
308
+ continue;
309
+ }
310
+ if (node.type !== "directory") {
311
+ context.writer.writeError(`rmdir: ${path}: Not a directory`);
312
+ continue;
313
+ }
314
+ if (node.children && node.children.length > 0) {
315
+ context.writer.writeError(
316
+ `rmdir: ${path}: Directory not empty`
317
+ );
318
+ continue;
319
+ }
320
+ fs.remove(path, true);
321
+ changed = true;
322
+ } catch (e) {
323
+ context.writer.writeError(e.message);
208
324
  }
209
- fs.remove(path);
210
- await fs.persist();
211
- } catch (e) {
212
- context.writer.writeError(e.message);
213
325
  }
326
+ if (changed) await fs.persist();
214
327
  }
215
328
  };
216
329
  var CliTouchCommandProcessor = class {
@@ -223,23 +336,56 @@ var CliTouchCommandProcessor = class {
223
336
  this.valueRequired = true;
224
337
  this.metadata = { icon: "\u{1F4C4}", module: "file management" };
225
338
  }
339
+ writeDescription(context) {
340
+ const t = context.translator;
341
+ writeWrapped(
342
+ context,
343
+ t.t("cli.touch.help.create_empty_file_update", "Create an empty file, or update the modified time of a file that already exists. The parent directory must already exist.")
344
+ );
345
+ context.writer.writeln();
346
+ writeUsage(context, [
347
+ ["touch <file...>", t.t("cli.touch.help.create_files_refresh_their", "Create the files, or refresh their timestamps")]
348
+ ]);
349
+ context.writer.writeln();
350
+ writeExamples(context, [
351
+ ["touch notes.txt", t.t("cli.touch.help.create_empty_notes_txt", "Create an empty notes.txt")],
352
+ ["touch ~/docs/todo.md", t.t("cli.touch.help.create_file_docs", "Create a file in ~/docs")]
353
+ ]);
354
+ }
226
355
  async processCommand(command, context) {
227
356
  const fs = context.services.getRequired(
228
357
  IFileSystemService_TOKEN
229
358
  );
230
- const path = command.value;
231
- if (!path) {
359
+ const paths = splitArgs(command.value || "");
360
+ if (paths.length === 0) {
232
361
  context.writer.writeError("touch: missing file operand");
233
362
  return;
234
363
  }
235
- try {
236
- fs.createFile(path);
237
- await fs.persist();
238
- } catch (e) {
239
- context.writer.writeError(e.message);
364
+ let changed = false;
365
+ for (const path of paths) {
366
+ try {
367
+ fs.createFile(path);
368
+ changed = true;
369
+ } catch (e) {
370
+ context.writer.writeError(e.message);
371
+ }
240
372
  }
373
+ if (changed) await fs.persist();
241
374
  }
242
375
  };
376
+
377
+ // src/lib/utils/lines.ts
378
+ function splitLines(content) {
379
+ if (content === "") return [];
380
+ const lines = content.split("\n");
381
+ if (lines[lines.length - 1] === "") lines.pop();
382
+ return lines;
383
+ }
384
+ function pipedText(data) {
385
+ return typeof data === "string" ? data : JSON.stringify(data);
386
+ }
387
+
388
+ // src/lib/processors/cli-cat-command-processor.ts
243
389
  var CliCatCommandProcessor = class {
244
390
  constructor() {
245
391
  this.command = "cat";
@@ -250,22 +396,58 @@ var CliCatCommandProcessor = class {
250
396
  this.valueRequired = true;
251
397
  this.metadata = { icon: "\u{1F4D6}", module: "file management" };
252
398
  }
399
+ writeDescription(context) {
400
+ const t = context.translator;
401
+ writeWrapped(
402
+ context,
403
+ t.t("cli.cat.help.print_contents_one_more", "Print the contents of one or more files, joined in order, or of piped input. Pipe the output into other commands to search, sort or count it.")
404
+ );
405
+ context.writer.writeln();
406
+ writeUsage(context, [
407
+ ["cat <file...>", t.t("cli.cat.help.print_files_one_after", "Print files one after another")],
408
+ ["cat <file> | <command>", t.t("cli.cat.help.send_file_another_command", "Send a file to another command")]
409
+ ]);
410
+ context.writer.writeln();
411
+ writeExamples(context, [
412
+ ["cat notes.txt", t.t("cli.cat.help.show_whole_file", "Show the whole file")],
413
+ ["cat app.log | grep error", t.t("cli.cat.help.show_only_lines_error", "Show only lines with error")],
414
+ ["cat names.txt | sort", t.t("cli.cat.help.print_lines_sorted", "Print the lines sorted")]
415
+ ]);
416
+ }
253
417
  async processCommand(command, context) {
254
418
  const fs = context.services.getRequired(
255
419
  IFileSystemService_TOKEN
256
420
  );
257
- const path = command.value;
258
- if (!path) {
421
+ const paths = splitArgs(command.value || "");
422
+ if (paths.length === 0) {
423
+ if (command.data != null) {
424
+ context.writer.writeln(pipedText(command.data));
425
+ return;
426
+ }
259
427
  context.writer.writeError("cat: missing file operand");
260
428
  return;
261
429
  }
262
- try {
263
- const content = fs.readFile(path);
264
- if (content !== null) {
265
- context.writer.writeln(content);
430
+ let output = "";
431
+ for (const path of paths) {
432
+ try {
433
+ if (fs.isDirectory(path)) {
434
+ context.writer.writeError(`cat: ${path}: Is a directory`);
435
+ continue;
436
+ }
437
+ const content = fs.readFile(path);
438
+ if (content === null) {
439
+ context.writer.writeError(
440
+ `cat: ${path}: No such file or directory`
441
+ );
442
+ continue;
443
+ }
444
+ output += content;
445
+ } catch (e) {
446
+ context.writer.writeError(e.message);
266
447
  }
267
- } catch (e) {
268
- context.writer.writeError(e.message);
448
+ }
449
+ if (output !== "") {
450
+ context.writer.writeln(output.replace(/\n$/, ""));
269
451
  }
270
452
  }
271
453
  };
@@ -279,60 +461,33 @@ var CliEchoCommandProcessor = class {
279
461
  this.extendsProcessor = true;
280
462
  this.metadata = { icon: "\u{1F4AC}", module: "file management" };
281
463
  }
464
+ writeDescription(context) {
465
+ const t = context.translator;
466
+ writeWrapped(
467
+ context,
468
+ t.t("cli.echo.help.print_text_terminal_add", "Print text to the terminal. Add > file to save it to a file (replacing it) or >> file to add it to the end.")
469
+ );
470
+ context.writer.writeln();
471
+ writeUsage(context, [
472
+ ["echo <text>", t.t("cli.echo.help.print_text", "Print text")],
473
+ ["echo <text> > <file>", t.t("cli.echo.help.write_text_file", "Write text to a file")],
474
+ ["echo <text> >> <file>", t.t("cli.echo.help.append_text_file", "Append text to a file")]
475
+ ]);
476
+ context.writer.writeln();
477
+ writeExamples(context, [
478
+ ["echo Hello world", t.t("cli.echo.help.print_greeting", "Print a greeting")],
479
+ ["echo first line > notes.txt", t.t("cli.echo.help.create_replace_notes_txt", "Create or replace notes.txt")],
480
+ ["echo another line >> notes.txt", t.t("cli.echo.help.add_line", "Add a line to it")]
481
+ ]);
482
+ }
282
483
  async processCommand(command, context) {
283
- const afterEcho = command.value || "";
284
- const hasRedirection = />>?\s*.+$/.test(afterEcho);
285
- if (!hasRedirection) {
286
- if (this.originalProcessor) {
287
- await this.originalProcessor.processCommand(command, context);
288
- } else {
289
- context.writer.writeln(afterEcho);
290
- }
484
+ if (this.originalProcessor) {
485
+ await this.originalProcessor.processCommand(command, context);
291
486
  return;
292
487
  }
293
- const fs = context.services.getRequired(
294
- IFileSystemService_TOKEN
295
- );
296
- let text;
297
- let filePath;
298
- let append = false;
299
- const appendMatch = afterEcho.match(/^(.*?)\s*>>\s*(.+)$/);
300
- const overwriteMatch = afterEcho.match(/^(.*?)\s*>\s*(.+)$/);
301
- if (appendMatch) {
302
- text = appendMatch[1].trim();
303
- filePath = appendMatch[2].trim();
304
- append = true;
305
- } else {
306
- text = overwriteMatch[1].trim();
307
- filePath = overwriteMatch[2].trim();
308
- }
309
- if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
310
- text = text.slice(1, -1);
311
- }
312
- text = text.replace(/\\([\\nrtv0])/g, (_, ch) => {
313
- switch (ch) {
314
- case "n":
315
- return "\n";
316
- case "t":
317
- return " ";
318
- case "r":
319
- return "\r";
320
- case "v":
321
- return "\v";
322
- case "0":
323
- return "\0";
324
- case "\\":
325
- return "\\";
326
- default:
327
- return ch;
328
- }
329
- });
330
- try {
331
- fs.writeFile(filePath, text + "\n", append);
332
- await fs.persist();
333
- } catch (e) {
334
- context.writer.writeError(e.message);
335
- }
488
+ const text = joinShellWords(command.value || "");
489
+ context.writer.writeln(text);
490
+ context.process.output(text);
336
491
  }
337
492
  };
338
493
  var CliRmCommandProcessor = class {
@@ -361,35 +516,58 @@ var CliRmCommandProcessor = class {
361
516
  }
362
517
  ];
363
518
  }
519
+ writeDescription(context) {
520
+ const t = context.translator;
521
+ writeWrapped(
522
+ context,
523
+ t.t("cli.rm.help.delete_file_directories_only", "Delete a file. Directories are only removed with -r, which deletes everything inside them. Deleted files cannot be recovered.")
524
+ );
525
+ context.writer.writeln();
526
+ writeUsage(context, [
527
+ ["rm <file...>", t.t("cli.rm.help.delete_one_more_files", "Delete one or more files")],
528
+ ["rm -r <dir>", t.t("cli.rm.help.delete_directory_contents", "Delete a directory and its contents")],
529
+ ["rm -f <file>", t.t("cli.rm.help.delete_without_error_if", "Delete without an error if it is missing")]
530
+ ]);
531
+ context.writer.writeln();
532
+ writeExamples(context, [
533
+ ["rm notes.bak", t.t("cli.rm.help.delete_one_file", "Delete one file")],
534
+ ["rm -r old-project", t.t("cli.rm.help.delete_whole_directory", "Delete a whole directory")],
535
+ ["rm -f maybe.txt", t.t("cli.rm.help.quiet_if_file_does", "Quiet if the file does not exist")]
536
+ ]);
537
+ }
364
538
  async processCommand(command, context) {
365
539
  const fs = context.services.getRequired(
366
540
  IFileSystemService_TOKEN
367
541
  );
368
- const path = command.value;
542
+ const paths = splitArgs(command.value || "");
369
543
  const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
370
544
  const force = command.args["force"] || command.args["f"];
371
- if (!path) {
372
- context.writer.writeError("rm: missing operand");
545
+ if (paths.length === 0) {
546
+ if (!force) context.writer.writeError("rm: missing operand");
373
547
  return;
374
548
  }
375
- try {
376
- if (!fs.exists(path)) {
377
- if (!force) {
378
- context.writer.writeError(
379
- `rm: ${path}: No such file or directory`
380
- );
549
+ let changed = false;
550
+ for (const path of paths) {
551
+ try {
552
+ if (!fs.exists(path)) {
553
+ if (!force) {
554
+ context.writer.writeError(
555
+ `rm: ${path}: No such file or directory`
556
+ );
557
+ }
558
+ continue;
381
559
  }
382
- return;
383
- }
384
- if (fs.isDirectory(path) && !recursive) {
385
- context.writer.writeError(`rm: ${path}: Is a directory`);
386
- return;
560
+ if (fs.isDirectory(path) && !recursive) {
561
+ context.writer.writeError(`rm: ${path}: Is a directory`);
562
+ continue;
563
+ }
564
+ fs.remove(path, recursive);
565
+ changed = true;
566
+ } catch (e) {
567
+ context.writer.writeError(e.message);
387
568
  }
388
- fs.remove(path, recursive);
389
- await fs.persist();
390
- } catch (e) {
391
- context.writer.writeError(e.message);
392
569
  }
570
+ if (changed) await fs.persist();
393
571
  }
394
572
  };
395
573
  var CliCpCommandProcessor = class {
@@ -407,9 +585,35 @@ var CliCpCommandProcessor = class {
407
585
  description: "Copy directories recursively",
408
586
  required: false,
409
587
  type: "boolean"
588
+ },
589
+ {
590
+ name: "force",
591
+ aliases: ["f"],
592
+ description: "Overwrite existing files (always done; accepted for -rf)",
593
+ required: false,
594
+ type: "boolean"
410
595
  }
411
596
  ];
412
597
  }
598
+ writeDescription(context) {
599
+ const t = context.translator;
600
+ writeWrapped(
601
+ context,
602
+ t.t("cli.cp.help.copy_file_new_path", "Copy a file to a new path, or into an existing directory. Copying a directory needs -r, which copies everything inside it. A file already at the destination is replaced.")
603
+ );
604
+ context.writer.writeln();
605
+ writeUsage(context, [
606
+ ["cp <source> <dest>", t.t("cli.cp.help.copy_file_new_name", "Copy a file to a new name")],
607
+ ["cp <source...> <dir>", t.t("cli.cp.help.copy_files_directory", "Copy files into a directory")],
608
+ ["cp -r <dir> <dest>", t.t("cli.cp.help.copy_directory_contents", "Copy a directory and its contents")]
609
+ ]);
610
+ context.writer.writeln();
611
+ writeExamples(context, [
612
+ ["cp notes.txt notes.bak", t.t("cli.cp.help.make_backup_copy", "Make a backup copy")],
613
+ ["cp report.txt ~/docs", t.t("cli.cp.help.copy_docs", "Copy into ~/docs")],
614
+ ["cp -r project project-copy", t.t("cli.cp.help.duplicate_whole_directory", "Duplicate a whole directory")]
615
+ ]);
616
+ }
413
617
  async processCommand(command, context) {
414
618
  const fs = context.services.getRequired(
415
619
  IFileSystemService_TOKEN
@@ -423,18 +627,26 @@ var CliCpCommandProcessor = class {
423
627
  );
424
628
  return;
425
629
  }
426
- const src = parts[0];
427
- const dest = parts[1];
428
- try {
429
- fs.copy(src, dest, recursive);
430
- await fs.persist();
431
- } catch (e) {
432
- context.writer.writeError(e.message);
630
+ const dest = parts[parts.length - 1];
631
+ const sources = parts.slice(0, -1);
632
+ if (sources.length > 1 && !fs.isDirectory(dest)) {
633
+ context.writer.writeError(`cp: target '${dest}' is not a directory`);
634
+ return;
433
635
  }
636
+ let changed = false;
637
+ for (const src of sources) {
638
+ try {
639
+ fs.copy(src, dest, recursive);
640
+ changed = true;
641
+ } catch (e) {
642
+ context.writer.writeError(e.message);
643
+ }
644
+ }
645
+ if (changed) await fs.persist();
434
646
  }
435
647
  parseArgs(command) {
436
648
  const raw = command.value || "";
437
- return raw.split(/\s+/).filter((t) => t && !t.startsWith("--") && !t.startsWith("-"));
649
+ return splitArgs(raw).filter(Boolean);
438
650
  }
439
651
  };
440
652
  var CliMvCommandProcessor = class {
@@ -446,6 +658,24 @@ var CliMvCommandProcessor = class {
446
658
  this.acceptsRawInput = true;
447
659
  this.metadata = { icon: "\u{1F4E6}", module: "file management" };
448
660
  }
661
+ writeDescription(context) {
662
+ const t = context.translator;
663
+ writeWrapped(
664
+ context,
665
+ t.t("cli.mv.help.move_rename_file_directory", "Move or rename a file or directory. If the destination is an existing directory, the source is moved into it; otherwise it is renamed. A file already at the destination is replaced.")
666
+ );
667
+ context.writer.writeln();
668
+ writeUsage(context, [
669
+ ["mv <source> <dest>", t.t("cli.mv.help.rename_file_directory", "Rename a file or directory")],
670
+ ["mv <source...> <dir>", t.t("cli.mv.help.move_them_directory", "Move them into a directory")]
671
+ ]);
672
+ context.writer.writeln();
673
+ writeExamples(context, [
674
+ ["mv draft.txt final.txt", t.t("cli.mv.help.rename_file", "Rename a file")],
675
+ ["mv final.txt ~/docs", t.t("cli.mv.help.move_docs", "Move it into ~/docs")],
676
+ ["mv old-dir new-dir", t.t("cli.mv.help.rename_directory", "Rename a directory")]
677
+ ]);
678
+ }
449
679
  async processCommand(command, context) {
450
680
  const fs = context.services.getRequired(
451
681
  IFileSystemService_TOKEN
@@ -456,18 +686,26 @@ var CliMvCommandProcessor = class {
456
686
  context.writer.writeln("Usage: mv <source> <destination>");
457
687
  return;
458
688
  }
459
- const src = parts[0];
460
- const dest = parts[1];
461
- try {
462
- fs.move(src, dest);
463
- await fs.persist();
464
- } catch (e) {
465
- context.writer.writeError(e.message);
689
+ const dest = parts[parts.length - 1];
690
+ const sources = parts.slice(0, -1);
691
+ if (sources.length > 1 && !fs.isDirectory(dest)) {
692
+ context.writer.writeError(`mv: target '${dest}' is not a directory`);
693
+ return;
694
+ }
695
+ let changed = false;
696
+ for (const src of sources) {
697
+ try {
698
+ fs.move(src, dest);
699
+ changed = true;
700
+ } catch (e) {
701
+ context.writer.writeError(e.message);
702
+ }
466
703
  }
704
+ if (changed) await fs.persist();
467
705
  }
468
706
  parseArgs(command) {
469
707
  const raw = command.value || "";
470
- return raw.split(/\s+/).filter((t) => t && !t.startsWith("--") && !t.startsWith("-"));
708
+ return splitArgs(raw).filter(Boolean);
471
709
  }
472
710
  };
473
711
  function readableChildren(fs, path) {
@@ -496,11 +734,29 @@ var CliTreeCommandProcessor = class {
496
734
  }
497
735
  ];
498
736
  }
737
+ writeDescription(context) {
738
+ const t = context.translator;
739
+ writeWrapped(
740
+ context,
741
+ t.t("cli.tree.help.draw_directory_everything_below", "Draw a directory and everything below it as a tree, directories first, followed by a count of directories and files. Defaults to the current directory.")
742
+ );
743
+ context.writer.writeln();
744
+ writeUsage(context, [
745
+ ["tree [dir]", t.t("cli.tree.help.show_whole_tree", "Show the whole tree")],
746
+ ["tree -L <n> [dir]", t.t("cli.tree.help.only_levels_deep", "Only <n> levels deep")]
747
+ ]);
748
+ context.writer.writeln();
749
+ writeExamples(context, [
750
+ ["tree", t.t("cli.tree.help.tree_current_directory", "Tree of the current directory")],
751
+ ["tree ~/docs", t.t("cli.tree.help.tree_docs", "Tree of ~/docs")],
752
+ ["tree -L 2", t.t("cli.tree.help.top_two_levels_only", "Top two levels only")]
753
+ ]);
754
+ }
499
755
  async processCommand(command, context) {
500
756
  const fs = context.services.getRequired(
501
757
  IFileSystemService_TOKEN
502
758
  );
503
- const targetPath = command.value || fs.getCurrentDirectory();
759
+ const targetPath = splitArgs(command.value || "").join(" ") || fs.getCurrentDirectory();
504
760
  const maxDepth = command.args["depth"] ? parseInt(command.args["depth"]) : command.args["L"] ? parseInt(command.args["L"]) : Infinity;
505
761
  try {
506
762
  const node = fs.getNode(targetPath);
@@ -595,21 +851,40 @@ var CliHeadCommandProcessor = class {
595
851
  }
596
852
  ];
597
853
  }
854
+ writeDescription(context) {
855
+ const t = context.translator;
856
+ writeWrapped(
857
+ context,
858
+ t.t("cli.head.help.print_first_lines_file", "Print the first lines of a file, 10 by default. Works on piped input too. With several files, each one gets a ==> name <== header.")
859
+ );
860
+ context.writer.writeln();
861
+ writeUsage(context, [
862
+ ["head <file>", t.t("cli.head.help.first_10_lines", "First 10 lines")],
863
+ ["head -n <count> <file>", t.t("cli.head.help.first_lines", "First <count> lines")],
864
+ ["<command> | head", t.t("cli.head.help.first_lines_piped_input", "First lines of piped input")]
865
+ ]);
866
+ context.writer.writeln();
867
+ writeExamples(context, [
868
+ ["head notes.txt", t.t("cli.head.help.show_start_file", "Show the start of a file")],
869
+ ["head -n 3 a.txt b.txt", t.t("cli.head.help.first_3_lines_each", "First 3 lines of each file")],
870
+ ["cat app.log | head -n 20", t.t("cli.head.help.first_20_lines_log", "First 20 lines of the log")]
871
+ ]);
872
+ }
598
873
  async processCommand(command, context) {
599
874
  const fs = context.services.getRequired(
600
875
  IFileSystemService_TOKEN
601
876
  );
602
- const count = parseInt(
603
- command.args["lines"] || command.args["n"] || "10",
604
- 10
605
- );
877
+ const countArg = command.args["lines"] ?? command.args["n"] ?? Object.keys(command.args).find((k) => /^\d+$/.test(k));
878
+ const parsedCount = parseInt(String(countArg ?? "10"), 10);
879
+ const count = isNaN(parsedCount) ? 10 : Math.abs(parsedCount);
606
880
  const paths = this.parsePaths(command);
607
881
  if (paths.length === 0) {
608
882
  if (command.data != null) {
609
- const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
610
- const lines = content.split("\n");
883
+ const lines = splitLines(pipedText(command.data));
611
884
  const selected = lines.slice(0, count);
612
- context.writer.writeln(selected.join("\n"));
885
+ if (selected.length > 0) {
886
+ context.writer.writeln(selected.join("\n"));
887
+ }
613
888
  return;
614
889
  }
615
890
  context.writer.writeError("head: missing file operand");
@@ -628,9 +903,11 @@ var CliHeadCommandProcessor = class {
628
903
  if (i > 0) context.writer.writeln();
629
904
  context.writer.writeln(`==> ${path} <==`);
630
905
  }
631
- const lines = content.split("\n");
906
+ const lines = splitLines(content);
632
907
  const selected = lines.slice(0, count);
633
- context.writer.writeln(selected.join("\n"));
908
+ if (selected.length > 0) {
909
+ context.writer.writeln(selected.join("\n"));
910
+ }
634
911
  } catch (e) {
635
912
  context.writer.writeError(`head: ${e.message}`);
636
913
  }
@@ -638,7 +915,7 @@ var CliHeadCommandProcessor = class {
638
915
  }
639
916
  parsePaths(command) {
640
917
  const raw = command.value || "";
641
- return raw.split(/\s+/).filter(Boolean);
918
+ return splitArgs(raw).filter(Boolean);
642
919
  }
643
920
  };
644
921
  var CliTailCommandProcessor = class {
@@ -660,21 +937,40 @@ var CliTailCommandProcessor = class {
660
937
  }
661
938
  ];
662
939
  }
940
+ writeDescription(context) {
941
+ const t = context.translator;
942
+ writeWrapped(
943
+ context,
944
+ t.t("cli.tail.help.print_last_lines_file", "Print the last lines of a file, 10 by default. Works on piped input too. With several files, each one gets a ==> name <== header.")
945
+ );
946
+ context.writer.writeln();
947
+ writeUsage(context, [
948
+ ["tail <file>", t.t("cli.tail.help.last_10_lines", "Last 10 lines")],
949
+ ["tail -n <count> <file>", t.t("cli.tail.help.last_lines", "Last <count> lines")],
950
+ ["<command> | tail", t.t("cli.tail.help.last_lines_piped_input", "Last lines of piped input")]
951
+ ]);
952
+ context.writer.writeln();
953
+ writeExamples(context, [
954
+ ["tail app.log", t.t("cli.tail.help.show_end_log", "Show the end of a log")],
955
+ ["tail -n 5 notes.txt", t.t("cli.tail.help.last_5_lines", "Last 5 lines")],
956
+ ["cat app.log | tail -n 20", t.t("cli.tail.help.last_20_lines_log", "Last 20 lines of the log")]
957
+ ]);
958
+ }
663
959
  async processCommand(command, context) {
664
960
  const fs = context.services.getRequired(
665
961
  IFileSystemService_TOKEN
666
962
  );
667
- const count = parseInt(
668
- command.args["lines"] || command.args["n"] || "10",
669
- 10
670
- );
963
+ const countArg = command.args["lines"] ?? command.args["n"] ?? Object.keys(command.args).find((k) => /^\d+$/.test(k));
964
+ const parsedCount = parseInt(String(countArg ?? "10"), 10);
965
+ const count = isNaN(parsedCount) ? 10 : Math.abs(parsedCount);
671
966
  const paths = this.parsePaths(command);
672
967
  if (paths.length === 0) {
673
968
  if (command.data != null) {
674
- const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
675
- const lines = content.split("\n");
676
- const selected = lines.slice(-count);
677
- context.writer.writeln(selected.join("\n"));
969
+ const lines = splitLines(pipedText(command.data));
970
+ const selected = count > 0 ? lines.slice(-count) : [];
971
+ if (selected.length > 0) {
972
+ context.writer.writeln(selected.join("\n"));
973
+ }
678
974
  return;
679
975
  }
680
976
  context.writer.writeError("tail: missing file operand");
@@ -693,9 +989,11 @@ var CliTailCommandProcessor = class {
693
989
  if (i > 0) context.writer.writeln();
694
990
  context.writer.writeln(`==> ${path} <==`);
695
991
  }
696
- const lines = content.split("\n");
697
- const selected = lines.slice(-count);
698
- context.writer.writeln(selected.join("\n"));
992
+ const lines = splitLines(content);
993
+ const selected = count > 0 ? lines.slice(-count) : [];
994
+ if (selected.length > 0) {
995
+ context.writer.writeln(selected.join("\n"));
996
+ }
699
997
  } catch (e) {
700
998
  context.writer.writeError(`tail: ${e.message}`);
701
999
  }
@@ -703,7 +1001,7 @@ var CliTailCommandProcessor = class {
703
1001
  }
704
1002
  parsePaths(command) {
705
1003
  const raw = command.value || "";
706
- return raw.split(/\s+/).filter(Boolean);
1004
+ return splitArgs(raw).filter(Boolean);
707
1005
  }
708
1006
  };
709
1007
  var CliWcCommandProcessor = class {
@@ -739,6 +1037,28 @@ var CliWcCommandProcessor = class {
739
1037
  }
740
1038
  ];
741
1039
  }
1040
+ writeDescription(context) {
1041
+ const t = context.translator;
1042
+ writeWrapped(
1043
+ context,
1044
+ t.t("cli.wc.help.count_lines_words_characters", "Count lines, words and characters in files or piped input. With several files, a total line is printed at the end.")
1045
+ );
1046
+ context.writer.writeln();
1047
+ writeUsage(context, [
1048
+ ["wc <file...>", t.t("cli.wc.help.lines_words_characters", "Lines, words and characters")],
1049
+ ["wc -l <file>", t.t("cli.wc.help.only_line_count", "Only the line count")],
1050
+ ["<command> | wc", t.t("cli.wc.help.count_piped_input", "Count piped input")]
1051
+ ]);
1052
+ context.writer.writeln();
1053
+ writeExamples(context, [
1054
+ ["wc notes.txt", t.t("cli.wc.help.all_three_counts", "All three counts")],
1055
+ ["wc -l a.txt b.txt", t.t("cli.wc.help.line_counts_total", "Line counts with a total")],
1056
+ [
1057
+ "cat app.log | grep error | wc -l",
1058
+ t.t("cli.wc.help.how_many_lines_mention", "How many lines mention error")
1059
+ ]
1060
+ ]);
1061
+ }
742
1062
  async processCommand(command, context) {
743
1063
  const fs = context.services.getRequired(
744
1064
  IFileSystemService_TOKEN
@@ -750,10 +1070,9 @@ var CliWcCommandProcessor = class {
750
1070
  const paths = this.parsePaths(command);
751
1071
  if (paths.length === 0) {
752
1072
  if (command.data != null) {
753
- const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
754
- const lineCount = content === "" ? 0 : content.split("\n").length;
755
- const wordCount = content.trim() === "" ? 0 : content.trim().split(/\s+/).length;
756
- const charCount = content.length;
1073
+ let content = pipedText(command.data);
1074
+ if (content !== "" && !content.endsWith("\n")) content += "\n";
1075
+ const { lineCount, wordCount, charCount } = this.count(content);
757
1076
  const parts = [];
758
1077
  if (showAll || showLines) parts.push(String(lineCount).padStart(8));
759
1078
  if (showAll || showWords) parts.push(String(wordCount).padStart(8));
@@ -774,9 +1093,7 @@ var CliWcCommandProcessor = class {
774
1093
  continue;
775
1094
  }
776
1095
  const content = fs.readFile(path) ?? "";
777
- const lineCount = content === "" ? 0 : content.split("\n").length;
778
- const wordCount = content.trim() === "" ? 0 : content.trim().split(/\s+/).length;
779
- const charCount = content.length;
1096
+ const { lineCount, wordCount, charCount } = this.count(content);
780
1097
  totalLines += lineCount;
781
1098
  totalWords += wordCount;
782
1099
  totalChars += charCount;
@@ -799,9 +1116,17 @@ var CliWcCommandProcessor = class {
799
1116
  context.writer.writeln(parts.join(""));
800
1117
  }
801
1118
  }
1119
+ /** Counts like Unix wc: lines are newline characters. */
1120
+ count(content) {
1121
+ return {
1122
+ lineCount: (content.match(/\n/g) ?? []).length,
1123
+ wordCount: content.trim() === "" ? 0 : content.trim().split(/\s+/).length,
1124
+ charCount: content.length
1125
+ };
1126
+ }
802
1127
  parsePaths(command) {
803
1128
  const raw = command.value || "";
804
- return raw.split(/\s+/).filter(Boolean);
1129
+ return splitArgs(raw).filter(Boolean);
805
1130
  }
806
1131
  };
807
1132
  var CliFindCommandProcessor = class {
@@ -815,7 +1140,13 @@ var CliFindCommandProcessor = class {
815
1140
  this.parameters = [
816
1141
  {
817
1142
  name: "name",
818
- description: "Match filename pattern (glob: * and ? supported)",
1143
+ description: "Match filename pattern (glob: * and ? supported), case-sensitive",
1144
+ required: false,
1145
+ type: "string"
1146
+ },
1147
+ {
1148
+ name: "iname",
1149
+ description: "Like --name, but case-insensitive",
819
1150
  required: false,
820
1151
  type: "string"
821
1152
  },
@@ -827,20 +1158,42 @@ var CliFindCommandProcessor = class {
827
1158
  },
828
1159
  {
829
1160
  name: "maxdepth",
830
- description: "Maximum directory depth to search",
1161
+ description: "Maximum depth: 0 is only the start path, 1 its direct children",
831
1162
  required: false,
832
1163
  type: "number"
833
1164
  }
834
1165
  ];
835
1166
  }
1167
+ writeDescription(context) {
1168
+ const t = context.translator;
1169
+ writeWrapped(
1170
+ context,
1171
+ t.t("cli.find.help.list_files_directories_under", "List files and directories under a path (the current directory by default), searching all subdirectories. The start path itself is listed first. Filter by name with a glob pattern (* and ?; --name is case-sensitive, --iname is not) or by type.")
1172
+ );
1173
+ context.writer.writeln();
1174
+ writeUsage(context, [
1175
+ ["find [path]", t.t("cli.find.help.list_everything_under_path", "List everything under a path")],
1176
+ ["find [path] --name <pattern>", t.t("cli.find.help.match_names_against_glob", "Match names against a glob")],
1177
+ ["find [path] --iname <pattern>", t.t("cli.find.help.same_ignoring_case", "Same, ignoring case")],
1178
+ ["find [path] --type f|d", t.t("cli.find.help.only_files_f_directories", "Only files (f) or directories (d)")]
1179
+ ]);
1180
+ context.writer.writeln();
1181
+ writeExamples(context, [
1182
+ ["find ~/docs", t.t("cli.find.help.list_everything_docs", "List everything in ~/docs")],
1183
+ ['find . --name "*.txt"', t.t("cli.find.help.find_all_txt_files", "Find all .txt files")],
1184
+ ["find . --type d --maxdepth 1", t.t("cli.find.help.directories_near_top", "Directories near the top")],
1185
+ ['find . --name "*.tmp" | xargs rm', t.t("cli.find.help.delete_all_tmp_files", "Delete all .tmp files")]
1186
+ ]);
1187
+ }
836
1188
  async processCommand(command, context) {
837
1189
  const fs = context.services.getRequired(
838
1190
  IFileSystemService_TOKEN
839
1191
  );
840
1192
  const searchPath = this.parseSearchPath(command) || fs.getCurrentDirectory();
841
- const namePattern = command.args["name"] || null;
842
1193
  const typeFilter = command.args["type"] || null;
843
- const maxDepth = command.args["maxdepth"] ? parseInt(command.args["maxdepth"], 10) : Infinity;
1194
+ const maxDepthArg = command.args["maxdepth"];
1195
+ const parsedDepth = parseInt(String(maxDepthArg ?? ""), 10);
1196
+ const maxDepth = maxDepthArg != null && maxDepthArg !== true && !isNaN(parsedDepth) ? parsedDepth : Infinity;
844
1197
  try {
845
1198
  const node = fs.getNode(searchPath);
846
1199
  if (!node) {
@@ -849,25 +1202,25 @@ var CliFindCommandProcessor = class {
849
1202
  );
850
1203
  return;
851
1204
  }
852
- if (node.type !== "directory") {
853
- context.writer.writeError(
854
- `find: '${searchPath}': Not a directory`
855
- );
856
- return;
857
- }
858
1205
  const resolvedBase = fs.resolvePath(searchPath);
859
- const nameRegex = namePattern ? this.globToRegex(namePattern) : null;
1206
+ const nameRegex = this.nameRegex(command);
860
1207
  const results = [];
861
1208
  this.fs = fs;
862
- this.walk(
863
- node,
864
- resolvedBase,
865
- 0,
866
- maxDepth,
867
- nameRegex,
868
- typeFilter,
869
- results
870
- );
1209
+ const baseName = resolvedBase.split("/").filter(Boolean).pop() ?? "/";
1210
+ if (this.matches(baseName, node.type, nameRegex, typeFilter)) {
1211
+ results.push(resolvedBase);
1212
+ }
1213
+ if (node.type === "directory" && maxDepth > 0) {
1214
+ this.walk(
1215
+ node,
1216
+ resolvedBase,
1217
+ 1,
1218
+ maxDepth,
1219
+ nameRegex,
1220
+ typeFilter,
1221
+ results
1222
+ );
1223
+ }
871
1224
  for (const r of results) {
872
1225
  context.writer.writeln(r);
873
1226
  }
@@ -875,32 +1228,41 @@ var CliFindCommandProcessor = class {
875
1228
  context.writer.writeError(`find: ${e.message}`);
876
1229
  }
877
1230
  }
1231
+ /** `--name` matches case-sensitively, `--iname` ignoring case. */
1232
+ nameRegex(command) {
1233
+ const name = command.args["name"];
1234
+ if (name != null && name !== true) {
1235
+ return this.globToRegex(String(name), false);
1236
+ }
1237
+ const iname = command.args["iname"];
1238
+ if (iname != null && iname !== true) {
1239
+ return this.globToRegex(String(iname), true);
1240
+ }
1241
+ return null;
1242
+ }
1243
+ matches(name, type, nameRegex, typeFilter) {
1244
+ if (nameRegex && !nameRegex.test(name)) return false;
1245
+ if (typeFilter === "f" && type !== "file") return false;
1246
+ if (typeFilter === "d" && type !== "directory") return false;
1247
+ return true;
1248
+ }
878
1249
  parseSearchPath(command) {
879
1250
  const raw = command.value || "";
880
- const tokens = raw.split(/\s+/).filter(Boolean);
1251
+ const tokens = splitArgs(raw).filter(Boolean);
881
1252
  return tokens.length > 0 ? tokens[0] : null;
882
1253
  }
883
- globToRegex(pattern) {
1254
+ globToRegex(pattern, ignoreCase) {
884
1255
  const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
885
1256
  const regexStr = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
886
- return new RegExp(`^${regexStr}$`, "i");
1257
+ return new RegExp(`^${regexStr}$`, ignoreCase ? "i" : "");
887
1258
  }
1259
+ /** Lists the children of `node`, which are at `depth` (1 = direct children). */
888
1260
  walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
889
1261
  const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
890
1262
  if (!children) return;
891
1263
  for (const child of children) {
892
1264
  const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
893
- let matches = true;
894
- if (nameRegex && !nameRegex.test(child.name)) {
895
- matches = false;
896
- }
897
- if (typeFilter === "f" && child.type !== "file") {
898
- matches = false;
899
- }
900
- if (typeFilter === "d" && child.type !== "directory") {
901
- matches = false;
902
- }
903
- if (matches) {
1265
+ if (this.matches(child.name, child.type, nameRegex, typeFilter)) {
904
1266
  results.push(childPath);
905
1267
  }
906
1268
  if (child.type === "directory" && depth < maxDepth) {
@@ -954,6 +1316,13 @@ var CliGrepCommandProcessor = class {
954
1316
  required: false,
955
1317
  type: "boolean"
956
1318
  },
1319
+ {
1320
+ name: "quiet",
1321
+ aliases: ["q"],
1322
+ description: "Print nothing; only the exit status tells whether a line matched",
1323
+ required: false,
1324
+ type: "boolean"
1325
+ },
957
1326
  {
958
1327
  name: "files-with-matches",
959
1328
  aliases: ["l"],
@@ -962,14 +1331,55 @@ var CliGrepCommandProcessor = class {
962
1331
  type: "boolean"
963
1332
  },
964
1333
  {
965
- name: "invert-match",
966
- aliases: ["v"],
967
- description: "Select non-matching lines",
1334
+ name: "invert-match",
1335
+ aliases: ["v"],
1336
+ description: "Select non-matching lines",
1337
+ required: false,
1338
+ type: "boolean"
1339
+ },
1340
+ {
1341
+ name: "word-regexp",
1342
+ aliases: ["w"],
1343
+ description: "Match only whole words",
1344
+ required: false,
1345
+ type: "boolean"
1346
+ },
1347
+ {
1348
+ name: "only-matching",
1349
+ aliases: ["o"],
1350
+ description: "Print only the matched parts, one per line",
1351
+ required: false,
1352
+ type: "boolean"
1353
+ },
1354
+ {
1355
+ name: "extended-regexp",
1356
+ aliases: ["E"],
1357
+ description: "Extended regular expressions (patterns always are)",
968
1358
  required: false,
969
1359
  type: "boolean"
970
1360
  }
971
1361
  ];
972
1362
  }
1363
+ writeDescription(context) {
1364
+ const t = context.translator;
1365
+ writeWrapped(
1366
+ context,
1367
+ t.t("cli.grep.help.print_lines_match_pattern", "Print the lines that match a pattern. The pattern is a regular expression, matches are highlighted, and the input can be files, directories (with -r) or piped text.")
1368
+ );
1369
+ context.writer.writeln();
1370
+ writeUsage(context, [
1371
+ ["grep <pattern> <file...>", t.t("cli.grep.help.search_one_more_files", "Search one or more files")],
1372
+ ["grep -r <pattern> <dir>", t.t("cli.grep.help.search_every_file_under", "Search every file under a directory")],
1373
+ ["<command> | grep <pattern>", t.t("cli.grep.help.filter_piped_input", "Filter piped input")]
1374
+ ]);
1375
+ context.writer.writeln();
1376
+ writeExamples(context, [
1377
+ ["grep -i error app.log", t.t("cli.grep.help.case_insensitive_search", "Case-insensitive search")],
1378
+ ["grep -r -n TODO src", t.t("cli.grep.help.find_todos_line_numbers", "Find TODOs with line numbers")],
1379
+ ["grep -v ^# config.txt", t.t("cli.grep.help.hide_comment_lines", "Hide comment lines")],
1380
+ ["cat app.log | grep -c ERROR", t.t("cli.grep.help.count_matching_lines", "Count matching lines")]
1381
+ ]);
1382
+ }
973
1383
  async processCommand(command, context) {
974
1384
  const fs = context.services.getRequired(
975
1385
  IFileSystemService_TOKEN
@@ -980,51 +1390,56 @@ var CliGrepCommandProcessor = class {
980
1390
  const countOnly = command.args["count"] || command.args["c"] || false;
981
1391
  const filesOnly = command.args["files-with-matches"] || command.args["l"] || false;
982
1392
  const invert = command.args["invert-match"] || command.args["v"] || false;
1393
+ const wordRegexp = command.args["word-regexp"] || command.args["w"] || false;
1394
+ const onlyMatching = command.args["only-matching"] || command.args["o"] || false;
1395
+ const quiet = command.args["quiet"] || command.args["q"] || false;
983
1396
  const { pattern, paths } = this.parseArgs(command);
984
1397
  if (!pattern) {
985
1398
  context.writer.writeError(
986
1399
  "grep: missing pattern. Usage: grep [options] <pattern> <file>"
987
1400
  );
1401
+ context.process.exit(2, { silent: true });
1402
+ return;
1403
+ }
1404
+ let regex;
1405
+ try {
1406
+ const source = wordRegexp ? `(?<!\\w)(?:${pattern})(?!\\w)` : pattern;
1407
+ regex = new RegExp(source, ignoreCase ? "gi" : "g");
1408
+ } catch {
1409
+ context.writer.writeError(`grep: invalid pattern '${pattern}'`);
1410
+ context.process.exit(2, { silent: true });
988
1411
  return;
989
1412
  }
990
1413
  if (paths.length === 0) {
991
1414
  if (command.data != null) {
992
- let regex2;
993
- try {
994
- regex2 = new RegExp(pattern, ignoreCase ? "gi" : "g");
995
- } catch {
996
- context.writer.writeError(`grep: invalid pattern '${pattern}'`);
997
- return;
998
- }
999
1415
  const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
1000
- this.grepContent(content, null, regex2, {
1416
+ const found2 = this.grepContent(content, null, regex, {
1001
1417
  ignoreCase,
1002
1418
  showLineNum,
1003
1419
  countOnly,
1004
1420
  filesOnly,
1005
- invert
1421
+ invert,
1422
+ onlyMatching,
1423
+ quiet
1006
1424
  }, context, false);
1425
+ this.exitStatus(context, found2, false);
1007
1426
  return;
1008
1427
  }
1009
1428
  context.writer.writeError(
1010
1429
  "grep: missing file operand. Usage: grep [options] <pattern> <file>"
1011
1430
  );
1012
- return;
1013
- }
1014
- let regex;
1015
- try {
1016
- regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
1017
- } catch {
1018
- context.writer.writeError(`grep: invalid pattern '${pattern}'`);
1431
+ context.process.exit(2, { silent: true });
1019
1432
  return;
1020
1433
  }
1021
1434
  const filePaths = [];
1435
+ let failed = false;
1022
1436
  for (const p of paths) {
1023
1437
  try {
1024
1438
  if (!fs.exists(p)) {
1025
1439
  context.writer.writeError(
1026
1440
  `grep: ${p}: No such file or directory`
1027
1441
  );
1442
+ failed = true;
1028
1443
  continue;
1029
1444
  }
1030
1445
  if (fs.isDirectory(p)) {
@@ -1032,6 +1447,7 @@ var CliGrepCommandProcessor = class {
1032
1447
  context.writer.writeError(
1033
1448
  `grep: ${p}: Is a directory`
1034
1449
  );
1450
+ failed = true;
1035
1451
  continue;
1036
1452
  }
1037
1453
  this.collectFiles(fs, fs.resolvePath(p), fs.getNode(p), filePaths);
@@ -1040,26 +1456,40 @@ var CliGrepCommandProcessor = class {
1040
1456
  }
1041
1457
  } catch (e) {
1042
1458
  context.writer.writeError(`grep: ${e.message}`);
1459
+ failed = true;
1043
1460
  }
1044
1461
  }
1045
1462
  const multiFile = filePaths.length > 1;
1463
+ let found = 0;
1046
1464
  for (const filePath of filePaths) {
1047
1465
  try {
1048
1466
  const content = fs.readFile(filePath) ?? "";
1049
- this.grepContent(content, filePath, regex, {
1467
+ found += this.grepContent(content, filePath, regex, {
1050
1468
  ignoreCase,
1051
1469
  showLineNum,
1052
1470
  countOnly,
1053
1471
  filesOnly,
1054
- invert
1472
+ invert,
1473
+ onlyMatching,
1474
+ quiet
1055
1475
  }, context, multiFile);
1056
1476
  } catch (e) {
1057
1477
  context.writer.writeError(`grep: ${filePath}: ${e.message}`);
1478
+ failed = true;
1058
1479
  }
1059
1480
  }
1481
+ this.exitStatus(context, found, failed);
1482
+ }
1483
+ /**
1484
+ * As Unix grep: 0 when a line matched, 1 when none did, 2 after an
1485
+ * error (so `if grep -q ...` and `grep x f && ...` work).
1486
+ */
1487
+ exitStatus(context, found, failed) {
1488
+ if (found > 0) return;
1489
+ context.process.exit(failed ? 2 : 1, { silent: true });
1060
1490
  }
1061
1491
  grepContent(content, filePath, regex, options, context, multiFile) {
1062
- const lines = content.split("\n");
1492
+ const lines = splitLines(content);
1063
1493
  let matchCount = 0;
1064
1494
  const matchingLines = [];
1065
1495
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
@@ -1072,6 +1502,7 @@ var CliGrepCommandProcessor = class {
1072
1502
  matchingLines.push({ num: lineIdx + 1, text: line });
1073
1503
  }
1074
1504
  }
1505
+ if (options.quiet) return matchCount;
1075
1506
  if (options.filesOnly) {
1076
1507
  if (matchCount > 0 && filePath) {
1077
1508
  context.writer.writeln(filePath);
@@ -1090,6 +1521,18 @@ var CliGrepCommandProcessor = class {
1090
1521
  parts.push(context.writer.wrapInColor(String(m.num), CliForegroundColor.Green));
1091
1522
  parts.push(":");
1092
1523
  }
1524
+ if (options.onlyMatching && !options.invert) {
1525
+ regex.lastIndex = 0;
1526
+ const prefix = parts.join("");
1527
+ for (const match of m.text.match(regex) ?? []) {
1528
+ if (match !== "") {
1529
+ context.writer.writeln(
1530
+ prefix + context.writer.wrapInColor(match, CliForegroundColor.Red)
1531
+ );
1532
+ }
1533
+ }
1534
+ continue;
1535
+ }
1093
1536
  if (!options.invert) {
1094
1537
  regex.lastIndex = 0;
1095
1538
  const highlighted = m.text.replace(
@@ -1103,10 +1546,11 @@ var CliGrepCommandProcessor = class {
1103
1546
  context.writer.writeln(parts.join(""));
1104
1547
  }
1105
1548
  }
1549
+ return matchCount;
1106
1550
  }
1107
1551
  parseArgs(command) {
1108
1552
  const raw = command.value || "";
1109
- const tokens = raw.split(/\s+/).filter(Boolean);
1553
+ const tokens = splitArgs(raw).filter(Boolean);
1110
1554
  if (tokens.length === 0) {
1111
1555
  return { pattern: null, paths: [] };
1112
1556
  }
@@ -1141,6 +1585,23 @@ var CliTacCommandProcessor = class {
1141
1585
  this.valueRequired = true;
1142
1586
  this.metadata = { icon: "\u{1F504}", module: "file management" };
1143
1587
  }
1588
+ writeDescription(context) {
1589
+ const t = context.translator;
1590
+ writeWrapped(
1591
+ context,
1592
+ t.t("cli.tac.help.print_file_lines_reverse", "Print a file with its lines in reverse order, last line first. Works on piped input too.")
1593
+ );
1594
+ context.writer.writeln();
1595
+ writeUsage(context, [
1596
+ ["tac <file>", t.t("cli.tac.help.print_file_bottom_top", "Print a file bottom to top")],
1597
+ ["<command> | tac", t.t("cli.tac.help.reverse_piped_input", "Reverse piped input")]
1598
+ ]);
1599
+ context.writer.writeln();
1600
+ writeExamples(context, [
1601
+ ["tac app.log", t.t("cli.tac.help.newest_log_lines_first", "Newest log lines first")],
1602
+ ["cat list.txt | tac", t.t("cli.tac.help.reverse_list", "Reverse a list")]
1603
+ ]);
1604
+ }
1144
1605
  async processCommand(command, context) {
1145
1606
  const fs = context.services.getRequired(
1146
1607
  IFileSystemService_TOKEN
@@ -1186,7 +1647,7 @@ var CliTacCommandProcessor = class {
1186
1647
  }
1187
1648
  parsePaths(command) {
1188
1649
  const raw = command.value || "";
1189
- return raw.split(/\s+/).filter(Boolean);
1650
+ return splitArgs(raw).filter(Boolean);
1190
1651
  }
1191
1652
  };
1192
1653
  var CliBasenameCommandProcessor = class {
@@ -1199,6 +1660,24 @@ var CliBasenameCommandProcessor = class {
1199
1660
  this.valueRequired = true;
1200
1661
  this.metadata = { icon: "\u{1F4DB}", module: "file management" };
1201
1662
  }
1663
+ writeDescription(context) {
1664
+ const t = context.translator;
1665
+ writeWrapped(
1666
+ context,
1667
+ t.t("cli.basename.help.print_last_part_path", "Print the last part of a path, the file or directory name, without the directories in front. If a suffix is given, it is removed from the end too.")
1668
+ );
1669
+ context.writer.writeln();
1670
+ writeUsage(context, [
1671
+ ["basename <path>", t.t("cli.basename.help.name_without_directories", "Name without the directories")],
1672
+ ["basename <path> <suffix>", t.t("cli.basename.help.also_remove_trailing_suffix", "Also remove a trailing suffix")]
1673
+ ]);
1674
+ context.writer.writeln();
1675
+ writeExamples(context, [
1676
+ ["basename /home/user/notes.txt", t.t("cli.basename.help.prints_notes_txt", "Prints notes.txt")],
1677
+ ["basename /home/user/notes.txt .txt", t.t("cli.basename.help.prints_notes", "Prints notes")],
1678
+ ["basename /home/user/", t.t("cli.basename.help.prints_user", "Prints user")]
1679
+ ]);
1680
+ }
1202
1681
  async processCommand(command, context) {
1203
1682
  const tokens = this.parseTokens(command);
1204
1683
  if (tokens.length === 0) {
@@ -1224,7 +1703,7 @@ var CliBasenameCommandProcessor = class {
1224
1703
  }
1225
1704
  parseTokens(command) {
1226
1705
  const raw = command.value || "";
1227
- return raw.split(/\s+/).filter(Boolean);
1706
+ return splitArgs(raw).filter(Boolean);
1228
1707
  }
1229
1708
  };
1230
1709
  var CliDirnameCommandProcessor = class {
@@ -1237,6 +1716,23 @@ var CliDirnameCommandProcessor = class {
1237
1716
  this.valueRequired = true;
1238
1717
  this.metadata = { icon: "\u{1F4C2}", module: "file management" };
1239
1718
  }
1719
+ writeDescription(context) {
1720
+ const t = context.translator;
1721
+ writeWrapped(
1722
+ context,
1723
+ t.t("cli.dirname.help.print_directory_part_path", "Print the directory part of a path, everything before the last /. A bare name with no / gives . (the current directory).")
1724
+ );
1725
+ context.writer.writeln();
1726
+ writeUsage(context, [
1727
+ ["dirname <path>", t.t("cli.dirname.help.path_without_last_part", "Path without its last part")]
1728
+ ]);
1729
+ context.writer.writeln();
1730
+ writeExamples(context, [
1731
+ ["dirname /home/user/notes.txt", t.t("cli.dirname.help.prints_home_user", "Prints /home/user")],
1732
+ ["dirname notes.txt", t.t("cli.dirname.help.prints", "Prints .")],
1733
+ ["dirname /home", t.t("cli.dirname.help.prints_2", "Prints /")]
1734
+ ]);
1735
+ }
1240
1736
  async processCommand(command, context) {
1241
1737
  const tokens = this.parseTokens(command);
1242
1738
  if (tokens.length === 0) {
@@ -1263,7 +1759,7 @@ var CliDirnameCommandProcessor = class {
1263
1759
  }
1264
1760
  parseTokens(command) {
1265
1761
  const raw = command.value || "";
1266
- return raw.split(/\s+/).filter(Boolean);
1762
+ return splitArgs(raw).filter(Boolean);
1267
1763
  }
1268
1764
  };
1269
1765
  var CliSortCommandProcessor = class {
@@ -1313,6 +1809,29 @@ var CliSortCommandProcessor = class {
1313
1809
  }
1314
1810
  ];
1315
1811
  }
1812
+ writeDescription(context) {
1813
+ const t = context.translator;
1814
+ writeWrapped(
1815
+ context,
1816
+ t.t("cli.sort.help.sort_lines_file_piped", "Sort the lines of a file or piped input, alphabetically by default. Sort numerically, in reverse, by a field, or drop duplicates. Several files are sorted together into one list.")
1817
+ );
1818
+ context.writer.writeln();
1819
+ writeUsage(context, [
1820
+ ["sort <file>", t.t("cli.sort.help.sort_lines_alphabetically", "Sort lines alphabetically")],
1821
+ [
1822
+ "sort -k <n> -t <sep> <file>",
1823
+ t.t("cli.sort.help.sort_field_split", "Sort by field <n>, split on <sep>")
1824
+ ],
1825
+ ["<command> | sort", t.t("cli.sort.help.sort_piped_input", "Sort piped input")]
1826
+ ]);
1827
+ context.writer.writeln();
1828
+ writeExamples(context, [
1829
+ ["sort names.txt", t.t("cli.sort.help.z", "A to Z")],
1830
+ ["sort -n -r scores.txt", t.t("cli.sort.help.largest_number_first", "Largest number first")],
1831
+ ["sort -t , -k 2 data.csv", t.t("cli.sort.help.sort_csv_second_column", "Sort a CSV by its second column")],
1832
+ ["cat words.txt | sort -u", t.t("cli.sort.help.sorted_without_duplicates", "Sorted, without duplicates")]
1833
+ ]);
1834
+ }
1316
1835
  async processCommand(command, context) {
1317
1836
  const fs = context.services.getRequired(
1318
1837
  IFileSystemService_TOKEN
@@ -1320,110 +1839,67 @@ var CliSortCommandProcessor = class {
1320
1839
  const reverse = !!command.args["r"] || !!command.args["reverse"];
1321
1840
  const numeric = !!command.args["n"] || !!command.args["numeric-sort"];
1322
1841
  const unique = !!command.args["u"] || !!command.args["unique"];
1323
- const keyField = command.args["k"] || command.args["key"];
1324
- const delimiter = command.args["t"] || command.args["field-separator"];
1842
+ const keyField = command.args["k"] ?? command.args["key"];
1843
+ const delimiter = command.args["t"] ?? command.args["field-separator"];
1325
1844
  const paths = this.parsePaths(command);
1845
+ let lines = [];
1326
1846
  if (paths.length === 0) {
1327
- if (command.data != null) {
1328
- const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
1329
- let lines = content.split("\n");
1330
- if (lines.length > 0 && lines[lines.length - 1] === "") {
1331
- lines.pop();
1332
- }
1333
- const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
1334
- lines.sort((a, b) => {
1335
- let va = a;
1336
- let vb = b;
1337
- if (keyIndex !== void 0 && keyIndex >= 0) {
1338
- const sepRegex = delimiter ? new RegExp(
1339
- delimiter.replace(
1340
- /[.*+?^${}()|[\]\\]/g,
1341
- "\\$&"
1342
- )
1343
- ) : /\s+/;
1344
- const fieldsA = a.split(sepRegex);
1345
- const fieldsB = b.split(sepRegex);
1346
- va = fieldsA[keyIndex] || "";
1347
- vb = fieldsB[keyIndex] || "";
1348
- }
1349
- if (numeric) {
1350
- const na = parseFloat(va) || 0;
1351
- const nb = parseFloat(vb) || 0;
1352
- return na - nb;
1353
- }
1354
- return va.localeCompare(vb);
1355
- });
1356
- if (unique) {
1357
- lines = lines.filter(
1358
- (line, i, arr) => i === 0 || line !== arr[i - 1]
1359
- );
1360
- }
1361
- if (reverse) {
1362
- lines.reverse();
1363
- }
1364
- context.writer.writeln(lines.join("\n"));
1847
+ if (command.data == null) {
1848
+ context.writer.writeError("sort: missing file operand");
1365
1849
  return;
1366
1850
  }
1367
- context.writer.writeError("sort: missing file operand");
1368
- return;
1369
- }
1370
- for (const path of paths) {
1371
- try {
1372
- if (fs.isDirectory(path)) {
1373
- context.writer.writeError(`sort: ${path}: Is a directory`);
1374
- continue;
1375
- }
1376
- const content = fs.readFile(path);
1377
- if (content === null) {
1378
- context.writer.writeError(
1379
- `sort: ${path}: No such file or directory`
1380
- );
1381
- continue;
1382
- }
1383
- let lines = content.split("\n");
1384
- if (lines.length > 0 && lines[lines.length - 1] === "") {
1385
- lines.pop();
1386
- }
1387
- const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
1388
- lines.sort((a, b) => {
1389
- let va = a;
1390
- let vb = b;
1391
- if (keyIndex !== void 0 && keyIndex >= 0) {
1392
- const sepRegex = delimiter ? new RegExp(
1393
- delimiter.replace(
1394
- /[.*+?^${}()|[\]\\]/g,
1395
- "\\$&"
1396
- )
1397
- ) : /\s+/;
1398
- const fieldsA = a.split(sepRegex);
1399
- const fieldsB = b.split(sepRegex);
1400
- va = fieldsA[keyIndex] || "";
1401
- vb = fieldsB[keyIndex] || "";
1851
+ lines = splitLines(pipedText(command.data));
1852
+ } else {
1853
+ for (const path of paths) {
1854
+ try {
1855
+ if (fs.isDirectory(path)) {
1856
+ context.writer.writeError(
1857
+ `sort: ${path}: Is a directory`
1858
+ );
1859
+ continue;
1402
1860
  }
1403
- if (numeric) {
1404
- const na = parseFloat(va) || 0;
1405
- const nb = parseFloat(vb) || 0;
1406
- return na - nb;
1861
+ const content = fs.readFile(path);
1862
+ if (content === null) {
1863
+ context.writer.writeError(
1864
+ `sort: ${path}: No such file or directory`
1865
+ );
1866
+ continue;
1407
1867
  }
1408
- return va.localeCompare(vb);
1409
- });
1410
- if (unique) {
1411
- lines = lines.filter(
1412
- (line, i, arr) => i === 0 || line !== arr[i - 1]
1413
- );
1414
- }
1415
- if (reverse) {
1416
- lines.reverse();
1868
+ lines.push(...splitLines(content));
1869
+ } catch (e) {
1870
+ context.writer.writeError(`sort: ${e.message}`);
1417
1871
  }
1418
- context.writer.writeln(lines.join("\n"));
1419
- } catch (e) {
1420
- context.writer.writeError(`sort: ${e.message}`);
1421
1872
  }
1422
1873
  }
1874
+ const keyIndex = keyField != null && keyField !== true ? parseInt(String(keyField), 10) - 1 : void 0;
1875
+ const sepRegex = delimiter != null && delimiter !== true ? new RegExp(
1876
+ String(delimiter).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
1877
+ ) : /\s+/;
1878
+ const keyOf = (line) => keyIndex !== void 0 && keyIndex >= 0 ? line.split(sepRegex)[keyIndex] || "" : line;
1879
+ const compare = (a, b) => {
1880
+ const va = keyOf(a);
1881
+ const vb = keyOf(b);
1882
+ if (numeric) {
1883
+ return (parseFloat(va) || 0) - (parseFloat(vb) || 0);
1884
+ }
1885
+ return va.localeCompare(vb);
1886
+ };
1887
+ lines.sort(compare);
1888
+ if (unique) {
1889
+ lines = lines.filter(
1890
+ (line, i, arr) => i === 0 || compare(line, arr[i - 1]) !== 0
1891
+ );
1892
+ }
1893
+ if (reverse) {
1894
+ lines.reverse();
1895
+ }
1896
+ if (lines.length > 0) {
1897
+ context.writer.writeln(lines.join("\n"));
1898
+ }
1423
1899
  }
1424
1900
  parsePaths(command) {
1425
1901
  const raw = command.value || "";
1426
- return raw.split(/\s+/).filter(Boolean);
1902
+ return splitArgs(raw).filter(Boolean);
1427
1903
  }
1428
1904
  };
1429
1905
  var CliUniqCommandProcessor = class {
@@ -1466,6 +1942,24 @@ var CliUniqCommandProcessor = class {
1466
1942
  }
1467
1943
  ];
1468
1944
  }
1945
+ writeDescription(context) {
1946
+ const t = context.translator;
1947
+ writeWrapped(
1948
+ context,
1949
+ t.t("cli.uniq.help.collapse_repeated_lines_next", "Collapse repeated lines that are next to each other into one. Only adjacent duplicates are merged, so sort the input first to catch them all.")
1950
+ );
1951
+ context.writer.writeln();
1952
+ writeUsage(context, [
1953
+ ["uniq <file>", t.t("cli.uniq.help.drop_adjacent_duplicate_lines", "Drop adjacent duplicate lines")],
1954
+ ["<command> | uniq", t.t("cli.uniq.help.filter_piped_input", "Filter piped input")]
1955
+ ]);
1956
+ context.writer.writeln();
1957
+ writeExamples(context, [
1958
+ ["uniq log.txt", t.t("cli.uniq.help.collapse_repeated_lines", "Collapse repeated lines")],
1959
+ ["sort words.txt | uniq -c", t.t("cli.uniq.help.count_how_often_each", "Count how often each line appears")],
1960
+ ["sort words.txt | uniq -d", t.t("cli.uniq.help.show_only_duplicated_lines", "Show only the duplicated lines")]
1961
+ ]);
1962
+ }
1469
1963
  async processCommand(command, context) {
1470
1964
  const fs = context.services.getRequired(
1471
1965
  IFileSystemService_TOKEN
@@ -1478,7 +1972,7 @@ var CliUniqCommandProcessor = class {
1478
1972
  if (paths.length === 0) {
1479
1973
  if (command.data != null) {
1480
1974
  const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
1481
- let lines = content.split("\n");
1975
+ const lines = content.split("\n");
1482
1976
  if (lines.length > 0 && lines[lines.length - 1] === "") {
1483
1977
  lines.pop();
1484
1978
  }
@@ -1526,7 +2020,7 @@ var CliUniqCommandProcessor = class {
1526
2020
  );
1527
2021
  continue;
1528
2022
  }
1529
- let lines = content.split("\n");
2023
+ const lines = content.split("\n");
1530
2024
  if (lines.length > 0 && lines[lines.length - 1] === "") {
1531
2025
  lines.pop();
1532
2026
  }
@@ -1561,7 +2055,7 @@ var CliUniqCommandProcessor = class {
1561
2055
  }
1562
2056
  parsePaths(command) {
1563
2057
  const raw = command.value || "";
1564
- return raw.split(/\s+/).filter(Boolean);
2058
+ return splitArgs(raw).filter(Boolean);
1565
2059
  }
1566
2060
  };
1567
2061
  var CliCutCommandProcessor = class {
@@ -1597,26 +2091,45 @@ var CliCutCommandProcessor = class {
1597
2091
  }
1598
2092
  ];
1599
2093
  }
2094
+ writeDescription(context) {
2095
+ const t = context.translator;
2096
+ writeWrapped(
2097
+ context,
2098
+ t.t("cli.cut.help.print_selected_parts_each", "Print selected parts of each line: fields split by a delimiter (-f, tab by default) or character positions (-c). A list can be single numbers, ranges or both, such as 1,3 or 2-4 or 3-.")
2099
+ );
2100
+ context.writer.writeln();
2101
+ writeUsage(context, [
2102
+ ["cut -d <sep> -f <list> <file>", t.t("cli.cut.help.select_fields", "Select fields")],
2103
+ ["cut -c <list> <file>", t.t("cli.cut.help.select_character_positions", "Select character positions")],
2104
+ ["<command> | cut -f <list>", t.t("cli.cut.help.cut_piped_input", "Cut piped input")]
2105
+ ]);
2106
+ context.writer.writeln();
2107
+ writeExamples(context, [
2108
+ ["cut -d , -f 1,3 data.csv", t.t("cli.cut.help.first_third_csv_column", "First and third CSV column")],
2109
+ ["cut -c 1-10 notes.txt", t.t("cli.cut.help.first_10_characters_each", "First 10 characters of each line")],
2110
+ ["cat users.txt | cut -d : -f 1-2", t.t("cli.cut.help.first_two_fields_split", "First two fields split on :")]
2111
+ ]);
2112
+ }
1600
2113
  async processCommand(command, context) {
1601
2114
  const fs = context.services.getRequired(
1602
2115
  IFileSystemService_TOKEN
1603
2116
  );
1604
2117
  const delimiter = command.args["d"] || command.args["delimiter"] || " ";
1605
- const fieldsSpec = command.args["f"] || command.args["fields"];
1606
- const charsSpec = command.args["c"] || command.args["characters"];
1607
- if (!fieldsSpec && !charsSpec) {
2118
+ const fieldsSpec = command.args["f"] ?? command.args["fields"];
2119
+ const charsSpec = command.args["c"] ?? command.args["characters"];
2120
+ if ((fieldsSpec === void 0 || fieldsSpec === "") && (charsSpec === void 0 || charsSpec === "")) {
1608
2121
  context.writer.writeError(
1609
2122
  "cut: you must specify either -f (fields) or -c (characters)"
1610
2123
  );
1611
2124
  return;
1612
2125
  }
1613
- const spec = fieldsSpec || charsSpec;
2126
+ const spec = String(fieldsSpec ?? charsSpec);
1614
2127
  const indices = this.parseSpec(spec);
1615
2128
  const paths = this.parsePaths(command);
1616
2129
  if (paths.length === 0) {
1617
2130
  if (command.data != null) {
1618
2131
  const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
1619
- let lines = content.split("\n");
2132
+ const lines = content.split("\n");
1620
2133
  if (lines.length > 0 && lines[lines.length - 1] === "") {
1621
2134
  lines.pop();
1622
2135
  }
@@ -1658,7 +2171,7 @@ var CliCutCommandProcessor = class {
1658
2171
  );
1659
2172
  continue;
1660
2173
  }
1661
- let lines = content.split("\n");
2174
+ const lines = content.split("\n");
1662
2175
  if (lines.length > 0 && lines[lines.length - 1] === "") {
1663
2176
  lines.pop();
1664
2177
  }
@@ -1718,7 +2231,7 @@ var CliCutCommandProcessor = class {
1718
2231
  }
1719
2232
  parsePaths(command) {
1720
2233
  const raw = command.value || "";
1721
- return raw.split(/\s+/).filter(Boolean);
2234
+ return splitArgs(raw).filter(Boolean);
1722
2235
  }
1723
2236
  };
1724
2237
  var CliPasteCommandProcessor = class {
@@ -1747,23 +2260,47 @@ var CliPasteCommandProcessor = class {
1747
2260
  }
1748
2261
  ];
1749
2262
  }
2263
+ writeDescription(context) {
2264
+ const t = context.translator;
2265
+ writeWrapped(
2266
+ context,
2267
+ t.t("cli.paste.help.join_files_side_side", "Join files side by side: line 1 of each file goes on one output line, separated by a tab, then line 2, and so on. With -s, each file is joined onto a single line instead. Piped input is read as the file - (or as the only input when no file is named); -d takes a list of delimiters used in turn.")
2268
+ );
2269
+ context.writer.writeln();
2270
+ writeUsage(context, [
2271
+ ["paste <file1> <file2...>", t.t("cli.paste.help.merge_files_line_line", "Merge files line by line")],
2272
+ ["paste -s <file>", t.t("cli.paste.help.join_all_lines_file", "Join all lines of a file into one")],
2273
+ ["<command> | paste - <file>", t.t("cli.paste.help.merge_piped_input_file", "Merge piped input with a file")]
2274
+ ]);
2275
+ context.writer.writeln();
2276
+ writeExamples(context, [
2277
+ ["paste names.txt ages.txt", t.t("cli.paste.help.two_columns_tab_separated", "Two columns, tab separated")],
2278
+ ["paste -d , a.txt b.txt", t.t("cli.paste.help.merge_comma_separated", "Merge as comma-separated")],
2279
+ ["paste -s -d , list.txt", t.t("cli.paste.help.turn_list_one_csv", "Turn a list into one CSV line")]
2280
+ ]);
2281
+ }
1750
2282
  async processCommand(command, context) {
1751
2283
  const fs = context.services.getRequired(
1752
2284
  IFileSystemService_TOKEN
1753
2285
  );
1754
- const delimiter = command.args["d"] || command.args["delimiters"] || " ";
2286
+ const delimiterArg = command.args["d"] ?? command.args["delimiters"] ?? " ";
2287
+ const delimiters = this.parseDelimiters(String(delimiterArg));
1755
2288
  const serial = !!command.args["s"] || !!command.args["serial"];
1756
2289
  const paths = this.parsePaths(command);
2290
+ const hasStdin = command.data != null;
1757
2291
  if (paths.length === 0) {
1758
- context.writer.writeError("paste: missing file operand");
1759
- return;
1760
- }
1761
- if (!serial && paths.length < 2) {
1762
- context.writer.writeError("paste: missing file operand");
1763
- return;
2292
+ if (!hasStdin) {
2293
+ context.writer.writeError("paste: missing file operand");
2294
+ return;
2295
+ }
2296
+ paths.push("-");
1764
2297
  }
1765
2298
  const fileLines = [];
1766
2299
  for (const path of paths) {
2300
+ if (path === "-" && hasStdin) {
2301
+ fileLines.push(splitLines(pipedText(command.data)));
2302
+ continue;
2303
+ }
1767
2304
  try {
1768
2305
  if (fs.isDirectory(path)) {
1769
2306
  context.writer.writeError(
@@ -1778,20 +2315,18 @@ var CliPasteCommandProcessor = class {
1778
2315
  );
1779
2316
  return;
1780
2317
  }
1781
- let lines = content.split("\n");
1782
- if (lines.length > 0 && lines[lines.length - 1] === "") {
1783
- lines.pop();
1784
- }
1785
- fileLines.push(lines);
2318
+ fileLines.push(splitLines(content));
1786
2319
  } catch (e) {
1787
2320
  context.writer.writeError(`paste: ${e.message}`);
1788
2321
  return;
1789
2322
  }
1790
2323
  }
2324
+ const join = (parts) => parts.reduce(
2325
+ (line, part, i) => i === 0 ? part : line + delimiters[(i - 1) % delimiters.length] + part,
2326
+ ""
2327
+ );
1791
2328
  if (serial) {
1792
- const output = fileLines.map(
1793
- (lines) => lines.join(delimiter)
1794
- );
2329
+ const output = fileLines.map((lines) => join(lines));
1795
2330
  context.writer.writeln(output.join("\n"));
1796
2331
  } else {
1797
2332
  const maxLines = Math.max(
@@ -1799,21 +2334,59 @@ var CliPasteCommandProcessor = class {
1799
2334
  );
1800
2335
  const output = [];
1801
2336
  for (let i = 0; i < maxLines; i++) {
1802
- const parts = fileLines.map((lines) => lines[i] || "");
1803
- output.push(parts.join(delimiter));
2337
+ output.push(join(fileLines.map((lines) => lines[i] ?? "")));
1804
2338
  }
1805
2339
  context.writer.writeln(output.join("\n"));
1806
2340
  }
1807
2341
  }
2342
+ /** `-d` list: each character in turn, with \t, \n, \\ and \0 (empty) escapes. */
2343
+ parseDelimiters(list) {
2344
+ const result = [];
2345
+ for (let i = 0; i < list.length; i++) {
2346
+ if (list[i] === "\\" && i + 1 < list.length) {
2347
+ const next = list[++i];
2348
+ result.push(
2349
+ next === "t" ? " " : next === "n" ? "\n" : next === "0" ? "" : next
2350
+ );
2351
+ } else {
2352
+ result.push(list[i]);
2353
+ }
2354
+ }
2355
+ return result.length > 0 ? result : [" "];
2356
+ }
2357
+ /**
2358
+ * The file operands. The engine drops a lone `-` from the value, so with
2359
+ * piped input they are read from the raw command line to keep it.
2360
+ */
1808
2361
  parsePaths(command) {
1809
- const raw = command.value || "";
1810
- return raw.split(/\s+/).filter(Boolean);
2362
+ const fromValue = splitArgs(command.value || "").filter(Boolean);
2363
+ if (command.data == null || !command.rawCommand) return fromValue;
2364
+ const words = splitArgs(command.rawCommand).slice(1);
2365
+ if (!words.includes("-")) return fromValue;
2366
+ const result = [];
2367
+ for (let i = 0; i < words.length; i++) {
2368
+ const word = words[i];
2369
+ if (word === "-" || !word.startsWith("-")) {
2370
+ result.push(word);
2371
+ } else if (/^(-d|--delimiters)$/.test(word)) {
2372
+ i++;
2373
+ }
2374
+ }
2375
+ return result;
1811
2376
  }
1812
2377
  };
1813
2378
  function expandSet(set) {
1814
2379
  const chars = [];
1815
2380
  let i = 0;
1816
2381
  while (i < set.length) {
2382
+ if (set[i] === "\\" && i + 1 < set.length) {
2383
+ const next = set[i + 1];
2384
+ chars.push(
2385
+ next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next
2386
+ );
2387
+ i += 2;
2388
+ continue;
2389
+ }
1817
2390
  if (set[i] === "[" && set[i + 1] === ":") {
1818
2391
  const end = set.indexOf(":]", i + 2);
1819
2392
  if (end !== -1) {
@@ -1823,7 +2396,7 @@ function expandSet(set) {
1823
2396
  continue;
1824
2397
  }
1825
2398
  }
1826
- if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !== void 0) {
2399
+ if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !== "\\") {
1827
2400
  const start = set.charCodeAt(i);
1828
2401
  const finish = set.charCodeAt(i + 2);
1829
2402
  if (start <= finish) {
@@ -1893,7 +2466,7 @@ function parseTokens(raw) {
1893
2466
  var CliTrCommandProcessor = class {
1894
2467
  constructor() {
1895
2468
  this.command = "tr";
1896
- this.description = "Translate or delete characters in a file";
2469
+ this.description = "Translate or delete characters in piped input";
1897
2470
  this.author = DefaultLibraryAuthor;
1898
2471
  this.version = LIBRARY_VERSION;
1899
2472
  this.acceptsRawInput = true;
@@ -1901,103 +2474,76 @@ var CliTrCommandProcessor = class {
1901
2474
  this.metadata = { icon: "\u{1F500}", module: "file management" };
1902
2475
  this.parameters = [
1903
2476
  {
1904
- name: "d",
2477
+ name: "delete",
2478
+ aliases: ["d"],
1905
2479
  description: "Delete characters in SET1",
1906
2480
  required: false,
1907
2481
  type: "boolean"
1908
2482
  },
1909
2483
  {
1910
- name: "s",
2484
+ name: "squeeze-repeats",
2485
+ aliases: ["s"],
1911
2486
  description: "Squeeze repeated characters in last operand set",
1912
2487
  required: false,
1913
2488
  type: "boolean"
1914
2489
  }
1915
2490
  ];
1916
2491
  }
1917
- async processCommand(command, context) {
1918
- const fs = context.services.getRequired(
1919
- IFileSystemService_TOKEN
2492
+ writeDescription(context) {
2493
+ const t = context.translator;
2494
+ writeWrapped(
2495
+ context,
2496
+ t.t("cli.tr.help.translate_characters_set1_matching", "Translate characters from SET1 into the matching characters of SET2, delete them (-d) or squeeze repeats into one (-s). Sets can use ranges like a-z and classes like [:upper:], [:lower:], [:digit:], [:alpha:], [:alnum:] and [:space:]. Escapes \\n, \\t and \\\\ are understood. Like Unix tr, it reads only piped input and takes no file operands: to transform a file, pipe it in with cat.")
1920
2497
  );
2498
+ context.writer.writeln();
2499
+ writeUsage(context, [
2500
+ ["<command> | tr <set1> <set2>", t.t("cli.tr.help.replace_characters", "Replace characters")],
2501
+ ["<command> | tr -d <set1>", t.t("cli.tr.help.delete_characters", "Delete characters")],
2502
+ ["<command> | tr -s <set1> [set2]", t.t("cli.tr.help.squeeze_repeated_characters", "Squeeze repeated characters")]
2503
+ ]);
2504
+ context.writer.writeln();
2505
+ writeExamples(context, [
2506
+ ["cat notes.txt | tr a-z A-Z", t.t("cli.tr.help.convert_file_upper_case", "Convert a file to upper case")],
2507
+ ["echo hello | tr a-z A-Z", t.t("cli.tr.help.upper_case_piped_text", "Upper-case piped text")],
2508
+ ["cat notes.txt | tr -d 0-9", t.t("cli.tr.help.remove_all_digits", "Remove all digits")],
2509
+ ["cat notes.txt | tr -s ' '", t.t("cli.tr.help.collapse_runs_spaces", "Collapse runs of spaces")]
2510
+ ]);
2511
+ }
2512
+ async processCommand(command, context) {
1921
2513
  const raw = command.value || "";
1922
2514
  const positional = parseTokens(raw);
1923
- const deleteMode = !!command.args["d"];
1924
- const squeezeMode = !!command.args["s"];
2515
+ const deleteMode = !!command.args["d"] || !!command.args["delete"];
2516
+ const squeezeMode = !!command.args["s"] || !!command.args["squeeze-repeats"];
1925
2517
  if (positional.length === 0) {
1926
- context.writer.writeError("tr: missing operand");
2518
+ this.fail(context, "tr: missing operand");
1927
2519
  return;
1928
2520
  }
1929
- let set1Str;
1930
- let set2Str;
1931
- let filePath;
1932
- if (deleteMode && squeezeMode) {
1933
- if (positional.length < 2) {
1934
- context.writer.writeError("tr: missing operand");
1935
- return;
1936
- }
1937
- set1Str = positional[0];
1938
- set2Str = positional[1];
1939
- if (positional.length >= 3) {
1940
- filePath = positional[2];
1941
- }
1942
- } else if (deleteMode) {
1943
- if (positional.length < 1) {
1944
- context.writer.writeError("tr: missing operand");
1945
- return;
1946
- }
1947
- set1Str = positional[0];
1948
- if (positional.length >= 2) {
1949
- filePath = positional[1];
1950
- }
2521
+ let setCount;
2522
+ if (deleteMode) {
2523
+ setCount = squeezeMode ? 2 : 1;
1951
2524
  } else if (squeezeMode) {
1952
- if (positional.length < 1) {
1953
- context.writer.writeError("tr: missing operand");
1954
- return;
1955
- }
1956
- if (positional.length >= 3) {
1957
- set1Str = positional[0];
1958
- set2Str = positional[1];
1959
- filePath = positional[2];
1960
- } else if (positional.length === 2) {
1961
- set1Str = positional[0];
1962
- filePath = positional[1];
1963
- } else {
1964
- set1Str = positional[0];
1965
- }
2525
+ setCount = Math.min(positional.length, 2);
1966
2526
  } else {
1967
- if (positional.length < 2) {
1968
- context.writer.writeError("tr: missing operand");
1969
- return;
1970
- }
1971
- set1Str = positional[0];
1972
- set2Str = positional[1];
1973
- if (positional.length >= 3) {
1974
- filePath = positional[2];
1975
- }
2527
+ setCount = 2;
1976
2528
  }
1977
- let content;
1978
- if (filePath) {
1979
- try {
1980
- if (fs.isDirectory(filePath)) {
1981
- context.writer.writeError(`tr: ${filePath}: Is a directory`);
1982
- return;
1983
- }
1984
- content = fs.readFile(filePath);
1985
- if (content === null) {
1986
- context.writer.writeError(
1987
- `tr: ${filePath}: No such file or directory`
1988
- );
1989
- return;
1990
- }
1991
- } catch (e) {
1992
- context.writer.writeError(`tr: ${e.message}`);
1993
- return;
1994
- }
1995
- } else if (command.data != null) {
1996
- content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
1997
- } else {
1998
- context.writer.writeError("tr: missing file operand");
2529
+ if (positional.length < setCount) {
2530
+ this.fail(context, "tr: missing operand");
2531
+ return;
2532
+ }
2533
+ if (positional.length > setCount) {
2534
+ this.fail(context, `tr: extra operand '${positional[setCount]}'`);
1999
2535
  return;
2000
2536
  }
2537
+ if (command.data == null) {
2538
+ this.fail(
2539
+ context,
2540
+ "tr: no input: tr reads only piped input and takes no file operands; use 'cat file | tr a-z A-Z'"
2541
+ );
2542
+ return;
2543
+ }
2544
+ const set1Str = positional[0];
2545
+ const set2Str = setCount === 2 ? positional[1] : void 0;
2546
+ const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
2001
2547
  try {
2002
2548
  const set1 = expandSet(set1Str);
2003
2549
  const set1Set = new Set(set1);
@@ -2020,9 +2566,13 @@ var CliTrCommandProcessor = class {
2020
2566
  }
2021
2567
  context.writer.writeln(result);
2022
2568
  } catch (e) {
2023
- context.writer.writeError(`tr: ${e.message}`);
2569
+ this.fail(context, `tr: ${e.message}`);
2024
2570
  }
2025
2571
  }
2572
+ fail(context, message) {
2573
+ context.writer.writeError(message);
2574
+ context.process.exit(1, { silent: true });
2575
+ }
2026
2576
  deleteChars(content, deleteSet) {
2027
2577
  let result = "";
2028
2578
  for (const ch of content) {
@@ -2069,6 +2619,22 @@ var CliStatCommandProcessor = class {
2069
2619
  this.metadata = { icon: "\u{1F4CA}", module: "file management" };
2070
2620
  this.parameters = [];
2071
2621
  }
2622
+ writeDescription(context) {
2623
+ const t = context.translator;
2624
+ writeWrapped(
2625
+ context,
2626
+ t.t("cli.stat.help.show_details_about_files", "Show details about files or directories: full path, size, type, permissions, owner, group, and when it was created and last modified.")
2627
+ );
2628
+ context.writer.writeln();
2629
+ writeUsage(context, [
2630
+ ["stat <path...>", t.t("cli.stat.help.show_details_each_path", "Show details for each path")]
2631
+ ]);
2632
+ context.writer.writeln();
2633
+ writeExamples(context, [
2634
+ ["stat notes.txt", t.t("cli.stat.help.details_one_file", "Details of one file")],
2635
+ ["stat ~/docs notes.txt", t.t("cli.stat.help.several_paths_once", "Several paths at once")]
2636
+ ]);
2637
+ }
2072
2638
  async processCommand(command, context) {
2073
2639
  const fs = context.services.getRequired(
2074
2640
  IFileSystemService_TOKEN
@@ -2105,7 +2671,7 @@ var CliStatCommandProcessor = class {
2105
2671
  }
2106
2672
  parsePaths(command) {
2107
2673
  const raw = command.value || "";
2108
- return raw.split(/\s+/).filter(Boolean);
2674
+ return splitArgs(raw).filter(Boolean);
2109
2675
  }
2110
2676
  };
2111
2677
  var CliChmodCommandProcessor = class {
@@ -2126,6 +2692,28 @@ var CliChmodCommandProcessor = class {
2126
2692
  }
2127
2693
  ];
2128
2694
  }
2695
+ writeDescription(context) {
2696
+ const t = context.translator;
2697
+ writeWrapped(
2698
+ context,
2699
+ t.t("cli.chmod.help.change_who_can_read", "Change who can read (r), write (w) and execute (x) a file. Use an octal mode such as 755 or a symbolic one: who (u user, g group, o others, a all), then + to add, - to remove or = to set, then the permissions.")
2700
+ );
2701
+ context.writer.writeln();
2702
+ writeUsage(context, [
2703
+ ["chmod <mode> <file...>", t.t("cli.chmod.help.set_permissions", "Set permissions")],
2704
+ [
2705
+ "chmod -R <mode> <dir>",
2706
+ t.t("cli.chmod.help.apply_directory_everything", "Apply to a directory and everything in it")
2707
+ ]
2708
+ ]);
2709
+ context.writer.writeln();
2710
+ writeExamples(context, [
2711
+ ["chmod 644 notes.txt", t.t("cli.chmod.help.rw_r_r_owner", "rw-r--r--: owner writes, others read")],
2712
+ ["chmod u+x script.sh", t.t("cli.chmod.help.let_owner_execute", "Let the owner execute it")],
2713
+ ["chmod go-w shared.txt", t.t("cli.chmod.help.take_away_write_group", "Take away write for group and others")],
2714
+ ["chmod -R 755 site", t.t("cli.chmod.help.set_whole_directory_tree", "Set a whole directory tree")]
2715
+ ]);
2716
+ }
2129
2717
  async processCommand(command, context) {
2130
2718
  const fs = context.services.getRequired(
2131
2719
  IFileSystemService_TOKEN
@@ -2159,7 +2747,7 @@ var CliChmodCommandProcessor = class {
2159
2747
  }
2160
2748
  parseArgs(command) {
2161
2749
  const raw = command.value || "";
2162
- const tokens = raw.split(/\s+/).filter(Boolean);
2750
+ const tokens = splitArgs(raw).filter(Boolean);
2163
2751
  const recursive = command.args["R"] || false;
2164
2752
  const mode = tokens.length > 0 ? tokens[0] : null;
2165
2753
  const paths = tokens.slice(1);
@@ -2246,6 +2834,31 @@ var CliChownCommandProcessor = class {
2246
2834
  }
2247
2835
  ];
2248
2836
  }
2837
+ writeDescription(context) {
2838
+ const t = context.translator;
2839
+ writeWrapped(
2840
+ context,
2841
+ t.t("cli.chown.help.change_owner_group_files", "Change the owner and/or group of files. Give user, user:group, user: (owner only) or :group (group only).")
2842
+ );
2843
+ context.writer.writeln();
2844
+ writeUsage(context, [
2845
+ [
2846
+ "chown <user>[:<group>] <file...>",
2847
+ t.t("cli.chown.help.change_owner_group_if", "Change owner, and group if given")
2848
+ ],
2849
+ ["chown :<group> <file>", t.t("cli.chown.help.change_only_group", "Change only the group")],
2850
+ [
2851
+ "chown -R <user> <dir>",
2852
+ t.t("cli.chown.help.apply_directory_everything", "Apply to a directory and everything in it")
2853
+ ]
2854
+ ]);
2855
+ context.writer.writeln();
2856
+ writeExamples(context, [
2857
+ ["chown alice notes.txt", t.t("cli.chown.help.make_alice_owner", "Make alice the owner")],
2858
+ ["chown alice:staff report.txt", t.t("cli.chown.help.set_owner_group", "Set owner and group")],
2859
+ ["chown -R bob ~/shared", t.t("cli.chown.help.give_bob_whole_directory", "Give bob a whole directory")]
2860
+ ]);
2861
+ }
2249
2862
  async processCommand(command, context) {
2250
2863
  const fs = context.services.getRequired(
2251
2864
  IFileSystemService_TOKEN
@@ -2279,7 +2892,7 @@ var CliChownCommandProcessor = class {
2279
2892
  }
2280
2893
  parseArgs(command) {
2281
2894
  const raw = command.value || "";
2282
- const tokens = raw.split(/\s+/).filter(Boolean);
2895
+ const tokens = splitArgs(raw).filter(Boolean);
2283
2896
  const recursive = command.args["R"] || false;
2284
2897
  const ownerSpec = tokens.length > 0 ? tokens[0] : null;
2285
2898
  const paths = tokens.slice(1);
@@ -2317,7 +2930,6 @@ var CliDuCommandProcessor = class {
2317
2930
  this.author = DefaultLibraryAuthor;
2318
2931
  this.version = LIBRARY_VERSION;
2319
2932
  this.acceptsRawInput = true;
2320
- this.valueRequired = true;
2321
2933
  this.metadata = { icon: "\u{1F4CF}", module: "file management" };
2322
2934
  this.parameters = [
2323
2935
  {
@@ -2341,14 +2953,32 @@ var CliDuCommandProcessor = class {
2341
2953
  }
2342
2954
  ];
2343
2955
  }
2956
+ writeDescription(context) {
2957
+ const t = context.translator;
2958
+ writeWrapped(
2959
+ context,
2960
+ t.t("cli.du.help.show_how_much_space", "Show how much space files and directories use, in bytes. For a directory, every subdirectory is listed with its total size; use -s for just the total or -d to limit how deep it goes.")
2961
+ );
2962
+ context.writer.writeln();
2963
+ writeUsage(context, [
2964
+ ["du [path...]", t.t("cli.du.help.sizes_path_default_current", "Sizes of a path (default: the current directory) and its subdirectories")],
2965
+ ["du -s <path>", t.t("cli.du.help.only_total", "Only the total")],
2966
+ ["du -d <n> <path>", t.t("cli.du.help.only_levels_deep", "Only <n> levels deep")]
2967
+ ]);
2968
+ context.writer.writeln();
2969
+ writeExamples(context, [
2970
+ ["du ~/docs", t.t("cli.du.help.size_each_directory_docs", "Size of each directory in ~/docs")],
2971
+ ["du -h -s ~", t.t("cli.du.help.total_home_k_m", "Total of your home in K/M/G")],
2972
+ ["du -h -d 1 .", t.t("cli.du.help.size_each_top_level", "Size of each top-level folder")]
2973
+ ]);
2974
+ }
2344
2975
  async processCommand(command, context) {
2345
2976
  const fs = context.services.getRequired(
2346
2977
  IFileSystemService_TOKEN
2347
2978
  );
2348
2979
  const { humanReadable, summaryOnly, maxDepth, paths } = this.parseArgs(command);
2349
2980
  if (paths.length === 0) {
2350
- context.writer.writeError("du: missing operand");
2351
- return;
2981
+ paths.push(fs.getCurrentDirectory());
2352
2982
  }
2353
2983
  for (const path of paths) {
2354
2984
  const resolved = fs.resolvePath(path);
@@ -2381,10 +3011,12 @@ var CliDuCommandProcessor = class {
2381
3011
  }
2382
3012
  parseArgs(command) {
2383
3013
  const raw = command.value || "";
2384
- const paths = raw.split(/\s+/).filter(Boolean);
3014
+ const paths = splitArgs(raw).filter(Boolean);
2385
3015
  const humanReadable = !!command.args["h"];
2386
3016
  const summaryOnly = !!command.args["s"];
2387
- const maxDepth = command.args["d"] || command.args["max-depth"] ? parseInt(command.args["d"] || command.args["max-depth"] || "0", 10) : Infinity;
3017
+ const depthArg = command.args["max-depth"] ?? command.args["d"];
3018
+ const parsedDepth = parseInt(String(depthArg ?? ""), 10);
3019
+ const maxDepth = depthArg != null && depthArg !== true && !isNaN(parsedDepth) ? parsedDepth : Infinity;
2388
3020
  return { humanReadable, summaryOnly, maxDepth, paths };
2389
3021
  }
2390
3022
  collectSizes(node, path, depth, maxDepth, summaryOnly, entries) {
@@ -2441,9 +3073,32 @@ var CliLnCommandProcessor = class {
2441
3073
  description: "Create a symbolic link",
2442
3074
  required: false,
2443
3075
  type: "boolean"
3076
+ },
3077
+ {
3078
+ name: "force",
3079
+ aliases: ["f"],
3080
+ description: "Replace an existing file at the link path",
3081
+ required: false,
3082
+ type: "boolean"
2444
3083
  }
2445
3084
  ];
2446
3085
  }
3086
+ writeDescription(context) {
3087
+ const t = context.translator;
3088
+ writeWrapped(
3089
+ context,
3090
+ t.t("cli.ln.help.create_symbolic_link_new", "Create a symbolic link: a new path that points to another file or directory. Only symbolic links (-s) are supported, and the target does not need to exist yet.")
3091
+ );
3092
+ context.writer.writeln();
3093
+ writeUsage(context, [
3094
+ ["ln -s <target> <link>", t.t("cli.ln.help.create_pointing", "Create <link> pointing at <target>")]
3095
+ ]);
3096
+ context.writer.writeln();
3097
+ writeExamples(context, [
3098
+ ["ln -s ~/docs/notes.txt notes", t.t("cli.ln.help.shortcut_file", "Shortcut to a file")],
3099
+ ["ln -s /var/log logs", t.t("cli.ln.help.shortcut_directory", "Shortcut to a directory")]
3100
+ ]);
3101
+ }
2447
3102
  async processCommand(command, context) {
2448
3103
  const fs = context.services.getRequired(
2449
3104
  IFileSystemService_TOKEN
@@ -2461,6 +3116,10 @@ var CliLnCommandProcessor = class {
2461
3116
  }
2462
3117
  const resolvedLink = fs.resolvePath(linkName);
2463
3118
  const resolvedTarget = fs.resolvePath(target);
3119
+ const force = !!(command.args["force"] || command.args["f"]);
3120
+ if (force && fs.exists(resolvedLink) && !fs.isDirectory(resolvedLink)) {
3121
+ fs.remove(resolvedLink);
3122
+ }
2464
3123
  if (fs.exists(resolvedLink)) {
2465
3124
  context.writer.writeError(
2466
3125
  `ln: failed to create symbolic link '${linkName}': File exists`
@@ -2477,7 +3136,7 @@ var CliLnCommandProcessor = class {
2477
3136
  }
2478
3137
  parseArgs(command) {
2479
3138
  const raw = command.value || "";
2480
- const tokens = raw.split(/\s+/).filter(Boolean);
3139
+ const tokens = splitArgs(raw).filter(Boolean);
2481
3140
  const symbolic = command.args["s"] || false;
2482
3141
  return {
2483
3142
  symbolic,
@@ -2512,25 +3171,75 @@ var CliSedCommandProcessor = class {
2512
3171
  {
2513
3172
  name: "expression",
2514
3173
  aliases: ["e"],
2515
- description: "Add the expression to the commands to be executed",
3174
+ description: "Add the expression to the commands to be executed (repeatable)",
2516
3175
  required: false,
2517
- type: "string"
3176
+ type: "array"
3177
+ },
3178
+ {
3179
+ name: "regexp-extended",
3180
+ aliases: ["E", "r"],
3181
+ description: "Use extended regular expressions: (, ), +, ?, | and { } without backslashes",
3182
+ required: false,
3183
+ type: "boolean"
2518
3184
  }
2519
3185
  ];
2520
3186
  }
3187
+ writeDescription(context) {
3188
+ const t = context.translator;
3189
+ writeWrapped(
3190
+ context,
3191
+ t.t("cli.sed.help.edit_text_line_line", "Edit text line by line with a script: substitute (s/old/new/), delete (d), print (p) or quit (q), optionally limited to a line number, a range (1,5, 3,$ or /start/,/end/) or lines matching /regex/; ! after an address inverts it and ; separates commands. The replacement can use & (the match) and \\1 to \\9 (groups, written \\(...\\) or, with -E, (...)). Reads files or piped input and prints the result, or rewrites the files with -i.")
3192
+ );
3193
+ context.writer.writeln();
3194
+ writeUsage(context, [
3195
+ ["sed '<script>' <file>", t.t("cli.sed.help.print_edited_file", "Print the edited file")],
3196
+ ["sed -i '<script>' <file>", t.t("cli.sed.help.save_edits_file", "Save the edits into the file")],
3197
+ ["sed -n '<script>p' <file>", t.t("cli.sed.help.print_only_selected_lines", "Print only selected lines")],
3198
+ ["<command> | sed '<script>'", t.t("cli.sed.help.edit_piped_input", "Edit piped input")]
3199
+ ]);
3200
+ context.writer.writeln();
3201
+ writeExamples(context, [
3202
+ ["sed 's/cat/dog/g' pets.txt", t.t("cli.sed.help.replace_every_cat_dog", "Replace every cat with dog")],
3203
+ ["sed -i 's/v1/v2/' config.txt", t.t("cli.sed.help.edit_file_place", "Edit the file in place")],
3204
+ ["sed '/^#/d' config.txt", t.t("cli.sed.help.drop_comment_lines", "Drop comment lines")],
3205
+ ["sed -n '1,5p' notes.txt", t.t("cli.sed.help.print_lines_1_5", "Print lines 1 to 5")],
3206
+ ["sed -E 's/(\\w+) (\\w+)/\\2 \\1/' names.txt", t.t("cli.sed.help.swap_two_words", "Swap two words")]
3207
+ ]);
3208
+ }
2521
3209
  async processCommand(command, context) {
2522
3210
  const fs = context.services.getRequired(
2523
3211
  IFileSystemService_TOKEN
2524
3212
  );
2525
- const { inPlace, suppress, expressions, filePath } = this.parseRawCommand(command);
3213
+ const { inPlace, suppress, extended, expressions, filePaths } = this.parseRawCommand(command);
2526
3214
  if (expressions.length === 0) {
2527
3215
  context.writer.writeError(
2528
3216
  "sed: missing expression. Usage: sed [options] 'expression' <file>"
2529
3217
  );
2530
3218
  return;
2531
3219
  }
2532
- let content;
2533
- if (filePath) {
3220
+ const parse = () => {
3221
+ try {
3222
+ return expressions.flatMap((e) => this.parseScript(e, extended));
3223
+ } catch (e) {
3224
+ context.writer.writeError(`sed: ${e.message}`);
3225
+ return null;
3226
+ }
3227
+ };
3228
+ if (filePaths.length === 0) {
3229
+ if (command.data == null) {
3230
+ context.writer.writeError(
3231
+ "sed: missing file operand. Usage: sed [options] 'expression' <file>"
3232
+ );
3233
+ return;
3234
+ }
3235
+ const script2 = parse();
3236
+ if (!script2) return;
3237
+ const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
3238
+ this.writeLines(context, this.run(script2, splitLines(content), suppress));
3239
+ return;
3240
+ }
3241
+ const contents = [];
3242
+ for (const filePath of filePaths) {
2534
3243
  const resolved = fs.resolvePath(filePath);
2535
3244
  if (!fs.exists(resolved)) {
2536
3245
  context.writer.writeError(
@@ -2542,116 +3251,125 @@ var CliSedCommandProcessor = class {
2542
3251
  context.writer.writeError(`sed: ${filePath}: Is a directory`);
2543
3252
  return;
2544
3253
  }
2545
- content = fs.readFile(resolved) ?? "";
2546
- } else if (command.data != null) {
2547
- content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
2548
- } else {
2549
- context.writer.writeError(
2550
- "sed: missing file operand. Usage: sed [options] 'expression' <file>"
2551
- );
2552
- return;
2553
- }
2554
- const parsedExpressions = [];
2555
- for (const expr of expressions) {
2556
3254
  try {
2557
- const parsed = this.parseExpression(expr);
2558
- parsedExpressions.push(parsed);
3255
+ contents.push(fs.readFile(resolved) ?? "");
2559
3256
  } catch (e) {
2560
3257
  context.writer.writeError(`sed: ${e.message}`);
2561
3258
  return;
2562
3259
  }
2563
3260
  }
2564
- const lines = content.split("\n");
2565
- const outputLines = [];
3261
+ if (inPlace) {
3262
+ for (let i = 0; i < filePaths.length; i++) {
3263
+ const script2 = parse();
3264
+ if (!script2) return;
3265
+ const output = this.run(script2, splitLines(contents[i]), suppress);
3266
+ const keepNewline = contents[i].endsWith("\n") && output.length > 0;
3267
+ try {
3268
+ fs.writeFile(
3269
+ fs.resolvePath(filePaths[i]),
3270
+ output.join("\n") + (keepNewline ? "\n" : "")
3271
+ );
3272
+ } catch (e) {
3273
+ context.writer.writeError(`sed: ${e.message}`);
3274
+ return;
3275
+ }
3276
+ }
3277
+ await fs.persist();
3278
+ return;
3279
+ }
3280
+ const script = parse();
3281
+ if (!script) return;
3282
+ const lines = contents.flatMap((c) => splitLines(c));
3283
+ this.writeLines(context, this.run(script, lines, suppress));
3284
+ }
3285
+ writeLines(context, lines) {
3286
+ for (const line of lines) {
3287
+ context.writer.writeln(line);
3288
+ }
3289
+ }
3290
+ /** Runs the script over the lines and returns the output lines. */
3291
+ run(script, lines, suppress) {
3292
+ const output = [];
2566
3293
  for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
2567
3294
  let line = lines[lineIdx];
2568
3295
  let deleted = false;
2569
- let printed = false;
2570
- for (const expr of parsedExpressions) {
2571
- if (deleted) break;
2572
- if (!this.addressMatches(
2573
- expr.address,
2574
- lineIdx + 1,
2575
- lines.length,
2576
- line
2577
- )) {
3296
+ let quit = false;
3297
+ for (const expr of script) {
3298
+ if (!this.addressMatches(expr, lineIdx + 1, lines.length, line)) {
2578
3299
  continue;
2579
3300
  }
2580
- switch (expr.command) {
2581
- case "s": {
2582
- const pattern = expr.pattern;
2583
- pattern.lastIndex = 0;
2584
- const newLine = line.replace(pattern, (...args) => {
2585
- const match = args[0];
2586
- return expr.replacement.replace(/&/g, match);
2587
- });
2588
- const matched = newLine !== line;
2589
- line = newLine;
2590
- if (matched && expr.printFlag) {
2591
- printed = true;
3301
+ if (expr.command === "s") {
3302
+ let replaced = false;
3303
+ let seen = 0;
3304
+ const pattern = expr.pattern;
3305
+ pattern.lastIndex = 0;
3306
+ line = line.replace(pattern, (...args) => {
3307
+ seen++;
3308
+ const nth = expr.occurrence;
3309
+ if (nth && (expr.globalFlag ? seen < nth : seen !== nth)) {
3310
+ return args[0];
2592
3311
  }
2593
- break;
2594
- }
2595
- case "d":
2596
- deleted = true;
2597
- break;
2598
- case "p":
2599
- printed = true;
2600
- break;
3312
+ replaced = true;
3313
+ const groups = args.slice(0, expr.groupCount + 1);
3314
+ return this.expandReplacement(expr.replacement, groups);
3315
+ });
3316
+ if (replaced && expr.printFlag) output.push(line);
3317
+ } else if (expr.command === "p") {
3318
+ output.push(line);
3319
+ } else if (expr.command === "d") {
3320
+ deleted = true;
3321
+ break;
3322
+ } else if (expr.command === "q") {
3323
+ quit = true;
3324
+ break;
2601
3325
  }
2602
3326
  }
2603
- if (!deleted) {
2604
- if (suppress) {
2605
- if (printed) {
2606
- outputLines.push(line);
2607
- }
3327
+ if (!deleted && !suppress) output.push(line);
3328
+ if (quit) break;
3329
+ }
3330
+ return output;
3331
+ }
3332
+ /** `&` is the match, `\1`..`\9` the groups, `\n` a newline, `\x` a literal x. */
3333
+ expandReplacement(replacement, groups) {
3334
+ let out = "";
3335
+ for (let i = 0; i < replacement.length; i++) {
3336
+ const ch = replacement[i];
3337
+ if (ch === "\\" && i + 1 < replacement.length) {
3338
+ const next = replacement[++i];
3339
+ if (next >= "0" && next <= "9") {
3340
+ out += groups[Number(next)] ?? "";
3341
+ } else if (next === "n") {
3342
+ out += "\n";
3343
+ } else if (next === "t") {
3344
+ out += " ";
2608
3345
  } else {
2609
- outputLines.push(line);
3346
+ out += next;
2610
3347
  }
3348
+ } else if (ch === "&") {
3349
+ out += groups[0];
3350
+ } else {
3351
+ out += ch;
2611
3352
  }
2612
3353
  }
2613
- const result = outputLines.join("\n");
2614
- if (inPlace && filePath) {
2615
- const resolved = fs.resolvePath(filePath);
2616
- fs.writeFile(resolved, result);
2617
- await fs.persist();
2618
- } else {
2619
- const resultLines = result.split("\n");
2620
- for (const line of resultLines) {
2621
- context.writer.writeln(line);
2622
- }
2623
- }
3354
+ return out;
2624
3355
  }
2625
3356
  parseRawCommand(command) {
2626
3357
  const raw = command.value || "";
2627
3358
  const tokens = this.tokenize(raw);
2628
3359
  const inPlace = !!command.args["in-place"] || !!command.args["i"];
2629
3360
  const suppress = !!command.args["quiet"] || !!command.args["n"];
3361
+ const extended = !!command.args["regexp-extended"] || !!command.args["E"] || !!command.args["r"];
2630
3362
  const expressions = [];
2631
- let filePath = null;
2632
- const nonFlagTokens = [];
2633
- if (command.args["expression"] || command.args["e"]) {
2634
- const eValue = command.args["expression"] || command.args["e"];
2635
- if (Array.isArray(eValue)) {
2636
- for (const v of eValue) {
2637
- if (v && typeof v === "string") {
2638
- expressions.push(v);
2639
- }
2640
- }
2641
- } else if (eValue && typeof eValue === "string") {
2642
- expressions.push(eValue);
3363
+ const eValue = command.args["expression"] ?? command.args["e"];
3364
+ for (const v of Array.isArray(eValue) ? eValue : [eValue]) {
3365
+ if (v != null && v !== true && v !== "") {
3366
+ expressions.push(String(v));
2643
3367
  }
2644
3368
  }
2645
- for (const t of tokens) {
2646
- nonFlagTokens.push(t);
2647
- }
2648
- if (expressions.length === 0 && nonFlagTokens.length > 0) {
2649
- expressions.push(nonFlagTokens.shift());
2650
- }
2651
- if (nonFlagTokens.length > 0) {
2652
- filePath = nonFlagTokens[nonFlagTokens.length - 1];
3369
+ if (expressions.length === 0 && tokens.length > 0) {
3370
+ expressions.push(tokens.shift());
2653
3371
  }
2654
- return { inPlace, suppress, expressions, filePath };
3372
+ return { inPlace, suppress, extended, expressions, filePaths: tokens };
2655
3373
  }
2656
3374
  tokenize(raw) {
2657
3375
  const tokens = [];
@@ -2661,73 +3379,85 @@ var CliSedCommandProcessor = class {
2661
3379
  i++;
2662
3380
  continue;
2663
3381
  }
2664
- if (raw[i] === "'" || raw[i] === '"') {
2665
- const quote = raw[i];
2666
- i++;
2667
- let token2 = "";
2668
- while (i < raw.length && raw[i] !== quote) {
2669
- token2 += raw[i];
2670
- i++;
2671
- }
2672
- if (i < raw.length) i++;
2673
- tokens.push(token2);
2674
- continue;
2675
- }
2676
3382
  let token = "";
2677
3383
  while (i < raw.length && raw[i] !== " " && raw[i] !== " ") {
2678
- token += raw[i];
2679
- i++;
3384
+ if (raw[i] === "'" || raw[i] === '"') {
3385
+ const quote = raw[i];
3386
+ i++;
3387
+ while (i < raw.length && raw[i] !== quote) {
3388
+ token += raw[i];
3389
+ i++;
3390
+ }
3391
+ if (i < raw.length) i++;
3392
+ } else {
3393
+ token += raw[i];
3394
+ i++;
3395
+ }
2680
3396
  }
2681
3397
  tokens.push(token);
2682
3398
  }
2683
3399
  return tokens;
2684
3400
  }
2685
- parseExpression(expr) {
3401
+ /** Commands of a script, separated by `;` or newlines. */
3402
+ parseScript(script, extended) {
3403
+ const result = [];
2686
3404
  let pos = 0;
2687
- let address = null;
2688
- if (pos < expr.length) {
2689
- const addrResult = this.parseAddress(expr, pos);
2690
- if (addrResult) {
2691
- address = addrResult.address;
2692
- pos = addrResult.pos;
2693
- if (pos < expr.length && expr[pos] === ",") {
2694
- pos++;
2695
- const endResult = this.parseAddress(expr, pos);
2696
- if (endResult) {
2697
- if (endResult.address.type === "last") {
2698
- address = {
2699
- type: "range",
2700
- startLine: address.line,
2701
- endLine: 0,
2702
- endIsLast: true
2703
- };
2704
- } else {
2705
- address = {
2706
- type: "range",
2707
- startLine: address.line,
2708
- endLine: endResult.address.line
2709
- };
2710
- }
2711
- pos = endResult.pos;
2712
- }
3405
+ const skipSeparators = () => {
3406
+ while (pos < script.length && /[\s;]/.test(script[pos])) pos++;
3407
+ };
3408
+ skipSeparators();
3409
+ while (pos < script.length) {
3410
+ const parsed = this.parseCommand(script, pos, extended);
3411
+ result.push(parsed.expression);
3412
+ pos = parsed.pos;
3413
+ while (pos < script.length && /[ \t]/.test(script[pos])) pos++;
3414
+ if (pos < script.length && !/[;\n]/.test(script[pos])) {
3415
+ throw new Error(`extra characters after command: '${script.slice(pos)}'`);
3416
+ }
3417
+ skipSeparators();
3418
+ }
3419
+ if (result.length === 0) {
3420
+ throw new Error(`invalid expression: '${script}'`);
3421
+ }
3422
+ return result;
3423
+ }
3424
+ parseCommand(expr, pos, extended) {
3425
+ let start = null;
3426
+ let end = null;
3427
+ const first = this.parseAddress(expr, pos, extended);
3428
+ if (first) {
3429
+ start = first.address;
3430
+ pos = first.pos;
3431
+ if (expr[pos] === ",") {
3432
+ const second = this.parseAddress(expr, pos + 1, extended);
3433
+ if (!second) {
3434
+ throw new Error(`unexpected ',' in '${expr}'`);
2713
3435
  }
3436
+ end = second.address;
3437
+ pos = second.pos;
2714
3438
  }
2715
3439
  }
3440
+ while (pos < expr.length && /[ \t]/.test(expr[pos])) pos++;
3441
+ let negate = false;
3442
+ if (expr[pos] === "!") {
3443
+ negate = true;
3444
+ pos++;
3445
+ while (pos < expr.length && /[ \t]/.test(expr[pos])) pos++;
3446
+ }
2716
3447
  if (pos >= expr.length) {
2717
3448
  throw new Error(`invalid expression: '${expr}'`);
2718
3449
  }
3450
+ const base = { start, end, negate, active: false };
2719
3451
  const cmd = expr[pos];
2720
3452
  if (cmd === "s") {
2721
- return this.parseSubstitution(expr, pos, address);
2722
- } else if (cmd === "d") {
2723
- return { address, command: "d" };
2724
- } else if (cmd === "p") {
2725
- return { address, command: "p" };
2726
- } else {
2727
- throw new Error(`unknown command: '${cmd}'`);
3453
+ return this.parseSubstitution(expr, pos, base, extended);
2728
3454
  }
3455
+ if (cmd === "d" || cmd === "p" || cmd === "q") {
3456
+ return { expression: { ...base, command: cmd }, pos: pos + 1 };
3457
+ }
3458
+ throw new Error(`unknown command: '${cmd}'`);
2729
3459
  }
2730
- parseAddress(expr, pos) {
3460
+ parseAddress(expr, pos, extended) {
2731
3461
  if (pos >= expr.length) return null;
2732
3462
  if (expr[pos] >= "0" && expr[pos] <= "9") {
2733
3463
  let numStr = "";
@@ -2744,51 +3474,73 @@ var CliSedCommandProcessor = class {
2744
3474
  return { address: { type: "last" }, pos: pos + 1 };
2745
3475
  }
2746
3476
  if (expr[pos] === "/") {
2747
- pos++;
2748
- let pattern = "";
2749
- while (pos < expr.length && expr[pos] !== "/") {
2750
- if (expr[pos] === "\\" && pos + 1 < expr.length) {
2751
- pattern += expr[pos + 1];
2752
- pos += 2;
2753
- } else {
2754
- pattern += expr[pos];
2755
- pos++;
2756
- }
2757
- }
2758
- if (pos < expr.length) pos++;
3477
+ const parsed = this.readDelimited(expr, pos + 1, "/");
2759
3478
  try {
2760
3479
  return {
2761
- address: { type: "regex", regex: new RegExp(pattern) },
2762
- pos
3480
+ address: {
3481
+ type: "regex",
3482
+ regex: new RegExp(this.toJsRegex(parsed.text, extended))
3483
+ },
3484
+ pos: parsed.pos
2763
3485
  };
2764
3486
  } catch {
2765
- throw new Error(`invalid regex in address: '${pattern}'`);
3487
+ throw new Error(`invalid regex in address: '${parsed.text}'`);
2766
3488
  }
2767
3489
  }
2768
3490
  return null;
2769
3491
  }
2770
- parseSubstitution(expr, pos, address) {
2771
- pos++;
2772
- if (pos >= expr.length) {
2773
- throw new Error(`invalid substitution expression: '${expr}'`);
2774
- }
2775
- const delimiter = expr[pos];
2776
- pos++;
2777
- let pattern = "";
3492
+ /**
3493
+ * Text up to the next unescaped delimiter, escapes kept (an escaped
3494
+ * delimiter becomes the plain character when it has no regex meaning).
3495
+ */
3496
+ readDelimited(expr, pos, delimiter) {
3497
+ let text = "";
2778
3498
  while (pos < expr.length && expr[pos] !== delimiter) {
2779
3499
  if (expr[pos] === "\\" && pos + 1 < expr.length) {
2780
- pattern += "\\" + expr[pos + 1];
3500
+ const next = expr[pos + 1];
3501
+ text += next === delimiter && /\w|\//.test(next) ? next : "\\" + next;
2781
3502
  pos += 2;
2782
3503
  } else {
2783
- pattern += expr[pos];
3504
+ text += expr[pos];
2784
3505
  pos++;
2785
3506
  }
2786
3507
  }
2787
3508
  if (pos < expr.length) pos++;
3509
+ return { text, pos };
3510
+ }
3511
+ /**
3512
+ * Basic regular expressions use \( \) \{ \} \+ \? \| for groups and
3513
+ * operators. When a pattern uses them (or never in -E mode), they are
3514
+ * turned into the JavaScript form and the bare characters made literal.
3515
+ */
3516
+ toJsRegex(pattern, extended) {
3517
+ if (extended || !/\\[(){}+?|]/.test(pattern)) return pattern;
3518
+ let out = "";
3519
+ for (let i = 0; i < pattern.length; i++) {
3520
+ const ch = pattern[i];
3521
+ if (ch === "\\" && i + 1 < pattern.length) {
3522
+ const next = pattern[++i];
3523
+ out += "(){}+?|".includes(next) ? next : "\\" + next;
3524
+ } else if ("(){}+?|".includes(ch)) {
3525
+ out += "\\" + ch;
3526
+ } else {
3527
+ out += ch;
3528
+ }
3529
+ }
3530
+ return out;
3531
+ }
3532
+ parseSubstitution(expr, pos, base, extended) {
3533
+ pos++;
3534
+ if (pos >= expr.length) {
3535
+ throw new Error(`invalid substitution expression: '${expr}'`);
3536
+ }
3537
+ const delimiter = expr[pos];
3538
+ const pattern = this.readDelimited(expr, pos + 1, delimiter);
3539
+ pos = pattern.pos;
2788
3540
  let replacement = "";
2789
3541
  while (pos < expr.length && expr[pos] !== delimiter) {
2790
3542
  if (expr[pos] === "\\" && pos + 1 < expr.length) {
2791
- replacement += expr[pos + 1];
3543
+ replacement += expr[pos + 1] === delimiter ? delimiter : "\\" + expr[pos + 1];
2792
3544
  pos += 2;
2793
3545
  } else {
2794
3546
  replacement += expr[pos];
@@ -2799,47 +3551,75 @@ var CliSedCommandProcessor = class {
2799
3551
  let globalFlag = false;
2800
3552
  let caseInsensitive = false;
2801
3553
  let printFlag = false;
2802
- while (pos < expr.length) {
3554
+ let occurrence = "";
3555
+ while (pos < expr.length && !/[;\n\s}]/.test(expr[pos])) {
2803
3556
  const f = expr[pos];
2804
3557
  if (f === "g") globalFlag = true;
2805
- else if (f === "i") caseInsensitive = true;
3558
+ else if (f === "i" || f === "I") caseInsensitive = true;
2806
3559
  else if (f === "p") printFlag = true;
3560
+ else if (f >= "0" && f <= "9") occurrence += f;
3561
+ else throw new Error(`unknown option to 's': '${f}'`);
2807
3562
  pos++;
2808
3563
  }
3564
+ const nth = occurrence ? parseInt(occurrence, 10) : void 0;
3565
+ if (nth === 0) {
3566
+ throw new Error("number option to 's' command may not be zero");
3567
+ }
2809
3568
  let regexFlags = "";
2810
3569
  if (caseInsensitive) regexFlags += "i";
2811
- if (globalFlag) regexFlags += "g";
3570
+ if (globalFlag || nth) regexFlags += "g";
2812
3571
  let regex;
3572
+ let groupCount;
2813
3573
  try {
2814
- regex = new RegExp(pattern, regexFlags);
3574
+ const source = this.toJsRegex(pattern.text, extended);
3575
+ regex = new RegExp(source, regexFlags);
3576
+ groupCount = (new RegExp(`${source}|`).exec("")?.length ?? 1) - 1;
2815
3577
  } catch {
2816
- throw new Error(`invalid regex: '${pattern}'`);
3578
+ throw new Error(`invalid regex: '${pattern.text}'`);
2817
3579
  }
2818
3580
  return {
2819
- address,
2820
- command: "s",
2821
- pattern: regex,
2822
- replacement,
2823
- globalFlag,
2824
- printFlag
3581
+ expression: {
3582
+ ...base,
3583
+ command: "s",
3584
+ pattern: regex,
3585
+ replacement,
3586
+ groupCount,
3587
+ printFlag,
3588
+ occurrence: nth,
3589
+ globalFlag
3590
+ },
3591
+ pos
2825
3592
  };
2826
3593
  }
2827
- addressMatches(address, lineNum, totalLines, lineText) {
2828
- if (!address) return true;
3594
+ addressMatches(expr, lineNum, totalLines, lineText) {
3595
+ let matches;
3596
+ if (!expr.start) {
3597
+ matches = true;
3598
+ } else if (!expr.end) {
3599
+ matches = this.singleMatches(expr.start, lineNum, totalLines, lineText);
3600
+ } else if (!expr.active) {
3601
+ matches = this.singleMatches(expr.start, lineNum, totalLines, lineText);
3602
+ if (matches) {
3603
+ const end = expr.end;
3604
+ const endsHere = end.type === "line" && end.line <= lineNum || end.type === "last" && lineNum === totalLines;
3605
+ expr.active = !endsHere;
3606
+ }
3607
+ } else {
3608
+ matches = true;
3609
+ const end = expr.end;
3610
+ const endsHere = end.type === "line" ? lineNum >= end.line : this.singleMatches(end, lineNum, totalLines, lineText);
3611
+ if (endsHere) expr.active = false;
3612
+ }
3613
+ return expr.negate ? !matches : matches;
3614
+ }
3615
+ singleMatches(address, lineNum, totalLines, lineText) {
2829
3616
  switch (address.type) {
2830
3617
  case "line":
2831
3618
  return lineNum === address.line;
2832
3619
  case "last":
2833
3620
  return lineNum === totalLines;
2834
- case "range": {
2835
- const start = address.startLine;
2836
- const end = address.endIsLast ? totalLines : address.endLine;
2837
- return lineNum >= start && lineNum <= end;
2838
- }
2839
3621
  case "regex":
2840
3622
  return address.regex.test(lineText);
2841
- default:
2842
- return true;
2843
3623
  }
2844
3624
  }
2845
3625
  };
@@ -2861,11 +3641,34 @@ var CliAwkCommandProcessor = class {
2861
3641
  }
2862
3642
  ];
2863
3643
  }
3644
+ writeDescription(context) {
3645
+ const t = context.translator;
3646
+ writeWrapped(
3647
+ context,
3648
+ t.t("cli.awk.help.process_text_records_fields", "Process text as records and fields: each line is split into fields $1, $2, ... ($0 is the whole line), and each rule runs its { action } on matching lines. Supports BEGIN and END blocks, /regex/ and field comparison patterns, print, variables, NR and NF. Quote the program; a simple one also works unquoted (awk {print $1} file).")
3649
+ );
3650
+ context.writer.writeln();
3651
+ writeUsage(context, [
3652
+ ["awk '<program>' <file>", t.t("cli.awk.help.run_program_over_file", "Run a program over a file")],
3653
+ [
3654
+ "awk -F <sep> '<program>' <file>",
3655
+ t.t("cli.awk.help.split_fields_instead_spaces", "Split fields on <sep> instead of spaces")
3656
+ ],
3657
+ ["<command> | awk '<program>'", t.t("cli.awk.help.run_program_over_piped", "Run a program over piped input")]
3658
+ ]);
3659
+ context.writer.writeln();
3660
+ writeExamples(context, [
3661
+ ["awk '{print $1}' data.txt", t.t("cli.awk.help.print_first_column", "Print the first column")],
3662
+ ["awk -F , '{print $2}' data.csv", t.t("cli.awk.help.second_column_csv", "Second column of a CSV")],
3663
+ ["awk '$3 > 100 {print $1}' sales.txt", t.t("cli.awk.help.filter_rows_field", "Filter rows by a field")],
3664
+ ["awk '{s += $2} END {print s}' data.txt", t.t("cli.awk.help.sum_second_column", "Sum the second column")]
3665
+ ]);
3666
+ }
2864
3667
  async processCommand(command, context) {
2865
3668
  const fs = context.services.getRequired(
2866
3669
  IFileSystemService_TOKEN
2867
3670
  );
2868
- const { program, filePath, fieldSep } = this.parseArgs(command);
3671
+ const { program, filePaths, fieldSep } = this.parseArgs(command, fs);
2869
3672
  if (!program) {
2870
3673
  context.writer.writeError(
2871
3674
  "awk: missing program. Usage: awk [-F sep] 'program' file"
@@ -2873,18 +3676,21 @@ var CliAwkCommandProcessor = class {
2873
3676
  return;
2874
3677
  }
2875
3678
  let content = "";
2876
- if (filePath) {
2877
- try {
2878
- if (!fs.exists(filePath)) {
2879
- context.writer.writeError(
2880
- `awk: ${filePath}: No such file or directory`
2881
- );
3679
+ if (filePaths.length > 0) {
3680
+ for (const filePath of filePaths) {
3681
+ try {
3682
+ if (!fs.exists(filePath)) {
3683
+ context.writer.writeError(
3684
+ `awk: ${filePath}: No such file or directory`
3685
+ );
3686
+ return;
3687
+ }
3688
+ const text = fs.readFile(filePath) ?? "";
3689
+ content += text === "" || text.endsWith("\n") ? text : text + "\n";
3690
+ } catch (e) {
3691
+ context.writer.writeError(`awk: ${e.message}`);
2882
3692
  return;
2883
3693
  }
2884
- content = fs.readFile(filePath) ?? "";
2885
- } catch (e) {
2886
- context.writer.writeError(`awk: ${e.message}`);
2887
- return;
2888
3694
  }
2889
3695
  } else if (command.data != null) {
2890
3696
  content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
@@ -2946,24 +3752,58 @@ var CliAwkCommandProcessor = class {
2946
3752
  context.writer.writeln(line);
2947
3753
  }
2948
3754
  }
2949
- parseArgs(command) {
2950
- const raw = command.value || "";
2951
- const fieldSep = command.args["F"] || null;
2952
- let rest = raw;
2953
- let program = null;
2954
- const sqMatch = rest.match(/^'((?:[^'\\]|\\.)*)'\s*/);
2955
- if (sqMatch) {
2956
- program = sqMatch[1];
2957
- rest = rest.slice(sqMatch[0].length);
2958
- } else {
2959
- const dqMatch = rest.match(/^"((?:[^"\\]|\\.)*)"\s*/);
2960
- if (dqMatch) {
2961
- program = dqMatch[1];
2962
- rest = rest.slice(dqMatch[0].length);
3755
+ /**
3756
+ * The program is the first operand, usually quoted. Unquoted, it may span
3757
+ * several words (`awk {print $1} file`): it is then everything before the
3758
+ * trailing operands that are existing files, or the whole value with
3759
+ * piped input.
3760
+ */
3761
+ parseArgs(command, fs) {
3762
+ const raw = (command.value || "").trim();
3763
+ const fieldArg = command.args["F"];
3764
+ const fieldSep = fieldArg != null && fieldArg !== true ? String(fieldArg) : null;
3765
+ const quoted = raw.match(/^'((?:[^'\\]|\\.)*)'(?:\s+|$)/) ?? raw.match(/^"((?:[^"\\]|\\.)*)"(?:\s+|$)/);
3766
+ if (quoted) {
3767
+ return {
3768
+ program: quoted[1],
3769
+ filePaths: splitArgs(raw.slice(quoted[0].length)),
3770
+ fieldSep
3771
+ };
3772
+ }
3773
+ const words = raw.split(/\s+/).filter(Boolean);
3774
+ if (words.length === 0) {
3775
+ return { program: null, filePaths: [], fieldSep };
3776
+ }
3777
+ const isFile = (word) => {
3778
+ try {
3779
+ const path = splitArgs(word).join(" ");
3780
+ return fs.exists(path) && !fs.isDirectory(path);
3781
+ } catch {
3782
+ return false;
2963
3783
  }
3784
+ };
3785
+ let split = words.length;
3786
+ while (split > 1 && isFile(words[split - 1])) split--;
3787
+ if (split === words.length && command.data == null && words.length > 1) {
3788
+ const program = words.slice(0, -1).join(" ");
3789
+ const last = words[words.length - 1];
3790
+ if (this.bracesBalanced(program) && !/[{}]/.test(last)) {
3791
+ split = words.length - 1;
3792
+ }
3793
+ }
3794
+ return {
3795
+ program: words.slice(0, split).join(" "),
3796
+ filePaths: words.slice(split).map((w) => splitArgs(w).join(" ")),
3797
+ fieldSep
3798
+ };
3799
+ }
3800
+ bracesBalanced(text) {
3801
+ let depth = 0;
3802
+ for (const ch of text) {
3803
+ if (ch === "{") depth++;
3804
+ else if (ch === "}" && --depth < 0) return false;
2964
3805
  }
2965
- const filePath = rest.trim() || null;
2966
- return { program, filePath, fieldSep };
3806
+ return depth === 0;
2967
3807
  }
2968
3808
  parseProgram(program) {
2969
3809
  const rules = [];
@@ -3337,6 +4177,24 @@ var CliDiffCommandProcessor = class {
3337
4177
  }
3338
4178
  ];
3339
4179
  }
4180
+ writeDescription(context) {
4181
+ const t = context.translator;
4182
+ writeWrapped(
4183
+ context,
4184
+ t.t("cli.diff.help.compare_two_files_line", "Compare two files line by line and show what changed: lines starting with < (or -) are only in the first file, lines starting with > (or +) only in the second.")
4185
+ );
4186
+ context.writer.writeln();
4187
+ writeUsage(context, [
4188
+ ["diff <file1> <file2>", t.t("cli.diff.help.show_differences", "Show the differences")],
4189
+ ["diff -u <file1> <file2>", t.t("cli.diff.help.unified_format_context", "Unified format with context")]
4190
+ ]);
4191
+ context.writer.writeln();
4192
+ writeExamples(context, [
4193
+ ["diff old.txt new.txt", t.t("cli.diff.help.what_changed_between_versions", "What changed between versions")],
4194
+ ["diff -u config.bak config.txt", t.t("cli.diff.help.review_edits_git_style", "Review edits, git style")],
4195
+ ["diff -i a.txt b.txt", t.t("cli.diff.help.ignore_upper_lower_case", "Ignore upper/lower case")]
4196
+ ]);
4197
+ }
3340
4198
  async processCommand(command, context) {
3341
4199
  const fs = context.services.getRequired(
3342
4200
  IFileSystemService_TOKEN
@@ -3501,13 +4359,13 @@ var CliDiffCommandProcessor = class {
3501
4359
  }
3502
4360
  parsePaths(command) {
3503
4361
  const raw = command.value || "";
3504
- return raw.split(/\s+/).filter(Boolean);
4362
+ return splitArgs(raw).filter(Boolean);
3505
4363
  }
3506
4364
  };
3507
4365
  var CliTeeCommandProcessor = class {
3508
4366
  constructor() {
3509
4367
  this.command = "tee";
3510
- this.description = "Read from a file and write to output files and stdout";
4368
+ this.description = "Copy piped input to files and to the output";
3511
4369
  this.author = DefaultLibraryAuthor;
3512
4370
  this.version = LIBRARY_VERSION;
3513
4371
  this.acceptsRawInput = true;
@@ -3523,72 +4381,56 @@ var CliTeeCommandProcessor = class {
3523
4381
  }
3524
4382
  ];
3525
4383
  }
4384
+ writeDescription(context) {
4385
+ const t = context.translator;
4386
+ writeWrapped(
4387
+ context,
4388
+ t.t("cli.tee.help.pass_piped_input_through", "Pass piped input through unchanged and save a copy of it to every file named, overwriting them or appending with -a. Like Unix tee, it reads only piped input: to copy a file, pipe it in with cat.")
4389
+ );
4390
+ context.writer.writeln();
4391
+ writeUsage(context, [
4392
+ ["<command> | tee <file...>", t.t("cli.tee.help.show_piped_output_save", "Show piped output and save it")],
4393
+ ["<command> | tee -a <file>", t.t("cli.tee.help.append_instead_overwriting", "Append instead of overwriting")]
4394
+ ]);
4395
+ context.writer.writeln();
4396
+ writeExamples(context, [
4397
+ ["cat notes.txt | tee copy.txt", t.t("cli.tee.help.show_save_copy", "Show and save a copy")],
4398
+ ["echo done | tee -a log.txt", t.t("cli.tee.help.add_line_log", "Add a line to a log")],
4399
+ [
4400
+ "cat source.txt | tee a.txt b.txt",
4401
+ t.t("cli.tee.help.copy_source_txt_txt", "Copy source.txt into a.txt and b.txt")
4402
+ ]
4403
+ ]);
4404
+ }
3526
4405
  async processCommand(command, context) {
3527
4406
  const fs = context.services.getRequired(
3528
4407
  IFileSystemService_TOKEN
3529
4408
  );
3530
4409
  const append = command.args["append"] || command.args["a"] || false;
3531
- const parsed = this.parseArgs(command);
3532
- let content;
3533
- if (command.data != null) {
3534
- content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
3535
- const outputFiles = this.getAllPaths(command);
3536
- context.writer.writeln(content);
3537
- for (const outFile of outputFiles) {
3538
- if (append && fs.exists(outFile)) {
3539
- fs.writeFile(outFile, content, true);
3540
- } else {
3541
- fs.writeFile(outFile, content);
3542
- }
3543
- }
3544
- await fs.persist();
3545
- } else {
3546
- if (parsed.outputFiles.length === 0 && !parsed.inputFile) {
3547
- context.writer.writeError("tee: missing operand");
3548
- return;
3549
- }
3550
- if (!parsed.inputFile) {
3551
- context.writer.writeError("tee: missing input file");
3552
- return;
3553
- }
3554
- if (parsed.outputFiles.length === 0) {
3555
- context.writer.writeError("tee: missing output file");
3556
- return;
3557
- }
3558
- if (!fs.exists(parsed.inputFile)) {
3559
- context.writer.writeError(
3560
- `tee: ${parsed.inputFile}: No such file or directory`
3561
- );
3562
- return;
3563
- }
3564
- content = fs.readFile(parsed.inputFile) || "";
3565
- context.writer.writeln(content);
3566
- for (const outFile of parsed.outputFiles) {
3567
- if (append && fs.exists(outFile)) {
3568
- fs.writeFile(outFile, content, true);
3569
- } else {
3570
- fs.writeFile(outFile, content);
3571
- }
4410
+ if (command.data == null) {
4411
+ context.writer.writeError(
4412
+ "tee: no input: tee copies piped input to files and the output; use 'cat file | tee out.txt'"
4413
+ );
4414
+ context.process.exit(1, { silent: true });
4415
+ return;
4416
+ }
4417
+ const content = pipedText(command.data);
4418
+ context.writer.writeln(content);
4419
+ const fileText = content === "" || content.endsWith("\n") ? content : content + "\n";
4420
+ let changed = false;
4421
+ for (const outFile of this.getAllPaths(command)) {
4422
+ try {
4423
+ fs.writeFile(outFile, fileText, append);
4424
+ changed = true;
4425
+ } catch (e) {
4426
+ context.writer.writeError(`tee: ${e.message}`);
3572
4427
  }
3573
- await fs.persist();
3574
4428
  }
4429
+ if (changed) await fs.persist();
3575
4430
  }
3576
4431
  getAllPaths(command) {
3577
4432
  const raw = command.value || "";
3578
- return raw.split(/\s+/).filter(Boolean);
3579
- }
3580
- parseArgs(command) {
3581
- const raw = command.value || "";
3582
- const paths = raw.split(/\s+/).filter(Boolean);
3583
- if (paths.length < 2) {
3584
- return {
3585
- outputFiles: [],
3586
- inputFile: paths[0] || null
3587
- };
3588
- }
3589
- const inputFile = paths[paths.length - 1];
3590
- const outputFiles = paths.slice(0, paths.length - 1);
3591
- return { outputFiles, inputFile };
4433
+ return splitArgs(raw).filter(Boolean);
3592
4434
  }
3593
4435
  };
3594
4436
  var CliXargsCommandProcessor = class {
@@ -3617,65 +4459,91 @@ var CliXargsCommandProcessor = class {
3617
4459
  }
3618
4460
  ];
3619
4461
  }
4462
+ writeDescription(context) {
4463
+ const t = context.translator;
4464
+ writeWrapped(
4465
+ context,
4466
+ t.t("cli.xargs.help.run_command_arguments_taken", "Run a command with arguments taken from input, separated by blanks or newlines. By default all of them are appended to one command; -n runs it in batches and -I runs it once per line, replacing a placeholder. xargs options go before the command; everything after it, flags included, belongs to the command. Input is piped text or, without a pipe, the last file named.")
4467
+ );
4468
+ context.writer.writeln();
4469
+ writeUsage(context, [
4470
+ ["<command> | xargs <cmd>", t.t("cli.xargs.help.append_input_lines", "Append input lines to <cmd>")],
4471
+ ["<command> | xargs -I {} <cmd {}>", t.t("cli.xargs.help.run_once_per_line", "Run <cmd> once per line")],
4472
+ ["xargs <cmd> <file>", t.t("cli.xargs.help.read_arguments_file", "Read the arguments from a file")]
4473
+ ]);
4474
+ context.writer.writeln();
4475
+ writeExamples(context, [
4476
+ ['find . --name "*.tmp" | xargs rm', t.t("cli.xargs.help.delete_every_tmp_file", "Delete every .tmp file")],
4477
+ [
4478
+ "cat files.txt | xargs -I {} cp {} backup",
4479
+ t.t("cli.xargs.help.copy_each_listed_file", "Copy each listed file")
4480
+ ],
4481
+ ["cat dirs.txt | xargs -n 1 mkdir", t.t("cli.xargs.help.one_mkdir_per_line", "One mkdir per line")],
4482
+ ['find . --name "*.bak" | xargs rm -r', t.t("cli.xargs.help.inner_command_keeps_flags", "The inner command keeps its flags")]
4483
+ ]);
4484
+ }
3620
4485
  async processCommand(command, context) {
3621
4486
  const fs = context.services.getRequired(
3622
4487
  IFileSystemService_TOKEN
3623
4488
  );
3624
- const replaceStr = command.args["replace"] || command.args["I"] || null;
3625
- const maxArgs = parseInt(
3626
- command.args["max-args"] || command.args["n"] || "0",
3627
- 10
3628
- );
3629
- const parsed = this.parseArgs(command);
4489
+ let parsed;
4490
+ try {
4491
+ parsed = command.rawCommand ? parseXargsLine(command.rawCommand) : {
4492
+ replace: stringArg(command.args["replace"] ?? command.args["I"]),
4493
+ maxArgs: parseInt(
4494
+ String(command.args["max-args"] ?? command.args["n"] ?? ""),
4495
+ 10
4496
+ ) || void 0,
4497
+ words: splitWords(command.value ?? "")
4498
+ };
4499
+ } catch (e) {
4500
+ context.writer.writeError(`xargs: ${e.message}`);
4501
+ return;
4502
+ }
4503
+ const replaceStr = parsed.replace;
4504
+ const maxArgs = parsed.maxArgs ?? 0;
3630
4505
  let commandTemplate;
3631
4506
  let argContent;
3632
4507
  if (command.data != null) {
3633
- commandTemplate = this.getAllNonFlagTokens(command).join(" ");
3634
- if (!commandTemplate) {
3635
- context.writer.writeError("xargs: missing command");
3636
- return;
3637
- }
3638
- argContent = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
4508
+ commandTemplate = parsed.words.map((w) => w.raw).join(" ") || "echo";
4509
+ argContent = pipedText(command.data);
3639
4510
  } else {
3640
- if (!parsed.commandTemplate) {
4511
+ if (parsed.words.length === 0) {
3641
4512
  context.writer.writeError("xargs: missing command");
3642
4513
  return;
3643
4514
  }
3644
- commandTemplate = parsed.commandTemplate;
3645
- if (!parsed.inputFile) {
4515
+ if (parsed.words.length < 2) {
3646
4516
  context.writer.writeError("xargs: missing input file");
3647
4517
  return;
3648
4518
  }
3649
- if (!fs.exists(parsed.inputFile)) {
4519
+ const inputFile = parsed.words[parsed.words.length - 1].value;
4520
+ commandTemplate = parsed.words.slice(0, -1).map((w) => w.raw).join(" ");
4521
+ if (!fs.exists(inputFile)) {
3650
4522
  context.writer.writeError(
3651
- `xargs: ${parsed.inputFile}: No such file or directory`
4523
+ `xargs: ${inputFile}: No such file or directory`
3652
4524
  );
3653
4525
  return;
3654
4526
  }
3655
- argContent = fs.readFile(parsed.inputFile) || "";
4527
+ argContent = fs.readFile(inputFile) || "";
3656
4528
  }
3657
- const args = argContent.split("\n").map((l) => l.trim()).filter(Boolean);
3658
- if (args.length === 0) return;
3659
4529
  const commands = [];
3660
4530
  if (replaceStr) {
3661
- for (const arg of args) {
3662
- const cmd = commandTemplate.replace(
3663
- new RegExp(this.escapeRegex(replaceStr), "g"),
3664
- arg
4531
+ const lines = argContent.split("\n").map((l) => l.trim()).filter(Boolean);
4532
+ const placeholder = new RegExp(this.escapeRegex(replaceStr), "g");
4533
+ for (const line of lines) {
4534
+ commands.push(
4535
+ commandTemplate.replace(placeholder, () => quoteArg(line))
3665
4536
  );
3666
- commands.push(cmd);
3667
4537
  }
3668
- } else if (maxArgs > 0) {
3669
- for (let i = 0; i < args.length; i += maxArgs) {
3670
- const group = args.slice(i, i + maxArgs);
4538
+ } else {
4539
+ const args = splitArgs(argContent).map(quoteArg);
4540
+ if (args.length === 0) return;
4541
+ const size = maxArgs > 0 ? maxArgs : args.length;
4542
+ for (let i = 0; i < args.length; i += size) {
3671
4543
  commands.push(
3672
- `${commandTemplate} ${group.join(" ")}`
4544
+ `${commandTemplate} ${args.slice(i, i + size).join(" ")}`
3673
4545
  );
3674
4546
  }
3675
- } else {
3676
- commands.push(
3677
- `${commandTemplate} ${args.join(" ")}`
3678
- );
3679
4547
  }
3680
4548
  const executor = context.executor;
3681
4549
  for (const cmd of commands) {
@@ -3690,27 +4558,64 @@ var CliXargsCommandProcessor = class {
3690
4558
  }
3691
4559
  }
3692
4560
  }
3693
- getAllNonFlagTokens(command) {
3694
- const raw = command.value || "";
3695
- return raw.split(/\s+/).filter(Boolean);
3696
- }
3697
4561
  escapeRegex(str) {
3698
4562
  return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3699
4563
  }
3700
- parseArgs(command) {
3701
- const raw = command.value || "";
3702
- const nonFlagTokens = raw.split(/\s+/).filter(Boolean);
3703
- if (nonFlagTokens.length < 2) {
3704
- return {
3705
- commandTemplate: nonFlagTokens[0] || null,
3706
- inputFile: null
3707
- };
4564
+ };
4565
+ function parseXargsLine(raw) {
4566
+ const words = splitWords(raw).slice(1);
4567
+ const result = { words: [] };
4568
+ let i = 0;
4569
+ const valueOf = (option, inline) => {
4570
+ if (inline !== "") return inline;
4571
+ if (i + 1 >= words.length) {
4572
+ throw new Error(`option requires an argument -- '${option}'`);
4573
+ }
4574
+ return words[++i].value;
4575
+ };
4576
+ for (; i < words.length; i++) {
4577
+ const word = words[i].raw;
4578
+ if (word === "--") {
4579
+ i++;
4580
+ break;
4581
+ }
4582
+ if (!word.startsWith("-") || word === "-") break;
4583
+ let match;
4584
+ if (match = word.match(/^(?:-I|--replace(?:=|$))(.*)$/)) {
4585
+ result.replace = valueOf("I", unquote(match[1]));
4586
+ } else if (word === "-i") {
4587
+ result.replace = "{}";
4588
+ } else if (match = word.match(/^(?:-n|--max-args(?:=|$))(.*)$/)) {
4589
+ const value = valueOf("n", unquote(match[1]));
4590
+ const count = parseInt(value, 10);
4591
+ if (isNaN(count) || count < 1) {
4592
+ throw new Error(`invalid number of arguments: '${value}'`);
4593
+ }
4594
+ result.maxArgs = count;
4595
+ } else {
4596
+ throw new Error(`invalid option -- '${word.replace(/^-+/, "")}'`);
3708
4597
  }
3709
- const inputFile = nonFlagTokens[nonFlagTokens.length - 1];
3710
- const commandTemplate = nonFlagTokens.slice(0, nonFlagTokens.length - 1).join(" ");
3711
- return { commandTemplate, inputFile };
3712
4598
  }
3713
- };
4599
+ result.words = words.slice(i);
4600
+ return result;
4601
+ }
4602
+ function splitWords(raw) {
4603
+ const words = [];
4604
+ const regex = /(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"']|["'])+/g;
4605
+ for (const match of raw.matchAll(regex)) {
4606
+ words.push({ raw: match[0], value: splitArgs(match[0]).join(" ") });
4607
+ }
4608
+ return words;
4609
+ }
4610
+ function unquote(value) {
4611
+ return splitArgs(value).join(" ");
4612
+ }
4613
+ function stringArg(value) {
4614
+ return value == null || value === true || value === "" ? void 0 : String(value);
4615
+ }
4616
+ function quoteArg(arg) {
4617
+ return quoteShellWord(arg);
4618
+ }
3714
4619
  var CliShCommandProcessor = class {
3715
4620
  constructor() {
3716
4621
  this.command = "sh";
@@ -3734,14 +4639,11 @@ var CliShCommandProcessor = class {
3734
4639
  const fs = context.services.getRequired(
3735
4640
  IFileSystemService_TOKEN
3736
4641
  );
3737
- let filePath = (command.value || "").trim();
3738
- if (!filePath && command.rawCommand) {
3739
- const raw = command.rawCommand.trim();
3740
- const firstSpace = raw.indexOf(" ");
3741
- if (firstSpace > -1) {
3742
- filePath = raw.slice(firstSpace + 1).trim();
3743
- }
4642
+ let words = splitArgs(command.value || "");
4643
+ if (words.length === 0 && command.rawCommand) {
4644
+ words = splitArgs(command.rawCommand).slice(1).filter((w) => !w.startsWith("-"));
3744
4645
  }
4646
+ const filePath = words[0] ?? "";
3745
4647
  if (!filePath) {
3746
4648
  context.writer.writeError("sh: missing file operand");
3747
4649
  context.process.exit(1, { silent: true });
@@ -3774,10 +4676,10 @@ var CliShCommandProcessor = class {
3774
4676
  await executeScript(content, context);
3775
4677
  }
3776
4678
  writeDescription(context) {
3777
- const { writer } = context;
3778
- writer.writeln(this.description);
4679
+ const { writer, translator: t } = context;
4680
+ writer.writeln(t.t("cli.sh.description", this.description));
3779
4681
  writer.writeln();
3780
- writer.writeln("Usage:");
4682
+ writer.writeln(t.t("cli.common.usage", "Usage:"));
3781
4683
  writer.writeln(
3782
4684
  ` ${writer.wrapInColor("sh <script.sh>", CliForegroundColor.Cyan)}`
3783
4685
  );
@@ -3788,74 +4690,48 @@ var CliShCommandProcessor = class {
3788
4690
  ` ${writer.wrapInColor(". <script.sh>", CliForegroundColor.Cyan)}`
3789
4691
  );
3790
4692
  writer.writeln(
3791
- ` ${writer.wrapInColor("./script.sh", CliForegroundColor.Cyan)} (requires execute permission)`
4693
+ ` ${writer.wrapInColor("./script.sh", CliForegroundColor.Cyan)} ${t.t("cli.sh.help.requires_execute", "(requires execute permission)")}`
3792
4694
  );
3793
4695
  writer.writeln();
3794
- writer.writeln("Script syntax:");
3795
- writer.writeln(" # Lines starting with # are comments");
3796
- writer.writeln(" VAR=value # Variable assignment");
3797
- writer.writeln(" echo $VAR # Variable substitution");
3798
- writer.writeln(" set -e # Stop on error (default)");
3799
- writer.writeln(" set +e # Continue on errors");
4696
+ writer.writeln(t.t("cli.sh.help.script_syntax", "Script syntax:"));
4697
+ writer.writeln(` # ${t.t("cli.sh.help.comments", "Lines starting with # are comments")}`);
4698
+ writer.writeln(` cmd1; cmd2 # ${t.t("cli.sh.help.several_commands", "Several commands on one line")}`);
4699
+ writer.writeln(` VAR=value # ${t.t("cli.sh.help.assignment", "Variable assignment")}`);
4700
+ writer.writeln(` echo $VAR # ${t.t("cli.sh.help.substitution", "Variable substitution")}`);
4701
+ writer.writeln(` set -e # ${t.t("cli.sh.help.stop_on_error", "Stop on error (default)")}`);
4702
+ writer.writeln(` set +e # ${t.t("cli.sh.help.continue_on_errors", "Continue on errors")}`);
4703
+ writer.writeln(` exit [n] # ${t.t("cli.sh.help.exit", "Stop the script with status n")}`);
4704
+ writer.writeln();
4705
+ writer.writeln(t.t("cli.sh.help.control_flow", "Control flow:"));
4706
+ writer.writeln(" if cmd; then ...; elif cmd; then ...; else ...; fi");
4707
+ writer.writeln(" for NAME in a b *.txt; do ...; done");
4708
+ writer.writeln(` while cmd; do ...; done ${t.t("cli.sh.help.until", "(until cmd: the opposite)")}`);
4709
+ writer.writeln(` break / continue [n] # ${t.t("cli.sh.help.break_continue", "Leave / skip in a loop")}`);
4710
+ writer.writeln(` ! cmd # ${t.t("cli.sh.help.negate", "Negate the exit status")}`);
4711
+ writer.writeln(
4712
+ ` ${t.t("cli.sh.help.conditions", "Conditions are commands:")} ${writer.wrapInColor("[ -f file ]", CliForegroundColor.Cyan)}, ${writer.wrapInColor('test "$A" = b', CliForegroundColor.Cyan)}, ${writer.wrapInColor("true", CliForegroundColor.Cyan)}, ${writer.wrapInColor("grep x file", CliForegroundColor.Cyan)}`
4713
+ );
4714
+ writer.writeln(` ${t.t("cli.sh.help.conditions_status", "(status 0 = true; see help test). Loops stop after 10000 iterations")}`);
4715
+ writer.writeln(` ${t.t("cli.sh.help.conditions_loops", "of while/until; Ctrl+C stops the script.")}`);
3800
4716
  writer.writeln();
3801
- writer.writeln("Example script:");
4717
+ writer.writeln(t.t("cli.sh.help.example_script", "Example script:"));
3802
4718
  writer.writeln(
3803
4719
  writer.wrapInColor(
3804
- " #!/bin/sh\n # Setup script\n NAME=world\n echo Hello $NAME\n mkdir -p /tmp/test\n echo done > /tmp/test/status.txt",
4720
+ " #!/bin/sh\n # Setup script\n NAME=world\n echo Hello $NAME\n mkdir -p /tmp/test\n for f in a b c; do\n echo $f > /tmp/test/$f.txt\n done\n if [ -f /tmp/test/a.txt ]; then\n echo done > /tmp/test/status.txt\n fi",
3805
4721
  CliForegroundColor.Green
3806
4722
  )
3807
4723
  );
3808
4724
  }
3809
4725
  };
4726
+ var SCRIPT_RUNNER_TOKEN = "cli-script-runner";
3810
4727
  async function executeScript(content, context) {
3811
- const lines = content.split("\n");
3812
- const variables = {};
3813
- let stopOnError = true;
3814
- for (const rawLine of lines) {
3815
- const line = rawLine.trim();
3816
- if (!line || line.startsWith("#")) {
3817
- continue;
3818
- }
3819
- if (line === "set -e") {
3820
- stopOnError = true;
3821
- continue;
3822
- }
3823
- if (line === "set +e") {
3824
- stopOnError = false;
3825
- continue;
3826
- }
3827
- const assignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
3828
- if (assignMatch) {
3829
- const varName = assignMatch[1];
3830
- let varValue = assignMatch[2];
3831
- if (varValue.startsWith('"') && varValue.endsWith('"') || varValue.startsWith("'") && varValue.endsWith("'")) {
3832
- varValue = varValue.slice(1, -1);
3833
- }
3834
- varValue = substituteVariables(varValue, variables);
3835
- variables[varName] = varValue;
3836
- continue;
3837
- }
3838
- const expanded = substituteVariables(line, variables);
3839
- await context.executor.executeCommand(expanded, context);
3840
- const exitCode = context.process.exitCode;
3841
- if (stopOnError && exitCode !== void 0 && exitCode !== 0) {
3842
- context.writer.writeError(
3843
- `sh: script aborted at line: ${line}`
3844
- );
3845
- return;
3846
- }
4728
+ const runner = context.services.get(SCRIPT_RUNNER_TOKEN);
4729
+ if (!runner) {
4730
+ context.writer.writeError("sh: script runner not available (update @qodalis/cli)");
4731
+ context.process.exit(1, { silent: true });
4732
+ return;
3847
4733
  }
3848
- }
3849
- function substituteVariables(input, variables) {
3850
- let result = input.replace(
3851
- /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
3852
- (_, name) => variables[name] ?? ""
3853
- );
3854
- result = result.replace(
3855
- /\$([A-Za-z_][A-Za-z0-9_]*)/g,
3856
- (_, name) => variables[name] ?? ""
3857
- );
3858
- return result;
4734
+ await runner.run(content, context, { name: "sh" });
3859
4735
  }
3860
4736
  var DB_NAME = "qodalis-cli-filesystem";
3861
4737
  var STORE_NAME = "filesystem";
@@ -4323,6 +5199,11 @@ var IndexedDbFileSystemService = class {
4323
5199
  }
4324
5200
  const destResolved = this.resolvePath(dest);
4325
5201
  const destNode = this.getNode(destResolved);
5202
+ if (srcNode.type === "directory" && (destResolved === srcResolved || destResolved.startsWith(srcResolved + "/"))) {
5203
+ throw new Error(
5204
+ `mv: cannot move '${src}' to a subdirectory of itself, '${dest}'`
5205
+ );
5206
+ }
4326
5207
  const srcParts = srcResolved.split("/").filter(Boolean);
4327
5208
  const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
4328
5209
  const srcParent = this.getNode(srcParentPath);
@@ -4330,9 +5211,9 @@ var IndexedDbFileSystemService = class {
4330
5211
  throw new Error(`mv: ${src}: No such file or directory`);
4331
5212
  }
4332
5213
  this.assertModifyDir(srcParent, srcParentPath, "mv", src);
4333
- const srcIndex = srcParent.children.findIndex(
4334
- (c) => c.name === srcNode.name
4335
- );
5214
+ if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
5215
+ throw new Error(`mv: '${src}' and '${dest}' are the same file`);
5216
+ }
4336
5217
  if (destNode && destNode.type === "directory") {
4337
5218
  this.assertModifyDir(destNode, destResolved, "mv", dest);
4338
5219
  if (!destNode.children) {
@@ -4344,7 +5225,7 @@ var IndexedDbFileSystemService = class {
4344
5225
  if (existingIdx !== -1) {
4345
5226
  destNode.children.splice(existingIdx, 1);
4346
5227
  }
4347
- srcParent.children.splice(srcIndex, 1);
5228
+ srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
4348
5229
  destNode.children.push(srcNode);
4349
5230
  destNode.modifiedAt = Date.now();
4350
5231
  } else {
@@ -4358,7 +5239,7 @@ var IndexedDbFileSystemService = class {
4358
5239
  );
4359
5240
  }
4360
5241
  this.assertModifyDir(destParent, destParentPath, "mv", dest);
4361
- srcParent.children.splice(srcIndex, 1);
5242
+ srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
4362
5243
  srcNode.name = newName;
4363
5244
  if (!destParent.children) {
4364
5245
  destParent.children = [];
@@ -4855,15 +5736,20 @@ var ServerFileSystemService = class {
4855
5736
  }
4856
5737
  const destResolved = this.resolvePath(dest);
4857
5738
  const destNode = this.getNode(destResolved);
5739
+ if (srcNode.type === "directory" && (destResolved === srcResolved || destResolved.startsWith(srcResolved + "/"))) {
5740
+ throw new Error(
5741
+ `mv: cannot move '${src}' to a subdirectory of itself, '${dest}'`
5742
+ );
5743
+ }
4858
5744
  const srcParts = srcResolved.split("/").filter(Boolean);
4859
5745
  const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
4860
5746
  const srcParent = this.getNode(srcParentPath);
4861
5747
  if (!srcParent || !srcParent.children) {
4862
5748
  throw new Error(`mv: ${src}: No such file or directory`);
4863
5749
  }
4864
- const srcIndex = srcParent.children.findIndex(
4865
- (c) => c.name === srcNode.name
4866
- );
5750
+ if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
5751
+ throw new Error(`mv: '${src}' and '${dest}' are the same file`);
5752
+ }
4867
5753
  if (destNode && destNode.type === "directory") {
4868
5754
  if (!destNode.children) {
4869
5755
  destNode.children = [];
@@ -4874,7 +5760,7 @@ var ServerFileSystemService = class {
4874
5760
  if (existingIdx !== -1) {
4875
5761
  destNode.children.splice(existingIdx, 1);
4876
5762
  }
4877
- srcParent.children.splice(srcIndex, 1);
5763
+ srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
4878
5764
  destNode.children.push(srcNode);
4879
5765
  destNode.modifiedAt = Date.now();
4880
5766
  } else {
@@ -4887,7 +5773,7 @@ var ServerFileSystemService = class {
4887
5773
  `mv: ${destParentPath}: No such file or directory`
4888
5774
  );
4889
5775
  }
4890
- srcParent.children.splice(srcIndex, 1);
5776
+ srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
4891
5777
  srcNode.name = newName;
4892
5778
  if (!destParent.children) {
4893
5779
  destParent.children = [];
@@ -5251,7 +6137,26 @@ var FILE_COMMANDS = /* @__PURE__ */ new Set([
5251
6137
  "find",
5252
6138
  "head",
5253
6139
  "tail",
5254
- "wc"
6140
+ "wc",
6141
+ "sort",
6142
+ "uniq",
6143
+ "cut",
6144
+ "paste",
6145
+ "tac",
6146
+ "diff",
6147
+ "sed",
6148
+ "awk",
6149
+ "du",
6150
+ "stat",
6151
+ "chmod",
6152
+ "chown",
6153
+ "ln",
6154
+ "tee",
6155
+ "basename",
6156
+ "dirname",
6157
+ "sh",
6158
+ "source",
6159
+ "xargs"
5255
6160
  ]);
5256
6161
  var FilePathCompletionProvider = class {
5257
6162
  constructor(fs) {
@@ -5270,7 +6175,14 @@ var FilePathCompletionProvider = class {
5270
6175
  if (token.startsWith("-")) {
5271
6176
  return [];
5272
6177
  }
5273
- return this.completePath(token);
6178
+ const quote = token.startsWith('"') || token.startsWith("'") ? token[0] : "";
6179
+ const partial = quote ? token.slice(1).replace(new RegExp(`${quote}$`), "") : token;
6180
+ const paths = this.completePath(partial);
6181
+ if (!quote && !paths.some((p) => /\s/.test(p))) {
6182
+ return paths;
6183
+ }
6184
+ const q = quote || '"';
6185
+ return paths.map((p) => p.endsWith("/") ? `${q}${p}` : `${q}${p}${q}`);
5274
6186
  }
5275
6187
  completePath(partial) {
5276
6188
  let dirPath;