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.
@@ -0,0 +1,14 @@
1
+ name: CI
2
+
3
+ on: [push, pull_request]
4
+
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+
9
+ steps:
10
+ - uses: actions/checkout@v4
11
+ - uses: oven-sh/setup-bun@v1
12
+ - run: make setup
13
+ - run: make lint
14
+ - run: make test-js
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "printable-shell-command",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "main": "./src/index.ts",
5
5
  "type": "module",
6
6
  "exports": {
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 LINE_WRAP_SEPARATOR = " \\\n ";
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
- lineWrap?: "none" | "by-entry";
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?: PrintOptions,
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
- #smallIndent(s: string, options?: PrintOptions): string {
173
- return (options?.mainIndentation ?? "") + s;
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
- #bigIndent(s: string, options?: PrintOptions): string {
177
- return this.#smallIndent((options?.argIndentation ?? " ") + s, options);
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
- const lines: string[] = [];
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
- lines.push(
184
- this.#smallIndent(
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
- lines.push(
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
- lines.push(
202
- this.#bigIndent(
203
- this.#escapeArg(part1, false, options) +
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
- return lines.join(LINE_WRAP_SEPARATOR);
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
+ });