aboutsummaryrefslogtreecommitdiff
path: root/cmd
diff options
context:
space:
mode:
authorJoseph Richey <joerichey94@gmail.com>2017-10-16 10:50:53 -0700
committerJoseph Richey <joerichey94@gmail.com>2017-10-19 02:22:25 -0700
commitb5cc60b2b974645f0d09721c292cd243d049cbcf (patch)
treefbc44d87ba8dd83e2238ff8e217ea560f56b3091 /cmd
parentb4299090c3e503ba0c49a6086b1a46c218ca45f4 (diff)
Refactor almost complete
Diffstat (limited to 'cmd')
-rw-r--r--cmd/cmd.go209
-rw-r--r--cmd/errors.go78
-rw-r--r--cmd/flag.go67
-rw-r--r--cmd/format.go (renamed from cmd/output.go)92
-rw-r--r--cmd/fscrypt/commands.go1
-rw-r--r--cmd/fscrypt/errors.go37
-rw-r--r--cmd/fscrypt/flags.go114
-rw-r--r--cmd/fscrypt/format.go162
-rw-r--r--cmd/fscrypt/fscrypt.go156
-rw-r--r--cmd/fscrypt/prompt.go62
-rw-r--r--cmd/fscrypt/setup.go23
-rw-r--r--cmd/fscrypt/strings.go90
-rw-r--r--cmd/helper.go20
-rw-r--r--cmd/info.go96
-rw-r--r--cmd/run.go199
-rw-r--r--cmd/strings.go20
-rw-r--r--cmd/version.go121
17 files changed, 619 insertions, 928 deletions
diff --git a/cmd/cmd.go b/cmd/cmd.go
index 945e945..3358015 100644
--- a/cmd/cmd.go
+++ b/cmd/cmd.go
@@ -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
}