From 4b9dee4b9b6697f91f26b8b083aaf32b91b68c1d Mon Sep 17 00:00:00 2001 From: Adam Chalkley Date: Sat, 17 Aug 2019 23:36:53 -0500 Subject: [PATCH] Initial Prototype This initial prototype supports: - Matching on specified file patterns - Flat (single-level) or recursive search - Keeping a specified number of older or newer matches - Limiting search to specified list of extensions - Toggling file removal (read-only by default) - Go modules (vs classic GOPATH setup) - Brief overview, examples for testing purposes refs #2, #4, #6 --- .gitignore | 6 + README.md | 111 ++++++++++ config.go | 72 ++++++ go.mod | 9 + go.sum | 15 ++ main.go | 113 ++++++++++ main_test.go | 26 +++ matches.go | 94 ++++++++ paths.go | 206 ++++++++++++++++++ .../sample_files_list_dev_web_app_server.txt | 183 ++++++++++++++++ testing/test.go | 135 ++++++++++++ 11 files changed, 970 insertions(+) create mode 100644 .gitignore create mode 100644 config.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go create mode 100644 matches.go create mode 100644 paths.go create mode 100644 testing/sample_files_list_dev_web_app_server.txt create mode 100644 testing/test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4bab0b0b --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.test +testing/ +*.exe + +# When building on non-Windows platform +elbow diff --git a/README.md b/README.md index 66ec1ab8..b6aee648 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,113 @@ # elbow + Elbow, Elbow grease. + +- [elbow](#elbow) + - [Purpose](#purpose) + - [Gotchas](#gotchas) + - [Setup test environment](#setup-test-environment) + - [Examples](#examples) + - [Overview](#overview) + - [Prune `.war` files from each branch recursively, keep newest 2](#prune-war-files-from-each-branch-recursively-keep-newest-2) + - [Build and run from test area, no options](#build-and-run-from-test-area-no-options) + - [References](#references) + - [Configuration object](#configuration-object) + - [Sorting files](#sorting-files) + - [Path/File Existence](#pathfile-existence) + - [Slice management](#slice-management) + +## Purpose + +Prune content matching specific patterns, either in a single directory or +recursively through a directory tree. The primary goal is to use this +application from a cron job to perform routine pruning of generated files that +would otherwise completely clog a filesystem. + +## Gotchas + +- File extensions are *case-sensitive* +- File name patterns are *case-sensitive* +- File name patterns, much like shell globs, can match more than you might + wish. Test carefully and do not provide the `--remove` flag until you are + ready to actually prune the content. + +## Setup test environment + +1. Launch container, VM or WSL instance +1. `cd /path/to/create/test/files` +1. `touch $(cat /path/to/this/repo/testing/sample_files_list_dev_web_app_server.txt)` +1. `cd /path/to/this/repo` +1. `go build` + +See next section for examples of running the app against the test files. + +## Examples + +### Overview + +The following steps illustrate a rough, overall idea of what this application +is intended to do. The steps illustrate building and running the application +from within an Ubuntu Linux Subsystem for Windows (WSL) instance. The `/t` +volume is present on the Windows host. + +The file extension used in the examples is for a `WAR` file that is generated +on a build system that our team maintains. The idea is that this application +could be run as a cron job to help ensure that only X copies (the most recent) +for each of three branches remain on the build box. + +There are better aproaches to managing those build artifacts, but that is the +problem that this tool seeks to solve in a simple way. + +### Prune `.war` files from each branch recursively, keep newest 2 + +```ShellSession +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow --path /tmp --extension ".war" --pattern "-master-" --keep 2 --recurse --remove +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow --path /tmp --extension ".war" --pattern "-masterqa-" --keep 2 --recurse --remove +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow --path /tmp --extension ".war" --pattern "-masterdev-" --keep 2 --recurse --remove +``` + +```ShellSession +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow -p /tmp -e ".war" -fp "-master-" -k 2 -r +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow -p /tmp -e ".war" -fp "-masterqa-" -k 2 -r +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow -p /tmp -e ".war" -fp "-masterdev-" -k 2 -r +``` + +Leave off `--remove` to display what *would* be removed. + +### Build and run from test area, no options + +This results in Help text being displayed. At a minimum, the path to process +has to be provided for the application to proceed. + +```ShellSession +cd /mnt/t/github/elbow; go build; cp -vf elbow /tmp/; cd /tmp/; ./elbow +``` + +## References + +The following unordered list of sites/examples provided guidance while +developing this application. Depending on when consulted, the original code +written based on that guidance may no longer be present in the active version +of this application. + +### Configuration object + +- +- +- +- +- + +### Sorting files + +- + +### Path/File Existence + +- + +### Slice management + +- +- +- diff --git a/config.go b/config.go new file mode 100644 index 00000000..5a8917a4 --- /dev/null +++ b/config.go @@ -0,0 +1,72 @@ +package main + +import ( + "github.com/integrii/flaggy" +) + +// Config represents a collection of configuration settings for this +// application. Config is created as early as possible upon application +// startup. +type Config struct { + FilePattern string + FileExtensions []string + StartPath string + RecursiveSearch bool + FilesToKeep int + KeepOldest bool + Remove bool +} + +// NewConfig returns a new Config pointer that can be chained with builder +// methods to set multiple configuration values inline without using pointers. +func NewConfig() *Config { + + // Explicitly initialize with intended defaults + return &Config{ + StartPath: "", + FilePattern: "", + // NOTE: This creates an empty slice (not nil since there is an + // underlying array of zero length) FileExtensions: []string{}, + // + // Leave at default value of nil slice instead by not providing a + // value here + // FileExtensions: []string, + FilesToKeep: 0, + RecursiveSearch: false, + KeepOldest: false, + Remove: false, + } + +} + +// SetupFlags applies settings provided by command-line flags +// TODO: Pull out +func (c *Config) SetupFlags(appName string, appDesc string) *Config { + + flaggy.SetName(appName) + flaggy.SetDescription(appDesc) + + flaggy.DefaultParser.ShowHelpOnUnexpected = true + + // Add flags + flaggy.String(&c.StartPath, "p", "path", "Path to process") + flaggy.String(&c.FilePattern, "fp", "pattern", "Substring pattern to compare filenames against. Wildcards are not supported.") + flaggy.StringSlice(&c.FileExtensions, "e", "extension", "Limit search to specified file extension. Specify as needed to match multiple required extensions.") + flaggy.Int(&c.FilesToKeep, "k", "keep", "Keep specified number of matching files") + flaggy.Bool(&c.RecursiveSearch, "r", "recurse", "Perform recursive search into subdirectories") + flaggy.Bool(&c.KeepOldest, "ko", "keep-old", "Keep oldest files instead of newer") + flaggy.Bool(&c.Remove, "rm", "remove", "Remove matched files") + + // Parse the flags + flaggy.Parse() + + // https://github.com/atc0005/elbow/issues/2#issuecomment-524032239 + // + // For flags, you can easily just check the value after calling + // flaggy.Parse(). If the value is set to something other than the + // default, then the caller supplied it. If it was the default value (set + // by you or the language), then it was not used. + + return c + +} diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..c3b79c4e --- /dev/null +++ b/go.mod @@ -0,0 +1,9 @@ +module github.com/atc0005/elbow + +go 1.12 + +require ( + github.com/integrii/flaggy v1.2.2 + github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad + github.com/stretchr/testify v1.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..a6acd0da --- /dev/null +++ b/go.sum @@ -0,0 +1,15 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/integrii/flaggy v1.2.2 h1:SzL5kyEaW+Cb3RLxGG1ch9FFDLQPB6QuMdYoNu5JIo0= +github.com/integrii/flaggy v1.2.2/go.mod h1:tnTxHeTJbah0gQ6/K0RW0J7fMUBk9MCF5blhm43LNpI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad h1:j5pg/OewZJyE6i3hIG4v3eQUvUyFdQkC8Nd/mjaEkxE= +github.com/r3labs/diff v0.0.0-20190801153147-a71de73c46ad/go.mod h1:ozniNEFS3j1qCwHKdvraMn1WJOsUxHd7lYfukEIS4cs= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/main.go b/main.go new file mode 100644 index 00000000..6419abbc --- /dev/null +++ b/main.go @@ -0,0 +1,113 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/integrii/flaggy" +) + +func main() { + + // DEBUG + // TODO: Enable this once leveled logging has been implemented. + //defaultConfig := NewConfig() + //fmt.Printf("Default configuration:\t%+v\n", defaultConfig) + + appName := "Elbow" + appDesc := "Prune content matching specific patterns, either in a single directory or recursively through a directory tree." + + config := NewConfig().SetupFlags(appName, appDesc) + + // DEBUG + // TODO: Enable this once leveled logging has been implemented. + //fmt.Printf("Our configuration:\t%+v\n", config) + + // DEBUG + log.Println("Confirm that requested path actually exists") + if !pathExists(config.StartPath) { + flaggy.ShowHelpAndExit(fmt.Sprintf("Error processing requested path: %q", config.StartPath)) + } + + // INFO + log.Println("Processing path:", config.StartPath) + + matches, err := processPath(config) + + // TODO + // How to handle errors from gathering removal candidates? + // Add optional flag to allow ignoring errors, fail immediately otherwise? + if err != nil { + log.Println("error:", err) + } + + // NOTE: If this sort order changes, make sure to update the later logic + // which retains the top or bottom X items (specific flag to preserve X + // number of files while pruning the others) + matches.sortByModTimeAsc() + + // DEBUG + log.Printf("Length of matches slice: %d\n", len(matches)) + + // DEBUG + log.Println("Early exit if no matching files were found.") + if len(matches) <= 0 { + + // INFO + fmt.Printf("No matches found in path %q for files with substring pattern of %q and with extensions %v\n", + config.StartPath, config.FilePattern, config.FileExtensions) + + // TODO: Not finding something is a valid outcome, so "normal" exit + // code? + os.Exit(0) + } + + var filesToPrune FileMatches + + // DEBUG + log.Printf("%d total items in matches", len(matches)) + log.Printf("%d items to keep per config.FilesToKeep", config.FilesToKeep) + + if config.KeepOldest { + // DEBUG + log.Println("Keeping older files") + log.Println("start at specified number to keep, go until end of slice") + filesToPrune = matches[config.FilesToKeep:] + } else { + // DEBUG + log.Println("Keeping newer files") + log.Println("start at beginning, go until specified number to keep") + filesToPrune = matches[:(len(matches) - config.FilesToKeep)] + } + + // DEBUG, INFO? + log.Printf("%d items to prune", len(filesToPrune)) + + log.Println("Prune specified files, do NOT ignore errors") + // TODO: Add support for ignoring errors (though I cannot immediately + // think of a good reason to do so) + removalResults, err := cleanPath(filesToPrune, false, config) + + // Show what we WERE able to successfully remove + // TODO: Refactor this into a function to handle displaying results? + log.Printf("%d files successfully removed\n", len(removalResults.SuccessfulRemovals)) + log.Println("----------------------------") + for _, file := range removalResults.SuccessfulRemovals { + log.Println("*", file.Name()) + } + + log.Printf("%d files failed to remove\n", len(removalResults.FailedRemovals)) + log.Println("----------------------------") + for _, file := range removalResults.FailedRemovals { + log.Println("*", file.Name()) + } + + // Determine if we need to display error, exit with unsuccessful error code + if err != nil { + log.Fatalf("Errors encountered while processing %s: %s", config.StartPath, err) + } + + log.Printf("%s successfully completed.", appName) + +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..43c021fe --- /dev/null +++ b/main_test.go @@ -0,0 +1,26 @@ +package main + +import "testing" + +func TestMain(t *testing.T) { + + defaultConfig := NewConfig() + + var emptySlice = []string{} + var nilSlice []string + + t.Logf("%v\n", emptySlice) + t.Log(len(emptySlice)) + t.Log("emptySlice is nil:", emptySlice == nil) + t.Log("-------------------------") + + t.Logf("%v\n", nilSlice) + t.Log(len(nilSlice)) + t.Log("nilSlice is nil:", nilSlice == nil) + t.Log("-------------------------") + + t.Logf("%v\n", defaultConfig.FileExtensions) + t.Log(len(defaultConfig.FileExtensions)) + t.Log("defaultConfig.FileExtensions is nil:", defaultConfig.FileExtensions == nil) + +} diff --git a/matches.go b/matches.go new file mode 100644 index 00000000..077042b9 --- /dev/null +++ b/matches.go @@ -0,0 +1,94 @@ +package main + +import ( + "log" + "os" + "path/filepath" + "sort" + "strings" +) + +// FileMatch represents a superset of statistics (including os.FileInfo) for a +// file matched by provided search criteria. This allows us to record the +// original full path while also +type FileMatch struct { + os.FileInfo + Path string +} + +// FileMatches is a slice of FileMatch objects +// TODO: Do I really need to abstract the fact that FileMatches is a slice of +// FileMatch objects? It seems that by hiding this it makes it harder to see +// that we're working with a slice? +type FileMatches []FileMatch + +func hasValidExtension(filename string, config *Config) bool { + + // NOTE: We do NOT compare extensions insensitively. We can add that + // functionality in the future if needed. + ext := filepath.Ext(filename) + + if len(config.FileExtensions) == 0 { + // DEBUG + log.Println("No extension limits have been set!") + log.Printf("Considering %s safe for removal\n", filename) + return true + } + + if inFileExtensionsPatterns(ext, config.FileExtensions) { + // DEBUG + log.Printf("%s has a valid extension for removal\n", filename) + return true + } + + // DEBUG + log.Println("hasValidExtension: returning false for:", filename) + log.Printf("hasValidExtension: returning false (%q not in %q)", + ext, config.FileExtensions) + return false +} + +func hasValidFilenamePattern(filename string, config *Config) bool { + + if strings.TrimSpace(config.FilePattern) == "" { + // DEBUG + log.Println("No FilePattern has been specified!") + log.Printf("Considering %s safe for removal\n", filename) + return true + } + + // Search for substring + if strings.Contains(filename, config.FilePattern) { + return true + } + + // DEBUG + log.Println("hasValidFilenamePattern: returning false for:", filename) + log.Printf("hasValidFilenamePattern: returning false (%q does not contain %q)", + filename, config.FilePattern) + return false +} + +// inFileExtensionsPatterns is a helper function to emulate Python's `if "x" +// in list:` functionality +func inFileExtensionsPatterns(ext string, exts []string) bool { + for _, pattern := range exts { + if ext == pattern { + return true + } + } + return false +} + +// TODO: Two methods, or one method with a boolean flag determining behavior? +func (fm FileMatches) sortByModTimeAsc() { + sort.Slice(fm, func(i, j int) bool { + return fm[i].ModTime().Before(fm[j].ModTime()) + }) +} + +func (fm FileMatches) sortByModTimeDesc() { + sort.Slice(fm, func(i, j int) bool { + return fm[i].ModTime().After(fm[j].ModTime()) + }) +} diff --git a/paths.go b/paths.go new file mode 100644 index 00000000..a8a9d518 --- /dev/null +++ b/paths.go @@ -0,0 +1,206 @@ +package main + +import ( + "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" +) + +// PathPruningResults represents the number of files that were successfully +// removed and those that were not. This is used in various calculations and +// to provide a brief summary of results to the user at program completion. +type PathPruningResults struct { + SuccessfulRemovals FileMatches + FailedRemovals FileMatches +} + +// cleanPath receives a slice of FileMatch objects and removes each file. Any +// errors encountered while removing files may optionally be ignored (default +// is to return immediately upon first error). The total number of files +// successfully removed is returned along with an error code (nil if no errors +// were encountered). +func cleanPath(files FileMatches, ignoreErrors bool, config *Config) (PathPruningResults, error) { + + // DEBUG + for _, file := range files { + + //fmt.Println("Details of file ...") + //fmt.Printf("%T / %+v\n", file, file) + //fmt.Println(file.ModTime().Format("2006-01-02 15:04:05")) + + // DEBUG + log.Printf("Full path: %s, ShortPath: %s, Size: %d, Modified: %v\n", + file.Path, + file.Name(), + file.Size(), + file.ModTime().Format("2006-01-02 15:04:05")) + } + + var removalResults PathPruningResults + + if !config.Remove { + + // INFO + log.Println("File removal not enabled.") + + // DEBUG + log.Println("listing what WOULD be removed") + log.Println("----------------------------") + for _, file := range files { + log.Println("*", file.Name()) + } + + // Nothing to show for this yet, but since the initial state reflects + // that we can return it as-is + return removalResults, nil + } + + for _, file := range files { + + filename := file.Name() + + // INFO + log.Println("Removing file:", filename) + + err := os.Remove(filename) + + if err != nil { + log.Println(fmt.Errorf("Failed to remove %s: %s", filename, err)) + + // Record failed removal, proceed to the next file + removalResults.FailedRemovals = append(removalResults.FailedRemovals, file) + continue + } + + // Record successful removal + removalResults.SuccessfulRemovals = append(removalResults.SuccessfulRemovals, file) + } + + // DEBUG + for _, file := range removalResults.FailedRemovals { + log.Println("Failed to remove:", file.Name()) + } + + return removalResults, nil + +} + +func pathExists(path string) bool { + + // Make sure path isn't empty + if strings.TrimSpace(path) == "" { + return false + } + + // https://gist.github.com/mattes/d13e273314c3b3ade33f + if _, err := os.Stat(path); !os.IsNotExist(err) { + return true + } + + return false + +} + +func processPath(config *Config) (FileMatches, error) { + + var matches FileMatches + var err error + + if config.RecursiveSearch { + // DEBUG + log.Println("Recursive option is enabled") + //log.Printf("%v", config) + + // Walk walks the file tree rooted at root, calling the anonymous function + // for each file or directory in the tree, including root. All errors that + // arise visiting files and directories are filtered by the anonymous + // function. The files are walked in lexical order, which makes the output + // deterministic but means that for very large directories Walk can be + // inefficient. Walk does not follow symbolic links. + err = filepath.Walk(config.StartPath, func(path string, info os.FileInfo, err error) error { + + // If an error is received, return it. If we return a non-nil error, this + // will stop the filepath.Walk() function from continuing to walk the + // path, and your main function will immediately move to the next line. + if err != nil { + return err + } + + // make sure we're not working with the root directory itself + if path != "." { + + // ignore directories + if info.IsDir() { + return nil + } + + if !hasValidExtension(path, config) { + return nil + } + + if !hasValidFilenamePattern(path, config) { + return nil + } + + // If we made it to this point, then we must assume that the file + // has met all criteria to be removed by this application. + fileMatch := FileMatch{FileInfo: info, Path: path} + matches = append(matches, fileMatch) + + } + + return err + }) + + } else { + + // If RecursiveSearch is not enabled, process just the provided StartPath + // NOTE: The same cleanPath() function is used in either case, the + // difference is in how the FileMatches slice is populated + + // DEBUG + log.Println("Recursive option is NOT enabled") + log.Printf("%v", config) + + // err is already declared earlier at a higher scope, so do not + // redeclare here + var files []os.FileInfo + files, err = ioutil.ReadDir(config.StartPath) + + // TODO: Do we really want to exit early at this point if there are + // failures evaluating some of the files? + // Is it possible to partially evaluate some of the files? + // TODO: Wrap error? + // if err != nil { + // log.Fatal("Error from ioutil.ReadDir():", err) + // } + + // Use []os.FileInfo returned from ioutil.ReadDir() to build slice of + // FileMatch objects + for _, file := range files { + + filename := file.Name() + + // Apply validity checks against filename. If validity fails, + // go to the next file in the list. + + if !hasValidExtension(filename, config) { + continue + } + + if !hasValidFilenamePattern(filename, config) { + continue + } + + // If we made it to this point, then we must assume that the file + // has met all criteria to be removed by this application. + fileMatch := FileMatch{FileInfo: file, Path: filename} + matches = append(matches, fileMatch) + } + } + + return matches, err +} diff --git a/testing/sample_files_list_dev_web_app_server.txt b/testing/sample_files_list_dev_web_app_server.txt new file mode 100644 index 00000000..1575be04 --- /dev/null +++ b/testing/sample_files_list_dev_web_app_server.txt @@ -0,0 +1,183 @@ +reach-masterdev-d9db6e2-20190501-1024.war +reach-masterqa-d9db6e2-20190501-1037.war +reach-masterdev-c1a3570-20190501-1117.war +reach-masterqa-c1a3570-20190501-1118.war +reach-master-c1a3570-20190501-1119.war +reach-masterdev-12812a5-20190503-1055.war +reach-masterdev-5f17dad-20190506-1057.war +reach-masterdev-c094373-20190506-1300.war +reach-masterdev-0d64ee6-20190507-0931.war +reach-masterdev-1e14b45-20190507-1203.war +reach-masterdev-1e14b45-20190507-1209.war +reach-masterqa-1e14b45-20190507-1210.war +reach-masterdev-6e83c12-20190507-1237.war +reach-masterqa-6e83c12-20190507-1239.war +reach-master-6e83c12-20190507-1240.war +reach-masterdev-a9ea1d0-20190508-1422.war +reach-masterqa-a9ea1d0-20190508-1424.war +reach-master-a9ea1d0-20190508-1425.war +reach-masterdev-6af941a-20190508-1541.war +reach-masterdev-3703072-20190508-1549.war +reach-masterdev-b05f230-20190508-1605.war +reach-masterdev-6e6296a-20190508-1612.war +reach-masterdev-b9ee547-20190508-1623.war +reach-masterdev-91544c8-20190508-1640.war +reach-masterdev-3aed21e-20190508-1649.war +reach-masterdev-3835cee-20190508-1652.war +reach-masterdev-b0df020-20190508-1655.war +reach-masterdev-240c509-20190508-1709.war +reach-masterdev-a31ca59-20190508-1719.war +reach-masterdev-b88a1e1-20190508-1745.war +reach-masterdev-cf39181-20190508-1807.war +reach-masterdev-3d2319c-20190508-1830.war +reach-masterdev-0973435-20190510-0846.war +reach-masterdev-0973435-20190510-0912.war +reach-masterdev-0973435-20190510-0923.war +reach-masterdev-0973435-20190510-0929.war +reach-masterdev-0973435-20190510-0935.war +reach-masterdev-a9ea1d0-20190510-0946.war +reach-masterdev-b61ebcf-20190510-0949.war +reach-masterdev-b61ebcf-20190510-0958.war +reach-masterdev-b61ebcf-20190510-1011.war +reach-masterdev-b61ebcf-20190510-1017.war +reach-masterdev-b61ebcf-20190510-1023.war +reach-masterdev-74d08fb-20190510-1035.war +reach-masterdev-74d08fb-20190510-1037.war +reach-masterdev-74d08fb-20190510-1045.war +reach-masterdev-74d08fb-20190510-1052.war +reach-masterqa-a9ea1d0-20190510-1054.war +reach-masterdev-d910f34-20190510-1216.war +reach-masterdev-82a921c-20190510-1256.war +reach-masterqa-a9ea1d0-20190510-1316.war +reach-masterqa-a9ea1d0-20190510-1321.war +reach-masterqa-a9ea1d0-20190510-1326.war +reach-masterqa-82a921c-20190510-1334.war +reach-masterdev-88265d4-20190513-0936.war +reach-masterdev-790c210-20190513-0943.war +reach-masterdev-9082db5-20190513-1019.war +reach-masterdev-1eeaf7d-20190513-1028.war +reach-masterdev-456d231-20190513-1039.war +reach-masterdev-bbd1f49-20190513-1058.war +reach-masterdev-159e35d-20190514-0826.war +reach-masterdev-1966779-20190514-0856.war +reach-masterdev-98a89e9-20190514-1027.war +reach-masterdev-3c17962-20190514-1103.war +reach-masterdev-3fdf35b-20190514-1209.war +reach-masterdev-bc23c80-20190514-1218.war +reach-masterdev-98cf38c-20190515-1530.war +reach-masterdev-04f229b-20190515-1548.war +reach-masterdev-e95d962-20190515-1556.war +reach-masterdev-71b1741-20190515-1559.war +reach-masterdev-9fb22c7-20190515-1616.war +reach-masterdev-84b4055-20190516-0911.war +reach-masterdev-c76f525-20190516-0920.war +reach-masterdev-cfa3516-20190516-1020.war +reach-masterdev-611d6a3-20190516-1121.war +reach-masterdev-054e77f-20190516-1211.war +reach-masterdev-6f1b84f-20190516-1257.war +reach-masterdev-04b80a8-20190517-1133.war +reach-masterdev-07d368b-20190520-1224.war +reach-masterdev-12aff13-20190522-1250.war +reach-masterdev-7cbe28c-20190522-1406.war +reach-masterdev-d6f98dc-20190523-1255.war +reach-masterdev-8f34a46-20190528-1003.war +reach-masterdev-25fbb8e-20190528-1208.war +reach-masterdev-c765a45-20190528-1233.war +reach-masterdev-c765a45-20190528-1253.war +reach-masterqa-c765a45-20190528-1255.war +reach-master-a9ea1d0-20190528-1256.war +reach-masterdev-b1e3655-20190530-1019.war +reach-masterqa-b1e3655-20190530-1021.war +reach-master-b1e3655-20190530-1023.war +reach-masterdev-ded74a1-20190531-1057.war +reach-masterqa-ded74a1-20190531-1059.war +reach-master-ded74a1-20190531-1100.war +reach-masterdev-16b0010-20190531-1107.war +reach-masterqa-16b0010-20190531-1109.war +reach-master-16b0010-20190531-1110.war +reach-masterdev-8c7d85c-20190603-0921.war +reach-masterdev-27dfec5-20190603-1106.war +reach-masterdev-27dfec5-20190603-1156.war +reach-masterdev-ef95383-20190604-1000.war +reach-masterdev-ef95383-20190606-1139.war +reach-masterqa-ef95383-20190606-1140.war +reach-master-ef95383-20190606-1142.war +reach-masterdev-4f1b84b-20190607-1149.war +reach-masterdev-6de3b98-20190617-1134.war +reach-masterdev-f449828-20190618-0928.war +reach-masterdev-c029213-20190618-0946.war +reach-masterdev-74f2ffa-20190618-1005.war +reach-masterdev-0f3bae6-20190618-1045.war +reach-masterdev-ed0dc08-20190618-1111.war +reach-masterqa-ed0dc08-20190618-1113.war +reach-master-ed0dc08-20190618-1114.war +reach-masterdev-dfb0916-20190618-1131.war +reach-masterqa-dfb0916-20190618-1133.war +reach-master-dfb0916-20190618-1134.war +reach-masterdev-85f400b-20190619-1041.war +reach-masterdev-60474af-20190619-1143.war +reach-masterqa-60474af-20190619-1223.war +reach-masterdev-f88fb89-20190619-1330.war +reach-masterqa-f88fb89-20190619-1332.war +reach-master-f88fb89-20190619-1333.war +reach-masterdev-4503997-20190628-0902.war +reach-masterqa-4503997-20190628-0904.war +reach-master-4503997-20190628-0906.war +reach-masterdev-b0cd4be-20190628-0924.war +reach-masterqa-b0cd4be-20190628-0926.war +reach-master-b0cd4be-20190628-0927.war +reach-masterdev-084c7ac-20190628-1014.war +reach-masterqa-084c7ac-20190628-1016.war +reach-master-084c7ac-20190628-1017.war +reach-masterdev-8872e87-20190628-1037.war +reach-masterdev-7465110-20190628-1050.war +reach-masterdev-65d5cb4-20190701-1210.war +reach-masterdev-3127a20-20190703-0936.war +reach-masterdev-6f36e00-20190703-0951.war +reach-masterqa-6f36e00-20190703-0957.war +reach-masterdev-d93b245-20190703-1013.war +reach-masterqa-d93b245-20190703-1015.war +reach-master-d93b245-20190703-1016.war +reach-masterdev-3303dd3-20190703-1118.war +reach-masterqa-3303dd3-20190703-1120.war +reach-master-3303dd3-20190703-1122.war +reach-masterdev-e1240d7-20190705-1309.war +reach-masterdev-c5352db-20190708-1044.war +reach-masterdev-731da2b-20190711-1241.war +reach-masterdev-2a66522-20190711-1253.war +reach-masterdev-dca952e-20190711-1311.war +reach-masterdev-f5fc322-20190711-1320.war +reach-masterdev-879a0c1-20190711-1334.war +reach-masterdev-a7e48a2-20190711-1345.war +reach-masterdev-1f7cc2c-20190711-1350.war +reach-masterdev-fa29509-20190712-0940.war +reach-masterdev-718f8bc-20190716-1215.war +reach-masterdev-cafe701-20190723-1029.war +reach-masterdev-383d19a-20190723-1142.war +reach-masterdev-1256c13-20190723-1157.war +reach-masterdev-757c4b7-20190724-0933.war +reach-masterqa-757c4b7-20190724-1111.war +reach-master-8c86a19-20190724-1157.war +reach-masterdev-c47c094-20190724-1330.war +reach-masterdev-6892483-20190725-1121.war +reach-masterdev-8084f14-20190725-1208.war +reach-masterdev-2538aeb-20190725-1227.war +reach-masterqa-2538aeb-20190725-1234.war +reach-master-5ada651-20190725-1251.war +reach-masterdev-6582528-20190726-1019.war +reach-masterdev-bed2692-20190726-1042.war +reach-masterqa-9d30cb2-20190726-1050.war +reach-master-5726040-20190726-1104.war +reach-masterdev-18d887c-20190802-1157.war +reach-masterqa-e0f9cab-20190802-1204.war +reach-master-fe75843-20190802-1246.war +reach-masterdev-060a69d-20190805-0959.war +reach-masterqa-089390d-20190805-1017.war +reach-masterdev-060a69d-20190805-1027.war +reach-masterqa-089390d-20190805-1031.war +reach-master-5e7ddfe-20190805-1055.war +reach-masterdev-b568724-20190816-1139.war +reach-masterqa-e340a56-20190816-1205.war +reach-masterdev-9ba650a-20190816-1243.war +reach-masterqa-d583d20-20190816-1245.war +reach-master-d583d20-20190816-1246.war diff --git a/testing/test.go b/testing/test.go new file mode 100644 index 00000000..17e00062 --- /dev/null +++ b/testing/test.go @@ -0,0 +1,135 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/integrii/flaggy" + "github.com/r3labs/diff" +) + +func main() { + + // create default configuration so that we can compare against it to + // determine whether the user has provided flags + defaultConfig := NewConfig() + //fmt.Printf("Default configuration:\t%+v\n", defaultConfig) + + appName := "Elbow" + appDesc := "Prune content matching specific patterns, either in a single directory or recursively through a directory tree." + + config := NewConfig().SetupFlags(appName, appDesc) + //fmt.Printf("Our configuration:\t%+v\n", config) + + changelog, err := diff.Diff(defaultConfig, config) + if err != nil { + log.Fatal(err) + } + + if len(changelog) > 0 { + log.Println("User specified command-line options") + fmt.Printf("Changes to default settings: %+v\n", changelog) + //fmt.Println("Changes to default settings:", changelog) + } else { + log.Println("User did not provide any command-line flags") + } + + // TODO: Print error message and exit since (evidently) the target + // starting path does not exist. + // + // https://gist.github.com/mattes/d13e273314c3b3ade33f + // + // if _, err := os.Stat("/path/to/whatever"); os.IsNotExist(err) { + // // path/to/whatever does not exist + // } + + // if _, err := os.Stat("/path/to/whatever"); !os.IsNotExist(err) { + // // path/to/whatever exists + // } + + log.Println("Processing path:", config.StartPath) + + os.Exit(0) + +} + +// Config represents a collection of configuration settings for this +// application. Config is created as early as possible upon application +// startup. +type Config struct { + FilePattern string `diff:"filepattern"` + FileExtensions []string `diff:"filextensions"` + StartPath string `diff:"startpath"` + RecursiveSearch bool `diff:"recursivesearch"` + FilesToKeep int `diff:"filestokeep"` + KeepOldest bool `diff:"keepoldest"` + Remove bool `diff:"remove"` +} + +// NOTE: I've found multiple examples that all return a pointer in order to +// support "chaining" where the new config feeds directly into the next +// method +// https://github.com/go-sql-driver/mysql/blob/877a9775f06853f611fb2d4e817d92479242d1cd/dsn.go#L67 +// https://github.com/aws/aws-sdk-go/blob/10878ad0389c5b3069815112ce888b191c8cd325/aws/config.go#L251 +// https://github.com/aws/aws-sdk-go/blob/master/aws/config.go +// +// https://github.com/aws/aws-sdk-go/blob/10878ad0389c5b3069815112ce888b191c8cd325/awstesting/integration/performance/s3GetObject/config.go#L25 +// https://github.com/aws/aws-sdk-go/blob/10878ad0389c5b3069815112ce888b191c8cd325/awstesting/integration/performance/s3GetObject/main.go#L25 +// +// +/* +func NewConfig() *Config { + return &Config{} +} +// WithRegion sets a config Region value returning a Config pointer for +// chaining. +func (c *Config) WithRegion(region string) *Config { + c.Region = ®ion + return c +} +*/ + +// NewConfig returns a new Config pointer that can be chained with builder +// methods to set multiple configuration values inline without using pointers. +func NewConfig() *Config { + + // Explicitly initialize with intended defaults + // Note: We compare against the default values in order to determine + // whether the user has specified a particular flag + return &Config{ + StartPath: "", + FilePattern: "", + FileExtensions: []string{}, + FilesToKeep: 0, + RecursiveSearch: false, + KeepOldest: false, + Remove: false, + } + +} + +// SetupFlags applies settings provided by command-line flags +// TODO: Pull out +func (c *Config) SetupFlags(appName string, appDesc string) *Config { + + flaggy.SetName(appName) + flaggy.SetDescription(appDesc) + + flaggy.DefaultParser.ShowHelpOnUnexpected = true + + // Add flags + flaggy.String(&c.StartPath, "p", "path", "Path to process") + flaggy.String(&c.FilePattern, "fp", "pattern", "File pattern to match against") + flaggy.StringSlice(&c.FileExtensions, "e", "extension", "Limit search to specified file extension") + flaggy.Int(&c.FilesToKeep, "k", "keep", "Keep specified number of matching files") + flaggy.Bool(&c.RecursiveSearch, "r", "recurse", "Perform recursive search into subdirectories") + flaggy.Bool(&c.KeepOldest, "ko", "keep-old", "Keep oldest files instead of newer") + flaggy.Bool(&c.Remove, "rm", "remove", "Remove matched files") + + // Parse the flags + flaggy.Parse() + + return c + +}