add bash (environment variable export) file format
This commit is contained in:
parent
c37893c209
commit
0059dd90ea
|
@ -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
|
||||||
|
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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 {
|
||||||
|
|
|
@ -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}
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
|
@ -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()))
|
||||||
|
}
|
|
@ -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
|
||||||
|
}
|
Loading…
Reference in New Issue