diff --git a/cmd/gomtree/cmd/mutate.go b/cmd/gomtree/cmd/mutate.go new file mode 100644 index 0000000..92f35e0 --- /dev/null +++ b/cmd/gomtree/cmd/mutate.go @@ -0,0 +1,184 @@ +package cmd + +import ( + "fmt" + "io" + "math" + "os" + "strings" + + cli "github.com/urfave/cli/v2" + "github.com/vbatts/go-mtree" +) + +func NewMutateCommand() *cli.Command { + + return &cli.Command{ + Name: "mutate", + Usage: "mutate an mtree", + Description: `Mutate an mtree to have different shapes. +TODO: more info examples`, + Action: mutateAction, + ArgsUsage: " [path to output]", + Flags: []cli.Flag{ + &cli.StringSliceFlag{ + Name: "strip-prefix", + }, + &cli.BoolFlag{ + Name: "keep-comments", + Value: false, + }, + &cli.BoolFlag{ + Name: "keep-blank", + Value: false, + }, + &cli.StringFlag{ + Name: "output", + TakesFile: true, + }, + }, + } +} + +func mutateAction(c *cli.Context) error { + mtreePath := c.Args().Get(0) + outputPath := c.Args().Get(1) + stripPrexies := c.StringSlice("strip-prefix") + keepComments := c.Bool("keep-comments") + keepBlank := c.Bool("keep-blank") + + if mtreePath == "" { + return fmt.Errorf("mtree path is required.") + } else if outputPath == "" { + outputPath = mtreePath + } + + file, err := os.Open(mtreePath) + if err != nil { + return fmt.Errorf("opening %s: %w", mtreePath, err) + } + + spec, err := mtree.ParseSpec(file) + if err != nil { + return fmt.Errorf("parsing mtree %s: %w", mtreePath, err) + } + + stripPrefixVisitor := stripPrefixVisitor{ + prefixes: stripPrexies, + } + tidyVisitor := tidyVisitor{ + keepComments: keepComments, + keepBlank: keepBlank, + } + visitors := []Visitor{ + &stripPrefixVisitor, + &tidyVisitor, + } + + dropped := map[int]bool{} + entries := []mtree.Entry{} + +skip: + for _, entry := range spec.Entries { + if entry.Parent != nil { + if _, ok := dropped[entry.Parent.Pos]; ok { + if entry.Type == mtree.DotDotType { + // directory for this .. has been dropped so shall this + continue + } + entry.Parent = entry.Parent.Parent + // TODO: i am not sure if this is the correct behavior + entry.Raw = strings.TrimPrefix(entry.Raw, " ") + } + } + + for _, visitor := range visitors { + drop, err := visitor.Visit(&entry) + if err != nil { + return err + } + + if drop { + dropped[entry.Pos] = true + continue skip + } + } + + entries = append(entries, entry) + } + + spec.Entries = entries + + var writer io.Writer = os.Stdout + if outputPath != "-" { + writer, err = os.Create(outputPath) + if err != nil { + return fmt.Errorf("creating output %s: %w", outputPath, err) + } + } + + _, err = spec.WriteTo(writer) + if err != nil { + return fmt.Errorf("writing mtree %s: %w", outputPath, err) + } + + return nil +} + +type Visitor interface { + Visit(entry *mtree.Entry) (bool, error) +} + +type tidyVisitor struct { + keepComments bool + keepBlank bool +} + +func (m *tidyVisitor) Visit(entry *mtree.Entry) (bool, error) { + if !m.keepComments && entry.Type == mtree.CommentType { + return true, nil + } else if !m.keepBlank && entry.Type == mtree.BlankType { + return true, nil + } + return false, nil +} + +type stripPrefixVisitor struct { + prefixes []string +} + +func (m *stripPrefixVisitor) Visit(entry *mtree.Entry) (bool, error) { + if entry.Type != mtree.FullType && entry.Type != mtree.RelativeType { + return false, nil + } + + fp, err := entry.Path() + if err != nil { + return false, err + } + pathSegments := strings.Split(fp, "/") + + for _, prefix := range m.prefixes { + + prefixSegments := strings.Split(prefix, "/") + minLen := int(math.Min(float64(len(pathSegments)), float64(len(prefixSegments)))) + matches := make([]string, minLen) + for i := 0; i < minLen; i++ { + if pathSegments[i] == prefixSegments[i] { + matches[i] = prefixSegments[i] + } + } + + strip := strings.Join(matches, "/") + if entry.Type == mtree.FullType { + entry.Name = strings.TrimPrefix(entry.Name, strip) + entry.Name = strings.TrimPrefix(entry.Name, "/") + if entry.Name == "" { + return true, nil + } + } else if fp == strip { + return true, nil + } + } + return false, nil +} diff --git a/cmd/gomtree/main.go b/cmd/gomtree/main.go index 90fff23..3de5825 100644 --- a/cmd/gomtree/main.go +++ b/cmd/gomtree/main.go @@ -42,6 +42,7 @@ to support xattrs and interacting with tar archives.` } app.Commands = []*cli.Command{ cmd.NewValidateCommand(), + cmd.NewMutateCommand(), } // Unfortunately urfave/cli is not at good at using DefaultCommand diff --git a/entry.go b/entry.go index 366a15b..084d595 100644 --- a/entry.go +++ b/entry.go @@ -134,7 +134,10 @@ func (e Entry) String() string { if e.Type == SpecialType || e.Type == FullType || inKeyValSlice("type=dir", e.Keywords) { return fmt.Sprintf("%s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " ")) } - return fmt.Sprintf(" %s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " ")) + if e.Parent != nil && e.Type != DotDotType { + return fmt.Sprintf(" %s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " ")) + } + return fmt.Sprintf("%s %s", e.Name, strings.Join(KeyValToString(e.Keywords), " ")) } // AllKeys returns the full set of KeyVal for the given entry, based on the diff --git a/parse.go b/parse.go index e385eaf..8cafba0 100644 --- a/parse.go +++ b/parse.go @@ -58,6 +58,7 @@ func ParseSpec(r io.Reader) (*DirectoryHierarchy, error) { e.Type = DotDotType e.Raw = str if creator.curDir != nil { + e.Parent = creator.curDir creator.curDir = creator.curDir.Parent } // nothing else to do here diff --git a/testdata/relative.mtree b/testdata/relative.mtree new file mode 100644 index 0000000..456e49e --- /dev/null +++ b/testdata/relative.mtree @@ -0,0 +1,15 @@ +# ./lib +lib type=dir mode=0644 + foo mode=0644 size=12288 time=1457644483.833957552 type=file + +.. + +./lib type=dir mode=0644 + + +ayo mode=0644 size=12288 time=1457644483.833957552 type=file + +lib/dir/sub type=dir +lib/dir/sub/file.txt type=file + +lib/dir/PKG.info type=file mode=0644