printable-shell-command 0.1.3 → 0.1.4
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/.github/workflows/CI.yaml +14 -0
- package/Makefile +15 -0
- package/package.json +1 -1
- package/src/index.ts +74 -25
- package/test/index.test.ts +152 -0
package/Makefile
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
.PHONY: test
|
|
2
|
+
test: lint test-js
|
|
3
|
+
|
|
1
4
|
.PHONY: lint
|
|
2
5
|
lint: setup
|
|
3
6
|
bun x @biomejs/biome check
|
|
@@ -6,6 +9,10 @@ lint: setup
|
|
|
6
9
|
format: setup
|
|
7
10
|
bun x @biomejs/biome check --write
|
|
8
11
|
|
|
12
|
+
.PHONY: test-js
|
|
13
|
+
test-js: setup
|
|
14
|
+
bun test
|
|
15
|
+
|
|
9
16
|
# https://github.com/lgarron/repo
|
|
10
17
|
REPO_COMMANDS = publish
|
|
11
18
|
|
|
@@ -18,3 +25,11 @@ ${REPO_COMMANDS}:
|
|
|
18
25
|
.PHONY: setup
|
|
19
26
|
setup:
|
|
20
27
|
bun install --frozen-lockfile
|
|
28
|
+
|
|
29
|
+
.PHONY: clean
|
|
30
|
+
clean:
|
|
31
|
+
# No-op
|
|
32
|
+
|
|
33
|
+
.PHONY: reset
|
|
34
|
+
reset: clean
|
|
35
|
+
rm -rf ./node_modules
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
+
const DEFAULT_MAIN_INDENTATION = "";
|
|
2
|
+
const DEFAULT_ARG_INDENTATION = " ";
|
|
3
|
+
const DEFAULT_ARGUMENT_LINE_WRAPPING = "by-entry";
|
|
4
|
+
|
|
1
5
|
const INLINE_SEPARATOR = " ";
|
|
2
|
-
const
|
|
6
|
+
const LINE_WRAP_LINE_END = " \\\n";
|
|
3
7
|
|
|
4
8
|
// biome-ignore lint/suspicious/noExplicitAny: This is the correct type nere.
|
|
5
9
|
function isString(s: any): s is string {
|
|
@@ -21,7 +25,12 @@ export interface PrintOptions {
|
|
|
21
25
|
// - "extra-safe": Quote all arguments, even ones that don't need it. This is
|
|
22
26
|
// more likely to be safe under all circumstances.
|
|
23
27
|
quoting?: "auto" | "extra-safe";
|
|
24
|
-
|
|
28
|
+
// Line wrapping to use between arguments. Defaults to `"by-entry"`.
|
|
29
|
+
argumentLineWrapping?:
|
|
30
|
+
| "by-entry"
|
|
31
|
+
| "nested-by-entry"
|
|
32
|
+
| "by-argument"
|
|
33
|
+
| "inline";
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
// https://mywiki.wooledge.org/BashGuide/SpecialCharacters
|
|
@@ -150,7 +159,7 @@ export class PrintableShellCommand {
|
|
|
150
159
|
#escapeArg(
|
|
151
160
|
arg: string,
|
|
152
161
|
isMainCommand: boolean,
|
|
153
|
-
options
|
|
162
|
+
options: PrintOptions,
|
|
154
163
|
): string {
|
|
155
164
|
const argCharacters = new Set(arg);
|
|
156
165
|
const specialShellCharacters = isMainCommand
|
|
@@ -169,46 +178,86 @@ export class PrintableShellCommand {
|
|
|
169
178
|
return arg;
|
|
170
179
|
}
|
|
171
180
|
|
|
172
|
-
#
|
|
173
|
-
return
|
|
181
|
+
#mainIndentation(options: PrintOptions): string {
|
|
182
|
+
return options?.mainIndentation ?? DEFAULT_MAIN_INDENTATION;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
#argIndentation(options: PrintOptions): string {
|
|
186
|
+
return (
|
|
187
|
+
this.#mainIndentation(options) +
|
|
188
|
+
(options?.argIndentation ?? DEFAULT_ARG_INDENTATION)
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
#lineWrapSeparator(options: PrintOptions): string {
|
|
193
|
+
return LINE_WRAP_LINE_END + this.#argIndentation(options);
|
|
174
194
|
}
|
|
175
195
|
|
|
176
|
-
#
|
|
177
|
-
|
|
196
|
+
#argPairSeparator(options: PrintOptions): string {
|
|
197
|
+
switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
|
|
198
|
+
case "by-entry": {
|
|
199
|
+
return INLINE_SEPARATOR;
|
|
200
|
+
}
|
|
201
|
+
case "nested-by-entry": {
|
|
202
|
+
return this.#lineWrapSeparator(options) + this.#argIndentation(options);
|
|
203
|
+
}
|
|
204
|
+
case "by-argument": {
|
|
205
|
+
return this.#lineWrapSeparator(options);
|
|
206
|
+
}
|
|
207
|
+
case "inline": {
|
|
208
|
+
return INLINE_SEPARATOR;
|
|
209
|
+
}
|
|
210
|
+
default:
|
|
211
|
+
throw new Error("Invalid argument line wrapping argument.");
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
#entrySeparator(options: PrintOptions): string {
|
|
216
|
+
switch (options?.argumentLineWrapping ?? DEFAULT_ARGUMENT_LINE_WRAPPING) {
|
|
217
|
+
case "by-entry": {
|
|
218
|
+
return LINE_WRAP_LINE_END + this.#argIndentation(options);
|
|
219
|
+
}
|
|
220
|
+
case "nested-by-entry": {
|
|
221
|
+
return LINE_WRAP_LINE_END + this.#argIndentation(options);
|
|
222
|
+
}
|
|
223
|
+
case "by-argument": {
|
|
224
|
+
return LINE_WRAP_LINE_END + this.#argIndentation(options);
|
|
225
|
+
}
|
|
226
|
+
case "inline": {
|
|
227
|
+
return INLINE_SEPARATOR;
|
|
228
|
+
}
|
|
229
|
+
default:
|
|
230
|
+
throw new Error("Invalid argument line wrapping argument.");
|
|
231
|
+
}
|
|
178
232
|
}
|
|
179
233
|
|
|
180
234
|
public getPrintableCommand(options?: PrintOptions): string {
|
|
181
|
-
|
|
235
|
+
// TODO: Why in the world does TypeScript not give the `options` arg the type of `PrintOptions | undefined`???
|
|
236
|
+
// biome-ignore lint/style/noParameterAssign: We want a default assignment without affecting the signature.
|
|
237
|
+
options ??= {};
|
|
238
|
+
const serializedEntries: string[] = [];
|
|
182
239
|
|
|
183
|
-
|
|
184
|
-
this.#
|
|
240
|
+
serializedEntries.push(
|
|
241
|
+
this.#mainIndentation(options) +
|
|
185
242
|
this.#escapeArg(this.commandName, true, options),
|
|
186
|
-
options,
|
|
187
|
-
),
|
|
188
243
|
);
|
|
189
244
|
|
|
190
|
-
// let pendingNewlineAfterPart = options?.separateLines === "dash-heuristic";
|
|
191
245
|
for (let i = 0; i < this.args.length; i++) {
|
|
192
246
|
const argsEntry = this.args[i];
|
|
193
247
|
|
|
194
248
|
if (isString(argsEntry)) {
|
|
195
|
-
|
|
196
|
-
this.#bigIndent(this.#escapeArg(argsEntry, false, options), options),
|
|
197
|
-
);
|
|
249
|
+
serializedEntries.push(this.#escapeArg(argsEntry, false, options));
|
|
198
250
|
} else {
|
|
199
251
|
const [part1, part2] = argsEntry;
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
this.#escapeArg(
|
|
204
|
-
INLINE_SEPARATOR +
|
|
205
|
-
this.#escapeArg(part2, false, options),
|
|
206
|
-
options,
|
|
207
|
-
),
|
|
252
|
+
serializedEntries.push(
|
|
253
|
+
this.#escapeArg(part1, false, options) +
|
|
254
|
+
this.#argPairSeparator(options) +
|
|
255
|
+
this.#escapeArg(part2, false, options),
|
|
208
256
|
);
|
|
209
257
|
}
|
|
210
258
|
}
|
|
211
|
-
|
|
259
|
+
|
|
260
|
+
return serializedEntries.join(this.#entrySeparator(options));
|
|
212
261
|
}
|
|
213
262
|
|
|
214
263
|
public print(options?: PrintOptions): void {
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { expect, test } from "bun:test";
|
|
2
|
+
import { PrintableShellCommand } from "../src";
|
|
3
|
+
|
|
4
|
+
const rsyncCommand = new PrintableShellCommand("rsync", [
|
|
5
|
+
"-avz",
|
|
6
|
+
["--exclude", ".DS_Store"],
|
|
7
|
+
["--exclude", ".git"],
|
|
8
|
+
"./dist/web/experiments.cubing.net/test/deploy/",
|
|
9
|
+
"experiments.cubing.net:~/experiments.cubing.net/test/deploy/",
|
|
10
|
+
]);
|
|
11
|
+
|
|
12
|
+
test("args for commands", () => {
|
|
13
|
+
expect(rsyncCommand.toCommandWithFlatArgs()).toEqual([
|
|
14
|
+
"rsync",
|
|
15
|
+
[
|
|
16
|
+
"-avz",
|
|
17
|
+
"--exclude",
|
|
18
|
+
".DS_Store",
|
|
19
|
+
"--exclude",
|
|
20
|
+
".git",
|
|
21
|
+
"./dist/web/experiments.cubing.net/test/deploy/",
|
|
22
|
+
"experiments.cubing.net:~/experiments.cubing.net/test/deploy/",
|
|
23
|
+
],
|
|
24
|
+
]);
|
|
25
|
+
expect(rsyncCommand.toFlatCommand()).toEqual([
|
|
26
|
+
"rsync",
|
|
27
|
+
"-avz",
|
|
28
|
+
"--exclude",
|
|
29
|
+
".DS_Store",
|
|
30
|
+
"--exclude",
|
|
31
|
+
".git",
|
|
32
|
+
"./dist/web/experiments.cubing.net/test/deploy/",
|
|
33
|
+
"experiments.cubing.net:~/experiments.cubing.net/test/deploy/",
|
|
34
|
+
]);
|
|
35
|
+
expect(rsyncCommand.toCommandWithFlatArgs()).toEqual(rsyncCommand.forNode());
|
|
36
|
+
expect(rsyncCommand.toFlatCommand()).toEqual(rsyncCommand.forBun());
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test("default formatting", () => {
|
|
40
|
+
expect(rsyncCommand.getPrintableCommand()).toEqual(
|
|
41
|
+
`rsync \\
|
|
42
|
+
-avz \\
|
|
43
|
+
--exclude .DS_Store \\
|
|
44
|
+
--exclude .git \\
|
|
45
|
+
./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
46
|
+
experiments.cubing.net:~/experiments.cubing.net/test/deploy/`,
|
|
47
|
+
);
|
|
48
|
+
expect(
|
|
49
|
+
rsyncCommand.getPrintableCommand({
|
|
50
|
+
quoting: "auto",
|
|
51
|
+
argumentLineWrapping: "by-entry",
|
|
52
|
+
}),
|
|
53
|
+
).toEqual(rsyncCommand.getPrintableCommand());
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("extra-safe quoting", () => {
|
|
57
|
+
expect(rsyncCommand.getPrintableCommand({ quoting: "extra-safe" })).toEqual(
|
|
58
|
+
`'rsync' \\
|
|
59
|
+
'-avz' \\
|
|
60
|
+
'--exclude' '.DS_Store' \\
|
|
61
|
+
'--exclude' '.git' \\
|
|
62
|
+
'./dist/web/experiments.cubing.net/test/deploy/' \\
|
|
63
|
+
'experiments.cubing.net:~/experiments.cubing.net/test/deploy/'`,
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("indentation", () => {
|
|
68
|
+
expect(
|
|
69
|
+
rsyncCommand.getPrintableCommand({ argIndentation: "\t \t" }),
|
|
70
|
+
).toEqual(
|
|
71
|
+
`rsync \\
|
|
72
|
+
-avz \\
|
|
73
|
+
--exclude .DS_Store \\
|
|
74
|
+
--exclude .git \\
|
|
75
|
+
./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
76
|
+
experiments.cubing.net:~/experiments.cubing.net/test/deploy/`,
|
|
77
|
+
);
|
|
78
|
+
expect(rsyncCommand.getPrintableCommand({ argIndentation: "↪ " })).toEqual(
|
|
79
|
+
`rsync \\
|
|
80
|
+
↪ -avz \\
|
|
81
|
+
↪ --exclude .DS_Store \\
|
|
82
|
+
↪ --exclude .git \\
|
|
83
|
+
↪ ./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
84
|
+
↪ experiments.cubing.net:~/experiments.cubing.net/test/deploy/`,
|
|
85
|
+
);
|
|
86
|
+
expect(rsyncCommand.getPrintableCommand({ mainIndentation: " " })).toEqual(
|
|
87
|
+
` rsync \\
|
|
88
|
+
-avz \\
|
|
89
|
+
--exclude .DS_Store \\
|
|
90
|
+
--exclude .git \\
|
|
91
|
+
./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
92
|
+
experiments.cubing.net:~/experiments.cubing.net/test/deploy/`,
|
|
93
|
+
);
|
|
94
|
+
expect(
|
|
95
|
+
rsyncCommand.getPrintableCommand({
|
|
96
|
+
mainIndentation: "🙈",
|
|
97
|
+
argIndentation: "🙉",
|
|
98
|
+
}),
|
|
99
|
+
).toEqual(
|
|
100
|
+
`🙈rsync \\
|
|
101
|
+
🙈🙉-avz \\
|
|
102
|
+
🙈🙉--exclude .DS_Store \\
|
|
103
|
+
🙈🙉--exclude .git \\
|
|
104
|
+
🙈🙉./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
105
|
+
🙈🙉experiments.cubing.net:~/experiments.cubing.net/test/deploy/`,
|
|
106
|
+
);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("line wrapping", () => {
|
|
110
|
+
expect(
|
|
111
|
+
rsyncCommand.getPrintableCommand({ argumentLineWrapping: "by-entry" }),
|
|
112
|
+
).toEqual(rsyncCommand.getPrintableCommand());
|
|
113
|
+
expect(
|
|
114
|
+
rsyncCommand.getPrintableCommand({
|
|
115
|
+
argumentLineWrapping: "nested-by-entry",
|
|
116
|
+
}),
|
|
117
|
+
).toEqual(`rsync \\
|
|
118
|
+
-avz \\
|
|
119
|
+
--exclude \\
|
|
120
|
+
.DS_Store \\
|
|
121
|
+
--exclude \\
|
|
122
|
+
.git \\
|
|
123
|
+
./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
124
|
+
experiments.cubing.net:~/experiments.cubing.net/test/deploy/`);
|
|
125
|
+
expect(
|
|
126
|
+
rsyncCommand.getPrintableCommand({ argumentLineWrapping: "by-argument" }),
|
|
127
|
+
).toEqual(`rsync \\
|
|
128
|
+
-avz \\
|
|
129
|
+
--exclude \\
|
|
130
|
+
.DS_Store \\
|
|
131
|
+
--exclude \\
|
|
132
|
+
.git \\
|
|
133
|
+
./dist/web/experiments.cubing.net/test/deploy/ \\
|
|
134
|
+
experiments.cubing.net:~/experiments.cubing.net/test/deploy/`);
|
|
135
|
+
expect(
|
|
136
|
+
rsyncCommand.getPrintableCommand({
|
|
137
|
+
argumentLineWrapping: "inline",
|
|
138
|
+
}),
|
|
139
|
+
).toEqual(
|
|
140
|
+
"rsync -avz --exclude .DS_Store --exclude .git ./dist/web/experiments.cubing.net/test/deploy/ experiments.cubing.net:~/experiments.cubing.net/test/deploy/",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("command with space is escaped by default", () => {
|
|
145
|
+
const command = new PrintableShellCommand(
|
|
146
|
+
"/Applications/My App.app/Contents/Resources/my-app",
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
expect(command.getPrintableCommand()).toEqual(
|
|
150
|
+
`'/Applications/My App.app/Contents/Resources/my-app'`,
|
|
151
|
+
);
|
|
152
|
+
});
|