ssh-config 4.1.5 → 4.2.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/Readme.md CHANGED
@@ -108,12 +108,14 @@ config.find({ Host: 'example1' })
108
108
  config.find(line => line.param == 'Host' && line.value == 'example1')
109
109
  ```
110
110
 
111
- ### `.remove` sections by Host or other criteria
111
+ ### `.remove` sections by Host / Match or function
112
112
 
113
113
  To remove sections, we can pass the section to `.remove(opts)`.
114
114
 
115
115
  ```js
116
116
  config.remove({ Host: 'example1' })
117
+ // or the ES2015 Array.prototype.find
118
+ config.remove(line => line.param == 'Host' && line.value == 'example1')
117
119
  ```
118
120
 
119
121
  ### `.append` sections
package/index.js CHANGED
@@ -1,473 +1,23 @@
1
- 'use strict'
2
-
3
- const glob = require('./src/glob')
4
-
5
- const RE_SPACE = /\s/
6
- const RE_LINE_BREAK = /\r|\n/
7
- const RE_SECTION_DIRECTIVE = /^(Host|Match)$/i
8
- const RE_MULTI_VALUE_DIRECTIVE = /^(GlobalKnownHostsFile|Host|IPQoS|SendEnv|UserKnownHostsFile|ProxyCommand)$/i
9
- const RE_QUOTE_DIRECTIVE = /^(?:CertificateFile|IdentityFile|IdentityAgent|User)$/i
10
- const RE_SINGLE_LINE_DIRECTIVE = /^(Include|IdentityFile)$/i
11
-
12
- const DIRECTIVE = 1
13
- const COMMENT = 2
14
-
15
- const MULTIPLE_VALUE_PROPS = [
16
- 'IdentityFile',
17
- 'LocalForward',
18
- 'RemoteForward',
19
- 'DynamicForward',
20
- 'CertificateFile'
21
- ]
22
-
23
- function compare(line, opts) {
24
- return opts.hasOwnProperty(line.param) && opts[line.param] === line.value
25
- }
26
-
27
- function getIndent(config) {
28
- for (const line of config) {
29
- if (RE_SECTION_DIRECTIVE.test(line.param)) {
30
- for (const subline of line.config) {
31
- if (subline.before) {
32
- return subline.before
33
- }
34
- }
35
- }
36
- }
37
-
38
- return ' '
39
- }
40
-
41
- class SSHConfig extends Array {
42
- /**
43
- * Query ssh config by host.
44
- *
45
- * @return {Object} The applied options of current Host
46
- */
47
- compute(host) {
48
- const obj = {}
49
- const setProperty = (name, value) => {
50
- if (MULTIPLE_VALUE_PROPS.includes(name)) {
51
- const list = obj[name] || (obj[name] = [])
52
- list.push(value)
53
- }
54
- else if (obj[name] == null) {
55
- obj[name] = value
56
- }
57
- }
58
-
59
- for (const line of this) {
60
- if (line.type !== DIRECTIVE) continue
61
- if (line.param === 'Host') {
62
- if (glob(line.value, host)) {
63
- setProperty(line.param, line.value)
64
-
65
- line.config
66
- .filter(line => line.type === DIRECTIVE)
67
- .forEach(line => setProperty(line.param, line.value))
68
- }
69
- }
70
- else if (line.param === 'Match') {
71
- // TODO
72
- }
73
- else {
74
- setProperty(line.param, line.value)
75
- }
76
- }
77
-
78
- return obj
79
- }
80
-
81
- /**
82
- * find section by Host or Match
83
- */
84
- find(opts = {}) {
85
- if (typeof opts === 'function') return super.find(opts)
86
-
87
- if (!(opts && ('Host' in opts || 'Match' in opts))) {
88
- throw new Error('Can only find by Host or Match')
89
- }
90
-
91
- return super.find(line => compare(line, opts))
92
- }
93
-
94
- /**
95
- * Remove section
96
- */
97
- remove(opts = {}) {
98
- if (!(opts && ('Host' in opts || 'Match' in opts))) {
99
- throw new Error('Can only remove by Host or Match')
100
- }
101
-
102
- const index = typeof opts === 'function'
103
- ? super.findIndex(opts)
104
- : super.findIndex(line => compare(line, opts))
105
-
106
- if (index >= 0) return this.splice(index, 1)
107
- }
108
-
109
- /**
110
- * toString()
111
- * @returns {string}
112
- */
113
- toString() {
114
- return this.constructor.stringify(this)
115
- }
116
-
117
-
118
- /**
119
- * Append new section to existing ssh config.
120
- * @param {Object} opts
121
- */
122
- append(opts) {
123
- const indent = getIndent(this)
124
- const lastEntry = this.length > 0 ? this[this.length - 1] : null
125
- let config = lastEntry && lastEntry.config || this
126
- let configWas = this
127
-
128
- let lastLine = config.length > 0 ? config[config.length - 1] : lastEntry
129
- if (lastLine && !lastLine.after) lastLine.after = '\n'
130
-
131
- for (const param in opts) {
132
- const line = {
133
- type: DIRECTIVE,
134
- param,
135
- separator: ' ',
136
- value: opts[param],
137
- before: '',
138
- after: '\n'
139
- }
140
-
141
- if (RE_SECTION_DIRECTIVE.test(param)) {
142
- config = configWas
143
- // separate sections with an extra newline
144
- // https://github.com/cyjake/ssh-config/issues/23#issuecomment-564768248
145
- if (lastLine && lastLine.after === '\n') lastLine.after += '\n'
146
- config.push(line)
147
- config = line.config = new SSHConfig()
148
- } else {
149
- line.before = config === configWas ? '' : indent
150
- config.push(line)
151
- }
152
- lastLine = line
153
- }
154
-
155
- return configWas
156
- }
157
-
158
- /**
159
- * Prepend new section to existing ssh config.
160
- * @param {Object} opts
161
- */
162
- prepend(opts, beforeFirstSection = false) {
163
- const indent = getIndent(this)
164
- let config = this
165
- let i = 0
166
-
167
- // insert above known sections
168
- if (beforeFirstSection) {
169
- while (i < this.length && !RE_SECTION_DIRECTIVE.test(this[i].param)) {
170
- i += 1
171
- }
172
-
173
- if (i >= this.length) { // No sections in original config
174
- return this.append(opts)
175
- }
176
- }
177
-
178
- // Prepend new section above the first section
179
- let sectionLineFound = false
180
- let processedLines = 0
181
-
182
- for (const param in opts) {
183
- processedLines += 1
184
- const line = {
185
- type: DIRECTIVE,
186
- param,
187
- separator: ' ',
188
- value: opts[param],
189
- before: '',
190
- after: '\n'
191
- }
192
-
193
- if (RE_SECTION_DIRECTIVE.test(param)) {
194
- config.splice(i, 0, line)
195
- config = line.config = new SSHConfig()
196
- sectionLineFound = true
197
- continue
198
- }
199
-
200
- // separate from previous sections with an extra newline
201
- if (processedLines === Object.keys(opts).length) {
202
- line.after += '\n'
203
- }
204
-
205
- if (!sectionLineFound) {
206
- config.splice(i, 0, line)
207
- i += 1
208
-
209
- // Add an extra newline if a single line directive like Include
210
- if (RE_SINGLE_LINE_DIRECTIVE.test(param)) {
211
- line.after += '\n'
212
- }
213
- continue
214
- }
215
-
216
- line.before = indent
217
- config.push(line)
218
- }
219
-
220
- return config
221
- }
222
-
223
- /**
224
- * Stringify structured object into ssh config text
225
- * @param {SSHConfig} config
226
- * @returns {string}
227
- */
228
- static stringify(config) {
229
- let str = ''
230
-
231
- function formatValue(value, quoted) {
232
- if (Array.isArray(value)) {
233
- return value.map(chunk => formatValue(chunk, RE_SPACE.test(chunk))).join(' ')
234
- }
235
- return quoted ? `"${value}"` : value
236
- }
237
-
238
- function formatDirective(line) {
239
- const quoted = line.quoted
240
- || (RE_QUOTE_DIRECTIVE.test(line.param) && RE_SPACE.test(line.value))
241
- const value = formatValue(line.value, quoted)
242
- return `${line.param}${line.separator}${value}`
243
- }
244
-
245
- const format = line => {
246
- str += line.before
247
-
248
- if (line.type === COMMENT) {
249
- str += line.content
250
- }
251
- else if (line.type === DIRECTIVE && MULTIPLE_VALUE_PROPS.includes(line.param)) {
252
- [].concat(line.value).forEach(function (value, i, values) {
253
- str += formatDirective({ ...line, value })
254
- if (i < values.length - 1) str += `\n${line.before}`
255
- })
256
- }
257
- else if (line.type === DIRECTIVE) {
258
- str += formatDirective(line)
259
- }
260
-
261
- str += line.after
262
-
263
- if (line.config) {
264
- line.config.forEach(format)
265
- }
266
- }
267
-
268
- config.forEach(format)
269
-
270
- return str
271
- }
272
-
273
- static get DIRECTIVE() {
274
- return DIRECTIVE
275
- }
276
-
277
- static get COMMENT() {
278
- return COMMENT
279
- }
280
-
281
- /**
282
- * Parse ssh config text into structured object.
283
- */
284
- static parse(str) {
285
- let i = 0
286
- let chr = next()
287
- let config = new SSHConfig()
288
- let configWas = config
289
-
290
- function next() {
291
- return str[i++]
292
- }
293
-
294
- function space() {
295
- let spaces = ''
296
-
297
- while (RE_SPACE.test(chr)) {
298
- spaces += chr
299
- chr = next()
300
- }
301
-
302
- return spaces
303
- }
304
-
305
- function linebreak() {
306
- let breaks = ''
307
-
308
- while (RE_LINE_BREAK.test(chr)) {
309
- breaks += chr
310
- chr = next()
311
- }
312
-
313
- return breaks
314
- }
315
-
316
- function parameter() {
317
- let param = ''
318
-
319
- while (chr && /[^ \t=]/.test(chr)) {
320
- param += chr
321
- chr = next()
322
- }
323
-
324
- return param
325
- }
326
-
327
- function separator() {
328
- let sep = space()
329
-
330
- if (chr === '=') {
331
- sep += chr
332
- chr = next()
333
- }
334
-
335
- return sep + space()
336
- }
337
-
338
- function value() {
339
- let val = ''
340
- let quoted = false
341
- let escaped = false
342
-
343
- while (chr && !RE_LINE_BREAK.test(chr)) {
344
- // backslash escapes only double quotes
345
- if (escaped) {
346
- val += chr === '"' ? chr : `\\${chr}`
347
- escaped = false
348
- }
349
- // ProxyCommand ssh -W "%h:%p" firewall.example.org
350
- else if (chr === '"' && (!val || quoted)) {
351
- quoted = !quoted
352
- }
353
- else if (chr === '\\') {
354
- escaped = true
355
- }
356
- else {
357
- val += chr
358
- }
359
- chr = next()
360
- }
361
-
362
- if (quoted || escaped) {
363
- throw new Error(`Unexpected line break at ${val}`)
364
- }
365
-
366
- return val.trim()
367
- }
368
-
369
- function comment() {
370
- const type = COMMENT
371
- let content = ''
372
-
373
- while (chr && !RE_LINE_BREAK.test(chr)) {
374
- content += chr
375
- chr = next()
376
- }
377
-
378
- return { type, content }
379
- }
380
-
381
- // Host *.co.uk
382
- // Host * !local.dev
383
- // Host "foo bar"
384
- function values() {
385
- const results = []
386
- let val = ''
387
- let quoted = false
388
- let escaped = false
389
-
390
- while (chr && !RE_LINE_BREAK.test(chr)) {
391
- if (escaped) {
392
- val += chr === '"' ? chr : `\\${chr}`
393
- escaped = false
394
- }
395
- else if (chr === '"') {
396
- quoted = !quoted
397
- }
398
- else if (chr === '\\') {
399
- escaped = true
400
- }
401
- else if (quoted) {
402
- val += chr
403
- }
404
- else if (/[ \t]/.test(chr)) {
405
- if (val) {
406
- results.push(val)
407
- val = ''
408
- }
409
- // otherwise ignore the space
410
- }
411
- else {
412
- val += chr
413
- }
414
-
415
- chr = next()
416
- }
417
-
418
- if (quoted || escaped) {
419
- throw new Error(`Unexpected line break at ${results.concat(val).join(' ')}`)
420
- }
421
- if (val) results.push(val)
422
- return results.length > 1 ? results : results[0]
423
- }
424
-
425
- function directive() {
426
- const type = DIRECTIVE
427
- const param = parameter()
428
- // Host "foo bar" baz
429
- const multiple = RE_MULTI_VALUE_DIRECTIVE.test(param)
430
- const result = {
431
- type,
432
- param,
433
- separator: separator(),
434
- quoted: !multiple && chr === '"',
435
- value: multiple ? values() : value()
436
- }
437
- if (!result.quoted) delete result.quoted
438
- return result
439
- }
440
-
441
- function line() {
442
- const before = space()
443
- const node = chr === '#' ? comment() : directive()
444
- const after = linebreak()
445
-
446
- node.before = before
447
- node.after = after
448
-
449
- return node
450
- }
451
-
452
- while (chr) {
453
- let node = line()
454
-
455
- if (node.type === DIRECTIVE && RE_SECTION_DIRECTIVE.test(node.param)) {
456
- config = configWas
457
- config.push(node)
458
- config = node.config = new SSHConfig()
459
- }
460
- else if (node.type === DIRECTIVE && !node.param) {
461
- // blank lines at file end
462
- config[config.length - 1].after += node.before
463
- }
464
- else {
465
- config.push(node)
466
- }
467
- }
468
-
469
- return configWas
470
- }
471
- }
472
-
473
- module.exports = SSHConfig
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ const ssh_config_1 = __importDefault(require("./src/ssh-config"));
21
+ __exportStar(require("./src/ssh-config"), exports);
22
+ exports.default = ssh_config_1.default;
23
+ //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ssh-config",
3
3
  "description": "SSH config parser and stringifier",
4
- "version": "4.1.5",
4
+ "version": "4.2.0",
5
5
  "author": "Chen Yangjian (https://www.cyj.me)",
6
6
  "repository": {
7
7
  "type": "git",
@@ -9,20 +9,23 @@
9
9
  },
10
10
  "files": [
11
11
  "index.js",
12
- "src",
13
- "types"
12
+ "src"
14
13
  ],
15
14
  "devDependencies": {
16
15
  "@types/mocha": "^9.1.0",
17
- "@types/node": "^17.0.23",
18
- "eslint": "^7.17.0",
16
+ "@types/node": "^17.0.45",
17
+ "@typescript-eslint/eslint-plugin": "^5.48.0",
18
+ "@typescript-eslint/parser": "^5.48.0",
19
+ "eslint": "^8.31.0",
19
20
  "heredoc": "^1.3.1",
20
21
  "mocha": "^8.2.1",
21
22
  "nyc": "^15.1.0",
22
23
  "typescript": "^4.6.3"
23
24
  },
24
25
  "scripts": {
25
- "lint": "eslint .",
26
+ "lint": "eslint --ext ts .",
27
+ "lint:fix": "eslint --ext ts --fix .",
28
+ "prepack": "tsc",
26
29
  "pretest": "tsc",
27
30
  "test": "NODE_OPTIONS=--enable-source-maps mocha --exit --recursive",
28
31
  "test:coverage": "nyc mocha --exit --recursive && nyc report --reporter=lcov"
@@ -30,6 +33,5 @@
30
33
  "engine": {
31
34
  "node": ">= 10.0.0"
32
35
  },
33
- "types": "types/index.d.ts",
34
36
  "license": "MIT"
35
37
  }
package/src/glob.js CHANGED
@@ -1,22 +1,18 @@
1
- 'use strict'
2
-
3
- function escapeChars(str, chars) {
4
- for (let char of chars) {
5
- str = str.replace(new RegExp('\\' + char, 'g'), '\\' + char)
6
- }
7
-
8
- return str
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function escapeChars(text, chars) {
4
+ for (let char of chars) {
5
+ text = text.replace(new RegExp('\\' + char, 'g'), '\\' + char);
6
+ }
7
+ return text;
9
8
  }
10
-
11
- function match(pattern, str) {
12
- pattern = escapeChars(pattern, '\\()[]{}.+^$|')
13
- pattern = pattern
14
- .replace(/\*/g, '.*')
15
- .replace(/\?/g, '.?')
16
-
17
- return new RegExp('^(?:' + pattern + ')$').test(str)
9
+ function match(pattern, text) {
10
+ pattern = escapeChars(pattern, '\\()[]{}.+^$|');
11
+ pattern = pattern
12
+ .replace(/\*/g, '.*')
13
+ .replace(/\?/g, '.?');
14
+ return new RegExp('^(?:' + pattern + ')$').test(text);
18
15
  }
19
-
20
16
  /**
21
17
  * A helper function to match input against [pattern-list](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5#PATTERNS).
22
18
  * According to `man ssh_config`, negated patterns shall be matched first.
@@ -24,23 +20,21 @@ function match(pattern, str) {
24
20
  * @param {string|string[]} patternList
25
21
  * @param {string} str
26
22
  */
27
- function glob(patternList, str) {
28
- const patterns = Array.isArray(patternList) ? patternList : patternList.split(/,/)
29
-
30
- // > If a negated entry is matched, then the Host entry is ignored, regardless of whether any other patterns on the line match.
31
- let result = false
32
- for (const pattern of patterns) {
33
- const negate = pattern[0] == '!'
34
-
35
- if (negate && match(pattern.slice(1), str)) {
36
- return false
37
- } else if (match(pattern, str)) {
38
- // wait until all of the pattern match results because there might be a negated pattern
39
- result = true
23
+ function glob(patternList, text) {
24
+ const patterns = Array.isArray(patternList) ? patternList : patternList.split(/,/);
25
+ // > If a negated entry is matched, then the Host entry is ignored, regardless of whether any other patterns on the line match.
26
+ let result = false;
27
+ for (const pattern of patterns) {
28
+ const negate = pattern[0] == '!';
29
+ if (negate && match(pattern.slice(1), text)) {
30
+ return false;
31
+ }
32
+ else if (match(pattern, text)) {
33
+ // wait until all of the pattern match results because there might be a negated pattern
34
+ result = true;
35
+ }
40
36
  }
41
- }
42
-
43
- return result
37
+ return result;
44
38
  }
45
-
46
- module.exports = glob
39
+ exports.default = glob;
40
+ //# sourceMappingURL=glob.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"glob.js","sourceRoot":"","sources":["glob.ts"],"names":[],"mappings":";;AACA,SAAS,WAAW,CAAC,IAAY,EAAE,KAAa;IAC9C,KAAK,IAAI,IAAI,IAAI,KAAK,EAAE;QACtB,IAAI,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,IAAI,GAAG,IAAI,EAAE,GAAG,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,CAAA;KAC/D;IAED,OAAO,IAAI,CAAA;AACb,CAAC;AAED,SAAS,KAAK,CAAC,OAAe,EAAE,IAAY;IAC1C,OAAO,GAAG,WAAW,CAAC,OAAO,EAAE,eAAe,CAAC,CAAA;IAC/C,OAAO,GAAG,OAAO;SACd,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC;SACpB,OAAO,CAAC,KAAK,EAAE,IAAI,CAAC,CAAA;IAEvB,OAAO,IAAI,MAAM,CAAC,MAAM,GAAG,OAAO,GAAG,IAAI,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAA;AACvD,CAAC;AAED;;;;;;GAMG;AACH,SAAS,IAAI,CAAC,WAA8B,EAAE,IAAY;IACxD,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,WAAW,CAAC,KAAK,CAAC,GAAG,CAAC,CAAA;IAElF,+HAA+H;IAC/H,IAAI,MAAM,GAAG,KAAK,CAAA;IAClB,KAAK,MAAM,OAAO,IAAI,QAAQ,EAAE;QAC9B,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,IAAI,GAAG,CAAA;QAEhC,IAAI,MAAM,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,IAAI,CAAC,EAAE;YAC3C,OAAO,KAAK,CAAA;SACb;aAAM,IAAI,KAAK,CAAC,OAAO,EAAE,IAAI,CAAC,EAAE;YAC/B,uFAAuF;YACvF,MAAM,GAAG,IAAI,CAAA;SACd;KACF;IAED,OAAO,MAAM,CAAA;AACf,CAAC;AAED,kBAAe,IAAI,CAAA"}
package/src/glob.ts ADDED
@@ -0,0 +1,45 @@
1
+
2
+ function escapeChars(text: string, chars: string) {
3
+ for (let char of chars) {
4
+ text = text.replace(new RegExp('\\' + char, 'g'), '\\' + char)
5
+ }
6
+
7
+ return text
8
+ }
9
+
10
+ function match(pattern: string, text: string) {
11
+ pattern = escapeChars(pattern, '\\()[]{}.+^$|')
12
+ pattern = pattern
13
+ .replace(/\*/g, '.*')
14
+ .replace(/\?/g, '.?')
15
+
16
+ return new RegExp('^(?:' + pattern + ')$').test(text)
17
+ }
18
+
19
+ /**
20
+ * A helper function to match input against [pattern-list](https://www.freebsd.org/cgi/man.cgi?query=ssh_config&sektion=5#PATTERNS).
21
+ * According to `man ssh_config`, negated patterns shall be matched first.
22
+ *
23
+ * @param {string|string[]} patternList
24
+ * @param {string} str
25
+ */
26
+ function glob(patternList: string | string[], text: string) {
27
+ const patterns = Array.isArray(patternList) ? patternList : patternList.split(/,/)
28
+
29
+ // > If a negated entry is matched, then the Host entry is ignored, regardless of whether any other patterns on the line match.
30
+ let result = false
31
+ for (const pattern of patterns) {
32
+ const negate = pattern[0] == '!'
33
+
34
+ if (negate && match(pattern.slice(1), text)) {
35
+ return false
36
+ } else if (match(pattern, text)) {
37
+ // wait until all of the pattern match results because there might be a negated pattern
38
+ result = true
39
+ }
40
+ }
41
+
42
+ return result
43
+ }
44
+
45
+ export default glob