@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.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, ICliDragDropService_TOKEN } from '@qodalis/cli-core';
|
|
1
|
+
import { createCliToken, ICliCompletionProvider_TOKEN, ICliFileTransferService_TOKEN, CliAccessDeniedError, BrowserFileTransferService, DefaultLibraryAuthor, CliForegroundColor, writeWrapped, writeUsage, writeExamples, joinShellWords, quoteShellWord, resolveHeaders, cliCanAccess, ICliUserSessionService_TOKEN, ICliUserEventService_TOKEN, ICliDragDropService_TOKEN, splitShellWords, isCliAccessDeniedError, ICliAuthorizationService_TOKEN } from '@qodalis/cli-core';
|
|
2
2
|
|
|
3
3
|
// src/lib/interfaces/i-file-system-service.ts
|
|
4
|
-
var IFileSystemService_TOKEN = "cli-file-system-service";
|
|
4
|
+
var IFileSystemService_TOKEN = createCliToken("cli-file-system-service");
|
|
5
5
|
|
|
6
6
|
// src/lib/version.ts
|
|
7
|
-
var LIBRARY_VERSION = "2.0.0-
|
|
7
|
+
var LIBRARY_VERSION = "2.0.0-rc.2";
|
|
8
8
|
var API_VERSION = 2;
|
|
9
|
+
function splitArgs(raw) {
|
|
10
|
+
return splitShellWords(raw);
|
|
11
|
+
}
|
|
9
12
|
|
|
10
13
|
// src/lib/processors/cli-ls-command-processor.ts
|
|
11
14
|
var CliLsCommandProcessor = class {
|
|
@@ -30,41 +33,63 @@ var CliLsCommandProcessor = class {
|
|
|
30
33
|
description: "Use long listing format",
|
|
31
34
|
required: false,
|
|
32
35
|
type: "boolean"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
name: "human-readable",
|
|
39
|
+
aliases: ["h"],
|
|
40
|
+
description: "With -l, print sizes like 1.5K and 2M",
|
|
41
|
+
required: false,
|
|
42
|
+
type: "boolean"
|
|
33
43
|
}
|
|
34
44
|
];
|
|
45
|
+
this.humanReadable = false;
|
|
35
46
|
}
|
|
36
47
|
async processCommand(command, context) {
|
|
37
|
-
const fs = context.services.
|
|
48
|
+
const fs = context.services.getRequired(
|
|
38
49
|
IFileSystemService_TOKEN
|
|
39
50
|
);
|
|
40
|
-
const
|
|
51
|
+
const paths = splitArgs(command.value || "");
|
|
41
52
|
const showAll = command.args["all"] || command.args["a"];
|
|
42
53
|
const longFormat = command.args["long"] || command.args["l"];
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
54
|
+
this.humanReadable = !!(command.args["human-readable"] || command.args["h"]);
|
|
55
|
+
const targets = paths.length > 0 ? paths : [fs.getCurrentDirectory()];
|
|
56
|
+
const piped = [];
|
|
57
|
+
targets.forEach((targetPath, index) => {
|
|
58
|
+
try {
|
|
59
|
+
const node = fs.getNode(targetPath);
|
|
60
|
+
const entries = node && node.type !== "directory" ? [{ ...node, name: targetPath }] : fs.listDirectory(targetPath);
|
|
61
|
+
const filtered = showAll ? entries : entries.filter((e) => !e.name.startsWith("."));
|
|
62
|
+
if (targets.length > 1) {
|
|
63
|
+
if (index > 0) context.writer.writeln();
|
|
64
|
+
context.writer.writeln(`${targetPath}:`);
|
|
52
65
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
66
|
+
if (filtered.length === 0) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
filtered.sort((a, b) => {
|
|
70
|
+
if (a.type !== b.type) {
|
|
71
|
+
return a.type === "directory" ? -1 : 1;
|
|
72
|
+
}
|
|
73
|
+
return a.name.localeCompare(b.name);
|
|
74
|
+
});
|
|
75
|
+
if (longFormat) {
|
|
76
|
+
this.writeLongFormat(filtered, context);
|
|
77
|
+
} else {
|
|
78
|
+
this.writeShortFormat(filtered, context);
|
|
79
|
+
piped.push(...filtered.map((e) => e.name));
|
|
80
|
+
}
|
|
81
|
+
} catch (e) {
|
|
82
|
+
context.writer.writeError(e.message);
|
|
59
83
|
}
|
|
60
|
-
}
|
|
61
|
-
|
|
84
|
+
});
|
|
85
|
+
if (!longFormat && piped.length > 0) {
|
|
86
|
+
context.process.output(piped.join("\n"));
|
|
62
87
|
}
|
|
63
88
|
}
|
|
64
89
|
writeDescription(context) {
|
|
65
90
|
context.writer.writeln(this.description);
|
|
66
91
|
context.writer.writeln();
|
|
67
|
-
context.writer.writeln("Usage: ls [path] [
|
|
92
|
+
context.writer.writeln("Usage: ls [path...] [-a|--all] [-l|--long] [-h|--human-readable]");
|
|
68
93
|
}
|
|
69
94
|
writeLongFormat(entries, context) {
|
|
70
95
|
const { writer } = context;
|
|
@@ -73,7 +98,7 @@ var CliLsCommandProcessor = class {
|
|
|
73
98
|
const perms = entry.permissions || (entry.type === "directory" ? "rwxr-xr-x" : "rw-r--r--");
|
|
74
99
|
const owner = entry.ownership?.uid || "-";
|
|
75
100
|
const group = entry.ownership?.gid || "-";
|
|
76
|
-
const size = entry.type
|
|
101
|
+
const size = entry.type !== "file" ? "-" : this.humanReadable ? this.formatSize(entry.size) : entry.size.toString();
|
|
77
102
|
const date = new Date(entry.modifiedAt);
|
|
78
103
|
const dateStr = date.toLocaleDateString("en-US", {
|
|
79
104
|
month: "short",
|
|
@@ -87,6 +112,17 @@ var CliLsCommandProcessor = class {
|
|
|
87
112
|
);
|
|
88
113
|
}
|
|
89
114
|
}
|
|
115
|
+
formatSize(bytes) {
|
|
116
|
+
const units = ["K", "M", "G", "T"];
|
|
117
|
+
let size = bytes;
|
|
118
|
+
let unit = "";
|
|
119
|
+
for (const next of units) {
|
|
120
|
+
if (size < 1024) break;
|
|
121
|
+
size /= 1024;
|
|
122
|
+
unit = next;
|
|
123
|
+
}
|
|
124
|
+
return unit ? `${size < 10 ? size.toFixed(1) : Math.round(size)}${unit}` : String(bytes);
|
|
125
|
+
}
|
|
90
126
|
writeShortFormat(entries, context) {
|
|
91
127
|
const { writer } = context;
|
|
92
128
|
const names = entries.map(
|
|
@@ -104,11 +140,30 @@ var CliCdCommandProcessor = class {
|
|
|
104
140
|
this.acceptsRawInput = true;
|
|
105
141
|
this.metadata = { icon: "\u{1F4C1}", module: "file management" };
|
|
106
142
|
}
|
|
143
|
+
writeDescription(context) {
|
|
144
|
+
const t = context.translator;
|
|
145
|
+
writeWrapped(
|
|
146
|
+
context,
|
|
147
|
+
t.t("cli.cd.help.change_current_directory_without", "Change the current directory. Without a path it goes to your home directory. Paths can be absolute (/...), relative to where you are, or start with ~ for home.")
|
|
148
|
+
);
|
|
149
|
+
context.writer.writeln();
|
|
150
|
+
writeUsage(context, [
|
|
151
|
+
["cd", t.t("cli.cd.help.go_home_directory", "Go to your home directory")],
|
|
152
|
+
["cd <dir>", t.t("cli.cd.help.go_directory", "Go into a directory")],
|
|
153
|
+
["cd ..", t.t("cli.cd.help.go_up_one_level", "Go up one level")]
|
|
154
|
+
]);
|
|
155
|
+
context.writer.writeln();
|
|
156
|
+
writeExamples(context, [
|
|
157
|
+
["cd ~/docs", t.t("cli.cd.help.go_docs_home", "Go to docs in your home")],
|
|
158
|
+
["cd ../projects", t.t("cli.cd.help.go_sibling_directory", "Go to a sibling directory")],
|
|
159
|
+
["cd /", t.t("cli.cd.help.go_root", "Go to the root")]
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
107
162
|
async processCommand(command, context) {
|
|
108
|
-
const fs = context.services.
|
|
163
|
+
const fs = context.services.getRequired(
|
|
109
164
|
IFileSystemService_TOKEN
|
|
110
165
|
);
|
|
111
|
-
const target = command.value || "~";
|
|
166
|
+
const target = splitArgs(command.value || "").join(" ") || "~";
|
|
112
167
|
try {
|
|
113
168
|
fs.setCurrentDirectory(target);
|
|
114
169
|
await fs.persist();
|
|
@@ -125,8 +180,24 @@ var CliPwdCommandProcessor = class {
|
|
|
125
180
|
this.version = LIBRARY_VERSION;
|
|
126
181
|
this.metadata = { icon: "\u{1F4C2}", module: "file management" };
|
|
127
182
|
}
|
|
183
|
+
writeDescription(context) {
|
|
184
|
+
const t = context.translator;
|
|
185
|
+
writeWrapped(
|
|
186
|
+
context,
|
|
187
|
+
t.t("cli.pwd.help.print_full_path_directory", "Print the full path of the directory you are in. Relative paths in other commands start from here.")
|
|
188
|
+
);
|
|
189
|
+
context.writer.writeln();
|
|
190
|
+
writeUsage(context, [
|
|
191
|
+
["pwd", t.t("cli.pwd.help.show_current_directory", "Show the current directory")]
|
|
192
|
+
]);
|
|
193
|
+
context.writer.writeln();
|
|
194
|
+
writeExamples(context, [
|
|
195
|
+
["pwd", t.t("cli.pwd.help.example_home_user", "For example /home/user")],
|
|
196
|
+
["cd ~/docs; pwd", t.t("cli.pwd.help.prints_home_user_docs", "Prints /home/user/docs")]
|
|
197
|
+
]);
|
|
198
|
+
}
|
|
128
199
|
async processCommand(command, context) {
|
|
129
|
-
const fs = context.services.
|
|
200
|
+
const fs = context.services.getRequired(
|
|
130
201
|
IFileSystemService_TOKEN
|
|
131
202
|
);
|
|
132
203
|
context.writer.writeln(fs.getCurrentDirectory());
|
|
@@ -151,22 +222,44 @@ var CliMkdirCommandProcessor = class {
|
|
|
151
222
|
}
|
|
152
223
|
];
|
|
153
224
|
}
|
|
225
|
+
writeDescription(context) {
|
|
226
|
+
const t = context.translator;
|
|
227
|
+
writeWrapped(
|
|
228
|
+
context,
|
|
229
|
+
t.t("cli.mkdir.help.create_new_directory_p", "Create a new directory. With -p, any missing parent directories are created too.")
|
|
230
|
+
);
|
|
231
|
+
context.writer.writeln();
|
|
232
|
+
writeUsage(context, [
|
|
233
|
+
["mkdir <dir...>", t.t("cli.mkdir.help.create_one_more_directories", "Create one or more directories")],
|
|
234
|
+
["mkdir -p <path>", t.t("cli.mkdir.help.create_directory_missing_parents", "Create a directory and its missing parents")]
|
|
235
|
+
]);
|
|
236
|
+
context.writer.writeln();
|
|
237
|
+
writeExamples(context, [
|
|
238
|
+
["mkdir projects", t.t("cli.mkdir.help.create_projects_current_directory", "Create projects in the current directory")],
|
|
239
|
+
["mkdir -p ~/docs/2026/notes", t.t("cli.mkdir.help.create_whole_path_once", "Create the whole path at once")]
|
|
240
|
+
]);
|
|
241
|
+
}
|
|
154
242
|
async processCommand(command, context) {
|
|
155
|
-
const fs = context.services.
|
|
243
|
+
const fs = context.services.getRequired(
|
|
156
244
|
IFileSystemService_TOKEN
|
|
157
245
|
);
|
|
158
|
-
const
|
|
246
|
+
const paths = splitArgs(command.value || "");
|
|
159
247
|
const recursive = command.args["parents"] || command.args["p"];
|
|
160
|
-
if (
|
|
248
|
+
if (paths.length === 0) {
|
|
161
249
|
context.writer.writeError("mkdir: missing operand");
|
|
162
250
|
return;
|
|
163
251
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
252
|
+
let changed = false;
|
|
253
|
+
for (const path of paths) {
|
|
254
|
+
try {
|
|
255
|
+
if (recursive && fs.isDirectory(path)) continue;
|
|
256
|
+
fs.createDirectory(path, recursive);
|
|
257
|
+
changed = true;
|
|
258
|
+
} catch (e) {
|
|
259
|
+
context.writer.writeError(e.message);
|
|
260
|
+
}
|
|
169
261
|
}
|
|
262
|
+
if (changed) await fs.persist();
|
|
170
263
|
}
|
|
171
264
|
};
|
|
172
265
|
var CliRmdirCommandProcessor = class {
|
|
@@ -179,38 +272,58 @@ var CliRmdirCommandProcessor = class {
|
|
|
179
272
|
this.valueRequired = true;
|
|
180
273
|
this.metadata = { icon: "\u{1F5D1}", module: "file management" };
|
|
181
274
|
}
|
|
275
|
+
writeDescription(context) {
|
|
276
|
+
const t = context.translator;
|
|
277
|
+
writeWrapped(
|
|
278
|
+
context,
|
|
279
|
+
t.t("cli.rmdir.help.remove_empty_directory_refuses", "Remove an empty directory. It refuses to remove a directory that still contains files; use rm -r for that.")
|
|
280
|
+
);
|
|
281
|
+
context.writer.writeln();
|
|
282
|
+
writeUsage(context, [
|
|
283
|
+
["rmdir <dir...>", t.t("cli.rmdir.help.remove_empty_directories", "Remove empty directories")]
|
|
284
|
+
]);
|
|
285
|
+
context.writer.writeln();
|
|
286
|
+
writeExamples(context, [
|
|
287
|
+
["rmdir empty-folder", t.t("cli.rmdir.help.remove_directory_current_one", "Remove a directory in the current one")],
|
|
288
|
+
["rmdir ~/docs/old", t.t("cli.rmdir.help.remove_full_path", "Remove by full path")]
|
|
289
|
+
]);
|
|
290
|
+
}
|
|
182
291
|
async processCommand(command, context) {
|
|
183
|
-
const fs = context.services.
|
|
292
|
+
const fs = context.services.getRequired(
|
|
184
293
|
IFileSystemService_TOKEN
|
|
185
294
|
);
|
|
186
|
-
const
|
|
187
|
-
if (
|
|
295
|
+
const paths = splitArgs(command.value || "");
|
|
296
|
+
if (paths.length === 0) {
|
|
188
297
|
context.writer.writeError("rmdir: missing operand");
|
|
189
298
|
return;
|
|
190
299
|
}
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
300
|
+
let changed = false;
|
|
301
|
+
for (const path of paths) {
|
|
302
|
+
try {
|
|
303
|
+
const node = fs.getNode(path);
|
|
304
|
+
if (!node) {
|
|
305
|
+
context.writer.writeError(
|
|
306
|
+
`rmdir: ${path}: No such file or directory`
|
|
307
|
+
);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
if (node.type !== "directory") {
|
|
311
|
+
context.writer.writeError(`rmdir: ${path}: Not a directory`);
|
|
312
|
+
continue;
|
|
313
|
+
}
|
|
314
|
+
if (node.children && node.children.length > 0) {
|
|
315
|
+
context.writer.writeError(
|
|
316
|
+
`rmdir: ${path}: Directory not empty`
|
|
317
|
+
);
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
fs.remove(path, true);
|
|
321
|
+
changed = true;
|
|
322
|
+
} catch (e) {
|
|
323
|
+
context.writer.writeError(e.message);
|
|
208
324
|
}
|
|
209
|
-
fs.remove(path);
|
|
210
|
-
await fs.persist();
|
|
211
|
-
} catch (e) {
|
|
212
|
-
context.writer.writeError(e.message);
|
|
213
325
|
}
|
|
326
|
+
if (changed) await fs.persist();
|
|
214
327
|
}
|
|
215
328
|
};
|
|
216
329
|
var CliTouchCommandProcessor = class {
|
|
@@ -223,23 +336,56 @@ var CliTouchCommandProcessor = class {
|
|
|
223
336
|
this.valueRequired = true;
|
|
224
337
|
this.metadata = { icon: "\u{1F4C4}", module: "file management" };
|
|
225
338
|
}
|
|
339
|
+
writeDescription(context) {
|
|
340
|
+
const t = context.translator;
|
|
341
|
+
writeWrapped(
|
|
342
|
+
context,
|
|
343
|
+
t.t("cli.touch.help.create_empty_file_update", "Create an empty file, or update the modified time of a file that already exists. The parent directory must already exist.")
|
|
344
|
+
);
|
|
345
|
+
context.writer.writeln();
|
|
346
|
+
writeUsage(context, [
|
|
347
|
+
["touch <file...>", t.t("cli.touch.help.create_files_refresh_their", "Create the files, or refresh their timestamps")]
|
|
348
|
+
]);
|
|
349
|
+
context.writer.writeln();
|
|
350
|
+
writeExamples(context, [
|
|
351
|
+
["touch notes.txt", t.t("cli.touch.help.create_empty_notes_txt", "Create an empty notes.txt")],
|
|
352
|
+
["touch ~/docs/todo.md", t.t("cli.touch.help.create_file_docs", "Create a file in ~/docs")]
|
|
353
|
+
]);
|
|
354
|
+
}
|
|
226
355
|
async processCommand(command, context) {
|
|
227
|
-
const fs = context.services.
|
|
356
|
+
const fs = context.services.getRequired(
|
|
228
357
|
IFileSystemService_TOKEN
|
|
229
358
|
);
|
|
230
|
-
const
|
|
231
|
-
if (
|
|
359
|
+
const paths = splitArgs(command.value || "");
|
|
360
|
+
if (paths.length === 0) {
|
|
232
361
|
context.writer.writeError("touch: missing file operand");
|
|
233
362
|
return;
|
|
234
363
|
}
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
364
|
+
let changed = false;
|
|
365
|
+
for (const path of paths) {
|
|
366
|
+
try {
|
|
367
|
+
fs.createFile(path);
|
|
368
|
+
changed = true;
|
|
369
|
+
} catch (e) {
|
|
370
|
+
context.writer.writeError(e.message);
|
|
371
|
+
}
|
|
240
372
|
}
|
|
373
|
+
if (changed) await fs.persist();
|
|
241
374
|
}
|
|
242
375
|
};
|
|
376
|
+
|
|
377
|
+
// src/lib/utils/lines.ts
|
|
378
|
+
function splitLines(content) {
|
|
379
|
+
if (content === "") return [];
|
|
380
|
+
const lines = content.split("\n");
|
|
381
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
382
|
+
return lines;
|
|
383
|
+
}
|
|
384
|
+
function pipedText(data) {
|
|
385
|
+
return typeof data === "string" ? data : JSON.stringify(data);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// src/lib/processors/cli-cat-command-processor.ts
|
|
243
389
|
var CliCatCommandProcessor = class {
|
|
244
390
|
constructor() {
|
|
245
391
|
this.command = "cat";
|
|
@@ -250,22 +396,58 @@ var CliCatCommandProcessor = class {
|
|
|
250
396
|
this.valueRequired = true;
|
|
251
397
|
this.metadata = { icon: "\u{1F4D6}", module: "file management" };
|
|
252
398
|
}
|
|
399
|
+
writeDescription(context) {
|
|
400
|
+
const t = context.translator;
|
|
401
|
+
writeWrapped(
|
|
402
|
+
context,
|
|
403
|
+
t.t("cli.cat.help.print_contents_one_more", "Print the contents of one or more files, joined in order, or of piped input. Pipe the output into other commands to search, sort or count it.")
|
|
404
|
+
);
|
|
405
|
+
context.writer.writeln();
|
|
406
|
+
writeUsage(context, [
|
|
407
|
+
["cat <file...>", t.t("cli.cat.help.print_files_one_after", "Print files one after another")],
|
|
408
|
+
["cat <file> | <command>", t.t("cli.cat.help.send_file_another_command", "Send a file to another command")]
|
|
409
|
+
]);
|
|
410
|
+
context.writer.writeln();
|
|
411
|
+
writeExamples(context, [
|
|
412
|
+
["cat notes.txt", t.t("cli.cat.help.show_whole_file", "Show the whole file")],
|
|
413
|
+
["cat app.log | grep error", t.t("cli.cat.help.show_only_lines_error", "Show only lines with error")],
|
|
414
|
+
["cat names.txt | sort", t.t("cli.cat.help.print_lines_sorted", "Print the lines sorted")]
|
|
415
|
+
]);
|
|
416
|
+
}
|
|
253
417
|
async processCommand(command, context) {
|
|
254
|
-
const fs = context.services.
|
|
418
|
+
const fs = context.services.getRequired(
|
|
255
419
|
IFileSystemService_TOKEN
|
|
256
420
|
);
|
|
257
|
-
const
|
|
258
|
-
if (
|
|
421
|
+
const paths = splitArgs(command.value || "");
|
|
422
|
+
if (paths.length === 0) {
|
|
423
|
+
if (command.data != null) {
|
|
424
|
+
context.writer.writeln(pipedText(command.data));
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
259
427
|
context.writer.writeError("cat: missing file operand");
|
|
260
428
|
return;
|
|
261
429
|
}
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
430
|
+
let output = "";
|
|
431
|
+
for (const path of paths) {
|
|
432
|
+
try {
|
|
433
|
+
if (fs.isDirectory(path)) {
|
|
434
|
+
context.writer.writeError(`cat: ${path}: Is a directory`);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const content = fs.readFile(path);
|
|
438
|
+
if (content === null) {
|
|
439
|
+
context.writer.writeError(
|
|
440
|
+
`cat: ${path}: No such file or directory`
|
|
441
|
+
);
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
output += content;
|
|
445
|
+
} catch (e) {
|
|
446
|
+
context.writer.writeError(e.message);
|
|
266
447
|
}
|
|
267
|
-
}
|
|
268
|
-
|
|
448
|
+
}
|
|
449
|
+
if (output !== "") {
|
|
450
|
+
context.writer.writeln(output.replace(/\n$/, ""));
|
|
269
451
|
}
|
|
270
452
|
}
|
|
271
453
|
};
|
|
@@ -279,60 +461,33 @@ var CliEchoCommandProcessor = class {
|
|
|
279
461
|
this.extendsProcessor = true;
|
|
280
462
|
this.metadata = { icon: "\u{1F4AC}", module: "file management" };
|
|
281
463
|
}
|
|
464
|
+
writeDescription(context) {
|
|
465
|
+
const t = context.translator;
|
|
466
|
+
writeWrapped(
|
|
467
|
+
context,
|
|
468
|
+
t.t("cli.echo.help.print_text_terminal_add", "Print text to the terminal. Add > file to save it to a file (replacing it) or >> file to add it to the end.")
|
|
469
|
+
);
|
|
470
|
+
context.writer.writeln();
|
|
471
|
+
writeUsage(context, [
|
|
472
|
+
["echo <text>", t.t("cli.echo.help.print_text", "Print text")],
|
|
473
|
+
["echo <text> > <file>", t.t("cli.echo.help.write_text_file", "Write text to a file")],
|
|
474
|
+
["echo <text> >> <file>", t.t("cli.echo.help.append_text_file", "Append text to a file")]
|
|
475
|
+
]);
|
|
476
|
+
context.writer.writeln();
|
|
477
|
+
writeExamples(context, [
|
|
478
|
+
["echo Hello world", t.t("cli.echo.help.print_greeting", "Print a greeting")],
|
|
479
|
+
["echo first line > notes.txt", t.t("cli.echo.help.create_replace_notes_txt", "Create or replace notes.txt")],
|
|
480
|
+
["echo another line >> notes.txt", t.t("cli.echo.help.add_line", "Add a line to it")]
|
|
481
|
+
]);
|
|
482
|
+
}
|
|
282
483
|
async processCommand(command, context) {
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
if (!hasRedirection) {
|
|
286
|
-
if (this.originalProcessor) {
|
|
287
|
-
await this.originalProcessor.processCommand(command, context);
|
|
288
|
-
} else {
|
|
289
|
-
context.writer.writeln(afterEcho);
|
|
290
|
-
}
|
|
484
|
+
if (this.originalProcessor) {
|
|
485
|
+
await this.originalProcessor.processCommand(command, context);
|
|
291
486
|
return;
|
|
292
487
|
}
|
|
293
|
-
const
|
|
294
|
-
|
|
295
|
-
);
|
|
296
|
-
let text;
|
|
297
|
-
let filePath;
|
|
298
|
-
let append = false;
|
|
299
|
-
const appendMatch = afterEcho.match(/^(.*?)\s*>>\s*(.+)$/);
|
|
300
|
-
const overwriteMatch = afterEcho.match(/^(.*?)\s*>\s*(.+)$/);
|
|
301
|
-
if (appendMatch) {
|
|
302
|
-
text = appendMatch[1].trim();
|
|
303
|
-
filePath = appendMatch[2].trim();
|
|
304
|
-
append = true;
|
|
305
|
-
} else {
|
|
306
|
-
text = overwriteMatch[1].trim();
|
|
307
|
-
filePath = overwriteMatch[2].trim();
|
|
308
|
-
}
|
|
309
|
-
if (text.startsWith('"') && text.endsWith('"') || text.startsWith("'") && text.endsWith("'")) {
|
|
310
|
-
text = text.slice(1, -1);
|
|
311
|
-
}
|
|
312
|
-
text = text.replace(/\\([\\nrtv0])/g, (_, ch) => {
|
|
313
|
-
switch (ch) {
|
|
314
|
-
case "n":
|
|
315
|
-
return "\n";
|
|
316
|
-
case "t":
|
|
317
|
-
return " ";
|
|
318
|
-
case "r":
|
|
319
|
-
return "\r";
|
|
320
|
-
case "v":
|
|
321
|
-
return "\v";
|
|
322
|
-
case "0":
|
|
323
|
-
return "\0";
|
|
324
|
-
case "\\":
|
|
325
|
-
return "\\";
|
|
326
|
-
default:
|
|
327
|
-
return ch;
|
|
328
|
-
}
|
|
329
|
-
});
|
|
330
|
-
try {
|
|
331
|
-
fs.writeFile(filePath, text + "\n", append);
|
|
332
|
-
await fs.persist();
|
|
333
|
-
} catch (e) {
|
|
334
|
-
context.writer.writeError(e.message);
|
|
335
|
-
}
|
|
488
|
+
const text = joinShellWords(command.value || "");
|
|
489
|
+
context.writer.writeln(text);
|
|
490
|
+
context.process.output(text);
|
|
336
491
|
}
|
|
337
492
|
};
|
|
338
493
|
var CliRmCommandProcessor = class {
|
|
@@ -361,35 +516,58 @@ var CliRmCommandProcessor = class {
|
|
|
361
516
|
}
|
|
362
517
|
];
|
|
363
518
|
}
|
|
519
|
+
writeDescription(context) {
|
|
520
|
+
const t = context.translator;
|
|
521
|
+
writeWrapped(
|
|
522
|
+
context,
|
|
523
|
+
t.t("cli.rm.help.delete_file_directories_only", "Delete a file. Directories are only removed with -r, which deletes everything inside them. Deleted files cannot be recovered.")
|
|
524
|
+
);
|
|
525
|
+
context.writer.writeln();
|
|
526
|
+
writeUsage(context, [
|
|
527
|
+
["rm <file...>", t.t("cli.rm.help.delete_one_more_files", "Delete one or more files")],
|
|
528
|
+
["rm -r <dir>", t.t("cli.rm.help.delete_directory_contents", "Delete a directory and its contents")],
|
|
529
|
+
["rm -f <file>", t.t("cli.rm.help.delete_without_error_if", "Delete without an error if it is missing")]
|
|
530
|
+
]);
|
|
531
|
+
context.writer.writeln();
|
|
532
|
+
writeExamples(context, [
|
|
533
|
+
["rm notes.bak", t.t("cli.rm.help.delete_one_file", "Delete one file")],
|
|
534
|
+
["rm -r old-project", t.t("cli.rm.help.delete_whole_directory", "Delete a whole directory")],
|
|
535
|
+
["rm -f maybe.txt", t.t("cli.rm.help.quiet_if_file_does", "Quiet if the file does not exist")]
|
|
536
|
+
]);
|
|
537
|
+
}
|
|
364
538
|
async processCommand(command, context) {
|
|
365
|
-
const fs = context.services.
|
|
539
|
+
const fs = context.services.getRequired(
|
|
366
540
|
IFileSystemService_TOKEN
|
|
367
541
|
);
|
|
368
|
-
const
|
|
542
|
+
const paths = splitArgs(command.value || "");
|
|
369
543
|
const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
|
|
370
544
|
const force = command.args["force"] || command.args["f"];
|
|
371
|
-
if (
|
|
372
|
-
context.writer.writeError("rm: missing operand");
|
|
545
|
+
if (paths.length === 0) {
|
|
546
|
+
if (!force) context.writer.writeError("rm: missing operand");
|
|
373
547
|
return;
|
|
374
548
|
}
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
549
|
+
let changed = false;
|
|
550
|
+
for (const path of paths) {
|
|
551
|
+
try {
|
|
552
|
+
if (!fs.exists(path)) {
|
|
553
|
+
if (!force) {
|
|
554
|
+
context.writer.writeError(
|
|
555
|
+
`rm: ${path}: No such file or directory`
|
|
556
|
+
);
|
|
557
|
+
}
|
|
558
|
+
continue;
|
|
381
559
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
560
|
+
if (fs.isDirectory(path) && !recursive) {
|
|
561
|
+
context.writer.writeError(`rm: ${path}: Is a directory`);
|
|
562
|
+
continue;
|
|
563
|
+
}
|
|
564
|
+
fs.remove(path, recursive);
|
|
565
|
+
changed = true;
|
|
566
|
+
} catch (e) {
|
|
567
|
+
context.writer.writeError(e.message);
|
|
387
568
|
}
|
|
388
|
-
fs.remove(path, recursive);
|
|
389
|
-
await fs.persist();
|
|
390
|
-
} catch (e) {
|
|
391
|
-
context.writer.writeError(e.message);
|
|
392
569
|
}
|
|
570
|
+
if (changed) await fs.persist();
|
|
393
571
|
}
|
|
394
572
|
};
|
|
395
573
|
var CliCpCommandProcessor = class {
|
|
@@ -407,11 +585,37 @@ var CliCpCommandProcessor = class {
|
|
|
407
585
|
description: "Copy directories recursively",
|
|
408
586
|
required: false,
|
|
409
587
|
type: "boolean"
|
|
588
|
+
},
|
|
589
|
+
{
|
|
590
|
+
name: "force",
|
|
591
|
+
aliases: ["f"],
|
|
592
|
+
description: "Overwrite existing files (always done; accepted for -rf)",
|
|
593
|
+
required: false,
|
|
594
|
+
type: "boolean"
|
|
410
595
|
}
|
|
411
596
|
];
|
|
412
597
|
}
|
|
598
|
+
writeDescription(context) {
|
|
599
|
+
const t = context.translator;
|
|
600
|
+
writeWrapped(
|
|
601
|
+
context,
|
|
602
|
+
t.t("cli.cp.help.copy_file_new_path", "Copy a file to a new path, or into an existing directory. Copying a directory needs -r, which copies everything inside it. A file already at the destination is replaced.")
|
|
603
|
+
);
|
|
604
|
+
context.writer.writeln();
|
|
605
|
+
writeUsage(context, [
|
|
606
|
+
["cp <source> <dest>", t.t("cli.cp.help.copy_file_new_name", "Copy a file to a new name")],
|
|
607
|
+
["cp <source...> <dir>", t.t("cli.cp.help.copy_files_directory", "Copy files into a directory")],
|
|
608
|
+
["cp -r <dir> <dest>", t.t("cli.cp.help.copy_directory_contents", "Copy a directory and its contents")]
|
|
609
|
+
]);
|
|
610
|
+
context.writer.writeln();
|
|
611
|
+
writeExamples(context, [
|
|
612
|
+
["cp notes.txt notes.bak", t.t("cli.cp.help.make_backup_copy", "Make a backup copy")],
|
|
613
|
+
["cp report.txt ~/docs", t.t("cli.cp.help.copy_docs", "Copy into ~/docs")],
|
|
614
|
+
["cp -r project project-copy", t.t("cli.cp.help.duplicate_whole_directory", "Duplicate a whole directory")]
|
|
615
|
+
]);
|
|
616
|
+
}
|
|
413
617
|
async processCommand(command, context) {
|
|
414
|
-
const fs = context.services.
|
|
618
|
+
const fs = context.services.getRequired(
|
|
415
619
|
IFileSystemService_TOKEN
|
|
416
620
|
);
|
|
417
621
|
const recursive = command.args["recursive"] || command.args["r"] || command.args["R"];
|
|
@@ -423,18 +627,26 @@ var CliCpCommandProcessor = class {
|
|
|
423
627
|
);
|
|
424
628
|
return;
|
|
425
629
|
}
|
|
426
|
-
const
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
|
|
630
|
+
const dest = parts[parts.length - 1];
|
|
631
|
+
const sources = parts.slice(0, -1);
|
|
632
|
+
if (sources.length > 1 && !fs.isDirectory(dest)) {
|
|
633
|
+
context.writer.writeError(`cp: target '${dest}' is not a directory`);
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
let changed = false;
|
|
637
|
+
for (const src of sources) {
|
|
638
|
+
try {
|
|
639
|
+
fs.copy(src, dest, recursive);
|
|
640
|
+
changed = true;
|
|
641
|
+
} catch (e) {
|
|
642
|
+
context.writer.writeError(e.message);
|
|
643
|
+
}
|
|
433
644
|
}
|
|
645
|
+
if (changed) await fs.persist();
|
|
434
646
|
}
|
|
435
647
|
parseArgs(command) {
|
|
436
648
|
const raw = command.value || "";
|
|
437
|
-
return raw
|
|
649
|
+
return splitArgs(raw).filter(Boolean);
|
|
438
650
|
}
|
|
439
651
|
};
|
|
440
652
|
var CliMvCommandProcessor = class {
|
|
@@ -446,8 +658,26 @@ var CliMvCommandProcessor = class {
|
|
|
446
658
|
this.acceptsRawInput = true;
|
|
447
659
|
this.metadata = { icon: "\u{1F4E6}", module: "file management" };
|
|
448
660
|
}
|
|
661
|
+
writeDescription(context) {
|
|
662
|
+
const t = context.translator;
|
|
663
|
+
writeWrapped(
|
|
664
|
+
context,
|
|
665
|
+
t.t("cli.mv.help.move_rename_file_directory", "Move or rename a file or directory. If the destination is an existing directory, the source is moved into it; otherwise it is renamed. A file already at the destination is replaced.")
|
|
666
|
+
);
|
|
667
|
+
context.writer.writeln();
|
|
668
|
+
writeUsage(context, [
|
|
669
|
+
["mv <source> <dest>", t.t("cli.mv.help.rename_file_directory", "Rename a file or directory")],
|
|
670
|
+
["mv <source...> <dir>", t.t("cli.mv.help.move_them_directory", "Move them into a directory")]
|
|
671
|
+
]);
|
|
672
|
+
context.writer.writeln();
|
|
673
|
+
writeExamples(context, [
|
|
674
|
+
["mv draft.txt final.txt", t.t("cli.mv.help.rename_file", "Rename a file")],
|
|
675
|
+
["mv final.txt ~/docs", t.t("cli.mv.help.move_docs", "Move it into ~/docs")],
|
|
676
|
+
["mv old-dir new-dir", t.t("cli.mv.help.rename_directory", "Rename a directory")]
|
|
677
|
+
]);
|
|
678
|
+
}
|
|
449
679
|
async processCommand(command, context) {
|
|
450
|
-
const fs = context.services.
|
|
680
|
+
const fs = context.services.getRequired(
|
|
451
681
|
IFileSystemService_TOKEN
|
|
452
682
|
);
|
|
453
683
|
const parts = this.parseArgs(command);
|
|
@@ -456,20 +686,36 @@ var CliMvCommandProcessor = class {
|
|
|
456
686
|
context.writer.writeln("Usage: mv <source> <destination>");
|
|
457
687
|
return;
|
|
458
688
|
}
|
|
459
|
-
const
|
|
460
|
-
const
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
} catch (e) {
|
|
465
|
-
context.writer.writeError(e.message);
|
|
689
|
+
const dest = parts[parts.length - 1];
|
|
690
|
+
const sources = parts.slice(0, -1);
|
|
691
|
+
if (sources.length > 1 && !fs.isDirectory(dest)) {
|
|
692
|
+
context.writer.writeError(`mv: target '${dest}' is not a directory`);
|
|
693
|
+
return;
|
|
466
694
|
}
|
|
695
|
+
let changed = false;
|
|
696
|
+
for (const src of sources) {
|
|
697
|
+
try {
|
|
698
|
+
fs.move(src, dest);
|
|
699
|
+
changed = true;
|
|
700
|
+
} catch (e) {
|
|
701
|
+
context.writer.writeError(e.message);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
if (changed) await fs.persist();
|
|
467
705
|
}
|
|
468
706
|
parseArgs(command) {
|
|
469
707
|
const raw = command.value || "";
|
|
470
|
-
return raw
|
|
708
|
+
return splitArgs(raw).filter(Boolean);
|
|
471
709
|
}
|
|
472
710
|
};
|
|
711
|
+
function readableChildren(fs, path) {
|
|
712
|
+
try {
|
|
713
|
+
return fs.listDirectory(path);
|
|
714
|
+
} catch (e) {
|
|
715
|
+
if (isCliAccessDeniedError(e)) return null;
|
|
716
|
+
throw e;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
473
719
|
var CliTreeCommandProcessor = class {
|
|
474
720
|
constructor() {
|
|
475
721
|
this.command = "tree";
|
|
@@ -488,11 +734,29 @@ var CliTreeCommandProcessor = class {
|
|
|
488
734
|
}
|
|
489
735
|
];
|
|
490
736
|
}
|
|
737
|
+
writeDescription(context) {
|
|
738
|
+
const t = context.translator;
|
|
739
|
+
writeWrapped(
|
|
740
|
+
context,
|
|
741
|
+
t.t("cli.tree.help.draw_directory_everything_below", "Draw a directory and everything below it as a tree, directories first, followed by a count of directories and files. Defaults to the current directory.")
|
|
742
|
+
);
|
|
743
|
+
context.writer.writeln();
|
|
744
|
+
writeUsage(context, [
|
|
745
|
+
["tree [dir]", t.t("cli.tree.help.show_whole_tree", "Show the whole tree")],
|
|
746
|
+
["tree -L <n> [dir]", t.t("cli.tree.help.only_levels_deep", "Only <n> levels deep")]
|
|
747
|
+
]);
|
|
748
|
+
context.writer.writeln();
|
|
749
|
+
writeExamples(context, [
|
|
750
|
+
["tree", t.t("cli.tree.help.tree_current_directory", "Tree of the current directory")],
|
|
751
|
+
["tree ~/docs", t.t("cli.tree.help.tree_docs", "Tree of ~/docs")],
|
|
752
|
+
["tree -L 2", t.t("cli.tree.help.top_two_levels_only", "Top two levels only")]
|
|
753
|
+
]);
|
|
754
|
+
}
|
|
491
755
|
async processCommand(command, context) {
|
|
492
|
-
const fs = context.services.
|
|
756
|
+
const fs = context.services.getRequired(
|
|
493
757
|
IFileSystemService_TOKEN
|
|
494
758
|
);
|
|
495
|
-
const targetPath = command.value || fs.getCurrentDirectory();
|
|
759
|
+
const targetPath = splitArgs(command.value || "").join(" ") || fs.getCurrentDirectory();
|
|
496
760
|
const maxDepth = command.args["depth"] ? parseInt(command.args["depth"]) : command.args["L"] ? parseInt(command.args["L"]) : Infinity;
|
|
497
761
|
try {
|
|
498
762
|
const node = fs.getNode(targetPath);
|
|
@@ -516,7 +780,8 @@ var CliTreeCommandProcessor = class {
|
|
|
516
780
|
)
|
|
517
781
|
);
|
|
518
782
|
const counts = { dirs: 0, files: 0 };
|
|
519
|
-
this.
|
|
783
|
+
this.fs = fs;
|
|
784
|
+
this.printTree(node, "", true, 0, maxDepth, context, counts, resolvedPath);
|
|
520
785
|
context.writer.writeln();
|
|
521
786
|
context.writer.writeln(
|
|
522
787
|
`${counts.dirs} directories, ${counts.files} files`
|
|
@@ -525,11 +790,15 @@ var CliTreeCommandProcessor = class {
|
|
|
525
790
|
context.writer.writeError(e.message);
|
|
526
791
|
}
|
|
527
792
|
}
|
|
528
|
-
printTree(node, prefix, isRoot, depth, maxDepth, context, counts) {
|
|
529
|
-
if (
|
|
793
|
+
printTree(node, prefix, isRoot, depth, maxDepth, context, counts, path = "/") {
|
|
794
|
+
if (depth >= maxDepth) {
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
const visible = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
798
|
+
if (!visible) {
|
|
530
799
|
return;
|
|
531
800
|
}
|
|
532
|
-
const children = [...
|
|
801
|
+
const children = [...visible].sort((a, b) => {
|
|
533
802
|
if (a.type !== b.type) {
|
|
534
803
|
return a.type === "directory" ? -1 : 1;
|
|
535
804
|
}
|
|
@@ -554,7 +823,8 @@ var CliTreeCommandProcessor = class {
|
|
|
554
823
|
depth + 1,
|
|
555
824
|
maxDepth,
|
|
556
825
|
context,
|
|
557
|
-
counts
|
|
826
|
+
counts,
|
|
827
|
+
path === "/" ? `/${child.name}` : `${path}/${child.name}`
|
|
558
828
|
);
|
|
559
829
|
} else {
|
|
560
830
|
counts.files++;
|
|
@@ -581,21 +851,40 @@ var CliHeadCommandProcessor = class {
|
|
|
581
851
|
}
|
|
582
852
|
];
|
|
583
853
|
}
|
|
854
|
+
writeDescription(context) {
|
|
855
|
+
const t = context.translator;
|
|
856
|
+
writeWrapped(
|
|
857
|
+
context,
|
|
858
|
+
t.t("cli.head.help.print_first_lines_file", "Print the first lines of a file, 10 by default. Works on piped input too. With several files, each one gets a ==> name <== header.")
|
|
859
|
+
);
|
|
860
|
+
context.writer.writeln();
|
|
861
|
+
writeUsage(context, [
|
|
862
|
+
["head <file>", t.t("cli.head.help.first_10_lines", "First 10 lines")],
|
|
863
|
+
["head -n <count> <file>", t.t("cli.head.help.first_lines", "First <count> lines")],
|
|
864
|
+
["<command> | head", t.t("cli.head.help.first_lines_piped_input", "First lines of piped input")]
|
|
865
|
+
]);
|
|
866
|
+
context.writer.writeln();
|
|
867
|
+
writeExamples(context, [
|
|
868
|
+
["head notes.txt", t.t("cli.head.help.show_start_file", "Show the start of a file")],
|
|
869
|
+
["head -n 3 a.txt b.txt", t.t("cli.head.help.first_3_lines_each", "First 3 lines of each file")],
|
|
870
|
+
["cat app.log | head -n 20", t.t("cli.head.help.first_20_lines_log", "First 20 lines of the log")]
|
|
871
|
+
]);
|
|
872
|
+
}
|
|
584
873
|
async processCommand(command, context) {
|
|
585
|
-
const fs = context.services.
|
|
874
|
+
const fs = context.services.getRequired(
|
|
586
875
|
IFileSystemService_TOKEN
|
|
587
876
|
);
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
);
|
|
877
|
+
const countArg = command.args["lines"] ?? command.args["n"] ?? Object.keys(command.args).find((k) => /^\d+$/.test(k));
|
|
878
|
+
const parsedCount = parseInt(String(countArg ?? "10"), 10);
|
|
879
|
+
const count = isNaN(parsedCount) ? 10 : Math.abs(parsedCount);
|
|
592
880
|
const paths = this.parsePaths(command);
|
|
593
881
|
if (paths.length === 0) {
|
|
594
882
|
if (command.data != null) {
|
|
595
|
-
const
|
|
596
|
-
const lines = content.split("\n");
|
|
883
|
+
const lines = splitLines(pipedText(command.data));
|
|
597
884
|
const selected = lines.slice(0, count);
|
|
598
|
-
|
|
885
|
+
if (selected.length > 0) {
|
|
886
|
+
context.writer.writeln(selected.join("\n"));
|
|
887
|
+
}
|
|
599
888
|
return;
|
|
600
889
|
}
|
|
601
890
|
context.writer.writeError("head: missing file operand");
|
|
@@ -614,9 +903,11 @@ var CliHeadCommandProcessor = class {
|
|
|
614
903
|
if (i > 0) context.writer.writeln();
|
|
615
904
|
context.writer.writeln(`==> ${path} <==`);
|
|
616
905
|
}
|
|
617
|
-
const lines = content
|
|
906
|
+
const lines = splitLines(content);
|
|
618
907
|
const selected = lines.slice(0, count);
|
|
619
|
-
|
|
908
|
+
if (selected.length > 0) {
|
|
909
|
+
context.writer.writeln(selected.join("\n"));
|
|
910
|
+
}
|
|
620
911
|
} catch (e) {
|
|
621
912
|
context.writer.writeError(`head: ${e.message}`);
|
|
622
913
|
}
|
|
@@ -624,7 +915,7 @@ var CliHeadCommandProcessor = class {
|
|
|
624
915
|
}
|
|
625
916
|
parsePaths(command) {
|
|
626
917
|
const raw = command.value || "";
|
|
627
|
-
return raw
|
|
918
|
+
return splitArgs(raw).filter(Boolean);
|
|
628
919
|
}
|
|
629
920
|
};
|
|
630
921
|
var CliTailCommandProcessor = class {
|
|
@@ -646,21 +937,40 @@ var CliTailCommandProcessor = class {
|
|
|
646
937
|
}
|
|
647
938
|
];
|
|
648
939
|
}
|
|
940
|
+
writeDescription(context) {
|
|
941
|
+
const t = context.translator;
|
|
942
|
+
writeWrapped(
|
|
943
|
+
context,
|
|
944
|
+
t.t("cli.tail.help.print_last_lines_file", "Print the last lines of a file, 10 by default. Works on piped input too. With several files, each one gets a ==> name <== header.")
|
|
945
|
+
);
|
|
946
|
+
context.writer.writeln();
|
|
947
|
+
writeUsage(context, [
|
|
948
|
+
["tail <file>", t.t("cli.tail.help.last_10_lines", "Last 10 lines")],
|
|
949
|
+
["tail -n <count> <file>", t.t("cli.tail.help.last_lines", "Last <count> lines")],
|
|
950
|
+
["<command> | tail", t.t("cli.tail.help.last_lines_piped_input", "Last lines of piped input")]
|
|
951
|
+
]);
|
|
952
|
+
context.writer.writeln();
|
|
953
|
+
writeExamples(context, [
|
|
954
|
+
["tail app.log", t.t("cli.tail.help.show_end_log", "Show the end of a log")],
|
|
955
|
+
["tail -n 5 notes.txt", t.t("cli.tail.help.last_5_lines", "Last 5 lines")],
|
|
956
|
+
["cat app.log | tail -n 20", t.t("cli.tail.help.last_20_lines_log", "Last 20 lines of the log")]
|
|
957
|
+
]);
|
|
958
|
+
}
|
|
649
959
|
async processCommand(command, context) {
|
|
650
|
-
const fs = context.services.
|
|
960
|
+
const fs = context.services.getRequired(
|
|
651
961
|
IFileSystemService_TOKEN
|
|
652
962
|
);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
);
|
|
963
|
+
const countArg = command.args["lines"] ?? command.args["n"] ?? Object.keys(command.args).find((k) => /^\d+$/.test(k));
|
|
964
|
+
const parsedCount = parseInt(String(countArg ?? "10"), 10);
|
|
965
|
+
const count = isNaN(parsedCount) ? 10 : Math.abs(parsedCount);
|
|
657
966
|
const paths = this.parsePaths(command);
|
|
658
967
|
if (paths.length === 0) {
|
|
659
968
|
if (command.data != null) {
|
|
660
|
-
const
|
|
661
|
-
const
|
|
662
|
-
|
|
663
|
-
|
|
969
|
+
const lines = splitLines(pipedText(command.data));
|
|
970
|
+
const selected = count > 0 ? lines.slice(-count) : [];
|
|
971
|
+
if (selected.length > 0) {
|
|
972
|
+
context.writer.writeln(selected.join("\n"));
|
|
973
|
+
}
|
|
664
974
|
return;
|
|
665
975
|
}
|
|
666
976
|
context.writer.writeError("tail: missing file operand");
|
|
@@ -679,9 +989,11 @@ var CliTailCommandProcessor = class {
|
|
|
679
989
|
if (i > 0) context.writer.writeln();
|
|
680
990
|
context.writer.writeln(`==> ${path} <==`);
|
|
681
991
|
}
|
|
682
|
-
const lines = content
|
|
683
|
-
const selected = lines.slice(-count);
|
|
684
|
-
|
|
992
|
+
const lines = splitLines(content);
|
|
993
|
+
const selected = count > 0 ? lines.slice(-count) : [];
|
|
994
|
+
if (selected.length > 0) {
|
|
995
|
+
context.writer.writeln(selected.join("\n"));
|
|
996
|
+
}
|
|
685
997
|
} catch (e) {
|
|
686
998
|
context.writer.writeError(`tail: ${e.message}`);
|
|
687
999
|
}
|
|
@@ -689,7 +1001,7 @@ var CliTailCommandProcessor = class {
|
|
|
689
1001
|
}
|
|
690
1002
|
parsePaths(command) {
|
|
691
1003
|
const raw = command.value || "";
|
|
692
|
-
return raw
|
|
1004
|
+
return splitArgs(raw).filter(Boolean);
|
|
693
1005
|
}
|
|
694
1006
|
};
|
|
695
1007
|
var CliWcCommandProcessor = class {
|
|
@@ -725,8 +1037,30 @@ var CliWcCommandProcessor = class {
|
|
|
725
1037
|
}
|
|
726
1038
|
];
|
|
727
1039
|
}
|
|
1040
|
+
writeDescription(context) {
|
|
1041
|
+
const t = context.translator;
|
|
1042
|
+
writeWrapped(
|
|
1043
|
+
context,
|
|
1044
|
+
t.t("cli.wc.help.count_lines_words_characters", "Count lines, words and characters in files or piped input. With several files, a total line is printed at the end.")
|
|
1045
|
+
);
|
|
1046
|
+
context.writer.writeln();
|
|
1047
|
+
writeUsage(context, [
|
|
1048
|
+
["wc <file...>", t.t("cli.wc.help.lines_words_characters", "Lines, words and characters")],
|
|
1049
|
+
["wc -l <file>", t.t("cli.wc.help.only_line_count", "Only the line count")],
|
|
1050
|
+
["<command> | wc", t.t("cli.wc.help.count_piped_input", "Count piped input")]
|
|
1051
|
+
]);
|
|
1052
|
+
context.writer.writeln();
|
|
1053
|
+
writeExamples(context, [
|
|
1054
|
+
["wc notes.txt", t.t("cli.wc.help.all_three_counts", "All three counts")],
|
|
1055
|
+
["wc -l a.txt b.txt", t.t("cli.wc.help.line_counts_total", "Line counts with a total")],
|
|
1056
|
+
[
|
|
1057
|
+
"cat app.log | grep error | wc -l",
|
|
1058
|
+
t.t("cli.wc.help.how_many_lines_mention", "How many lines mention error")
|
|
1059
|
+
]
|
|
1060
|
+
]);
|
|
1061
|
+
}
|
|
728
1062
|
async processCommand(command, context) {
|
|
729
|
-
const fs = context.services.
|
|
1063
|
+
const fs = context.services.getRequired(
|
|
730
1064
|
IFileSystemService_TOKEN
|
|
731
1065
|
);
|
|
732
1066
|
const showLines = command.args["lines"] || command.args["l"] || false;
|
|
@@ -736,10 +1070,9 @@ var CliWcCommandProcessor = class {
|
|
|
736
1070
|
const paths = this.parsePaths(command);
|
|
737
1071
|
if (paths.length === 0) {
|
|
738
1072
|
if (command.data != null) {
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
const charCount = content.length;
|
|
1073
|
+
let content = pipedText(command.data);
|
|
1074
|
+
if (content !== "" && !content.endsWith("\n")) content += "\n";
|
|
1075
|
+
const { lineCount, wordCount, charCount } = this.count(content);
|
|
743
1076
|
const parts = [];
|
|
744
1077
|
if (showAll || showLines) parts.push(String(lineCount).padStart(8));
|
|
745
1078
|
if (showAll || showWords) parts.push(String(wordCount).padStart(8));
|
|
@@ -760,9 +1093,7 @@ var CliWcCommandProcessor = class {
|
|
|
760
1093
|
continue;
|
|
761
1094
|
}
|
|
762
1095
|
const content = fs.readFile(path) ?? "";
|
|
763
|
-
const lineCount
|
|
764
|
-
const wordCount = content.trim() === "" ? 0 : content.trim().split(/\s+/).length;
|
|
765
|
-
const charCount = content.length;
|
|
1096
|
+
const { lineCount, wordCount, charCount } = this.count(content);
|
|
766
1097
|
totalLines += lineCount;
|
|
767
1098
|
totalWords += wordCount;
|
|
768
1099
|
totalChars += charCount;
|
|
@@ -785,9 +1116,17 @@ var CliWcCommandProcessor = class {
|
|
|
785
1116
|
context.writer.writeln(parts.join(""));
|
|
786
1117
|
}
|
|
787
1118
|
}
|
|
1119
|
+
/** Counts like Unix wc: lines are newline characters. */
|
|
1120
|
+
count(content) {
|
|
1121
|
+
return {
|
|
1122
|
+
lineCount: (content.match(/\n/g) ?? []).length,
|
|
1123
|
+
wordCount: content.trim() === "" ? 0 : content.trim().split(/\s+/).length,
|
|
1124
|
+
charCount: content.length
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
788
1127
|
parsePaths(command) {
|
|
789
1128
|
const raw = command.value || "";
|
|
790
|
-
return raw
|
|
1129
|
+
return splitArgs(raw).filter(Boolean);
|
|
791
1130
|
}
|
|
792
1131
|
};
|
|
793
1132
|
var CliFindCommandProcessor = class {
|
|
@@ -801,7 +1140,13 @@ var CliFindCommandProcessor = class {
|
|
|
801
1140
|
this.parameters = [
|
|
802
1141
|
{
|
|
803
1142
|
name: "name",
|
|
804
|
-
description: "Match filename pattern (glob: * and ? supported)",
|
|
1143
|
+
description: "Match filename pattern (glob: * and ? supported), case-sensitive",
|
|
1144
|
+
required: false,
|
|
1145
|
+
type: "string"
|
|
1146
|
+
},
|
|
1147
|
+
{
|
|
1148
|
+
name: "iname",
|
|
1149
|
+
description: "Like --name, but case-insensitive",
|
|
805
1150
|
required: false,
|
|
806
1151
|
type: "string"
|
|
807
1152
|
},
|
|
@@ -813,20 +1158,42 @@ var CliFindCommandProcessor = class {
|
|
|
813
1158
|
},
|
|
814
1159
|
{
|
|
815
1160
|
name: "maxdepth",
|
|
816
|
-
description: "Maximum
|
|
1161
|
+
description: "Maximum depth: 0 is only the start path, 1 its direct children",
|
|
817
1162
|
required: false,
|
|
818
1163
|
type: "number"
|
|
819
1164
|
}
|
|
820
1165
|
];
|
|
821
1166
|
}
|
|
1167
|
+
writeDescription(context) {
|
|
1168
|
+
const t = context.translator;
|
|
1169
|
+
writeWrapped(
|
|
1170
|
+
context,
|
|
1171
|
+
t.t("cli.find.help.list_files_directories_under", "List files and directories under a path (the current directory by default), searching all subdirectories. The start path itself is listed first. Filter by name with a glob pattern (* and ?; --name is case-sensitive, --iname is not) or by type.")
|
|
1172
|
+
);
|
|
1173
|
+
context.writer.writeln();
|
|
1174
|
+
writeUsage(context, [
|
|
1175
|
+
["find [path]", t.t("cli.find.help.list_everything_under_path", "List everything under a path")],
|
|
1176
|
+
["find [path] --name <pattern>", t.t("cli.find.help.match_names_against_glob", "Match names against a glob")],
|
|
1177
|
+
["find [path] --iname <pattern>", t.t("cli.find.help.same_ignoring_case", "Same, ignoring case")],
|
|
1178
|
+
["find [path] --type f|d", t.t("cli.find.help.only_files_f_directories", "Only files (f) or directories (d)")]
|
|
1179
|
+
]);
|
|
1180
|
+
context.writer.writeln();
|
|
1181
|
+
writeExamples(context, [
|
|
1182
|
+
["find ~/docs", t.t("cli.find.help.list_everything_docs", "List everything in ~/docs")],
|
|
1183
|
+
['find . --name "*.txt"', t.t("cli.find.help.find_all_txt_files", "Find all .txt files")],
|
|
1184
|
+
["find . --type d --maxdepth 1", t.t("cli.find.help.directories_near_top", "Directories near the top")],
|
|
1185
|
+
['find . --name "*.tmp" | xargs rm', t.t("cli.find.help.delete_all_tmp_files", "Delete all .tmp files")]
|
|
1186
|
+
]);
|
|
1187
|
+
}
|
|
822
1188
|
async processCommand(command, context) {
|
|
823
|
-
const fs = context.services.
|
|
1189
|
+
const fs = context.services.getRequired(
|
|
824
1190
|
IFileSystemService_TOKEN
|
|
825
1191
|
);
|
|
826
1192
|
const searchPath = this.parseSearchPath(command) || fs.getCurrentDirectory();
|
|
827
|
-
const namePattern = command.args["name"] || null;
|
|
828
1193
|
const typeFilter = command.args["type"] || null;
|
|
829
|
-
const
|
|
1194
|
+
const maxDepthArg = command.args["maxdepth"];
|
|
1195
|
+
const parsedDepth = parseInt(String(maxDepthArg ?? ""), 10);
|
|
1196
|
+
const maxDepth = maxDepthArg != null && maxDepthArg !== true && !isNaN(parsedDepth) ? parsedDepth : Infinity;
|
|
830
1197
|
try {
|
|
831
1198
|
const node = fs.getNode(searchPath);
|
|
832
1199
|
if (!node) {
|
|
@@ -835,24 +1202,25 @@ var CliFindCommandProcessor = class {
|
|
|
835
1202
|
);
|
|
836
1203
|
return;
|
|
837
1204
|
}
|
|
838
|
-
if (node.type !== "directory") {
|
|
839
|
-
context.writer.writeError(
|
|
840
|
-
`find: '${searchPath}': Not a directory`
|
|
841
|
-
);
|
|
842
|
-
return;
|
|
843
|
-
}
|
|
844
1205
|
const resolvedBase = fs.resolvePath(searchPath);
|
|
845
|
-
const nameRegex =
|
|
1206
|
+
const nameRegex = this.nameRegex(command);
|
|
846
1207
|
const results = [];
|
|
847
|
-
this.
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1208
|
+
this.fs = fs;
|
|
1209
|
+
const baseName = resolvedBase.split("/").filter(Boolean).pop() ?? "/";
|
|
1210
|
+
if (this.matches(baseName, node.type, nameRegex, typeFilter)) {
|
|
1211
|
+
results.push(resolvedBase);
|
|
1212
|
+
}
|
|
1213
|
+
if (node.type === "directory" && maxDepth > 0) {
|
|
1214
|
+
this.walk(
|
|
1215
|
+
node,
|
|
1216
|
+
resolvedBase,
|
|
1217
|
+
1,
|
|
1218
|
+
maxDepth,
|
|
1219
|
+
nameRegex,
|
|
1220
|
+
typeFilter,
|
|
1221
|
+
results
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
856
1224
|
for (const r of results) {
|
|
857
1225
|
context.writer.writeln(r);
|
|
858
1226
|
}
|
|
@@ -860,31 +1228,41 @@ var CliFindCommandProcessor = class {
|
|
|
860
1228
|
context.writer.writeError(`find: ${e.message}`);
|
|
861
1229
|
}
|
|
862
1230
|
}
|
|
1231
|
+
/** `--name` matches case-sensitively, `--iname` ignoring case. */
|
|
1232
|
+
nameRegex(command) {
|
|
1233
|
+
const name = command.args["name"];
|
|
1234
|
+
if (name != null && name !== true) {
|
|
1235
|
+
return this.globToRegex(String(name), false);
|
|
1236
|
+
}
|
|
1237
|
+
const iname = command.args["iname"];
|
|
1238
|
+
if (iname != null && iname !== true) {
|
|
1239
|
+
return this.globToRegex(String(iname), true);
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
matches(name, type, nameRegex, typeFilter) {
|
|
1244
|
+
if (nameRegex && !nameRegex.test(name)) return false;
|
|
1245
|
+
if (typeFilter === "f" && type !== "file") return false;
|
|
1246
|
+
if (typeFilter === "d" && type !== "directory") return false;
|
|
1247
|
+
return true;
|
|
1248
|
+
}
|
|
863
1249
|
parseSearchPath(command) {
|
|
864
1250
|
const raw = command.value || "";
|
|
865
|
-
const tokens = raw
|
|
1251
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
866
1252
|
return tokens.length > 0 ? tokens[0] : null;
|
|
867
1253
|
}
|
|
868
|
-
globToRegex(pattern) {
|
|
1254
|
+
globToRegex(pattern, ignoreCase) {
|
|
869
1255
|
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&");
|
|
870
1256
|
const regexStr = escaped.replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
871
|
-
return new RegExp(`^${regexStr}$`, "i");
|
|
1257
|
+
return new RegExp(`^${regexStr}$`, ignoreCase ? "i" : "");
|
|
872
1258
|
}
|
|
1259
|
+
/** Lists the children of `node`, which are at `depth` (1 = direct children). */
|
|
873
1260
|
walk(node, currentPath, depth, maxDepth, nameRegex, typeFilter, results) {
|
|
874
|
-
|
|
875
|
-
|
|
1261
|
+
const children = this.fs ? readableChildren(this.fs, currentPath) : node.children;
|
|
1262
|
+
if (!children) return;
|
|
1263
|
+
for (const child of children) {
|
|
876
1264
|
const childPath = currentPath === "/" ? `/${child.name}` : `${currentPath}/${child.name}`;
|
|
877
|
-
|
|
878
|
-
if (nameRegex && !nameRegex.test(child.name)) {
|
|
879
|
-
matches = false;
|
|
880
|
-
}
|
|
881
|
-
if (typeFilter === "f" && child.type !== "file") {
|
|
882
|
-
matches = false;
|
|
883
|
-
}
|
|
884
|
-
if (typeFilter === "d" && child.type !== "directory") {
|
|
885
|
-
matches = false;
|
|
886
|
-
}
|
|
887
|
-
if (matches) {
|
|
1265
|
+
if (this.matches(child.name, child.type, nameRegex, typeFilter)) {
|
|
888
1266
|
results.push(childPath);
|
|
889
1267
|
}
|
|
890
1268
|
if (child.type === "directory" && depth < maxDepth) {
|
|
@@ -938,6 +1316,13 @@ var CliGrepCommandProcessor = class {
|
|
|
938
1316
|
required: false,
|
|
939
1317
|
type: "boolean"
|
|
940
1318
|
},
|
|
1319
|
+
{
|
|
1320
|
+
name: "quiet",
|
|
1321
|
+
aliases: ["q"],
|
|
1322
|
+
description: "Print nothing; only the exit status tells whether a line matched",
|
|
1323
|
+
required: false,
|
|
1324
|
+
type: "boolean"
|
|
1325
|
+
},
|
|
941
1326
|
{
|
|
942
1327
|
name: "files-with-matches",
|
|
943
1328
|
aliases: ["l"],
|
|
@@ -951,11 +1336,52 @@ var CliGrepCommandProcessor = class {
|
|
|
951
1336
|
description: "Select non-matching lines",
|
|
952
1337
|
required: false,
|
|
953
1338
|
type: "boolean"
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
name: "word-regexp",
|
|
1342
|
+
aliases: ["w"],
|
|
1343
|
+
description: "Match only whole words",
|
|
1344
|
+
required: false,
|
|
1345
|
+
type: "boolean"
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
name: "only-matching",
|
|
1349
|
+
aliases: ["o"],
|
|
1350
|
+
description: "Print only the matched parts, one per line",
|
|
1351
|
+
required: false,
|
|
1352
|
+
type: "boolean"
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
name: "extended-regexp",
|
|
1356
|
+
aliases: ["E"],
|
|
1357
|
+
description: "Extended regular expressions (patterns always are)",
|
|
1358
|
+
required: false,
|
|
1359
|
+
type: "boolean"
|
|
954
1360
|
}
|
|
955
1361
|
];
|
|
956
1362
|
}
|
|
1363
|
+
writeDescription(context) {
|
|
1364
|
+
const t = context.translator;
|
|
1365
|
+
writeWrapped(
|
|
1366
|
+
context,
|
|
1367
|
+
t.t("cli.grep.help.print_lines_match_pattern", "Print the lines that match a pattern. The pattern is a regular expression, matches are highlighted, and the input can be files, directories (with -r) or piped text.")
|
|
1368
|
+
);
|
|
1369
|
+
context.writer.writeln();
|
|
1370
|
+
writeUsage(context, [
|
|
1371
|
+
["grep <pattern> <file...>", t.t("cli.grep.help.search_one_more_files", "Search one or more files")],
|
|
1372
|
+
["grep -r <pattern> <dir>", t.t("cli.grep.help.search_every_file_under", "Search every file under a directory")],
|
|
1373
|
+
["<command> | grep <pattern>", t.t("cli.grep.help.filter_piped_input", "Filter piped input")]
|
|
1374
|
+
]);
|
|
1375
|
+
context.writer.writeln();
|
|
1376
|
+
writeExamples(context, [
|
|
1377
|
+
["grep -i error app.log", t.t("cli.grep.help.case_insensitive_search", "Case-insensitive search")],
|
|
1378
|
+
["grep -r -n TODO src", t.t("cli.grep.help.find_todos_line_numbers", "Find TODOs with line numbers")],
|
|
1379
|
+
["grep -v ^# config.txt", t.t("cli.grep.help.hide_comment_lines", "Hide comment lines")],
|
|
1380
|
+
["cat app.log | grep -c ERROR", t.t("cli.grep.help.count_matching_lines", "Count matching lines")]
|
|
1381
|
+
]);
|
|
1382
|
+
}
|
|
957
1383
|
async processCommand(command, context) {
|
|
958
|
-
const fs = context.services.
|
|
1384
|
+
const fs = context.services.getRequired(
|
|
959
1385
|
IFileSystemService_TOKEN
|
|
960
1386
|
);
|
|
961
1387
|
const ignoreCase = command.args["ignore-case"] || command.args["i"] || false;
|
|
@@ -964,51 +1390,56 @@ var CliGrepCommandProcessor = class {
|
|
|
964
1390
|
const countOnly = command.args["count"] || command.args["c"] || false;
|
|
965
1391
|
const filesOnly = command.args["files-with-matches"] || command.args["l"] || false;
|
|
966
1392
|
const invert = command.args["invert-match"] || command.args["v"] || false;
|
|
1393
|
+
const wordRegexp = command.args["word-regexp"] || command.args["w"] || false;
|
|
1394
|
+
const onlyMatching = command.args["only-matching"] || command.args["o"] || false;
|
|
1395
|
+
const quiet = command.args["quiet"] || command.args["q"] || false;
|
|
967
1396
|
const { pattern, paths } = this.parseArgs(command);
|
|
968
1397
|
if (!pattern) {
|
|
969
1398
|
context.writer.writeError(
|
|
970
1399
|
"grep: missing pattern. Usage: grep [options] <pattern> <file>"
|
|
971
1400
|
);
|
|
1401
|
+
context.process.exit(2, { silent: true });
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
let regex;
|
|
1405
|
+
try {
|
|
1406
|
+
const source = wordRegexp ? `(?<!\\w)(?:${pattern})(?!\\w)` : pattern;
|
|
1407
|
+
regex = new RegExp(source, ignoreCase ? "gi" : "g");
|
|
1408
|
+
} catch {
|
|
1409
|
+
context.writer.writeError(`grep: invalid pattern '${pattern}'`);
|
|
1410
|
+
context.process.exit(2, { silent: true });
|
|
972
1411
|
return;
|
|
973
1412
|
}
|
|
974
1413
|
if (paths.length === 0) {
|
|
975
1414
|
if (command.data != null) {
|
|
976
|
-
let regex2;
|
|
977
|
-
try {
|
|
978
|
-
regex2 = new RegExp(pattern, ignoreCase ? "gi" : "g");
|
|
979
|
-
} catch {
|
|
980
|
-
context.writer.writeError(`grep: invalid pattern '${pattern}'`);
|
|
981
|
-
return;
|
|
982
|
-
}
|
|
983
1415
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
984
|
-
this.grepContent(content, null,
|
|
1416
|
+
const found2 = this.grepContent(content, null, regex, {
|
|
985
1417
|
ignoreCase,
|
|
986
1418
|
showLineNum,
|
|
987
1419
|
countOnly,
|
|
988
1420
|
filesOnly,
|
|
989
|
-
invert
|
|
1421
|
+
invert,
|
|
1422
|
+
onlyMatching,
|
|
1423
|
+
quiet
|
|
990
1424
|
}, context, false);
|
|
1425
|
+
this.exitStatus(context, found2, false);
|
|
991
1426
|
return;
|
|
992
1427
|
}
|
|
993
1428
|
context.writer.writeError(
|
|
994
1429
|
"grep: missing file operand. Usage: grep [options] <pattern> <file>"
|
|
995
1430
|
);
|
|
996
|
-
|
|
997
|
-
}
|
|
998
|
-
let regex;
|
|
999
|
-
try {
|
|
1000
|
-
regex = new RegExp(pattern, ignoreCase ? "gi" : "g");
|
|
1001
|
-
} catch {
|
|
1002
|
-
context.writer.writeError(`grep: invalid pattern '${pattern}'`);
|
|
1431
|
+
context.process.exit(2, { silent: true });
|
|
1003
1432
|
return;
|
|
1004
1433
|
}
|
|
1005
1434
|
const filePaths = [];
|
|
1435
|
+
let failed = false;
|
|
1006
1436
|
for (const p of paths) {
|
|
1007
1437
|
try {
|
|
1008
1438
|
if (!fs.exists(p)) {
|
|
1009
1439
|
context.writer.writeError(
|
|
1010
1440
|
`grep: ${p}: No such file or directory`
|
|
1011
1441
|
);
|
|
1442
|
+
failed = true;
|
|
1012
1443
|
continue;
|
|
1013
1444
|
}
|
|
1014
1445
|
if (fs.isDirectory(p)) {
|
|
@@ -1016,6 +1447,7 @@ var CliGrepCommandProcessor = class {
|
|
|
1016
1447
|
context.writer.writeError(
|
|
1017
1448
|
`grep: ${p}: Is a directory`
|
|
1018
1449
|
);
|
|
1450
|
+
failed = true;
|
|
1019
1451
|
continue;
|
|
1020
1452
|
}
|
|
1021
1453
|
this.collectFiles(fs, fs.resolvePath(p), fs.getNode(p), filePaths);
|
|
@@ -1024,26 +1456,40 @@ var CliGrepCommandProcessor = class {
|
|
|
1024
1456
|
}
|
|
1025
1457
|
} catch (e) {
|
|
1026
1458
|
context.writer.writeError(`grep: ${e.message}`);
|
|
1459
|
+
failed = true;
|
|
1027
1460
|
}
|
|
1028
1461
|
}
|
|
1029
1462
|
const multiFile = filePaths.length > 1;
|
|
1463
|
+
let found = 0;
|
|
1030
1464
|
for (const filePath of filePaths) {
|
|
1031
1465
|
try {
|
|
1032
1466
|
const content = fs.readFile(filePath) ?? "";
|
|
1033
|
-
this.grepContent(content, filePath, regex, {
|
|
1467
|
+
found += this.grepContent(content, filePath, regex, {
|
|
1034
1468
|
ignoreCase,
|
|
1035
1469
|
showLineNum,
|
|
1036
1470
|
countOnly,
|
|
1037
1471
|
filesOnly,
|
|
1038
|
-
invert
|
|
1472
|
+
invert,
|
|
1473
|
+
onlyMatching,
|
|
1474
|
+
quiet
|
|
1039
1475
|
}, context, multiFile);
|
|
1040
1476
|
} catch (e) {
|
|
1041
1477
|
context.writer.writeError(`grep: ${filePath}: ${e.message}`);
|
|
1478
|
+
failed = true;
|
|
1042
1479
|
}
|
|
1043
1480
|
}
|
|
1481
|
+
this.exitStatus(context, found, failed);
|
|
1482
|
+
}
|
|
1483
|
+
/**
|
|
1484
|
+
* As Unix grep: 0 when a line matched, 1 when none did, 2 after an
|
|
1485
|
+
* error (so `if grep -q ...` and `grep x f && ...` work).
|
|
1486
|
+
*/
|
|
1487
|
+
exitStatus(context, found, failed) {
|
|
1488
|
+
if (found > 0) return;
|
|
1489
|
+
context.process.exit(failed ? 2 : 1, { silent: true });
|
|
1044
1490
|
}
|
|
1045
1491
|
grepContent(content, filePath, regex, options, context, multiFile) {
|
|
1046
|
-
const lines = content
|
|
1492
|
+
const lines = splitLines(content);
|
|
1047
1493
|
let matchCount = 0;
|
|
1048
1494
|
const matchingLines = [];
|
|
1049
1495
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
@@ -1056,6 +1502,7 @@ var CliGrepCommandProcessor = class {
|
|
|
1056
1502
|
matchingLines.push({ num: lineIdx + 1, text: line });
|
|
1057
1503
|
}
|
|
1058
1504
|
}
|
|
1505
|
+
if (options.quiet) return matchCount;
|
|
1059
1506
|
if (options.filesOnly) {
|
|
1060
1507
|
if (matchCount > 0 && filePath) {
|
|
1061
1508
|
context.writer.writeln(filePath);
|
|
@@ -1074,6 +1521,18 @@ var CliGrepCommandProcessor = class {
|
|
|
1074
1521
|
parts.push(context.writer.wrapInColor(String(m.num), CliForegroundColor.Green));
|
|
1075
1522
|
parts.push(":");
|
|
1076
1523
|
}
|
|
1524
|
+
if (options.onlyMatching && !options.invert) {
|
|
1525
|
+
regex.lastIndex = 0;
|
|
1526
|
+
const prefix = parts.join("");
|
|
1527
|
+
for (const match of m.text.match(regex) ?? []) {
|
|
1528
|
+
if (match !== "") {
|
|
1529
|
+
context.writer.writeln(
|
|
1530
|
+
prefix + context.writer.wrapInColor(match, CliForegroundColor.Red)
|
|
1531
|
+
);
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1534
|
+
continue;
|
|
1535
|
+
}
|
|
1077
1536
|
if (!options.invert) {
|
|
1078
1537
|
regex.lastIndex = 0;
|
|
1079
1538
|
const highlighted = m.text.replace(
|
|
@@ -1087,10 +1546,11 @@ var CliGrepCommandProcessor = class {
|
|
|
1087
1546
|
context.writer.writeln(parts.join(""));
|
|
1088
1547
|
}
|
|
1089
1548
|
}
|
|
1549
|
+
return matchCount;
|
|
1090
1550
|
}
|
|
1091
1551
|
parseArgs(command) {
|
|
1092
1552
|
const raw = command.value || "";
|
|
1093
|
-
const tokens = raw
|
|
1553
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
1094
1554
|
if (tokens.length === 0) {
|
|
1095
1555
|
return { pattern: null, paths: [] };
|
|
1096
1556
|
}
|
|
@@ -1103,8 +1563,9 @@ var CliGrepCommandProcessor = class {
|
|
|
1103
1563
|
};
|
|
1104
1564
|
}
|
|
1105
1565
|
collectFiles(fs, basePath, node, result) {
|
|
1106
|
-
|
|
1107
|
-
|
|
1566
|
+
const children = readableChildren(fs, basePath);
|
|
1567
|
+
if (!children) return;
|
|
1568
|
+
for (const child of children) {
|
|
1108
1569
|
const childPath = basePath === "/" ? `/${child.name}` : `${basePath}/${child.name}`;
|
|
1109
1570
|
if (child.type === "file") {
|
|
1110
1571
|
result.push(childPath);
|
|
@@ -1124,8 +1585,25 @@ var CliTacCommandProcessor = class {
|
|
|
1124
1585
|
this.valueRequired = true;
|
|
1125
1586
|
this.metadata = { icon: "\u{1F504}", module: "file management" };
|
|
1126
1587
|
}
|
|
1588
|
+
writeDescription(context) {
|
|
1589
|
+
const t = context.translator;
|
|
1590
|
+
writeWrapped(
|
|
1591
|
+
context,
|
|
1592
|
+
t.t("cli.tac.help.print_file_lines_reverse", "Print a file with its lines in reverse order, last line first. Works on piped input too.")
|
|
1593
|
+
);
|
|
1594
|
+
context.writer.writeln();
|
|
1595
|
+
writeUsage(context, [
|
|
1596
|
+
["tac <file>", t.t("cli.tac.help.print_file_bottom_top", "Print a file bottom to top")],
|
|
1597
|
+
["<command> | tac", t.t("cli.tac.help.reverse_piped_input", "Reverse piped input")]
|
|
1598
|
+
]);
|
|
1599
|
+
context.writer.writeln();
|
|
1600
|
+
writeExamples(context, [
|
|
1601
|
+
["tac app.log", t.t("cli.tac.help.newest_log_lines_first", "Newest log lines first")],
|
|
1602
|
+
["cat list.txt | tac", t.t("cli.tac.help.reverse_list", "Reverse a list")]
|
|
1603
|
+
]);
|
|
1604
|
+
}
|
|
1127
1605
|
async processCommand(command, context) {
|
|
1128
|
-
const fs = context.services.
|
|
1606
|
+
const fs = context.services.getRequired(
|
|
1129
1607
|
IFileSystemService_TOKEN
|
|
1130
1608
|
);
|
|
1131
1609
|
const paths = this.parsePaths(command);
|
|
@@ -1169,7 +1647,7 @@ var CliTacCommandProcessor = class {
|
|
|
1169
1647
|
}
|
|
1170
1648
|
parsePaths(command) {
|
|
1171
1649
|
const raw = command.value || "";
|
|
1172
|
-
return raw
|
|
1650
|
+
return splitArgs(raw).filter(Boolean);
|
|
1173
1651
|
}
|
|
1174
1652
|
};
|
|
1175
1653
|
var CliBasenameCommandProcessor = class {
|
|
@@ -1182,6 +1660,24 @@ var CliBasenameCommandProcessor = class {
|
|
|
1182
1660
|
this.valueRequired = true;
|
|
1183
1661
|
this.metadata = { icon: "\u{1F4DB}", module: "file management" };
|
|
1184
1662
|
}
|
|
1663
|
+
writeDescription(context) {
|
|
1664
|
+
const t = context.translator;
|
|
1665
|
+
writeWrapped(
|
|
1666
|
+
context,
|
|
1667
|
+
t.t("cli.basename.help.print_last_part_path", "Print the last part of a path, the file or directory name, without the directories in front. If a suffix is given, it is removed from the end too.")
|
|
1668
|
+
);
|
|
1669
|
+
context.writer.writeln();
|
|
1670
|
+
writeUsage(context, [
|
|
1671
|
+
["basename <path>", t.t("cli.basename.help.name_without_directories", "Name without the directories")],
|
|
1672
|
+
["basename <path> <suffix>", t.t("cli.basename.help.also_remove_trailing_suffix", "Also remove a trailing suffix")]
|
|
1673
|
+
]);
|
|
1674
|
+
context.writer.writeln();
|
|
1675
|
+
writeExamples(context, [
|
|
1676
|
+
["basename /home/user/notes.txt", t.t("cli.basename.help.prints_notes_txt", "Prints notes.txt")],
|
|
1677
|
+
["basename /home/user/notes.txt .txt", t.t("cli.basename.help.prints_notes", "Prints notes")],
|
|
1678
|
+
["basename /home/user/", t.t("cli.basename.help.prints_user", "Prints user")]
|
|
1679
|
+
]);
|
|
1680
|
+
}
|
|
1185
1681
|
async processCommand(command, context) {
|
|
1186
1682
|
const tokens = this.parseTokens(command);
|
|
1187
1683
|
if (tokens.length === 0) {
|
|
@@ -1207,7 +1703,7 @@ var CliBasenameCommandProcessor = class {
|
|
|
1207
1703
|
}
|
|
1208
1704
|
parseTokens(command) {
|
|
1209
1705
|
const raw = command.value || "";
|
|
1210
|
-
return raw
|
|
1706
|
+
return splitArgs(raw).filter(Boolean);
|
|
1211
1707
|
}
|
|
1212
1708
|
};
|
|
1213
1709
|
var CliDirnameCommandProcessor = class {
|
|
@@ -1220,6 +1716,23 @@ var CliDirnameCommandProcessor = class {
|
|
|
1220
1716
|
this.valueRequired = true;
|
|
1221
1717
|
this.metadata = { icon: "\u{1F4C2}", module: "file management" };
|
|
1222
1718
|
}
|
|
1719
|
+
writeDescription(context) {
|
|
1720
|
+
const t = context.translator;
|
|
1721
|
+
writeWrapped(
|
|
1722
|
+
context,
|
|
1723
|
+
t.t("cli.dirname.help.print_directory_part_path", "Print the directory part of a path, everything before the last /. A bare name with no / gives . (the current directory).")
|
|
1724
|
+
);
|
|
1725
|
+
context.writer.writeln();
|
|
1726
|
+
writeUsage(context, [
|
|
1727
|
+
["dirname <path>", t.t("cli.dirname.help.path_without_last_part", "Path without its last part")]
|
|
1728
|
+
]);
|
|
1729
|
+
context.writer.writeln();
|
|
1730
|
+
writeExamples(context, [
|
|
1731
|
+
["dirname /home/user/notes.txt", t.t("cli.dirname.help.prints_home_user", "Prints /home/user")],
|
|
1732
|
+
["dirname notes.txt", t.t("cli.dirname.help.prints", "Prints .")],
|
|
1733
|
+
["dirname /home", t.t("cli.dirname.help.prints_2", "Prints /")]
|
|
1734
|
+
]);
|
|
1735
|
+
}
|
|
1223
1736
|
async processCommand(command, context) {
|
|
1224
1737
|
const tokens = this.parseTokens(command);
|
|
1225
1738
|
if (tokens.length === 0) {
|
|
@@ -1246,7 +1759,7 @@ var CliDirnameCommandProcessor = class {
|
|
|
1246
1759
|
}
|
|
1247
1760
|
parseTokens(command) {
|
|
1248
1761
|
const raw = command.value || "";
|
|
1249
|
-
return raw
|
|
1762
|
+
return splitArgs(raw).filter(Boolean);
|
|
1250
1763
|
}
|
|
1251
1764
|
};
|
|
1252
1765
|
var CliSortCommandProcessor = class {
|
|
@@ -1296,117 +1809,97 @@ var CliSortCommandProcessor = class {
|
|
|
1296
1809
|
}
|
|
1297
1810
|
];
|
|
1298
1811
|
}
|
|
1812
|
+
writeDescription(context) {
|
|
1813
|
+
const t = context.translator;
|
|
1814
|
+
writeWrapped(
|
|
1815
|
+
context,
|
|
1816
|
+
t.t("cli.sort.help.sort_lines_file_piped", "Sort the lines of a file or piped input, alphabetically by default. Sort numerically, in reverse, by a field, or drop duplicates. Several files are sorted together into one list.")
|
|
1817
|
+
);
|
|
1818
|
+
context.writer.writeln();
|
|
1819
|
+
writeUsage(context, [
|
|
1820
|
+
["sort <file>", t.t("cli.sort.help.sort_lines_alphabetically", "Sort lines alphabetically")],
|
|
1821
|
+
[
|
|
1822
|
+
"sort -k <n> -t <sep> <file>",
|
|
1823
|
+
t.t("cli.sort.help.sort_field_split", "Sort by field <n>, split on <sep>")
|
|
1824
|
+
],
|
|
1825
|
+
["<command> | sort", t.t("cli.sort.help.sort_piped_input", "Sort piped input")]
|
|
1826
|
+
]);
|
|
1827
|
+
context.writer.writeln();
|
|
1828
|
+
writeExamples(context, [
|
|
1829
|
+
["sort names.txt", t.t("cli.sort.help.z", "A to Z")],
|
|
1830
|
+
["sort -n -r scores.txt", t.t("cli.sort.help.largest_number_first", "Largest number first")],
|
|
1831
|
+
["sort -t , -k 2 data.csv", t.t("cli.sort.help.sort_csv_second_column", "Sort a CSV by its second column")],
|
|
1832
|
+
["cat words.txt | sort -u", t.t("cli.sort.help.sorted_without_duplicates", "Sorted, without duplicates")]
|
|
1833
|
+
]);
|
|
1834
|
+
}
|
|
1299
1835
|
async processCommand(command, context) {
|
|
1300
|
-
const fs = context.services.
|
|
1836
|
+
const fs = context.services.getRequired(
|
|
1301
1837
|
IFileSystemService_TOKEN
|
|
1302
1838
|
);
|
|
1303
1839
|
const reverse = !!command.args["r"] || !!command.args["reverse"];
|
|
1304
1840
|
const numeric = !!command.args["n"] || !!command.args["numeric-sort"];
|
|
1305
1841
|
const unique = !!command.args["u"] || !!command.args["unique"];
|
|
1306
|
-
const keyField = command.args["k"]
|
|
1307
|
-
const delimiter = command.args["t"]
|
|
1842
|
+
const keyField = command.args["k"] ?? command.args["key"];
|
|
1843
|
+
const delimiter = command.args["t"] ?? command.args["field-separator"];
|
|
1308
1844
|
const paths = this.parsePaths(command);
|
|
1845
|
+
let lines = [];
|
|
1309
1846
|
if (paths.length === 0) {
|
|
1310
|
-
if (command.data
|
|
1311
|
-
|
|
1312
|
-
let lines = content.split("\n");
|
|
1313
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1314
|
-
lines.pop();
|
|
1315
|
-
}
|
|
1316
|
-
const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
|
|
1317
|
-
lines.sort((a, b) => {
|
|
1318
|
-
let va = a;
|
|
1319
|
-
let vb = b;
|
|
1320
|
-
if (keyIndex !== void 0 && keyIndex >= 0) {
|
|
1321
|
-
const sepRegex = delimiter ? new RegExp(
|
|
1322
|
-
delimiter.replace(
|
|
1323
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1324
|
-
"\\$&"
|
|
1325
|
-
)
|
|
1326
|
-
) : /\s+/;
|
|
1327
|
-
const fieldsA = a.split(sepRegex);
|
|
1328
|
-
const fieldsB = b.split(sepRegex);
|
|
1329
|
-
va = fieldsA[keyIndex] || "";
|
|
1330
|
-
vb = fieldsB[keyIndex] || "";
|
|
1331
|
-
}
|
|
1332
|
-
if (numeric) {
|
|
1333
|
-
const na = parseFloat(va) || 0;
|
|
1334
|
-
const nb = parseFloat(vb) || 0;
|
|
1335
|
-
return na - nb;
|
|
1336
|
-
}
|
|
1337
|
-
return va.localeCompare(vb);
|
|
1338
|
-
});
|
|
1339
|
-
if (unique) {
|
|
1340
|
-
lines = lines.filter(
|
|
1341
|
-
(line, i, arr) => i === 0 || line !== arr[i - 1]
|
|
1342
|
-
);
|
|
1343
|
-
}
|
|
1344
|
-
if (reverse) {
|
|
1345
|
-
lines.reverse();
|
|
1346
|
-
}
|
|
1347
|
-
context.writer.writeln(lines.join("\n"));
|
|
1847
|
+
if (command.data == null) {
|
|
1848
|
+
context.writer.writeError("sort: missing file operand");
|
|
1348
1849
|
return;
|
|
1349
1850
|
}
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
const content = fs.readFile(path);
|
|
1360
|
-
if (content === null) {
|
|
1361
|
-
context.writer.writeError(
|
|
1362
|
-
`sort: ${path}: No such file or directory`
|
|
1363
|
-
);
|
|
1364
|
-
continue;
|
|
1365
|
-
}
|
|
1366
|
-
let lines = content.split("\n");
|
|
1367
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1368
|
-
lines.pop();
|
|
1369
|
-
}
|
|
1370
|
-
const keyIndex = keyField ? parseInt(keyField, 10) - 1 : void 0;
|
|
1371
|
-
lines.sort((a, b) => {
|
|
1372
|
-
let va = a;
|
|
1373
|
-
let vb = b;
|
|
1374
|
-
if (keyIndex !== void 0 && keyIndex >= 0) {
|
|
1375
|
-
const sepRegex = delimiter ? new RegExp(
|
|
1376
|
-
delimiter.replace(
|
|
1377
|
-
/[.*+?^${}()|[\]\\]/g,
|
|
1378
|
-
"\\$&"
|
|
1379
|
-
)
|
|
1380
|
-
) : /\s+/;
|
|
1381
|
-
const fieldsA = a.split(sepRegex);
|
|
1382
|
-
const fieldsB = b.split(sepRegex);
|
|
1383
|
-
va = fieldsA[keyIndex] || "";
|
|
1384
|
-
vb = fieldsB[keyIndex] || "";
|
|
1851
|
+
lines = splitLines(pipedText(command.data));
|
|
1852
|
+
} else {
|
|
1853
|
+
for (const path of paths) {
|
|
1854
|
+
try {
|
|
1855
|
+
if (fs.isDirectory(path)) {
|
|
1856
|
+
context.writer.writeError(
|
|
1857
|
+
`sort: ${path}: Is a directory`
|
|
1858
|
+
);
|
|
1859
|
+
continue;
|
|
1385
1860
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1861
|
+
const content = fs.readFile(path);
|
|
1862
|
+
if (content === null) {
|
|
1863
|
+
context.writer.writeError(
|
|
1864
|
+
`sort: ${path}: No such file or directory`
|
|
1865
|
+
);
|
|
1866
|
+
continue;
|
|
1390
1867
|
}
|
|
1391
|
-
|
|
1392
|
-
})
|
|
1393
|
-
|
|
1394
|
-
lines = lines.filter(
|
|
1395
|
-
(line, i, arr) => i === 0 || line !== arr[i - 1]
|
|
1396
|
-
);
|
|
1868
|
+
lines.push(...splitLines(content));
|
|
1869
|
+
} catch (e) {
|
|
1870
|
+
context.writer.writeError(`sort: ${e.message}`);
|
|
1397
1871
|
}
|
|
1398
|
-
if (reverse) {
|
|
1399
|
-
lines.reverse();
|
|
1400
|
-
}
|
|
1401
|
-
context.writer.writeln(lines.join("\n"));
|
|
1402
|
-
} catch (e) {
|
|
1403
|
-
context.writer.writeError(`sort: ${e.message}`);
|
|
1404
1872
|
}
|
|
1405
1873
|
}
|
|
1874
|
+
const keyIndex = keyField != null && keyField !== true ? parseInt(String(keyField), 10) - 1 : void 0;
|
|
1875
|
+
const sepRegex = delimiter != null && delimiter !== true ? new RegExp(
|
|
1876
|
+
String(delimiter).replace(/[.*+?^${}()|[\]\\]/g, "\\$&")
|
|
1877
|
+
) : /\s+/;
|
|
1878
|
+
const keyOf = (line) => keyIndex !== void 0 && keyIndex >= 0 ? line.split(sepRegex)[keyIndex] || "" : line;
|
|
1879
|
+
const compare = (a, b) => {
|
|
1880
|
+
const va = keyOf(a);
|
|
1881
|
+
const vb = keyOf(b);
|
|
1882
|
+
if (numeric) {
|
|
1883
|
+
return (parseFloat(va) || 0) - (parseFloat(vb) || 0);
|
|
1884
|
+
}
|
|
1885
|
+
return va.localeCompare(vb);
|
|
1886
|
+
};
|
|
1887
|
+
lines.sort(compare);
|
|
1888
|
+
if (unique) {
|
|
1889
|
+
lines = lines.filter(
|
|
1890
|
+
(line, i, arr) => i === 0 || compare(line, arr[i - 1]) !== 0
|
|
1891
|
+
);
|
|
1892
|
+
}
|
|
1893
|
+
if (reverse) {
|
|
1894
|
+
lines.reverse();
|
|
1895
|
+
}
|
|
1896
|
+
if (lines.length > 0) {
|
|
1897
|
+
context.writer.writeln(lines.join("\n"));
|
|
1898
|
+
}
|
|
1406
1899
|
}
|
|
1407
1900
|
parsePaths(command) {
|
|
1408
1901
|
const raw = command.value || "";
|
|
1409
|
-
return raw
|
|
1902
|
+
return splitArgs(raw).filter(Boolean);
|
|
1410
1903
|
}
|
|
1411
1904
|
};
|
|
1412
1905
|
var CliUniqCommandProcessor = class {
|
|
@@ -1449,8 +1942,26 @@ var CliUniqCommandProcessor = class {
|
|
|
1449
1942
|
}
|
|
1450
1943
|
];
|
|
1451
1944
|
}
|
|
1945
|
+
writeDescription(context) {
|
|
1946
|
+
const t = context.translator;
|
|
1947
|
+
writeWrapped(
|
|
1948
|
+
context,
|
|
1949
|
+
t.t("cli.uniq.help.collapse_repeated_lines_next", "Collapse repeated lines that are next to each other into one. Only adjacent duplicates are merged, so sort the input first to catch them all.")
|
|
1950
|
+
);
|
|
1951
|
+
context.writer.writeln();
|
|
1952
|
+
writeUsage(context, [
|
|
1953
|
+
["uniq <file>", t.t("cli.uniq.help.drop_adjacent_duplicate_lines", "Drop adjacent duplicate lines")],
|
|
1954
|
+
["<command> | uniq", t.t("cli.uniq.help.filter_piped_input", "Filter piped input")]
|
|
1955
|
+
]);
|
|
1956
|
+
context.writer.writeln();
|
|
1957
|
+
writeExamples(context, [
|
|
1958
|
+
["uniq log.txt", t.t("cli.uniq.help.collapse_repeated_lines", "Collapse repeated lines")],
|
|
1959
|
+
["sort words.txt | uniq -c", t.t("cli.uniq.help.count_how_often_each", "Count how often each line appears")],
|
|
1960
|
+
["sort words.txt | uniq -d", t.t("cli.uniq.help.show_only_duplicated_lines", "Show only the duplicated lines")]
|
|
1961
|
+
]);
|
|
1962
|
+
}
|
|
1452
1963
|
async processCommand(command, context) {
|
|
1453
|
-
const fs = context.services.
|
|
1964
|
+
const fs = context.services.getRequired(
|
|
1454
1965
|
IFileSystemService_TOKEN
|
|
1455
1966
|
);
|
|
1456
1967
|
const showCount = !!command.args["c"] || !!command.args["count"];
|
|
@@ -1461,7 +1972,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1461
1972
|
if (paths.length === 0) {
|
|
1462
1973
|
if (command.data != null) {
|
|
1463
1974
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1464
|
-
|
|
1975
|
+
const lines = content.split("\n");
|
|
1465
1976
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1466
1977
|
lines.pop();
|
|
1467
1978
|
}
|
|
@@ -1509,7 +2020,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1509
2020
|
);
|
|
1510
2021
|
continue;
|
|
1511
2022
|
}
|
|
1512
|
-
|
|
2023
|
+
const lines = content.split("\n");
|
|
1513
2024
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1514
2025
|
lines.pop();
|
|
1515
2026
|
}
|
|
@@ -1544,7 +2055,7 @@ var CliUniqCommandProcessor = class {
|
|
|
1544
2055
|
}
|
|
1545
2056
|
parsePaths(command) {
|
|
1546
2057
|
const raw = command.value || "";
|
|
1547
|
-
return raw
|
|
2058
|
+
return splitArgs(raw).filter(Boolean);
|
|
1548
2059
|
}
|
|
1549
2060
|
};
|
|
1550
2061
|
var CliCutCommandProcessor = class {
|
|
@@ -1580,26 +2091,45 @@ var CliCutCommandProcessor = class {
|
|
|
1580
2091
|
}
|
|
1581
2092
|
];
|
|
1582
2093
|
}
|
|
2094
|
+
writeDescription(context) {
|
|
2095
|
+
const t = context.translator;
|
|
2096
|
+
writeWrapped(
|
|
2097
|
+
context,
|
|
2098
|
+
t.t("cli.cut.help.print_selected_parts_each", "Print selected parts of each line: fields split by a delimiter (-f, tab by default) or character positions (-c). A list can be single numbers, ranges or both, such as 1,3 or 2-4 or 3-.")
|
|
2099
|
+
);
|
|
2100
|
+
context.writer.writeln();
|
|
2101
|
+
writeUsage(context, [
|
|
2102
|
+
["cut -d <sep> -f <list> <file>", t.t("cli.cut.help.select_fields", "Select fields")],
|
|
2103
|
+
["cut -c <list> <file>", t.t("cli.cut.help.select_character_positions", "Select character positions")],
|
|
2104
|
+
["<command> | cut -f <list>", t.t("cli.cut.help.cut_piped_input", "Cut piped input")]
|
|
2105
|
+
]);
|
|
2106
|
+
context.writer.writeln();
|
|
2107
|
+
writeExamples(context, [
|
|
2108
|
+
["cut -d , -f 1,3 data.csv", t.t("cli.cut.help.first_third_csv_column", "First and third CSV column")],
|
|
2109
|
+
["cut -c 1-10 notes.txt", t.t("cli.cut.help.first_10_characters_each", "First 10 characters of each line")],
|
|
2110
|
+
["cat users.txt | cut -d : -f 1-2", t.t("cli.cut.help.first_two_fields_split", "First two fields split on :")]
|
|
2111
|
+
]);
|
|
2112
|
+
}
|
|
1583
2113
|
async processCommand(command, context) {
|
|
1584
|
-
const fs = context.services.
|
|
2114
|
+
const fs = context.services.getRequired(
|
|
1585
2115
|
IFileSystemService_TOKEN
|
|
1586
2116
|
);
|
|
1587
2117
|
const delimiter = command.args["d"] || command.args["delimiter"] || " ";
|
|
1588
|
-
const fieldsSpec = command.args["f"]
|
|
1589
|
-
const charsSpec = command.args["c"]
|
|
1590
|
-
if (
|
|
2118
|
+
const fieldsSpec = command.args["f"] ?? command.args["fields"];
|
|
2119
|
+
const charsSpec = command.args["c"] ?? command.args["characters"];
|
|
2120
|
+
if ((fieldsSpec === void 0 || fieldsSpec === "") && (charsSpec === void 0 || charsSpec === "")) {
|
|
1591
2121
|
context.writer.writeError(
|
|
1592
2122
|
"cut: you must specify either -f (fields) or -c (characters)"
|
|
1593
2123
|
);
|
|
1594
2124
|
return;
|
|
1595
2125
|
}
|
|
1596
|
-
const spec = fieldsSpec
|
|
2126
|
+
const spec = String(fieldsSpec ?? charsSpec);
|
|
1597
2127
|
const indices = this.parseSpec(spec);
|
|
1598
2128
|
const paths = this.parsePaths(command);
|
|
1599
2129
|
if (paths.length === 0) {
|
|
1600
2130
|
if (command.data != null) {
|
|
1601
2131
|
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1602
|
-
|
|
2132
|
+
const lines = content.split("\n");
|
|
1603
2133
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1604
2134
|
lines.pop();
|
|
1605
2135
|
}
|
|
@@ -1641,7 +2171,7 @@ var CliCutCommandProcessor = class {
|
|
|
1641
2171
|
);
|
|
1642
2172
|
continue;
|
|
1643
2173
|
}
|
|
1644
|
-
|
|
2174
|
+
const lines = content.split("\n");
|
|
1645
2175
|
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1646
2176
|
lines.pop();
|
|
1647
2177
|
}
|
|
@@ -1701,7 +2231,7 @@ var CliCutCommandProcessor = class {
|
|
|
1701
2231
|
}
|
|
1702
2232
|
parsePaths(command) {
|
|
1703
2233
|
const raw = command.value || "";
|
|
1704
|
-
return raw
|
|
2234
|
+
return splitArgs(raw).filter(Boolean);
|
|
1705
2235
|
}
|
|
1706
2236
|
};
|
|
1707
2237
|
var CliPasteCommandProcessor = class {
|
|
@@ -1730,23 +2260,47 @@ var CliPasteCommandProcessor = class {
|
|
|
1730
2260
|
}
|
|
1731
2261
|
];
|
|
1732
2262
|
}
|
|
2263
|
+
writeDescription(context) {
|
|
2264
|
+
const t = context.translator;
|
|
2265
|
+
writeWrapped(
|
|
2266
|
+
context,
|
|
2267
|
+
t.t("cli.paste.help.join_files_side_side", "Join files side by side: line 1 of each file goes on one output line, separated by a tab, then line 2, and so on. With -s, each file is joined onto a single line instead. Piped input is read as the file - (or as the only input when no file is named); -d takes a list of delimiters used in turn.")
|
|
2268
|
+
);
|
|
2269
|
+
context.writer.writeln();
|
|
2270
|
+
writeUsage(context, [
|
|
2271
|
+
["paste <file1> <file2...>", t.t("cli.paste.help.merge_files_line_line", "Merge files line by line")],
|
|
2272
|
+
["paste -s <file>", t.t("cli.paste.help.join_all_lines_file", "Join all lines of a file into one")],
|
|
2273
|
+
["<command> | paste - <file>", t.t("cli.paste.help.merge_piped_input_file", "Merge piped input with a file")]
|
|
2274
|
+
]);
|
|
2275
|
+
context.writer.writeln();
|
|
2276
|
+
writeExamples(context, [
|
|
2277
|
+
["paste names.txt ages.txt", t.t("cli.paste.help.two_columns_tab_separated", "Two columns, tab separated")],
|
|
2278
|
+
["paste -d , a.txt b.txt", t.t("cli.paste.help.merge_comma_separated", "Merge as comma-separated")],
|
|
2279
|
+
["paste -s -d , list.txt", t.t("cli.paste.help.turn_list_one_csv", "Turn a list into one CSV line")]
|
|
2280
|
+
]);
|
|
2281
|
+
}
|
|
1733
2282
|
async processCommand(command, context) {
|
|
1734
|
-
const fs = context.services.
|
|
2283
|
+
const fs = context.services.getRequired(
|
|
1735
2284
|
IFileSystemService_TOKEN
|
|
1736
2285
|
);
|
|
1737
|
-
const
|
|
2286
|
+
const delimiterArg = command.args["d"] ?? command.args["delimiters"] ?? " ";
|
|
2287
|
+
const delimiters = this.parseDelimiters(String(delimiterArg));
|
|
1738
2288
|
const serial = !!command.args["s"] || !!command.args["serial"];
|
|
1739
2289
|
const paths = this.parsePaths(command);
|
|
2290
|
+
const hasStdin = command.data != null;
|
|
1740
2291
|
if (paths.length === 0) {
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
return;
|
|
2292
|
+
if (!hasStdin) {
|
|
2293
|
+
context.writer.writeError("paste: missing file operand");
|
|
2294
|
+
return;
|
|
2295
|
+
}
|
|
2296
|
+
paths.push("-");
|
|
1747
2297
|
}
|
|
1748
2298
|
const fileLines = [];
|
|
1749
2299
|
for (const path of paths) {
|
|
2300
|
+
if (path === "-" && hasStdin) {
|
|
2301
|
+
fileLines.push(splitLines(pipedText(command.data)));
|
|
2302
|
+
continue;
|
|
2303
|
+
}
|
|
1750
2304
|
try {
|
|
1751
2305
|
if (fs.isDirectory(path)) {
|
|
1752
2306
|
context.writer.writeError(
|
|
@@ -1761,20 +2315,18 @@ var CliPasteCommandProcessor = class {
|
|
|
1761
2315
|
);
|
|
1762
2316
|
return;
|
|
1763
2317
|
}
|
|
1764
|
-
|
|
1765
|
-
if (lines.length > 0 && lines[lines.length - 1] === "") {
|
|
1766
|
-
lines.pop();
|
|
1767
|
-
}
|
|
1768
|
-
fileLines.push(lines);
|
|
2318
|
+
fileLines.push(splitLines(content));
|
|
1769
2319
|
} catch (e) {
|
|
1770
2320
|
context.writer.writeError(`paste: ${e.message}`);
|
|
1771
2321
|
return;
|
|
1772
2322
|
}
|
|
1773
2323
|
}
|
|
2324
|
+
const join = (parts) => parts.reduce(
|
|
2325
|
+
(line, part, i) => i === 0 ? part : line + delimiters[(i - 1) % delimiters.length] + part,
|
|
2326
|
+
""
|
|
2327
|
+
);
|
|
1774
2328
|
if (serial) {
|
|
1775
|
-
const output = fileLines.map(
|
|
1776
|
-
(lines) => lines.join(delimiter)
|
|
1777
|
-
);
|
|
2329
|
+
const output = fileLines.map((lines) => join(lines));
|
|
1778
2330
|
context.writer.writeln(output.join("\n"));
|
|
1779
2331
|
} else {
|
|
1780
2332
|
const maxLines = Math.max(
|
|
@@ -1782,21 +2334,59 @@ var CliPasteCommandProcessor = class {
|
|
|
1782
2334
|
);
|
|
1783
2335
|
const output = [];
|
|
1784
2336
|
for (let i = 0; i < maxLines; i++) {
|
|
1785
|
-
|
|
1786
|
-
output.push(parts.join(delimiter));
|
|
2337
|
+
output.push(join(fileLines.map((lines) => lines[i] ?? "")));
|
|
1787
2338
|
}
|
|
1788
2339
|
context.writer.writeln(output.join("\n"));
|
|
1789
2340
|
}
|
|
1790
2341
|
}
|
|
2342
|
+
/** `-d` list: each character in turn, with \t, \n, \\ and \0 (empty) escapes. */
|
|
2343
|
+
parseDelimiters(list) {
|
|
2344
|
+
const result = [];
|
|
2345
|
+
for (let i = 0; i < list.length; i++) {
|
|
2346
|
+
if (list[i] === "\\" && i + 1 < list.length) {
|
|
2347
|
+
const next = list[++i];
|
|
2348
|
+
result.push(
|
|
2349
|
+
next === "t" ? " " : next === "n" ? "\n" : next === "0" ? "" : next
|
|
2350
|
+
);
|
|
2351
|
+
} else {
|
|
2352
|
+
result.push(list[i]);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
return result.length > 0 ? result : [" "];
|
|
2356
|
+
}
|
|
2357
|
+
/**
|
|
2358
|
+
* The file operands. The engine drops a lone `-` from the value, so with
|
|
2359
|
+
* piped input they are read from the raw command line to keep it.
|
|
2360
|
+
*/
|
|
1791
2361
|
parsePaths(command) {
|
|
1792
|
-
const
|
|
1793
|
-
|
|
2362
|
+
const fromValue = splitArgs(command.value || "").filter(Boolean);
|
|
2363
|
+
if (command.data == null || !command.rawCommand) return fromValue;
|
|
2364
|
+
const words = splitArgs(command.rawCommand).slice(1);
|
|
2365
|
+
if (!words.includes("-")) return fromValue;
|
|
2366
|
+
const result = [];
|
|
2367
|
+
for (let i = 0; i < words.length; i++) {
|
|
2368
|
+
const word = words[i];
|
|
2369
|
+
if (word === "-" || !word.startsWith("-")) {
|
|
2370
|
+
result.push(word);
|
|
2371
|
+
} else if (/^(-d|--delimiters)$/.test(word)) {
|
|
2372
|
+
i++;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2375
|
+
return result;
|
|
1794
2376
|
}
|
|
1795
2377
|
};
|
|
1796
2378
|
function expandSet(set) {
|
|
1797
2379
|
const chars = [];
|
|
1798
2380
|
let i = 0;
|
|
1799
2381
|
while (i < set.length) {
|
|
2382
|
+
if (set[i] === "\\" && i + 1 < set.length) {
|
|
2383
|
+
const next = set[i + 1];
|
|
2384
|
+
chars.push(
|
|
2385
|
+
next === "n" ? "\n" : next === "t" ? " " : next === "r" ? "\r" : next
|
|
2386
|
+
);
|
|
2387
|
+
i += 2;
|
|
2388
|
+
continue;
|
|
2389
|
+
}
|
|
1800
2390
|
if (set[i] === "[" && set[i + 1] === ":") {
|
|
1801
2391
|
const end = set.indexOf(":]", i + 2);
|
|
1802
2392
|
if (end !== -1) {
|
|
@@ -1806,7 +2396,7 @@ function expandSet(set) {
|
|
|
1806
2396
|
continue;
|
|
1807
2397
|
}
|
|
1808
2398
|
}
|
|
1809
|
-
if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !==
|
|
2399
|
+
if (i + 2 < set.length && set[i + 1] === "-" && set[i + 2] !== "\\") {
|
|
1810
2400
|
const start = set.charCodeAt(i);
|
|
1811
2401
|
const finish = set.charCodeAt(i + 2);
|
|
1812
2402
|
if (start <= finish) {
|
|
@@ -1876,7 +2466,7 @@ function parseTokens(raw) {
|
|
|
1876
2466
|
var CliTrCommandProcessor = class {
|
|
1877
2467
|
constructor() {
|
|
1878
2468
|
this.command = "tr";
|
|
1879
|
-
this.description = "Translate or delete characters in
|
|
2469
|
+
this.description = "Translate or delete characters in piped input";
|
|
1880
2470
|
this.author = DefaultLibraryAuthor;
|
|
1881
2471
|
this.version = LIBRARY_VERSION;
|
|
1882
2472
|
this.acceptsRawInput = true;
|
|
@@ -1884,103 +2474,76 @@ var CliTrCommandProcessor = class {
|
|
|
1884
2474
|
this.metadata = { icon: "\u{1F500}", module: "file management" };
|
|
1885
2475
|
this.parameters = [
|
|
1886
2476
|
{
|
|
1887
|
-
name: "
|
|
2477
|
+
name: "delete",
|
|
2478
|
+
aliases: ["d"],
|
|
1888
2479
|
description: "Delete characters in SET1",
|
|
1889
2480
|
required: false,
|
|
1890
2481
|
type: "boolean"
|
|
1891
2482
|
},
|
|
1892
2483
|
{
|
|
1893
|
-
name: "
|
|
2484
|
+
name: "squeeze-repeats",
|
|
2485
|
+
aliases: ["s"],
|
|
1894
2486
|
description: "Squeeze repeated characters in last operand set",
|
|
1895
2487
|
required: false,
|
|
1896
2488
|
type: "boolean"
|
|
1897
2489
|
}
|
|
1898
2490
|
];
|
|
1899
2491
|
}
|
|
1900
|
-
|
|
1901
|
-
const
|
|
1902
|
-
|
|
2492
|
+
writeDescription(context) {
|
|
2493
|
+
const t = context.translator;
|
|
2494
|
+
writeWrapped(
|
|
2495
|
+
context,
|
|
2496
|
+
t.t("cli.tr.help.translate_characters_set1_matching", "Translate characters from SET1 into the matching characters of SET2, delete them (-d) or squeeze repeats into one (-s). Sets can use ranges like a-z and classes like [:upper:], [:lower:], [:digit:], [:alpha:], [:alnum:] and [:space:]. Escapes \\n, \\t and \\\\ are understood. Like Unix tr, it reads only piped input and takes no file operands: to transform a file, pipe it in with cat.")
|
|
1903
2497
|
);
|
|
2498
|
+
context.writer.writeln();
|
|
2499
|
+
writeUsage(context, [
|
|
2500
|
+
["<command> | tr <set1> <set2>", t.t("cli.tr.help.replace_characters", "Replace characters")],
|
|
2501
|
+
["<command> | tr -d <set1>", t.t("cli.tr.help.delete_characters", "Delete characters")],
|
|
2502
|
+
["<command> | tr -s <set1> [set2]", t.t("cli.tr.help.squeeze_repeated_characters", "Squeeze repeated characters")]
|
|
2503
|
+
]);
|
|
2504
|
+
context.writer.writeln();
|
|
2505
|
+
writeExamples(context, [
|
|
2506
|
+
["cat notes.txt | tr a-z A-Z", t.t("cli.tr.help.convert_file_upper_case", "Convert a file to upper case")],
|
|
2507
|
+
["echo hello | tr a-z A-Z", t.t("cli.tr.help.upper_case_piped_text", "Upper-case piped text")],
|
|
2508
|
+
["cat notes.txt | tr -d 0-9", t.t("cli.tr.help.remove_all_digits", "Remove all digits")],
|
|
2509
|
+
["cat notes.txt | tr -s ' '", t.t("cli.tr.help.collapse_runs_spaces", "Collapse runs of spaces")]
|
|
2510
|
+
]);
|
|
2511
|
+
}
|
|
2512
|
+
async processCommand(command, context) {
|
|
1904
2513
|
const raw = command.value || "";
|
|
1905
2514
|
const positional = parseTokens(raw);
|
|
1906
|
-
const deleteMode = !!command.args["d"];
|
|
1907
|
-
const squeezeMode = !!command.args["s"];
|
|
2515
|
+
const deleteMode = !!command.args["d"] || !!command.args["delete"];
|
|
2516
|
+
const squeezeMode = !!command.args["s"] || !!command.args["squeeze-repeats"];
|
|
1908
2517
|
if (positional.length === 0) {
|
|
1909
|
-
|
|
2518
|
+
this.fail(context, "tr: missing operand");
|
|
1910
2519
|
return;
|
|
1911
2520
|
}
|
|
1912
|
-
let
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
if (deleteMode && squeezeMode) {
|
|
1916
|
-
if (positional.length < 2) {
|
|
1917
|
-
context.writer.writeError("tr: missing operand");
|
|
1918
|
-
return;
|
|
1919
|
-
}
|
|
1920
|
-
set1Str = positional[0];
|
|
1921
|
-
set2Str = positional[1];
|
|
1922
|
-
if (positional.length >= 3) {
|
|
1923
|
-
filePath = positional[2];
|
|
1924
|
-
}
|
|
1925
|
-
} else if (deleteMode) {
|
|
1926
|
-
if (positional.length < 1) {
|
|
1927
|
-
context.writer.writeError("tr: missing operand");
|
|
1928
|
-
return;
|
|
1929
|
-
}
|
|
1930
|
-
set1Str = positional[0];
|
|
1931
|
-
if (positional.length >= 2) {
|
|
1932
|
-
filePath = positional[1];
|
|
1933
|
-
}
|
|
2521
|
+
let setCount;
|
|
2522
|
+
if (deleteMode) {
|
|
2523
|
+
setCount = squeezeMode ? 2 : 1;
|
|
1934
2524
|
} else if (squeezeMode) {
|
|
1935
|
-
|
|
1936
|
-
context.writer.writeError("tr: missing operand");
|
|
1937
|
-
return;
|
|
1938
|
-
}
|
|
1939
|
-
if (positional.length >= 3) {
|
|
1940
|
-
set1Str = positional[0];
|
|
1941
|
-
set2Str = positional[1];
|
|
1942
|
-
filePath = positional[2];
|
|
1943
|
-
} else if (positional.length === 2) {
|
|
1944
|
-
set1Str = positional[0];
|
|
1945
|
-
filePath = positional[1];
|
|
1946
|
-
} else {
|
|
1947
|
-
set1Str = positional[0];
|
|
1948
|
-
}
|
|
2525
|
+
setCount = Math.min(positional.length, 2);
|
|
1949
2526
|
} else {
|
|
1950
|
-
|
|
1951
|
-
context.writer.writeError("tr: missing operand");
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
set1Str = positional[0];
|
|
1955
|
-
set2Str = positional[1];
|
|
1956
|
-
if (positional.length >= 3) {
|
|
1957
|
-
filePath = positional[2];
|
|
1958
|
-
}
|
|
2527
|
+
setCount = 2;
|
|
1959
2528
|
}
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
}
|
|
1974
|
-
} catch (e) {
|
|
1975
|
-
context.writer.writeError(`tr: ${e.message}`);
|
|
1976
|
-
return;
|
|
1977
|
-
}
|
|
1978
|
-
} else if (command.data != null) {
|
|
1979
|
-
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1980
|
-
} else {
|
|
1981
|
-
context.writer.writeError("tr: missing file operand");
|
|
2529
|
+
if (positional.length < setCount) {
|
|
2530
|
+
this.fail(context, "tr: missing operand");
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
if (positional.length > setCount) {
|
|
2534
|
+
this.fail(context, `tr: extra operand '${positional[setCount]}'`);
|
|
2535
|
+
return;
|
|
2536
|
+
}
|
|
2537
|
+
if (command.data == null) {
|
|
2538
|
+
this.fail(
|
|
2539
|
+
context,
|
|
2540
|
+
"tr: no input: tr reads only piped input and takes no file operands; use 'cat file | tr a-z A-Z'"
|
|
2541
|
+
);
|
|
1982
2542
|
return;
|
|
1983
2543
|
}
|
|
2544
|
+
const set1Str = positional[0];
|
|
2545
|
+
const set2Str = setCount === 2 ? positional[1] : void 0;
|
|
2546
|
+
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
1984
2547
|
try {
|
|
1985
2548
|
const set1 = expandSet(set1Str);
|
|
1986
2549
|
const set1Set = new Set(set1);
|
|
@@ -2003,9 +2566,13 @@ var CliTrCommandProcessor = class {
|
|
|
2003
2566
|
}
|
|
2004
2567
|
context.writer.writeln(result);
|
|
2005
2568
|
} catch (e) {
|
|
2006
|
-
|
|
2569
|
+
this.fail(context, `tr: ${e.message}`);
|
|
2007
2570
|
}
|
|
2008
2571
|
}
|
|
2572
|
+
fail(context, message) {
|
|
2573
|
+
context.writer.writeError(message);
|
|
2574
|
+
context.process.exit(1, { silent: true });
|
|
2575
|
+
}
|
|
2009
2576
|
deleteChars(content, deleteSet) {
|
|
2010
2577
|
let result = "";
|
|
2011
2578
|
for (const ch of content) {
|
|
@@ -2052,8 +2619,24 @@ var CliStatCommandProcessor = class {
|
|
|
2052
2619
|
this.metadata = { icon: "\u{1F4CA}", module: "file management" };
|
|
2053
2620
|
this.parameters = [];
|
|
2054
2621
|
}
|
|
2622
|
+
writeDescription(context) {
|
|
2623
|
+
const t = context.translator;
|
|
2624
|
+
writeWrapped(
|
|
2625
|
+
context,
|
|
2626
|
+
t.t("cli.stat.help.show_details_about_files", "Show details about files or directories: full path, size, type, permissions, owner, group, and when it was created and last modified.")
|
|
2627
|
+
);
|
|
2628
|
+
context.writer.writeln();
|
|
2629
|
+
writeUsage(context, [
|
|
2630
|
+
["stat <path...>", t.t("cli.stat.help.show_details_each_path", "Show details for each path")]
|
|
2631
|
+
]);
|
|
2632
|
+
context.writer.writeln();
|
|
2633
|
+
writeExamples(context, [
|
|
2634
|
+
["stat notes.txt", t.t("cli.stat.help.details_one_file", "Details of one file")],
|
|
2635
|
+
["stat ~/docs notes.txt", t.t("cli.stat.help.several_paths_once", "Several paths at once")]
|
|
2636
|
+
]);
|
|
2637
|
+
}
|
|
2055
2638
|
async processCommand(command, context) {
|
|
2056
|
-
const fs = context.services.
|
|
2639
|
+
const fs = context.services.getRequired(
|
|
2057
2640
|
IFileSystemService_TOKEN
|
|
2058
2641
|
);
|
|
2059
2642
|
const paths = this.parsePaths(command);
|
|
@@ -2063,6 +2646,10 @@ var CliStatCommandProcessor = class {
|
|
|
2063
2646
|
}
|
|
2064
2647
|
for (const path of paths) {
|
|
2065
2648
|
const resolved = fs.resolvePath(path);
|
|
2649
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2650
|
+
context.writer.writeError(`stat: cannot stat '${path}': Permission denied`);
|
|
2651
|
+
continue;
|
|
2652
|
+
}
|
|
2066
2653
|
const node = fs.getNode(resolved);
|
|
2067
2654
|
if (!node) {
|
|
2068
2655
|
context.writer.writeError(
|
|
@@ -2084,7 +2671,7 @@ var CliStatCommandProcessor = class {
|
|
|
2084
2671
|
}
|
|
2085
2672
|
parsePaths(command) {
|
|
2086
2673
|
const raw = command.value || "";
|
|
2087
|
-
return raw
|
|
2674
|
+
return splitArgs(raw).filter(Boolean);
|
|
2088
2675
|
}
|
|
2089
2676
|
};
|
|
2090
2677
|
var CliChmodCommandProcessor = class {
|
|
@@ -2105,8 +2692,30 @@ var CliChmodCommandProcessor = class {
|
|
|
2105
2692
|
}
|
|
2106
2693
|
];
|
|
2107
2694
|
}
|
|
2695
|
+
writeDescription(context) {
|
|
2696
|
+
const t = context.translator;
|
|
2697
|
+
writeWrapped(
|
|
2698
|
+
context,
|
|
2699
|
+
t.t("cli.chmod.help.change_who_can_read", "Change who can read (r), write (w) and execute (x) a file. Use an octal mode such as 755 or a symbolic one: who (u user, g group, o others, a all), then + to add, - to remove or = to set, then the permissions.")
|
|
2700
|
+
);
|
|
2701
|
+
context.writer.writeln();
|
|
2702
|
+
writeUsage(context, [
|
|
2703
|
+
["chmod <mode> <file...>", t.t("cli.chmod.help.set_permissions", "Set permissions")],
|
|
2704
|
+
[
|
|
2705
|
+
"chmod -R <mode> <dir>",
|
|
2706
|
+
t.t("cli.chmod.help.apply_directory_everything", "Apply to a directory and everything in it")
|
|
2707
|
+
]
|
|
2708
|
+
]);
|
|
2709
|
+
context.writer.writeln();
|
|
2710
|
+
writeExamples(context, [
|
|
2711
|
+
["chmod 644 notes.txt", t.t("cli.chmod.help.rw_r_r_owner", "rw-r--r--: owner writes, others read")],
|
|
2712
|
+
["chmod u+x script.sh", t.t("cli.chmod.help.let_owner_execute", "Let the owner execute it")],
|
|
2713
|
+
["chmod go-w shared.txt", t.t("cli.chmod.help.take_away_write_group", "Take away write for group and others")],
|
|
2714
|
+
["chmod -R 755 site", t.t("cli.chmod.help.set_whole_directory_tree", "Set a whole directory tree")]
|
|
2715
|
+
]);
|
|
2716
|
+
}
|
|
2108
2717
|
async processCommand(command, context) {
|
|
2109
|
-
const fs = context.services.
|
|
2718
|
+
const fs = context.services.getRequired(
|
|
2110
2719
|
IFileSystemService_TOKEN
|
|
2111
2720
|
);
|
|
2112
2721
|
const { recursive, mode, paths } = this.parseArgs(command);
|
|
@@ -2138,7 +2747,7 @@ var CliChmodCommandProcessor = class {
|
|
|
2138
2747
|
}
|
|
2139
2748
|
parseArgs(command) {
|
|
2140
2749
|
const raw = command.value || "";
|
|
2141
|
-
const tokens = raw
|
|
2750
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2142
2751
|
const recursive = command.args["R"] || false;
|
|
2143
2752
|
const mode = tokens.length > 0 ? tokens[0] : null;
|
|
2144
2753
|
const paths = tokens.slice(1);
|
|
@@ -2225,8 +2834,33 @@ var CliChownCommandProcessor = class {
|
|
|
2225
2834
|
}
|
|
2226
2835
|
];
|
|
2227
2836
|
}
|
|
2837
|
+
writeDescription(context) {
|
|
2838
|
+
const t = context.translator;
|
|
2839
|
+
writeWrapped(
|
|
2840
|
+
context,
|
|
2841
|
+
t.t("cli.chown.help.change_owner_group_files", "Change the owner and/or group of files. Give user, user:group, user: (owner only) or :group (group only).")
|
|
2842
|
+
);
|
|
2843
|
+
context.writer.writeln();
|
|
2844
|
+
writeUsage(context, [
|
|
2845
|
+
[
|
|
2846
|
+
"chown <user>[:<group>] <file...>",
|
|
2847
|
+
t.t("cli.chown.help.change_owner_group_if", "Change owner, and group if given")
|
|
2848
|
+
],
|
|
2849
|
+
["chown :<group> <file>", t.t("cli.chown.help.change_only_group", "Change only the group")],
|
|
2850
|
+
[
|
|
2851
|
+
"chown -R <user> <dir>",
|
|
2852
|
+
t.t("cli.chown.help.apply_directory_everything", "Apply to a directory and everything in it")
|
|
2853
|
+
]
|
|
2854
|
+
]);
|
|
2855
|
+
context.writer.writeln();
|
|
2856
|
+
writeExamples(context, [
|
|
2857
|
+
["chown alice notes.txt", t.t("cli.chown.help.make_alice_owner", "Make alice the owner")],
|
|
2858
|
+
["chown alice:staff report.txt", t.t("cli.chown.help.set_owner_group", "Set owner and group")],
|
|
2859
|
+
["chown -R bob ~/shared", t.t("cli.chown.help.give_bob_whole_directory", "Give bob a whole directory")]
|
|
2860
|
+
]);
|
|
2861
|
+
}
|
|
2228
2862
|
async processCommand(command, context) {
|
|
2229
|
-
const fs = context.services.
|
|
2863
|
+
const fs = context.services.getRequired(
|
|
2230
2864
|
IFileSystemService_TOKEN
|
|
2231
2865
|
);
|
|
2232
2866
|
const { recursive, ownerSpec, paths } = this.parseArgs(command);
|
|
@@ -2258,7 +2892,7 @@ var CliChownCommandProcessor = class {
|
|
|
2258
2892
|
}
|
|
2259
2893
|
parseArgs(command) {
|
|
2260
2894
|
const raw = command.value || "";
|
|
2261
|
-
const tokens = raw
|
|
2895
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2262
2896
|
const recursive = command.args["R"] || false;
|
|
2263
2897
|
const ownerSpec = tokens.length > 0 ? tokens[0] : null;
|
|
2264
2898
|
const paths = tokens.slice(1);
|
|
@@ -2296,7 +2930,6 @@ var CliDuCommandProcessor = class {
|
|
|
2296
2930
|
this.author = DefaultLibraryAuthor;
|
|
2297
2931
|
this.version = LIBRARY_VERSION;
|
|
2298
2932
|
this.acceptsRawInput = true;
|
|
2299
|
-
this.valueRequired = true;
|
|
2300
2933
|
this.metadata = { icon: "\u{1F4CF}", module: "file management" };
|
|
2301
2934
|
this.parameters = [
|
|
2302
2935
|
{
|
|
@@ -2320,17 +2953,39 @@ var CliDuCommandProcessor = class {
|
|
|
2320
2953
|
}
|
|
2321
2954
|
];
|
|
2322
2955
|
}
|
|
2956
|
+
writeDescription(context) {
|
|
2957
|
+
const t = context.translator;
|
|
2958
|
+
writeWrapped(
|
|
2959
|
+
context,
|
|
2960
|
+
t.t("cli.du.help.show_how_much_space", "Show how much space files and directories use, in bytes. For a directory, every subdirectory is listed with its total size; use -s for just the total or -d to limit how deep it goes.")
|
|
2961
|
+
);
|
|
2962
|
+
context.writer.writeln();
|
|
2963
|
+
writeUsage(context, [
|
|
2964
|
+
["du [path...]", t.t("cli.du.help.sizes_path_default_current", "Sizes of a path (default: the current directory) and its subdirectories")],
|
|
2965
|
+
["du -s <path>", t.t("cli.du.help.only_total", "Only the total")],
|
|
2966
|
+
["du -d <n> <path>", t.t("cli.du.help.only_levels_deep", "Only <n> levels deep")]
|
|
2967
|
+
]);
|
|
2968
|
+
context.writer.writeln();
|
|
2969
|
+
writeExamples(context, [
|
|
2970
|
+
["du ~/docs", t.t("cli.du.help.size_each_directory_docs", "Size of each directory in ~/docs")],
|
|
2971
|
+
["du -h -s ~", t.t("cli.du.help.total_home_k_m", "Total of your home in K/M/G")],
|
|
2972
|
+
["du -h -d 1 .", t.t("cli.du.help.size_each_top_level", "Size of each top-level folder")]
|
|
2973
|
+
]);
|
|
2974
|
+
}
|
|
2323
2975
|
async processCommand(command, context) {
|
|
2324
|
-
const fs = context.services.
|
|
2976
|
+
const fs = context.services.getRequired(
|
|
2325
2977
|
IFileSystemService_TOKEN
|
|
2326
2978
|
);
|
|
2327
2979
|
const { humanReadable, summaryOnly, maxDepth, paths } = this.parseArgs(command);
|
|
2328
2980
|
if (paths.length === 0) {
|
|
2329
|
-
|
|
2330
|
-
return;
|
|
2981
|
+
paths.push(fs.getCurrentDirectory());
|
|
2331
2982
|
}
|
|
2332
2983
|
for (const path of paths) {
|
|
2333
2984
|
const resolved = fs.resolvePath(path);
|
|
2985
|
+
if (fs.canTraverse && !fs.canTraverse(resolved)) {
|
|
2986
|
+
context.writer.writeError(`du: cannot access '${path}': Permission denied`);
|
|
2987
|
+
continue;
|
|
2988
|
+
}
|
|
2334
2989
|
const node = fs.getNode(resolved);
|
|
2335
2990
|
if (!node) {
|
|
2336
2991
|
context.writer.writeError(
|
|
@@ -2344,6 +2999,7 @@ var CliDuCommandProcessor = class {
|
|
|
2344
2999
|
);
|
|
2345
3000
|
} else {
|
|
2346
3001
|
const entries = [];
|
|
3002
|
+
this.fs = fs;
|
|
2347
3003
|
this.collectSizes(node, resolved, 0, maxDepth, summaryOnly, entries);
|
|
2348
3004
|
for (const entry of entries) {
|
|
2349
3005
|
context.writer.writeln(
|
|
@@ -2355,10 +3011,12 @@ var CliDuCommandProcessor = class {
|
|
|
2355
3011
|
}
|
|
2356
3012
|
parseArgs(command) {
|
|
2357
3013
|
const raw = command.value || "";
|
|
2358
|
-
const paths = raw
|
|
3014
|
+
const paths = splitArgs(raw).filter(Boolean);
|
|
2359
3015
|
const humanReadable = !!command.args["h"];
|
|
2360
3016
|
const summaryOnly = !!command.args["s"];
|
|
2361
|
-
const
|
|
3017
|
+
const depthArg = command.args["max-depth"] ?? command.args["d"];
|
|
3018
|
+
const parsedDepth = parseInt(String(depthArg ?? ""), 10);
|
|
3019
|
+
const maxDepth = depthArg != null && depthArg !== true && !isNaN(parsedDepth) ? parsedDepth : Infinity;
|
|
2362
3020
|
return { humanReadable, summaryOnly, maxDepth, paths };
|
|
2363
3021
|
}
|
|
2364
3022
|
collectSizes(node, path, depth, maxDepth, summaryOnly, entries) {
|
|
@@ -2366,8 +3024,9 @@ var CliDuCommandProcessor = class {
|
|
|
2366
3024
|
return node.size;
|
|
2367
3025
|
}
|
|
2368
3026
|
let total = 0;
|
|
2369
|
-
|
|
2370
|
-
|
|
3027
|
+
const children = this.fs ? readableChildren(this.fs, path) : node.children;
|
|
3028
|
+
if (children) {
|
|
3029
|
+
for (const child of children) {
|
|
2371
3030
|
const childPath = path === "/" ? `/${child.name}` : `${path}/${child.name}`;
|
|
2372
3031
|
total += this.collectSizes(
|
|
2373
3032
|
child,
|
|
@@ -2414,11 +3073,34 @@ var CliLnCommandProcessor = class {
|
|
|
2414
3073
|
description: "Create a symbolic link",
|
|
2415
3074
|
required: false,
|
|
2416
3075
|
type: "boolean"
|
|
3076
|
+
},
|
|
3077
|
+
{
|
|
3078
|
+
name: "force",
|
|
3079
|
+
aliases: ["f"],
|
|
3080
|
+
description: "Replace an existing file at the link path",
|
|
3081
|
+
required: false,
|
|
3082
|
+
type: "boolean"
|
|
2417
3083
|
}
|
|
2418
3084
|
];
|
|
2419
3085
|
}
|
|
3086
|
+
writeDescription(context) {
|
|
3087
|
+
const t = context.translator;
|
|
3088
|
+
writeWrapped(
|
|
3089
|
+
context,
|
|
3090
|
+
t.t("cli.ln.help.create_symbolic_link_new", "Create a symbolic link: a new path that points to another file or directory. Only symbolic links (-s) are supported, and the target does not need to exist yet.")
|
|
3091
|
+
);
|
|
3092
|
+
context.writer.writeln();
|
|
3093
|
+
writeUsage(context, [
|
|
3094
|
+
["ln -s <target> <link>", t.t("cli.ln.help.create_pointing", "Create <link> pointing at <target>")]
|
|
3095
|
+
]);
|
|
3096
|
+
context.writer.writeln();
|
|
3097
|
+
writeExamples(context, [
|
|
3098
|
+
["ln -s ~/docs/notes.txt notes", t.t("cli.ln.help.shortcut_file", "Shortcut to a file")],
|
|
3099
|
+
["ln -s /var/log logs", t.t("cli.ln.help.shortcut_directory", "Shortcut to a directory")]
|
|
3100
|
+
]);
|
|
3101
|
+
}
|
|
2420
3102
|
async processCommand(command, context) {
|
|
2421
|
-
const fs = context.services.
|
|
3103
|
+
const fs = context.services.getRequired(
|
|
2422
3104
|
IFileSystemService_TOKEN
|
|
2423
3105
|
);
|
|
2424
3106
|
const { symbolic, target, linkName } = this.parseArgs(command);
|
|
@@ -2434,6 +3116,10 @@ var CliLnCommandProcessor = class {
|
|
|
2434
3116
|
}
|
|
2435
3117
|
const resolvedLink = fs.resolvePath(linkName);
|
|
2436
3118
|
const resolvedTarget = fs.resolvePath(target);
|
|
3119
|
+
const force = !!(command.args["force"] || command.args["f"]);
|
|
3120
|
+
if (force && fs.exists(resolvedLink) && !fs.isDirectory(resolvedLink)) {
|
|
3121
|
+
fs.remove(resolvedLink);
|
|
3122
|
+
}
|
|
2437
3123
|
if (fs.exists(resolvedLink)) {
|
|
2438
3124
|
context.writer.writeError(
|
|
2439
3125
|
`ln: failed to create symbolic link '${linkName}': File exists`
|
|
@@ -2450,7 +3136,7 @@ var CliLnCommandProcessor = class {
|
|
|
2450
3136
|
}
|
|
2451
3137
|
parseArgs(command) {
|
|
2452
3138
|
const raw = command.value || "";
|
|
2453
|
-
const tokens = raw
|
|
3139
|
+
const tokens = splitArgs(raw).filter(Boolean);
|
|
2454
3140
|
const symbolic = command.args["s"] || false;
|
|
2455
3141
|
return {
|
|
2456
3142
|
symbolic,
|
|
@@ -2485,25 +3171,75 @@ var CliSedCommandProcessor = class {
|
|
|
2485
3171
|
{
|
|
2486
3172
|
name: "expression",
|
|
2487
3173
|
aliases: ["e"],
|
|
2488
|
-
description: "Add the expression to the commands to be executed",
|
|
3174
|
+
description: "Add the expression to the commands to be executed (repeatable)",
|
|
2489
3175
|
required: false,
|
|
2490
|
-
type: "
|
|
3176
|
+
type: "array"
|
|
3177
|
+
},
|
|
3178
|
+
{
|
|
3179
|
+
name: "regexp-extended",
|
|
3180
|
+
aliases: ["E", "r"],
|
|
3181
|
+
description: "Use extended regular expressions: (, ), +, ?, | and { } without backslashes",
|
|
3182
|
+
required: false,
|
|
3183
|
+
type: "boolean"
|
|
2491
3184
|
}
|
|
2492
3185
|
];
|
|
2493
3186
|
}
|
|
3187
|
+
writeDescription(context) {
|
|
3188
|
+
const t = context.translator;
|
|
3189
|
+
writeWrapped(
|
|
3190
|
+
context,
|
|
3191
|
+
t.t("cli.sed.help.edit_text_line_line", "Edit text line by line with a script: substitute (s/old/new/), delete (d), print (p) or quit (q), optionally limited to a line number, a range (1,5, 3,$ or /start/,/end/) or lines matching /regex/; ! after an address inverts it and ; separates commands. The replacement can use & (the match) and \\1 to \\9 (groups, written \\(...\\) or, with -E, (...)). Reads files or piped input and prints the result, or rewrites the files with -i.")
|
|
3192
|
+
);
|
|
3193
|
+
context.writer.writeln();
|
|
3194
|
+
writeUsage(context, [
|
|
3195
|
+
["sed '<script>' <file>", t.t("cli.sed.help.print_edited_file", "Print the edited file")],
|
|
3196
|
+
["sed -i '<script>' <file>", t.t("cli.sed.help.save_edits_file", "Save the edits into the file")],
|
|
3197
|
+
["sed -n '<script>p' <file>", t.t("cli.sed.help.print_only_selected_lines", "Print only selected lines")],
|
|
3198
|
+
["<command> | sed '<script>'", t.t("cli.sed.help.edit_piped_input", "Edit piped input")]
|
|
3199
|
+
]);
|
|
3200
|
+
context.writer.writeln();
|
|
3201
|
+
writeExamples(context, [
|
|
3202
|
+
["sed 's/cat/dog/g' pets.txt", t.t("cli.sed.help.replace_every_cat_dog", "Replace every cat with dog")],
|
|
3203
|
+
["sed -i 's/v1/v2/' config.txt", t.t("cli.sed.help.edit_file_place", "Edit the file in place")],
|
|
3204
|
+
["sed '/^#/d' config.txt", t.t("cli.sed.help.drop_comment_lines", "Drop comment lines")],
|
|
3205
|
+
["sed -n '1,5p' notes.txt", t.t("cli.sed.help.print_lines_1_5", "Print lines 1 to 5")],
|
|
3206
|
+
["sed -E 's/(\\w+) (\\w+)/\\2 \\1/' names.txt", t.t("cli.sed.help.swap_two_words", "Swap two words")]
|
|
3207
|
+
]);
|
|
3208
|
+
}
|
|
2494
3209
|
async processCommand(command, context) {
|
|
2495
|
-
const fs = context.services.
|
|
3210
|
+
const fs = context.services.getRequired(
|
|
2496
3211
|
IFileSystemService_TOKEN
|
|
2497
3212
|
);
|
|
2498
|
-
const { inPlace, suppress, expressions,
|
|
3213
|
+
const { inPlace, suppress, extended, expressions, filePaths } = this.parseRawCommand(command);
|
|
2499
3214
|
if (expressions.length === 0) {
|
|
2500
3215
|
context.writer.writeError(
|
|
2501
3216
|
"sed: missing expression. Usage: sed [options] 'expression' <file>"
|
|
2502
3217
|
);
|
|
2503
3218
|
return;
|
|
2504
3219
|
}
|
|
2505
|
-
|
|
2506
|
-
|
|
3220
|
+
const parse = () => {
|
|
3221
|
+
try {
|
|
3222
|
+
return expressions.flatMap((e) => this.parseScript(e, extended));
|
|
3223
|
+
} catch (e) {
|
|
3224
|
+
context.writer.writeError(`sed: ${e.message}`);
|
|
3225
|
+
return null;
|
|
3226
|
+
}
|
|
3227
|
+
};
|
|
3228
|
+
if (filePaths.length === 0) {
|
|
3229
|
+
if (command.data == null) {
|
|
3230
|
+
context.writer.writeError(
|
|
3231
|
+
"sed: missing file operand. Usage: sed [options] 'expression' <file>"
|
|
3232
|
+
);
|
|
3233
|
+
return;
|
|
3234
|
+
}
|
|
3235
|
+
const script2 = parse();
|
|
3236
|
+
if (!script2) return;
|
|
3237
|
+
const content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
3238
|
+
this.writeLines(context, this.run(script2, splitLines(content), suppress));
|
|
3239
|
+
return;
|
|
3240
|
+
}
|
|
3241
|
+
const contents = [];
|
|
3242
|
+
for (const filePath of filePaths) {
|
|
2507
3243
|
const resolved = fs.resolvePath(filePath);
|
|
2508
3244
|
if (!fs.exists(resolved)) {
|
|
2509
3245
|
context.writer.writeError(
|
|
@@ -2515,116 +3251,125 @@ var CliSedCommandProcessor = class {
|
|
|
2515
3251
|
context.writer.writeError(`sed: ${filePath}: Is a directory`);
|
|
2516
3252
|
return;
|
|
2517
3253
|
}
|
|
2518
|
-
content = fs.readFile(resolved) ?? "";
|
|
2519
|
-
} else if (command.data != null) {
|
|
2520
|
-
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
2521
|
-
} else {
|
|
2522
|
-
context.writer.writeError(
|
|
2523
|
-
"sed: missing file operand. Usage: sed [options] 'expression' <file>"
|
|
2524
|
-
);
|
|
2525
|
-
return;
|
|
2526
|
-
}
|
|
2527
|
-
const parsedExpressions = [];
|
|
2528
|
-
for (const expr of expressions) {
|
|
2529
3254
|
try {
|
|
2530
|
-
|
|
2531
|
-
parsedExpressions.push(parsed);
|
|
3255
|
+
contents.push(fs.readFile(resolved) ?? "");
|
|
2532
3256
|
} catch (e) {
|
|
2533
3257
|
context.writer.writeError(`sed: ${e.message}`);
|
|
2534
3258
|
return;
|
|
2535
3259
|
}
|
|
2536
3260
|
}
|
|
2537
|
-
|
|
2538
|
-
|
|
3261
|
+
if (inPlace) {
|
|
3262
|
+
for (let i = 0; i < filePaths.length; i++) {
|
|
3263
|
+
const script2 = parse();
|
|
3264
|
+
if (!script2) return;
|
|
3265
|
+
const output = this.run(script2, splitLines(contents[i]), suppress);
|
|
3266
|
+
const keepNewline = contents[i].endsWith("\n") && output.length > 0;
|
|
3267
|
+
try {
|
|
3268
|
+
fs.writeFile(
|
|
3269
|
+
fs.resolvePath(filePaths[i]),
|
|
3270
|
+
output.join("\n") + (keepNewline ? "\n" : "")
|
|
3271
|
+
);
|
|
3272
|
+
} catch (e) {
|
|
3273
|
+
context.writer.writeError(`sed: ${e.message}`);
|
|
3274
|
+
return;
|
|
3275
|
+
}
|
|
3276
|
+
}
|
|
3277
|
+
await fs.persist();
|
|
3278
|
+
return;
|
|
3279
|
+
}
|
|
3280
|
+
const script = parse();
|
|
3281
|
+
if (!script) return;
|
|
3282
|
+
const lines = contents.flatMap((c) => splitLines(c));
|
|
3283
|
+
this.writeLines(context, this.run(script, lines, suppress));
|
|
3284
|
+
}
|
|
3285
|
+
writeLines(context, lines) {
|
|
3286
|
+
for (const line of lines) {
|
|
3287
|
+
context.writer.writeln(line);
|
|
3288
|
+
}
|
|
3289
|
+
}
|
|
3290
|
+
/** Runs the script over the lines and returns the output lines. */
|
|
3291
|
+
run(script, lines, suppress) {
|
|
3292
|
+
const output = [];
|
|
2539
3293
|
for (let lineIdx = 0; lineIdx < lines.length; lineIdx++) {
|
|
2540
3294
|
let line = lines[lineIdx];
|
|
2541
3295
|
let deleted = false;
|
|
2542
|
-
let
|
|
2543
|
-
for (const expr of
|
|
2544
|
-
if (
|
|
2545
|
-
if (!this.addressMatches(
|
|
2546
|
-
expr.address,
|
|
2547
|
-
lineIdx + 1,
|
|
2548
|
-
lines.length,
|
|
2549
|
-
line
|
|
2550
|
-
)) {
|
|
3296
|
+
let quit = false;
|
|
3297
|
+
for (const expr of script) {
|
|
3298
|
+
if (!this.addressMatches(expr, lineIdx + 1, lines.length, line)) {
|
|
2551
3299
|
continue;
|
|
2552
3300
|
}
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
if (matched && expr.printFlag) {
|
|
2564
|
-
printed = true;
|
|
3301
|
+
if (expr.command === "s") {
|
|
3302
|
+
let replaced = false;
|
|
3303
|
+
let seen = 0;
|
|
3304
|
+
const pattern = expr.pattern;
|
|
3305
|
+
pattern.lastIndex = 0;
|
|
3306
|
+
line = line.replace(pattern, (...args) => {
|
|
3307
|
+
seen++;
|
|
3308
|
+
const nth = expr.occurrence;
|
|
3309
|
+
if (nth && (expr.globalFlag ? seen < nth : seen !== nth)) {
|
|
3310
|
+
return args[0];
|
|
2565
3311
|
}
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
|
|
2572
|
-
|
|
2573
|
-
|
|
3312
|
+
replaced = true;
|
|
3313
|
+
const groups = args.slice(0, expr.groupCount + 1);
|
|
3314
|
+
return this.expandReplacement(expr.replacement, groups);
|
|
3315
|
+
});
|
|
3316
|
+
if (replaced && expr.printFlag) output.push(line);
|
|
3317
|
+
} else if (expr.command === "p") {
|
|
3318
|
+
output.push(line);
|
|
3319
|
+
} else if (expr.command === "d") {
|
|
3320
|
+
deleted = true;
|
|
3321
|
+
break;
|
|
3322
|
+
} else if (expr.command === "q") {
|
|
3323
|
+
quit = true;
|
|
3324
|
+
break;
|
|
2574
3325
|
}
|
|
2575
3326
|
}
|
|
2576
|
-
if (!deleted)
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
3327
|
+
if (!deleted && !suppress) output.push(line);
|
|
3328
|
+
if (quit) break;
|
|
3329
|
+
}
|
|
3330
|
+
return output;
|
|
3331
|
+
}
|
|
3332
|
+
/** `&` is the match, `\1`..`\9` the groups, `\n` a newline, `\x` a literal x. */
|
|
3333
|
+
expandReplacement(replacement, groups) {
|
|
3334
|
+
let out = "";
|
|
3335
|
+
for (let i = 0; i < replacement.length; i++) {
|
|
3336
|
+
const ch = replacement[i];
|
|
3337
|
+
if (ch === "\\" && i + 1 < replacement.length) {
|
|
3338
|
+
const next = replacement[++i];
|
|
3339
|
+
if (next >= "0" && next <= "9") {
|
|
3340
|
+
out += groups[Number(next)] ?? "";
|
|
3341
|
+
} else if (next === "n") {
|
|
3342
|
+
out += "\n";
|
|
3343
|
+
} else if (next === "t") {
|
|
3344
|
+
out += " ";
|
|
2581
3345
|
} else {
|
|
2582
|
-
|
|
3346
|
+
out += next;
|
|
2583
3347
|
}
|
|
3348
|
+
} else if (ch === "&") {
|
|
3349
|
+
out += groups[0];
|
|
3350
|
+
} else {
|
|
3351
|
+
out += ch;
|
|
2584
3352
|
}
|
|
2585
3353
|
}
|
|
2586
|
-
|
|
2587
|
-
if (inPlace && filePath) {
|
|
2588
|
-
const resolved = fs.resolvePath(filePath);
|
|
2589
|
-
fs.writeFile(resolved, result);
|
|
2590
|
-
await fs.persist();
|
|
2591
|
-
} else {
|
|
2592
|
-
const resultLines = result.split("\n");
|
|
2593
|
-
for (const line of resultLines) {
|
|
2594
|
-
context.writer.writeln(line);
|
|
2595
|
-
}
|
|
2596
|
-
}
|
|
3354
|
+
return out;
|
|
2597
3355
|
}
|
|
2598
3356
|
parseRawCommand(command) {
|
|
2599
3357
|
const raw = command.value || "";
|
|
2600
3358
|
const tokens = this.tokenize(raw);
|
|
2601
3359
|
const inPlace = !!command.args["in-place"] || !!command.args["i"];
|
|
2602
3360
|
const suppress = !!command.args["quiet"] || !!command.args["n"];
|
|
3361
|
+
const extended = !!command.args["regexp-extended"] || !!command.args["E"] || !!command.args["r"];
|
|
2603
3362
|
const expressions = [];
|
|
2604
|
-
|
|
2605
|
-
const
|
|
2606
|
-
|
|
2607
|
-
|
|
2608
|
-
if (Array.isArray(eValue)) {
|
|
2609
|
-
for (const v of eValue) {
|
|
2610
|
-
if (v && typeof v === "string") {
|
|
2611
|
-
expressions.push(v);
|
|
2612
|
-
}
|
|
2613
|
-
}
|
|
2614
|
-
} else if (eValue && typeof eValue === "string") {
|
|
2615
|
-
expressions.push(eValue);
|
|
3363
|
+
const eValue = command.args["expression"] ?? command.args["e"];
|
|
3364
|
+
for (const v of Array.isArray(eValue) ? eValue : [eValue]) {
|
|
3365
|
+
if (v != null && v !== true && v !== "") {
|
|
3366
|
+
expressions.push(String(v));
|
|
2616
3367
|
}
|
|
2617
3368
|
}
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
}
|
|
2621
|
-
if (expressions.length === 0 && nonFlagTokens.length > 0) {
|
|
2622
|
-
expressions.push(nonFlagTokens.shift());
|
|
3369
|
+
if (expressions.length === 0 && tokens.length > 0) {
|
|
3370
|
+
expressions.push(tokens.shift());
|
|
2623
3371
|
}
|
|
2624
|
-
|
|
2625
|
-
filePath = nonFlagTokens[nonFlagTokens.length - 1];
|
|
2626
|
-
}
|
|
2627
|
-
return { inPlace, suppress, expressions, filePath };
|
|
3372
|
+
return { inPlace, suppress, extended, expressions, filePaths: tokens };
|
|
2628
3373
|
}
|
|
2629
3374
|
tokenize(raw) {
|
|
2630
3375
|
const tokens = [];
|
|
@@ -2634,73 +3379,85 @@ var CliSedCommandProcessor = class {
|
|
|
2634
3379
|
i++;
|
|
2635
3380
|
continue;
|
|
2636
3381
|
}
|
|
2637
|
-
if (raw[i] === "'" || raw[i] === '"') {
|
|
2638
|
-
const quote = raw[i];
|
|
2639
|
-
i++;
|
|
2640
|
-
let token2 = "";
|
|
2641
|
-
while (i < raw.length && raw[i] !== quote) {
|
|
2642
|
-
token2 += raw[i];
|
|
2643
|
-
i++;
|
|
2644
|
-
}
|
|
2645
|
-
if (i < raw.length) i++;
|
|
2646
|
-
tokens.push(token2);
|
|
2647
|
-
continue;
|
|
2648
|
-
}
|
|
2649
3382
|
let token = "";
|
|
2650
3383
|
while (i < raw.length && raw[i] !== " " && raw[i] !== " ") {
|
|
2651
|
-
|
|
2652
|
-
|
|
3384
|
+
if (raw[i] === "'" || raw[i] === '"') {
|
|
3385
|
+
const quote = raw[i];
|
|
3386
|
+
i++;
|
|
3387
|
+
while (i < raw.length && raw[i] !== quote) {
|
|
3388
|
+
token += raw[i];
|
|
3389
|
+
i++;
|
|
3390
|
+
}
|
|
3391
|
+
if (i < raw.length) i++;
|
|
3392
|
+
} else {
|
|
3393
|
+
token += raw[i];
|
|
3394
|
+
i++;
|
|
3395
|
+
}
|
|
2653
3396
|
}
|
|
2654
3397
|
tokens.push(token);
|
|
2655
3398
|
}
|
|
2656
|
-
return tokens;
|
|
3399
|
+
return tokens;
|
|
3400
|
+
}
|
|
3401
|
+
/** Commands of a script, separated by `;` or newlines. */
|
|
3402
|
+
parseScript(script, extended) {
|
|
3403
|
+
const result = [];
|
|
3404
|
+
let pos = 0;
|
|
3405
|
+
const skipSeparators = () => {
|
|
3406
|
+
while (pos < script.length && /[\s;]/.test(script[pos])) pos++;
|
|
3407
|
+
};
|
|
3408
|
+
skipSeparators();
|
|
3409
|
+
while (pos < script.length) {
|
|
3410
|
+
const parsed = this.parseCommand(script, pos, extended);
|
|
3411
|
+
result.push(parsed.expression);
|
|
3412
|
+
pos = parsed.pos;
|
|
3413
|
+
while (pos < script.length && /[ \t]/.test(script[pos])) pos++;
|
|
3414
|
+
if (pos < script.length && !/[;\n]/.test(script[pos])) {
|
|
3415
|
+
throw new Error(`extra characters after command: '${script.slice(pos)}'`);
|
|
3416
|
+
}
|
|
3417
|
+
skipSeparators();
|
|
3418
|
+
}
|
|
3419
|
+
if (result.length === 0) {
|
|
3420
|
+
throw new Error(`invalid expression: '${script}'`);
|
|
3421
|
+
}
|
|
3422
|
+
return result;
|
|
2657
3423
|
}
|
|
2658
|
-
|
|
2659
|
-
let
|
|
2660
|
-
let
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
if (endResult) {
|
|
2670
|
-
if (endResult.address.type === "last") {
|
|
2671
|
-
address = {
|
|
2672
|
-
type: "range",
|
|
2673
|
-
startLine: address.line,
|
|
2674
|
-
endLine: 0,
|
|
2675
|
-
endIsLast: true
|
|
2676
|
-
};
|
|
2677
|
-
} else {
|
|
2678
|
-
address = {
|
|
2679
|
-
type: "range",
|
|
2680
|
-
startLine: address.line,
|
|
2681
|
-
endLine: endResult.address.line
|
|
2682
|
-
};
|
|
2683
|
-
}
|
|
2684
|
-
pos = endResult.pos;
|
|
2685
|
-
}
|
|
3424
|
+
parseCommand(expr, pos, extended) {
|
|
3425
|
+
let start = null;
|
|
3426
|
+
let end = null;
|
|
3427
|
+
const first = this.parseAddress(expr, pos, extended);
|
|
3428
|
+
if (first) {
|
|
3429
|
+
start = first.address;
|
|
3430
|
+
pos = first.pos;
|
|
3431
|
+
if (expr[pos] === ",") {
|
|
3432
|
+
const second = this.parseAddress(expr, pos + 1, extended);
|
|
3433
|
+
if (!second) {
|
|
3434
|
+
throw new Error(`unexpected ',' in '${expr}'`);
|
|
2686
3435
|
}
|
|
3436
|
+
end = second.address;
|
|
3437
|
+
pos = second.pos;
|
|
2687
3438
|
}
|
|
2688
3439
|
}
|
|
3440
|
+
while (pos < expr.length && /[ \t]/.test(expr[pos])) pos++;
|
|
3441
|
+
let negate = false;
|
|
3442
|
+
if (expr[pos] === "!") {
|
|
3443
|
+
negate = true;
|
|
3444
|
+
pos++;
|
|
3445
|
+
while (pos < expr.length && /[ \t]/.test(expr[pos])) pos++;
|
|
3446
|
+
}
|
|
2689
3447
|
if (pos >= expr.length) {
|
|
2690
3448
|
throw new Error(`invalid expression: '${expr}'`);
|
|
2691
3449
|
}
|
|
3450
|
+
const base = { start, end, negate, active: false };
|
|
2692
3451
|
const cmd = expr[pos];
|
|
2693
3452
|
if (cmd === "s") {
|
|
2694
|
-
return this.parseSubstitution(expr, pos,
|
|
2695
|
-
}
|
|
2696
|
-
|
|
2697
|
-
|
|
2698
|
-
return { address, command: "p" };
|
|
2699
|
-
} else {
|
|
2700
|
-
throw new Error(`unknown command: '${cmd}'`);
|
|
3453
|
+
return this.parseSubstitution(expr, pos, base, extended);
|
|
3454
|
+
}
|
|
3455
|
+
if (cmd === "d" || cmd === "p" || cmd === "q") {
|
|
3456
|
+
return { expression: { ...base, command: cmd }, pos: pos + 1 };
|
|
2701
3457
|
}
|
|
3458
|
+
throw new Error(`unknown command: '${cmd}'`);
|
|
2702
3459
|
}
|
|
2703
|
-
parseAddress(expr, pos) {
|
|
3460
|
+
parseAddress(expr, pos, extended) {
|
|
2704
3461
|
if (pos >= expr.length) return null;
|
|
2705
3462
|
if (expr[pos] >= "0" && expr[pos] <= "9") {
|
|
2706
3463
|
let numStr = "";
|
|
@@ -2717,51 +3474,73 @@ var CliSedCommandProcessor = class {
|
|
|
2717
3474
|
return { address: { type: "last" }, pos: pos + 1 };
|
|
2718
3475
|
}
|
|
2719
3476
|
if (expr[pos] === "/") {
|
|
2720
|
-
pos
|
|
2721
|
-
let pattern = "";
|
|
2722
|
-
while (pos < expr.length && expr[pos] !== "/") {
|
|
2723
|
-
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2724
|
-
pattern += expr[pos + 1];
|
|
2725
|
-
pos += 2;
|
|
2726
|
-
} else {
|
|
2727
|
-
pattern += expr[pos];
|
|
2728
|
-
pos++;
|
|
2729
|
-
}
|
|
2730
|
-
}
|
|
2731
|
-
if (pos < expr.length) pos++;
|
|
3477
|
+
const parsed = this.readDelimited(expr, pos + 1, "/");
|
|
2732
3478
|
try {
|
|
2733
3479
|
return {
|
|
2734
|
-
address: {
|
|
2735
|
-
|
|
3480
|
+
address: {
|
|
3481
|
+
type: "regex",
|
|
3482
|
+
regex: new RegExp(this.toJsRegex(parsed.text, extended))
|
|
3483
|
+
},
|
|
3484
|
+
pos: parsed.pos
|
|
2736
3485
|
};
|
|
2737
3486
|
} catch {
|
|
2738
|
-
throw new Error(`invalid regex in address: '${
|
|
3487
|
+
throw new Error(`invalid regex in address: '${parsed.text}'`);
|
|
2739
3488
|
}
|
|
2740
3489
|
}
|
|
2741
3490
|
return null;
|
|
2742
3491
|
}
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
pos++;
|
|
2750
|
-
let pattern = "";
|
|
3492
|
+
/**
|
|
3493
|
+
* Text up to the next unescaped delimiter, escapes kept (an escaped
|
|
3494
|
+
* delimiter becomes the plain character when it has no regex meaning).
|
|
3495
|
+
*/
|
|
3496
|
+
readDelimited(expr, pos, delimiter) {
|
|
3497
|
+
let text = "";
|
|
2751
3498
|
while (pos < expr.length && expr[pos] !== delimiter) {
|
|
2752
3499
|
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2753
|
-
|
|
3500
|
+
const next = expr[pos + 1];
|
|
3501
|
+
text += next === delimiter && /\w|\//.test(next) ? next : "\\" + next;
|
|
2754
3502
|
pos += 2;
|
|
2755
3503
|
} else {
|
|
2756
|
-
|
|
3504
|
+
text += expr[pos];
|
|
2757
3505
|
pos++;
|
|
2758
3506
|
}
|
|
2759
3507
|
}
|
|
2760
3508
|
if (pos < expr.length) pos++;
|
|
3509
|
+
return { text, pos };
|
|
3510
|
+
}
|
|
3511
|
+
/**
|
|
3512
|
+
* Basic regular expressions use \( \) \{ \} \+ \? \| for groups and
|
|
3513
|
+
* operators. When a pattern uses them (or never in -E mode), they are
|
|
3514
|
+
* turned into the JavaScript form and the bare characters made literal.
|
|
3515
|
+
*/
|
|
3516
|
+
toJsRegex(pattern, extended) {
|
|
3517
|
+
if (extended || !/\\[(){}+?|]/.test(pattern)) return pattern;
|
|
3518
|
+
let out = "";
|
|
3519
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
3520
|
+
const ch = pattern[i];
|
|
3521
|
+
if (ch === "\\" && i + 1 < pattern.length) {
|
|
3522
|
+
const next = pattern[++i];
|
|
3523
|
+
out += "(){}+?|".includes(next) ? next : "\\" + next;
|
|
3524
|
+
} else if ("(){}+?|".includes(ch)) {
|
|
3525
|
+
out += "\\" + ch;
|
|
3526
|
+
} else {
|
|
3527
|
+
out += ch;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
return out;
|
|
3531
|
+
}
|
|
3532
|
+
parseSubstitution(expr, pos, base, extended) {
|
|
3533
|
+
pos++;
|
|
3534
|
+
if (pos >= expr.length) {
|
|
3535
|
+
throw new Error(`invalid substitution expression: '${expr}'`);
|
|
3536
|
+
}
|
|
3537
|
+
const delimiter = expr[pos];
|
|
3538
|
+
const pattern = this.readDelimited(expr, pos + 1, delimiter);
|
|
3539
|
+
pos = pattern.pos;
|
|
2761
3540
|
let replacement = "";
|
|
2762
3541
|
while (pos < expr.length && expr[pos] !== delimiter) {
|
|
2763
3542
|
if (expr[pos] === "\\" && pos + 1 < expr.length) {
|
|
2764
|
-
replacement += expr[pos + 1];
|
|
3543
|
+
replacement += expr[pos + 1] === delimiter ? delimiter : "\\" + expr[pos + 1];
|
|
2765
3544
|
pos += 2;
|
|
2766
3545
|
} else {
|
|
2767
3546
|
replacement += expr[pos];
|
|
@@ -2772,47 +3551,75 @@ var CliSedCommandProcessor = class {
|
|
|
2772
3551
|
let globalFlag = false;
|
|
2773
3552
|
let caseInsensitive = false;
|
|
2774
3553
|
let printFlag = false;
|
|
2775
|
-
|
|
3554
|
+
let occurrence = "";
|
|
3555
|
+
while (pos < expr.length && !/[;\n\s}]/.test(expr[pos])) {
|
|
2776
3556
|
const f = expr[pos];
|
|
2777
3557
|
if (f === "g") globalFlag = true;
|
|
2778
|
-
else if (f === "i") caseInsensitive = true;
|
|
3558
|
+
else if (f === "i" || f === "I") caseInsensitive = true;
|
|
2779
3559
|
else if (f === "p") printFlag = true;
|
|
3560
|
+
else if (f >= "0" && f <= "9") occurrence += f;
|
|
3561
|
+
else throw new Error(`unknown option to 's': '${f}'`);
|
|
2780
3562
|
pos++;
|
|
2781
3563
|
}
|
|
3564
|
+
const nth = occurrence ? parseInt(occurrence, 10) : void 0;
|
|
3565
|
+
if (nth === 0) {
|
|
3566
|
+
throw new Error("number option to 's' command may not be zero");
|
|
3567
|
+
}
|
|
2782
3568
|
let regexFlags = "";
|
|
2783
3569
|
if (caseInsensitive) regexFlags += "i";
|
|
2784
|
-
if (globalFlag) regexFlags += "g";
|
|
3570
|
+
if (globalFlag || nth) regexFlags += "g";
|
|
2785
3571
|
let regex;
|
|
3572
|
+
let groupCount;
|
|
2786
3573
|
try {
|
|
2787
|
-
|
|
3574
|
+
const source = this.toJsRegex(pattern.text, extended);
|
|
3575
|
+
regex = new RegExp(source, regexFlags);
|
|
3576
|
+
groupCount = (new RegExp(`${source}|`).exec("")?.length ?? 1) - 1;
|
|
2788
3577
|
} catch {
|
|
2789
|
-
throw new Error(`invalid regex: '${pattern}'`);
|
|
3578
|
+
throw new Error(`invalid regex: '${pattern.text}'`);
|
|
2790
3579
|
}
|
|
2791
3580
|
return {
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
3581
|
+
expression: {
|
|
3582
|
+
...base,
|
|
3583
|
+
command: "s",
|
|
3584
|
+
pattern: regex,
|
|
3585
|
+
replacement,
|
|
3586
|
+
groupCount,
|
|
3587
|
+
printFlag,
|
|
3588
|
+
occurrence: nth,
|
|
3589
|
+
globalFlag
|
|
3590
|
+
},
|
|
3591
|
+
pos
|
|
2798
3592
|
};
|
|
2799
3593
|
}
|
|
2800
|
-
addressMatches(
|
|
2801
|
-
|
|
3594
|
+
addressMatches(expr, lineNum, totalLines, lineText) {
|
|
3595
|
+
let matches;
|
|
3596
|
+
if (!expr.start) {
|
|
3597
|
+
matches = true;
|
|
3598
|
+
} else if (!expr.end) {
|
|
3599
|
+
matches = this.singleMatches(expr.start, lineNum, totalLines, lineText);
|
|
3600
|
+
} else if (!expr.active) {
|
|
3601
|
+
matches = this.singleMatches(expr.start, lineNum, totalLines, lineText);
|
|
3602
|
+
if (matches) {
|
|
3603
|
+
const end = expr.end;
|
|
3604
|
+
const endsHere = end.type === "line" && end.line <= lineNum || end.type === "last" && lineNum === totalLines;
|
|
3605
|
+
expr.active = !endsHere;
|
|
3606
|
+
}
|
|
3607
|
+
} else {
|
|
3608
|
+
matches = true;
|
|
3609
|
+
const end = expr.end;
|
|
3610
|
+
const endsHere = end.type === "line" ? lineNum >= end.line : this.singleMatches(end, lineNum, totalLines, lineText);
|
|
3611
|
+
if (endsHere) expr.active = false;
|
|
3612
|
+
}
|
|
3613
|
+
return expr.negate ? !matches : matches;
|
|
3614
|
+
}
|
|
3615
|
+
singleMatches(address, lineNum, totalLines, lineText) {
|
|
2802
3616
|
switch (address.type) {
|
|
2803
3617
|
case "line":
|
|
2804
3618
|
return lineNum === address.line;
|
|
2805
3619
|
case "last":
|
|
2806
3620
|
return lineNum === totalLines;
|
|
2807
|
-
case "range": {
|
|
2808
|
-
const start = address.startLine;
|
|
2809
|
-
const end = address.endIsLast ? totalLines : address.endLine;
|
|
2810
|
-
return lineNum >= start && lineNum <= end;
|
|
2811
|
-
}
|
|
2812
3621
|
case "regex":
|
|
2813
3622
|
return address.regex.test(lineText);
|
|
2814
|
-
default:
|
|
2815
|
-
return true;
|
|
2816
3623
|
}
|
|
2817
3624
|
}
|
|
2818
3625
|
};
|
|
@@ -2834,11 +3641,34 @@ var CliAwkCommandProcessor = class {
|
|
|
2834
3641
|
}
|
|
2835
3642
|
];
|
|
2836
3643
|
}
|
|
3644
|
+
writeDescription(context) {
|
|
3645
|
+
const t = context.translator;
|
|
3646
|
+
writeWrapped(
|
|
3647
|
+
context,
|
|
3648
|
+
t.t("cli.awk.help.process_text_records_fields", "Process text as records and fields: each line is split into fields $1, $2, ... ($0 is the whole line), and each rule runs its { action } on matching lines. Supports BEGIN and END blocks, /regex/ and field comparison patterns, print, variables, NR and NF. Quote the program; a simple one also works unquoted (awk {print $1} file).")
|
|
3649
|
+
);
|
|
3650
|
+
context.writer.writeln();
|
|
3651
|
+
writeUsage(context, [
|
|
3652
|
+
["awk '<program>' <file>", t.t("cli.awk.help.run_program_over_file", "Run a program over a file")],
|
|
3653
|
+
[
|
|
3654
|
+
"awk -F <sep> '<program>' <file>",
|
|
3655
|
+
t.t("cli.awk.help.split_fields_instead_spaces", "Split fields on <sep> instead of spaces")
|
|
3656
|
+
],
|
|
3657
|
+
["<command> | awk '<program>'", t.t("cli.awk.help.run_program_over_piped", "Run a program over piped input")]
|
|
3658
|
+
]);
|
|
3659
|
+
context.writer.writeln();
|
|
3660
|
+
writeExamples(context, [
|
|
3661
|
+
["awk '{print $1}' data.txt", t.t("cli.awk.help.print_first_column", "Print the first column")],
|
|
3662
|
+
["awk -F , '{print $2}' data.csv", t.t("cli.awk.help.second_column_csv", "Second column of a CSV")],
|
|
3663
|
+
["awk '$3 > 100 {print $1}' sales.txt", t.t("cli.awk.help.filter_rows_field", "Filter rows by a field")],
|
|
3664
|
+
["awk '{s += $2} END {print s}' data.txt", t.t("cli.awk.help.sum_second_column", "Sum the second column")]
|
|
3665
|
+
]);
|
|
3666
|
+
}
|
|
2837
3667
|
async processCommand(command, context) {
|
|
2838
|
-
const fs = context.services.
|
|
3668
|
+
const fs = context.services.getRequired(
|
|
2839
3669
|
IFileSystemService_TOKEN
|
|
2840
3670
|
);
|
|
2841
|
-
const { program,
|
|
3671
|
+
const { program, filePaths, fieldSep } = this.parseArgs(command, fs);
|
|
2842
3672
|
if (!program) {
|
|
2843
3673
|
context.writer.writeError(
|
|
2844
3674
|
"awk: missing program. Usage: awk [-F sep] 'program' file"
|
|
@@ -2846,18 +3676,21 @@ var CliAwkCommandProcessor = class {
|
|
|
2846
3676
|
return;
|
|
2847
3677
|
}
|
|
2848
3678
|
let content = "";
|
|
2849
|
-
if (
|
|
2850
|
-
|
|
2851
|
-
|
|
2852
|
-
|
|
2853
|
-
|
|
2854
|
-
|
|
3679
|
+
if (filePaths.length > 0) {
|
|
3680
|
+
for (const filePath of filePaths) {
|
|
3681
|
+
try {
|
|
3682
|
+
if (!fs.exists(filePath)) {
|
|
3683
|
+
context.writer.writeError(
|
|
3684
|
+
`awk: ${filePath}: No such file or directory`
|
|
3685
|
+
);
|
|
3686
|
+
return;
|
|
3687
|
+
}
|
|
3688
|
+
const text = fs.readFile(filePath) ?? "";
|
|
3689
|
+
content += text === "" || text.endsWith("\n") ? text : text + "\n";
|
|
3690
|
+
} catch (e) {
|
|
3691
|
+
context.writer.writeError(`awk: ${e.message}`);
|
|
2855
3692
|
return;
|
|
2856
3693
|
}
|
|
2857
|
-
content = fs.readFile(filePath) ?? "";
|
|
2858
|
-
} catch (e) {
|
|
2859
|
-
context.writer.writeError(`awk: ${e.message}`);
|
|
2860
|
-
return;
|
|
2861
3694
|
}
|
|
2862
3695
|
} else if (command.data != null) {
|
|
2863
3696
|
content = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
@@ -2919,24 +3752,58 @@ var CliAwkCommandProcessor = class {
|
|
|
2919
3752
|
context.writer.writeln(line);
|
|
2920
3753
|
}
|
|
2921
3754
|
}
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
2932
|
-
|
|
2933
|
-
|
|
2934
|
-
|
|
2935
|
-
|
|
3755
|
+
/**
|
|
3756
|
+
* The program is the first operand, usually quoted. Unquoted, it may span
|
|
3757
|
+
* several words (`awk {print $1} file`): it is then everything before the
|
|
3758
|
+
* trailing operands that are existing files, or the whole value with
|
|
3759
|
+
* piped input.
|
|
3760
|
+
*/
|
|
3761
|
+
parseArgs(command, fs) {
|
|
3762
|
+
const raw = (command.value || "").trim();
|
|
3763
|
+
const fieldArg = command.args["F"];
|
|
3764
|
+
const fieldSep = fieldArg != null && fieldArg !== true ? String(fieldArg) : null;
|
|
3765
|
+
const quoted = raw.match(/^'((?:[^'\\]|\\.)*)'(?:\s+|$)/) ?? raw.match(/^"((?:[^"\\]|\\.)*)"(?:\s+|$)/);
|
|
3766
|
+
if (quoted) {
|
|
3767
|
+
return {
|
|
3768
|
+
program: quoted[1],
|
|
3769
|
+
filePaths: splitArgs(raw.slice(quoted[0].length)),
|
|
3770
|
+
fieldSep
|
|
3771
|
+
};
|
|
3772
|
+
}
|
|
3773
|
+
const words = raw.split(/\s+/).filter(Boolean);
|
|
3774
|
+
if (words.length === 0) {
|
|
3775
|
+
return { program: null, filePaths: [], fieldSep };
|
|
3776
|
+
}
|
|
3777
|
+
const isFile = (word) => {
|
|
3778
|
+
try {
|
|
3779
|
+
const path = splitArgs(word).join(" ");
|
|
3780
|
+
return fs.exists(path) && !fs.isDirectory(path);
|
|
3781
|
+
} catch {
|
|
3782
|
+
return false;
|
|
2936
3783
|
}
|
|
3784
|
+
};
|
|
3785
|
+
let split = words.length;
|
|
3786
|
+
while (split > 1 && isFile(words[split - 1])) split--;
|
|
3787
|
+
if (split === words.length && command.data == null && words.length > 1) {
|
|
3788
|
+
const program = words.slice(0, -1).join(" ");
|
|
3789
|
+
const last = words[words.length - 1];
|
|
3790
|
+
if (this.bracesBalanced(program) && !/[{}]/.test(last)) {
|
|
3791
|
+
split = words.length - 1;
|
|
3792
|
+
}
|
|
3793
|
+
}
|
|
3794
|
+
return {
|
|
3795
|
+
program: words.slice(0, split).join(" "),
|
|
3796
|
+
filePaths: words.slice(split).map((w) => splitArgs(w).join(" ")),
|
|
3797
|
+
fieldSep
|
|
3798
|
+
};
|
|
3799
|
+
}
|
|
3800
|
+
bracesBalanced(text) {
|
|
3801
|
+
let depth = 0;
|
|
3802
|
+
for (const ch of text) {
|
|
3803
|
+
if (ch === "{") depth++;
|
|
3804
|
+
else if (ch === "}" && --depth < 0) return false;
|
|
2937
3805
|
}
|
|
2938
|
-
|
|
2939
|
-
return { program, filePath, fieldSep };
|
|
3806
|
+
return depth === 0;
|
|
2940
3807
|
}
|
|
2941
3808
|
parseProgram(program) {
|
|
2942
3809
|
const rules = [];
|
|
@@ -3310,8 +4177,26 @@ var CliDiffCommandProcessor = class {
|
|
|
3310
4177
|
}
|
|
3311
4178
|
];
|
|
3312
4179
|
}
|
|
4180
|
+
writeDescription(context) {
|
|
4181
|
+
const t = context.translator;
|
|
4182
|
+
writeWrapped(
|
|
4183
|
+
context,
|
|
4184
|
+
t.t("cli.diff.help.compare_two_files_line", "Compare two files line by line and show what changed: lines starting with < (or -) are only in the first file, lines starting with > (or +) only in the second.")
|
|
4185
|
+
);
|
|
4186
|
+
context.writer.writeln();
|
|
4187
|
+
writeUsage(context, [
|
|
4188
|
+
["diff <file1> <file2>", t.t("cli.diff.help.show_differences", "Show the differences")],
|
|
4189
|
+
["diff -u <file1> <file2>", t.t("cli.diff.help.unified_format_context", "Unified format with context")]
|
|
4190
|
+
]);
|
|
4191
|
+
context.writer.writeln();
|
|
4192
|
+
writeExamples(context, [
|
|
4193
|
+
["diff old.txt new.txt", t.t("cli.diff.help.what_changed_between_versions", "What changed between versions")],
|
|
4194
|
+
["diff -u config.bak config.txt", t.t("cli.diff.help.review_edits_git_style", "Review edits, git style")],
|
|
4195
|
+
["diff -i a.txt b.txt", t.t("cli.diff.help.ignore_upper_lower_case", "Ignore upper/lower case")]
|
|
4196
|
+
]);
|
|
4197
|
+
}
|
|
3313
4198
|
async processCommand(command, context) {
|
|
3314
|
-
const fs = context.services.
|
|
4199
|
+
const fs = context.services.getRequired(
|
|
3315
4200
|
IFileSystemService_TOKEN
|
|
3316
4201
|
);
|
|
3317
4202
|
const unified = command.args["unified"] || command.args["u"] || false;
|
|
@@ -3474,13 +4359,13 @@ var CliDiffCommandProcessor = class {
|
|
|
3474
4359
|
}
|
|
3475
4360
|
parsePaths(command) {
|
|
3476
4361
|
const raw = command.value || "";
|
|
3477
|
-
return raw
|
|
4362
|
+
return splitArgs(raw).filter(Boolean);
|
|
3478
4363
|
}
|
|
3479
4364
|
};
|
|
3480
4365
|
var CliTeeCommandProcessor = class {
|
|
3481
4366
|
constructor() {
|
|
3482
4367
|
this.command = "tee";
|
|
3483
|
-
this.description = "
|
|
4368
|
+
this.description = "Copy piped input to files and to the output";
|
|
3484
4369
|
this.author = DefaultLibraryAuthor;
|
|
3485
4370
|
this.version = LIBRARY_VERSION;
|
|
3486
4371
|
this.acceptsRawInput = true;
|
|
@@ -3496,72 +4381,56 @@ var CliTeeCommandProcessor = class {
|
|
|
3496
4381
|
}
|
|
3497
4382
|
];
|
|
3498
4383
|
}
|
|
4384
|
+
writeDescription(context) {
|
|
4385
|
+
const t = context.translator;
|
|
4386
|
+
writeWrapped(
|
|
4387
|
+
context,
|
|
4388
|
+
t.t("cli.tee.help.pass_piped_input_through", "Pass piped input through unchanged and save a copy of it to every file named, overwriting them or appending with -a. Like Unix tee, it reads only piped input: to copy a file, pipe it in with cat.")
|
|
4389
|
+
);
|
|
4390
|
+
context.writer.writeln();
|
|
4391
|
+
writeUsage(context, [
|
|
4392
|
+
["<command> | tee <file...>", t.t("cli.tee.help.show_piped_output_save", "Show piped output and save it")],
|
|
4393
|
+
["<command> | tee -a <file>", t.t("cli.tee.help.append_instead_overwriting", "Append instead of overwriting")]
|
|
4394
|
+
]);
|
|
4395
|
+
context.writer.writeln();
|
|
4396
|
+
writeExamples(context, [
|
|
4397
|
+
["cat notes.txt | tee copy.txt", t.t("cli.tee.help.show_save_copy", "Show and save a copy")],
|
|
4398
|
+
["echo done | tee -a log.txt", t.t("cli.tee.help.add_line_log", "Add a line to a log")],
|
|
4399
|
+
[
|
|
4400
|
+
"cat source.txt | tee a.txt b.txt",
|
|
4401
|
+
t.t("cli.tee.help.copy_source_txt_txt", "Copy source.txt into a.txt and b.txt")
|
|
4402
|
+
]
|
|
4403
|
+
]);
|
|
4404
|
+
}
|
|
3499
4405
|
async processCommand(command, context) {
|
|
3500
|
-
const fs = context.services.
|
|
4406
|
+
const fs = context.services.getRequired(
|
|
3501
4407
|
IFileSystemService_TOKEN
|
|
3502
4408
|
);
|
|
3503
4409
|
const append = command.args["append"] || command.args["a"] || false;
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
3508
|
-
|
|
3509
|
-
|
|
3510
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
|
|
3519
|
-
|
|
3520
|
-
context.writer.writeError(
|
|
3521
|
-
return;
|
|
3522
|
-
}
|
|
3523
|
-
if (!parsed.inputFile) {
|
|
3524
|
-
context.writer.writeError("tee: missing input file");
|
|
3525
|
-
return;
|
|
3526
|
-
}
|
|
3527
|
-
if (parsed.outputFiles.length === 0) {
|
|
3528
|
-
context.writer.writeError("tee: missing output file");
|
|
3529
|
-
return;
|
|
3530
|
-
}
|
|
3531
|
-
if (!fs.exists(parsed.inputFile)) {
|
|
3532
|
-
context.writer.writeError(
|
|
3533
|
-
`tee: ${parsed.inputFile}: No such file or directory`
|
|
3534
|
-
);
|
|
3535
|
-
return;
|
|
3536
|
-
}
|
|
3537
|
-
content = fs.readFile(parsed.inputFile) || "";
|
|
3538
|
-
context.writer.writeln(content);
|
|
3539
|
-
for (const outFile of parsed.outputFiles) {
|
|
3540
|
-
if (append && fs.exists(outFile)) {
|
|
3541
|
-
fs.writeFile(outFile, content, true);
|
|
3542
|
-
} else {
|
|
3543
|
-
fs.writeFile(outFile, content);
|
|
3544
|
-
}
|
|
4410
|
+
if (command.data == null) {
|
|
4411
|
+
context.writer.writeError(
|
|
4412
|
+
"tee: no input: tee copies piped input to files and the output; use 'cat file | tee out.txt'"
|
|
4413
|
+
);
|
|
4414
|
+
context.process.exit(1, { silent: true });
|
|
4415
|
+
return;
|
|
4416
|
+
}
|
|
4417
|
+
const content = pipedText(command.data);
|
|
4418
|
+
context.writer.writeln(content);
|
|
4419
|
+
const fileText = content === "" || content.endsWith("\n") ? content : content + "\n";
|
|
4420
|
+
let changed = false;
|
|
4421
|
+
for (const outFile of this.getAllPaths(command)) {
|
|
4422
|
+
try {
|
|
4423
|
+
fs.writeFile(outFile, fileText, append);
|
|
4424
|
+
changed = true;
|
|
4425
|
+
} catch (e) {
|
|
4426
|
+
context.writer.writeError(`tee: ${e.message}`);
|
|
3545
4427
|
}
|
|
3546
|
-
await fs.persist();
|
|
3547
4428
|
}
|
|
4429
|
+
if (changed) await fs.persist();
|
|
3548
4430
|
}
|
|
3549
4431
|
getAllPaths(command) {
|
|
3550
4432
|
const raw = command.value || "";
|
|
3551
|
-
return raw
|
|
3552
|
-
}
|
|
3553
|
-
parseArgs(command) {
|
|
3554
|
-
const raw = command.value || "";
|
|
3555
|
-
const paths = raw.split(/\s+/).filter(Boolean);
|
|
3556
|
-
if (paths.length < 2) {
|
|
3557
|
-
return {
|
|
3558
|
-
outputFiles: [],
|
|
3559
|
-
inputFile: paths[0] || null
|
|
3560
|
-
};
|
|
3561
|
-
}
|
|
3562
|
-
const inputFile = paths[paths.length - 1];
|
|
3563
|
-
const outputFiles = paths.slice(0, paths.length - 1);
|
|
3564
|
-
return { outputFiles, inputFile };
|
|
4433
|
+
return splitArgs(raw).filter(Boolean);
|
|
3565
4434
|
}
|
|
3566
4435
|
};
|
|
3567
4436
|
var CliXargsCommandProcessor = class {
|
|
@@ -3590,65 +4459,91 @@ var CliXargsCommandProcessor = class {
|
|
|
3590
4459
|
}
|
|
3591
4460
|
];
|
|
3592
4461
|
}
|
|
4462
|
+
writeDescription(context) {
|
|
4463
|
+
const t = context.translator;
|
|
4464
|
+
writeWrapped(
|
|
4465
|
+
context,
|
|
4466
|
+
t.t("cli.xargs.help.run_command_arguments_taken", "Run a command with arguments taken from input, separated by blanks or newlines. By default all of them are appended to one command; -n runs it in batches and -I runs it once per line, replacing a placeholder. xargs options go before the command; everything after it, flags included, belongs to the command. Input is piped text or, without a pipe, the last file named.")
|
|
4467
|
+
);
|
|
4468
|
+
context.writer.writeln();
|
|
4469
|
+
writeUsage(context, [
|
|
4470
|
+
["<command> | xargs <cmd>", t.t("cli.xargs.help.append_input_lines", "Append input lines to <cmd>")],
|
|
4471
|
+
["<command> | xargs -I {} <cmd {}>", t.t("cli.xargs.help.run_once_per_line", "Run <cmd> once per line")],
|
|
4472
|
+
["xargs <cmd> <file>", t.t("cli.xargs.help.read_arguments_file", "Read the arguments from a file")]
|
|
4473
|
+
]);
|
|
4474
|
+
context.writer.writeln();
|
|
4475
|
+
writeExamples(context, [
|
|
4476
|
+
['find . --name "*.tmp" | xargs rm', t.t("cli.xargs.help.delete_every_tmp_file", "Delete every .tmp file")],
|
|
4477
|
+
[
|
|
4478
|
+
"cat files.txt | xargs -I {} cp {} backup",
|
|
4479
|
+
t.t("cli.xargs.help.copy_each_listed_file", "Copy each listed file")
|
|
4480
|
+
],
|
|
4481
|
+
["cat dirs.txt | xargs -n 1 mkdir", t.t("cli.xargs.help.one_mkdir_per_line", "One mkdir per line")],
|
|
4482
|
+
['find . --name "*.bak" | xargs rm -r', t.t("cli.xargs.help.inner_command_keeps_flags", "The inner command keeps its flags")]
|
|
4483
|
+
]);
|
|
4484
|
+
}
|
|
3593
4485
|
async processCommand(command, context) {
|
|
3594
|
-
const fs = context.services.
|
|
4486
|
+
const fs = context.services.getRequired(
|
|
3595
4487
|
IFileSystemService_TOKEN
|
|
3596
4488
|
);
|
|
3597
|
-
|
|
3598
|
-
|
|
3599
|
-
command.
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
4489
|
+
let parsed;
|
|
4490
|
+
try {
|
|
4491
|
+
parsed = command.rawCommand ? parseXargsLine(command.rawCommand) : {
|
|
4492
|
+
replace: stringArg(command.args["replace"] ?? command.args["I"]),
|
|
4493
|
+
maxArgs: parseInt(
|
|
4494
|
+
String(command.args["max-args"] ?? command.args["n"] ?? ""),
|
|
4495
|
+
10
|
|
4496
|
+
) || void 0,
|
|
4497
|
+
words: splitWords(command.value ?? "")
|
|
4498
|
+
};
|
|
4499
|
+
} catch (e) {
|
|
4500
|
+
context.writer.writeError(`xargs: ${e.message}`);
|
|
4501
|
+
return;
|
|
4502
|
+
}
|
|
4503
|
+
const replaceStr = parsed.replace;
|
|
4504
|
+
const maxArgs = parsed.maxArgs ?? 0;
|
|
3603
4505
|
let commandTemplate;
|
|
3604
4506
|
let argContent;
|
|
3605
4507
|
if (command.data != null) {
|
|
3606
|
-
commandTemplate =
|
|
3607
|
-
|
|
3608
|
-
context.writer.writeError("xargs: missing command");
|
|
3609
|
-
return;
|
|
3610
|
-
}
|
|
3611
|
-
argContent = typeof command.data === "string" ? command.data : JSON.stringify(command.data);
|
|
4508
|
+
commandTemplate = parsed.words.map((w) => w.raw).join(" ") || "echo";
|
|
4509
|
+
argContent = pipedText(command.data);
|
|
3612
4510
|
} else {
|
|
3613
|
-
if (
|
|
4511
|
+
if (parsed.words.length === 0) {
|
|
3614
4512
|
context.writer.writeError("xargs: missing command");
|
|
3615
4513
|
return;
|
|
3616
4514
|
}
|
|
3617
|
-
|
|
3618
|
-
if (!parsed.inputFile) {
|
|
4515
|
+
if (parsed.words.length < 2) {
|
|
3619
4516
|
context.writer.writeError("xargs: missing input file");
|
|
3620
4517
|
return;
|
|
3621
4518
|
}
|
|
3622
|
-
|
|
4519
|
+
const inputFile = parsed.words[parsed.words.length - 1].value;
|
|
4520
|
+
commandTemplate = parsed.words.slice(0, -1).map((w) => w.raw).join(" ");
|
|
4521
|
+
if (!fs.exists(inputFile)) {
|
|
3623
4522
|
context.writer.writeError(
|
|
3624
|
-
`xargs: ${
|
|
4523
|
+
`xargs: ${inputFile}: No such file or directory`
|
|
3625
4524
|
);
|
|
3626
4525
|
return;
|
|
3627
4526
|
}
|
|
3628
|
-
argContent = fs.readFile(
|
|
4527
|
+
argContent = fs.readFile(inputFile) || "";
|
|
3629
4528
|
}
|
|
3630
|
-
const args = argContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
3631
|
-
if (args.length === 0) return;
|
|
3632
4529
|
const commands = [];
|
|
3633
4530
|
if (replaceStr) {
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
4531
|
+
const lines = argContent.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
4532
|
+
const placeholder = new RegExp(this.escapeRegex(replaceStr), "g");
|
|
4533
|
+
for (const line of lines) {
|
|
4534
|
+
commands.push(
|
|
4535
|
+
commandTemplate.replace(placeholder, () => quoteArg(line))
|
|
3638
4536
|
);
|
|
3639
|
-
commands.push(cmd);
|
|
3640
4537
|
}
|
|
3641
|
-
} else
|
|
3642
|
-
|
|
3643
|
-
|
|
4538
|
+
} else {
|
|
4539
|
+
const args = splitArgs(argContent).map(quoteArg);
|
|
4540
|
+
if (args.length === 0) return;
|
|
4541
|
+
const size = maxArgs > 0 ? maxArgs : args.length;
|
|
4542
|
+
for (let i = 0; i < args.length; i += size) {
|
|
3644
4543
|
commands.push(
|
|
3645
|
-
`${commandTemplate} ${
|
|
4544
|
+
`${commandTemplate} ${args.slice(i, i + size).join(" ")}`
|
|
3646
4545
|
);
|
|
3647
4546
|
}
|
|
3648
|
-
} else {
|
|
3649
|
-
commands.push(
|
|
3650
|
-
`${commandTemplate} ${args.join(" ")}`
|
|
3651
|
-
);
|
|
3652
4547
|
}
|
|
3653
4548
|
const executor = context.executor;
|
|
3654
4549
|
for (const cmd of commands) {
|
|
@@ -3663,27 +4558,64 @@ var CliXargsCommandProcessor = class {
|
|
|
3663
4558
|
}
|
|
3664
4559
|
}
|
|
3665
4560
|
}
|
|
3666
|
-
getAllNonFlagTokens(command) {
|
|
3667
|
-
const raw = command.value || "";
|
|
3668
|
-
return raw.split(/\s+/).filter(Boolean);
|
|
3669
|
-
}
|
|
3670
4561
|
escapeRegex(str) {
|
|
3671
4562
|
return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3672
4563
|
}
|
|
3673
|
-
|
|
3674
|
-
|
|
3675
|
-
|
|
3676
|
-
|
|
3677
|
-
|
|
3678
|
-
|
|
3679
|
-
|
|
3680
|
-
|
|
4564
|
+
};
|
|
4565
|
+
function parseXargsLine(raw) {
|
|
4566
|
+
const words = splitWords(raw).slice(1);
|
|
4567
|
+
const result = { words: [] };
|
|
4568
|
+
let i = 0;
|
|
4569
|
+
const valueOf = (option, inline) => {
|
|
4570
|
+
if (inline !== "") return inline;
|
|
4571
|
+
if (i + 1 >= words.length) {
|
|
4572
|
+
throw new Error(`option requires an argument -- '${option}'`);
|
|
4573
|
+
}
|
|
4574
|
+
return words[++i].value;
|
|
4575
|
+
};
|
|
4576
|
+
for (; i < words.length; i++) {
|
|
4577
|
+
const word = words[i].raw;
|
|
4578
|
+
if (word === "--") {
|
|
4579
|
+
i++;
|
|
4580
|
+
break;
|
|
4581
|
+
}
|
|
4582
|
+
if (!word.startsWith("-") || word === "-") break;
|
|
4583
|
+
let match;
|
|
4584
|
+
if (match = word.match(/^(?:-I|--replace(?:=|$))(.*)$/)) {
|
|
4585
|
+
result.replace = valueOf("I", unquote(match[1]));
|
|
4586
|
+
} else if (word === "-i") {
|
|
4587
|
+
result.replace = "{}";
|
|
4588
|
+
} else if (match = word.match(/^(?:-n|--max-args(?:=|$))(.*)$/)) {
|
|
4589
|
+
const value = valueOf("n", unquote(match[1]));
|
|
4590
|
+
const count = parseInt(value, 10);
|
|
4591
|
+
if (isNaN(count) || count < 1) {
|
|
4592
|
+
throw new Error(`invalid number of arguments: '${value}'`);
|
|
4593
|
+
}
|
|
4594
|
+
result.maxArgs = count;
|
|
4595
|
+
} else {
|
|
4596
|
+
throw new Error(`invalid option -- '${word.replace(/^-+/, "")}'`);
|
|
3681
4597
|
}
|
|
3682
|
-
const inputFile = nonFlagTokens[nonFlagTokens.length - 1];
|
|
3683
|
-
const commandTemplate = nonFlagTokens.slice(0, nonFlagTokens.length - 1).join(" ");
|
|
3684
|
-
return { commandTemplate, inputFile };
|
|
3685
4598
|
}
|
|
3686
|
-
|
|
4599
|
+
result.words = words.slice(i);
|
|
4600
|
+
return result;
|
|
4601
|
+
}
|
|
4602
|
+
function splitWords(raw) {
|
|
4603
|
+
const words = [];
|
|
4604
|
+
const regex = /(?:"(?:[^"\\]|\\.)*"|'[^']*'|[^\s"']|["'])+/g;
|
|
4605
|
+
for (const match of raw.matchAll(regex)) {
|
|
4606
|
+
words.push({ raw: match[0], value: splitArgs(match[0]).join(" ") });
|
|
4607
|
+
}
|
|
4608
|
+
return words;
|
|
4609
|
+
}
|
|
4610
|
+
function unquote(value) {
|
|
4611
|
+
return splitArgs(value).join(" ");
|
|
4612
|
+
}
|
|
4613
|
+
function stringArg(value) {
|
|
4614
|
+
return value == null || value === true || value === "" ? void 0 : String(value);
|
|
4615
|
+
}
|
|
4616
|
+
function quoteArg(arg) {
|
|
4617
|
+
return quoteShellWord(arg);
|
|
4618
|
+
}
|
|
3687
4619
|
var CliShCommandProcessor = class {
|
|
3688
4620
|
constructor() {
|
|
3689
4621
|
this.command = "sh";
|
|
@@ -3704,17 +4636,14 @@ var CliShCommandProcessor = class {
|
|
|
3704
4636
|
];
|
|
3705
4637
|
}
|
|
3706
4638
|
async processCommand(command, context) {
|
|
3707
|
-
const fs = context.services.
|
|
4639
|
+
const fs = context.services.getRequired(
|
|
3708
4640
|
IFileSystemService_TOKEN
|
|
3709
4641
|
);
|
|
3710
|
-
let
|
|
3711
|
-
if (
|
|
3712
|
-
|
|
3713
|
-
const firstSpace = raw.indexOf(" ");
|
|
3714
|
-
if (firstSpace > -1) {
|
|
3715
|
-
filePath = raw.slice(firstSpace + 1).trim();
|
|
3716
|
-
}
|
|
4642
|
+
let words = splitArgs(command.value || "");
|
|
4643
|
+
if (words.length === 0 && command.rawCommand) {
|
|
4644
|
+
words = splitArgs(command.rawCommand).slice(1).filter((w) => !w.startsWith("-"));
|
|
3717
4645
|
}
|
|
4646
|
+
const filePath = words[0] ?? "";
|
|
3718
4647
|
if (!filePath) {
|
|
3719
4648
|
context.writer.writeError("sh: missing file operand");
|
|
3720
4649
|
context.process.exit(1, { silent: true });
|
|
@@ -3747,10 +4676,10 @@ var CliShCommandProcessor = class {
|
|
|
3747
4676
|
await executeScript(content, context);
|
|
3748
4677
|
}
|
|
3749
4678
|
writeDescription(context) {
|
|
3750
|
-
const { writer } = context;
|
|
3751
|
-
writer.writeln(this.description);
|
|
4679
|
+
const { writer, translator: t } = context;
|
|
4680
|
+
writer.writeln(t.t("cli.sh.description", this.description));
|
|
3752
4681
|
writer.writeln();
|
|
3753
|
-
writer.writeln("Usage:");
|
|
4682
|
+
writer.writeln(t.t("cli.common.usage", "Usage:"));
|
|
3754
4683
|
writer.writeln(
|
|
3755
4684
|
` ${writer.wrapInColor("sh <script.sh>", CliForegroundColor.Cyan)}`
|
|
3756
4685
|
);
|
|
@@ -3761,83 +4690,57 @@ var CliShCommandProcessor = class {
|
|
|
3761
4690
|
` ${writer.wrapInColor(". <script.sh>", CliForegroundColor.Cyan)}`
|
|
3762
4691
|
);
|
|
3763
4692
|
writer.writeln(
|
|
3764
|
-
` ${writer.wrapInColor("./script.sh", CliForegroundColor.Cyan)} (requires execute permission)`
|
|
4693
|
+
` ${writer.wrapInColor("./script.sh", CliForegroundColor.Cyan)} ${t.t("cli.sh.help.requires_execute", "(requires execute permission)")}`
|
|
3765
4694
|
);
|
|
3766
4695
|
writer.writeln();
|
|
3767
|
-
writer.writeln("Script syntax:");
|
|
3768
|
-
writer.writeln(
|
|
3769
|
-
writer.writeln(
|
|
3770
|
-
writer.writeln(
|
|
3771
|
-
writer.writeln(
|
|
3772
|
-
writer.writeln(
|
|
4696
|
+
writer.writeln(t.t("cli.sh.help.script_syntax", "Script syntax:"));
|
|
4697
|
+
writer.writeln(` # ${t.t("cli.sh.help.comments", "Lines starting with # are comments")}`);
|
|
4698
|
+
writer.writeln(` cmd1; cmd2 # ${t.t("cli.sh.help.several_commands", "Several commands on one line")}`);
|
|
4699
|
+
writer.writeln(` VAR=value # ${t.t("cli.sh.help.assignment", "Variable assignment")}`);
|
|
4700
|
+
writer.writeln(` echo $VAR # ${t.t("cli.sh.help.substitution", "Variable substitution")}`);
|
|
4701
|
+
writer.writeln(` set -e # ${t.t("cli.sh.help.stop_on_error", "Stop on error (default)")}`);
|
|
4702
|
+
writer.writeln(` set +e # ${t.t("cli.sh.help.continue_on_errors", "Continue on errors")}`);
|
|
4703
|
+
writer.writeln(` exit [n] # ${t.t("cli.sh.help.exit", "Stop the script with status n")}`);
|
|
4704
|
+
writer.writeln();
|
|
4705
|
+
writer.writeln(t.t("cli.sh.help.control_flow", "Control flow:"));
|
|
4706
|
+
writer.writeln(" if cmd; then ...; elif cmd; then ...; else ...; fi");
|
|
4707
|
+
writer.writeln(" for NAME in a b *.txt; do ...; done");
|
|
4708
|
+
writer.writeln(` while cmd; do ...; done ${t.t("cli.sh.help.until", "(until cmd: the opposite)")}`);
|
|
4709
|
+
writer.writeln(` break / continue [n] # ${t.t("cli.sh.help.break_continue", "Leave / skip in a loop")}`);
|
|
4710
|
+
writer.writeln(` ! cmd # ${t.t("cli.sh.help.negate", "Negate the exit status")}`);
|
|
4711
|
+
writer.writeln(
|
|
4712
|
+
` ${t.t("cli.sh.help.conditions", "Conditions are commands:")} ${writer.wrapInColor("[ -f file ]", CliForegroundColor.Cyan)}, ${writer.wrapInColor('test "$A" = b', CliForegroundColor.Cyan)}, ${writer.wrapInColor("true", CliForegroundColor.Cyan)}, ${writer.wrapInColor("grep x file", CliForegroundColor.Cyan)}`
|
|
4713
|
+
);
|
|
4714
|
+
writer.writeln(` ${t.t("cli.sh.help.conditions_status", "(status 0 = true; see help test). Loops stop after 10000 iterations")}`);
|
|
4715
|
+
writer.writeln(` ${t.t("cli.sh.help.conditions_loops", "of while/until; Ctrl+C stops the script.")}`);
|
|
3773
4716
|
writer.writeln();
|
|
3774
|
-
writer.writeln("Example script:");
|
|
4717
|
+
writer.writeln(t.t("cli.sh.help.example_script", "Example script:"));
|
|
3775
4718
|
writer.writeln(
|
|
3776
4719
|
writer.wrapInColor(
|
|
3777
|
-
" #!/bin/sh\n # Setup script\n NAME=world\n echo Hello $NAME\n mkdir -p /tmp/test\n echo done > /tmp/test/status.txt",
|
|
4720
|
+
" #!/bin/sh\n # Setup script\n NAME=world\n echo Hello $NAME\n mkdir -p /tmp/test\n for f in a b c; do\n echo $f > /tmp/test/$f.txt\n done\n if [ -f /tmp/test/a.txt ]; then\n echo done > /tmp/test/status.txt\n fi",
|
|
3778
4721
|
CliForegroundColor.Green
|
|
3779
4722
|
)
|
|
3780
4723
|
);
|
|
3781
4724
|
}
|
|
3782
4725
|
};
|
|
4726
|
+
var SCRIPT_RUNNER_TOKEN = "cli-script-runner";
|
|
3783
4727
|
async function executeScript(content, context) {
|
|
3784
|
-
const
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
if (!line || line.startsWith("#")) {
|
|
3790
|
-
continue;
|
|
3791
|
-
}
|
|
3792
|
-
if (line === "set -e") {
|
|
3793
|
-
stopOnError = true;
|
|
3794
|
-
continue;
|
|
3795
|
-
}
|
|
3796
|
-
if (line === "set +e") {
|
|
3797
|
-
stopOnError = false;
|
|
3798
|
-
continue;
|
|
3799
|
-
}
|
|
3800
|
-
const assignMatch = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
|
|
3801
|
-
if (assignMatch) {
|
|
3802
|
-
const varName = assignMatch[1];
|
|
3803
|
-
let varValue = assignMatch[2];
|
|
3804
|
-
if (varValue.startsWith('"') && varValue.endsWith('"') || varValue.startsWith("'") && varValue.endsWith("'")) {
|
|
3805
|
-
varValue = varValue.slice(1, -1);
|
|
3806
|
-
}
|
|
3807
|
-
varValue = substituteVariables(varValue, variables);
|
|
3808
|
-
variables[varName] = varValue;
|
|
3809
|
-
continue;
|
|
3810
|
-
}
|
|
3811
|
-
const expanded = substituteVariables(line, variables);
|
|
3812
|
-
await context.executor.executeCommand(expanded, context);
|
|
3813
|
-
const exitCode = context.process.exitCode;
|
|
3814
|
-
if (stopOnError && exitCode !== void 0 && exitCode !== 0) {
|
|
3815
|
-
context.writer.writeError(
|
|
3816
|
-
`sh: script aborted at line: ${line}`
|
|
3817
|
-
);
|
|
3818
|
-
return;
|
|
3819
|
-
}
|
|
4728
|
+
const runner = context.services.get(SCRIPT_RUNNER_TOKEN);
|
|
4729
|
+
if (!runner) {
|
|
4730
|
+
context.writer.writeError("sh: script runner not available (update @qodalis/cli)");
|
|
4731
|
+
context.process.exit(1, { silent: true });
|
|
4732
|
+
return;
|
|
3820
4733
|
}
|
|
4734
|
+
await runner.run(content, context, { name: "sh" });
|
|
3821
4735
|
}
|
|
3822
|
-
function substituteVariables(input, variables) {
|
|
3823
|
-
let result = input.replace(
|
|
3824
|
-
/\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g,
|
|
3825
|
-
(_, name) => variables[name] ?? ""
|
|
3826
|
-
);
|
|
3827
|
-
result = result.replace(
|
|
3828
|
-
/\$([A-Za-z_][A-Za-z0-9_]*)/g,
|
|
3829
|
-
(_, name) => variables[name] ?? ""
|
|
3830
|
-
);
|
|
3831
|
-
return result;
|
|
3832
|
-
}
|
|
3833
|
-
|
|
3834
|
-
// src/lib/services/indexed-db-file-system.service.ts
|
|
3835
4736
|
var DB_NAME = "qodalis-cli-filesystem";
|
|
3836
4737
|
var STORE_NAME = "filesystem";
|
|
3837
4738
|
var ROOT_KEY = "root";
|
|
3838
4739
|
var CWD_KEY = "cwd";
|
|
3839
4740
|
var HOME_KEY = "home";
|
|
3840
4741
|
var DEFAULT_HOME = "/";
|
|
4742
|
+
var SYSTEM_OWNERSHIP = { uid: "root", gid: "admin" };
|
|
4743
|
+
var ANONYMOUS_OWNERSHIP = { uid: "nobody", gid: "nogroup" };
|
|
3841
4744
|
function createSeedFileSystem() {
|
|
3842
4745
|
const now = Date.now();
|
|
3843
4746
|
return {
|
|
@@ -3885,6 +4788,88 @@ var IndexedDbFileSystemService = class {
|
|
|
3885
4788
|
this.homePath = DEFAULT_HOME;
|
|
3886
4789
|
this.currentUid = null;
|
|
3887
4790
|
this.currentGroups = [];
|
|
4791
|
+
this.systemDepth = 0;
|
|
4792
|
+
}
|
|
4793
|
+
// --- Access control ---
|
|
4794
|
+
setAccessPolicy(policy) {
|
|
4795
|
+
this.accessPolicy = policy;
|
|
4796
|
+
}
|
|
4797
|
+
runAsSystem(fn) {
|
|
4798
|
+
this.systemDepth++;
|
|
4799
|
+
try {
|
|
4800
|
+
return fn();
|
|
4801
|
+
} finally {
|
|
4802
|
+
this.systemDepth--;
|
|
4803
|
+
}
|
|
4804
|
+
}
|
|
4805
|
+
reassignOwner(fromUid, toUid) {
|
|
4806
|
+
const from = fromUid.toLowerCase();
|
|
4807
|
+
if (!from || !toUid) return 0;
|
|
4808
|
+
let changed = 0;
|
|
4809
|
+
const visit = (node) => {
|
|
4810
|
+
if (node.ownership && node.ownership.uid.toLowerCase() === from) {
|
|
4811
|
+
node.ownership = { ...node.ownership, uid: toUid };
|
|
4812
|
+
changed++;
|
|
4813
|
+
}
|
|
4814
|
+
node.children?.forEach(visit);
|
|
4815
|
+
};
|
|
4816
|
+
visit(this.root);
|
|
4817
|
+
return changed;
|
|
4818
|
+
}
|
|
4819
|
+
canTraverse(path) {
|
|
4820
|
+
try {
|
|
4821
|
+
this.assertTraverse(this.resolvePath(path), "stat", path);
|
|
4822
|
+
return true;
|
|
4823
|
+
} catch {
|
|
4824
|
+
return false;
|
|
4825
|
+
}
|
|
4826
|
+
}
|
|
4827
|
+
get enforcing() {
|
|
4828
|
+
return !!this.accessPolicy && this.systemDepth === 0;
|
|
4829
|
+
}
|
|
4830
|
+
deny(op, path, reason = "Permission denied") {
|
|
4831
|
+
throw new CliAccessDeniedError(`${op}: ${path}: ${reason}`);
|
|
4832
|
+
}
|
|
4833
|
+
/** `x` on every existing directory on the way to `resolved` (excluding the target). */
|
|
4834
|
+
assertTraverse(resolved, op, display) {
|
|
4835
|
+
if (!this.enforcing) return;
|
|
4836
|
+
const parts = resolved.split("/").filter(Boolean);
|
|
4837
|
+
let current = this.root;
|
|
4838
|
+
for (let i = 0; i < parts.length; i++) {
|
|
4839
|
+
if (!current || current.type !== "directory") return;
|
|
4840
|
+
if (!this.accessPolicy.canAccess(current, "execute")) this.deny(op, display);
|
|
4841
|
+
current = current.children?.find((c) => c.name === parts[i]);
|
|
4842
|
+
}
|
|
4843
|
+
}
|
|
4844
|
+
assertAccess(node, action, op, display) {
|
|
4845
|
+
if (this.enforcing && !this.accessPolicy.canAccess(node, action)) this.deny(op, display);
|
|
4846
|
+
}
|
|
4847
|
+
/** Recursive delete: every directory inside must allow removing its entries (`w` + `x`, readable). */
|
|
4848
|
+
assertRemovableTree(dir, dirPath, display) {
|
|
4849
|
+
if (!this.enforcing) return;
|
|
4850
|
+
this.assertAccess(dir, "read", "rm", display);
|
|
4851
|
+
this.assertAccess(dir, "execute", "rm", display);
|
|
4852
|
+
if ((dir.children?.length ?? 0) > 0) this.assertAccess(dir, "write", "rm", display);
|
|
4853
|
+
for (const child of dir.children ?? []) {
|
|
4854
|
+
if (child.type === "directory") this.assertRemovableTree(child, `${dirPath}/${child.name}`, display);
|
|
4855
|
+
}
|
|
4856
|
+
}
|
|
4857
|
+
/** Copying reads everything it copies: `r` on every file, `r` + `x` on every directory. */
|
|
4858
|
+
assertReadableTree(node, display) {
|
|
4859
|
+
if (!this.enforcing) return;
|
|
4860
|
+
this.assertAccess(node, "read", "cp", display);
|
|
4861
|
+
if (node.type !== "directory") return;
|
|
4862
|
+
this.assertAccess(node, "execute", "cp", display);
|
|
4863
|
+
for (const child of node.children ?? []) {
|
|
4864
|
+
this.assertReadableTree(child, `${display.replace(/\/+$/, "")}/${child.name}`);
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
/** Creating/removing entries needs `w` and `x` on the containing directory. */
|
|
4868
|
+
assertModifyDir(dir, dirPath, op, display) {
|
|
4869
|
+
if (!this.enforcing) return;
|
|
4870
|
+
this.assertTraverse(dirPath, op, display);
|
|
4871
|
+
this.assertAccess(dir, "execute", op, display);
|
|
4872
|
+
this.assertAccess(dir, "write", op, display);
|
|
3888
4873
|
}
|
|
3889
4874
|
// --- User context ---
|
|
3890
4875
|
setCurrentUser(uid, groups) {
|
|
@@ -3892,7 +4877,9 @@ var IndexedDbFileSystemService = class {
|
|
|
3892
4877
|
this.currentGroups = groups;
|
|
3893
4878
|
}
|
|
3894
4879
|
getDefaultOwnership() {
|
|
3895
|
-
if (!this.currentUid)
|
|
4880
|
+
if (!this.currentUid) {
|
|
4881
|
+
return this.accessPolicy ? { ...ANONYMOUS_OWNERSHIP } : void 0;
|
|
4882
|
+
}
|
|
3896
4883
|
return {
|
|
3897
4884
|
uid: this.currentUid,
|
|
3898
4885
|
gid: this.currentGroups[0] || "users"
|
|
@@ -3911,6 +4898,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3911
4898
|
if (node.type !== "directory") {
|
|
3912
4899
|
throw new Error(`cd: ${path}: Not a directory`);
|
|
3913
4900
|
}
|
|
4901
|
+
this.assertTraverse(resolved, "cd", path);
|
|
4902
|
+
this.assertAccess(node, "execute", "cd", path);
|
|
3914
4903
|
this.cwd = resolved;
|
|
3915
4904
|
}
|
|
3916
4905
|
getHomePath() {
|
|
@@ -3967,6 +4956,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3967
4956
|
if (node.type !== "directory") {
|
|
3968
4957
|
throw new Error(`ls: ${path}: Not a directory`);
|
|
3969
4958
|
}
|
|
4959
|
+
this.assertTraverse(this.resolvePath(path), "ls", path);
|
|
4960
|
+
this.assertAccess(node, "read", "ls", path);
|
|
3970
4961
|
return node.children || [];
|
|
3971
4962
|
}
|
|
3972
4963
|
readFile(path) {
|
|
@@ -3977,6 +4968,8 @@ var IndexedDbFileSystemService = class {
|
|
|
3977
4968
|
if (node.type === "directory") {
|
|
3978
4969
|
throw new Error(`cat: ${path}: Is a directory`);
|
|
3979
4970
|
}
|
|
4971
|
+
this.assertTraverse(this.resolvePath(path), "cat", path);
|
|
4972
|
+
this.assertAccess(node, "read", "cat", path);
|
|
3980
4973
|
return node.content ?? "";
|
|
3981
4974
|
}
|
|
3982
4975
|
exists(path) {
|
|
@@ -4001,6 +4994,10 @@ var IndexedDbFileSystemService = class {
|
|
|
4001
4994
|
}
|
|
4002
4995
|
let child = current.children.find((c) => c.name === part);
|
|
4003
4996
|
if (!child) {
|
|
4997
|
+
if (this.enforcing) {
|
|
4998
|
+
this.assertAccess(current, "execute", "mkdir", path);
|
|
4999
|
+
this.assertAccess(current, "write", "mkdir", path);
|
|
5000
|
+
}
|
|
4004
5001
|
const now = Date.now();
|
|
4005
5002
|
child = {
|
|
4006
5003
|
name: part,
|
|
@@ -4016,6 +5013,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4016
5013
|
current.modifiedAt = now;
|
|
4017
5014
|
} else if (child.type !== "directory") {
|
|
4018
5015
|
throw new Error(`mkdir: ${part}: Not a directory`);
|
|
5016
|
+
} else {
|
|
5017
|
+
this.assertAccess(child, "execute", "mkdir", path);
|
|
4019
5018
|
}
|
|
4020
5019
|
current = child;
|
|
4021
5020
|
}
|
|
@@ -4034,6 +5033,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4034
5033
|
if (parent.children?.find((c) => c.name === dirName)) {
|
|
4035
5034
|
throw new Error(`mkdir: ${path}: File exists`);
|
|
4036
5035
|
}
|
|
5036
|
+
this.assertModifyDir(parent, parentPath, "mkdir", path);
|
|
4037
5037
|
const now = Date.now();
|
|
4038
5038
|
if (!parent.children) {
|
|
4039
5039
|
parent.children = [];
|
|
@@ -4068,6 +5068,12 @@ var IndexedDbFileSystemService = class {
|
|
|
4068
5068
|
}
|
|
4069
5069
|
const existing = parent.children?.find((c) => c.name === fileName);
|
|
4070
5070
|
const now = Date.now();
|
|
5071
|
+
if (existing) {
|
|
5072
|
+
this.assertTraverse(resolved, "touch", path);
|
|
5073
|
+
this.assertAccess(existing, "write", "touch", path);
|
|
5074
|
+
} else {
|
|
5075
|
+
this.assertModifyDir(parent, parentPath, "touch", path);
|
|
5076
|
+
}
|
|
4071
5077
|
if (existing) {
|
|
4072
5078
|
existing.modifiedAt = now;
|
|
4073
5079
|
} else {
|
|
@@ -4094,6 +5100,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4094
5100
|
if (node.type === "directory") {
|
|
4095
5101
|
throw new Error(`write: ${path}: Is a directory`);
|
|
4096
5102
|
}
|
|
5103
|
+
this.assertTraverse(resolved, "write", path);
|
|
5104
|
+
this.assertAccess(node, "write", "write", path);
|
|
4097
5105
|
const now = Date.now();
|
|
4098
5106
|
node.content = append ? (node.content ?? "") + content : content;
|
|
4099
5107
|
node.size = new Blob([node.content]).size;
|
|
@@ -4122,6 +5130,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4122
5130
|
if (target.type === "directory" && !recursive) {
|
|
4123
5131
|
throw new Error(`rm: ${path}: Is a directory`);
|
|
4124
5132
|
}
|
|
5133
|
+
this.assertModifyDir(parent, parentPath, "rm", path);
|
|
5134
|
+
if (target.type === "directory") this.assertRemovableTree(target, resolved, path);
|
|
4125
5135
|
parent.children.splice(index, 1);
|
|
4126
5136
|
parent.modifiedAt = Date.now();
|
|
4127
5137
|
}
|
|
@@ -4136,10 +5146,13 @@ var IndexedDbFileSystemService = class {
|
|
|
4136
5146
|
`cp: -r not specified; omitting directory '${src}'`
|
|
4137
5147
|
);
|
|
4138
5148
|
}
|
|
5149
|
+
this.assertTraverse(srcResolved, "cp", src);
|
|
5150
|
+
this.assertReadableTree(srcNode, src);
|
|
4139
5151
|
const destResolved = this.resolvePath(dest);
|
|
4140
5152
|
const destNode = this.getNode(destResolved);
|
|
4141
5153
|
if (destNode && destNode.type === "directory") {
|
|
4142
|
-
|
|
5154
|
+
this.assertModifyDir(destNode, destResolved, "cp", dest);
|
|
5155
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4143
5156
|
if (!destNode.children) {
|
|
4144
5157
|
destNode.children = [];
|
|
4145
5158
|
}
|
|
@@ -4159,7 +5172,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4159
5172
|
if (!parent || parent.type !== "directory") {
|
|
4160
5173
|
throw new Error(`cp: ${parentPath}: No such file or directory`);
|
|
4161
5174
|
}
|
|
4162
|
-
|
|
5175
|
+
this.assertModifyDir(parent, parentPath, "cp", dest);
|
|
5176
|
+
const clone = this.ownedByCurrentUser(this.cloneNode(srcNode));
|
|
4163
5177
|
clone.name = newName;
|
|
4164
5178
|
if (!parent.children) {
|
|
4165
5179
|
parent.children = [];
|
|
@@ -4185,16 +5199,23 @@ var IndexedDbFileSystemService = class {
|
|
|
4185
5199
|
}
|
|
4186
5200
|
const destResolved = this.resolvePath(dest);
|
|
4187
5201
|
const destNode = this.getNode(destResolved);
|
|
5202
|
+
if (srcNode.type === "directory" && (destResolved === srcResolved || destResolved.startsWith(srcResolved + "/"))) {
|
|
5203
|
+
throw new Error(
|
|
5204
|
+
`mv: cannot move '${src}' to a subdirectory of itself, '${dest}'`
|
|
5205
|
+
);
|
|
5206
|
+
}
|
|
4188
5207
|
const srcParts = srcResolved.split("/").filter(Boolean);
|
|
4189
5208
|
const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
|
|
4190
5209
|
const srcParent = this.getNode(srcParentPath);
|
|
4191
5210
|
if (!srcParent || !srcParent.children) {
|
|
4192
5211
|
throw new Error(`mv: ${src}: No such file or directory`);
|
|
4193
5212
|
}
|
|
4194
|
-
|
|
4195
|
-
|
|
4196
|
-
|
|
5213
|
+
this.assertModifyDir(srcParent, srcParentPath, "mv", src);
|
|
5214
|
+
if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
|
|
5215
|
+
throw new Error(`mv: '${src}' and '${dest}' are the same file`);
|
|
5216
|
+
}
|
|
4197
5217
|
if (destNode && destNode.type === "directory") {
|
|
5218
|
+
this.assertModifyDir(destNode, destResolved, "mv", dest);
|
|
4198
5219
|
if (!destNode.children) {
|
|
4199
5220
|
destNode.children = [];
|
|
4200
5221
|
}
|
|
@@ -4204,7 +5225,7 @@ var IndexedDbFileSystemService = class {
|
|
|
4204
5225
|
if (existingIdx !== -1) {
|
|
4205
5226
|
destNode.children.splice(existingIdx, 1);
|
|
4206
5227
|
}
|
|
4207
|
-
srcParent.children.splice(
|
|
5228
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4208
5229
|
destNode.children.push(srcNode);
|
|
4209
5230
|
destNode.modifiedAt = Date.now();
|
|
4210
5231
|
} else {
|
|
@@ -4217,7 +5238,8 @@ var IndexedDbFileSystemService = class {
|
|
|
4217
5238
|
`mv: ${destParentPath}: No such file or directory`
|
|
4218
5239
|
);
|
|
4219
5240
|
}
|
|
4220
|
-
|
|
5241
|
+
this.assertModifyDir(destParent, destParentPath, "mv", dest);
|
|
5242
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4221
5243
|
srcNode.name = newName;
|
|
4222
5244
|
if (!destParent.children) {
|
|
4223
5245
|
destParent.children = [];
|
|
@@ -4240,6 +5262,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4240
5262
|
if (!node) {
|
|
4241
5263
|
throw new Error(`chmod: ${path}: No such file or directory`);
|
|
4242
5264
|
}
|
|
5265
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node)) {
|
|
5266
|
+
this.deny("chmod", path, "Operation not permitted");
|
|
5267
|
+
}
|
|
4243
5268
|
node.permissions = permissions;
|
|
4244
5269
|
}
|
|
4245
5270
|
chown(path, ownership) {
|
|
@@ -4248,6 +5273,9 @@ var IndexedDbFileSystemService = class {
|
|
|
4248
5273
|
if (!node) {
|
|
4249
5274
|
throw new Error(`chown: ${path}: No such file or directory`);
|
|
4250
5275
|
}
|
|
5276
|
+
if (this.enforcing && !this.accessPolicy.canChangeOwnership(node, ownership)) {
|
|
5277
|
+
this.deny("chown", path, "Operation not permitted");
|
|
5278
|
+
}
|
|
4251
5279
|
node.ownership = ownership;
|
|
4252
5280
|
}
|
|
4253
5281
|
// --- Persistence ---
|
|
@@ -4278,8 +5306,10 @@ var IndexedDbFileSystemService = class {
|
|
|
4278
5306
|
const tx = db.transaction(STORE_NAME, "readwrite");
|
|
4279
5307
|
const store = tx.objectStore(STORE_NAME);
|
|
4280
5308
|
store.put(this.root, ROOT_KEY);
|
|
4281
|
-
|
|
4282
|
-
|
|
5309
|
+
if (!this.currentUid) {
|
|
5310
|
+
store.put(this.cwd, CWD_KEY);
|
|
5311
|
+
store.put(this.homePath, HOME_KEY);
|
|
5312
|
+
}
|
|
4283
5313
|
await new Promise((resolve, reject) => {
|
|
4284
5314
|
tx.oncomplete = () => resolve();
|
|
4285
5315
|
tx.onerror = () => reject(tx.error);
|
|
@@ -4289,6 +5319,18 @@ var IndexedDbFileSystemService = class {
|
|
|
4289
5319
|
}
|
|
4290
5320
|
}
|
|
4291
5321
|
// --- Private helpers ---
|
|
5322
|
+
/** A copy belongs to whoever made it (like `cp` without `-p`) */
|
|
5323
|
+
ownedByCurrentUser(node) {
|
|
5324
|
+
const owner = this.getDefaultOwnership();
|
|
5325
|
+
if (owner) {
|
|
5326
|
+
const apply = (n) => {
|
|
5327
|
+
n.ownership = { ...owner };
|
|
5328
|
+
n.children?.forEach(apply);
|
|
5329
|
+
};
|
|
5330
|
+
apply(node);
|
|
5331
|
+
}
|
|
5332
|
+
return node;
|
|
5333
|
+
}
|
|
4292
5334
|
cloneNode(node) {
|
|
4293
5335
|
const now = Date.now();
|
|
4294
5336
|
const clone = {
|
|
@@ -4330,8 +5372,6 @@ var IndexedDbFileSystemService = class {
|
|
|
4330
5372
|
});
|
|
4331
5373
|
}
|
|
4332
5374
|
};
|
|
4333
|
-
|
|
4334
|
-
// src/lib/services/server-file-system.service.ts
|
|
4335
5375
|
var DEFAULT_HOME2 = "/home/user";
|
|
4336
5376
|
function createSeedFileSystem2() {
|
|
4337
5377
|
const now = Date.now();
|
|
@@ -4381,6 +5421,7 @@ var ServerFileSystemService = class {
|
|
|
4381
5421
|
this.currentUid = null;
|
|
4382
5422
|
this.currentGroups = [];
|
|
4383
5423
|
this.baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
5424
|
+
this.headersProvider = options.headers;
|
|
4384
5425
|
}
|
|
4385
5426
|
// --- User context ---
|
|
4386
5427
|
setCurrentUser(uid, groups) {
|
|
@@ -4697,15 +5738,20 @@ var ServerFileSystemService = class {
|
|
|
4697
5738
|
}
|
|
4698
5739
|
const destResolved = this.resolvePath(dest);
|
|
4699
5740
|
const destNode = this.getNode(destResolved);
|
|
5741
|
+
if (srcNode.type === "directory" && (destResolved === srcResolved || destResolved.startsWith(srcResolved + "/"))) {
|
|
5742
|
+
throw new Error(
|
|
5743
|
+
`mv: cannot move '${src}' to a subdirectory of itself, '${dest}'`
|
|
5744
|
+
);
|
|
5745
|
+
}
|
|
4700
5746
|
const srcParts = srcResolved.split("/").filter(Boolean);
|
|
4701
5747
|
const srcParentPath = "/" + srcParts.slice(0, -1).join("/");
|
|
4702
5748
|
const srcParent = this.getNode(srcParentPath);
|
|
4703
5749
|
if (!srcParent || !srcParent.children) {
|
|
4704
5750
|
throw new Error(`mv: ${src}: No such file or directory`);
|
|
4705
5751
|
}
|
|
4706
|
-
|
|
4707
|
-
(
|
|
4708
|
-
|
|
5752
|
+
if (destResolved === srcResolved || destNode && destNode.type === "directory" && destNode === srcParent) {
|
|
5753
|
+
throw new Error(`mv: '${src}' and '${dest}' are the same file`);
|
|
5754
|
+
}
|
|
4709
5755
|
if (destNode && destNode.type === "directory") {
|
|
4710
5756
|
if (!destNode.children) {
|
|
4711
5757
|
destNode.children = [];
|
|
@@ -4716,7 +5762,7 @@ var ServerFileSystemService = class {
|
|
|
4716
5762
|
if (existingIdx !== -1) {
|
|
4717
5763
|
destNode.children.splice(existingIdx, 1);
|
|
4718
5764
|
}
|
|
4719
|
-
srcParent.children.splice(
|
|
5765
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4720
5766
|
destNode.children.push(srcNode);
|
|
4721
5767
|
destNode.modifiedAt = Date.now();
|
|
4722
5768
|
} else {
|
|
@@ -4729,7 +5775,7 @@ var ServerFileSystemService = class {
|
|
|
4729
5775
|
`mv: ${destParentPath}: No such file or directory`
|
|
4730
5776
|
);
|
|
4731
5777
|
}
|
|
4732
|
-
srcParent.children.splice(
|
|
5778
|
+
srcParent.children.splice(srcParent.children.indexOf(srcNode), 1);
|
|
4733
5779
|
srcNode.name = newName;
|
|
4734
5780
|
if (!destParent.children) {
|
|
4735
5781
|
destParent.children = [];
|
|
@@ -4788,7 +5834,14 @@ var ServerFileSystemService = class {
|
|
|
4788
5834
|
// --- Private: server communication ---
|
|
4789
5835
|
async serverFetch(endpoint, options) {
|
|
4790
5836
|
const url = `${this.baseUrl}/api/qcli/fs${endpoint}`;
|
|
4791
|
-
const
|
|
5837
|
+
const mergedHeaders = {
|
|
5838
|
+
...resolveHeaders(this.headersProvider),
|
|
5839
|
+
...options?.headers ?? {}
|
|
5840
|
+
};
|
|
5841
|
+
const response = await fetch(url, {
|
|
5842
|
+
...options,
|
|
5843
|
+
headers: mergedHeaders
|
|
5844
|
+
});
|
|
4792
5845
|
if (!response.ok) {
|
|
4793
5846
|
const body = await response.text();
|
|
4794
5847
|
throw new Error(
|
|
@@ -4957,6 +6010,115 @@ var VirtualFsFileTransferService = class {
|
|
|
4957
6010
|
return this._browserService.uploadFromBrowser(accept);
|
|
4958
6011
|
}
|
|
4959
6012
|
};
|
|
6013
|
+
var CliFilesPermissions = {
|
|
6014
|
+
/** Bypass all file permission checks */
|
|
6015
|
+
Admin: "files:admin",
|
|
6016
|
+
/** chmod/chown files you do not own */
|
|
6017
|
+
Chown: "files:chown"
|
|
6018
|
+
};
|
|
6019
|
+
var HOME_DIR_PERMISSIONS = "rwxr-x---";
|
|
6020
|
+
function authorization(context) {
|
|
6021
|
+
try {
|
|
6022
|
+
return context.services.get(ICliAuthorizationService_TOKEN);
|
|
6023
|
+
} catch {
|
|
6024
|
+
return void 0;
|
|
6025
|
+
}
|
|
6026
|
+
}
|
|
6027
|
+
function createFileAccessPolicy(context) {
|
|
6028
|
+
return {
|
|
6029
|
+
// Nodes without an owner (system seed, legacy data) belong to root:admin
|
|
6030
|
+
canAccess: (node, action) => cliCanAccess(
|
|
6031
|
+
context,
|
|
6032
|
+
node.ownership ? node : { ...node, ownership: SYSTEM_OWNERSHIP },
|
|
6033
|
+
action,
|
|
6034
|
+
[CliFilesPermissions.Admin]
|
|
6035
|
+
),
|
|
6036
|
+
canChangeOwnership: (node, newOwnership) => {
|
|
6037
|
+
const auth = authorization(context);
|
|
6038
|
+
if (!auth) return true;
|
|
6039
|
+
if (auth.can(CliFilesPermissions.Admin) || auth.can(CliFilesPermissions.Chown)) return true;
|
|
6040
|
+
const principal = auth.getPrincipal();
|
|
6041
|
+
if (!principal) return false;
|
|
6042
|
+
const owns = !!node.ownership && (node.ownership.uid === principal.name || node.ownership.uid === principal.id);
|
|
6043
|
+
const keepsOwner = !newOwnership || newOwnership.uid === node.ownership?.uid || newOwnership.uid === principal.name;
|
|
6044
|
+
return owns && keepsOwner;
|
|
6045
|
+
}
|
|
6046
|
+
};
|
|
6047
|
+
}
|
|
6048
|
+
function followUserSession(context, fs) {
|
|
6049
|
+
let sessions;
|
|
6050
|
+
try {
|
|
6051
|
+
sessions = context.services.get(ICliUserSessionService_TOKEN);
|
|
6052
|
+
} catch {
|
|
6053
|
+
sessions = void 0;
|
|
6054
|
+
}
|
|
6055
|
+
if (!sessions) return void 0;
|
|
6056
|
+
let previousUser = null;
|
|
6057
|
+
return sessions.getUserSession().subscribe((session) => {
|
|
6058
|
+
if (!session?.user) {
|
|
6059
|
+
fs.setCurrentUser("", []);
|
|
6060
|
+
if (previousUser !== null || !canEnter(fs, fs.getCurrentDirectory())) {
|
|
6061
|
+
try {
|
|
6062
|
+
fs.setCurrentDirectory("/");
|
|
6063
|
+
void fs.persist();
|
|
6064
|
+
} catch {
|
|
6065
|
+
}
|
|
6066
|
+
}
|
|
6067
|
+
previousUser = null;
|
|
6068
|
+
return;
|
|
6069
|
+
}
|
|
6070
|
+
const user = session.user;
|
|
6071
|
+
fs.setCurrentUser(user.name, user.groups ?? []);
|
|
6072
|
+
if (user.homeDir) {
|
|
6073
|
+
const home = user.homeDir;
|
|
6074
|
+
const system = (fn) => fs.runAsSystem ? fs.runAsSystem(fn) : fn();
|
|
6075
|
+
if (!fs.exists(home)) {
|
|
6076
|
+
system(() => {
|
|
6077
|
+
fs.createDirectory(home, true);
|
|
6078
|
+
if (home !== "/") {
|
|
6079
|
+
fs.chown(home, { uid: user.name, gid: user.groups?.[0] ?? "users" });
|
|
6080
|
+
fs.chmod(home, HOME_DIR_PERMISSIONS);
|
|
6081
|
+
}
|
|
6082
|
+
});
|
|
6083
|
+
void fs.persist();
|
|
6084
|
+
}
|
|
6085
|
+
fs.setHomePath(home);
|
|
6086
|
+
if (previousUser !== user.id) {
|
|
6087
|
+
try {
|
|
6088
|
+
fs.setCurrentDirectory(home);
|
|
6089
|
+
} catch {
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
}
|
|
6093
|
+
previousUser = user.id;
|
|
6094
|
+
});
|
|
6095
|
+
}
|
|
6096
|
+
function canEnter(fs, path) {
|
|
6097
|
+
try {
|
|
6098
|
+
fs.setCurrentDirectory(path);
|
|
6099
|
+
return true;
|
|
6100
|
+
} catch {
|
|
6101
|
+
return false;
|
|
6102
|
+
}
|
|
6103
|
+
}
|
|
6104
|
+
function followUserAccounts(context, fs) {
|
|
6105
|
+
let events;
|
|
6106
|
+
try {
|
|
6107
|
+
events = context.services.get(ICliUserEventService_TOKEN);
|
|
6108
|
+
} catch {
|
|
6109
|
+
events = void 0;
|
|
6110
|
+
}
|
|
6111
|
+
if (!events || !fs.reassignOwner) return void 0;
|
|
6112
|
+
return events.events$.subscribe((event) => {
|
|
6113
|
+
let changed = 0;
|
|
6114
|
+
if (event.type === "user_updated" && event.previousName && event.previousName !== event.user?.name) {
|
|
6115
|
+
changed = fs.reassignOwner(event.previousName, event.user.name);
|
|
6116
|
+
} else if (event.type === "user_deleted" && event.user?.name) {
|
|
6117
|
+
changed = fs.reassignOwner(event.user.name, SYSTEM_OWNERSHIP.uid);
|
|
6118
|
+
}
|
|
6119
|
+
if (changed > 0) void fs.persist().catch(() => void 0);
|
|
6120
|
+
});
|
|
6121
|
+
}
|
|
4960
6122
|
|
|
4961
6123
|
// src/lib/completion/file-path-completion-provider.ts
|
|
4962
6124
|
var FILE_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -4977,7 +6139,26 @@ var FILE_COMMANDS = /* @__PURE__ */ new Set([
|
|
|
4977
6139
|
"find",
|
|
4978
6140
|
"head",
|
|
4979
6141
|
"tail",
|
|
4980
|
-
"wc"
|
|
6142
|
+
"wc",
|
|
6143
|
+
"sort",
|
|
6144
|
+
"uniq",
|
|
6145
|
+
"cut",
|
|
6146
|
+
"paste",
|
|
6147
|
+
"tac",
|
|
6148
|
+
"diff",
|
|
6149
|
+
"sed",
|
|
6150
|
+
"awk",
|
|
6151
|
+
"du",
|
|
6152
|
+
"stat",
|
|
6153
|
+
"chmod",
|
|
6154
|
+
"chown",
|
|
6155
|
+
"ln",
|
|
6156
|
+
"tee",
|
|
6157
|
+
"basename",
|
|
6158
|
+
"dirname",
|
|
6159
|
+
"sh",
|
|
6160
|
+
"source",
|
|
6161
|
+
"xargs"
|
|
4981
6162
|
]);
|
|
4982
6163
|
var FilePathCompletionProvider = class {
|
|
4983
6164
|
constructor(fs) {
|
|
@@ -4996,7 +6177,14 @@ var FilePathCompletionProvider = class {
|
|
|
4996
6177
|
if (token.startsWith("-")) {
|
|
4997
6178
|
return [];
|
|
4998
6179
|
}
|
|
4999
|
-
|
|
6180
|
+
const quote = token.startsWith('"') || token.startsWith("'") ? token[0] : "";
|
|
6181
|
+
const partial = quote ? token.slice(1).replace(new RegExp(`${quote}$`), "") : token;
|
|
6182
|
+
const paths = this.completePath(partial);
|
|
6183
|
+
if (!quote && !paths.some((p) => /\s/.test(p))) {
|
|
6184
|
+
return paths;
|
|
6185
|
+
}
|
|
6186
|
+
const q = quote || '"';
|
|
6187
|
+
return paths.map((p) => p.endsWith("/") ? `${q}${p}` : `${q}${p}${q}`);
|
|
5000
6188
|
}
|
|
5001
6189
|
completePath(partial) {
|
|
5002
6190
|
let dirPath;
|
|
@@ -5057,7 +6245,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5057
6245
|
];
|
|
5058
6246
|
}
|
|
5059
6247
|
async processCommand(command, context) {
|
|
5060
|
-
const fs = context.services.
|
|
6248
|
+
const fs = context.services.getRequired(
|
|
5061
6249
|
IFileSystemService_TOKEN
|
|
5062
6250
|
);
|
|
5063
6251
|
const fileService = context.services.get(
|
|
@@ -5070,6 +6258,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5070
6258
|
const accept = command.args?.["accept"];
|
|
5071
6259
|
const destPath = command.value?.trim() || void 0;
|
|
5072
6260
|
context.spinner?.show("Waiting for file selection...");
|
|
6261
|
+
context.notifier.info("Waiting for file selection");
|
|
5073
6262
|
const picked = await fileService.uploadFromBrowser(accept);
|
|
5074
6263
|
if (!picked) {
|
|
5075
6264
|
context.spinner?.hide();
|
|
@@ -5082,6 +6271,7 @@ var CliUploadCommandProcessor = class {
|
|
|
5082
6271
|
filename.startsWith("/") ? filename : cwd + "/" + filename
|
|
5083
6272
|
);
|
|
5084
6273
|
context.spinner?.show(`Saving "${picked.name}" (${picked.content.length} bytes)...`);
|
|
6274
|
+
context.notifier.info(`Saving ${picked.name}`);
|
|
5085
6275
|
try {
|
|
5086
6276
|
fs.writeFile(resolved, picked.content);
|
|
5087
6277
|
await fs.persist();
|
|
@@ -5149,6 +6339,10 @@ var filesModule = {
|
|
|
5149
6339
|
new CliShCommandProcessor(),
|
|
5150
6340
|
new CliUploadCommandProcessor()
|
|
5151
6341
|
],
|
|
6342
|
+
permissions: [
|
|
6343
|
+
{ name: CliFilesPermissions.Admin, description: "Bypass file permission checks" },
|
|
6344
|
+
{ name: CliFilesPermissions.Chown, description: "chmod/chown files owned by others" }
|
|
6345
|
+
],
|
|
5152
6346
|
services: [
|
|
5153
6347
|
{
|
|
5154
6348
|
provide: IFileSystemService_TOKEN,
|
|
@@ -5180,7 +6374,7 @@ var filesModule = {
|
|
|
5180
6374
|
return { ...this, config };
|
|
5181
6375
|
},
|
|
5182
6376
|
async onInit(context) {
|
|
5183
|
-
const fs = context.services.
|
|
6377
|
+
const fs = context.services.getRequired(
|
|
5184
6378
|
IFileSystemService_TOKEN
|
|
5185
6379
|
);
|
|
5186
6380
|
await fs.initialize();
|
|
@@ -5191,7 +6385,7 @@ var filesModule = {
|
|
|
5191
6385
|
}
|
|
5192
6386
|
let dragDrop;
|
|
5193
6387
|
try {
|
|
5194
|
-
dragDrop = context.services.
|
|
6388
|
+
dragDrop = context.services.getRequired(ICliDragDropService_TOKEN);
|
|
5195
6389
|
} catch {
|
|
5196
6390
|
}
|
|
5197
6391
|
if (dragDrop) {
|
|
@@ -5220,13 +6414,25 @@ var filesModule = {
|
|
|
5220
6414
|
});
|
|
5221
6415
|
}
|
|
5222
6416
|
},
|
|
6417
|
+
async onAfterBoot(context) {
|
|
6418
|
+
const fs = context.services.getRequired(IFileSystemService_TOKEN);
|
|
6419
|
+
fs.setAccessPolicy?.(createFileAccessPolicy(context));
|
|
6420
|
+
this._sessionSubscription?.unsubscribe();
|
|
6421
|
+
this._sessionSubscription = followUserSession(context, fs);
|
|
6422
|
+
this._accountsSubscription?.unsubscribe();
|
|
6423
|
+
this._accountsSubscription = followUserAccounts(context, fs);
|
|
6424
|
+
},
|
|
5223
6425
|
async onDestroy(_context) {
|
|
5224
6426
|
const sub = this._dragDropSubscription;
|
|
5225
6427
|
sub?.unsubscribe();
|
|
5226
6428
|
this._dragDropSubscription = void 0;
|
|
6429
|
+
this._sessionSubscription?.unsubscribe();
|
|
6430
|
+
this._sessionSubscription = void 0;
|
|
6431
|
+
this._accountsSubscription?.unsubscribe();
|
|
6432
|
+
this._accountsSubscription = void 0;
|
|
5227
6433
|
}
|
|
5228
6434
|
};
|
|
5229
6435
|
|
|
5230
|
-
export { CliAwkCommandProcessor, CliBasenameCommandProcessor, CliCatCommandProcessor, CliCdCommandProcessor, CliChmodCommandProcessor, CliChownCommandProcessor, CliCpCommandProcessor, CliCutCommandProcessor, CliDiffCommandProcessor, CliDirnameCommandProcessor, CliDuCommandProcessor, CliEchoCommandProcessor, CliFindCommandProcessor, CliGrepCommandProcessor, CliHeadCommandProcessor, CliLnCommandProcessor, CliLsCommandProcessor, CliMkdirCommandProcessor, CliMvCommandProcessor, CliPasteCommandProcessor, CliPwdCommandProcessor, CliRmCommandProcessor, CliRmdirCommandProcessor, CliSedCommandProcessor, CliShCommandProcessor, CliSortCommandProcessor, CliStatCommandProcessor, CliTacCommandProcessor, CliTailCommandProcessor, CliTeeCommandProcessor, CliTouchCommandProcessor, CliTrCommandProcessor, CliTreeCommandProcessor, CliUniqCommandProcessor, CliWcCommandProcessor, CliXargsCommandProcessor, FilePathCompletionProvider, IFileSystemService_TOKEN, IndexedDbFileSystemService, ServerFileSystemService, VirtualFsFileTransferService, executeScript, filesModule };
|
|
6436
|
+
export { ANONYMOUS_OWNERSHIP, CliAwkCommandProcessor, CliBasenameCommandProcessor, CliCatCommandProcessor, CliCdCommandProcessor, CliChmodCommandProcessor, CliChownCommandProcessor, CliCpCommandProcessor, CliCutCommandProcessor, CliDiffCommandProcessor, CliDirnameCommandProcessor, CliDuCommandProcessor, CliEchoCommandProcessor, CliFilesPermissions, CliFindCommandProcessor, CliGrepCommandProcessor, CliHeadCommandProcessor, CliLnCommandProcessor, CliLsCommandProcessor, CliMkdirCommandProcessor, CliMvCommandProcessor, CliPasteCommandProcessor, CliPwdCommandProcessor, CliRmCommandProcessor, CliRmdirCommandProcessor, CliSedCommandProcessor, CliShCommandProcessor, CliSortCommandProcessor, CliStatCommandProcessor, CliTacCommandProcessor, CliTailCommandProcessor, CliTeeCommandProcessor, CliTouchCommandProcessor, CliTrCommandProcessor, CliTreeCommandProcessor, CliUniqCommandProcessor, CliWcCommandProcessor, CliXargsCommandProcessor, FilePathCompletionProvider, HOME_DIR_PERMISSIONS, IFileSystemService_TOKEN, IndexedDbFileSystemService, SYSTEM_OWNERSHIP, ServerFileSystemService, VirtualFsFileTransferService, createFileAccessPolicy, executeScript, filesModule, followUserAccounts, followUserSession };
|
|
5231
6437
|
//# sourceMappingURL=public-api.mjs.map
|
|
5232
6438
|
//# sourceMappingURL=public-api.mjs.map
|