package cli import ( "fmt" "io" "os" "time" "github.com/spf13/afero" "github.com/urfave/cli/v2" "sneak.berlin/go/mfer/internal/log" ) // CLIApp is the main CLI application container. It holds configuration, // I/O streams, and filesystem abstraction to enable testing and flexibility. type CLIApp struct { appname string version string gitrev string startupTime time.Time exitCode int app *cli.App Stdin io.Reader // Standard input stream Stdout io.Writer // Standard output stream for normal output Stderr io.Writer // Standard error stream for diagnostics Fs afero.Fs // Filesystem abstraction for all file operations } const banner = ` ___ ___ ___ ___ /__/\ / /\ / /\ / /\ | |::\ / /:/_ / /:/_ / /::\ | |:|:\ / /:/ /\ / /:/ /\ / /:/\:\ __|__|:|\:\ / /:/ /:/ / /:/ /:/_ / /:/~/:/ /__/::::| \:\ /__/:/ /:/ /__/:/ /:/ /\ /__/:/ /:/___ \ \:\~~\__\/ \ \:\/:/ \ \:\/:/ /:/ \ \:\/:::::/ \ \:\ \ \::/ \ \::/ /:/ \ \::/~~~~ \ \:\ \ \:\ \ \:\/:/ \ \:\ \ \:\ \ \:\ \ \::/ \ \:\ \__\/ \__\/ \__\/ \__\/` func (mfa *CLIApp) printBanner() { log.Info(banner) } // VersionString returns the version and git revision formatted for display. func (mfa *CLIApp) VersionString() string { return fmt.Sprintf("%s (%s)", mfa.version, mfa.gitrev) } func (mfa *CLIApp) setVerbosity(quiet bool, v int) { _, present := os.LookupEnv("MFER_DEBUG") if present { log.EnableDebugLogging() } else if quiet { log.SetLevel(log.ErrorLevel) } else { log.SetLevelFromVerbosity(v) } } func (mfa *CLIApp) run(args []string) { mfa.startupTime = time.Now() if NO_COLOR { // shoutout to rob pike who thinks it's juvenile log.DisableStyling() } // Configure log package to use our I/O streams log.SetOutput(mfa.Stdout, mfa.Stderr) log.Init() var verbosity int mfa.app = &cli.App{ Name: mfa.appname, Usage: "Manifest generator", Version: mfa.VersionString(), EnableBashCompletion: true, Writer: mfa.Stdout, ErrWriter: mfa.Stderr, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "verbose", Usage: "Verbosity level", Aliases: []string{"v"}, Count: &verbosity, }, &cli.BoolFlag{ Name: "quiet", Usage: "don't produce output except on error", Aliases: []string{"q"}, }, }, Commands: []*cli.Command{ { Name: "generate", Aliases: []string{"gen"}, Usage: "Generate manifest file", Action: func(c *cli.Context) error { mfa.setVerbosity(c.Bool("quiet"), verbosity) mfa.printBanner() return mfa.generateManifestOperation(c) }, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "FollowSymLinks", Aliases: []string{"follow-symlinks"}, Usage: "Resolve encountered symlinks", }, &cli.BoolFlag{ Name: "IncludeDotfiles", Aliases: []string{"include-dotfiles"}, Usage: "Include dot (hidden) files (excluded by default)", }, &cli.StringFlag{ Name: "output", Value: "./.index.mf", Aliases: []string{"o"}, Usage: "Specify output filename", }, &cli.BoolFlag{ Name: "force", Aliases: []string{"f"}, Usage: "Overwrite output file if it exists", }, &cli.BoolFlag{ Name: "progress", Aliases: []string{"P"}, Usage: "Show progress during enumeration and scanning", }, }, }, { Name: "check", Usage: "Validate files using manifest file", ArgsUsage: "[manifest file]", Action: func(c *cli.Context) error { mfa.setVerbosity(c.Bool("quiet"), verbosity) mfa.printBanner() return mfa.checkManifestOperation(c) }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "base", Aliases: []string{"b"}, Value: ".", Usage: "Base directory for resolving relative paths from manifest", }, &cli.BoolFlag{ Name: "progress", Aliases: []string{"P"}, Usage: "Show progress during checking", }, &cli.BoolFlag{ Name: "no-extra-files", Usage: "Fail if files exist in base directory that are not in manifest", }, }, }, { Name: "freshen", Usage: "Update manifest with changed, new, and removed files", ArgsUsage: "[manifest file]", Action: func(c *cli.Context) error { mfa.setVerbosity(c.Bool("quiet"), verbosity) mfa.printBanner() return mfa.freshenManifestOperation(c) }, Flags: []cli.Flag{ &cli.StringFlag{ Name: "base", Aliases: []string{"b"}, Value: ".", Usage: "Base directory for resolving relative paths", }, &cli.BoolFlag{ Name: "FollowSymLinks", Aliases: []string{"follow-symlinks"}, Usage: "Resolve encountered symlinks", }, &cli.BoolFlag{ Name: "IncludeDotfiles", Aliases: []string{"include-dotfiles"}, Usage: "Include dot (hidden) files (excluded by default)", }, &cli.BoolFlag{ Name: "progress", Aliases: []string{"P"}, Usage: "Show progress during scanning and hashing", }, }, }, { Name: "version", Usage: "Show version", Action: func(c *cli.Context) error { _, _ = fmt.Fprintln(mfa.Stdout, mfa.VersionString()) return nil }, }, { Name: "fetch", Usage: "fetch manifest and referenced files", Action: func(c *cli.Context) error { mfa.setVerbosity(c.Bool("quiet"), verbosity) mfa.printBanner() return mfa.fetchManifestOperation(c) }, }, }, } mfa.app.HideVersion = true err := mfa.app.Run(args) if err != nil { mfa.exitCode = 1 log.WithError(err).Debugf("exiting") } }