diff options
Diffstat (limited to 'cmd/fscrypt')
| -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 |
8 files changed, 57 insertions, 588 deletions
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", |