'use strict'
const glob = require('./lib/glob')
const RE_SPACE = /\s/
const RE_LINE_BREAK = /\r|\n/
const RE_SECTION_DIRECTIVE = /^(Host|Match)$/i
const RE_QUOTED = /^(")(.*)\1$/
const DIRECTIVE = 1
const COMMENT = 2
class SSHConfig extends Array {
/**
* Query ssh config by host.
*
* @return {Object} The applied options of current Host
*/
compute(host) {
let obj = {}
let setProperty = (name, value) => {
if (name === 'IdentityFile') {
let list = obj[name] || (obj[name] = [])
list.push(value)
}
else if (obj[name] === undefined) {
obj[name] = value
}
}
for (let i = 0; i < this.length; i++) {
let line = this[i]
if (line.type !== DIRECTIVE) {
continue
}
else if (line.param === 'Host') {
if (glob(line.value, host)) {
setProperty(line.param, line.value)
line.config
.filter(line => line.type === DIRECTIVE)
.forEach(line => setProperty(line.param, line.value))
}
}
else if (line.param === 'Match') {
// TODO
}
else {
setProperty(line.param, line.value)
}
}
return obj
}
/**
* find section by Host or Match
*/
find(opts) {
let result = this.constructor.find(this, opts)
return result ? result[0] : null
}
/**
* Remove section
*/
remove(opts) {
let result = this.constructor.find(this, opts)
if (result) {
return this.splice(result[1], 1)
}
}
/**
* toString()
*/
toString() {
return this.constructor.stringify(this)
}
/**
* Append new section to existing ssh config.
*/
append(opts) {
let indent = ' '
outer:
for (const line of this) {
if (RE_SECTION_DIRECTIVE.test(line.param)) {
for (const subline of line.config) {
if (subline.before) {
indent = subline.before
break outer
}
}
}
}
if (this.length > 0) {
let lastLine = this[this.length - 1]
if (lastLine.config) lastLine = lastLine.config[lastLine.config.length - 1]
if (!lastLine.after.trim()) {
lastLine.after = '\n'
}
}
let config = this
let configWas = this
for (const param in opts) {
const line = {
type: DIRECTIVE,
param,
separator: ' ',
value: opts[param],
before: '',
after: '\n'
}
if (RE_SECTION_DIRECTIVE.test(param)) {
config = configWas
config.push(line)
config = line.config = new SSHConfig()
} else {
line.before = config === configWas ? '' : indent
config.push(line)
}
}
return configWas
}
static find(config, opts) {
if (!(opts && ('Host' in opts || 'Match' in opts))) {
throw new Error('Can only find by Host or Match')
}
for (let i = 0; i < config.length; i++) {
const line = config[i]
if (line.type === DIRECTIVE &&
RE_SECTION_DIRECTIVE.test(line.param) &&
line.param in opts &&
opts[line.param] === line.value) {
return [line, i]
}
}
return null
}
/**
* Stringify structured object into ssh config text
*/
static stringify(config) {
let str = ''
let format = line => {
str += line.before
if (line.type === COMMENT) {
str += line.content
}
else if (line.type === DIRECTIVE) {
str += line.quoted || (line.param == 'IdentityFile' && RE_SPACE.test(line.value))
? `${line.param}${line.separator}"${line.value}"`
: `${line.param}${line.separator}${line.value}`
}
str += line.after
if (line.config) {
line.config.forEach(format)
}
}
config.forEach(format)
return str
}
static get DIRECTIVE() {
return DIRECTIVE
}
static get COMMENT() {
return COMMENT
}
/**
* Parse ssh config text into structured object.
*/
static parse(str) {
let i = 0
let chr = next()
let config = new SSHConfig()
let configWas = config
function next() {
return str[i++]
}
function space() {
let spaces = ''
while (RE_SPACE.test(chr)) {
spaces += chr
chr = next()
}
return spaces
}
function linebreak() {
let breaks = ''
while (RE_LINE_BREAK.test(chr)) {
breaks += chr
chr = next()
}
return breaks
}
function option() {
let opt = ''
while (chr && chr !== ' ' && chr !== '=') {
opt += chr
chr = next()
}
return opt
}
function separator() {
let sep = space()
if (chr === '=') {
sep += chr
chr = next()
}
return sep + space()
}
function value() {
let val = ''
while (chr && !RE_LINE_BREAK.test(chr)) {
val += chr
chr = next()
}
return val.trim()
}
function comment() {
let type = COMMENT
let content = ''
while (chr && !RE_LINE_BREAK.test(chr)) {
content += chr
chr = next()
}
return { type, content }
}
function directive() {
let type = DIRECTIVE
return {
type,
param: option(),
separator: separator(),
value: value()
}
}
function line() {
let before = space()
let node = chr === '#' ? comment() : directive()
let after = linebreak()
node.before = before
node.after = after
if (RE_QUOTED.test(node.value)) {
node.value = node.value.replace(RE_QUOTED, '$2')
node.quoted = true
}
return node
}
while (chr) {
let node = line()
if (node.type === DIRECTIVE && RE_SECTION_DIRECTIVE.test(node.param)) {
config = configWas
config.push(node)
config = node.config = new SSHConfig()
}
else {
config.push(node)
}
}
return configWas
}
}
module.exports = SSHConfig