Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: cyjake/ssh-config
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v4.4.4
Choose a base ref
...
head repository: cyjake/ssh-config
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: v5.0.0
Choose a head ref
  • 2 commits
  • 7 files changed
  • 1 contributor

Commits on Aug 9, 2024

  1. fix: reserve quotation marks when handling multiple values (#85)

    fixes #84
    cyjake authored Aug 9, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    3dfb490 View commit details
  2. release: v5.0.0

    cyjake committed Aug 9, 2024

    Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
    Copy the full SHA
    ab4a6eb View commit details
Showing with 201 additions and 65 deletions.
  1. +9 −0 History.md
  2. +1 −1 package.json
  3. +33 −20 src/ssh-config.ts
  4. +9 −0 test/helpers.ts
  5. +8 −4 test/unit/parse.test.ts
  6. +116 −37 test/unit/ssh-config.test.ts
  7. +25 −3 test/unit/stringify.test.ts
9 changes: 9 additions & 0 deletions History.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
5.0.0 / 2024-08-09
==================

## What's Changed
* fix: reserve quotation marks when handling multiple values by @cyjake in https://github.com/cyjake/ssh-config/pull/85


**Full Changelog**: https://github.com/cyjake/ssh-config/compare/v4.4.4...v5.0.0

4.4.4 / 2024-05-09
==================

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "ssh-config",
"description": "SSH config parser and stringifier",
"version": "4.4.4",
"version": "5.0.0",
"author": "Chen Yangjian (https://www.cyj.me)",
"repository": {
"type": "git",
53 changes: 33 additions & 20 deletions src/ssh-config.ts
Original file line number Diff line number Diff line change
@@ -25,7 +25,7 @@ export interface Directive {
after: string;
param: string;
separator: Separator;
value: string | string[];
value: string | { val: string, separator: string, quoted?: boolean }[];
quoted?: boolean;
}

@@ -34,7 +34,7 @@ export interface Section extends Directive {
}

export interface Match extends Section {
criteria: Record<string, string | string[]>
criteria: Record<string, string | { val: string, separator: string, quoted?: boolean }[]>
}

export interface Comment {
@@ -125,8 +125,9 @@ function match(criteria: Match['criteria'], context: ComputeContext): boolean {

for (const key in criteria) {
const criterion = criteria[key]
const values = Array.isArray(criterion) ? criterion.map(({ val }) => val) : criterion

if (!testCriterion(key, criterion)) {
if (!testCriterion(key, values)) {
return false
}
}
@@ -207,7 +208,7 @@ export default class SSHConfig extends Array<Line> {
const doPass = () => {
for (const line of this) {
if (line.type !== LineType.DIRECTIVE) continue
if (line.param === 'Host' && glob(line.value, context.params.Host)) {
if (line.param === 'Host' && glob(Array.isArray(line.value) ? line.value.map(({ val }) => val) : line.value, context.params.Host)) {
let canonicalizeHostName = false
let canonicalDomains: string[] = []
setProperty(line.param, line.value)
@@ -218,7 +219,7 @@ export default class SSHConfig extends Array<Line> {
canonicalizeHostName = true
}
if (/^CanonicalDomains$/i.test(subline.param) && Array.isArray(subline.value)) {
canonicalDomains = subline.value
canonicalDomains = subline.value.map(({ val }) => val)
}
}
}
@@ -333,7 +334,7 @@ export default class SSHConfig extends Array<Line> {
type: LineType.DIRECTIVE,
param,
separator: ' ',
value,
value: Array.isArray(value) ? value.map((val, i) => ({ val, separator: i === 0 ? '' : ' ' })) : value,
before: sectionLineFound ? indent : indent.replace(/ |\t/, ''),
after: '\n',
}
@@ -386,7 +387,7 @@ export default class SSHConfig extends Array<Line> {
type: LineType.DIRECTIVE,
param,
separator: ' ',
value,
value: Array.isArray(value) ? value.map((val, i) => ({ val, separator: i === 0 ? '' : ' ' })) : value,
before: '',
after: '\n',
}
@@ -530,8 +531,13 @@ export function parse(text: string): SSHConfig {
// Host * !local.dev
// Host "foo bar"
function values() {
const results: string[] = []
const results: { val: string, separator: string, quoted: boolean }[] = []
let val = ''
// whether current value is quoted or not
let valQuoted = false
// the separator preceding current value
let valSeparator = ' '
// whether current context is within quotations or not
let quoted = false
let escaped = false

@@ -548,11 +554,14 @@ export function parse(text: string): SSHConfig {
}
else if (quoted) {
val += chr
valQuoted = true
}
else if (/[ \t=]/.test(chr)) {
if (val) {
results.push(val)
results.push({ val, separator: valSeparator, quoted: valQuoted })
val = ''
valQuoted = false
valSeparator = chr
}
// otherwise ignore the space
}
@@ -567,10 +576,10 @@ export function parse(text: string): SSHConfig {
}

if (quoted || escaped) {
throw new Error(`Unexpected line break at ${results.concat(val).join(' ')}`)
throw new Error(`Unexpected line break at ${results.map(({ val }) => val).concat(val).join(' ')}`)
}
if (val) results.push(val)
return results.length > 1 ? results : results[0]
if (val) results.push({ val, separator: valSeparator, quoted: valQuoted })
return results.length > 1 ? results : results[0].val
}

function directive() {
@@ -592,12 +601,12 @@ export function parse(text: string): SSHConfig {
const criteria: Match['criteria'] = {}

if (typeof result.value === 'string') {
result.value = [result.value]
result.value = [{ val: result.value, separator: '', quoted: result.quoted }]
}

let i = 0
while (i < result.value.length) {
const keyword = result.value[i]
const { val: keyword } = result.value[i]

switch (keyword.toLowerCase()) {
case 'all':
@@ -610,7 +619,7 @@ export function parse(text: string): SSHConfig {
if (i + 1 >= result.value.length) {
throw new Error(`Missing value for match criteria ${keyword}`)
}
criteria[keyword] = result.value[i + 1]
criteria[keyword] = result.value[i + 1].val
i += 2
break
}
@@ -663,7 +672,11 @@ export function stringify(config: SSHConfig): string {

function formatValue(value: string | string[] | Record<string, any>, quoted: boolean) {
if (Array.isArray(value)) {
return value.map(chunk => formatValue(chunk, RE_SPACE.test(chunk))).join(' ')
let result = ''
for (const { val, separator, quoted } of value) {
result += (result ? separator : '') + formatValue(val, quoted || RE_SPACE.test(val))
}
return result
}
return quoted ? `"${value}"` : value
}
@@ -675,15 +688,15 @@ export function stringify(config: SSHConfig): string {
return `${line.param}${line.separator}${value}`
}

const format = line => {
const format = (line: Line) => {
str += line.before

if (line.type === LineType.COMMENT) {
str += line.content
}
else if (line.type === LineType.DIRECTIVE && MULTIPLE_VALUE_PROPS.includes(line.param)) {
[].concat(line.value).forEach(function (value, i, values) {
str += formatDirective({ ...line, value })
(Array.isArray(line.value) ? line.value : [line.value]).forEach((value, i, values) => {
str += formatDirective({ ...line, value: typeof value !== 'string' ? value.val : value })
if (i < values.length - 1) str += `\n${line.before}`
})
}
@@ -693,7 +706,7 @@ export function stringify(config: SSHConfig): string {

str += line.after

if (line.config) {
if ('config' in line) {
line.config.forEach(format)
}
}
9 changes: 9 additions & 0 deletions test/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const stripPattern = /^[ \t]*(?=[^\s]+)/mg

export function heredoc(text: string) {
const indentLen = text.match(stripPattern)!.reduce((min, line) => Math.min(min, line.length), Infinity)
const indent = new RegExp('^[ \\t]{' + indentLen + '}', 'mg')
return indentLen > 0
? text.replace(indent, '').trimStart().replace(/ +?$/, '')
: text
}
12 changes: 8 additions & 4 deletions test/unit/parse.test.ts
Original file line number Diff line number Diff line change
@@ -145,7 +145,8 @@ describe('parse', function() {
const config = parse('ProxyCommand ssh -W "%h:%p" firewall.example.org')
assert.equal(config[0].type, DIRECTIVE)
assert.equal(config[0].param, 'ProxyCommand')
assert.deepEqual(config[0].value, ['ssh', '-W', '%h:%p', 'firewall.example.org'])
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), ['ssh', '-W', '%h:%p', 'firewall.example.org'])
})

// https://github.com/microsoft/vscode-remote-release/issues/5562
@@ -159,7 +160,8 @@ describe('parse', function() {
assert.ok('config' in config[0])
assert.equal(config[0].config[0].type, DIRECTIVE)
assert.equal(config[0].config[0].param, 'ProxyCommand')
assert.deepEqual(config[0].config[0].value, ['C:\\foo bar\\baz.exe', 'arg', 'arg', 'arg'])
assert.ok(Array.isArray(config[0].config[0].value))
assert.deepEqual(config[0].config[0].value.map(({ val }) => val), ['C:\\foo bar\\baz.exe', 'arg', 'arg', 'arg'])
})

it('.parse open ended values', function() {
@@ -180,7 +182,8 @@ describe('parse', function() {

assert.equal(config[0].type, DIRECTIVE)
assert.equal(config[0].param, 'Host')
assert.deepEqual(config[0].value, [
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), [
'foo',
'!*.bar',
'baz ham',
@@ -192,7 +195,8 @@ describe('parse', function() {
const config = parse('Host me local wi*ldcard? thisVM "two words"')

assert.equal(config[0].type, DIRECTIVE)
assert.deepEqual(config[0].value, [
assert.ok(Array.isArray(config[0].value))
assert.deepEqual(config[0].value.map(({ val }) => val), [
'me',
'local',
'wi*ldcard?',
Loading