diff options
| author | Joseph Richey <joerichey94@gmail.com> | 2017-10-16 10:50:53 -0700 |
|---|---|---|
| committer | Joseph Richey <joerichey94@gmail.com> | 2017-10-19 02:22:25 -0700 |
| commit | b5cc60b2b974645f0d09721c292cd243d049cbcf (patch) | |
| tree | fbc44d87ba8dd83e2238ff8e217ea560f56b3091 | |
| parent | b4299090c3e503ba0c49a6086b1a46c218ca45f4 (diff) | |
Refactor almost complete
| -rw-r--r-- | cmd/cmd.go | 209 | ||||
| -rw-r--r-- | cmd/errors.go | 78 | ||||
| -rw-r--r-- | cmd/flag.go | 67 | ||||
| -rw-r--r-- | cmd/format.go (renamed from cmd/output.go) | 92 | ||||
| -rw-r--r-- | cmd/fscrypt/commands.go | 1 | ||||
| -rw-r--r-- | cmd/fscrypt/errors.go | 37 | ||||
| -rw-r--r-- | cmd/fscrypt/flags.go | 114 | ||||
| -rw-r--r-- | cmd/fscrypt/format.go | 162 | ||||
| -rw-r--r-- | cmd/fscrypt/fscrypt.go | 156 | ||||
| -rw-r--r-- | cmd/fscrypt/prompt.go | 62 | ||||
| -rw-r--r-- | cmd/fscrypt/setup.go | 23 | ||||
| -rw-r--r-- | cmd/fscrypt/strings.go | 90 | ||||
| -rw-r--r-- | cmd/helper.go | 20 | ||||
| -rw-r--r-- | cmd/info.go | 96 | ||||
| -rw-r--r-- | cmd/run.go | 199 | ||||
| -rw-r--r-- | cmd/strings.go | 20 | ||||
| -rw-r--r-- | cmd/version.go | 121 | ||||
| -rw-r--r-- | ext4/ext4.go | 44 | ||||
| -rw-r--r-- | ext4/feature_flag.go | 2 |
19 files changed, 639 insertions, 954 deletions
@@ -1,5 +1,5 @@ /* - * cmd.go - Main interface to cmd package (running, Cmd and Flag structs, etc) + * cmd.go - Main interface to cmd package (Context, Command, Flag, etc...) * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.com) @@ -17,34 +17,93 @@ * the License. */ -// Package cmd is the common library for writing command line binaries. -// This package is mainly a wrapper around github.com/urfave/cli, but provides -// additional support to make the usage look similar to the man page. -// -// The main componets are the `Cmd`, `Argument`, and `Flag` types which can be -// used to define a top-level command with many potential subcommands. This -// package also presents a smaller interface than urfave/cli, making it easier -// to use for other commands. +// Package cmd is the common library for writing command line binaries. The main +// componets are the `Command`, `Context`, `Argument`, and `Flag` types which +// can be used to define a top-level command with many potential subcommands. package cmd import ( "fmt" - "io" "os" - "text/template" - "time" + "path/filepath" - "github.com/blang/semver" + "github.com/pkg/errors" ) +// Command represents a command with many potential sub-commands. +type Command struct { + Name string + Title string + UsageLines []string + SubCommands []*Command + InheritArguments bool + Arguments []*Argument + InheritFlags bool + Flags []Flag + ManPage *ManPage + Action Action +} + +// Run executes the command with os.Args, equivalent to c.RunArgs(os.Args). +func (c *Command) Run() { + c.RunArgs(os.Args) +} + +// RunArgs executes the command with the provided args. If the Name argument is +// empty, args[0]'s basename is used instead. If the command fails, this method +// will not return. +func (c *Command) RunArgs(args []string) { + binaryPath, args := args[0], args[1:] + if c.Name == "" { + c.Name = filepath.Base(binaryPath) + } + + // Create our initial context by sorting the arguments. + ctx := &Context{Command: c} + ctx.Args, ctx.flagArgs = sortArgs(args) + + ctx.run() +} + +// Action contains the implementation of a command. If a normal error is +// returned, the error will be printed out (with an optional explanation) and +// Run will exit with FailureCode. If a usage error is returned, the error and +// the commnd's usage are printed out and Run will exit with UsageFailureCode. +// Returning nil causes Run to return. +type Action func(ctx *Context) error + // Context represents the state of a running application, and is the only thing -// passed to a CommandFunc. +// passed to an Action. type Context struct { - Command *Command - Parent *Context - Info *Info - Args []string + // The current command being executed + Command *Command + // The context of the parent command before this command was executed. + // Nil if this is the root context. + Parent *Context + // The non-flag arguments being passed to the command. + Args []string + // The flag arguments being passed to the command. flagArgs []string + // The mapping of error causes to help strings + errorMap map[error]string +} + +// FullArguments returns the list of arguments for the current command and its +// parent arguments (if InheritArguments) is true. +func (ctx *Context) FullArguments() []*Argument { + if ctx.Parent == nil || !ctx.Command.InheritArguments { + return ctx.Command.Arguments + } + return append(ctx.Command.Arguments, ctx.Parent.FullArguments()...) +} + +// FullFlags returns the list of flags for the current command and its parent +// arguments (if InheritFlags) is true. +func (ctx *Context) FullFlags() []Flag { + if ctx.Parent == nil || !ctx.Command.InheritFlags { + return ctx.Command.Flags + } + return append(ctx.Command.Flags, ctx.Parent.FullFlags()...) } // FullName returns the space-separated name of the command and all parents. @@ -55,41 +114,30 @@ func (ctx *Context) FullName() string { return fmt.Sprintf("%s %s", ctx.Parent.FullName(), ctx.Command.Name) } +// Info returns the same information as cmd.Info. This method only exists so +// that Info can be accessed in an output template. +func (*Context) Info() *InfoData { + return Info +} + // ManPage returns the man page entry for this context. It is either the ManPage // for the the current command or the closet Parent. func (ctx *Context) ManPage() *ManPage { - if ctx.Command.ManPage.Section != 0 || ctx.Parent == nil { + if ctx.Parent == nil || ctx.Command.ManPage != nil { return ctx.Command.ManPage } return ctx.Parent.ManPage() } -// Creates an anonymous template from the text, and runs it with the provided -// Context and writer. Panics if text has a bad format or execution fails. -func (ctx *Context) executeTemplate(w io.Writer, text string) { - tmpl := template.Must(template.New("").Parse(text)) - if err := tmpl.Execute(w, ctx); err != nil { - panic(err) +// getHelp tries to find a helpMap and then lookup the error by it's cause. +func (ctx *Context) getHelp(err error) string { + if ctx.errorMap != nil { + return ctx.errorMap[errors.Cause(err)] } -} - -func (ctx *Context) execute() { - fmt.Printf("%+v\n", ctx) - return -} - -// Info is a parsed view of the corresponding global variables. -type Info struct { - Version semver.Version - BuildTime time.Time - Authors []Author - Copyright string -} - -// Author contains the contact information for a contributor. -type Author struct { - Name string - Email string + if ctx.Parent == nil { + return "" + } + return ctx.Parent.getHelp(err) } // Argument represents a parameter passed to a function. It has an optional @@ -101,77 +149,10 @@ type Argument struct { func (a *Argument) String() string { return fmt.Sprintf("<%s>", a.ArgName) } -// ManPage a man page with a title and section. +// ManPage a man page with a name and section. type ManPage struct { - Title string + Name string Section int } -// CommandFunc contains the implementation of a command. If a normal error is -// returned, the error will be printed out (with an optional explanation) and -// Run will exit with FailureCode. If a usage error is returned, the error and -// the commnd's usage are printed out and Run will exit with UsageFailureCode. -// Returning nil causes Run to return. -type CommandFunc func(ctx *Context) error - -// Command represents a command with many potential top-level commands. This is -// transformed into a cli.Command in Run(). -type Command struct { - Name string - Title string - UsageLines []string - SubCommands []*Command - InheritArguments bool - Arguments []*Argument - InheritFlags bool - Flags []Flag - ManPage *ManPage - Action CommandFunc -} - -// Run executes the command with os.Args, equivalent to c.RunArgs(os.Args). -func (c *Command) Run() { - c.RunArgs(os.Args) -} - -// RunArgs executes the command with the provided args. If the Name argument is -// empty, args[0]'s basename is used instead. If the command fails, this method -// will not return. -func (c *Command) RunArgs(args []string) { - binaryName, args := args[0], args[1:] - if c.Name == "" { - c.Name = binaryName - } - - // Create our initial context by sorting the args and parsing the tags. - ctx := &Context{ - Command: c, - Info: parseInfo(), - } - ctx.Args, ctx.flagArgs = sortArgs(args) - - ctx.execute() -} - -// Divide the arguments into flag arguments (those starting with "-") and normal -// arguments. If "--" appears in the list, it will classified as a normal -// argument as well as all arguments following it. Also removes empty args. -func sortArgs(args []string) (normalArgs, flagArgs []string) { - var arg string - for len(args) > 0 { - arg, args = args[0], args[1:] - if arg == "" { - continue - } - if arg == "--" { - normalArgs = append(normalArgs, arg) - normalArgs = append(normalArgs, args...) - return - } else if arg[0] == '-' { - flagArgs = append(flagArgs, arg) - } else { - normalArgs = append(normalArgs, arg) - } - } - return -} +func (m *ManPage) String() string { return fmt.Sprintf("%s(%d)", m.Name, m.Section) } diff --git a/cmd/errors.go b/cmd/errors.go index 07a1d05..0252fed 100644 --- a/cmd/errors.go +++ b/cmd/errors.go @@ -20,19 +20,59 @@ package cmd import ( + "fmt" + "os" + "github.com/pkg/errors" "github.com/google/fscrypt/util" ) -// Common errors used across commands +// Common errors used across tools var ( - ErrUnknownVersion = errors.New("unknown (missing version tag)") + ErrUnknownVersion = errors.New("unknown version (missing version tag)") ErrCanceled = errors.New("operation canceled by user") ErrMustForce = errors.New("operation must be forced") ErrNotRoot = errors.New("operation must be run as root") ) +// Error return codes +var ( + FailureCode = 1 + UsageFailureCode = 2 +) + +// UsageError is an error type used to denote that a command was incorrectly +// specified. Returning this type from an Action will cause print the command's +// usage to os.Stdout before exiting with UsageFailureCode. +type UsageError string + +func (u UsageError) Error() string { return string(u) } + +// CheckExpectedArgs returns a UsageError if the number of arguements in the +// context does not match expectedArgs. If atMost is set, the number of args +// is allowed to be less than expectedArgs. +func CheckExpectedArgs(ctx *Context, expectedArgs int, atMost bool) error { + // Check the number of arguements and build the message. + nArgs := len(ctx.Args) + message := "expected" + if atMost { + if nArgs <= expectedArgs { + return nil + } + message += " at most" + } else { + if nArgs == expectedArgs { + return nil + } + } + // We have the wrong number of arguments + message += fmt.Sprintf(" %s, got %s", + Pluralize(expectedArgs, "argument"), + Pluralize(nArgs, "argument")) + return UsageError(message) +} + // CheckIfRoot returns an error if the current user is not the root user. func CheckIfRoot() error { if id := util.CurrentUserID(); id != 0 { @@ -40,3 +80,37 @@ func CheckIfRoot() error { } return nil } + +// CheckRequiredFlags returns a UsageError if all of the required flags are not +// set. Only StringFlags are currently supported. +func CheckRequiredFlags(flags []*StringFlag) error { + for _, flag := range flags { + if flag.Value == "" { + return UsageError(fmt.Sprintf("required flag %s not set", flag)) + } + } + return nil +} + +// processError TODO(joerichey) +func (ctx *Context) processError(err error) { + if err == nil { + return + } + + fmt.Fprintf(os.Stderr, "%s: %s\n", ctx.FullName(), err) + // Usage Errors should print the usage information + if _, ok := err.(UsageError); ok { + ExecuteTemplate(os.Stderr, TemplateUsage, ctx) + os.Exit(UsageFailureCode) + return + } + + // Errors with a help text should print it out. + if helpText := ctx.getHelp(err); helpText != "" { + fmt.Fprintln(os.Stderr) + fmt.Fprintln(os.Stderr, wrapText(helpText, 0)) + } + os.Exit(FailureCode) + return +} diff --git a/cmd/flag.go b/cmd/flag.go index 5f864b9..24fd86d 100644 --- a/cmd/flag.go +++ b/cmd/flag.go @@ -26,6 +26,29 @@ import ( "time"
)
+// Useful flags that can be used with a variety of commands.
+var (
+ HelpFlag = &BoolFlag{
+ Name: "help",
+ ShortName: 'h',
+ Usage: "Prints a help text for any command or sub-command.",
+ }
+ VerboseFlag = &BoolFlag{
+ Name: "verbose",
+ Usage: "Prints additional debug messages.",
+ }
+ QuietFlag = &BoolFlag{
+ Name: "quiet",
+ Usage: `Prints nothing except for errors and uses any default
+ option instead of prompting the user.`,
+ }
+ ForceFlag = &BoolFlag{
+ Name: "force",
+ Usage: `Print no confirmation prompts or warnings and
+ automatically proceed with the requested action.`,
+ }
+)
+
// Flag represents a flag that can be passed to a command. The Name, ArgName,
// and Usage are used to format and display the flag.
type Flag interface {
@@ -38,7 +61,7 @@ type Flag interface { }
// Formats as "--name" or as "--name=<argName>" if argName is present.
-func formatHelper(name, argName string) string {
+func flagFormatHelper(name, argName string) string {
if argName != "" {
return fmt.Sprintf("--%s=<%s>", name, argName)
}
@@ -46,7 +69,7 @@ func formatHelper(name, argName string) string { }
// Appends (default: <default>) to the usage if defaultString is present.
-func usageHelper(usage, defaultString string) string {
+func flagUsageHelper(usage, defaultString string) string {
if defaultString != "" {
usage += fmt.Sprintf(" (default: %s)", defaultString)
}
@@ -55,25 +78,37 @@ func usageHelper(usage, defaultString string) string { // BoolFlag is a Flag of type bool.
type BoolFlag struct {
- Name string
- Usage string
- Default bool
- Value bool
+ Name string
+ ShortName byte
+ Usage string
+ Default bool
+ Value bool
}
// String always uses the smaller format, as it has no ArgName.
-func (f *BoolFlag) String() string { return formatHelper(f.Name, "") }
+func (f *BoolFlag) String() string {
+ name := f.Name
+ if f.ShortName != 0 {
+ name += ", -" + string(f.ShortName)
+ }
+ return flagFormatHelper(name, "")
+}
// FullUsage shows the default if it's true (flag is implicitly passed).
func (f *BoolFlag) FullUsage() string {
if !f.Default {
- return usageHelper(f.Usage, "")
+ return flagUsageHelper(f.Usage, "")
}
- return usageHelper(f.Usage, "true")
+ return flagUsageHelper(f.Usage, "true")
}
// Apply uses BoolFlag's value to set a flag.BoolVar on the FlagSet.
-func (f *BoolFlag) Apply(s *flag.FlagSet) { s.BoolVar(&f.Value, f.Name, f.Default, f.Usage) }
+func (f *BoolFlag) Apply(s *flag.FlagSet) {
+ s.BoolVar(&f.Value, f.Name, f.Default, f.Usage)
+ if f.ShortName != 0 {
+ s.BoolVar(&f.Value, string(f.ShortName), f.Default, f.Usage)
+ }
+}
// StringFlag is a Flag of type string.
type StringFlag struct {
@@ -84,14 +119,14 @@ type StringFlag struct { Value string
}
-func (f *StringFlag) String() string { return formatHelper(f.Name, f.ArgName) }
+func (f *StringFlag) String() string { return flagFormatHelper(f.Name, f.ArgName) }
// FullUsage shows the deafult if the string is non-empty.
func (f *StringFlag) FullUsage() string {
if f.Default == "" {
- return usageHelper(f.Usage, "")
+ return flagUsageHelper(f.Usage, "")
}
- return usageHelper(f.Usage, strconv.Quote(f.Default))
+ return flagUsageHelper(f.Usage, strconv.Quote(f.Default))
}
// Apply uses StringFlag's value to set a flag.StringVar on the FlagSet.
@@ -106,14 +141,14 @@ type DurationFlag struct { Value time.Duration
}
-func (f *DurationFlag) String() string { return formatHelper(f.Name, f.ArgName) }
+func (f *DurationFlag) String() string { return flagFormatHelper(f.Name, f.ArgName) }
// FullUsage shows the default if the duration is non-zero.
func (f *DurationFlag) FullUsage() string {
if f.Default == 0 {
- return usageHelper(f.Usage, "")
+ return flagUsageHelper(f.Usage, "")
}
- return usageHelper(f.Usage, f.Default.String())
+ return flagUsageHelper(f.Usage, f.Default.String())
}
// Apply uses DurationFlag's value to set a flag.DurationVar on the FlagSet.
diff --git a/cmd/output.go b/cmd/format.go index c3a79a4..69fd0e9 100644 --- a/cmd/output.go +++ b/cmd/format.go @@ -1,5 +1,5 @@ /* - * output.go - Functions for handling command line formatting and output. + * format.go - Functions for handling output formatting. * * Copyright 2017 Google Inc. * Author: Joe Richey (joerichey@google.com) @@ -23,16 +23,22 @@ import ( "bytes" "fmt" "io" - "io/ioutil" - "log" "os" "strings" + "text/template" "unicode/utf8" "github.com/google/fscrypt/util" "golang.org/x/crypto/ssh/terminal" ) +// Suffixes for questions with a yes or no default +const ( + defaultYesSuffix = "[Y/n]" + defaultNoSuffix = "[y/N]" +) + +// Variables which control how output is formmatted and where it goes. var ( // TabWidth is the number of spaces used to display a tab. TabWidth = 8 @@ -48,34 +54,6 @@ var ( // output (errors should just return the appropriate error). If not set, // it is automatically set based on the provided flags. Output io.Writer - // HelpFlag writes help to Stdout - HelpFlag = &BoolFlag{ - Name: "help", - Usage: "Prints this help text for commands and subcommands", - } - // VerboseFlag indicates that all logging output should be printed. - VerboseFlag = &BoolFlag{ - Name: "verbose", - Usage: "Prints additional debug messages.", - } - // QuietFlag indicates that no normal output should be printed. - QuietFlag = &BoolFlag{ - Name: "quiet", - Usage: `Prints nothing except for errors and uses any default - option instead of prompting the user.`, - } - // ForceFlag indicates that the operation should proceed if possible. - ForceFlag = &BoolFlag{ - Name: "force", - Usage: `Print no confirmation prompts or warnings and - automatically proceed with the requested action.`, - } -) - -// Suffixes for questions with a yes or no default -const ( - defaultYesSuffix = "[Y/n]" - defaultNoSuffix = "[y/N]" ) // We use the width of the terminal unless we cannot get the width. @@ -92,18 +70,21 @@ func init() { } // Takes an input string text, and wraps the text so that each line begins with -// numTabs tabs and ends with a newline (except the last line), and each line -// has length less than lineLength. If the text contains a word which is too -// long, that word gets its own line. +// numTabs tabs (except the first line) and ends with a newline (except the last +// line), and each line has length less than lineLength. If the text contains a +// word which is too long, that word gets its own line. func wrapText(text string, numTabs int) string { // We use a buffer to format the wrapped text so we get O(n) runtime var buffer bytes.Buffer spaceLeft := 0 maxTextLen := LineLength - numTabs*TabWidth delimiter := strings.Repeat("\t", numTabs) - for _, word := range strings.Fields(text) { + for i, word := range strings.Fields(text) { wordLen := utf8.RuneCountInString(word) - if wordLen >= spaceLeft { + if i == 0 { + buffer.WriteString(word) + spaceLeft = maxTextLen - wordLen + } else if wordLen >= spaceLeft { // If no room left, write the word on the next line. buffer.WriteString("\n") buffer.WriteString(delimiter) @@ -120,22 +101,22 @@ func wrapText(text string, numTabs int) string { return buffer.String() } -// Configures the Output and log output io.Writers. Called before running -// commands but after processing flags. -func setupOutput() { - if VerboseFlag.Value { - log.SetOutput(os.Stdout) - } else { - log.SetOutput(ioutil.Discard) - } - if Output != nil { - return +// Add words to this map if pluralization does not just involve adding an s. +var plurals = map[string]string{ + "policy": "policies", +} + +// Pluralize returns the correct pluralization of a work along with the +// specified count. This means Pluralize(1, "policy") = "1 policy" but +// Pluralize(2, "policy") = "2 policies". +func Pluralize(count int, word string) string { + if count == 1 { + return fmt.Sprintf("%d %s", count, word) } - if QuietFlag.Value { - Output = ioutil.Discard - } else { - Output = os.Stdout + if plural, ok := plurals[word]; ok { + return fmt.Sprintf("%d %s", count, plural) } + return fmt.Sprintf("%d %ss", count, word) } // AskQuestion asks the user a yes or no question. Returning a boolean on a @@ -199,3 +180,14 @@ func AskConfirmation(question, warning string, defaultChoice bool) error { } return nil } + +// ExecuteTemplate creates an anonymous template from the text, and runs it with +// the provided Context and writer. Panics if text cannot be executed. +func ExecuteTemplate(w io.Writer, text string, ctx *Context) { + tmpl := template.Must(template.New("").Funcs(template.FuncMap{ + "wrapText": wrapText, + }).Parse(text)) + if err := tmpl.Execute(w, ctx); err != nil { + panic(err) + } +} diff --git a/cmd/fscrypt/commands.go b/cmd/fscrypt/commands.go index 2f23a0f..e8d32da 100644 --- a/cmd/fscrypt/commands.go +++ b/cmd/fscrypt/commands.go @@ -35,7 +35,6 @@ import ( "github.com/google/fscrypt/util" ) -// Setup is a command which can to global or per-filesystem initialization. var Setup = cli.Command{ Name: "setup", ArgsUsage: fmt.Sprintf("[%s]", mountpointArg), diff --git a/cmd/fscrypt/errors.go b/cmd/fscrypt/errors.go index 81a6798..4ce6133 100644 --- a/cmd/fscrypt/errors.go +++ b/cmd/fscrypt/errors.go @@ -22,10 +22,7 @@ package main import ( - "bytes" "fmt" - "os" - "path/filepath" "unicode/utf8" "github.com/pkg/errors" @@ -165,40 +162,6 @@ func newExitError(c *cli.Context, err error) error { return cli.NewExitError(message, failureExitCode) } -// usageError implements cli.ExitCoder to will print the usage and the return a -// non-zero value. This error should be used when a command is used incorrectly. -type usageError struct { - c *cli.Context - message string -} - -func (u *usageError) Error() string { - return fmt.Sprintf("%s: %s", getFullName(u.c), u.message) -} - -// We get the help to print after the error by having it run right before the -// application exits. This is very nasty, but there isn't a better way to do it -// with the constraints of urfave/cli. -func (u *usageError) ExitCode() int { - // Redirect help output to a buffer, so we can customize it. - buf := new(bytes.Buffer) - oldWriter := u.c.App.Writer - u.c.App.Writer = buf - - // Get the appropriate help - if getFullName(u.c) == filepath.Base(os.Args[0]) { - cli.ShowAppHelp(u.c) - } else { - cli.ShowCommandHelp(u.c, u.c.Command.Name) - } - - // Remove first line from help and print it out - buf.ReadBytes('\n') - buf.WriteTo(oldWriter) - u.c.App.Writer = oldWriter - return failureExitCode -} - // expectedArgsErr creates a usage error for the incorrect number of arguments // being specified. atMost should be true only if any number of arguments from 0 // to expectedArgs would be acceptable. diff --git a/cmd/fscrypt/flags.go b/cmd/fscrypt/flags.go index 5137eff..69126bc 100644 --- a/cmd/fscrypt/flags.go +++ b/cmd/fscrypt/flags.go @@ -22,133 +22,19 @@ package main import ( - "flag" "fmt" "log" "os/user" "regexp" - "strconv" "time" - "github.com/urfave/cli" - "github.com/google/fscrypt/actions" "github.com/google/fscrypt/security" "github.com/google/fscrypt/util" ) -// We define the types boolFlag, durationFlag, and stringFlag here instead of -// using those present in urfave/cli because we need them to conform to the -// prettyFlag interface (in format.go). The Getters just get the corresponding -// variables, String() just uses longDisplay, and Apply just sets the -// corresponding type of flag. -type boolFlag struct { - Name string - Usage string - Default bool - Value bool -} - -func (b *boolFlag) GetName() string { return b.Name } -func (b *boolFlag) GetArgName() string { return "" } -func (b *boolFlag) GetUsage() string { return b.Usage } - -func (b *boolFlag) String() string { - if !b.Default { - return longDisplay(b) - } - return longDisplay(b, strconv.FormatBool(b.Default)) -} - -func (b *boolFlag) Apply(set *flag.FlagSet) { - set.BoolVar(&b.Value, b.Name, b.Default, b.Usage) -} - -type durationFlag struct { - Name string - ArgName string - Usage string - Default time.Duration - Value time.Duration -} - -func (d *durationFlag) GetName() string { return d.Name } -func (d *durationFlag) GetArgName() string { return d.ArgName } -func (d *durationFlag) GetUsage() string { return d.Usage } - -func (d *durationFlag) String() string { - if d.Default == 0 { - return longDisplay(d) - } - return longDisplay(d, d.Value.String()) -} - -func (d *durationFlag) Apply(set *flag.FlagSet) { - set.DurationVar(&d.Value, d.Name, d.Default, d.Usage) -} - -type stringFlag struct { - Name string - ArgName string - Usage string - Default string - Value string -} - -func (s *stringFlag) GetName() string { return s.Name } -func (s *stringFlag) GetArgName() string { return s.ArgName } -func (s *stringFlag) GetUsage() string { return s.Usage } - -func (s *stringFlag) String() string { - if s.Default == "" { - return longDisplay(s) - } - return longDisplay(s, strconv.Quote(s.Default)) -} - -func (s *stringFlag) Apply(set *flag.FlagSet) { - set.StringVar(&s.Value, s.Name, s.Default, s.Usage) -} - -var ( - // allFlags contains every defined flag (used for formatting). - // UPDATE THIS ARRAY WHEN ADDING NEW FLAGS!!! - // TODO(joerichey) add presubmit rule to enforce this - allFlags = []prettyFlag{helpFlag, versionFlag, verboseFlag, quietFlag, - forceFlag, legacyFlag, skipUnlockFlag, timeTargetFlag, - sourceFlag, nameFlag, keyFileFlag, protectorFlag, - unlockWithFlag, policyFlag} - // universalFlags contains flags that should be on every command - universalFlags = []cli.Flag{verboseFlag, quietFlag, helpFlag} -) - // Bool flags: used to switch some behavior on or off var ( - helpFlag = &boolFlag{ - Name: "help", - Usage: `Prints help screen for commands and subcommands.`, - } - versionFlag = &boolFlag{ - Name: "version", - Usage: `Prints version and license information.`, - } - verboseFlag = &boolFlag{ - Name: "verbose", - Usage: `Prints additional debug messages to standard output.`, - } - quietFlag = &boolFlag{ - Name: "quiet", - Usage: `Prints nothing to standard output except for errors. - Selects the default for any options that would normally - show a prompt.`, - } - forceFlag = &boolFlag{ - Name: "force", - Usage: fmt.Sprintf(`Suppresses all confirmation prompts and - warnings, causing any action to automatically proceed. - WARNING: This bypasses confirmations for protective - operations, use with care.`), - } legacyFlag = &boolFlag{ Name: "legacy", Usage: `Allow for support of older kernels with ext4 (before diff --git a/cmd/fscrypt/format.go b/cmd/fscrypt/format.go deleted file mode 100644 index ef009d3..0000000 --- a/cmd/fscrypt/format.go +++ /dev/null @@ -1,162 +0,0 @@ -/* - * format.go - Contains all the functionality for formatting the command line - * output. This includes formatting the description and flags so that the whole - * text is <= LineLength characters. - * - * Copyright 2017 Google Inc. - * Author: Joe Richey (joerichey@google.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package main - -import ( - "bytes" - "fmt" - "os" - "regexp" - "strings" - "unicode/utf8" - - "github.com/urfave/cli" - "golang.org/x/crypto/ssh/terminal" - - "github.com/google/fscrypt/util" -) - -var ( - // lineLength is the maximum width of fscrypt's formatted output. It is - // usually the width of the terminal. - lineLength int - fallbackLineLength = 80 // fallback is punch cards - maxLineLength = 120 - // IndentLength is the number spaces to indent by. - indentLength = 2 - // length of the longest shortDisplay for a flag - maxShortDisplay int - // how much the a flag's usage text needs to be moved over - flagPaddingLength int -) - -// We use the init() function to compute our longest short display length. This -// is then used to compute the formatting and padding strings. This ensures we -// will always have room to display our flags, and the flag descriptions always -// appear in the same place. -func init() { - for _, flag := range allFlags { - displayLength := utf8.RuneCountInString(shortDisplay(flag)) - if displayLength > maxShortDisplay { - maxShortDisplay = displayLength - } - } - - // Pad usage enough so the flags have room. - flagPaddingLength = maxShortDisplay + 2*indentLength - - // We use the width of the terminal unless we cannot get the width. - width, _, err := terminal.GetSize(int(os.Stdout.Fd())) - if err != nil { - lineLength = fallbackLineLength - } else { - lineLength = util.MinInt(width, maxLineLength) - } - -} - -// Flags that conform to this interface can be used with an urfave/cli -// application and can be printed in the correct format. -type prettyFlag interface { - cli.Flag - GetArgName() string - GetUsage() string -} - -// How a flag should appear on the command line. We have two formats: -// --name -// --name=ARG_NAME -// The ARG_NAME appears if the prettyFlag's GetArgName() method returns a -// non-empty string. The returned string from shortDisplay() does not include -// any leading or trailing whitespace. -func shortDisplay(f prettyFlag) string { - if argName := f.GetArgName(); argName != "" { - return fmt.Sprintf("--%s=%s", f.GetName(), argName) - } - return fmt.Sprintf("--%s", f.GetName()) -} - -// How our flags should appear when displaying their usage. An example would be: -// -// --help Prints help screen for commands and subcommands. -// -// If a default is specified, this if appended to the usage. Example: -// -// --legacy Allow for support of older kernels with ext4 -// (before v4.8) and F2FS (before v4.6) filesystems. -// (default: true) -// -func longDisplay(f prettyFlag, defaultString ...string) string { - usage := f.GetUsage() - if len(defaultString) > 0 { - usage += fmt.Sprintf(" (default: %v)", defaultString[0]) - } - - // We pad the the shortDisplay on the right with enough spaces to equal - // the longest flag's display - shortDisp := shortDisplay(f) - length := utf8.RuneCountInString(shortDisp) - shortDisp += strings.Repeat(" ", maxShortDisplay-length) - - return indent + shortDisp + indent + wrapText(usage, flagPaddingLength) -} - -// Regex that determines if we are starting an ordered list -var listRegex = regexp.MustCompile(`^\([\d]+\)$`) - -// Takes an input string text, and wraps the text so that each line begins with -// padding spaces (except for the first line), ends with a newline (except the -// last line), and each line has length less than lineLength. If the text -// contains a word which is too long, that word gets its own line. -func wrapText(text string, padding int) string { - // We use a buffer to format the wrapped text so we get O(n) runtime - var buffer bytes.Buffer - spaceLeft := 0 - maxTextLen := lineLength - padding - delimiter := strings.Repeat(" ", padding) - for i, word := range strings.Fields(text) { - wordLen := utf8.RuneCountInString(word) - switch { - case i == 0: - // No delimiter for the first line - buffer.WriteString(word) - spaceLeft = maxTextLen - wordLen - case listRegex.Match([]byte(word)): - // Add an additional line to separate list items. - buffer.WriteString("\n") - fallthrough - case wordLen+1 > spaceLeft: - // If no room left, write the word on the next line. - buffer.WriteString("\n") - buffer.WriteString(delimiter) - buffer.WriteString(word) - spaceLeft = maxTextLen - wordLen - default: - // Write word on this line - buffer.WriteByte(' ') - buffer.WriteString(word) - spaceLeft -= 1 + wordLen - } - } - - return buffer.String() -} diff --git a/cmd/fscrypt/fscrypt.go b/cmd/fscrypt/fscrypt.go index d6162f6..f1649fc 100644 --- a/cmd/fscrypt/fscrypt.go +++ b/cmd/fscrypt/fscrypt.go @@ -22,127 +22,63 @@ fscrypt is a command line tool for managing linux filesystem encryption. */ -// +build linux,cgo - package main import ( "fmt" - "io/ioutil" - "log" - "os" - "time" - - "github.com/urfave/cli" -) -var ( - // Current version of the program (set by Makefile) - version string - // Formatted build time (set by Makefile) - buildTime string - // Authors to display in the info command - Authors = []cli.Author{{ - Name: "Joe Richey", - Email: "joerichey@google.com", - }} + "github.com/google/fscrypt/cmd" ) -func main() { - cli.AppHelpTemplate = appHelpTemplate - cli.CommandHelpTemplate = commandHelpTemplate - cli.SubcommandHelpTemplate = subcommandHelpTemplate - - // Create our command line application - app := cli.NewApp() - app.Usage = shortUsage - app.Authors = Authors - app.Copyright = apache2GoogleCopyright - - // Grab the version and compilation time passed in from the Makefile. - app.Version = version - app.Compiled, _ = time.Parse(time.UnixDate, buildTime) - app.OnUsageError = onUsageError - - // Setup global flags - cli.HelpFlag = helpFlag - cli.VersionFlag = versionFlag - cli.VersionPrinter = func(c *cli.Context) { - cli.HelpPrinter(c.App.Writer, versionInfoTemplate, c.App) - } - app.Flags = universalFlags - - // We hide the help subcommand so that "fscrypt <command> --help" works - // and "fscrypt <command> help" does not. - app.HideHelp = true - - // Initialize command list and setup all of the commands. - app.Action = defaultAction - app.Commands = []cli.Command{Setup, Encrypt, Unlock, Purge, Status, Metadata} - for i := range app.Commands { - setupCommand(&app.Commands[i]) - } - - app.Run(os.Args) +func main() { fscryptCommand.Run() } + +var fscryptCommand = cmd.Command{ + Title: "manage linux filesystem encryption", + UsageLines: []string{ + fmt.Sprintf("<command> [arguments] [command options] [%s | %s]", + cmd.VerboseFlag, cmd.QuietFlag), + cmd.VersionUsage, + }, + SubCommands: []*Command{ + &setupCommand, + &encryptCommand, + // unlockCommand, + // purgeCommand, + // statusCommand, + // metadataCommand, + cmd.VersionCommand, + }, + Flags: []cmd.Flag{cmd.VerboseFlag, cmd.QuietFlag, cmd.HelpFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt", Section: 8}, } -// setupCommand performs some common setup for each command. This includes -// hiding the help, formating the description, adding in the necessary -// flags, setting up error handlers, etc... Note that the command is modified -// in place and its subcommands are also setup. -func setupCommand(command *cli.Command) { - command.Description = wrapText(command.Description, indentLength) - command.HideHelp = true - command.Flags = append(command.Flags, universalFlags...) - - if command.Action == nil { - command.Action = defaultAction - } - - // Setup function handlers - command.OnUsageError = onUsageError - if len(command.Subcommands) == 0 { - command.Before = setupBefore - } else { - // Cleanup subcommands (if applicable) - for i := range command.Subcommands { - setupCommand(&command.Subcommands[i]) - } - } +// setup performs global or per-filesystem initialization of fscrypt data. +var setupCommand = &cmd.Command{ + Name: "setup", + Title: "setup a system/filesystem to use fscrypt", + UsageLines: []string{ + fmt.Sprintf("[options]"), + fmt.Sprintf("%s [%s]", mountpointArg, cmd.ForceFlag), + }, + Arguments: []*cmd.Argument{mountpointArg}, + InheritFlags: true, + Flags: []cmd.Flag{configFileFlag, targetFlag, legacyFlag, cmd.ForceFlag}, + ManPage: &cmd.ManPage{Name: "fscrypt-setup", Section: 8}, + Action: setupAction, } -// setupBefore makes sure our logs, errors, and output are going to the correct -// io.Writers and that we haven't over-specified our flags. We only print the -// logs when using verbose, and only print normal stuff when not using quiet. -// When running with sudo, this function also verifies that we have the proper -// keyring linkage enabled. -func setupBefore(c *cli.Context) error { - log.SetOutput(ioutil.Discard) - c.App.Writer = ioutil.Discard - - if verboseFlag.Value { - log.SetOutput(os.Stdout) +func setupAction(c *cmd.Context) error { + switch len(c.Args) { + case 0: + // Case (1) - global setup + return createGlobalConfig(configFileFlag.Value) + case 1: + // Case (2) - filesystem setup + return setupFilesystem(c.Args[0]) + default: + return cmd.CheckExpectedArgs(c, 1, true) } - if !quietFlag.Value { - c.App.Writer = os.Stdout - } - return nil } -// defaultAction will be run when no command is specified. -func defaultAction(c *cli.Context) error { - // Always default to showing the help - if helpFlag.Value { - cli.ShowAppHelp(c) - return nil - } - - // Only exit when not calling with the help command - var message string - if args := c.Args(); args.Present() { - message = fmt.Sprintf("command \"%s\" not found", args.First()) - } else { - message = "no command was specified" - } - return &usageError{c, message} -} +// encrypt performs the functions of setupDirectory and Unlock in one command. +var encryptCommand = &cmd.Command{} diff --git a/cmd/fscrypt/prompt.go b/cmd/fscrypt/prompt.go index 0031e8f..bccf534 100644 --- a/cmd/fscrypt/prompt.go +++ b/cmd/fscrypt/prompt.go @@ -25,7 +25,6 @@ import ( "os" "os/user" "strconv" - "strings" "github.com/pkg/errors" @@ -47,67 +46,6 @@ var sourceDescriptions = map[metadata.SourceType]string{ metadata.SourceType_raw_key: "A raw 256-bit key", } -// askQuestion asks the user a yes or no question. Returning a boolean on a -// successful answer and an error if there was not a response from the user. -// Returns the defaultChoice on empty input (or in quiet mode). -func askQuestion(question string, defaultChoice bool) (bool, error) { - // If in quiet mode, we just use the default - if quietFlag.Value { - return defaultChoice, nil - } - // Loop until failure or valid input - for { - if defaultChoice { - fmt.Print(question + defaultYesSuffix) - } else { - fmt.Print(question + defaultNoSuffix) - } - - input, err := util.ReadLine() - if err != nil { - return false, err - } - - switch strings.ToLower(input) { - case "y", "yes": - return true, nil - case "n", "no": - return false, nil - case "": - return defaultChoice, nil - } - } -} - -// askConfirmation asks the user for confirmation of a specific action. An error -// is returned if the user declines or IO fails. -func askConfirmation(question string, defaultChoice bool, warning string) error { - // All confirmations are "yes" if we are forcing. - if forceFlag.Value { - return nil - } - - // Defaults of "no" require forcing. - if !defaultChoice { - if quietFlag.Value { - return ErrNoDesctructiveOps - } - } - - if warning != "" && !quietFlag.Value { - fmt.Println(wrapText("WARNING: "+warning, 0)) - } - - confirmed, err := askQuestion(question, defaultChoice) - if err != nil { - return err - } - if !confirmed { - return ErrCanceled - } - return nil -} - // usernameFromID returns the username for the provided UID. If the UID does not // correspond to a user or the username is blank, an error is returned. func usernameFromID(uid int64) (string, error) { diff --git a/cmd/fscrypt/setup.go b/cmd/fscrypt/setup.go index 72dfbdb..63f0c51 100644 --- a/cmd/fscrypt/setup.go +++ b/cmd/fscrypt/setup.go @@ -22,46 +22,45 @@ package main import ( "fmt" - "io" "os" "github.com/google/fscrypt/actions" - "github.com/google/fscrypt/util" + "github.com/google/fscrypt/cmd" ) // createGlobalConfig creates (or overwrites) the global config file -func createGlobalConfig(w io.Writer, path string) error { - if !util.IsUserRoot() { - return ErrMustBeRoot +func createGlobalConfig(path string) error { + if err := cmd.CheckIfRoot(); err != nil { + return err } // Ask to create or replace the config file _, err := os.Stat(path) switch { case err == nil: - err = askConfirmation(fmt.Sprintf("Replace %q?", path), false, "") + err = cmd.AskConfirmation(fmt.Sprintf("Replace %q?", path), "", false) if err == nil { err = os.Remove(path) } case os.IsNotExist(err): - err = askConfirmation(fmt.Sprintf("Create %q?", path), true, "") + err = cmd.AskConfirmation(fmt.Sprintf("Create %q?", path), "", true) } if err != nil { return err } - fmt.Fprintln(w, "Customizing passphrase hashing difficulty for this system...") + fmt.Fprintln(cmd.Output, "Customizing passphrase hashing difficulty for this system...") err = actions.CreateConfigFile(timeTargetFlag.Value, legacyFlag.Value) if err != nil { return err } - fmt.Fprintf(w, "Created global config file at %q.\n", path) + fmt.Fprintf(cmd.Output, "Created global config file at %q.\n", path) return nil } // setupFilesystem creates the directories for a filesystem to use fscrypt. -func setupFilesystem(w io.Writer, path string) error { +func setupFilesystem(path string) error { ctx, err := actions.NewContextFromMountpoint(path, nil) if err != nil { return err @@ -71,8 +70,8 @@ func setupFilesystem(w io.Writer, path string) error { return err } - fmt.Fprintf(w, "Metadata directories created at %q.\n", ctx.Mount.BaseDir()) - fmt.Fprintf(w, "Filesystem %q (%s) ready for use with %s encryption.\n", + fmt.Fprintf(Output, "Metadata directories created at %q.\n", ctx.Mount.BaseDir()) + fmt.Fprintf(Output, "Filesystem %q (%s) ready for use with %s encryption.\n", ctx.Mount.Path, ctx.Mount.Device, ctx.Mount.Filesystem) return nil } diff --git a/cmd/fscrypt/strings.go b/cmd/fscrypt/strings.go index e90abe1..07b6b64 100644 --- a/cmd/fscrypt/strings.go +++ b/cmd/fscrypt/strings.go @@ -22,26 +22,6 @@ package main import ( "fmt" - "strings" -) - -// Global application strings -const ( - shortUsage = "A tool for managing Linux filesystem encryption" - - apache2GoogleCopyright = `Copyright 2017 Google, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.` ) // Argument usage strings @@ -52,76 +32,6 @@ const ( mountpointIDArg = mountpointArg + ":ID" ) -// Text Templates which format our command line output (using text/template) -var ( - // indent is the prefix for the output lines in each section - indent = strings.Repeat(" ", indentLength) - // Top level help output: what is printed for "fscrypt" or "fscrypt --help" - appHelpTemplate = `{{.HelpName}} - {{.Usage}} - -Usage: -` + indent + `{{.HelpName}} COMMAND [arguments] [options] - -Commands:{{range .VisibleCommands}} -` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}} -{{if .Description}} -Description: -` + indent + `{{.Description}} -{{end}} -Options: -{{range .VisibleFlags}}{{.}} - -{{end}}` - - // Command help output, used when a command has no subcommands - commandHelpTemplate = `{{.HelpName}} - {{.Usage}} - -Usage: -` + indent + `{{.HelpName}}{{if .ArgsUsage}} {{.ArgsUsage}}{{end}}{{if .VisibleFlags}} [options]{{end}} -{{if .Description}} -Description: -` + indent + `{{.Description}} -{{end}}{{if .VisibleFlags}} -Options: -{{range .VisibleFlags}}{{.}} - -{{end}}{{end}}` - - // Subcommand help output, used when a command has subcommands - subcommandHelpTemplate = `{{.HelpName}} - {{.Usage}} - -Usage: -` + indent + `{{.HelpName}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}SUBCOMMAND [arguments]{{end}}{{if .VisibleFlags}} [options]{{end}} - -Subcommands:{{range .VisibleCommands}} -` + indent + `{{join .Names ", "}}{{"\t- "}}{{.Usage}}{{end}} -{{if .Description}} -Description: -` + indent + `{{.Description}} -{{end}}{{if .VisibleFlags}} -Options: -{{range .VisibleFlags}}{{.}} - -{{end}}{{end}}` - - // Additional info, used with "fscrypt version" - versionInfoTemplate = `{{.HelpName}} - {{.Usage}} - -{{if .Version}}Version: -` + indent + `{{.Version}} - -{{end}}{{if .Compiled}}Compiled: -` + indent + `{{.Compiled}} - -{{end}}{{if len .Authors}}Author{{with $length := len .Authors}}{{if ne 1 $length}}s{{end}}{{end}}:{{range .Authors}} -` + indent + `{{.}}{{end}} - -{{end}}{{if .Copyright}}Copyright: -` + indent + `{{.Copyright}} - -{{end}}` -) - // Add words to this map if pluralization does not just involve adding an s. var plurals = map[string]string{ "policy": "policies", diff --git a/cmd/helper.go b/cmd/helper.go deleted file mode 100644 index 77f6b2f..0000000 --- a/cmd/helper.go +++ /dev/null @@ -1,20 +0,0 @@ -/* - * helper.go - Helper functions for using the cmd package - * - * Copyright 2017 Google Inc. - * Author: Joe Richey (joerichey@google.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - - package cmd diff --git a/cmd/info.go b/cmd/info.go deleted file mode 100644 index 96079ea..0000000 --- a/cmd/info.go +++ /dev/null @@ -1,96 +0,0 @@ -/* - * info.go - Global information about the program. - * - * Copyright 2017 Google Inc. - * Author: Joe Richey (joerichey@google.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package cmd - -import ( - "time" - - "github.com/blang/semver" - "github.com/pkg/errors" -) - -var ( - // VersionTag (if set) will be displayed in both the short and long - // version output and can be accessed though Context.Info.Version. - // VersionTag must be formatted using Semver (http://semver.org/). - // - // Often set in Makefile with "-X cmd.VersionTag=$(VERSION)" - VersionTag string - // BuildTimeTag (if set) will be displayed in the long version - // output and can be accessed thought Context.Info.BuildTime. This - // string must be formatted as the output of UNIX `date`. - // - // Often set in Makefile with "-X cmd.BuildTimeTag=$(shell date)" - BuildTimeTag string - // Authors (if non-empty) are displayed in the long version output and - // can be accessed though Context.Info.Authors. - Authors []Author - // Copyright (if set) is displayed in the long version output and can - // be accessed through Context.Info.Copyright. - Copyright string -) - -// fscrypt specific initialization -func init() { - Authors = []Author{{ - Name: "Joe Richey", - Email: "joerichey@google.com", - }} - Copyright = `Copyright 2017 Google, Inc. - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License.` -} - -// Creates the Info structure by parsing the above global variables. Panics if -// the variables to parse are in the incorrect format. -func parseInfo() *Info { - var err error - - var t time.Time - if BuildTimeTag != "" { - if t, err = time.Parse(time.UnixDate, BuildTimeTag); err != nil { - panic(err) - } - } - - var v semver.Version - if VersionTag != "" { - if v, err = semver.ParseTolerant(VersionTag); err != nil { - panic(errors.Wrapf(err, "semver: parsing %q", VersionTag)) - } - } - - return &Info{ - Version: v, - BuildTime: t, - Authors: Authors, - Copyright: Copyright, - } -} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..a23de2d --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,199 @@ +/*
+ * run.go - Functions to setup and run Command Actions.
+ *
+ * Copyright 2017 Google Inc.
+ * Author: Joe Richey (joerichey@google.com)
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package cmd
+
+import (
+ "flag"
+ "fmt"
+ "io/ioutil"
+ "log"
+ "os"
+)
+
+var (
+ // HelpAction is performed whenever a Command uses the HelpFlag.
+ HelpAction Action = func(ctx *Context) error {
+ ExecuteTemplate(os.Stdout, TemplateTitle, ctx)
+ ExecuteTemplate(os.Stdout, TemplateUsage, ctx)
+ return nil
+ }
+ // DefaultAction is preformed when a Command has no Action specified.
+ DefaultAction = HelpAction
+ // stopFlag indicates that everything after the flag is an argument, not
+ // a command or flag.
+ stopFlag = "--"
+)
+
+// TemplateTitle describes the format of a Command's one line title.
+var TemplateTitle = "{{.FullName}}{{if .Command.Title}} - {{.Command.Title}}{{end}}\n"
+
+// TemplateUsage describes the format of a Command's usage.
+var TemplateUsage = `{{with $lines := .Command.UsageLines}}
+Usage:
+{{- range $lines}}
+ {{$.FullName}} {{. -}}
+{{end}}
+{{end -}}
+
+{{with $commands := .Command.SubCommands}}
+Commands:
+{{- range $commands}}
+ {{.Name}}{{if .Title}} - {{.Title}}{{end -}}
+{{end}}
+{{end -}}
+
+{{with $arguments := .FullArguments}}
+Arguments:
+{{- range $arguments}}
+ {{.}}
+ {{wrapText .Usage 2 -}}
+{{end}}
+{{end -}}
+
+{{with $flags := .FullFlags}}
+Options:
+{{- range $flags}}
+ {{.}}
+ {{wrapText .FullUsage 2 -}}
+{{end}}
+{{end -}}
+
+{{with .ManPage}}
+For more information, see {{.}}.
+{{end -}}
+`
+
+// Divide the arguments into flag arguments (those starting with "-") and normal
+// arguments. If "--" appears in the list, it will classified as a normal
+// argument as well as all arguments following it. Also removes empty args.
+func sortArgs(args []string) (normalArgs, flagArgs []string) {
+ var arg string
+ for len(args) > 0 {
+ arg, args = args[0], args[1:]
+ if arg == "" {
+ continue
+ }
+ if arg == stopFlag {
+ normalArgs = append(normalArgs, arg)
+ normalArgs = append(normalArgs, args...)
+ return
+ } else if arg[0] == '-' {
+ flagArgs = append(flagArgs, arg)
+ } else {
+ normalArgs = append(normalArgs, arg)
+ }
+ }
+ return
+}
+
+// Returns the name of the requested sub-command or empty string (if a
+// sub-command was not requested).
+func subCommandName(ctx *Context) string {
+ // We must have actual arguments and subcommands to run a sub-command.
+ if len(ctx.Command.SubCommands) == 0 || len(ctx.Args) == 0 {
+ return ""
+ }
+ name := ctx.Args[0]
+ if name == stopFlag {
+ return ""
+ }
+ return name
+}
+
+// Returns the appropriate child context with a sub-command whose name matches
+// the provided name. If no sub-commands match the provied name, handle the
+// appropriate error and do not return.
+func getSubContext(ctx *Context, name string) *Context {
+ for _, subCommand := range ctx.Command.SubCommands {
+ if subCommand.Name == name {
+ return &Context{
+ Command: subCommand,
+ Parent: ctx,
+ Args: ctx.Args[1:],
+ flagArgs: ctx.flagArgs,
+ }
+ }
+ }
+ ctx.processError(UsageError(fmt.Sprintf("unknown command %q", name)))
+ return nil
+}
+
+// Configures the Output and log output io.Writers. Called before running
+// commands but after processing flags.
+func setupOutput() {
+ if VerboseFlag.Value {
+ log.SetOutput(os.Stdout)
+ } else {
+ log.SetOutput(ioutil.Discard)
+ }
+ if Output != nil {
+ return
+ }
+ if QuietFlag.Value {
+ Output = ioutil.Discard
+ } else {
+ Output = os.Stdout
+ }
+}
+
+// Remove the stopFlag from the args if it is present. Args are modified
+// in-place and the correctly sized slice is returned.
+func setupArgs(args []string) []string {
+ for i, arg := range args {
+ if arg == stopFlag {
+ return append(args[:i], args[i+1:]...)
+ }
+ }
+ return args
+}
+
+// Return a command's action, the HelpAction, or DefaultAction.
+func getAction(cmd *Command) Action {
+ if HelpFlag.Value {
+ return HelpAction
+ }
+ if cmd.Action == nil {
+ return DefaultAction
+ }
+ return cmd.Action
+}
+
+func (ctx *Context) run() {
+ if name := subCommandName(ctx); name != "" {
+ getSubContext(ctx, name).run()
+ return
+ }
+
+ flagSet := flag.NewFlagSet("", flag.ContinueOnError)
+ flagSet.SetOutput(ioutil.Discard)
+ for _, flag := range ctx.FullFlags() {
+ flag.Apply(flagSet)
+ }
+ if err := flagSet.Parse(ctx.flagArgs); err != nil {
+ ctx.processError(err)
+ return
+ }
+
+ setupOutput()
+ ctx.Args = setupArgs(ctx.Args)
+ action := getAction(ctx.Command)
+
+ ctx.processError(action(ctx))
+}
diff --git a/cmd/strings.go b/cmd/strings.go deleted file mode 100644 index 16c80f2..0000000 --- a/cmd/strings.go +++ /dev/null @@ -1,20 +0,0 @@ -/* - * strings.go - Strings and templates for output formatting - * - * Copyright 2017 Google Inc. - * Author: Joe Richey (joerichey@google.com) - * - * Licensed under the Apache License, Version 2.0 (the "License"); you may not - * use this file except in compliance with the License. You may obtain a copy of - * the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT - * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the - * License for the specific language governing permissions and limitations under - * the License. - */ - -package cmd diff --git a/cmd/version.go b/cmd/version.go index 99097b5..219684d 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -21,33 +21,89 @@ package cmd import ( "fmt" + "time" "github.com/blang/semver" + "github.com/pkg/errors" ) -// Templates for use with the version command, which both parse the Info var. -var ( - VersionTemplate = "{{.FullName}} {{.Info.Version}}\n" - VersionLongTemplate = `{{if .Info.BuildTime}} -Compiled: - {{.Info.BuildTime}} -{{end}} +// Info contains global information about the program. +var Info = &InfoData{} -{{with $length := len .Info.Authors}} -{{if $length}} -Author{{if ne 1 $length}}s{{end}}: -{{range .Info.Authors}} - {{.Name}}{{if .Email}} <{{.Email}}>{{end}} -{{end}} -{{end}} -{{end}} +// InfoData describes the structure of our global program information +type InfoData struct { + // Version (if set) will be displayed in both the short and long version + // output. This can be set directly or using VersionTag at link time. + Version semver.Version + // BuildTime (if set) will be displayed in the long version output. This + // can be set directory or by setting cmd.BuildTimeTag in a linking + // flag. s + // + BuildTime time.Time + // Authors (if non-empty) are displayed in the long version output. + Authors []Author + // Copyright (if set) is displayed in the long version output. + Copyright string +} -{{if .Info.Copyright}} -Copyright: -{{.Info.Copyright}} -{{end}}` +// Author contains the contact information for a contributor. +type Author struct { + Name string + Email string +} + +// We have to use separate Tag variables, because build tags of the form: +// "-X cmd.Info.Version=foo" +// are invalid. +var ( + // VersionTag can be set via the linker, and its value will be used to + // set Info.Version. Format this tag using Semver (http://semver.org/). + // Example: + // "-X cmd.VersionTag=1.2.3-beta" + VersionTag string + // BuildTimeTag can be set via the linker, and its value will be used to + // set Info.BuildTime. Format this tag like the output of UNIX's `date`. + // Example: + // "-X cmd.BuildTimeTag=Thu Oct 12 21:32:02 PDT 2017" + BuildTimeTag string ) +func init() { + var err error + // parse the tag variables + if VersionTag != "" { + Info.Version, err = semver.ParseTolerant(VersionTag) + if err != nil { + panic(errors.Wrapf(err, "semver: parsing %q", VersionTag)) + } + } + if BuildTimeTag != "" { + Info.BuildTime, err = time.Parse(time.UnixDate, BuildTimeTag) + if err != nil { + panic(err) + } + } + + // fscrypt specific initialization + Info.Authors = []Author{{ + Name: "Joe Richey", + Email: "joerichey@google.com", + }} + Info.Copyright = `Copyright 2017 Google, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License.` +} + // VersionCommand is a command which will display either the VersionTag (by // default) or the full version information: version, copyright, authors, etc... var VersionCommand = &Command{ @@ -67,13 +123,34 @@ var longFlag = &BoolFlag{ Usage: "Print the detailed version, build, and copyright information.", } +// TemplateVersionShort describes the format of the one line version command. +var TemplateVersionShort = "{{.FullName}} {{.Info.Version}}\n" + +// TemplateVersionLong describes the format of the additional version data. +var TemplateVersionLong = `{{if not .Info.BuildTime.IsZero}} +Compiled: + {{.Info.BuildTime}} +{{end -}} + +{{with $length := len .Info.Authors}} +Author{{if ne 1 $length}}s{{end}}: +{{- range $.Info.Authors}} + {{.Name}}{{if .Email}} <{{.Email}}>{{end -}} +{{end}} +{{end -}} + +{{if .Info.Copyright}} +Copyright: + {{.Info.Copyright}} +{{end}}` + func versionAction(ctx *Context) error { - if ctx.Info.Version.Equals(semver.Version{}) { + if Info.Version.Equals(semver.Version{}) { return ErrUnknownVersion } - ctx.executeTemplate(Output, VersionTemplate) + ExecuteTemplate(Output, TemplateVersionShort, ctx) if longFlag.Value { - ctx.executeTemplate(Output, VersionLongTemplate) + ExecuteTemplate(Output, TemplateVersionLong, ctx) } return nil } diff --git a/ext4/ext4.go b/ext4/ext4.go index 80d229d..dcf8204 100644 --- a/ext4/ext4.go +++ b/ext4/ext4.go @@ -25,58 +25,50 @@ import ( "github.com/google/fscrypt/cmd" ) -// Arguments used with the ext4 enable/disable commands. var ( - MountpointArg = &cmd.Argument{ + mountpointArg = &cmd.Argument{ ArgName: "mountpoint", Usage: "the path to an ext4 filesystem's mountpoint", } - DeviceArg = &cmd.Argument{ + deviceArg = &cmd.Argument{ ArgName: "device", Usage: "the path to a device containing an ext4 filesystem", } - Ext4Usage = fmt.Sprintf("(%s | %s) [options]", MountpointArg, DeviceArg) + ext4Usage = fmt.Sprintf("(%s | %s) [options]", mountpointArg, deviceArg) ) -// Commands for running the ext4 enable/disable commands. -var () +func main() { ext4Command.Run() } -var Ext4Command = &cmd.Command{ - Title: "toggle ext4 filesystem encryption flag", +var ext4Command = &cmd.Command{ + Title: "manage ext4 encryption feature flag", UsageLines: []string{ - fmt.Sprintf("(enable | disable) %s", Ext4Usage), + fmt.Sprintf("enable %s", ext4Usage), + fmt.Sprintf("disable %s", ext4Usage), cmd.VersionUsage, }, - SubCommands: []*cmd.Command{EnableCommand, DisableCommand, cmd.VersionCommand}, - Arguments: []*cmd.Argument{MountpointArg, DeviceArg}, + SubCommands: []*cmd.Command{enableCommand, disableCommand, cmd.VersionCommand}, + Arguments: []*cmd.Argument{mountpointArg, deviceArg}, Flags: []cmd.Flag{cmd.ForceFlag, cmd.VerboseFlag, cmd.HelpFlag}, - ManPage: &cmd.ManPage{ - Title: "fscrypt-ext4", - Section: 8, - }, + ManPage: &cmd.ManPage{Name: "fscrypt-ext4", Section: 8}, } - -var EnableCommand = &cmd.Command{ +var enableCommand = &cmd.Command{ Name: "enable", Title: "turn on encryption for an ext4 filesystem", - UsageLines: []string{Ext4Usage}, + UsageLines: []string{ext4Usage}, InheritArguments: true, InheritFlags: true, - Action: func(ctx *cmd.Context) error { return toggleState(ctx, true) }, + Action: func(c *cmd.Context) error { return toggleState(c, true) }, } - -var DisableCommand = &cmd.Command{ +var disableCommand = &cmd.Command{ Name: "disable", Title: "turn off encryption for an ext4 filesystem", - UsageLines: []string{Ext4Usage}, + UsageLines: []string{ext4Usage}, InheritArguments: true, InheritFlags: true, - Action: func(ctx *cmd.Context) error { return toggleState(ctx, false) }, + Action: func(c *cmd.Context) error { return toggleState(c, false) }, } -func main() { Ext4Command.Run() } - -func toggleState(ctx *cmd.Context, enable bool) error { +func toggleState(c *cmd.Context, enable bool) error { fmt.Fprintf(cmd.Output, "Toggle value = %v", enable) return nil } diff --git a/ext4/feature_flag.go b/ext4/feature_flag.go index ab618e2..58b3669 100644 --- a/ext4/feature_flag.go +++ b/ext4/feature_flag.go @@ -1,3 +1,5 @@ +// +build linux,cgo + /* * feature_flag.go - Changes encryption flag for an ext4 filesystem. * |