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