@qodalis/cli-files 2.0.0-beta.9 → 2.0.0-rc.2
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 +225 -58
- package/public-api.d.ts +225 -58
- package/public-api.js +2114 -901
- package/public-api.js.map +1 -1
- package/public-api.mjs +2109 -903
- package/public-api.mjs.map +1 -1
- package/umd/index.global.js +1225 -414
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-
|
|
9
|
+
var LIBRARY_VERSION = "2.0.0-rc.2";
|
|
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
|
-
const fs = context.services.
|
|
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
|
-
const fs = context.services.
|
|
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,8 +182,24 @@ 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
|
-
const fs = context.services.
|
|
202
|
+
const fs = context.services.getRequired(
|
|
132
203
|
IFileSystemService_TOKEN
|
|
133
204
|
);
|
|
134
205
|
context.writer.writeln(fs.getCurrentDirectory());
|
|
@@ -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
|
-
const fs = context.services.
|
|
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
|
-
const fs = context.services.
|
|
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
|
-
const fs = context.services.
|
|
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
|
-
const fs = context.services.
|
|
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
|
-
const fs = context.services.
|
|
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,11 +587,37 @@ 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
|
-
const fs = context.services.
|
|
620
|
+
const fs = context.services.getRequired(
|
|
417
621
|
IFileSystemService_TOKEN
|
|
418
622
|
);
|
|
419
623
|
const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
|
|
@@ -425,18 +629,26 @@ var CliCpCommandProcessor = class {
|
|
|
425
629
|
);
|
|
426
630
|
return;
|
|
427
631
|
}
|
|
428
|
-
const
|
|
429
|
-
const
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
}
|
|
434
|
-
|
|
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;
|
|
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
|
+
}
|
|
435
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,8 +660,26 @@ 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
|
-
const fs = context.services.
|
|
682
|
+
const fs = context.services.getRequired(
|
|
453
683
|
IFileSystemService_TOKEN
|
|
454
684
|
);
|
|
455
685
|
const parts = this.parseArgs(command);
|
|
@@ -458,20 +688,36 @@ 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
|
-
} catch (e) {
|
|
467
|
-
context.writer.writeError(e.message);
|
|
691
|
+
const dest = parts[parts.length - 1];
|
|
692
|
+
const sources = parts.slice(0, -1);
|
|
693
|
+
if (sources.length > 1 && !fs.isDirectory(dest)) {
|
|
694
|
+
context.writer.writeError(`mv: target '${dest}' is not a directory`);
|
|
695
|
+
return;
|
|
468
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
|
+
}
|
|
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
|
};
|
|
713
|
+
function readableChildren(fs, path) {
|
|
714
|
+
try {
|
|
715
|
+
return fs.listDirectory(path);
|
|
716
|
+
} catch (e) {
|
|
717
|
+
if (cliCore.isCliAccessDeniedError(e)) return null;
|
|
718
|
+
throw e;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
475
721
|
var CliTreeCommandProcessor = class {
|
|
476
722
|
constructor() {
|
|
477
723
|
this.command = "tree";
|
|
@@ -490,11 +736,29 @@ var CliTreeCommandProcessor = class {
|
|
|
490
736
|
}
|
|
491
737
|
];
|
|
492
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
|
+
}
|
|
493
757
|
async processCommand(command, context) {
|
|
494
|
-
const fs = context.services.
|
|
758
|
+
const fs = context.services.getRequired(
|
|
495
759
|
IFileSystemService_TOKEN
|
|
496
760
|
);
|
|
497
|
-
const targetPath = command.value || fs.getCurrentDirectory();
|
|
761
|
+
const targetPath = splitArgs(command.value || "").join(" ") || fs.getCurrentDirectory();
|
|
498
762
|
const maxDepth = command.args["depth"] ? parseInt(command.args["depth"]) : command.args["L"] ? parseInt(command.args["L"]) : Infinity;
|
|
499
763
|
try {
|
|
500
764
|
const node = fs.getNode(targetPath);
|
|
@@ -518,7 +782,8 @@ var CliTreeCommandProcessor = class {
|
|
|
518
782
|
)
|
|
519
783
|
);
|
|
520
784
|
const counts = { dirs: 0, files: 0 };
|
|
521
|
-
this.
|
|
785
|
+
this.fs = fs;
|
|
786
|
+
this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
|
|
522
787
|
context.writer.writeln();
|
|
523
788
|
context.writer.writeln(
|
|
524
789
|
`${counts.dirs} directories, ${counts.files} files`
|
|
@@ -527,11 +792,15 @@ var CliTreeCommandProcessor = class {
|
|
|
527
792
|
context.writer.writeError(e.message);
|
|
528
793
|
}
|
|
529
794
|
}
|
|
530
|
-
printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
|
|
531
|
-
if (
|
|
795
|
+
printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
|
|
796
|
+
if (depth >= maxDepth) {
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
const visible = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
800
|
+
if (!visible) {
|
|
532
801
|
return;
|
|
533
802
|
}
|
|
534
|
-
const children = [...
|
|
803
|
+
const children = [...visible].sort((a, b) => {
|
|
535
804
|
if (a.type !== b.type) {
|
|
536
805
|
return a.type === "directory" ? -1 : 1;
|
|
537
806
|
}
|
|
@@ -556,7 +825,8 @@ var CliTreeCommandProcessor = class {
|
|
|
556
825
|
depth + 1,
|
|
557
826
|
maxDepth,
|
|
558
827
|
context,
|
|
559
|
-
counts
|
|
828
|
+
counts,
|
|
829
|
+
path === "/" ? `/${child.name}` : `${path}/${child.name}`
|
|
560
830
|
);
|
|
561
831
|
} else {
|
|
562
832
|
counts.files++;
|
|
@@ -583,21 +853,40 @@ var CliHeadCommandProcessor = class {
|
|
|
583
853
|
}
|
|
584
854
|
];
|
|
585
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
|
+
}
|
|
586
875
|
async processCommand(command, context) {
|
|
587
|
-
const fs = context.services.
|
|
876
|
+
const fs = context.services.getRequired(
|
|
588
877
|
IFileSystemService_TOKEN
|
|
589
878
|
);
|
|
590
|
-
const
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
);
|
|
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);
|
|
594
882
|
const paths = this.parsePaths(command);
|
|
595
883
|
if (paths.length === 0) {
|
|
596
884
|
if (command.data != null) {
|
|
597
|
-
const
|
|
598
|
-
const lines = content.split("\n");
|
|
885
|
+
const lines = splitLines(pipedText(command.data));
|
|
599
886
|
const selected = lines.slice(0, count);
|
|
600
|
-
|
|
887
|
+
if (selected.length > 0) {
|
|
888
|
+
context.writer.writeln(selected.join("\n"));
|
|
889
|
+
}
|
|
601
890
|
return;
|
|
602
891
|
}
|
|
603
892
|
context.writer.writeError("head: missing file operand");
|
|
@@ -616,9 +905,11 @@ var CliHeadCommandProcessor = class {
|
|
|
616
905
|
if (i > 0) context.writer.writeln();
|
|
617
906
|
context.writer.writeln(`==> ${path} <==`);
|
|
618
907
|
}
|
|
619
|
-
const lines = content
|
|
908
|
+
const lines = splitLines(content);
|
|
620
909
|
const selected = lines.slice(0, count);
|
|
621
|
-
|
|
910
|
+
if (selected.length > 0) {
|
|
911
|
+
context.writer.writeln(selected.join("\n"));
|
|
912
|
+
}
|
|
622
913
|
} catch (e) {
|
|
623
914
|
context.writer.writeError(`head: ${e.message}`);
|
|
624
915
|
}
|
|
@@ -626,7 +917,7 @@ var CliHeadCommandProcessor = class {
|
|
|
626
917
|
}
|
|
627
918
|
parsePaths(command) {
|
|
628
919
|
const raw = command.value || "";
|
|
629
|
-
return raw
|
|
920
|
+
return splitArgs(raw).filter(Boolean);
|
|
630
921
|
}
|
|
631
922
|
};
|
|
632
923
|
var CliTailCommandProcessor = class {
|
|
@@ -648,21 +939,40 @@ var CliTailCommandProcessor = class {
|
|
|
648
939
|
}
|
|
649
940
|
];
|
|
650
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
|
+
}
|
|
651
961
|
async processCommand(command, context) {
|
|
652
|
-
const fs = context.services.
|
|
962
|
+
const fs = context.services.getRequired(
|
|
653
963
|
IFileSystemService_TOKEN
|
|
654
964
|
);
|
|
655
|
-
const
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
);
|
|
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);
|
|
659
968
|
const paths = this.parsePaths(command);
|
|
660
969
|
if (paths.length === 0) {
|
|
661
970
|
if (command.data != null) {
|
|
662
|
-
const
|
|
663
|
-
const
|
|
664
|
-
|
|
665
|
-
|
|
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
|
+
}
|
|
666
976
|
return;
|
|
667
977
|
}
|
|
668
978
|
context.writer.writeError("tail: missing file operand");
|
|
@@ -681,9 +991,11 @@ var CliTailCommandProcessor = class {
|
|
|
681
991
|
if (i > 0) context.writer.writeln();
|
|
682
992
|
context.writer.writeln(`==> ${path} <==`);
|
|
683
993
|
}
|
|
684
|
-
const lines = content
|
|
685
|
-
const selected = lines.slice(-count);
|
|
686
|
-
|
|
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
|
+
}
|
|
687
999
|
} catch (e) {
|
|
688
1000
|
context.writer.writeError(`tail: ${e.message}`);
|
|
689
1001
|
}
|
|
@@ -691,7 +1003,7 @@ var CliTailCommandProcessor = class {
|
|
|
691
1003
|
}
|
|
692
1004
|
parsePaths(command) {
|
|
693
1005
|
const raw = command.value || "";
|
|
694
|
-
return raw
|
|
1006
|
+
return splitArgs(raw).filter(Boolean);
|
|
695
1007
|
}
|
|
696
1008
|
};
|
|
697
1009
|
var CliWcCommandProcessor = class {
|
|
@@ -727,8 +1039,30 @@ var CliWcCommandProcessor = class {
|
|
|
727
1039
|
}
|
|
728
1040
|
];
|
|
729
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
|
+
}
|
|
730
1064
|
async processCommand(command, context) {
|
|
731
|
-
const fs = context.services.
|
|
1065
|
+
const fs = context.services.getRequired(
|
|
732
1066
|
IFileSystemService_TOKEN
|
|
733
1067
|
);
|
|
734
1068
|
const showLines = command.args["lines"] || command.args["l"] || false;
|
|
@@ -738,10 +1072,9 @@ var CliWcCommandProcessor = class {
|
|
|
738
1072
|
const paths = this.parsePaths(command);
|
|
739
1073
|
if (paths.length === 0) {
|
|
740
1074
|
if (command.data != null) {
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
const
|
|
744
|
-
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);
|
|
745
1078
|
const parts = [];
|
|
746
1079
|
if (showAll || showLines) parts.push(String(lineCount).padStart(8));
|
|
747
1080
|
if (showAll || showWords) parts.push(String(wordCount).padStart(8));
|
|
@@ -762,9 +1095,7 @@ var CliWcCommandProcessor = class {
|
|
|
762
1095
|
continue;
|
|
763
1096
|
}
|
|
764
1097
|
const content = fs.readFile(path) ?? "";
|
|
765
|
-
const lineCount
|
|
766
|
-
const wordCount = content.trim() === "" ? 0 : content.trim().split(/\s+/).length;
|
|
767
|
-
const charCount = content.length;
|
|
1098
|
+
const { lineCount, wordCount, charCount } = this.count(content);
|
|
768
1099
|
totalLines += lineCount;
|
|
769
1100
|
totalWords += wordCount;
|
|
770
1101
|
totalChars += charCount;
|
|
@@ -787,9 +1118,17 @@ var CliWcCommandProcessor = class {
|
|
|
787
1118
|
context.writer.writeln(parts.join(""));
|
|
788
1119
|
}
|
|
789
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
|
+
}
|
|
790
1129
|
parsePaths(command) {
|
|
791
1130
|
const raw = command.value || "";
|
|
792
|
-
return raw
|
|
1131
|
+
return splitArgs(raw).filter(Boolean);
|
|
793
1132
|
}
|
|
794
1133
|
};
|
|
795
1134
|
var CliFindCommandProcessor = class {
|
|
@@ -803,7 +1142,13 @@ var CliFindCommandProcessor = class {
|
|
|
803
1142
|
this.parameters = [
|
|
804
1143
|
{
|
|
805
1144
|
name: "name",
|
|
806
|
-
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",
|
|
807
1152
|
required: false,
|
|
808
1153
|
type: "string"
|
|
809
1154
|
},
|
|
@@ -815,20 +1160,42 @@ var CliFindCommandProcessor = class {
|
|
|
815
1160
|
},
|
|
816
1161
|
{
|
|
817
1162
|
name: "maxdepth",
|
|
818
|
-
description: "Maximum
|
|
1163
|
+
description: "Maximum depth: 0 is only the start path, 1 its direct children",
|
|
819
1164
|
required: false,
|
|
820
1165
|
type: "number"
|
|
821
1166
|
}
|
|
822
1167
|
];
|
|
823
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
|
+
}
|
|
824
1190
|
async processCommand(command, context) {
|
|
825
|
-
const fs = context.services.
|
|
1191
|
+
const fs = context.services.getRequired(
|
|
826
1192
|
IFileSystemService_TOKEN
|
|
827
1193
|
);
|
|
828
1194
|
const searchPath = this.parseSearchPath(command) || fs.getCurrentDirectory();
|
|
829
|
-
const namePattern = command.args["name"] || null;
|
|
830
1195
|
const typeFilter = command.args["type"] || null;
|
|
831
|
-
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;
|
|
832
1199
|
try {
|
|
833
1200
|
const node = fs.getNode(searchPath);
|
|
834
1201
|
if (!node) {
|
|
@@ -837,24 +1204,25 @@ var CliFindCommandProcessor = class {
|
|
|
837
1204
|
);
|
|
838
1205
|
return;
|
|
839
1206
|
}
|
|
840
|
-
if (node.type !== "directory") {
|
|
841
|
-
context.writer.writeError(
|
|
842
|
-
`find: '${searchPath}': Not a directory`
|
|
843
|
-
);
|
|
844
|
-
return;
|
|
845
|
-
}
|
|
846
1207
|
const resolvedBase = fs.resolvePath(searchPath);
|
|
847
|
-
const nameRegex =
|
|
1208
|
+
const nameRegex = this.nameRegex(command);
|
|
848
1209
|
const results = [];
|
|
849
|
-
this.
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1210
|
+
this.fs = fs;
|
|
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
|
+
}
|
|
858
1226
|
for (const r of results) {
|
|
859
1227
|
context.writer.writeln(r);
|
|
860
1228
|
}
|
|
@@ -862,31 +1230,41 @@ var CliFindCommandProcessor = class {
|
|
|
862
1230
|
context.writer.writeError(`find: ${e.message}`);
|
|
863
1231
|
}
|
|
864
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
|
+
}
|
|
865
1251
|
parseSearchPath(command) {
|
|
866
1252
|
const raw = command.value || "";
|
|
867
|
-
const tokens = raw
|
|
1253
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
868
1254
|
return tokens.length > 0 ? tokens[0] : null;
|
|
869
1255
|
}
|
|
870
|
-
globToRegex(pattern) {
|
|
1256
|
+
globToRegex(pattern, ignoreCase) {
|
|
871
1257
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
872
1258
|
const regexStr = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
873
|
-
return new RegExp(`^${regexStr}$`, "i");
|
|
1259
|
+
return new RegExp(`^${regexStr}$`, ignoreCase ? "i" : "");
|
|
874
1260
|
}
|
|
1261
|
+
/** Lists the children of `node`, which are at `depth` (1 = direct children). */
|
|
875
1262
|
walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
|
|
876
|
-
|
|
877
|
-
|
|
1263
|
+
const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
|
|
1264
|
+
if (!children) return;
|
|
1265
|
+
for (const child of children) {
|
|
878
1266
|
const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
|
|
879
|
-
|
|
880
|
-
if (nameRegex && !nameRegex.test(child.name)) {
|
|
881
|
-
matches = false;
|
|
882
|
-
}
|
|
883
|
-
if (typeFilter === "f" && child.type !== "file") {
|
|
884
|
-
matches = false;
|
|
885
|
-
}
|
|
886
|
-
if (typeFilter === "d" && child.type !== "directory") {
|
|
887
|
-
matches = false;
|
|
888
|
-
}
|
|
889
|
-
if (matches) {
|
|
1267
|
+
if (this.matches(child.name, child.type, nameRegex, typeFilter)) {
|
|
890
1268
|
results.push(childPath);
|
|
891
1269
|
}
|
|
892
1270
|
if (child.type === "directory" && depth < maxDepth) {
|
|
@@ -940,6 +1318,13 @@ var CliGrepCommandProcessor = class {
|
|
|
940
1318
|
required: false,
|
|
941
1319
|
type: "boolean"
|
|
942
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
|
+
},
|
|
943
1328
|
{
|
|
944
1329
|
name: "files-with-matches",
|
|
945
1330
|
aliases: ["l"],
|
|
@@ -953,11 +1338,52 @@ var CliGrepCommandProcessor = class {
|
|
|
953
1338
|
description: "Select non-matching lines",
|
|
954
1339
|
required: false,
|
|
955
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)",
|
|
1360
|
+
required: false,
|
|
1361
|
+
type: "boolean"
|
|
956
1362
|
}
|
|
957
1363
|
];
|
|
958
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
|
+
}
|
|
959
1385
|
async processCommand(command, context) {
|
|
960
|
-
const fs = context.services.
|
|
1386
|
+
const fs = context.services.getRequired(
|
|
961
1387
|
IFileSystemService_TOKEN
|
|
962
1388
|
);
|
|
963
1389
|
const ignoreCase = command.args["ignore-case"] || command.args["i"] || false;
|
|
@@ -966,51 +1392,56 @@ var CliGrepCommandProcessor = class {
|
|
|
966
1392
|
const countOnly = command.args["count"] || command.args["c"] || false;
|
|
967
1393
|
const filesOnly = command.args["files-with-matches"] || command.args["l"] || false;
|
|
968
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;
|
|
969
1398
|
const { pattern, paths } = this.parseArgs(command);
|
|
970
1399
|
if (!pattern) {
|
|
971
1400
|
context.writer.writeError(
|
|
972
1401
|
"grep: missing pattern. Usage: grep [options] <pattern> <file>"
|
|
973
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 });
|
|
974
1413
|
return;
|
|
975
1414
|
}
|
|
976
1415
|
if (paths.length === 0) {
|
|
977
1416
|
if (command.data != null) {
|
|
978
|
-
let regex2;
|
|
979
|
-
try {
|
|
980
|
-
regex2 = new RegExp(pattern, ignoreCase ? "gi" : "g");
|
|
981
|
-
} catch {
|
|
982
|
-
context.writer.writeError(`grep: invalid pattern '${pattern}'`);
|
|
983
|
-
return;
|
|
984
|
-
}
|
|
985
1417
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
986
|
-
this.grepContent(content, null,
|
|
1418
|
+
const found2 = this.grepContent(content, null, regex, {
|
|
987
1419
|
ignoreCase,
|
|
988
1420
|
showLineNum,
|
|
989
1421
|
countOnly,
|
|
990
1422
|
filesOnly,
|
|
991
|
-
invert
|
|
1423
|
+
invert,
|
|
1424
|
+
onlyMatching,
|
|
1425
|
+
quiet
|
|
992
1426
|
}, context, false);
|
|
1427
|
+
this.exitStatus(context, found2, false);
|
|
993
1428
|
return;
|
|
994
1429
|
}
|
|
995
1430
|
context.writer.writeError(
|
|
996
1431
|
"grep: missing file operand. Usage: grep [options] <pattern> <file>"
|
|
997
1432
|
);
|
|
998
|
-
|
|
999
|
-
}
|
|
1000
|
-
let regex;
|
|
1001
|
-
try {
|
|
1002
|
-
regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
|
|
1003
|
-
} catch {
|
|
1004
|
-
context.writer.writeError(`grep: invalid pattern '${pattern}'`);
|
|
1433
|
+
context.process.exit(2, { silent: true });
|
|
1005
1434
|
return;
|
|
1006
1435
|
}
|
|
1007
1436
|
const filePaths = [];
|
|
1437
|
+
let failed = false;
|
|
1008
1438
|
for (const p of paths) {
|
|
1009
1439
|
try {
|
|
1010
1440
|
if (!fs.exists(p)) {
|
|
1011
1441
|
context.writer.writeError(
|
|
1012
1442
|
`grep: ${p}: No such file or directory`
|
|
1013
1443
|
);
|
|
1444
|
+
failed = true;
|
|
1014
1445
|
continue;
|
|
1015
1446
|
}
|
|
1016
1447
|
if (fs.isDirectory(p)) {
|
|
@@ -1018,6 +1449,7 @@ var CliGrepCommandProcessor = class {
|
|
|
1018
1449
|
context.writer.writeError(
|
|
1019
1450
|
`grep: ${p}: Is a directory`
|
|
1020
1451
|
);
|
|
1452
|
+
failed = true;
|
|
1021
1453
|
continue;
|
|
1022
1454
|
}
|
|
1023
1455
|
this.collectFiles(fs, fs.resolvePath(p), fs.getNode(p), filePaths);
|
|
@@ -1026,26 +1458,40 @@ var CliGrepCommandProcessor = class {
|
|
|
1026
1458
|
}
|
|
1027
1459
|
} catch (e) {
|
|
1028
1460
|
context.writer.writeError(`grep: ${e.message}`);
|
|
1461
|
+
failed = true;
|
|
1029
1462
|
}
|
|
1030
1463
|
}
|
|
1031
1464
|
const multiFile = filePaths.length > 1;
|
|
1465
|
+
let found = 0;
|
|
1032
1466
|
for (const filePath of filePaths) {
|
|
1033
1467
|
try {
|
|
1034
1468
|
const content = fs.readFile(filePath) ?? "";
|
|
1035
|
-
this.grepContent(content, filePath, regex, {
|
|
1469
|
+
found += this.grepContent(content, filePath, regex, {
|
|
1036
1470
|
ignoreCase,
|
|
1037
1471
|
showLineNum,
|
|
1038
1472
|
countOnly,
|
|
1039
1473
|
filesOnly,
|
|
1040
|
-
invert
|
|
1474
|
+
invert,
|
|
1475
|
+
onlyMatching,
|
|
1476
|
+
quiet
|
|
1041
1477
|
}, context, multiFile);
|
|
1042
1478
|
} catch (e) {
|
|
1043
1479
|
context.writer.writeError(`grep: ${filePath}: ${e.message}`);
|
|
1480
|
+
failed = true;
|
|
1044
1481
|
}
|
|
1045
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 });
|
|
1046
1492
|
}
|
|
1047
1493
|
grepContent(content, filePath, regex, options, context, multiFile) {
|
|
1048
|
-
const lines = content
|
|
1494
|
+
const lines = splitLines(content);
|
|
1049
1495
|
let matchCount = 0;
|
|
1050
1496
|
const matchingLines = [];
|
|
1051
1497
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
@@ -1058,6 +1504,7 @@ var CliGrepCommandProcessor = class {
|
|
|
1058
1504
|
matchingLines.push({ num: lineIdx + 1, text: line });
|
|
1059
1505
|
}
|
|
1060
1506
|
}
|
|
1507
|
+
if (options.quiet) return matchCount;
|
|
1061
1508
|
if (options.filesOnly) {
|
|
1062
1509
|
if (matchCount > 0 && filePath) {
|
|
1063
1510
|
context.writer.writeln(filePath);
|
|
@@ -1076,6 +1523,18 @@ var CliGrepCommandProcessor = class {
|
|
|
1076
1523
|
parts.push(context.writer.wrapInColor(String(m.num), cliCore.CliForegroundColor.Green));
|
|
1077
1524
|
parts.push(":");
|
|
1078
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
|
+
}
|
|
1079
1538
|
if (!options.invert) {
|
|
1080
1539
|
regex.lastIndex = 0;
|
|
1081
1540
|
const highlighted = m.text.replace(
|
|
@@ -1089,10 +1548,11 @@ var CliGrepCommandProcessor = class {
|
|
|
1089
1548
|
context.writer.writeln(parts.join(""));
|
|
1090
1549
|
}
|
|
1091
1550
|
}
|
|
1551
|
+
return matchCount;
|
|
1092
1552
|
}
|
|
1093
1553
|
parseArgs(command) {
|
|
1094
1554
|
const raw = command.value || "";
|
|
1095
|
-
const tokens = raw
|
|
1555
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
1096
1556
|
if (tokens.length === 0) {
|
|
1097
1557
|
return { pattern: null, paths: [] };
|
|
1098
1558
|
}
|
|
@@ -1105,8 +1565,9 @@ var CliGrepCommandProcessor = class {
|
|
|
1105
1565
|
};
|
|
1106
1566
|
}
|
|
1107
1567
|
collectFiles(fs, basePath, node, result) {
|
|
1108
|
-
|
|
1109
|
-
|
|
1568
|
+
const children = readableChildren(fs, basePath);
|
|
1569
|
+
if (!children) return;
|
|
1570
|
+
for (const child of children) {
|
|
1110
1571
|
const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
|
|
1111
1572
|
if (child.type === "file") {
|
|
1112
1573
|
result.push(childPath);
|
|
@@ -1126,8 +1587,25 @@ var CliTacCommandProcessor = class {
|
|
|
1126
1587
|
this.valueRequired = true;
|
|
1127
1588
|
this.metadata = { icon: "\u{1F504}", module: "file management" };
|
|
1128
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
|
+
}
|
|
1129
1607
|
async processCommand(command, context) {
|
|
1130
|
-
const fs = context.services.
|
|
1608
|
+
const fs = context.services.getRequired(
|
|
1131
1609
|
IFileSystemService_TOKEN
|
|
1132
1610
|
);
|
|
1133
1611
|
const paths = this.parsePaths(command);
|
|
@@ -1171,7 +1649,7 @@ var CliTacCommandProcessor = class {
|
|
|
1171
1649
|
}
|
|
1172
1650
|
parsePaths(command) {
|
|
1173
1651
|
const raw = command.value || "";
|
|
1174
|
-
return raw
|
|
1652
|
+
return splitArgs(raw).filter(Boolean);
|
|
1175
1653
|
}
|
|
1176
1654
|
};
|
|
1177
1655
|
var CliBasenameCommandProcessor = class {
|
|
@@ -1184,6 +1662,24 @@ var CliBasenameCommandProcessor = class {
|
|
|
1184
1662
|
this.valueRequired = true;
|
|
1185
1663
|
this.metadata = { icon: "\u{1F4DB}", module: "file management" };
|
|
1186
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
|
+
}
|
|
1187
1683
|
async processCommand(command, context) {
|
|
1188
1684
|
const tokens = this.parseTokens(command);
|
|
1189
1685
|
if (tokens.length === 0) {
|
|
@@ -1209,7 +1705,7 @@ var CliBasenameCommandProcessor = class {
|
|
|
1209
1705
|
}
|
|
1210
1706
|
parseTokens(command) {
|
|
1211
1707
|
const raw = command.value || "";
|
|
1212
|
-
return raw
|
|
1708
|
+
return splitArgs(raw).filter(Boolean);
|
|
1213
1709
|
}
|
|
1214
1710
|
};
|
|
1215
1711
|
var CliDirnameCommandProcessor = class {
|
|
@@ -1222,6 +1718,23 @@ var CliDirnameCommandProcessor = class {
|
|
|
1222
1718
|
this.valueRequired = true;
|
|
1223
1719
|
this.metadata = { icon: "\u{1F4C2}", module: "file management" };
|
|
1224
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
|
+
}
|
|
1225
1738
|
async processCommand(command, context) {
|
|
1226
1739
|
const tokens = this.parseTokens(command);
|
|
1227
1740
|
if (tokens.length === 0) {
|
|
@@ -1248,7 +1761,7 @@ var CliDirnameCommandProcessor = class {
|
|
|
1248
1761
|
}
|
|
1249
1762
|
parseTokens(command) {
|
|
1250
1763
|
const raw = command.value || "";
|
|
1251
|
-
return raw
|
|
1764
|
+
return splitArgs(raw).filter(Boolean);
|
|
1252
1765
|
}
|
|
1253
1766
|
};
|
|
1254
1767
|
var CliSortCommandProcessor = class {
|
|
@@ -1298,117 +1811,97 @@ var CliSortCommandProcessor = class {
|
|
|
1298
1811
|
}
|
|
1299
1812
|
];
|
|
1300
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
|
+
}
|
|
1301
1837
|
async processCommand(command, context) {
|
|
1302
|
-
const fs = context.services.
|
|
1838
|
+
const fs = context.services.getRequired(
|
|
1303
1839
|
IFileSystemService_TOKEN
|
|
1304
1840
|
);
|
|
1305
1841
|
const reverse = !!command.args["r"] || !!command.args["reverse"];
|
|
1306
1842
|
const numeric = !!command.args["n"] || !!command.args["numeric-sort"];
|
|
1307
1843
|
const unique = !!command.args["u"] || !!command.args["unique"];
|
|
1308
|
-
const keyField = command.args["k"]
|
|
1309
|
-
const delimiter = command.args["t"]
|
|
1844
|
+
const keyField = command.args["k"] ?? command.args["key"];
|
|
1845
|
+
const delimiter = command.args["t"] ?? command.args["field-separator"];
|
|
1310
1846
|
const paths = this.parsePaths(command);
|
|
1847
|
+
let lines = [];
|
|
1311
1848
|
if (paths.length === 0) {
|
|
1312
|
-
if (command.data
|
|
1313
|
-
|
|
1314
|
-
let lines = content.split("\n");
|
|
1315
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1316
|
-
lines.pop();
|
|
1317
|
-
}
|
|
1318
|
-
const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
|
|
1319
|
-
lines.sort((a, b) => {
|
|
1320
|
-
let va = a;
|
|
1321
|
-
let vb = b;
|
|
1322
|
-
if (keyIndex !== void 0 && keyIndex >= 0) {
|
|
1323
|
-
const sepRegex = delimiter ? new RegExp(
|
|
1324
|
-
delimiter.replace(
|
|
1325
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1326
|
-
"\\$&"
|
|
1327
|
-
)
|
|
1328
|
-
) : /\s+/;
|
|
1329
|
-
const fieldsA = a.split(sepRegex);
|
|
1330
|
-
const fieldsB = b.split(sepRegex);
|
|
1331
|
-
va = fieldsA[keyIndex] || "";
|
|
1332
|
-
vb = fieldsB[keyIndex] || "";
|
|
1333
|
-
}
|
|
1334
|
-
if (numeric) {
|
|
1335
|
-
const na = parseFloat(va) || 0;
|
|
1336
|
-
const nb = parseFloat(vb) || 0;
|
|
1337
|
-
return na - nb;
|
|
1338
|
-
}
|
|
1339
|
-
return va.localeCompare(vb);
|
|
1340
|
-
});
|
|
1341
|
-
if (unique) {
|
|
1342
|
-
lines = lines.filter(
|
|
1343
|
-
(line, i, arr) => i === 0 || line !== arr[i - 1]
|
|
1344
|
-
);
|
|
1345
|
-
}
|
|
1346
|
-
if (reverse) {
|
|
1347
|
-
lines.reverse();
|
|
1348
|
-
}
|
|
1349
|
-
context.writer.writeln(lines.join("\n"));
|
|
1849
|
+
if (command.data == null) {
|
|
1850
|
+
context.writer.writeError("sort: missing file operand");
|
|
1350
1851
|
return;
|
|
1351
1852
|
}
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
const content = fs.readFile(path);
|
|
1362
|
-
if (content === null) {
|
|
1363
|
-
context.writer.writeError(
|
|
1364
|
-
`sort: ${path}: No such file or directory`
|
|
1365
|
-
);
|
|
1366
|
-
continue;
|
|
1367
|
-
}
|
|
1368
|
-
let lines = content.split("\n");
|
|
1369
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1370
|
-
lines.pop();
|
|
1371
|
-
}
|
|
1372
|
-
const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
|
|
1373
|
-
lines.sort((a, b) => {
|
|
1374
|
-
let va = a;
|
|
1375
|
-
let vb = b;
|
|
1376
|
-
if (keyIndex !== void 0 && keyIndex >= 0) {
|
|
1377
|
-
const sepRegex = delimiter ? new RegExp(
|
|
1378
|
-
delimiter.replace(
|
|
1379
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1380
|
-
"\\$&"
|
|
1381
|
-
)
|
|
1382
|
-
) : /\s+/;
|
|
1383
|
-
const fieldsA = a.split(sepRegex);
|
|
1384
|
-
const fieldsB = b.split(sepRegex);
|
|
1385
|
-
va = fieldsA[keyIndex] || "";
|
|
1386
|
-
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;
|
|
1387
1862
|
}
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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;
|
|
1392
1869
|
}
|
|
1393
|
-
|
|
1394
|
-
})
|
|
1395
|
-
|
|
1396
|
-
lines = lines.filter(
|
|
1397
|
-
(line, i, arr) => i === 0 || line !== arr[i - 1]
|
|
1398
|
-
);
|
|
1870
|
+
lines.push(...splitLines(content));
|
|
1871
|
+
} catch (e) {
|
|
1872
|
+
context.writer.writeError(`sort: ${e.message}`);
|
|
1399
1873
|
}
|
|
1400
|
-
if (reverse) {
|
|
1401
|
-
lines.reverse();
|
|
1402
|
-
}
|
|
1403
|
-
context.writer.writeln(lines.join("\n"));
|
|
1404
|
-
} catch (e) {
|
|
1405
|
-
context.writer.writeError(`sort: ${e.message}`);
|
|
1406
1874
|
}
|
|
1407
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
|
+
}
|
|
1408
1901
|
}
|
|
1409
1902
|
parsePaths(command) {
|
|
1410
1903
|
const raw = command.value || "";
|
|
1411
|
-
return raw
|
|
1904
|
+
return splitArgs(raw).filter(Boolean);
|
|
1412
1905
|
}
|
|
1413
1906
|
};
|
|
1414
1907
|
var CliUniqCommandProcessor = class {
|
|
@@ -1451,8 +1944,26 @@ var CliUniqCommandProcessor = class {
|
|
|
1451
1944
|
}
|
|
1452
1945
|
];
|
|
1453
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
|
+
}
|
|
1454
1965
|
async processCommand(command, context) {
|
|
1455
|
-
const fs = context.services.
|
|
1966
|
+
const fs = context.services.getRequired(
|
|
1456
1967
|
IFileSystemService_TOKEN
|
|
1457
1968
|
);
|
|
1458
1969
|
const showCount = !!command.args["c"] || !!command.args["count"];
|
|
@@ -1463,7 +1974,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1463
1974
|
if (paths.length === 0) {
|
|
1464
1975
|
if (command.data != null) {
|
|
1465
1976
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1466
|
-
|
|
1977
|
+
const lines = content.split("\n");
|
|
1467
1978
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1468
1979
|
lines.pop();
|
|
1469
1980
|
}
|
|
@@ -1511,7 +2022,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1511
2022
|
);
|
|
1512
2023
|
continue;
|
|
1513
2024
|
}
|
|
1514
|
-
|
|
2025
|
+
const lines = content.split("\n");
|
|
1515
2026
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1516
2027
|
lines.pop();
|
|
1517
2028
|
}
|
|
@@ -1546,7 +2057,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1546
2057
|
}
|
|
1547
2058
|
parsePaths(command) {
|
|
1548
2059
|
const raw = command.value || "";
|
|
1549
|
-
return raw
|
|
2060
|
+
return splitArgs(raw).filter(Boolean);
|
|
1550
2061
|
}
|
|
1551
2062
|
};
|
|
1552
2063
|
var CliCutCommandProcessor = class {
|
|
@@ -1582,26 +2093,45 @@ var CliCutCommandProcessor = class {
|
|
|
1582
2093
|
}
|
|
1583
2094
|
];
|
|
1584
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
|
+
}
|
|
1585
2115
|
async processCommand(command, context) {
|
|
1586
|
-
const fs = context.services.
|
|
2116
|
+
const fs = context.services.getRequired(
|
|
1587
2117
|
IFileSystemService_TOKEN
|
|
1588
2118
|
);
|
|
1589
2119
|
const delimiter = command.args["d"] || command.args["delimiter"] || " ";
|
|
1590
|
-
const fieldsSpec = command.args["f"]
|
|
1591
|
-
const charsSpec = command.args["c"]
|
|
1592
|
-
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 === "")) {
|
|
1593
2123
|
context.writer.writeError(
|
|
1594
2124
|
"cut: you must specify either -f (fields) or -c (characters)"
|
|
1595
2125
|
);
|
|
1596
2126
|
return;
|
|
1597
2127
|
}
|
|
1598
|
-
const spec = fieldsSpec
|
|
2128
|
+
const spec = String(fieldsSpec ?? charsSpec);
|
|
1599
2129
|
const indices = this.parseSpec(spec);
|
|
1600
2130
|
const paths = this.parsePaths(command);
|
|
1601
2131
|
if (paths.length === 0) {
|
|
1602
2132
|
if (command.data != null) {
|
|
1603
2133
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1604
|
-
|
|
2134
|
+
const lines = content.split("\n");
|
|
1605
2135
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1606
2136
|
lines.pop();
|
|
1607
2137
|
}
|
|
@@ -1643,7 +2173,7 @@ var CliCutCommandProcessor = class {
|
|
|
1643
2173
|
);
|
|
1644
2174
|
continue;
|
|
1645
2175
|
}
|
|
1646
|
-
|
|
2176
|
+
const lines = content.split("\n");
|
|
1647
2177
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1648
2178
|
lines.pop();
|
|
1649
2179
|
}
|
|
@@ -1703,7 +2233,7 @@ var CliCutCommandProcessor = class {
|
|
|
1703
2233
|
}
|
|
1704
2234
|
parsePaths(command) {
|
|
1705
2235
|
const raw = command.value || "";
|
|
1706
|
-
return raw
|
|
2236
|
+
return splitArgs(raw).filter(Boolean);
|
|
1707
2237
|
}
|
|
1708
2238
|
};
|
|
1709
2239
|
var CliPasteCommandProcessor = class {
|
|
@@ -1732,23 +2262,47 @@ var CliPasteCommandProcessor = class {
|
|
|
1732
2262
|
}
|
|
1733
2263
|
];
|
|
1734
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
|
+
}
|
|
1735
2284
|
async processCommand(command, context) {
|
|
1736
|
-
const fs = context.services.
|
|
2285
|
+
const fs = context.services.getRequired(
|
|
1737
2286
|
IFileSystemService_TOKEN
|
|
1738
2287
|
);
|
|
1739
|
-
const
|
|
2288
|
+
const delimiterArg = command.args["d"] ?? command.args["delimiters"] ?? " ";
|
|
2289
|
+
const delimiters = this.parseDelimiters(String(delimiterArg));
|
|
1740
2290
|
const serial = !!command.args["s"] || !!command.args["serial"];
|
|
1741
2291
|
const paths = this.parsePaths(command);
|
|
2292
|
+
const hasStdin = command.data != null;
|
|
1742
2293
|
if (paths.length === 0) {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
return;
|
|
2294
|
+
if (!hasStdin) {
|
|
2295
|
+
context.writer.writeError("paste: missing file operand");
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2298
|
+
paths.push("-");
|
|
1749
2299
|
}
|
|
1750
2300
|
const fileLines = [];
|
|
1751
2301
|
for (const path of paths) {
|
|
2302
|
+
if (path === "-" && hasStdin) {
|
|
2303
|
+
fileLines.push(splitLines(pipedText(command.data)));
|
|
2304
|
+
continue;
|
|
2305
|
+
}
|
|
1752
2306
|
try {
|
|
1753
2307
|
if (fs.isDirectory(path)) {
|
|
1754
2308
|
context.writer.writeError(
|
|
@@ -1763,20 +2317,18 @@ var CliPasteCommandProcessor = class {
|
|
|
1763
2317
|
);
|
|
1764
2318
|
return;
|
|
1765
2319
|
}
|
|
1766
|
-
|
|
1767
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1768
|
-
lines.pop();
|
|
1769
|
-
}
|
|
1770
|
-
fileLines.push(lines);
|
|
2320
|
+
fileLines.push(splitLines(content));
|
|
1771
2321
|
} catch (e) {
|
|
1772
2322
|
context.writer.writeError(`paste: ${e.message}`);
|
|
1773
2323
|
return;
|
|
1774
2324
|
}
|
|
1775
2325
|
}
|
|
2326
|
+
const join = (parts) => parts.reduce(
|
|
2327
|
+
(line, part, i) => i === 0 ? part : line + delimiters[(i - 1) % delimiters.length] + part,
|
|
2328
|
+
""
|
|
2329
|
+
);
|
|
1776
2330
|
if (serial) {
|
|
1777
|
-
const output = fileLines.map(
|
|
1778
|
-
(lines) => lines.join(delimiter)
|
|
1779
|
-
);
|
|
2331
|
+
const output = fileLines.map((lines) => join(lines));
|
|
1780
2332
|
context.writer.writeln(output.join("\n"));
|
|
1781
2333
|
} else {
|
|
1782
2334
|
const maxLines = Math.max(
|
|
@@ -1784,21 +2336,59 @@ var CliPasteCommandProcessor = class {
|
|
|
1784
2336
|
);
|
|
1785
2337
|
const output = [];
|
|
1786
2338
|
for (let i = 0; i < maxLines; i++) {
|
|
1787
|
-
|
|
1788
|
-
output.push(parts.join(delimiter));
|
|
2339
|
+
output.push(join(fileLines.map((lines) => lines[i] ?? "")));
|
|
1789
2340
|
}
|
|
1790
2341
|
context.writer.writeln(output.join("\n"));
|
|
1791
2342
|
}
|
|
1792
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
|
+
*/
|
|
1793
2363
|
parsePaths(command) {
|
|
1794
|
-
const
|
|
1795
|
-
|
|
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;
|
|
1796
2378
|
}
|
|
1797
2379
|
};
|
|
1798
2380
|
function expandSet(set) {
|
|
1799
2381
|
const chars = [];
|
|
1800
2382
|
let i = 0;
|
|
1801
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
|
+
}
|
|
1802
2392
|
if (set[i] === "[" && set[i + 1] === ":") {
|
|
1803
2393
|
const end = set.indexOf(":]", i + 2);
|
|
1804
2394
|
if (end !== -1) {
|
|
@@ -1808,7 +2398,7 @@ function expandSet(set) {
|
|
|
1808
2398
|
continue;
|
|
1809
2399
|
}
|
|
1810
2400
|
}
|
|
1811
|
-
if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !==
|
|
2401
|
+
if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !== "\\") {
|
|
1812
2402
|
const start = set.charCodeAt(i);
|
|
1813
2403
|
const finish = set.charCodeAt(i + 2);
|
|
1814
2404
|
if (start <= finish) {
|
|
@@ -1878,7 +2468,7 @@ function parseTokens(raw) {
|
|
|
1878
2468
|
var CliTrCommandProcessor = class {
|
|
1879
2469
|
constructor() {
|
|
1880
2470
|
this.command = "tr";
|
|
1881
|
-
this.description = "Translate or delete characters in
|
|
2471
|
+
this.description = "Translate or delete characters in piped input";
|
|
1882
2472
|
this.author = cliCore.DefaultLibraryAuthor;
|
|
1883
2473
|
this.version = LIBRARY_VERSION;
|
|
1884
2474
|
this.acceptsRawInput = true;
|
|
@@ -1886,103 +2476,76 @@ var CliTrCommandProcessor = class {
|
|
|
1886
2476
|
this.metadata = { icon: "\u{1F500}", module: "file management" };
|
|
1887
2477
|
this.parameters = [
|
|
1888
2478
|
{
|
|
1889
|
-
name: "
|
|
2479
|
+
name: "delete",
|
|
2480
|
+
aliases: ["d"],
|
|
1890
2481
|
description: "Delete characters in SET1",
|
|
1891
2482
|
required: false,
|
|
1892
2483
|
type: "boolean"
|
|
1893
2484
|
},
|
|
1894
2485
|
{
|
|
1895
|
-
name: "
|
|
2486
|
+
name: "squeeze-repeats",
|
|
2487
|
+
aliases: ["s"],
|
|
1896
2488
|
description: "Squeeze repeated characters in last operand set",
|
|
1897
2489
|
required: false,
|
|
1898
2490
|
type: "boolean"
|
|
1899
2491
|
}
|
|
1900
2492
|
];
|
|
1901
2493
|
}
|
|
1902
|
-
|
|
1903
|
-
const
|
|
1904
|
-
|
|
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.")
|
|
1905
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) {
|
|
1906
2515
|
const raw = command.value || "";
|
|
1907
2516
|
const positional = parseTokens(raw);
|
|
1908
|
-
const deleteMode = !!command.args["d"];
|
|
1909
|
-
const squeezeMode = !!command.args["s"];
|
|
2517
|
+
const deleteMode = !!command.args["d"] || !!command.args["delete"];
|
|
2518
|
+
const squeezeMode = !!command.args["s"] || !!command.args["squeeze-repeats"];
|
|
1910
2519
|
if (positional.length === 0) {
|
|
1911
|
-
|
|
2520
|
+
this.fail(context, "tr: missing operand");
|
|
1912
2521
|
return;
|
|
1913
2522
|
}
|
|
1914
|
-
let
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
if (deleteMode && squeezeMode) {
|
|
1918
|
-
if (positional.length < 2) {
|
|
1919
|
-
context.writer.writeError("tr: missing operand");
|
|
1920
|
-
return;
|
|
1921
|
-
}
|
|
1922
|
-
set1Str = positional[0];
|
|
1923
|
-
set2Str = positional[1];
|
|
1924
|
-
if (positional.length >= 3) {
|
|
1925
|
-
filePath = positional[2];
|
|
1926
|
-
}
|
|
1927
|
-
} else if (deleteMode) {
|
|
1928
|
-
if (positional.length < 1) {
|
|
1929
|
-
context.writer.writeError("tr: missing operand");
|
|
1930
|
-
return;
|
|
1931
|
-
}
|
|
1932
|
-
set1Str = positional[0];
|
|
1933
|
-
if (positional.length >= 2) {
|
|
1934
|
-
filePath = positional[1];
|
|
1935
|
-
}
|
|
2523
|
+
let setCount;
|
|
2524
|
+
if (deleteMode) {
|
|
2525
|
+
setCount = squeezeMode ? 2 : 1;
|
|
1936
2526
|
} else if (squeezeMode) {
|
|
1937
|
-
|
|
1938
|
-
context.writer.writeError("tr: missing operand");
|
|
1939
|
-
return;
|
|
1940
|
-
}
|
|
1941
|
-
if (positional.length >= 3) {
|
|
1942
|
-
set1Str = positional[0];
|
|
1943
|
-
set2Str = positional[1];
|
|
1944
|
-
filePath = positional[2];
|
|
1945
|
-
} else if (positional.length === 2) {
|
|
1946
|
-
set1Str = positional[0];
|
|
1947
|
-
filePath = positional[1];
|
|
1948
|
-
} else {
|
|
1949
|
-
set1Str = positional[0];
|
|
1950
|
-
}
|
|
2527
|
+
setCount = Math.min(positional.length, 2);
|
|
1951
2528
|
} else {
|
|
1952
|
-
|
|
1953
|
-
context.writer.writeError("tr: missing operand");
|
|
1954
|
-
return;
|
|
1955
|
-
}
|
|
1956
|
-
set1Str = positional[0];
|
|
1957
|
-
set2Str = positional[1];
|
|
1958
|
-
if (positional.length >= 3) {
|
|
1959
|
-
filePath = positional[2];
|
|
1960
|
-
}
|
|
2529
|
+
setCount = 2;
|
|
1961
2530
|
}
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
}
|
|
1976
|
-
} catch (e) {
|
|
1977
|
-
context.writer.writeError(`tr: ${e.message}`);
|
|
1978
|
-
return;
|
|
1979
|
-
}
|
|
1980
|
-
} else if (command.data != null) {
|
|
1981
|
-
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1982
|
-
} else {
|
|
1983
|
-
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]}'`);
|
|
2537
|
+
return;
|
|
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
|
+
);
|
|
1984
2544
|
return;
|
|
1985
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);
|
|
1986
2549
|
try {
|
|
1987
2550
|
const set1 = expandSet(set1Str);
|
|
1988
2551
|
const set1Set = new Set(set1);
|
|
@@ -2005,9 +2568,13 @@ var CliTrCommandProcessor = class {
|
|
|
2005
2568
|
}
|
|
2006
2569
|
context.writer.writeln(result);
|
|
2007
2570
|
} catch (e) {
|
|
2008
|
-
|
|
2571
|
+
this.fail(context, `tr: ${e.message}`);
|
|
2009
2572
|
}
|
|
2010
2573
|
}
|
|
2574
|
+
fail(context, message) {
|
|
2575
|
+
context.writer.writeError(message);
|
|
2576
|
+
context.process.exit(1, { silent: true });
|
|
2577
|
+
}
|
|
2011
2578
|
deleteChars(content, deleteSet) {
|
|
2012
2579
|
let result = "";
|
|
2013
2580
|
for (const ch of content) {
|
|
@@ -2054,8 +2621,24 @@ var CliStatCommandProcessor = class {
|
|
|
2054
2621
|
this.metadata = { icon: "\u{1F4CA}", module: "file management" };
|
|
2055
2622
|
this.parameters = [];
|
|
2056
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
|
+
}
|
|
2057
2640
|
async processCommand(command, context) {
|
|
2058
|
-
const fs = context.services.
|
|
2641
|
+
const fs = context.services.getRequired(
|
|
2059
2642
|
IFileSystemService_TOKEN
|
|
2060
2643
|
);
|
|
2061
2644
|
const paths = this.parsePaths(command);
|
|
@@ -2065,6 +2648,10 @@ var CliStatCommandProcessor = class {
|
|
|
2065
2648
|
}
|
|
2066
2649
|
for (const path of paths) {
|
|
2067
2650
|
const resolved = fs.resolvePath(path);
|
|
2651
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2652
|
+
context.writer.writeError(`stat: cannot stat '${path}': Permission denied`);
|
|
2653
|
+
continue;
|
|
2654
|
+
}
|
|
2068
2655
|
const node = fs.getNode(resolved);
|
|
2069
2656
|
if (!node) {
|
|
2070
2657
|
context.writer.writeError(
|
|
@@ -2086,7 +2673,7 @@ var CliStatCommandProcessor = class {
|
|
|
2086
2673
|
}
|
|
2087
2674
|
parsePaths(command) {
|
|
2088
2675
|
const raw = command.value || "";
|
|
2089
|
-
return raw
|
|
2676
|
+
return splitArgs(raw).filter(Boolean);
|
|
2090
2677
|
}
|
|
2091
2678
|
};
|
|
2092
2679
|
var CliChmodCommandProcessor = class {
|
|
@@ -2107,8 +2694,30 @@ var CliChmodCommandProcessor = class {
|
|
|
2107
2694
|
}
|
|
2108
2695
|
];
|
|
2109
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
|
+
}
|
|
2110
2719
|
async processCommand(command, context) {
|
|
2111
|
-
const fs = context.services.
|
|
2720
|
+
const fs = context.services.getRequired(
|
|
2112
2721
|
IFileSystemService_TOKEN
|
|
2113
2722
|
);
|
|
2114
2723
|
const { recursive, mode, paths } = this.parseArgs(command);
|
|
@@ -2140,7 +2749,7 @@ var CliChmodCommandProcessor = class {
|
|
|
2140
2749
|
}
|
|
2141
2750
|
parseArgs(command) {
|
|
2142
2751
|
const raw = command.value || "";
|
|
2143
|
-
const tokens = raw
|
|
2752
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2144
2753
|
const recursive = command.args["R"] || false;
|
|
2145
2754
|
const mode = tokens.length > 0 ? tokens[0] : null;
|
|
2146
2755
|
const paths = tokens.slice(1);
|
|
@@ -2227,8 +2836,33 @@ var CliChownCommandProcessor = class {
|
|
|
2227
2836
|
}
|
|
2228
2837
|
];
|
|
2229
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
|
+
}
|
|
2230
2864
|
async processCommand(command, context) {
|
|
2231
|
-
const fs = context.services.
|
|
2865
|
+
const fs = context.services.getRequired(
|
|
2232
2866
|
IFileSystemService_TOKEN
|
|
2233
2867
|
);
|
|
2234
2868
|
const { recursive, ownerSpec, paths } = this.parseArgs(command);
|
|
@@ -2260,7 +2894,7 @@ var CliChownCommandProcessor = class {
|
|
|
2260
2894
|
}
|
|
2261
2895
|
parseArgs(command) {
|
|
2262
2896
|
const raw = command.value || "";
|
|
2263
|
-
const tokens = raw
|
|
2897
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2264
2898
|
const recursive = command.args["R"] || false;
|
|
2265
2899
|
const ownerSpec = tokens.length > 0 ? tokens[0] : null;
|
|
2266
2900
|
const paths = tokens.slice(1);
|
|
@@ -2298,7 +2932,6 @@ var CliDuCommandProcessor = class {
|
|
|
2298
2932
|
this.author = cliCore.DefaultLibraryAuthor;
|
|
2299
2933
|
this.version = LIBRARY_VERSION;
|
|
2300
2934
|
this.acceptsRawInput = true;
|
|
2301
|
-
this.valueRequired = true;
|
|
2302
2935
|
this.metadata = { icon: "\u{1F4CF}", module: "file management" };
|
|
2303
2936
|
this.parameters = [
|
|
2304
2937
|
{
|
|
@@ -2322,17 +2955,39 @@ var CliDuCommandProcessor = class {
|
|
|
2322
2955
|
}
|
|
2323
2956
|
];
|
|
2324
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
|
+
}
|
|
2325
2977
|
async processCommand(command, context) {
|
|
2326
|
-
const fs = context.services.
|
|
2978
|
+
const fs = context.services.getRequired(
|
|
2327
2979
|
IFileSystemService_TOKEN
|
|
2328
2980
|
);
|
|
2329
2981
|
const { humanReadable, summaryOnly, maxDepth, paths } = this.parseArgs(command);
|
|
2330
2982
|
if (paths.length === 0) {
|
|
2331
|
-
|
|
2332
|
-
return;
|
|
2983
|
+
paths.push(fs.getCurrentDirectory());
|
|
2333
2984
|
}
|
|
2334
2985
|
for (const path of paths) {
|
|
2335
2986
|
const resolved = fs.resolvePath(path);
|
|
2987
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2988
|
+
context.writer.writeError(`du: cannot access '${path}': Permission denied`);
|
|
2989
|
+
continue;
|
|
2990
|
+
}
|
|
2336
2991
|
const node = fs.getNode(resolved);
|
|
2337
2992
|
if (!node) {
|
|
2338
2993
|
context.writer.writeError(
|
|
@@ -2346,6 +3001,7 @@ var CliDuCommandProcessor = class {
|
|
|
2346
3001
|
);
|
|
2347
3002
|
} else {
|
|
2348
3003
|
const entries = [];
|
|
3004
|
+
this.fs = fs;
|
|
2349
3005
|
this.collectSizes(node, resolved, 0, maxDepth, summaryOnly, entries);
|
|
2350
3006
|
for (const entry of entries) {
|
|
2351
3007
|
context.writer.writeln(
|
|
@@ -2357,10 +3013,12 @@ var CliDuCommandProcessor = class {
|
|
|
2357
3013
|
}
|
|
2358
3014
|
parseArgs(command) {
|
|
2359
3015
|
const raw = command.value || "";
|
|
2360
|
-
const paths = raw
|
|
3016
|
+
const paths = splitArgs(raw).filter(Boolean);
|
|
2361
3017
|
const humanReadable = !!command.args["h"];
|
|
2362
3018
|
const summaryOnly = !!command.args["s"];
|
|
2363
|
-
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;
|
|
2364
3022
|
return { humanReadable, summaryOnly, maxDepth, paths };
|
|
2365
3023
|
}
|
|
2366
3024
|
collectSizes(node, path, depth, maxDepth, summaryOnly, entries) {
|
|
@@ -2368,8 +3026,9 @@ var CliDuCommandProcessor = class {
|
|
|
2368
3026
|
return node.size;
|
|
2369
3027
|
}
|
|
2370
3028
|
let total = 0;
|
|
2371
|
-
|
|
2372
|
-
|
|
3029
|
+
const children = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
3030
|
+
if (children) {
|
|
3031
|
+
for (const child of children) {
|
|
2373
3032
|
const childPath = path === "/" ? `/${child.name}` : `${path}/${child.name}`;
|
|
2374
3033
|
total += this.collectSizes(
|
|
2375
3034
|
child,
|
|
@@ -2416,11 +3075,34 @@ var CliLnCommandProcessor = class {
|
|
|
2416
3075
|
description: "Create a symbolic link",
|
|
2417
3076
|
required: false,
|
|
2418
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"
|
|
2419
3085
|
}
|
|
2420
3086
|
];
|
|
2421
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
|
+
}
|
|
2422
3104
|
async processCommand(command, context) {
|
|
2423
|
-
const fs = context.services.
|
|
3105
|
+
const fs = context.services.getRequired(
|
|
2424
3106
|
IFileSystemService_TOKEN
|
|
2425
3107
|
);
|
|
2426
3108
|
const { symbolic, target, linkName } = this.parseArgs(command);
|
|
@@ -2436,6 +3118,10 @@ var CliLnCommandProcessor = class {
|
|
|
2436
3118
|
}
|
|
2437
3119
|
const resolvedLink = fs.resolvePath(linkName);
|
|
2438
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
|
+
}
|
|
2439
3125
|
if (fs.exists(resolvedLink)) {
|
|
2440
3126
|
context.writer.writeError(
|
|
2441
3127
|
`ln: failed to create symbolic link '${linkName}': File exists`
|
|
@@ -2452,7 +3138,7 @@ var CliLnCommandProcessor = class {
|
|
|
2452
3138
|
}
|
|
2453
3139
|
parseArgs(command) {
|
|
2454
3140
|
const raw = command.value || "";
|
|
2455
|
-
const tokens = raw
|
|
3141
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2456
3142
|
const symbolic = command.args["s"] || false;
|
|
2457
3143
|
return {
|
|
2458
3144
|
symbolic,
|
|
@@ -2487,25 +3173,75 @@ var CliSedCommandProcessor = class {
|
|
|
2487
3173
|
{
|
|
2488
3174
|
name: "expression",
|
|
2489
3175
|
aliases: ["e"],
|
|
2490
|
-
description: "Add the expression to the commands to be executed",
|
|
3176
|
+
description: "Add the expression to the commands to be executed (repeatable)",
|
|
2491
3177
|
required: false,
|
|
2492
|
-
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"
|
|
2493
3186
|
}
|
|
2494
3187
|
];
|
|
2495
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
|
+
}
|
|
2496
3211
|
async processCommand(command, context) {
|
|
2497
|
-
const fs = context.services.
|
|
3212
|
+
const fs = context.services.getRequired(
|
|
2498
3213
|
IFileSystemService_TOKEN
|
|
2499
3214
|
);
|
|
2500
|
-
const { inPlace, suppress, expressions,
|
|
3215
|
+
const { inPlace, suppress, extended, expressions, filePaths } = this.parseRawCommand(command);
|
|
2501
3216
|
if (expressions.length === 0) {
|
|
2502
3217
|
context.writer.writeError(
|
|
2503
3218
|
"sed: missing expression. Usage: sed [options] 'expression' <file>"
|
|
2504
3219
|
);
|
|
2505
3220
|
return;
|
|
2506
3221
|
}
|
|
2507
|
-
|
|
2508
|
-
|
|
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) {
|
|
2509
3245
|
const resolved = fs.resolvePath(filePath);
|
|
2510
3246
|
if (!fs.exists(resolved)) {
|
|
2511
3247
|
context.writer.writeError(
|
|
@@ -2517,116 +3253,125 @@ var CliSedCommandProcessor = class {
|
|
|
2517
3253
|
context.writer.writeError(`sed: ${filePath}: Is a directory`);
|
|
2518
3254
|
return;
|
|
2519
3255
|
}
|
|
2520
|
-
content = fs.readFile(resolved) ?? "";
|
|
2521
|
-
} else if (command.data != null) {
|
|
2522
|
-
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
2523
|
-
} else {
|
|
2524
|
-
context.writer.writeError(
|
|
2525
|
-
"sed: missing file operand. Usage: sed [options] 'expression' <file>"
|
|
2526
|
-
);
|
|
2527
|
-
return;
|
|
2528
|
-
}
|
|
2529
|
-
const parsedExpressions = [];
|
|
2530
|
-
for (const expr of expressions) {
|
|
2531
3256
|
try {
|
|
2532
|
-
|
|
2533
|
-
parsedExpressions.push(parsed);
|
|
3257
|
+
contents.push(fs.readFile(resolved) ?? "");
|
|
2534
3258
|
} catch (e) {
|
|
2535
3259
|
context.writer.writeError(`sed: ${e.message}`);
|
|
2536
3260
|
return;
|
|
2537
3261
|
}
|
|
2538
3262
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
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 = [];
|
|
2541
3295
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
2542
3296
|
let line = lines[lineIdx];
|
|
2543
3297
|
let deleted = false;
|
|
2544
|
-
let
|
|
2545
|
-
for (const expr of
|
|
2546
|
-
if (
|
|
2547
|
-
if (!this.addressMatches(
|
|
2548
|
-
expr.address,
|
|
2549
|
-
lineIdx + 1,
|
|
2550
|
-
lines.length,
|
|
2551
|
-
line
|
|
2552
|
-
)) {
|
|
3298
|
+
let quit = false;
|
|
3299
|
+
for (const expr of script) {
|
|
3300
|
+
if (!this.addressMatches(expr, lineIdx + 1, lines.length, line)) {
|
|
2553
3301
|
continue;
|
|
2554
3302
|
}
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
if (matched && expr.printFlag) {
|
|
2566
|
-
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];
|
|
2567
3313
|
}
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
2574
|
-
|
|
2575
|
-
|
|
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;
|
|
2576
3327
|
}
|
|
2577
3328
|
}
|
|
2578
|
-
if (!deleted)
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
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 += " ";
|
|
2583
3347
|
} else {
|
|
2584
|
-
|
|
3348
|
+
out += next;
|
|
2585
3349
|
}
|
|
3350
|
+
} else if (ch === "&") {
|
|
3351
|
+
out += groups[0];
|
|
3352
|
+
} else {
|
|
3353
|
+
out += ch;
|
|
2586
3354
|
}
|
|
2587
3355
|
}
|
|
2588
|
-
|
|
2589
|
-
if (inPlace && filePath) {
|
|
2590
|
-
const resolved = fs.resolvePath(filePath);
|
|
2591
|
-
fs.writeFile(resolved, result);
|
|
2592
|
-
await fs.persist();
|
|
2593
|
-
} else {
|
|
2594
|
-
const resultLines = result.split("\n");
|
|
2595
|
-
for (const line of resultLines) {
|
|
2596
|
-
context.writer.writeln(line);
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
3356
|
+
return out;
|
|
2599
3357
|
}
|
|
2600
3358
|
parseRawCommand(command) {
|
|
2601
3359
|
const raw = command.value || "";
|
|
2602
3360
|
const tokens = this.tokenize(raw);
|
|
2603
3361
|
const inPlace = !!command.args["in-place"] || !!command.args["i"];
|
|
2604
3362
|
const suppress = !!command.args["quiet"] || !!command.args["n"];
|
|
3363
|
+
const extended = !!command.args["regexp-extended"] || !!command.args["E"] || !!command.args["r"];
|
|
2605
3364
|
const expressions = [];
|
|
2606
|
-
|
|
2607
|
-
const
|
|
2608
|
-
|
|
2609
|
-
|
|
2610
|
-
if (Array.isArray(eValue)) {
|
|
2611
|
-
for (const v of eValue) {
|
|
2612
|
-
if (v && typeof v === "string") {
|
|
2613
|
-
expressions.push(v);
|
|
2614
|
-
}
|
|
2615
|
-
}
|
|
2616
|
-
} else if (eValue && typeof eValue === "string") {
|
|
2617
|
-
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));
|
|
2618
3369
|
}
|
|
2619
3370
|
}
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
}
|
|
2623
|
-
if (expressions.length === 0 && nonFlagTokens.length > 0) {
|
|
2624
|
-
expressions.push(nonFlagTokens.shift());
|
|
3371
|
+
if (expressions.length === 0 && tokens.length > 0) {
|
|
3372
|
+
expressions.push(tokens.shift());
|
|
2625
3373
|
}
|
|
2626
|
-
|
|
2627
|
-
filePath = nonFlagTokens[nonFlagTokens.length - 1];
|
|
2628
|
-
}
|
|
2629
|
-
return { inPlace, suppress, expressions, filePath };
|
|
3374
|
+
return { inPlace, suppress, extended, expressions, filePaths: tokens };
|
|
2630
3375
|
}
|
|
2631
3376
|
tokenize(raw) {
|
|
2632
3377
|
const tokens = [];
|
|
@@ -2636,73 +3381,85 @@ var CliSedCommandProcessor = class {
|
|
|
2636
3381
|
i++;
|
|
2637
3382
|
continue;
|
|
2638
3383
|
}
|
|
2639
|
-
if (raw[i] === "'" || raw[i] === '"') {
|
|
2640
|
-
const quote = raw[i];
|
|
2641
|
-
i++;
|
|
2642
|
-
let token2 = "";
|
|
2643
|
-
while (i < raw.length && raw[i] !== quote) {
|
|
2644
|
-
token2 += raw[i];
|
|
2645
|
-
i++;
|
|
2646
|
-
}
|
|
2647
|
-
if (i < raw.length) i++;
|
|
2648
|
-
tokens.push(token2);
|
|
2649
|
-
continue;
|
|
2650
|
-
}
|
|
2651
3384
|
let token = "";
|
|
2652
3385
|
while (i < raw.length && raw[i] !== " " && raw[i] !== " ") {
|
|
2653
|
-
|
|
2654
|
-
|
|
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
|
+
}
|
|
2655
3398
|
}
|
|
2656
3399
|
tokens.push(token);
|
|
2657
3400
|
}
|
|
2658
|
-
return tokens;
|
|
3401
|
+
return tokens;
|
|
3402
|
+
}
|
|
3403
|
+
/** Commands of a script, separated by `;` or newlines. */
|
|
3404
|
+
parseScript(script, extended) {
|
|
3405
|
+
const result = [];
|
|
3406
|
+
let pos = 0;
|
|
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;
|
|
2659
3425
|
}
|
|
2660
|
-
|
|
2661
|
-
let
|
|
2662
|
-
let
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
if (endResult) {
|
|
2672
|
-
if (endResult.address.type === "last") {
|
|
2673
|
-
address = {
|
|
2674
|
-
type: "range",
|
|
2675
|
-
startLine: address.line,
|
|
2676
|
-
endLine: 0,
|
|
2677
|
-
endIsLast: true
|
|
2678
|
-
};
|
|
2679
|
-
} else {
|
|
2680
|
-
address = {
|
|
2681
|
-
type: "range",
|
|
2682
|
-
startLine: address.line,
|
|
2683
|
-
endLine: endResult.address.line
|
|
2684
|
-
};
|
|
2685
|
-
}
|
|
2686
|
-
pos = endResult.pos;
|
|
2687
|
-
}
|
|
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}'`);
|
|
2688
3437
|
}
|
|
3438
|
+
end = second.address;
|
|
3439
|
+
pos = second.pos;
|
|
2689
3440
|
}
|
|
2690
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
|
+
}
|
|
2691
3449
|
if (pos >= expr.length) {
|
|
2692
3450
|
throw new Error(`invalid expression: '${expr}'`);
|
|
2693
3451
|
}
|
|
3452
|
+
const base = { start, end, negate, active: false };
|
|
2694
3453
|
const cmd = expr[pos];
|
|
2695
3454
|
if (cmd === "s") {
|
|
2696
|
-
return this.parseSubstitution(expr, pos,
|
|
2697
|
-
}
|
|
2698
|
-
|
|
2699
|
-
|
|
2700
|
-
return { address, command: "p" };
|
|
2701
|
-
} else {
|
|
2702
|
-
throw new Error(`unknown command: '${cmd}'`);
|
|
3455
|
+
return this.parseSubstitution(expr, pos, base, extended);
|
|
3456
|
+
}
|
|
3457
|
+
if (cmd === "d" || cmd === "p" || cmd === "q") {
|
|
3458
|
+
return { expression: { ...base, command: cmd }, pos: pos + 1 };
|
|
2703
3459
|
}
|
|
3460
|
+
throw new Error(`unknown command: '${cmd}'`);
|
|
2704
3461
|
}
|
|
2705
|
-
parseAddress(expr, pos) {
|
|
3462
|
+
parseAddress(expr, pos, extended) {
|
|
2706
3463
|
if (pos >= expr.length) return null;
|
|
2707
3464
|
if (expr[pos] >= "0" && expr[pos] <= "9") {
|
|
2708
3465
|
let numStr = "";
|
|
@@ -2719,51 +3476,73 @@ var CliSedCommandProcessor = class {
|
|
|
2719
3476
|
return { address: { type: "last" }, pos: pos + 1 };
|
|
2720
3477
|
}
|
|
2721
3478
|
if (expr[pos] === "/") {
|
|
2722
|
-
pos
|
|
2723
|
-
let pattern = "";
|
|
2724
|
-
while (pos < expr.length && expr[pos] !== "/") {
|
|
2725
|
-
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2726
|
-
pattern += expr[pos + 1];
|
|
2727
|
-
pos += 2;
|
|
2728
|
-
} else {
|
|
2729
|
-
pattern += expr[pos];
|
|
2730
|
-
pos++;
|
|
2731
|
-
}
|
|
2732
|
-
}
|
|
2733
|
-
if (pos < expr.length) pos++;
|
|
3479
|
+
const parsed = this.readDelimited(expr, pos + 1, "/");
|
|
2734
3480
|
try {
|
|
2735
3481
|
return {
|
|
2736
|
-
address: {
|
|
2737
|
-
|
|
3482
|
+
address: {
|
|
3483
|
+
type: "regex",
|
|
3484
|
+
regex: new RegExp(this.toJsRegex(parsed.text, extended))
|
|
3485
|
+
},
|
|
3486
|
+
pos: parsed.pos
|
|
2738
3487
|
};
|
|
2739
3488
|
} catch {
|
|
2740
|
-
throw new Error(`invalid regex in address: '${
|
|
3489
|
+
throw new Error(`invalid regex in address: '${parsed.text}'`);
|
|
2741
3490
|
}
|
|
2742
3491
|
}
|
|
2743
3492
|
return null;
|
|
2744
3493
|
}
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
pos++;
|
|
2752
|
-
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 = "";
|
|
2753
3500
|
while (pos < expr.length && expr[pos] !== delimiter) {
|
|
2754
3501
|
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2755
|
-
|
|
3502
|
+
const next = expr[pos + 1];
|
|
3503
|
+
text += next === delimiter && /\w|\//.test(next) ? next : "\\" + next;
|
|
2756
3504
|
pos += 2;
|
|
2757
3505
|
} else {
|
|
2758
|
-
|
|
3506
|
+
text += expr[pos];
|
|
2759
3507
|
pos++;
|
|
2760
3508
|
}
|
|
2761
3509
|
}
|
|
2762
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;
|
|
2763
3542
|
let replacement = "";
|
|
2764
3543
|
while (pos < expr.length && expr[pos] !== delimiter) {
|
|
2765
3544
|
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2766
|
-
replacement += expr[pos + 1];
|
|
3545
|
+
replacement += expr[pos + 1] === delimiter ? delimiter : "\\" + expr[pos + 1];
|
|
2767
3546
|
pos += 2;
|
|
2768
3547
|
} else {
|
|
2769
3548
|
replacement += expr[pos];
|
|
@@ -2774,47 +3553,75 @@ var CliSedCommandProcessor = class {
|
|
|
2774
3553
|
let globalFlag = false;
|
|
2775
3554
|
let caseInsensitive = false;
|
|
2776
3555
|
let printFlag = false;
|
|
2777
|
-
|
|
3556
|
+
let occurrence = "";
|
|
3557
|
+
while (pos < expr.length && !/[;\n\s}]/.test(expr[pos])) {
|
|
2778
3558
|
const f = expr[pos];
|
|
2779
3559
|
if (f === "g") globalFlag = true;
|
|
2780
|
-
else if (f === "i") caseInsensitive = true;
|
|
3560
|
+
else if (f === "i" || f === "I") caseInsensitive = true;
|
|
2781
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}'`);
|
|
2782
3564
|
pos++;
|
|
2783
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
|
+
}
|
|
2784
3570
|
let regexFlags = "";
|
|
2785
3571
|
if (caseInsensitive) regexFlags += "i";
|
|
2786
|
-
if (globalFlag) regexFlags += "g";
|
|
3572
|
+
if (globalFlag || nth) regexFlags += "g";
|
|
2787
3573
|
let regex;
|
|
3574
|
+
let groupCount;
|
|
2788
3575
|
try {
|
|
2789
|
-
|
|
3576
|
+
const source = this.toJsRegex(pattern.text, extended);
|
|
3577
|
+
regex = new RegExp(source, regexFlags);
|
|
3578
|
+
groupCount = (new RegExp(`${source}|`).exec("")?.length ?? 1) - 1;
|
|
2790
3579
|
} catch {
|
|
2791
|
-
throw new Error(`invalid regex: '${pattern}'`);
|
|
3580
|
+
throw new Error(`invalid regex: '${pattern.text}'`);
|
|
2792
3581
|
}
|
|
2793
3582
|
return {
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3583
|
+
expression: {
|
|
3584
|
+
...base,
|
|
3585
|
+
command: "s",
|
|
3586
|
+
pattern: regex,
|
|
3587
|
+
replacement,
|
|
3588
|
+
groupCount,
|
|
3589
|
+
printFlag,
|
|
3590
|
+
occurrence: nth,
|
|
3591
|
+
globalFlag
|
|
3592
|
+
},
|
|
3593
|
+
pos
|
|
2800
3594
|
};
|
|
2801
3595
|
}
|
|
2802
|
-
addressMatches(
|
|
2803
|
-
|
|
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) {
|
|
2804
3618
|
switch (address.type) {
|
|
2805
3619
|
case "line":
|
|
2806
3620
|
return lineNum === address.line;
|
|
2807
3621
|
case "last":
|
|
2808
3622
|
return lineNum === totalLines;
|
|
2809
|
-
case "range": {
|
|
2810
|
-
const start = address.startLine;
|
|
2811
|
-
const end = address.endIsLast ? totalLines : address.endLine;
|
|
2812
|
-
return lineNum >= start && lineNum <= end;
|
|
2813
|
-
}
|
|
2814
3623
|
case "regex":
|
|
2815
3624
|
return address.regex.test(lineText);
|
|
2816
|
-
default:
|
|
2817
|
-
return true;
|
|
2818
3625
|
}
|
|
2819
3626
|
}
|
|
2820
3627
|
};
|
|
@@ -2836,11 +3643,34 @@ var CliAwkCommandProcessor = class {
|
|
|
2836
3643
|
}
|
|
2837
3644
|
];
|
|
2838
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
|
+
}
|
|
2839
3669
|
async processCommand(command, context) {
|
|
2840
|
-
const fs = context.services.
|
|
3670
|
+
const fs = context.services.getRequired(
|
|
2841
3671
|
IFileSystemService_TOKEN
|
|
2842
3672
|
);
|
|
2843
|
-
const { program,
|
|
3673
|
+
const { program, filePaths, fieldSep } = this.parseArgs(command, fs);
|
|
2844
3674
|
if (!program) {
|
|
2845
3675
|
context.writer.writeError(
|
|
2846
3676
|
"awk: missing program. Usage: awk [-F sep] 'program' file"
|
|
@@ -2848,18 +3678,21 @@ var CliAwkCommandProcessor = class {
|
|
|
2848
3678
|
return;
|
|
2849
3679
|
}
|
|
2850
3680
|
let content = "";
|
|
2851
|
-
if (
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
2855
|
-
|
|
2856
|
-
|
|
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}`);
|
|
2857
3694
|
return;
|
|
2858
3695
|
}
|
|
2859
|
-
content = fs.readFile(filePath) ?? "";
|
|
2860
|
-
} catch (e) {
|
|
2861
|
-
context.writer.writeError(`awk: ${e.message}`);
|
|
2862
|
-
return;
|
|
2863
3696
|
}
|
|
2864
3697
|
} else if (command.data != null) {
|
|
2865
3698
|
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
@@ -2921,24 +3754,58 @@ var CliAwkCommandProcessor = class {
|
|
|
2921
3754
|
context.writer.writeln(line);
|
|
2922
3755
|
}
|
|
2923
3756
|
}
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
|
|
2937
|
-
|
|
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;
|
|
2938
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;
|
|
2939
3807
|
}
|
|
2940
|
-
|
|
2941
|
-
return { program, filePath, fieldSep };
|
|
3808
|
+
return depth === 0;
|
|
2942
3809
|
}
|
|
2943
3810
|
parseProgram(program) {
|
|
2944
3811
|
const rules = [];
|
|
@@ -3312,8 +4179,26 @@ var CliDiffCommandProcessor = class {
|
|
|
3312
4179
|
}
|
|
3313
4180
|
];
|
|
3314
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
|
+
}
|
|
3315
4200
|
async processCommand(command, context) {
|
|
3316
|
-
const fs = context.services.
|
|
4201
|
+
const fs = context.services.getRequired(
|
|
3317
4202
|
IFileSystemService_TOKEN
|
|
3318
4203
|
);
|
|
3319
4204
|
const unified = command.args["unified"] || command.args["u"] || false;
|
|
@@ -3476,13 +4361,13 @@ var CliDiffCommandProcessor = class {
|
|
|
3476
4361
|
}
|
|
3477
4362
|
parsePaths(command) {
|
|
3478
4363
|
const raw = command.value || "";
|
|
3479
|
-
return raw
|
|
4364
|
+
return splitArgs(raw).filter(Boolean);
|
|
3480
4365
|
}
|
|
3481
4366
|
};
|
|
3482
4367
|
var CliTeeCommandProcessor = class {
|
|
3483
4368
|
constructor() {
|
|
3484
4369
|
this.command = "tee";
|
|
3485
|
-
this.description = "
|
|
4370
|
+
this.description = "Copy piped input to files and to the output";
|
|
3486
4371
|
this.author = cliCore.DefaultLibraryAuthor;
|
|
3487
4372
|
this.version = LIBRARY_VERSION;
|
|
3488
4373
|
this.acceptsRawInput = true;
|
|
@@ -3498,72 +4383,56 @@ var CliTeeCommandProcessor = class {
|
|
|
3498
4383
|
}
|
|
3499
4384
|
];
|
|
3500
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
|
+
}
|
|
3501
4407
|
async processCommand(command, context) {
|
|
3502
|
-
const fs = context.services.
|
|
4408
|
+
const fs = context.services.getRequired(
|
|
3503
4409
|
IFileSystemService_TOKEN
|
|
3504
4410
|
);
|
|
3505
4411
|
const append = command.args["append"] || command.args["a"] || false;
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
context.writer.writeError(
|
|
3523
|
-
return;
|
|
3524
|
-
}
|
|
3525
|
-
if (!parsed.inputFile) {
|
|
3526
|
-
context.writer.writeError("tee: missing input file");
|
|
3527
|
-
return;
|
|
3528
|
-
}
|
|
3529
|
-
if (parsed.outputFiles.length === 0) {
|
|
3530
|
-
context.writer.writeError("tee: missing output file");
|
|
3531
|
-
return;
|
|
3532
|
-
}
|
|
3533
|
-
if (!fs.exists(parsed.inputFile)) {
|
|
3534
|
-
context.writer.writeError(
|
|
3535
|
-
`tee: ${parsed.inputFile}: No such file or directory`
|
|
3536
|
-
);
|
|
3537
|
-
return;
|
|
3538
|
-
}
|
|
3539
|
-
content = fs.readFile(parsed.inputFile) || "";
|
|
3540
|
-
context.writer.writeln(content);
|
|
3541
|
-
for (const outFile of parsed.outputFiles) {
|
|
3542
|
-
if (append && fs.exists(outFile)) {
|
|
3543
|
-
fs.writeFile(outFile, content, true);
|
|
3544
|
-
} else {
|
|
3545
|
-
fs.writeFile(outFile, content);
|
|
3546
|
-
}
|
|
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}`);
|
|
3547
4429
|
}
|
|
3548
|
-
await fs.persist();
|
|
3549
4430
|
}
|
|
4431
|
+
if (changed) await fs.persist();
|
|
3550
4432
|
}
|
|
3551
4433
|
getAllPaths(command) {
|
|
3552
4434
|
const raw = command.value || "";
|
|
3553
|
-
return raw
|
|
3554
|
-
}
|
|
3555
|
-
parseArgs(command) {
|
|
3556
|
-
const raw = command.value || "";
|
|
3557
|
-
const paths = raw.split(/\s+/).filter(Boolean);
|
|
3558
|
-
if (paths.length < 2) {
|
|
3559
|
-
return {
|
|
3560
|
-
outputFiles: [],
|
|
3561
|
-
inputFile: paths[0] || null
|
|
3562
|
-
};
|
|
3563
|
-
}
|
|
3564
|
-
const inputFile = paths[paths.length - 1];
|
|
3565
|
-
const outputFiles = paths.slice(0, paths.length - 1);
|
|
3566
|
-
return { outputFiles, inputFile };
|
|
4435
|
+
return splitArgs(raw).filter(Boolean);
|
|
3567
4436
|
}
|
|
3568
4437
|
};
|
|
3569
4438
|
var CliXargsCommandProcessor = class {
|
|
@@ -3592,65 +4461,91 @@ var CliXargsCommandProcessor = class {
|
|
|
3592
4461
|
}
|
|
3593
4462
|
];
|
|
3594
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
|
+
}
|
|
3595
4487
|
async processCommand(command, context) {
|
|
3596
|
-
const fs = context.services.
|
|
4488
|
+
const fs = context.services.getRequired(
|
|
3597
4489
|
IFileSystemService_TOKEN
|
|
3598
4490
|
);
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
command.
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
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;
|
|
3605
4507
|
let commandTemplate;
|
|
3606
4508
|
let argContent;
|
|
3607
4509
|
if (command.data != null) {
|
|
3608
|
-
commandTemplate =
|
|
3609
|
-
|
|
3610
|
-
context.writer.writeError("xargs: missing command");
|
|
3611
|
-
return;
|
|
3612
|
-
}
|
|
3613
|
-
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);
|
|
3614
4512
|
} else {
|
|
3615
|
-
if (
|
|
4513
|
+
if (parsed.words.length === 0) {
|
|
3616
4514
|
context.writer.writeError("xargs: missing command");
|
|
3617
4515
|
return;
|
|
3618
4516
|
}
|
|
3619
|
-
|
|
3620
|
-
if (!parsed.inputFile) {
|
|
4517
|
+
if (parsed.words.length < 2) {
|
|
3621
4518
|
context.writer.writeError("xargs: missing input file");
|
|
3622
4519
|
return;
|
|
3623
4520
|
}
|
|
3624
|
-
|
|
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)) {
|
|
3625
4524
|
context.writer.writeError(
|
|
3626
|
-
`xargs: ${
|
|
4525
|
+
`xargs: ${inputFile}: No such file or directory`
|
|
3627
4526
|
);
|
|
3628
4527
|
return;
|
|
3629
4528
|
}
|
|
3630
|
-
argContent = fs.readFile(
|
|
4529
|
+
argContent = fs.readFile(inputFile) || "";
|
|
3631
4530
|
}
|
|
3632
|
-
const args = argContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3633
|
-
if (args.length === 0) return;
|
|
3634
4531
|
const commands = [];
|
|
3635
4532
|
if (replaceStr) {
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
|
|
3639
|
-
|
|
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))
|
|
3640
4538
|
);
|
|
3641
|
-
commands.push(cmd);
|
|
3642
4539
|
}
|
|
3643
|
-
} else
|
|
3644
|
-
|
|
3645
|
-
|
|
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) {
|
|
3646
4545
|
commands.push(
|
|
3647
|
-
`${commandTemplate} ${
|
|
4546
|
+
`${commandTemplate} ${args.slice(i, i + size).join(" ")}`
|
|
3648
4547
|
);
|
|
3649
4548
|
}
|
|
3650
|
-
} else {
|
|
3651
|
-
commands.push(
|
|
3652
|
-
`${commandTemplate} ${args.join(" ")}`
|
|
3653
|
-
);
|
|
3654
4549
|
}
|
|
3655
4550
|
const executor = context.executor;
|
|
3656
4551
|
for (const cmd of commands) {
|
|
@@ -3665,27 +4560,64 @@ var CliXargsCommandProcessor = class {
|
|
|
3665
4560
|
}
|
|
3666
4561
|
}
|
|
3667
4562
|
}
|
|
3668
|
-
getAllNonFlagTokens(command) {
|
|
3669
|
-
const raw = command.value || "";
|
|
3670
|
-
return raw.split(/\s+/).filter(Boolean);
|
|
3671
|
-
}
|
|
3672
4563
|
escapeRegex(str) {
|
|
3673
4564
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3674
4565
|
}
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
3681
|
-
|
|
3682
|
-
|
|
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(/^-+/, "")}'`);
|
|
3683
4599
|
}
|
|
3684
|
-
const inputFile = nonFlagTokens[nonFlagTokens.length - 1];
|
|
3685
|
-
const commandTemplate = nonFlagTokens.slice(0, nonFlagTokens.length - 1).join(" ");
|
|
3686
|
-
return { commandTemplate, inputFile };
|
|
3687
4600
|
}
|
|
3688
|
-
|
|
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
|
+
}
|
|
3689
4621
|
var CliShCommandProcessor = class {
|
|
3690
4622
|
constructor() {
|
|
3691
4623
|
this.command = "sh";
|
|
@@ -3706,17 +4638,14 @@ var CliShCommandProcessor = class {
|
|
|
3706
4638
|
];
|
|
3707
4639
|
}
|
|
3708
4640
|
async processCommand(command, context) {
|
|
3709
|
-
const fs = context.services.
|
|
4641
|
+
const fs = context.services.getRequired(
|
|
3710
4642
|
IFileSystemService_TOKEN
|
|
3711
4643
|
);
|
|
3712
|
-
let
|
|
3713
|
-
if (
|
|
3714
|
-
|
|
3715
|
-
const firstSpace = raw.indexOf(" ");
|
|
3716
|
-
if (firstSpace > -1) {
|
|
3717
|
-
filePath = raw.slice(firstSpace + 1).trim();
|
|
3718
|
-
}
|
|
4644
|
+
let words = splitArgs(command.value || "");
|
|
4645
|
+
if (words.length === 0 && command.rawCommand) {
|
|
4646
|
+
words = splitArgs(command.rawCommand).slice(1).filter((w) => !w.startsWith("-"));
|
|
3719
4647
|
}
|
|
4648
|
+
const filePath = words[0] ?? "";
|
|
3720
4649
|
if (!filePath) {
|
|
3721
4650
|
context.writer.writeError("sh: missing file operand");
|
|
3722
4651
|
context.process.exit(1, { silent: true });
|
|
@@ -3749,10 +4678,10 @@ var CliShCommandProcessor = class {
|
|
|
3749
4678
|
await executeScript(content, context);
|
|
3750
4679
|
}
|
|
3751
4680
|
writeDescription(context) {
|
|
3752
|
-
const { writer } = context;
|
|
3753
|
-
writer.writeln(this.description);
|
|
4681
|
+
const { writer, translator: t } = context;
|
|
4682
|
+
writer.writeln(t.t("cli.sh.description", this.description));
|
|
3754
4683
|
writer.writeln();
|
|
3755
|
-
writer.writeln("Usage:");
|
|
4684
|
+
writer.writeln(t.t("cli.common.usage", "Usage:"));
|
|
3756
4685
|
writer.writeln(
|
|
3757
4686
|
` ${writer.wrapInColor("sh <script.sh>", cliCore.CliForegroundColor.Cyan)}`
|
|
3758
4687
|
);
|
|
@@ -3763,83 +4692,57 @@ var CliShCommandProcessor = class {
|
|
|
3763
4692
|
` ${writer.wrapInColor(". <script.sh>", cliCore.CliForegroundColor.Cyan)}`
|
|
3764
4693
|
);
|
|
3765
4694
|
writer.writeln(
|
|
3766
|
-
` ${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)")}`
|
|
3767
4696
|
);
|
|
3768
4697
|
writer.writeln();
|
|
3769
|
-
writer.writeln("Script syntax:");
|
|
3770
|
-
writer.writeln(
|
|
3771
|
-
writer.writeln(
|
|
3772
|
-
writer.writeln(
|
|
3773
|
-
writer.writeln(
|
|
3774
|
-
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.")}`);
|
|
3775
4718
|
writer.writeln();
|
|
3776
|
-
writer.writeln("Example script:");
|
|
4719
|
+
writer.writeln(t.t("cli.sh.help.example_script", "Example script:"));
|
|
3777
4720
|
writer.writeln(
|
|
3778
4721
|
writer.wrapInColor(
|
|
3779
|
-
" #!/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",
|
|
3780
4723
|
cliCore.CliForegroundColor.Green
|
|
3781
4724
|
)
|
|
3782
4725
|
);
|
|
3783
4726
|
}
|
|
3784
4727
|
};
|
|
4728
|
+
var SCRIPT_RUNNER_TOKEN = "cli-script-runner";
|
|
3785
4729
|
async function executeScript(content, context) {
|
|
3786
|
-
const
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
if (!line || line.startsWith("#")) {
|
|
3792
|
-
continue;
|
|
3793
|
-
}
|
|
3794
|
-
if (line === "set -e") {
|
|
3795
|
-
stopOnError = true;
|
|
3796
|
-
continue;
|
|
3797
|
-
}
|
|
3798
|
-
if (line === "set +e") {
|
|
3799
|
-
stopOnError = false;
|
|
3800
|
-
continue;
|
|
3801
|
-
}
|
|
3802
|
-
const assignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
3803
|
-
if (assignMatch) {
|
|
3804
|
-
const varName = assignMatch[1];
|
|
3805
|
-
let varValue = assignMatch[2];
|
|
3806
|
-
if (varValue.startsWith('"') && varValue.endsWith('"') || varValue.startsWith("'") && varValue.endsWith("'")) {
|
|
3807
|
-
varValue = varValue.slice(1, -1);
|
|
3808
|
-
}
|
|
3809
|
-
varValue = substituteVariables(varValue, variables);
|
|
3810
|
-
variables[varName] = varValue;
|
|
3811
|
-
continue;
|
|
3812
|
-
}
|
|
3813
|
-
const expanded = substituteVariables(line, variables);
|
|
3814
|
-
await context.executor.executeCommand(expanded, context);
|
|
3815
|
-
const exitCode = context.process.exitCode;
|
|
3816
|
-
if (stopOnError && exitCode !== void 0 && exitCode !== 0) {
|
|
3817
|
-
context.writer.writeError(
|
|
3818
|
-
`sh: script aborted at line: ${line}`
|
|
3819
|
-
);
|
|
3820
|
-
return;
|
|
3821
|
-
}
|
|
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;
|
|
3822
4735
|
}
|
|
4736
|
+
await runner.run(content, context, { name: "sh" });
|
|
3823
4737
|
}
|
|
3824
|
-
function substituteVariables(input, variables) {
|
|
3825
|
-
let result = input.replace(
|
|
3826
|
-
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
3827
|
-
(_, name) => variables[name] ?? ""
|
|
3828
|
-
);
|
|
3829
|
-
result = result.replace(
|
|
3830
|
-
/\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
3831
|
-
(_, name) => variables[name] ?? ""
|
|
3832
|
-
);
|
|
3833
|
-
return result;
|
|
3834
|
-
}
|
|
3835
|
-
|
|
3836
|
-
// src/lib/services/indexed-db-file-system.service.ts
|
|
3837
4738
|
var DB_NAME = "qodalis-cli-filesystem";
|
|
3838
4739
|
var STORE_NAME = "filesystem";
|
|
3839
4740
|
var ROOT_KEY = "root";
|
|
3840
4741
|
var CWD_KEY = "cwd";
|
|
3841
4742
|
var HOME_KEY = "home";
|
|
3842
4743
|
var DEFAULT_HOME = "/";
|
|
4744
|
+
var SYSTEM_OWNERSHIP = { uid: "root", gid: "admin" };
|
|
4745
|
+
var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
|
|
3843
4746
|
function createSeedFileSystem() {
|
|
3844
4747
|
const now = Date.now();
|
|
3845
4748
|
return {
|
|
@@ -3887,6 +4790,88 @@ var IndexedDbFileSystemService = class {
|
|
|
3887
4790
|
this.homePath = DEFAULT_HOME;
|
|
3888
4791
|
this.currentUid = null;
|
|
3889
4792
|
this.currentGroups = [];
|
|
4793
|
+
this.systemDepth = 0;
|
|
4794
|
+
}
|
|
4795
|
+
// --- Access control ---
|
|
4796
|
+
setAccessPolicy(policy) {
|
|
4797
|
+
this.accessPolicy = policy;
|
|
4798
|
+
}
|
|
4799
|
+
runAsSystem(fn) {
|
|
4800
|
+
this.systemDepth++;
|
|
4801
|
+
try {
|
|
4802
|
+
return fn();
|
|
4803
|
+
} finally {
|
|
4804
|
+
this.systemDepth--;
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
reassignOwner(fromUid, toUid) {
|
|
4808
|
+
const from = fromUid.toLowerCase();
|
|
4809
|
+
if (!from || !toUid) return 0;
|
|
4810
|
+
let changed = 0;
|
|
4811
|
+
const visit = (node) => {
|
|
4812
|
+
if (node.ownership && node.ownership.uid.toLowerCase() === from) {
|
|
4813
|
+
node.ownership = { ...node.ownership, uid: toUid };
|
|
4814
|
+
changed++;
|
|
4815
|
+
}
|
|
4816
|
+
node.children?.forEach(visit);
|
|
4817
|
+
};
|
|
4818
|
+
visit(this.root);
|
|
4819
|
+
return changed;
|
|
4820
|
+
}
|
|
4821
|
+
canTraverse(path) {
|
|
4822
|
+
try {
|
|
4823
|
+
this.assertTraverse(this.resolvePath(path), "stat", path);
|
|
4824
|
+
return true;
|
|
4825
|
+
} catch {
|
|
4826
|
+
return false;
|
|
4827
|
+
}
|
|
4828
|
+
}
|
|
4829
|
+
get enforcing() {
|
|
4830
|
+
return !!this.accessPolicy && this.systemDepth === 0;
|
|
4831
|
+
}
|
|
4832
|
+
deny(op, path, reason = "Permission denied") {
|
|
4833
|
+
throw new cliCore.CliAccessDeniedError(`${op}: ${path}: ${reason}`);
|
|
4834
|
+
}
|
|
4835
|
+
/** `x` on every existing directory on the way to `resolved` (excluding the target). */
|
|
4836
|
+
assertTraverse(resolved, op, display) {
|
|
4837
|
+
if (!this.enforcing) return;
|
|
4838
|
+
const parts = resolved.split("/").filter(Boolean);
|
|
4839
|
+
let current = this.root;
|
|
4840
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4841
|
+
if (!current || current.type !== "directory") return;
|
|
4842
|
+
if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
|
|
4843
|
+
current = current.children?.find((c) => c.name === parts[i]);
|
|
4844
|
+
}
|
|
4845
|
+
}
|
|
4846
|
+
assertAccess(node, action, op, display) {
|
|
4847
|
+
if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
|
|
4848
|
+
}
|
|
4849
|
+
/** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
|
|
4850
|
+
assertRemovableTree(dir, dirPath, display) {
|
|
4851
|
+
if (!this.enforcing) return;
|
|
4852
|
+
this.assertAccess(dir, "read", "rm", display);
|
|
4853
|
+
this.assertAccess(dir, "execute", "rm", display);
|
|
4854
|
+
if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
|
|
4855
|
+
for (const child of dir.children ?? []) {
|
|
4856
|
+
if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
|
|
4857
|
+
}
|
|
4858
|
+
}
|
|
4859
|
+
/** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
|
|
4860
|
+
assertReadableTree(node, display) {
|
|
4861
|
+
if (!this.enforcing) return;
|
|
4862
|
+
this.assertAccess(node, "read", "cp", display);
|
|
4863
|
+
if (node.type !== "directory") return;
|
|
4864
|
+
this.assertAccess(node, "execute", "cp", display);
|
|
4865
|
+
for (const child of node.children ?? []) {
|
|
4866
|
+
this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
|
|
4867
|
+
}
|
|
4868
|
+
}
|
|
4869
|
+
/** Creating/removing entries needs `w` and `x` on the containing directory. */
|
|
4870
|
+
assertModifyDir(dir, dirPath, op, display) {
|
|
4871
|
+
if (!this.enforcing) return;
|
|
4872
|
+
this.assertTraverse(dirPath, op, display);
|
|
4873
|
+
this.assertAccess(dir, "execute", op, display);
|
|
4874
|
+
this.assertAccess(dir, "write", op, display);
|
|
3890
4875
|
}
|
|
3891
4876
|
// --- User context ---
|
|
3892
4877
|
setCurrentUser(uid, groups) {
|
|
@@ -3894,7 +4879,9 @@ var IndexedDbFileSystemService = class {
|
|
|
3894
4879
|
this.currentGroups = groups;
|
|
3895
4880
|
}
|
|
3896
4881
|
getDefaultOwnership() {
|
|
3897
|
-
if (!this.currentUid)
|
|
4882
|
+
if (!this.currentUid) {
|
|
4883
|
+
return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
|
|
4884
|
+
}
|
|
3898
4885
|
return {
|
|
3899
4886
|
uid: this.currentUid,
|
|
3900
4887
|
gid: this.currentGroups[0] || "users"
|
|
@@ -3913,6 +4900,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3913
4900
|
if (node.type !== "directory") {
|
|
3914
4901
|
throw new Error(`cd: ${path}: Not a directory`);
|
|
3915
4902
|
}
|
|
4903
|
+
this.assertTraverse(resolved, "cd", path);
|
|
4904
|
+
this.assertAccess(node, "execute", "cd", path);
|
|
3916
4905
|
this.cwd = resolved;
|
|
3917
4906
|
}
|
|
3918
4907
|
getHomePath() {
|
|
@@ -3969,6 +4958,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3969
4958
|
if (node.type !== "directory") {
|
|
3970
4959
|
throw new Error(`ls: ${path}: Not a directory`);
|
|
3971
4960
|
}
|
|
4961
|
+
this.assertTraverse(this.resolvePath(path), "ls", path);
|
|
4962
|
+
this.assertAccess(node, "read", "ls", path);
|
|
3972
4963
|
return node.children || [];
|
|
3973
4964
|
}
|
|
3974
4965
|
readFile(path) {
|
|
@@ -3979,6 +4970,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3979
4970
|
if (node.type === "directory") {
|
|
3980
4971
|
throw new Error(`cat: ${path}: Is a directory`);
|
|
3981
4972
|
}
|
|
4973
|
+
this.assertTraverse(this.resolvePath(path), "cat", path);
|
|
4974
|
+
this.assertAccess(node, "read", "cat", path);
|
|
3982
4975
|
return node.content ?? "";
|
|
3983
4976
|
}
|
|
3984
4977
|
exists(path) {
|
|
@@ -4003,6 +4996,10 @@ var IndexedDbFileSystemService = class {
|
|
|
4003
4996
|
}
|
|
4004
4997
|
let child = current.children.find((c) => c.name === part);
|
|
4005
4998
|
if (!child) {
|
|
4999
|
+
if (this.enforcing) {
|
|
5000
|
+
this.assertAccess(current, "execute", "mkdir", path);
|
|
5001
|
+
this.assertAccess(current, "write", "mkdir", path);
|
|
5002
|
+
}
|
|
4006
5003
|
const now = Date.now();
|
|
4007
5004
|
child = {
|
|
4008
5005
|
name: part,
|
|
@@ -4018,6 +5015,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4018
5015
|
current.modifiedAt = now;
|
|
4019
5016
|
} else if (child.type !== "directory") {
|
|
4020
5017
|
throw new Error(`mkdir: ${part}: Not a directory`);
|
|
5018
|
+
} else {
|
|
5019
|
+
this.assertAccess(child, "execute", "mkdir", path);
|
|
4021
5020
|
}
|
|
4022
5021
|
current = child;
|
|
4023
5022
|
}
|
|
@@ -4036,6 +5035,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4036
5035
|
if (parent.children?.find((c) => c.name === dirName)) {
|
|
4037
5036
|
throw new Error(`mkdir: ${path}: File exists`);
|
|
4038
5037
|
}
|
|
5038
|
+
this.assertModifyDir(parent, parentPath, "mkdir", path);
|
|
4039
5039
|
const now = Date.now();
|
|
4040
5040
|
if (!parent.children) {
|
|
4041
5041
|
parent.children = [];
|
|
@@ -4070,6 +5070,12 @@ var IndexedDbFileSystemService = class {
|
|
|
4070
5070
|
}
|
|
4071
5071
|
const existing = parent.children?.find((c) => c.name === fileName);
|
|
4072
5072
|
const now = Date.now();
|
|
5073
|
+
if (existing) {
|
|
5074
|
+
this.assertTraverse(resolved, "touch", path);
|
|
5075
|
+
this.assertAccess(existing, "write", "touch", path);
|
|
5076
|
+
} else {
|
|
5077
|
+
this.assertModifyDir(parent, parentPath, "touch", path);
|
|
5078
|
+
}
|
|
4073
5079
|
if (existing) {
|
|
4074
5080
|
existing.modifiedAt = now;
|
|
4075
5081
|
} else {
|
|
@@ -4096,6 +5102,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4096
5102
|
if (node.type === "directory") {
|
|
4097
5103
|
throw new Error(`write: ${path}: Is a directory`);
|
|
4098
5104
|
}
|
|
5105
|
+
this.assertTraverse(resolved, "write", path);
|
|
5106
|
+
this.assertAccess(node, "write", "write", path);
|
|
4099
5107
|
const now = Date.now();
|
|
4100
5108
|
node.content = append ? (node.content ?? "") + content : content;
|
|
4101
5109
|
node.size = new Blob([node.content]).size;
|
|
@@ -4124,6 +5132,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4124
5132
|
if (target.type === "directory" && !recursive) {
|
|
4125
5133
|
throw new Error(`rm: ${path}: Is a directory`);
|
|
4126
5134
|
}
|
|
5135
|
+
this.assertModifyDir(parent, parentPath, "rm", path);
|
|
5136
|
+
if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
|
|
4127
5137
|
parent.children.splice(index, 1);
|
|
4128
5138
|
parent.modifiedAt = Date.now();
|
|
4129
5139
|
}
|
|
@@ -4138,10 +5148,13 @@ var IndexedDbFileSystemService = class {
|
|
|
4138
5148
|
`cp: -r not specified; omitting directory '${src}'`
|
|
4139
5149
|
);
|
|
4140
5150
|
}
|
|
5151
|
+
this.assertTraverse(srcResolved, "cp", src);
|
|
5152
|
+
this.assertReadableTree(srcNode, src);
|
|
4141
5153
|
const destResolved = this.resolvePath(dest);
|
|
4142
5154
|
const destNode = this.getNode(destResolved);
|
|
4143
5155
|
if (destNode && destNode.type === "directory") {
|
|
4144
|
-
|
|
5156
|
+
this.assertModifyDir(destNode, destResolved, "cp", dest);
|
|
5157
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4145
5158
|
if (!destNode.children) {
|
|
4146
5159
|
destNode.children = [];
|
|
4147
5160
|
}
|
|
@@ -4161,7 +5174,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4161
5174
|
if (!parent || parent.type !== "directory") {
|
|
4162
5175
|
throw new Error(`cp: ${parentPath}: No such file or directory`);
|
|
4163
5176
|
}
|
|
4164
|
-
|
|
5177
|
+
this.assertModifyDir(parent, parentPath, "cp", dest);
|
|
5178
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4165
5179
|
clone.name = newName;
|
|
4166
5180
|
if (!parent.children) {
|
|
4167
5181
|
parent.children = [];
|
|
@@ -4187,16 +5201,23 @@ var IndexedDbFileSystemService = class {
|
|
|
4187
5201
|
}
|
|
4188
5202
|
const destResolved = this.resolvePath(dest);
|
|
4189
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
|
+
}
|
|
4190
5209
|
const srcParts = srcResolved.split("/").filter(Boolean);
|
|
4191
5210
|
const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
|
|
4192
5211
|
const srcParent = this.getNode(srcParentPath);
|
|
4193
5212
|
if (!srcParent || !srcParent.children) {
|
|
4194
5213
|
throw new Error(`mv: ${src}: No such file or directory`);
|
|
4195
5214
|
}
|
|
4196
|
-
|
|
4197
|
-
|
|
4198
|
-
|
|
5215
|
+
this.assertModifyDir(srcParent, srcParentPath, "mv", src);
|
|
5216
|
+
if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
|
|
5217
|
+
throw new Error(`mv: '${src}' and '${dest}' are the same file`);
|
|
5218
|
+
}
|
|
4199
5219
|
if (destNode && destNode.type === "directory") {
|
|
5220
|
+
this.assertModifyDir(destNode, destResolved, "mv", dest);
|
|
4200
5221
|
if (!destNode.children) {
|
|
4201
5222
|
destNode.children = [];
|
|
4202
5223
|
}
|
|
@@ -4206,7 +5227,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4206
5227
|
if (existingIdx !== -1) {
|
|
4207
5228
|
destNode.children.splice(existingIdx, 1);
|
|
4208
5229
|
}
|
|
4209
|
-
srcParent.children.splice(
|
|
5230
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4210
5231
|
destNode.children.push(srcNode);
|
|
4211
5232
|
destNode.modifiedAt = Date.now();
|
|
4212
5233
|
} else {
|
|
@@ -4219,7 +5240,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4219
5240
|
`mv: ${destParentPath}: No such file or directory`
|
|
4220
5241
|
);
|
|
4221
5242
|
}
|
|
4222
|
-
|
|
5243
|
+
this.assertModifyDir(destParent, destParentPath, "mv", dest);
|
|
5244
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4223
5245
|
srcNode.name = newName;
|
|
4224
5246
|
if (!destParent.children) {
|
|
4225
5247
|
destParent.children = [];
|
|
@@ -4242,6 +5264,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4242
5264
|
if (!node) {
|
|
4243
5265
|
throw new Error(`chmod: ${path}: No such file or directory`);
|
|
4244
5266
|
}
|
|
5267
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
|
|
5268
|
+
this.deny("chmod", path, "Operation not permitted");
|
|
5269
|
+
}
|
|
4245
5270
|
node.permissions = permissions;
|
|
4246
5271
|
}
|
|
4247
5272
|
chown(path, ownership) {
|
|
@@ -4250,6 +5275,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4250
5275
|
if (!node) {
|
|
4251
5276
|
throw new Error(`chown: ${path}: No such file or directory`);
|
|
4252
5277
|
}
|
|
5278
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
|
|
5279
|
+
this.deny("chown", path, "Operation not permitted");
|
|
5280
|
+
}
|
|
4253
5281
|
node.ownership = ownership;
|
|
4254
5282
|
}
|
|
4255
5283
|
// --- Persistence ---
|
|
@@ -4280,8 +5308,10 @@ var IndexedDbFileSystemService = class {
|
|
|
4280
5308
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
4281
5309
|
const store = tx.objectStore(STORE_NAME);
|
|
4282
5310
|
store.put(this.root, ROOT_KEY);
|
|
4283
|
-
|
|
4284
|
-
|
|
5311
|
+
if (!this.currentUid) {
|
|
5312
|
+
store.put(this.cwd, CWD_KEY);
|
|
5313
|
+
store.put(this.homePath, HOME_KEY);
|
|
5314
|
+
}
|
|
4285
5315
|
await new Promise((resolve, reject) => {
|
|
4286
5316
|
tx.oncomplete = () => resolve();
|
|
4287
5317
|
tx.onerror = () => reject(tx.error);
|
|
@@ -4291,6 +5321,18 @@ var IndexedDbFileSystemService = class {
|
|
|
4291
5321
|
}
|
|
4292
5322
|
}
|
|
4293
5323
|
// --- Private helpers ---
|
|
5324
|
+
/** A copy belongs to whoever made it (like `cp` without `-p`) */
|
|
5325
|
+
ownedByCurrentUser(node) {
|
|
5326
|
+
const owner = this.getDefaultOwnership();
|
|
5327
|
+
if (owner) {
|
|
5328
|
+
const apply = (n) => {
|
|
5329
|
+
n.ownership = { ...owner };
|
|
5330
|
+
n.children?.forEach(apply);
|
|
5331
|
+
};
|
|
5332
|
+
apply(node);
|
|
5333
|
+
}
|
|
5334
|
+
return node;
|
|
5335
|
+
}
|
|
4294
5336
|
cloneNode(node) {
|
|
4295
5337
|
const now = Date.now();
|
|
4296
5338
|
const clone = {
|
|
@@ -4332,8 +5374,6 @@ var IndexedDbFileSystemService = class {
|
|
|
4332
5374
|
});
|
|
4333
5375
|
}
|
|
4334
5376
|
};
|
|
4335
|
-
|
|
4336
|
-
// src/lib/services/server-file-system.service.ts
|
|
4337
5377
|
var DEFAULT_HOME2 = "/home/user";
|
|
4338
5378
|
function createSeedFileSystem2() {
|
|
4339
5379
|
const now = Date.now();
|
|
@@ -4383,6 +5423,7 @@ var ServerFileSystemService = class {
|
|
|
4383
5423
|
this.currentUid = null;
|
|
4384
5424
|
this.currentGroups = [];
|
|
4385
5425
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
5426
|
+
this.headersProvider = options.headers;
|
|
4386
5427
|
}
|
|
4387
5428
|
// --- User context ---
|
|
4388
5429
|
setCurrentUser(uid, groups) {
|
|
@@ -4699,15 +5740,20 @@ var ServerFileSystemService = class {
|
|
|
4699
5740
|
}
|
|
4700
5741
|
const destResolved = this.resolvePath(dest);
|
|
4701
5742
|
const destNode = this.getNode(destResolved);
|
|
5743
|
+
if (srcNode.type === "directory" && (destResolved === srcResolved || destResolved.startsWith(srcResolved + "/"))) {
|
|
5744
|
+
throw new Error(
|
|
5745
|
+
`mv: cannot move '${src}' to a subdirectory of itself, '${dest}'`
|
|
5746
|
+
);
|
|
5747
|
+
}
|
|
4702
5748
|
const srcParts = srcResolved.split("/").filter(Boolean);
|
|
4703
5749
|
const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
|
|
4704
5750
|
const srcParent = this.getNode(srcParentPath);
|
|
4705
5751
|
if (!srcParent || !srcParent.children) {
|
|
4706
5752
|
throw new Error(`mv: ${src}: No such file or directory`);
|
|
4707
5753
|
}
|
|
4708
|
-
|
|
4709
|
-
(
|
|
4710
|
-
|
|
5754
|
+
if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
|
|
5755
|
+
throw new Error(`mv: '${src}' and '${dest}' are the same file`);
|
|
5756
|
+
}
|
|
4711
5757
|
if (destNode && destNode.type === "directory") {
|
|
4712
5758
|
if (!destNode.children) {
|
|
4713
5759
|
destNode.children = [];
|
|
@@ -4718,7 +5764,7 @@ var ServerFileSystemService = class {
|
|
|
4718
5764
|
if (existingIdx !== -1) {
|
|
4719
5765
|
destNode.children.splice(existingIdx, 1);
|
|
4720
5766
|
}
|
|
4721
|
-
srcParent.children.splice(
|
|
5767
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4722
5768
|
destNode.children.push(srcNode);
|
|
4723
5769
|
destNode.modifiedAt = Date.now();
|
|
4724
5770
|
} else {
|
|
@@ -4731,7 +5777,7 @@ var ServerFileSystemService = class {
|
|
|
4731
5777
|
`mv: ${destParentPath}: No such file or directory`
|
|
4732
5778
|
);
|
|
4733
5779
|
}
|
|
4734
|
-
srcParent.children.splice(
|
|
5780
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4735
5781
|
srcNode.name = newName;
|
|
4736
5782
|
if (!destParent.children) {
|
|
4737
5783
|
destParent.children = [];
|
|
@@ -4790,7 +5836,14 @@ var ServerFileSystemService = class {
|
|
|
4790
5836
|
// --- Private: server communication ---
|
|
4791
5837
|
async serverFetch(endpoint, options) {
|
|
4792
5838
|
const url = `${this.baseUrl}/api/qcli/fs${endpoint}`;
|
|
4793
|
-
const
|
|
5839
|
+
const mergedHeaders = {
|
|
5840
|
+
...cliCore.resolveHeaders(this.headersProvider),
|
|
5841
|
+
...options?.headers ?? {}
|
|
5842
|
+
};
|
|
5843
|
+
const response = await fetch(url, {
|
|
5844
|
+
...options,
|
|
5845
|
+
headers: mergedHeaders
|
|
5846
|
+
});
|
|
4794
5847
|
if (!response.ok) {
|
|
4795
5848
|
const body = await response.text();
|
|
4796
5849
|
throw new Error(
|
|
@@ -4959,6 +6012,115 @@ var VirtualFsFileTransferService = class {
|
|
|
4959
6012
|
return this._browserService.uploadFromBrowser(accept);
|
|
4960
6013
|
}
|
|
4961
6014
|
};
|
|
6015
|
+
var CliFilesPermissions = {
|
|
6016
|
+
/** Bypass all file permission checks */
|
|
6017
|
+
Admin: "files:admin",
|
|
6018
|
+
/** chmod/chown files you do not own */
|
|
6019
|
+
Chown: "files:chown"
|
|
6020
|
+
};
|
|
6021
|
+
var HOME_DIR_PERMISSIONS = "rwxr-x---";
|
|
6022
|
+
function authorization(context) {
|
|
6023
|
+
try {
|
|
6024
|
+
return context.services.get(cliCore.ICliAuthorizationService_TOKEN);
|
|
6025
|
+
} catch {
|
|
6026
|
+
return void 0;
|
|
6027
|
+
}
|
|
6028
|
+
}
|
|
6029
|
+
function createFileAccessPolicy(context) {
|
|
6030
|
+
return {
|
|
6031
|
+
// Nodes without an owner (system seed, legacy data) belong to root:admin
|
|
6032
|
+
canAccess: (node, action) => cliCore.cliCanAccess(
|
|
6033
|
+
context,
|
|
6034
|
+
node.ownership ? node : { ...node, ownership: SYSTEM_OWNERSHIP },
|
|
6035
|
+
action,
|
|
6036
|
+
[CliFilesPermissions.Admin]
|
|
6037
|
+
),
|
|
6038
|
+
canChangeOwnership: (node, newOwnership) => {
|
|
6039
|
+
const auth = authorization(context);
|
|
6040
|
+
if (!auth) return true;
|
|
6041
|
+
if (auth.can(CliFilesPermissions.Admin) || auth.can(CliFilesPermissions.Chown)) return true;
|
|
6042
|
+
const principal = auth.getPrincipal();
|
|
6043
|
+
if (!principal) return false;
|
|
6044
|
+
const owns = !!node.ownership && (node.ownership.uid === principal.name || node.ownership.uid === principal.id);
|
|
6045
|
+
const keepsOwner = !newOwnership || newOwnership.uid === node.ownership?.uid || newOwnership.uid === principal.name;
|
|
6046
|
+
return owns && keepsOwner;
|
|
6047
|
+
}
|
|
6048
|
+
};
|
|
6049
|
+
}
|
|
6050
|
+
function followUserSession(context, fs) {
|
|
6051
|
+
let sessions;
|
|
6052
|
+
try {
|
|
6053
|
+
sessions = context.services.get(cliCore.ICliUserSessionService_TOKEN);
|
|
6054
|
+
} catch {
|
|
6055
|
+
sessions = void 0;
|
|
6056
|
+
}
|
|
6057
|
+
if (!sessions) return void 0;
|
|
6058
|
+
let previousUser = null;
|
|
6059
|
+
return sessions.getUserSession().subscribe((session) => {
|
|
6060
|
+
if (!session?.user) {
|
|
6061
|
+
fs.setCurrentUser("", []);
|
|
6062
|
+
if (previousUser !== null || !canEnter(fs, fs.getCurrentDirectory())) {
|
|
6063
|
+
try {
|
|
6064
|
+
fs.setCurrentDirectory("/");
|
|
6065
|
+
void fs.persist();
|
|
6066
|
+
} catch {
|
|
6067
|
+
}
|
|
6068
|
+
}
|
|
6069
|
+
previousUser = null;
|
|
6070
|
+
return;
|
|
6071
|
+
}
|
|
6072
|
+
const user = session.user;
|
|
6073
|
+
fs.setCurrentUser(user.name, user.groups ?? []);
|
|
6074
|
+
if (user.homeDir) {
|
|
6075
|
+
const home = user.homeDir;
|
|
6076
|
+
const system = (fn) => fs.runAsSystem ? fs.runAsSystem(fn) : fn();
|
|
6077
|
+
if (!fs.exists(home)) {
|
|
6078
|
+
system(() => {
|
|
6079
|
+
fs.createDirectory(home, true);
|
|
6080
|
+
if (home !== "/") {
|
|
6081
|
+
fs.chown(home, { uid: user.name, gid: user.groups?.[0] ?? "users" });
|
|
6082
|
+
fs.chmod(home, HOME_DIR_PERMISSIONS);
|
|
6083
|
+
}
|
|
6084
|
+
});
|
|
6085
|
+
void fs.persist();
|
|
6086
|
+
}
|
|
6087
|
+
fs.setHomePath(home);
|
|
6088
|
+
if (previousUser !== user.id) {
|
|
6089
|
+
try {
|
|
6090
|
+
fs.setCurrentDirectory(home);
|
|
6091
|
+
} catch {
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
}
|
|
6095
|
+
previousUser = user.id;
|
|
6096
|
+
});
|
|
6097
|
+
}
|
|
6098
|
+
function canEnter(fs, path) {
|
|
6099
|
+
try {
|
|
6100
|
+
fs.setCurrentDirectory(path);
|
|
6101
|
+
return true;
|
|
6102
|
+
} catch {
|
|
6103
|
+
return false;
|
|
6104
|
+
}
|
|
6105
|
+
}
|
|
6106
|
+
function followUserAccounts(context, fs) {
|
|
6107
|
+
let events;
|
|
6108
|
+
try {
|
|
6109
|
+
events = context.services.get(cliCore.ICliUserEventService_TOKEN);
|
|
6110
|
+
} catch {
|
|
6111
|
+
events = void 0;
|
|
6112
|
+
}
|
|
6113
|
+
if (!events || !fs.reassignOwner) return void 0;
|
|
6114
|
+
return events.events$.subscribe((event) => {
|
|
6115
|
+
let changed = 0;
|
|
6116
|
+
if (event.type === "user_updated" && event.previousName && event.previousName !== event.user?.name) {
|
|
6117
|
+
changed = fs.reassignOwner(event.previousName, event.user.name);
|
|
6118
|
+
} else if (event.type === "user_deleted" && event.user?.name) {
|
|
6119
|
+
changed = fs.reassignOwner(event.user.name, SYSTEM_OWNERSHIP.uid);
|
|
6120
|
+
}
|
|
6121
|
+
if (changed > 0) void fs.persist().catch(() => void 0);
|
|
6122
|
+
});
|
|
6123
|
+
}
|
|
4962
6124
|
|
|
4963
6125
|
// src/lib/completion/file-path-completion-provider.ts
|
|
4964
6126
|
var FILE_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -4979,7 +6141,26 @@ var FILE_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
4979
6141
|
"find",
|
|
4980
6142
|
"head",
|
|
4981
6143
|
"tail",
|
|
4982
|
-
"wc"
|
|
6144
|
+
"wc",
|
|
6145
|
+
"sort",
|
|
6146
|
+
"uniq",
|
|
6147
|
+
"cut",
|
|
6148
|
+
"paste",
|
|
6149
|
+
"tac",
|
|
6150
|
+
"diff",
|
|
6151
|
+
"sed",
|
|
6152
|
+
"awk",
|
|
6153
|
+
"du",
|
|
6154
|
+
"stat",
|
|
6155
|
+
"chmod",
|
|
6156
|
+
"chown",
|
|
6157
|
+
"ln",
|
|
6158
|
+
"tee",
|
|
6159
|
+
"basename",
|
|
6160
|
+
"dirname",
|
|
6161
|
+
"sh",
|
|
6162
|
+
"source",
|
|
6163
|
+
"xargs"
|
|
4983
6164
|
]);
|
|
4984
6165
|
var FilePathCompletionProvider = class {
|
|
4985
6166
|
constructor(fs) {
|
|
@@ -4998,7 +6179,14 @@ var FilePathCompletionProvider = class {
|
|
|
4998
6179
|
if (token.startsWith("-")) {
|
|
4999
6180
|
return [];
|
|
5000
6181
|
}
|
|
5001
|
-
|
|
6182
|
+
const quote = token.startsWith('"') || token.startsWith("'") ? token[0] : "";
|
|
6183
|
+
const partial = quote ? token.slice(1).replace(new RegExp(`${quote}$`), "") : token;
|
|
6184
|
+
const paths = this.completePath(partial);
|
|
6185
|
+
if (!quote && !paths.some((p) => /\s/.test(p))) {
|
|
6186
|
+
return paths;
|
|
6187
|
+
}
|
|
6188
|
+
const q = quote || '"';
|
|
6189
|
+
return paths.map((p) => p.endsWith("/") ? `${q}${p}` : `${q}${p}${q}`);
|
|
5002
6190
|
}
|
|
5003
6191
|
completePath(partial) {
|
|
5004
6192
|
let dirPath;
|
|
@@ -5059,7 +6247,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5059
6247
|
];
|
|
5060
6248
|
}
|
|
5061
6249
|
async processCommand(command, context) {
|
|
5062
|
-
const fs = context.services.
|
|
6250
|
+
const fs = context.services.getRequired(
|
|
5063
6251
|
IFileSystemService_TOKEN
|
|
5064
6252
|
);
|
|
5065
6253
|
const fileService = context.services.get(
|
|
@@ -5072,6 +6260,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5072
6260
|
const accept = command.args?.["accept"];
|
|
5073
6261
|
const destPath = command.value?.trim() || void 0;
|
|
5074
6262
|
context.spinner?.show("Waiting for file selection...");
|
|
6263
|
+
context.notifier.info("Waiting for file selection");
|
|
5075
6264
|
const picked = await fileService.uploadFromBrowser(accept);
|
|
5076
6265
|
if (!picked) {
|
|
5077
6266
|
context.spinner?.hide();
|
|
@@ -5084,6 +6273,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5084
6273
|
filename.startsWith("/") ? filename : cwd + "/" + filename
|
|
5085
6274
|
);
|
|
5086
6275
|
context.spinner?.show(`Saving "${picked.name}" (${picked.content.length} bytes)...`);
|
|
6276
|
+
context.notifier.info(`Saving ${picked.name}`);
|
|
5087
6277
|
try {
|
|
5088
6278
|
fs.writeFile(resolved, picked.content);
|
|
5089
6279
|
await fs.persist();
|
|
@@ -5151,6 +6341,10 @@ var filesModule = {
|
|
|
5151
6341
|
new CliShCommandProcessor(),
|
|
5152
6342
|
new CliUploadCommandProcessor()
|
|
5153
6343
|
],
|
|
6344
|
+
permissions: [
|
|
6345
|
+
{ name: CliFilesPermissions.Admin, description: "Bypass file permission checks" },
|
|
6346
|
+
{ name: CliFilesPermissions.Chown, description: "chmod/chown files owned by others" }
|
|
6347
|
+
],
|
|
5154
6348
|
services: [
|
|
5155
6349
|
{
|
|
5156
6350
|
provide: IFileSystemService_TOKEN,
|
|
@@ -5182,7 +6376,7 @@ var filesModule = {
|
|
|
5182
6376
|
return { ...this, config };
|
|
5183
6377
|
},
|
|
5184
6378
|
async onInit(context) {
|
|
5185
|
-
const fs = context.services.
|
|
6379
|
+
const fs = context.services.getRequired(
|
|
5186
6380
|
IFileSystemService_TOKEN
|
|
5187
6381
|
);
|
|
5188
6382
|
await fs.initialize();
|
|
@@ -5193,7 +6387,7 @@ var filesModule = {
|
|
|
5193
6387
|
}
|
|
5194
6388
|
let dragDrop;
|
|
5195
6389
|
try {
|
|
5196
|
-
dragDrop = context.services.
|
|
6390
|
+
dragDrop = context.services.getRequired(cliCore.ICliDragDropService_TOKEN);
|
|
5197
6391
|
} catch {
|
|
5198
6392
|
}
|
|
5199
6393
|
if (dragDrop) {
|
|
@@ -5222,13 +6416,26 @@ var filesModule = {
|
|
|
5222
6416
|
});
|
|
5223
6417
|
}
|
|
5224
6418
|
},
|
|
6419
|
+
async onAfterBoot(context) {
|
|
6420
|
+
const fs = context.services.getRequired(IFileSystemService_TOKEN);
|
|
6421
|
+
fs.setAccessPolicy?.(createFileAccessPolicy(context));
|
|
6422
|
+
this._sessionSubscription?.unsubscribe();
|
|
6423
|
+
this._sessionSubscription = followUserSession(context, fs);
|
|
6424
|
+
this._accountsSubscription?.unsubscribe();
|
|
6425
|
+
this._accountsSubscription = followUserAccounts(context, fs);
|
|
6426
|
+
},
|
|
5225
6427
|
async onDestroy(_context) {
|
|
5226
6428
|
const sub = this._dragDropSubscription;
|
|
5227
6429
|
sub?.unsubscribe();
|
|
5228
6430
|
this._dragDropSubscription = void 0;
|
|
6431
|
+
this._sessionSubscription?.unsubscribe();
|
|
6432
|
+
this._sessionSubscription = void 0;
|
|
6433
|
+
this._accountsSubscription?.unsubscribe();
|
|
6434
|
+
this._accountsSubscription = void 0;
|
|
5229
6435
|
}
|
|
5230
6436
|
};
|
|
5231
6437
|
|
|
6438
|
+
exports.ANONYMOUS_OWNERSHIP = ANONYMOUS_OWNERSHIP;
|
|
5232
6439
|
exports.CliAwkCommandProcessor = CliAwkCommandProcessor;
|
|
5233
6440
|
exports.CliBasenameCommandProcessor = CliBasenameCommandProcessor;
|
|
5234
6441
|
exports.CliCatCommandProcessor = CliCatCommandProcessor;
|
|
@@ -5241,6 +6448,7 @@ exports.CliDiffCommandProcessor = CliDiffCommandProcessor;
|
|
|
5241
6448
|
exports.CliDirnameCommandProcessor = CliDirnameCommandProcessor;
|
|
5242
6449
|
exports.CliDuCommandProcessor = CliDuCommandProcessor;
|
|
5243
6450
|
exports.CliEchoCommandProcessor = CliEchoCommandProcessor;
|
|
6451
|
+
exports.CliFilesPermissions = CliFilesPermissions;
|
|
5244
6452
|
exports.CliFindCommandProcessor = CliFindCommandProcessor;
|
|
5245
6453
|
exports.CliGrepCommandProcessor = CliGrepCommandProcessor;
|
|
5246
6454
|
exports.CliHeadCommandProcessor = CliHeadCommandProcessor;
|
|
@@ -5266,11 +6474,16 @@ exports.CliUniqCommandProcessor = CliUniqCommandProcessor;
|
|
|
5266
6474
|
exports.CliWcCommandProcessor = CliWcCommandProcessor;
|
|
5267
6475
|
exports.CliXargsCommandProcessor = CliXargsCommandProcessor;
|
|
5268
6476
|
exports.FilePathCompletionProvider = FilePathCompletionProvider;
|
|
6477
|
+
exports.HOME_DIR_PERMISSIONS = HOME_DIR_PERMISSIONS;
|
|
5269
6478
|
exports.IFileSystemService_TOKEN = IFileSystemService_TOKEN;
|
|
5270
6479
|
exports.IndexedDbFileSystemService = IndexedDbFileSystemService;
|
|
6480
|
+
exports.SYSTEM_OWNERSHIP = SYSTEM_OWNERSHIP;
|
|
5271
6481
|
exports.ServerFileSystemService = ServerFileSystemService;
|
|
5272
6482
|
exports.VirtualFsFileTransferService = VirtualFsFileTransferService;
|
|
6483
|
+
exports.createFileAccessPolicy = createFileAccessPolicy;
|
|
5273
6484
|
exports.executeScript = executeScript;
|
|
5274
6485
|
exports.filesModule = filesModule;
|
|
6486
|
+
exports.followUserAccounts = followUserAccounts;
|
|
6487
|
+
exports.followUserSession = followUserSession;
|
|
5275
6488
|
//# sourceMappingURL=public-api.js.map
|
|
5276
6489
|
//# sourceMappingURL=public-api.js.map
|