Add alpha SSH config parser

The error handling is nonexistent and there's no easy way to get
data out. But we can parse a SSH config file into a Go struct, and
roundtrip that struct back to a file that looks (roughly) the same.
This commit is contained in:
Kevin Burke
2017-04-16 21:13:13 -07:00
commit 29f594a81c
9 changed files with 701 additions and 0 deletions

147
parser.go Normal file
View File

@@ -0,0 +1,147 @@
package ssh_config
import (
"fmt"
"strings"
)
type sshParser struct {
flow chan token
config *Config
tokensBuffer []token
currentTable []string
seenTableKeys []string
}
type sshParserStateFn func() sshParserStateFn
// Formats and panics an error message based on a token
func (p *sshParser) raiseError(tok *token, msg string, args ...interface{}) {
panic(tok.Position.String() + ": " + fmt.Sprintf(msg, args...))
}
func (p *sshParser) run() {
for state := p.parseStart; state != nil; {
state = state()
}
}
func (p *sshParser) peek() *token {
if len(p.tokensBuffer) != 0 {
return &(p.tokensBuffer[0])
}
tok, ok := <-p.flow
if !ok {
return nil
}
p.tokensBuffer = append(p.tokensBuffer, tok)
return &tok
}
func (p *sshParser) getToken() *token {
if len(p.tokensBuffer) != 0 {
tok := p.tokensBuffer[0]
p.tokensBuffer = p.tokensBuffer[1:]
return &tok
}
tok, ok := <-p.flow
if !ok {
return nil
}
return &tok
}
func (p *sshParser) parseStart() sshParserStateFn {
tok := p.peek()
// end of stream, parsing is finished
if tok == nil {
return nil
}
switch tok.typ {
case tokenComment, tokenEmptyLine:
return p.parseComment
case tokenKey:
return p.parseKV
case tokenEOF:
return nil
default:
p.raiseError(tok, fmt.Sprintf("unexpected token %q\n", tok))
}
return nil
}
func (p *sshParser) parseKV() sshParserStateFn {
key := p.getToken()
p.assume(tokenString)
val := p.getToken()
comment := ""
tok := p.peek()
if tok.typ == tokenComment && tok.Position.Line == val.Position.Line {
tok = p.getToken()
comment = tok.val
}
if key.val == "Host" {
patterns := strings.Split(val.val, " ")
for i := range patterns {
if patterns[i] == "" {
patterns = append(patterns[:i], patterns[i+1:]...)
}
}
p.config.Hosts = append(p.config.Hosts, &Host{
Patterns: patterns,
Nodes: make([]Node, 0),
EOLComment: comment,
})
return p.parseStart
}
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
kv := &KV{
Key: key.val,
Value: val.val,
Comment: comment,
leadingSpace: uint16(key.Position.Col) - 1,
position: key.Position,
}
lastHost.Nodes = append(lastHost.Nodes, kv)
return p.parseStart
}
func (p *sshParser) parseComment() sshParserStateFn {
comment := p.getToken()
lastHost := p.config.Hosts[len(p.config.Hosts)-1]
lastHost.Nodes = append(lastHost.Nodes, &Empty{
Comment: comment.val,
// account for the "#" as well
leadingSpace: comment.Position.Col - 2,
position: comment.Position,
})
return p.parseStart
}
// assume peeks at the next token and ensures it's the right type
func (p *sshParser) assume(typ tokenType) {
tok := p.peek()
if tok == nil {
p.raiseError(tok, "was expecting token %s, but token stream is empty", tok)
}
if tok.typ != typ {
p.raiseError(tok, "was expecting token %s, but got %s instead", typ, tok)
}
}
func parseSSH(flow chan token) *Config {
result := newConfig()
result.position = Position{1, 1}
parser := &sshParser{
flow: flow,
config: result,
tokensBuffer: make([]token, 0),
currentTable: make([]string, 0),
seenTableKeys: make([]string, 0),
}
parser.run()
return result
}