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