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