add bash (environment variable export) file format

This commit is contained in:
Adrien Marquès 2018-11-18 19:08:18 +01:00
parent c37893c209
commit 0059dd90ea
8 changed files with 521 additions and 1 deletions

99
internal/cnf/bash.go Normal file
View File

@ -0,0 +1,99 @@
package cnf
import (
lib "github.com/xdrm-brackets/nix-amer/internal/cnf/parser/bash"
"io"
"strings"
)
type bash struct {
data *lib.File
parsed bool
}
// ReadFrom implements io.ReaderFrom
func (d *bash) ReadFrom(_reader io.Reader) (int64, error) {
d.data = new(lib.File)
// 1. get bash decoder
decoder := lib.NewDecoder(_reader)
err := decoder.Decode(d.data)
if err != nil {
return 0, err
}
d.parsed = true
return 0, nil
}
// WriteTo implements io.WriterTo
func (d *bash) WriteTo(_writer io.Writer) (int64, error) {
encoder := lib.NewEncoder(_writer)
return 0, encoder.Encode(d.data)
}
// browse returns the target of a dot-separated path (as an interface{} chain where the last is the target if found)
// if <create> is true, create if does not exist
func (d *bash) browse(_path string, create ...bool) (*lib.Line, bool) {
mustCreate := len(create) > 0 && create[0]
// 1. extract path
path := strings.Split(_path, ".")
field := path[len(path)-1]
// 2. nothing
if len(path) < 1 {
return &lib.Line{}, true
}
// 3. iterate over path / nested fields
for _, line := range d.data.Lines {
if line.Type == lib.ASSIGNMENT && len(line.Components) > 1 && line.Components[1] == field {
return line, true
}
}
// 4. create assignment
if mustCreate {
assignment := &lib.Line{
Type: lib.ASSIGNMENT,
Components: []string{"", field, "", ""},
}
d.data.Lines = append(d.data.Lines, assignment)
return assignment, true
}
return nil, false
}
// Get returns the value of a dot-separated path, and if it exists
func (d *bash) Get(_path string) (string, bool) {
// 1. browse path
last, found := d.browse(_path, true)
if !found {
return "", false
}
// 2. Get last field
return last.Components[2], true
}
// Set the value of a dot-separated path, and creates it if not found
func (d *bash) Set(_path, _value string) bool {
// 1. browse path
last, found := d.browse(_path, true)
if !found {
return false
}
// 2. Set value
last.Components[2] = _value
return true
}

219
internal/cnf/bash_test.go Normal file
View File

@ -0,0 +1,219 @@
package cnf
import (
"bytes"
"testing"
)
func TestBashGet(t *testing.T) {
tests := []struct {
raw string
key string
}{
{"key=value;\n", "key"},
{" \t key=value;\n", "key"},
{"key=value; #comment\n", "key"},
{"\t key=value; #comment\n", "key"},
{"key=value; # comment\n", "key"},
{"\t \tkey=value; # comment\n", "key"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// extract value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != "value" {
t.Errorf("[%d] expected 'value' got '%s'", i, value)
}
}
}
func TestBashSetPathExists(t *testing.T) {
tests := []struct {
raw string
key string
value string
}{
{"key=value;\n", "key", "newvalue"},
{" \t key=value;\n", "key", "newvalue"},
{"key=value; #comment\n", "key", "newvalue"},
{"\t key=value; #comment\n", "key", "newvalue"},
{"key=value; # comment\n", "key", "newvalue"},
{"\t \tkey=value; # comment\n", "key", "newvalue"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
}
}
}
func TestBashSetCreatePath(t *testing.T) {
tests := []struct {
raw string
key string
ignore string // path to field that must be present after transformation
value string
}{
{"ignore=xxx;\n", "key", "ignore", "newvalue"},
{"unknown-line\nignore=xxx;\n", "key", "ignore", "newvalue"},
}
for i, test := range tests {
parser := new(bash)
reader := bytes.NewBufferString(test.raw)
// try to extract value
_, err := parser.ReadFrom(reader)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
continue
}
// check that ignore field is still there
value, found = parser.Get(test.ignore)
if !found {
t.Errorf("[%d] expected ignore field, got none", i)
continue
}
// check value
if value != "xxx" {
t.Errorf("[%d] expected ignore value to be '%s' got '%s'", i, "xxx", value)
continue
}
}
}
func TestBashSetCreateEncode(t *testing.T) {
tests := []struct {
raw string
key string
value string
encoded string
}{
{"ignore=xxx;\n", "key", `"newvalue"`, "ignore=xxx;\nkey=\"newvalue\";\n"},
}
for i, test := range tests {
parser := new(bash)
r, w := bytes.NewBufferString(test.raw), new(bytes.Buffer)
// try to extract value
_, err := parser.ReadFrom(r)
if err != nil {
t.Errorf("[%d] parse error: %s", i, err)
continue
}
// update value
if !parser.Set(test.key, test.value) {
t.Errorf("[%d] cannot set '%s' to '%s'", i, test.key, test.value)
continue
}
// check new value
value, found := parser.Get(test.key)
if !found {
t.Errorf("[%d] expected a result, got none", i)
continue
}
// check value
if value != test.value {
t.Errorf("[%d] expected '%s' got '%s'", i, test.value, value)
continue
}
// writeToBuffer
_, err = parser.WriteTo(w)
if err != nil {
t.Errorf("[%d] unexpected write error <%s>", i, err)
continue
}
encoded := w.String()
// check value
if encoded != test.encoded {
t.Errorf("[%d] wrong encoded value \n-=-=-= HAVE =-=-=-\n%s\n-=-=-= WANT =-=-=-\n%s\n-=-=-=-=-=\n", i, escape(test.encoded), escape(encoded))
continue
}
// try to write
}
}

View File

@ -70,6 +70,8 @@ func loadFromExtension(ext string) ConfigurationFormat {
return new(yaml) return new(yaml)
case ".nginx": case ".nginx":
return new(nginx) return new(nginx)
case ".sh":
return new(bash)
default: default:
return nil return nil
} }
@ -86,7 +88,7 @@ func loadFromContent(path string) (ConfigurationFormat, error) {
defer file.Close() defer file.Close()
// extensions ordered by strictness of the language's syntax // extensions ordered by strictness of the language's syntax
extensions := []string{".json", ".nginx", ".ini", ".yaml"} extensions := []string{".json", ".nginx", ".ini", ".sh", ".yaml"}
// try to load each available extension // try to load each available extension
for _, ext := range extensions { for _, ext := range extensions {

View File

@ -0,0 +1,20 @@
package bash
import (
"io"
)
// File represents a bash file
type File struct {
Lines []*Line
}
// NewDecoder implements parser.T
func NewDecoder(r io.Reader) *Decoder {
return &Decoder{reader: r}
}
// NewEncoder implements parser.T
func NewEncoder(w io.Writer) *Encoder {
return &Encoder{writer: w}
}

View File

@ -0,0 +1,72 @@
package bash
import (
"bufio"
"io"
"regexp"
"strings"
)
// Decoder implements parser.Decoder
type Decoder struct{ reader io.Reader }
// Decode is the main function of the parser.Decoder interface
func (d *Decoder) Decode(v interface{}) error {
// check 'v'
if v == nil {
return ErrNullReceiver
}
vcast, ok := v.(*File)
if !ok {
return ErrInvalidReceiver
}
vcast.Lines = make([]*Line, 0)
r := bufio.NewReader(d.reader)
n := -1 // line number
// regexes
reAssign := regexp.MustCompile(`(?m)^(\s*)([A-Za-z0-9_]+)=([^;]+);?\s*(#.+)?$`)
eof := false
for {
n++
if eof {
break
}
l := &Line{Type: ANY}
// 1. read line
line, err := r.ReadString('\n')
if err == io.EOF {
if len(line) > 0 {
eof = true
} else {
break
}
} else if err != nil {
return &LineError{n, err}
}
line = strings.Trim(line, "\r\n")
// 2. ignore empty
if len(strings.Trim(line, " \t\r\n")) < 1 {
continue
}
// 3. assignment
match := reAssign.FindStringSubmatch(line)
if match != nil {
l.Type = ASSIGNMENT
l.Components = match[1:]
}
// 4. add to file
vcast.Lines = append(vcast.Lines, l)
}
return nil
}

View File

@ -0,0 +1,59 @@
package bash
import (
"fmt"
"io"
"strings"
)
// Encoder implements parser.Encoder
type Encoder struct {
writer io.Writer
prefix []byte
indent []byte
}
// Encode is the main function of the parser.Encoder interface
func (e *Encoder) Encode(v interface{}) error {
// check 'v'
vcast, ok := v.(*File)
if !ok {
return ErrInvalidReceiver
}
// empty config
if len(vcast.Lines) < 1 {
return nil
}
for _, line := range vcast.Lines {
// line representation
repr := ""
// ASSIGNMENT
if line.Type == ASSIGNMENT {
repr = fmt.Sprintf("%s%s=%s;", line.Components[0], line.Components[1], line.Components[2])
// optional comment
if len(line.Components[3]) > 0 {
repr += fmt.Sprintf(" #%s", line.Components[3])
}
// ANY
} else {
repr = strings.Join(line.Components, "")
}
repr += "\n"
_, err := e.writer.Write([]byte(repr))
if err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,23 @@
package bash
import (
"fmt"
"github.com/xdrm-brackets/nix-amer/internal/clifmt"
)
// ErrNullReceiver is raised when a null receiver is provided
var ErrNullReceiver = fmt.Errorf("receiver must not be null")
// ErrInvalidReceiver is raised when an invalid receiver is provided
var ErrInvalidReceiver = fmt.Errorf("receiver must be compatible with *File")
// LineError wraps errors with a line index
type LineError struct {
Line int
Err error
}
// Error implements Error
func (le LineError) Error() string {
return fmt.Sprintf(":%d %s", le.Line, clifmt.Color(31, le.Err.Error()))
}

View File

@ -0,0 +1,26 @@
package bash
// LineType enumerates available line types
type LineType byte
const (
// ANY is the default line type (all except assignments)
ANY LineType = iota
// ASSIGNMENT line
ASSIGNMENT
)
// Line represents a meaningful line
type Line struct {
// Type of line
Type LineType
// Components of the line
//
// When Type = ASSIGNMENT :
// [0] = indentation (spaces and tabs)
// [1] = variable name
// [2] = variable value
// [3] = comment (optional)
Components []string
}