forked from bash-lsp/bash-language-server
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathsh.ts
143 lines (115 loc) · 3.47 KB
/
sh.ts
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
import * as ChildProcess from 'child_process'
import { logger } from './logger'
import { isWindows } from './platform'
/**
* Execute the following sh program.
*/
export function execShellScript(
body: string,
cmd = isWindows() ? 'cmd.exe' : 'bash',
): Promise<string> {
const args = []
if (cmd === 'cmd.exe') {
args.push('/c', body)
} else {
args.push('--noprofile', '--norc', '-c', body)
}
const process = ChildProcess.spawn(cmd, args)
return new Promise((resolve, reject) => {
let output = ''
const handleClose = (returnCode: number | Error) => {
if (returnCode === 0) {
resolve(output)
} else {
reject(`Failed to execute ${body}`)
}
}
process.stdout.on('data', (buffer) => {
output += buffer
})
process.on('close', handleClose)
process.on('error', handleClose)
})
}
// Currently only reserved words where documentation doesn't make sense.
// At least on OS X these just return the builtin man. On ubuntu there
// are no documentation for them.
const WORDS_WITHOUT_DOCUMENTATION = new Set([
'else',
'fi',
'then',
'esac',
'elif',
'done',
])
/**
* Get documentation for the given word by using help and man.
*/
export async function getShellDocumentationWithoutCache({
word,
}: {
word: string
}): Promise<string | null> {
if (word.split(' ').length > 1) {
throw new Error(`lookupDocumentation should be given a word, received "${word}"`)
}
if (WORDS_WITHOUT_DOCUMENTATION.has(word)) {
return null
}
const DOCUMENTATION_COMMANDS = [
{ type: 'help', command: `help ${word} | col -bx` },
// We have experimented with setting MANWIDTH to different values for reformatting.
// The default line width of the terminal works fine for hover, but could be better
// for completions.
{ type: 'man', command: `man -P cat ${word} | col -bx` },
]
for (const { type, command } of DOCUMENTATION_COMMANDS) {
try {
const documentation = await execShellScript(command)
if (documentation) {
let formattedDocumentation = documentation.trim()
if (type === 'man') {
formattedDocumentation = formatManOutput(formattedDocumentation)
}
if (formattedDocumentation) {
return formattedDocumentation
}
}
} catch (error) {
// Ignoring if command fails and store failure in cache
logger.error(`getShellDocumentation failed for "${word}"`, error)
}
}
return null
}
export function formatManOutput(manOutput: string): string {
const indexNameBlock = manOutput.indexOf('NAME')
const indexBeforeFooter = manOutput.lastIndexOf('\n')
if (indexNameBlock < 0 || indexBeforeFooter < 0) {
return manOutput
}
const formattedManOutput = manOutput.slice(indexNameBlock, indexBeforeFooter)
if (!formattedManOutput) {
logger.error(`formatManOutput failed`, { manOutput })
return manOutput
}
return formattedManOutput
}
/**
* Only works for one-parameter (serializable) functions.
*/
/* eslint-disable @typescript-eslint/ban-types */
export function memorize<T extends Function>(func: T): T {
const cache = new Map()
const returnFunc = async function (arg: any) {
const cacheKey = JSON.stringify(arg)
if (cache.has(cacheKey)) {
return cache.get(cacheKey)
}
const result = await func(arg)
cache.set(cacheKey, result)
return result
}
return returnFunc as any
}
export const getShellDocumentation = memorize(getShellDocumentationWithoutCache)