diff --git a/internal/cnf/bash.go b/internal/cnf/bash.go new file mode 100644 index 0000000..c2ca47d --- /dev/null +++ b/internal/cnf/bash.go @@ -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 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 + +} diff --git a/internal/cnf/bash_test.go b/internal/cnf/bash_test.go new file mode 100644 index 0000000..5ab3586 --- /dev/null +++ b/internal/cnf/bash_test.go @@ -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 + } + +} diff --git a/internal/cnf/loader.go b/internal/cnf/loader.go index bec0f63..1a00956 100644 --- a/internal/cnf/loader.go +++ b/internal/cnf/loader.go @@ -70,6 +70,8 @@ func loadFromExtension(ext string) ConfigurationFormat { return new(yaml) case ".nginx": return new(nginx) + case ".sh": + return new(bash) default: return nil } @@ -86,7 +88,7 @@ func loadFromContent(path string) (ConfigurationFormat, error) { defer file.Close() // 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 for _, ext := range extensions { diff --git a/internal/cnf/parser/bash/bash.go b/internal/cnf/parser/bash/bash.go new file mode 100644 index 0000000..7159657 --- /dev/null +++ b/internal/cnf/parser/bash/bash.go @@ -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} +} diff --git a/internal/cnf/parser/bash/decoder.go b/internal/cnf/parser/bash/decoder.go new file mode 100644 index 0000000..65dda6f --- /dev/null +++ b/internal/cnf/parser/bash/decoder.go @@ -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 +} diff --git a/internal/cnf/parser/bash/encoder.go b/internal/cnf/parser/bash/encoder.go new file mode 100644 index 0000000..a7c63e9 --- /dev/null +++ b/internal/cnf/parser/bash/encoder.go @@ -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 +} diff --git a/internal/cnf/parser/bash/errors.go b/internal/cnf/parser/bash/errors.go new file mode 100644 index 0000000..075ef53 --- /dev/null +++ b/internal/cnf/parser/bash/errors.go @@ -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())) +} diff --git a/internal/cnf/parser/bash/line.go b/internal/cnf/parser/bash/line.go new file mode 100644 index 0000000..fae1929 --- /dev/null +++ b/internal/cnf/parser/bash/line.go @@ -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 +}