diff --git a/hierarchy.go b/hierarchy.go new file mode 100644 index 0000000..09d06d7 --- /dev/null +++ b/hierarchy.go @@ -0,0 +1,65 @@ +package mtree + +import ( + "fmt" + "io" + "sort" + "strings" +) + +// DirectoryHierarchy is the mapped structure for an mtree directory hierarchy +// spec +type DirectoryHierarchy struct { + Entries []Entry +} + +// WriteTo simplifies the output of the resulting hierarchy spec +func (dh DirectoryHierarchy) WriteTo(w io.Writer) (n int64, err error) { + sort.Sort(byPos(dh.Entries)) + var sum int64 + for _, e := range dh.Entries { + i, err := io.WriteString(w, e.String()+"\n") + if err != nil { + return sum, err + } + sum += int64(i) + } + return sum, nil +} + +type byPos []Entry + +func (bp byPos) Len() int { return len(bp) } +func (bp byPos) Less(i, j int) bool { return bp[i].Pos < bp[j].Pos } +func (bp byPos) Swap(i, j int) { bp[i], bp[j] = bp[j], bp[i] } + +// Entry is each component of content in the mtree spec file +type Entry struct { + Pos int // order in the spec + Raw string // file or directory name + Name string // file or directory name + Keywords []string // TODO(vbatts) maybe a keyword typed set of values? + Type EntryType +} + +func (e Entry) String() string { + if e.Raw != "" { + return e.Raw + } + // TODO(vbatts) if type is RelativeType and a keyword of not type=dir + return fmt.Sprintf("%s %s", e.Name, strings.Join(e.Keywords, " ")) +} + +// EntryType are the formats of lines in an mtree spec file +type EntryType int + +// The types of lines to be found in an mtree spec file +const ( + SignatureType EntryType = iota // first line of the file, like `#mtree v2.0` + BlankType // blank lines are ignored + CommentType // Lines beginning with `#` are ignored + SpecialType // line that has `/` prefix issue a "special" command (currently only /set and /unset) + RelativeType // if the first white-space delimited word does not have a '/' in it. Options/keywords are applied. + DotDotType // .. - A relative path step. keywords/options are ignored + FullType // if the first word on the line has a `/` after the first character, it interpretted as a file pathname with options +) diff --git a/mtree.go b/mtree.go deleted file mode 100644 index 686cd16..0000000 --- a/mtree.go +++ /dev/null @@ -1,58 +0,0 @@ -package mtree - -import "io" - -// Positioner responds with the newline delimited position -type Positioner interface { - Pos() int -} - -// DirectoryHierarchy is the mapped structure for an mtree directory hierarchy -// spec -type DirectoryHierarchy struct { - Comments []Comment - Entries []Entry -} - -// WriteTo simplifies the output of the resulting hierarchy spec -func (dh DirectoryHierarchy) WriteTo(w io.Writer) (n int64, err error) { - return 0, nil -} - -// Comment stores arbitrary metadata for the spec. Sometimes "user", "machine", -// "tree", and "date". But most of the time, it includes the relative path of -// the directory being stepped into. Or a "signature" like `#mtree v2.0`, -type Comment struct { - Position int - Str string - // TODO(vbatts) include a comment line parser -} - -// Pos returns the line of this comment -func (c Comment) Pos() int { - return c.Position -} - -// Entry is each component of content in the mtree spec file -type Entry struct { - Position int // order in the spec - Name string // file or directory name - Keywords []string // TODO(vbatts) maybe a keyword typed set of values? - str string // raw string. needed? - Type EntryType -} - -// Pos returns the line of this comment -func (e Entry) Pos() int { - return e.Position -} - -type EntryType int - -const ( - SpecialType int = iota // line that has `/` prefix issue a "special" command (currently only /set and /unset) - FileType // indented line - DirectoryType // ^ matched line, that is not /set - PathStepType // .. - keywords/options are ignored - FullType // if the first word on the line has a `/` after the first character, it interpretted as a file pathname with options -) diff --git a/mtree_test.go b/mtree_test.go index b40a851..b4f12bd 100644 --- a/mtree_test.go +++ b/mtree_test.go @@ -1,11 +1,8 @@ package mtree import ( - "bufio" "fmt" - "log" "os" - "strings" "testing" ) @@ -18,24 +15,22 @@ func TestParser(t *testing.T) { func() { fh, err := os.Open(file) if err != nil { - log.Println(err) + t.Error(err) return } defer fh.Close() - s := bufio.NewScanner(fh) - for s.Scan() { - str := s.Text() - switch { - case strings.HasPrefix(str, "#"): - continue - default: - } - fmt.Printf("%q\n", str) + dh, err := ParseSpec(fh) + if err != nil { + t.Error(err) } - if err := s.Err(); err != nil { - log.Println("ERROR:", err) + fmt.Printf("%q", dh) + + _, err = dh.WriteTo(os.Stdout) + if err != nil { + t.Error(err) } + }() } } diff --git a/parse.go b/parse.go new file mode 100644 index 0000000..9d3b25e --- /dev/null +++ b/parse.go @@ -0,0 +1,77 @@ +package mtree + +import ( + "bufio" + "io" + "strings" +) + +func ParseSpec(r io.Reader) (*DirectoryHierarchy, error) { + s := bufio.NewScanner(r) + i := int(0) + dh := DirectoryHierarchy{} + for s.Scan() { + str := s.Text() + e := Entry{Pos: i} + switch { + case strings.HasPrefix(str, "#"): + e.Raw = str + if strings.HasPrefix(str, "#mtree") { + e.Type = SignatureType + } else { + e.Type = CommentType + // from here, the comment could be "# key: value" metadata + // or a relative path hint + } + case str == "": + e.Type = BlankType + // nothing else to do here + case strings.HasPrefix(str, "/"): + e.Type = SpecialType + // collapse any escaped newlines + for { + if strings.HasSuffix(str, `\`) { + str = str[:len(str)-1] + s.Scan() + str += s.Text() + } else { + break + } + } + // parse the options + f := strings.Fields(str) + e.Name = f[0] + e.Keywords = f[1:] + case len(strings.Fields(str)) > 0 && strings.Fields(str)[0] == "..": + e.Type = DotDotType + e.Raw = str + // nothing else to do here + case len(strings.Fields(str)) > 0: + // collapse any escaped newlines + for { + if strings.HasSuffix(str, `\`) { + str = str[:len(str)-1] + s.Scan() + str += s.Text() + } else { + break + } + } + // parse the options + f := strings.Fields(str) + if strings.Contains(str, "/") { + e.Type = FullType + } else { + e.Type = RelativeType + } + e.Name = f[0] + e.Keywords = f[1:] + default: + // TODO(vbatts) log a warning? + continue + } + dh.Entries = append(dh.Entries, e) + i++ + } + return &dh, s.Err() +}