-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathcli.mjs
228 lines (182 loc) · 5.79 KB
/
cli.mjs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
import * as l from './lang.mjs'
import * as s from './str.mjs'
import * as c from './coll.mjs'
// Returns OS args in Deno and Node. Returns `[]` in other environemnts.
export function args() {
return globalThis.Deno?.args ?? globalThis.process?.args ?? []
}
// Returns the OS arg at the given index, or undefined. Uses `args`.
export function arg(ind) {return args()[l.reqNat(ind)]}
export function consoleCols() {
return (
globalThis.Deno?.consoleSize?.()?.columns ??
globalThis.process?.stdout?.columns
) | 0
}
/*
Clears the console, returning true if cleared and false otherwise. The optional
argument "soft", if true, avoids clearing the scrollback buffer; the default is
to clear scrollback.
If the environment doesn't appear to be a browser or a TTY, for example if the
program's output is piped to a file, this should be a nop. Note that
`console.clear` doesn't always perform this detection, and may print garbage to
stdout, so we avoid calling it unless we're sure. For example, `console.clear`
has TTY detection in Node 16 but not in Deno 1.17. We also don't want to rely
on Node's detection, because various polyfills/shims for Node globals may not
implement that.
*/
export function emptty(soft) {
soft = l.laxBool(soft)
const Deno = globalThis.Deno
if (Deno?.isatty) {
if (Deno.isatty()) {
Deno.stdout.writeSync(soft ? arrClearSoft() : arrClearHard())
return true
}
return false
}
const process = globalThis.process
if (process?.stdout) {
if (process.stdout.isTTY) {
process.stdout.write(soft ? arrClearSoft() : arrClearHard())
return true
}
return false
}
if (l.isObj(globalThis.document)) {
console.clear()
return true
}
return false
}
/*
Parser for CLI args. Features:
* Supports flags prefixed with `-`, `--`.
* Supports `=` pairs.
* Separates flags from unflagged args.
* Parses flags into a map.
* Stores remaining args as an array.
* On-demand parsing of booleans and numbers.
*/
export class Flag extends s.StrMap {
constructor(val) {
super()
this.args = []
this.mut(val)
}
boolOpt(key) {
const val = this.get(key)
return val === `` || s.boolOpt(val)
}
bool(key) {
const val = this.get(key)
return val === `` || s.bool(val)
}
mut(val) {
if (l.isArr(val)) return this.mutFromArr(val)
return super.mut(val)
}
mutFromArr(val) {
l.reqArr(val)
let flag
for (val of val) {
l.reqStr(val)
if (!isFlag(val)) {
if (flag) {
this.append(flag, val)
flag = undefined
continue
}
this.args.push(val)
continue
}
if (flag) {
this.set(flag, ``)
flag = undefined
}
const ind = val.indexOf(`=`)
if (ind >= 0) {
this.append(unFlag(val.slice(0, ind)), val.slice(ind+1))
continue
}
flag = unFlag(val)
}
if (flag) this.set(flag, ``)
return this
}
static os() {return new this(args())}
}
/*
Simple "env" map with support for parsing "env properties" strings. The parser
supports comments with `#` but not `!`, doesn't support backslash escapes, and
doesn't allow whitespace around `=`. Doesn't perform any IO; see `io_deno.mjs`
→ `EnvMap` which is a subclass.
*/
export class EnvMap extends c.Bmap {
set(key, val) {return super.set(l.reqStr(key), l.render(val))}
mut(val) {
if (l.isStr(val)) return this.mutFromStr(val)
return super.mut(val)
}
mutFromStr(val) {
for (const line of this.lines(val)) this.addLine(line)
return this
}
addLine(val) {
const mat = l.reqStr(val).match(/^(\w+)=(.*)$/)
if (!mat) throw SyntaxError(`expected valid env/properties line, got ${l.show(val)}`)
this.set(mat[1], mat[2])
return this
}
lines(val) {
return s.lines(val).map(s.trim).filter(this.isLineNonEmpty, this)
}
isLineEmpty(val) {
val = s.trim(val)
return !val || val.startsWith(`#`)
}
isLineNonEmpty(val) {return !this.isLineEmpty(val)}
}
// Standard terminal escape sequence. Same as "\x1b" or "\033".
// Reference: https://en.wikipedia.org/wiki/ANSI_escape_code.
export const TERM_ESC = `\x1b`
// Control Sequence Introducer. Used for other codes.
export const TERM_ESC_CSI = TERM_ESC + `[`
// Update cursor position to first row, first column.
export const TERM_ESC_CUP = TERM_ESC_CSI + `1;1H`
// Supposed to clear the screen without clearing the scrollback, aka soft clear.
// Seems insufficient on its own, at least in some terminals.
export const TERM_ESC_ERASE2 = TERM_ESC_CSI + `2J`
// Supposed to clear the screen and the scrollback, aka hard clear.
// Seems insufficient on its own, at least in some terminals.
export const TERM_ESC_ERASE3 = TERM_ESC_CSI + `3J`
// Supposed to reset the terminal to initial state, aka super hard clear.
// Seems insufficient on its own, at least in some terminals.
export const TERM_ESC_RESET = TERM_ESC + `c`
// Clear screen without clearing scrollback. Note that the behavior of this
// escape sequence is not consistent between languages and environments.
export const TERM_ESC_CLEAR_SOFT = TERM_ESC_RESET
// Clear screen AND scrollback.
export const TERM_ESC_CLEAR_HARD = TERM_ESC_CUP + TERM_ESC_RESET + TERM_ESC_ERASE3
let ARR_CLEAR_SOFT
export function arrClearSoft() {
return ARR_CLEAR_SOFT ??= new TextEncoder().encode(TERM_ESC_CLEAR_SOFT)
}
let ARR_CLEAR_HARD
export function arrClearHard() {
return ARR_CLEAR_HARD ??= new TextEncoder().encode(TERM_ESC_CLEAR_HARD)
}
export async function timed(tag, fun) {
const pre = tag ? s.san`[${tag}] ` : ``
const start = performance.now()
try {
return await fun()
}
finally {
const end = performance.now()
console.log(s.san`${pre}done in ${end - start} ms`)
}
}
/* Internal */
function isFlag(str) {return str.startsWith(`-`)}
function unFlag(str) {return s.stripPreAll(str, `-`)}