@taliesinb/mash 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +275 -2
- package/bin/mash.ts +253 -0
- package/dist/bin/mash.d.ts +39 -0
- package/dist/bin/mash.d.ts.map +1 -0
- package/dist/bin/mash.js +281 -0
- package/dist/bin/mash.js.map +1 -0
- package/dist/src/analysis/analyze.d.ts +97 -0
- package/dist/src/analysis/analyze.d.ts.map +1 -0
- package/dist/src/analysis/analyze.js +1406 -0
- package/dist/src/analysis/analyze.js.map +1 -0
- package/dist/src/analysis/fs.d.ts +39 -0
- package/dist/src/analysis/fs.d.ts.map +1 -0
- package/dist/src/analysis/fs.js +117 -0
- package/dist/src/analysis/fs.js.map +1 -0
- package/dist/src/analysis/outputs.d.ts +16 -0
- package/dist/src/analysis/outputs.d.ts.map +1 -0
- package/dist/src/analysis/outputs.js +277 -0
- package/dist/src/analysis/outputs.js.map +1 -0
- package/dist/src/analysis/value.d.ts +76 -0
- package/dist/src/analysis/value.d.ts.map +1 -0
- package/dist/src/analysis/value.js +248 -0
- package/dist/src/analysis/value.js.map +1 -0
- package/dist/src/behaviors/defs/core.d.ts +15 -0
- package/dist/src/behaviors/defs/core.d.ts.map +1 -0
- package/dist/src/behaviors/defs/core.js +1084 -0
- package/dist/src/behaviors/defs/core.js.map +1 -0
- package/dist/src/behaviors/defs/devtools.d.ts +9 -0
- package/dist/src/behaviors/defs/devtools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/devtools.js +432 -0
- package/dist/src/behaviors/defs/devtools.js.map +1 -0
- package/dist/src/behaviors/defs/macos.d.ts +12 -0
- package/dist/src/behaviors/defs/macos.d.ts.map +1 -0
- package/dist/src/behaviors/defs/macos.js +454 -0
- package/dist/src/behaviors/defs/macos.js.map +1 -0
- package/dist/src/behaviors/defs/tools.d.ts +9 -0
- package/dist/src/behaviors/defs/tools.d.ts.map +1 -0
- package/dist/src/behaviors/defs/tools.js +639 -0
- package/dist/src/behaviors/defs/tools.js.map +1 -0
- package/dist/src/behaviors/dsl.d.ts +95 -0
- package/dist/src/behaviors/dsl.d.ts.map +1 -0
- package/dist/src/behaviors/dsl.js +267 -0
- package/dist/src/behaviors/dsl.js.map +1 -0
- package/dist/src/behaviors/index.d.ts +12 -0
- package/dist/src/behaviors/index.d.ts.map +1 -0
- package/dist/src/behaviors/index.js +18 -0
- package/dist/src/behaviors/index.js.map +1 -0
- package/dist/src/behaviors/registry.d.ts +45 -0
- package/dist/src/behaviors/registry.d.ts.map +1 -0
- package/dist/src/behaviors/registry.js +266 -0
- package/dist/src/behaviors/registry.js.map +1 -0
- package/dist/src/behaviors/roles.d.ts +29 -0
- package/dist/src/behaviors/roles.d.ts.map +1 -0
- package/dist/src/behaviors/roles.js +198 -0
- package/dist/src/behaviors/roles.js.map +1 -0
- package/dist/src/behaviors/types.d.ts +120 -0
- package/dist/src/behaviors/types.d.ts.map +1 -0
- package/dist/src/behaviors/types.js +12 -0
- package/dist/src/behaviors/types.js.map +1 -0
- package/dist/src/index.d.ts +27 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +24 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/integrations/deepsea.d.ts +121 -0
- package/dist/src/integrations/deepsea.d.ts.map +1 -0
- package/dist/src/integrations/deepsea.js +199 -0
- package/dist/src/integrations/deepsea.js.map +1 -0
- package/dist/src/machine.d.ts +139 -0
- package/dist/src/machine.d.ts.map +1 -0
- package/dist/src/machine.js +400 -0
- package/dist/src/machine.js.map +1 -0
- package/dist/src/parser/ast.d.ts +369 -0
- package/dist/src/parser/ast.d.ts.map +1 -0
- package/dist/src/parser/ast.js +17 -0
- package/dist/src/parser/ast.js.map +1 -0
- package/dist/src/parser/parser.d.ts +48 -0
- package/dist/src/parser/parser.d.ts.map +1 -0
- package/dist/src/parser/parser.js +1950 -0
- package/dist/src/parser/parser.js.map +1 -0
- package/dist/src/policy/effects.d.ts +86 -0
- package/dist/src/policy/effects.d.ts.map +1 -0
- package/dist/src/policy/effects.js +53 -0
- package/dist/src/policy/effects.js.map +1 -0
- package/dist/src/policy/policy.d.ts +130 -0
- package/dist/src/policy/policy.d.ts.map +1 -0
- package/dist/src/policy/policy.js +532 -0
- package/dist/src/policy/policy.js.map +1 -0
- package/dist/src/printer/doc.d.ts +151 -0
- package/dist/src/printer/doc.d.ts.map +1 -0
- package/dist/src/printer/doc.js +409 -0
- package/dist/src/printer/doc.js.map +1 -0
- package/dist/src/printer/highlight.d.ts +53 -0
- package/dist/src/printer/highlight.d.ts.map +1 -0
- package/dist/src/printer/highlight.js +339 -0
- package/dist/src/printer/highlight.js.map +1 -0
- package/dist/src/printer/html.d.ts +72 -0
- package/dist/src/printer/html.d.ts.map +1 -0
- package/dist/src/printer/html.js +423 -0
- package/dist/src/printer/html.js.map +1 -0
- package/dist/src/printer/print.d.ts +63 -0
- package/dist/src/printer/print.d.ts.map +1 -0
- package/dist/src/printer/print.js +484 -0
- package/dist/src/printer/print.js.map +1 -0
- package/dist/src/printer/summary.d.ts +50 -0
- package/dist/src/printer/summary.d.ts.map +1 -0
- package/dist/src/printer/summary.js +621 -0
- package/dist/src/printer/summary.js.map +1 -0
- package/dist/src/trace/events.d.ts +183 -0
- package/dist/src/trace/events.d.ts.map +1 -0
- package/dist/src/trace/events.js +9 -0
- package/dist/src/trace/events.js.map +1 -0
- package/dist/src/trace/progress.d.ts +58 -0
- package/dist/src/trace/progress.d.ts.map +1 -0
- package/dist/src/trace/progress.js +83 -0
- package/dist/src/trace/progress.js.map +1 -0
- package/dist/src/vm/arith.d.ts +29 -0
- package/dist/src/vm/arith.d.ts.map +1 -0
- package/dist/src/vm/arith.js +258 -0
- package/dist/src/vm/arith.js.map +1 -0
- package/dist/src/vm/builtins.d.ts +31 -0
- package/dist/src/vm/builtins.d.ts.map +1 -0
- package/dist/src/vm/builtins.js +1773 -0
- package/dist/src/vm/builtins.js.map +1 -0
- package/dist/src/vm/emulate.d.ts +75 -0
- package/dist/src/vm/emulate.d.ts.map +1 -0
- package/dist/src/vm/emulate.js +380 -0
- package/dist/src/vm/emulate.js.map +1 -0
- package/dist/src/vm/expand.d.ts +68 -0
- package/dist/src/vm/expand.d.ts.map +1 -0
- package/dist/src/vm/expand.js +744 -0
- package/dist/src/vm/expand.js.map +1 -0
- package/dist/src/vm/external.d.ts +34 -0
- package/dist/src/vm/external.d.ts.map +1 -0
- package/dist/src/vm/external.js +228 -0
- package/dist/src/vm/external.js.map +1 -0
- package/dist/src/vm/fifo.d.ts +66 -0
- package/dist/src/vm/fifo.d.ts.map +1 -0
- package/dist/src/vm/fifo.js +232 -0
- package/dist/src/vm/fifo.js.map +1 -0
- package/dist/src/vm/interp.d.ts +209 -0
- package/dist/src/vm/interp.d.ts.map +1 -0
- package/dist/src/vm/interp.js +1641 -0
- package/dist/src/vm/interp.js.map +1 -0
- package/dist/src/vm/io.d.ts +180 -0
- package/dist/src/vm/io.d.ts.map +1 -0
- package/dist/src/vm/io.js +502 -0
- package/dist/src/vm/io.js.map +1 -0
- package/dist/src/vm/pattern.d.ts +46 -0
- package/dist/src/vm/pattern.d.ts.map +1 -0
- package/dist/src/vm/pattern.js +498 -0
- package/dist/src/vm/pattern.js.map +1 -0
- package/dist/src/vm/proc.d.ts +94 -0
- package/dist/src/vm/proc.d.ts.map +1 -0
- package/dist/src/vm/proc.js +130 -0
- package/dist/src/vm/proc.js.map +1 -0
- package/dist/src/vm/state.d.ts +104 -0
- package/dist/src/vm/state.d.ts.map +1 -0
- package/dist/src/vm/state.js +233 -0
- package/dist/src/vm/state.js.map +1 -0
- package/dist/src/vm/test.d.ts +11 -0
- package/dist/src/vm/test.d.ts.map +1 -0
- package/dist/src/vm/test.js +277 -0
- package/dist/src/vm/test.js.map +1 -0
- package/package.json +63 -4
- package/src/analysis/analyze.ts +1395 -0
- package/src/analysis/fs.ts +121 -0
- package/src/analysis/outputs.ts +242 -0
- package/src/analysis/value.ts +264 -0
- package/src/behaviors/defs/core.ts +970 -0
- package/src/behaviors/defs/devtools.ts +400 -0
- package/src/behaviors/defs/macos.ts +426 -0
- package/src/behaviors/defs/tools.ts +596 -0
- package/src/behaviors/dsl.ts +293 -0
- package/src/behaviors/index.ts +19 -0
- package/src/behaviors/registry.ts +261 -0
- package/src/behaviors/roles.ts +187 -0
- package/src/behaviors/types.ts +127 -0
- package/src/index.ts +27 -0
- package/src/integrations/deepsea.ts +259 -0
- package/src/machine.ts +483 -0
- package/src/parser/ast.ts +472 -0
- package/src/parser/parser.ts +1826 -0
- package/src/policy/effects.ts +102 -0
- package/src/policy/policy.ts +578 -0
- package/src/printer/doc.ts +454 -0
- package/src/printer/highlight.ts +283 -0
- package/src/printer/html.ts +441 -0
- package/src/printer/print.ts +528 -0
- package/src/printer/summary.ts +601 -0
- package/src/trace/events.ts +102 -0
- package/src/trace/progress.ts +119 -0
- package/src/vm/arith.ts +236 -0
- package/src/vm/builtins.ts +1590 -0
- package/src/vm/emulate.ts +337 -0
- package/src/vm/expand.ts +721 -0
- package/src/vm/external.ts +265 -0
- package/src/vm/fifo.ts +226 -0
- package/src/vm/interp.ts +1623 -0
- package/src/vm/io.ts +570 -0
- package/src/vm/pattern.ts +477 -0
- package/src/vm/proc.ts +147 -0
- package/src/vm/state.ts +267 -0
- package/src/vm/test.ts +253 -0
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pretty-printer: renders a parsed script as readable bash that parses to the
|
|
3
|
+
* same AST (see docs/pretty-printer.md).
|
|
4
|
+
*
|
|
5
|
+
* Words, assignment values, redirections, here-doc bodies, arithmetic and
|
|
6
|
+
* `[[ ]]` expressions are emitted as their exact source text; only the layout
|
|
7
|
+
* (newlines, ` \` continuations, indentation) is regenerated.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type * as A from '../parser/ast.ts'
|
|
11
|
+
import { parse, literalOf } from '../parser/parser.ts'
|
|
12
|
+
import type { ArgRole } from '../behaviors/types.ts'
|
|
13
|
+
import { simpleCommandRoles } from './highlight.ts'
|
|
14
|
+
import {
|
|
15
|
+
type AtomInfo, type Doc, blankLine, compoundGroup, cont, contForced, fill, group, hard, heredoc, indent, join, mark, nl, render, src, suffix, tok,
|
|
16
|
+
} from './doc.ts'
|
|
17
|
+
|
|
18
|
+
export interface FormatOptions {
|
|
19
|
+
/** Target line width (default 80). Single words longer than this overflow. */
|
|
20
|
+
width?: number
|
|
21
|
+
/** Indentation step (default 2). Wrapped arguments are indented twice this. */
|
|
22
|
+
indent?: number
|
|
23
|
+
/** Where `|`, `&&`, `||` go when a chain is broken: end of line (default) or start (with `\` continuations). */
|
|
24
|
+
operators?: 'trailing' | 'leading'
|
|
25
|
+
/**
|
|
26
|
+
* Prefix assignments (`FOO=1 BAR=2 cmd`): `auto` (default) puts each on its
|
|
27
|
+
* own line when there are two or more, or when the command does not fit;
|
|
28
|
+
* `always` does so for any number; `inline` only when it does not fit.
|
|
29
|
+
*/
|
|
30
|
+
assignments?: 'auto' | 'always' | 'inline'
|
|
31
|
+
/** Compound commands: `auto` (default) keeps short ones on one line when they fit; `expanded` always expands. */
|
|
32
|
+
compounds?: 'auto' | 'expanded'
|
|
33
|
+
/** Reformat multi-line `$(…)` / `<(…)` bodies inside words (default false: verbatim). */
|
|
34
|
+
formatSubstitutions?: boolean
|
|
35
|
+
/** Keep at most this many blank lines between statements (default 1; 0 drops them). */
|
|
36
|
+
blankLines?: number
|
|
37
|
+
/** `function` keyword: normalize to `name()` (default) or preserve the original header. */
|
|
38
|
+
functionKeyword?: 'normalize' | 'preserve'
|
|
39
|
+
/**
|
|
40
|
+
* Top-level statements that share a source line (`a; b; c`). bash reads a
|
|
41
|
+
* script line by line: an expansion error (bad arithmetic, readonly
|
|
42
|
+
* assignment) discards the rest of the *logical* line, and an alias is only
|
|
43
|
+
* active from the next line. So:
|
|
44
|
+
* - `continuation` (default): one per line joined by `; \` — exactly the same behavior;
|
|
45
|
+
* - `newline`: plain separate lines (differs only after such an error);
|
|
46
|
+
* - `preserve`: keep them on one line.
|
|
47
|
+
* Inside compound commands and `$(…)` layout never matters (they are parsed whole).
|
|
48
|
+
*/
|
|
49
|
+
statements?: 'continuation' | 'newline' | 'preserve'
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface FormatResult {
|
|
53
|
+
text: string
|
|
54
|
+
/** Output range of every statement and command, by AST node id. */
|
|
55
|
+
map: Map<number, { start: number; end: number }>
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Options with defaults applied. */
|
|
59
|
+
export type Opts = Required<FormatOptions>
|
|
60
|
+
|
|
61
|
+
const DEFAULTS: Opts = {
|
|
62
|
+
width: 80, indent: 2, operators: 'trailing', assignments: 'auto', compounds: 'auto',
|
|
63
|
+
formatSubstitutions: false, blankLines: 1, functionKeyword: 'normalize', statements: 'continuation',
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Parse and pretty-print `source`. Throws ParseError on invalid input. */
|
|
67
|
+
export function format(source: string, options: FormatOptions = {}): FormatResult {
|
|
68
|
+
return printScript(parse(source), source, options)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Pretty-print an already parsed script; `source` must be the text it was parsed from. */
|
|
72
|
+
export function printScript(script: A.Script, source: string, options: FormatOptions = {}): FormatResult {
|
|
73
|
+
const { doc, opts } = buildDoc(script, source, options)
|
|
74
|
+
const r = render(doc, opts.width, { source })
|
|
75
|
+
return { text: r.text, map: r.map }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The layout document for a script (shared by the text and HTML renderers). */
|
|
79
|
+
export function buildDoc(script: A.Script, source: string, options: FormatOptions = {}): { doc: Doc; opts: Opts } {
|
|
80
|
+
const opts = { ...DEFAULTS, ...options }
|
|
81
|
+
const p = new Printer(source, script.comments ?? [], opts)
|
|
82
|
+
p.lineHazard = usesLineSensitiveBuiltins(script)
|
|
83
|
+
const doc = p.topLevel(script)
|
|
84
|
+
const leftover = p.remainingComments()
|
|
85
|
+
return { doc: leftover.length ? [doc, hard, join(hard, leftover)] : doc, opts }
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** `alias` and `shopt` change how *later lines* are parsed: never split their lines (docs/pretty-printer.md §5). */
|
|
89
|
+
function usesLineSensitiveBuiltins(script: A.Script): boolean {
|
|
90
|
+
let found = false
|
|
91
|
+
const visit = (n: unknown): void => {
|
|
92
|
+
if (found || n === null || typeof n !== 'object') return
|
|
93
|
+
if (Array.isArray(n)) {
|
|
94
|
+
for (const x of n) visit(x)
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
const node = n as { type?: string; words?: (A.Word | A.Assign)[] }
|
|
98
|
+
if (node.type === 'SimpleCommand') {
|
|
99
|
+
const w = node.words?.[0]
|
|
100
|
+
const name = w && w.type === 'Word' ? literalOf(w) : null
|
|
101
|
+
if (name === 'alias' || name === 'shopt' || name === 'unalias') found = true
|
|
102
|
+
}
|
|
103
|
+
for (const [k, v] of Object.entries(node)) if (k !== 'span' && v && typeof v === 'object') visit(v)
|
|
104
|
+
}
|
|
105
|
+
visit(script.body)
|
|
106
|
+
return found
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
type Body = A.Stmt[]
|
|
110
|
+
|
|
111
|
+
const kw = (text: string): Doc => tok(text, 'kw')
|
|
112
|
+
const op = (text: string): Doc => tok(text, 'op')
|
|
113
|
+
|
|
114
|
+
/** The plain text of a simple document (src/tok/string), or null. */
|
|
115
|
+
function plain(d: Doc, source: string): string | null {
|
|
116
|
+
if (typeof d === 'string') return d
|
|
117
|
+
if (Array.isArray(d)) return null
|
|
118
|
+
if (d.t === 'src') return source.slice(d.start, d.end)
|
|
119
|
+
if (d.t === 'tok') return d.text
|
|
120
|
+
return null
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
class Printer {
|
|
124
|
+
readonly src: string
|
|
125
|
+
readonly opts: Opts
|
|
126
|
+
lineHazard = false
|
|
127
|
+
private readonly comments: A.Comment[]
|
|
128
|
+
private readonly used: boolean[]
|
|
129
|
+
|
|
130
|
+
constructor(source: string, comments: A.Comment[], opts: Opts) {
|
|
131
|
+
this.src = source
|
|
132
|
+
this.comments = comments
|
|
133
|
+
this.used = comments.map(() => false)
|
|
134
|
+
this.opts = opts
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Verbatim source, consuming the comments inside it. */
|
|
138
|
+
private verbatim(span: A.Span, atom?: AtomInfo, cls?: string): Doc {
|
|
139
|
+
this.consume(span)
|
|
140
|
+
return src(span.start, span.end, atom, cls)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private commentDoc(c: A.Comment): Doc {
|
|
144
|
+
const i = this.comments.indexOf(c)
|
|
145
|
+
return src(c.span.start, c.span.end, { id: -(i + 1), kind: 'cm', weight: 0.8 }, 'cm')
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// -------------------------------------------------------------------------
|
|
149
|
+
// Comments
|
|
150
|
+
// -------------------------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
/** Mark comments inside a verbatim span as printed (they are part of the text). */
|
|
153
|
+
private consume(span: A.Span): void {
|
|
154
|
+
this.comments.forEach((c, i) => {
|
|
155
|
+
if (!this.used[i] && c.span.start >= span.start && c.span.end <= span.end) this.used[i] = true
|
|
156
|
+
})
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Unprinted comments that start before `offset`, in order (marked printed). */
|
|
160
|
+
private take(offset: number): A.Comment[] {
|
|
161
|
+
const out: A.Comment[] = []
|
|
162
|
+
this.comments.forEach((c, i) => {
|
|
163
|
+
if (!this.used[i] && c.span.start < offset) {
|
|
164
|
+
this.used[i] = true
|
|
165
|
+
out.push(c)
|
|
166
|
+
}
|
|
167
|
+
})
|
|
168
|
+
return out
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** The unprinted comment that follows `end` on the same source line, if any. */
|
|
172
|
+
private trailing(end: number, limit: number): A.Comment | null {
|
|
173
|
+
for (let i = 0; i < this.comments.length; i++) {
|
|
174
|
+
const c = this.comments[i]
|
|
175
|
+
if (this.used[i] || c.span.start < end || c.span.start >= limit) continue
|
|
176
|
+
if (this.src.slice(end, c.span.start).includes('\n')) return null
|
|
177
|
+
this.used[i] = true
|
|
178
|
+
return c
|
|
179
|
+
}
|
|
180
|
+
return null
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
private trailingDoc(c: A.Comment): Doc {
|
|
184
|
+
return suffix([' ', this.commentDoc(c)])
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
remainingComments(): Doc[] {
|
|
188
|
+
return this.take(Infinity).map((c) => this.commentDoc(c))
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// -------------------------------------------------------------------------
|
|
192
|
+
// Statement lists
|
|
193
|
+
// -------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
topLevel(script: A.Script): Doc {
|
|
196
|
+
return this.list(script.body, script.span.end, true, true)
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* A statement list. `alwaysBreak` for the script top level; otherwise the
|
|
201
|
+
* separators are `nl` breaks that print `; ` when the enclosing group fits.
|
|
202
|
+
*/
|
|
203
|
+
private list(stmts: Body, endOffset: number, alwaysBreak: boolean, top = false): Doc {
|
|
204
|
+
const parts: Doc[] = []
|
|
205
|
+
stmts.forEach((s, i) => {
|
|
206
|
+
const leading = this.take(s.span.start)
|
|
207
|
+
if (i > 0) {
|
|
208
|
+
const prev = stmts[i - 1]
|
|
209
|
+
const between = this.src.slice(prev.span.end, s.span.start)
|
|
210
|
+
// the same *logical* line: backslash-newline continuations do not end it
|
|
211
|
+
const sameLine = !between.replace(/\\\n/g, '').includes('\n')
|
|
212
|
+
if (top && sameLine && leading.length === 0 && this.opts.statements === 'continuation') {
|
|
213
|
+
parts.push(prev.background ? '' : op(';'), contForced)
|
|
214
|
+
} else if (sameLine && leading.length === 0 && ((top && this.opts.statements === 'preserve') || (this.lineHazard && this.opts.statements === 'newline'))) {
|
|
215
|
+
parts.push(prev.background ? ' ' : [op(';'), ' '])
|
|
216
|
+
} else {
|
|
217
|
+
const blank = this.opts.blankLines > 0 && /\n[ \t]*\n/.test(between)
|
|
218
|
+
if (alwaysBreak || leading.length > 0) parts.push(blank ? blankLine : hard)
|
|
219
|
+
else parts.push(nl(prev.background ? ' ' : '; ', blank))
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const c of leading) parts.push(this.commentDoc(c), hard)
|
|
223
|
+
parts.push(this.stmt(s))
|
|
224
|
+
const next = stmts[i + 1]
|
|
225
|
+
const t = this.trailing(s.span.end, next ? next.span.start : endOffset)
|
|
226
|
+
if (t) parts.push(this.trailingDoc(t))
|
|
227
|
+
})
|
|
228
|
+
return parts
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Does a body qualify to stay on one line (`compounds: 'auto'`)? */
|
|
232
|
+
private simpleBody(body: Body): boolean {
|
|
233
|
+
if (this.opts.compounds === 'expanded' || body.length !== 1) return false
|
|
234
|
+
const c = body[0].command
|
|
235
|
+
return c.type === 'SimpleCommand' || c.type === 'Pipeline' || c.type === 'AndOr'
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private stmt(s: A.Stmt): Doc {
|
|
239
|
+
return mark(s.id, [this.command(s.command), s.background ? [' ', op('&')] : ''])
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// -------------------------------------------------------------------------
|
|
243
|
+
// Commands
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
|
|
246
|
+
private command(c: A.Command): Doc {
|
|
247
|
+
return mark(c.id, this.commandInner(c))
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
private commandInner(c: A.Command): Doc {
|
|
251
|
+
switch (c.type) {
|
|
252
|
+
case 'SimpleCommand': return this.simple(c)
|
|
253
|
+
case 'Pipeline': return this.pipeline(c)
|
|
254
|
+
case 'AndOr': return this.andOr(c)
|
|
255
|
+
case 'FunctionDef': return this.functionDef(c)
|
|
256
|
+
default: return [this.compound(c), this.compoundRedirs(c)]
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Truncation preference for an argument word: strings and substitutions first. */
|
|
261
|
+
/**
|
|
262
|
+
* Truncation preference for an argument word: strings and substitutions
|
|
263
|
+
* first, then paths (shortened from the left), then plain words. Command
|
|
264
|
+
* names, flags and subcommands are never shortened.
|
|
265
|
+
*/
|
|
266
|
+
private wordAtom(w: A.Word, role: 'cmd' | 'arg', argRole?: ArgRole): AtomInfo | undefined {
|
|
267
|
+
if (role === 'cmd' || argRole === 'cmd' || argRole === 'subcommand') return undefined
|
|
268
|
+
const text = this.src.slice(w.span.start, w.span.end)
|
|
269
|
+
if (/^-/.test(text) && !/[=\s]/.test(text)) return undefined
|
|
270
|
+
if ((argRole === 'file' || argRole === 'dir') && text.includes('/') && !text.includes('\n')) return { id: w.id, kind: 'word', weight: 0.9, tail: true }
|
|
271
|
+
const rich = w.parts.some((p) => p.type !== 'Lit' && p.type !== 'Escaped')
|
|
272
|
+
return { id: w.id, kind: 'word', weight: rich ? 1 : 0.6 }
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private wordDoc(w: A.Word, role: 'cmd' | 'arg' = 'arg', argRole?: ArgRole): Doc {
|
|
276
|
+
if (this.opts.formatSubstitutions) {
|
|
277
|
+
const d = this.wordWithSubstitutions(w)
|
|
278
|
+
if (d) return d
|
|
279
|
+
}
|
|
280
|
+
return this.verbatim(w.span, this.wordAtom(w, role, argRole))
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** Words containing multi-line `$(…)` / `<(…)`: format their bodies, keep the rest verbatim. */
|
|
284
|
+
private wordWithSubstitutions(w: A.Word): Doc | null {
|
|
285
|
+
const substs: (A.CmdSubst | A.ProcSubst)[] = []
|
|
286
|
+
const visit = (parts: A.WordPart[]): void => {
|
|
287
|
+
for (const p of parts) {
|
|
288
|
+
if ((p.type === 'CmdSubst' && !p.backquote) || p.type === 'ProcSubst') {
|
|
289
|
+
if (this.src.slice(p.span.start, p.span.end).includes('\n') && p.body.length > 0) substs.push(p)
|
|
290
|
+
} else if (p.type === 'DblQuoted') visit(p.parts)
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
visit(w.parts)
|
|
294
|
+
if (substs.length === 0) return null
|
|
295
|
+
const out: Doc[] = []
|
|
296
|
+
let cursor = w.span.start
|
|
297
|
+
for (const s of substs) {
|
|
298
|
+
out.push(this.verbatim({ start: cursor, end: s.span.start }))
|
|
299
|
+
const open = s.type === 'CmdSubst' ? '$(' : `${s.direction}(`
|
|
300
|
+
out.push(tok(open, 'sub'), indent([hard, this.list(s.body, s.span.end, true)], this.opts.indent), hard, tok(')', 'sub'))
|
|
301
|
+
this.take(s.span.end) // comments right before `)`
|
|
302
|
+
cursor = s.span.end
|
|
303
|
+
}
|
|
304
|
+
out.push(this.verbatim({ start: cursor, end: w.span.end }))
|
|
305
|
+
return out
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** `name=value`: the `name=` part verbatim, the value as a word (so `$(…)` in it can be formatted). */
|
|
309
|
+
private assignDoc(a: A.Assign): Doc {
|
|
310
|
+
if (a.value?.type !== 'Word') return this.verbatim(a.span)
|
|
311
|
+
return [this.verbatim({ start: a.span.start, end: a.value.span.start }), this.wordDoc(a.value)]
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/** The separator before a closing keyword (`done`, `fi`, `}`): `; ` flat, or ` ` after a background `&`. */
|
|
315
|
+
private closer(body: Body): Doc {
|
|
316
|
+
return nl(body[body.length - 1]?.background ? ' ' : '; ')
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
private redirDoc(r: A.Redirect): Doc {
|
|
320
|
+
if (!r.heredoc) {
|
|
321
|
+
// `> path`: the target is a path atom (an fd duplication like `2>&1` stays whole)
|
|
322
|
+
const op = { start: r.span.start, end: r.target.span.start }
|
|
323
|
+
const target = this.src.slice(r.target.span.start, r.target.span.end)
|
|
324
|
+
if (r.target.span.end > r.target.span.start && !/&\s*$/.test(this.src.slice(op.start, op.end)) && !target.includes('\n')) {
|
|
325
|
+
const tail = target.includes('/')
|
|
326
|
+
return [this.verbatim(op), this.verbatim(r.target.span, { id: r.target.id, kind: 'word', weight: tail ? 0.9 : 0.6, tail })]
|
|
327
|
+
}
|
|
328
|
+
return this.verbatim(r.span)
|
|
329
|
+
}
|
|
330
|
+
const text = this.verbatim(r.span)
|
|
331
|
+
const b = r.heredoc.body.span
|
|
332
|
+
this.consume(b)
|
|
333
|
+
return [text, heredoc(b.start, b.end, r.heredoc.delimiter, { id: r.id, kind: 'hd', weight: 1 })]
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
private simple(c: A.SimpleCommand): Doc {
|
|
337
|
+
const assigns = c.assigns.map((a) => this.assignDoc(a))
|
|
338
|
+
let first = true
|
|
339
|
+
let roles: (ArgRole | undefined)[] = []
|
|
340
|
+
try {
|
|
341
|
+
roles = simpleCommandRoles(c)
|
|
342
|
+
} catch {
|
|
343
|
+
// display only
|
|
344
|
+
}
|
|
345
|
+
const words = c.words.map((w, i) => {
|
|
346
|
+
if (w.type === 'Assign') return this.assignDoc(w)
|
|
347
|
+
const d = this.wordDoc(w, first ? 'cmd' : 'arg', roles[i])
|
|
348
|
+
first = false
|
|
349
|
+
return d
|
|
350
|
+
})
|
|
351
|
+
const redirs = c.redirs.map((r) => this.redirDoc(r))
|
|
352
|
+
// keep `-o value`, `-qm value` (combined short flags) and `--opt value` together when wrapping
|
|
353
|
+
const units: Doc[] = []
|
|
354
|
+
for (let i = 0; i < words.length; i++) {
|
|
355
|
+
const w = plain(words[i], this.src)
|
|
356
|
+
const next = i + 1 < words.length ? plain(words[i + 1], this.src) : null
|
|
357
|
+
if (i > 0 && w !== null && next !== null && /^(-[A-Za-z]+|--[A-Za-z][\w-]*)$/.test(w) && !next.startsWith('-')) {
|
|
358
|
+
units.push([words[i], ' ', words[i + 1]])
|
|
359
|
+
i++
|
|
360
|
+
} else units.push(words[i])
|
|
361
|
+
}
|
|
362
|
+
units.push(...redirs)
|
|
363
|
+
// the command name, then arguments filled onto lines and indented twice
|
|
364
|
+
const filled: Doc = units.length <= 1 ? (units[0] ?? '') : [units[0], indent(fill(fillParts(units.slice(1))), this.opts.indent * 2)]
|
|
365
|
+
if (assigns.length === 0) return group(filled)
|
|
366
|
+
if (units.length === 0) return group(join(cont, assigns))
|
|
367
|
+
const ownLines = this.opts.assignments === 'always' || (this.opts.assignments === 'auto' && assigns.length >= 2)
|
|
368
|
+
if (ownLines) return [join(contForced, assigns), contForced, group(filled)]
|
|
369
|
+
return group([join(cont, assigns), cont, group(filled)])
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private operatorChain(items: A.Command[], ops: string[]): Doc {
|
|
373
|
+
const leading = this.opts.operators === 'leading'
|
|
374
|
+
const out: Doc[] = [this.command(items[0])]
|
|
375
|
+
const rest: Doc[] = []
|
|
376
|
+
for (let i = 1; i < items.length; i++) {
|
|
377
|
+
const o = ops[i - 1]
|
|
378
|
+
const comments = this.take(items[i].span.start)
|
|
379
|
+
if (leading && comments.length === 0) {
|
|
380
|
+
rest.push(cont, op(o), ' ', this.command(items[i]))
|
|
381
|
+
} else {
|
|
382
|
+
rest.push(' ', op(o))
|
|
383
|
+
for (const cm of comments) rest.push(this.trailingDoc(cm))
|
|
384
|
+
rest.push(nl(' '), this.command(items[i]))
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
out.push(indent(rest, this.opts.indent))
|
|
388
|
+
return group(out)
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private pipeline(c: A.Pipeline): Doc {
|
|
392
|
+
const first = c.stages[0]
|
|
393
|
+
const prefix = this.src.slice(c.span.start, first.span.start).trim().replace(/\s+/g, ' ')
|
|
394
|
+
const ops = c.pipeStderr.map((e) => (e ? '|&' : '|'))
|
|
395
|
+
const chain = c.stages.length === 1 ? this.command(first) : this.operatorChain(c.stages, ops)
|
|
396
|
+
return prefix ? [kw(prefix), ' ', chain] : chain
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
private andOr(c: A.AndOr): Doc {
|
|
400
|
+
return this.operatorChain(c.items, c.ops)
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
private functionDef(f: A.FunctionDef): Doc {
|
|
404
|
+
const header = this.opts.functionKeyword === 'preserve'
|
|
405
|
+
? tok(this.src.slice(f.span.start, f.body.span.start).trim().replace(/\s+/g, ' '), 'fn')
|
|
406
|
+
: tok(`${f.name}()`, 'fn')
|
|
407
|
+
return [header, ' ', this.command(f.body)]
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
private compoundRedirs(c: A.CompoundCommand): Doc {
|
|
411
|
+
if (c.redirs.length === 0) return ''
|
|
412
|
+
return [' ', join(' ', c.redirs.map((r) => this.redirDoc(r)))]
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Source of a compound up to its first redirection (verbatim forms: `(( ))`, `[[ ]]`). */
|
|
416
|
+
private compoundText(c: A.CompoundCommand): Doc {
|
|
417
|
+
let end = c.redirs.length > 0 ? c.redirs[0].span.start : c.span.end
|
|
418
|
+
while (end > c.span.start && /\s/.test(this.src[end - 1])) end--
|
|
419
|
+
return this.verbatim({ start: c.span.start, end }, { id: c.id, kind: 'word', weight: 0.3 })
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
private body(body: Body, endOffset: number): Doc {
|
|
423
|
+
return indent([nl(' '), this.list(body, endOffset, false)], this.opts.indent)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
private compound(c: A.CompoundCommand): Doc {
|
|
427
|
+
switch (c.type) {
|
|
428
|
+
case 'Group':
|
|
429
|
+
return compoundGroup([kw('{'), this.body(c.body, c.span.end), this.closer(c.body), kw('}')], !this.groupable(c.body))
|
|
430
|
+
case 'Subshell':
|
|
431
|
+
return compoundGroup([kw('('), this.body(c.body, c.span.end), nl(' '), kw(')')], !this.groupable(c.body))
|
|
432
|
+
case 'If': {
|
|
433
|
+
const parts: Doc[] = []
|
|
434
|
+
const flatOk = c.clauses.every((cl) => this.simpleBody(cl.body) && cl.cond.length === 1) && (c.elseBody === null || this.simpleBody(c.elseBody))
|
|
435
|
+
c.clauses.forEach((cl, i) => {
|
|
436
|
+
if (i > 0) parts.push(this.closer(c.clauses[i - 1].body), kw('elif'), ' ')
|
|
437
|
+
else parts.push(kw('if'), ' ')
|
|
438
|
+
parts.push(this.condList(cl.cond, cl.body[0]?.span.start ?? c.span.end), op(';'), ' ', kw('then'), this.body(cl.body, c.span.end))
|
|
439
|
+
})
|
|
440
|
+
const lastBody = c.clauses[c.clauses.length - 1].body
|
|
441
|
+
if (c.elseBody) parts.push(this.closer(lastBody), kw('else'), this.body(c.elseBody, c.span.end))
|
|
442
|
+
parts.push(this.closer(c.elseBody ?? lastBody), kw('fi'))
|
|
443
|
+
return compoundGroup(parts, !flatOk)
|
|
444
|
+
}
|
|
445
|
+
case 'Loop':
|
|
446
|
+
return compoundGroup([kw(c.kind), ' ', this.condList(c.cond, c.body[0]?.span.start ?? c.span.end), op(';'), ' ', kw('do'), this.body(c.body, c.span.end), this.closer(c.body), kw('done')], !this.simpleBody(c.body))
|
|
447
|
+
case 'For':
|
|
448
|
+
case 'Select': {
|
|
449
|
+
const head: Doc[] = [kw(c.type === 'For' ? 'for' : 'select'), ' ', tok(c.variable, 'var')]
|
|
450
|
+
if (c.items !== null) head.push(' ', kw('in'), ...c.items.flatMap((w) => [' ', this.wordDoc(w)]))
|
|
451
|
+
return compoundGroup([head, op(';'), ' ', kw('do'), this.body(c.body, c.span.end), this.closer(c.body), kw('done')], !this.simpleBody(c.body))
|
|
452
|
+
}
|
|
453
|
+
case 'ArithFor':
|
|
454
|
+
return compoundGroup([this.arithForHead(c), op(';'), ' ', kw('do'), this.body(c.body, c.span.end), this.closer(c.body), kw('done')], !this.simpleBody(c.body))
|
|
455
|
+
case 'Case':
|
|
456
|
+
return this.caseDoc(c)
|
|
457
|
+
case 'ArithCommand':
|
|
458
|
+
case 'CondCommand':
|
|
459
|
+
return this.compoundText(c)
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
private groupable(body: Body): boolean {
|
|
464
|
+
if (this.opts.compounds === 'expanded') return false
|
|
465
|
+
return body.length > 0 && body.every((s) => {
|
|
466
|
+
const t = s.command.type
|
|
467
|
+
return t === 'SimpleCommand' || t === 'Pipeline' || t === 'AndOr'
|
|
468
|
+
}) && body.length <= 3
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/**
|
|
472
|
+
* `if`/`while` conditions: statements joined by `; ` (a trailing comment
|
|
473
|
+
* breaks the line). `limit` is where the body starts: a comment after it
|
|
474
|
+
* belongs to the body or the whole compound, not to the condition.
|
|
475
|
+
*/
|
|
476
|
+
private condList(stmts: Body, limit: number): Doc {
|
|
477
|
+
const parts: Doc[] = []
|
|
478
|
+
stmts.forEach((s, i) => {
|
|
479
|
+
const leading = this.take(s.span.start)
|
|
480
|
+
if (i > 0) parts.push(leading.length ? hard : [op(';'), ' '])
|
|
481
|
+
for (const cm of leading) parts.push(this.commentDoc(cm), hard)
|
|
482
|
+
parts.push(this.stmt(s))
|
|
483
|
+
const next = stmts[i + 1]
|
|
484
|
+
const t = this.trailing(s.span.end, next ? next.span.start : limit)
|
|
485
|
+
if (t) parts.push(this.trailingDoc(t))
|
|
486
|
+
})
|
|
487
|
+
return parts
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
private arithForHead(c: A.ArithFor): Doc {
|
|
491
|
+
const exprs = [c.init, c.cond, c.step].filter((e): e is A.ArithExpr => e !== null)
|
|
492
|
+
const from = exprs.length ? exprs[exprs.length - 1].span.end : c.span.start
|
|
493
|
+
const close = this.src.indexOf('))', from)
|
|
494
|
+
return this.verbatim({ start: c.span.start, end: close + 2 })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
private caseDoc(c: A.Case): Doc {
|
|
498
|
+
const ind = this.opts.indent
|
|
499
|
+
const word = this.wordDoc(c.word)
|
|
500
|
+
const items: Doc[] = []
|
|
501
|
+
for (const item of c.items) {
|
|
502
|
+
const leading = this.take(item.span.start)
|
|
503
|
+
for (const cm of leading) items.push(hard, this.commentDoc(cm))
|
|
504
|
+
const pats = join(op('|'), item.patterns.map((p) => this.wordDoc(p)))
|
|
505
|
+
const term = op(item.terminator)
|
|
506
|
+
if (item.body.length === 0) {
|
|
507
|
+
items.push(hard, [pats, op(')'), ' ', term])
|
|
508
|
+
continue
|
|
509
|
+
}
|
|
510
|
+
const body = this.list(item.body, item.span.end, false)
|
|
511
|
+
items.push(hard, compoundGroup([pats, op(')'), indent([nl(' '), body, nl(' '), term], ind)], !this.simpleBody(item.body)))
|
|
512
|
+
}
|
|
513
|
+
const tail = this.take(c.span.end)
|
|
514
|
+
for (const cm of tail) items.push(hard, this.commentDoc(cm))
|
|
515
|
+
return group([kw('case'), ' ', word, ' ', kw('in'), indent(items, ind), hard, kw('esac')], true)
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Arguments as fill parts: `['', cont, a, cont, b, …]`. Fill alternates
|
|
521
|
+
* content and separator; the empty first content lets the separator between
|
|
522
|
+
* the command name and its first argument break like the others.
|
|
523
|
+
*/
|
|
524
|
+
function fillParts(units: Doc[]): Doc[] {
|
|
525
|
+
const parts: Doc[] = ['']
|
|
526
|
+
for (const u of units) parts.push(cont, u)
|
|
527
|
+
return parts
|
|
528
|
+
}
|