From ff792cf43bf366a63cd86b788a90e6d6a5328e40 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 21 Apr 2018 15:35:03 -0400 Subject: [PATCH 01/69] vendor github.com/pkg/errors vendor/github.com/urfave/cli --- vendor/github.com/pkg/errors/LICENSE | 23 + vendor/github.com/pkg/errors/README.md | 52 + vendor/github.com/pkg/errors/appveyor.yml | 32 + vendor/github.com/pkg/errors/errors.go | 269 +++ vendor/github.com/pkg/errors/stack.go | 147 ++ vendor/github.com/urfave/cli/CHANGELOG.md | 435 +++++ .../github.com/urfave/cli/CODE_OF_CONDUCT.md | 74 + vendor/github.com/urfave/cli/CONTRIBUTING.md | 19 + vendor/github.com/urfave/cli/LICENSE | 21 + vendor/github.com/urfave/cli/MAINTAINERS.md | 1 + vendor/github.com/urfave/cli/README.md | 1526 +++++++++++++++++ vendor/github.com/urfave/cli/app.go | 508 ++++++ vendor/github.com/urfave/cli/appveyor.yml | 26 + vendor/github.com/urfave/cli/category.go | 44 + vendor/github.com/urfave/cli/cli.go | 22 + vendor/github.com/urfave/cli/command.go | 336 ++++ vendor/github.com/urfave/cli/context.go | 287 ++++ vendor/github.com/urfave/cli/errors.go | 115 ++ vendor/github.com/urfave/cli/flag-types.json | 93 + vendor/github.com/urfave/cli/flag.go | 786 +++++++++ .../github.com/urfave/cli/flag_generated.go | 640 +++++++ vendor/github.com/urfave/cli/funcs.go | 44 + .../github.com/urfave/cli/generate-flag-types | 256 +++ vendor/github.com/urfave/cli/help.go | 345 ++++ vendor/github.com/urfave/cli/runtests | 122 ++ vendor/github.com/urfave/cli/sort.go | 29 + vendor/vendor.json | 19 + 27 files changed, 6271 insertions(+) create mode 100644 vendor/github.com/pkg/errors/LICENSE create mode 100644 vendor/github.com/pkg/errors/README.md create mode 100644 vendor/github.com/pkg/errors/appveyor.yml create mode 100644 vendor/github.com/pkg/errors/errors.go create mode 100644 vendor/github.com/pkg/errors/stack.go create mode 100644 vendor/github.com/urfave/cli/CHANGELOG.md create mode 100644 vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/urfave/cli/CONTRIBUTING.md create mode 100644 vendor/github.com/urfave/cli/LICENSE create mode 100644 vendor/github.com/urfave/cli/MAINTAINERS.md create mode 100644 vendor/github.com/urfave/cli/README.md create mode 100644 vendor/github.com/urfave/cli/app.go create mode 100644 vendor/github.com/urfave/cli/appveyor.yml create mode 100644 vendor/github.com/urfave/cli/category.go create mode 100644 vendor/github.com/urfave/cli/cli.go create mode 100644 vendor/github.com/urfave/cli/command.go create mode 100644 vendor/github.com/urfave/cli/context.go create mode 100644 vendor/github.com/urfave/cli/errors.go create mode 100644 vendor/github.com/urfave/cli/flag-types.json create mode 100644 vendor/github.com/urfave/cli/flag.go create mode 100644 vendor/github.com/urfave/cli/flag_generated.go create mode 100644 vendor/github.com/urfave/cli/funcs.go create mode 100755 vendor/github.com/urfave/cli/generate-flag-types create mode 100644 vendor/github.com/urfave/cli/help.go create mode 100755 vendor/github.com/urfave/cli/runtests create mode 100644 vendor/github.com/urfave/cli/sort.go create mode 100644 vendor/vendor.json diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE new file mode 100644 index 00000000..835ba3e7 --- /dev/null +++ b/vendor/github.com/pkg/errors/LICENSE @@ -0,0 +1,23 @@ +Copyright (c) 2015, Dave Cheney +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md new file mode 100644 index 00000000..6483ba2a --- /dev/null +++ b/vendor/github.com/pkg/errors/README.md @@ -0,0 +1,52 @@ +# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge) + +Package errors provides simple error handling primitives. + +`go get github.com/pkg/errors` + +The traditional error handling idiom in Go is roughly akin to +```go +if err != nil { + return err +} +``` +which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. + +## Adding context to an error + +The errors.Wrap function returns a new error that adds context to the original error. For example +```go +_, err := ioutil.ReadAll(r) +if err != nil { + return errors.Wrap(err, "read failed") +} +``` +## Retrieving the cause of an error + +Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. +```go +type causer interface { + Cause() error +} +``` +`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: +```go +switch err := errors.Cause(err).(type) { +case *MyError: + // handle specifically +default: + // unknown error +} +``` + +[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). + +## Contributing + +We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. + +Before proposing a change, please discuss your change by raising an issue. + +## License + +BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml new file mode 100644 index 00000000..a932eade --- /dev/null +++ b/vendor/github.com/pkg/errors/appveyor.yml @@ -0,0 +1,32 @@ +version: build-{build}.{branch} + +clone_folder: C:\gopath\src\github.com\pkg\errors +shallow_clone: true # for startup speed + +environment: + GOPATH: C:\gopath + +platform: + - x64 + +# http://www.appveyor.com/docs/installed-software +install: + # some helpful output for debugging builds + - go version + - go env + # pre-installed MinGW at C:\MinGW is 32bit only + # but MSYS2 at C:\msys64 has mingw64 + - set PATH=C:\msys64\mingw64\bin;%PATH% + - gcc --version + - g++ --version + +build_script: + - go install -v ./... + +test_script: + - set PATH=C:\gopath\bin;%PATH% + - go test -v ./... + +#artifacts: +# - path: '%GOPATH%\bin\*.exe' +deploy: off diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go new file mode 100644 index 00000000..842ee804 --- /dev/null +++ b/vendor/github.com/pkg/errors/errors.go @@ -0,0 +1,269 @@ +// Package errors provides simple error handling primitives. +// +// The traditional error handling idiom in Go is roughly akin to +// +// if err != nil { +// return err +// } +// +// which applied recursively up the call stack results in error reports +// without context or debugging information. The errors package allows +// programmers to add context to the failure path in their code in a way +// that does not destroy the original value of the error. +// +// Adding context to an error +// +// The errors.Wrap function returns a new error that adds context to the +// original error by recording a stack trace at the point Wrap is called, +// and the supplied message. For example +// +// _, err := ioutil.ReadAll(r) +// if err != nil { +// return errors.Wrap(err, "read failed") +// } +// +// If additional control is required the errors.WithStack and errors.WithMessage +// functions destructure errors.Wrap into its component operations of annotating +// an error with a stack trace and an a message, respectively. +// +// Retrieving the cause of an error +// +// Using errors.Wrap constructs a stack of errors, adding context to the +// preceding error. Depending on the nature of the error it may be necessary +// to reverse the operation of errors.Wrap to retrieve the original error +// for inspection. Any error value which implements this interface +// +// type causer interface { +// Cause() error +// } +// +// can be inspected by errors.Cause. errors.Cause will recursively retrieve +// the topmost error which does not implement causer, which is assumed to be +// the original cause. For example: +// +// switch err := errors.Cause(err).(type) { +// case *MyError: +// // handle specifically +// default: +// // unknown error +// } +// +// causer interface is not exported by this package, but is considered a part +// of stable public API. +// +// Formatted printing of errors +// +// All error values returned from this package implement fmt.Formatter and can +// be formatted by the fmt package. The following verbs are supported +// +// %s print the error. If the error has a Cause it will be +// printed recursively +// %v see %s +// %+v extended format. Each Frame of the error's StackTrace will +// be printed in detail. +// +// Retrieving the stack trace of an error or wrapper +// +// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are +// invoked. This information can be retrieved with the following interface. +// +// type stackTracer interface { +// StackTrace() errors.StackTrace +// } +// +// Where errors.StackTrace is defined as +// +// type StackTrace []Frame +// +// The Frame type represents a call site in the stack trace. Frame supports +// the fmt.Formatter interface that can be used for printing information about +// the stack trace of this error. For example: +// +// if err, ok := err.(stackTracer); ok { +// for _, f := range err.StackTrace() { +// fmt.Printf("%+s:%d", f) +// } +// } +// +// stackTracer interface is not exported by this package, but is considered a part +// of stable public API. +// +// See the documentation for Frame.Format for more details. +package errors + +import ( + "fmt" + "io" +) + +// New returns an error with the supplied message. +// New also records the stack trace at the point it was called. +func New(message string) error { + return &fundamental{ + msg: message, + stack: callers(), + } +} + +// Errorf formats according to a format specifier and returns the string +// as a value that satisfies error. +// Errorf also records the stack trace at the point it was called. +func Errorf(format string, args ...interface{}) error { + return &fundamental{ + msg: fmt.Sprintf(format, args...), + stack: callers(), + } +} + +// fundamental is an error that has a message and a stack, but no caller. +type fundamental struct { + msg string + *stack +} + +func (f *fundamental) Error() string { return f.msg } + +func (f *fundamental) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + io.WriteString(s, f.msg) + f.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, f.msg) + case 'q': + fmt.Fprintf(s, "%q", f.msg) + } +} + +// WithStack annotates err with a stack trace at the point WithStack was called. +// If err is nil, WithStack returns nil. +func WithStack(err error) error { + if err == nil { + return nil + } + return &withStack{ + err, + callers(), + } +} + +type withStack struct { + error + *stack +} + +func (w *withStack) Cause() error { return w.error } + +func (w *withStack) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v", w.Cause()) + w.stack.Format(s, verb) + return + } + fallthrough + case 's': + io.WriteString(s, w.Error()) + case 'q': + fmt.Fprintf(s, "%q", w.Error()) + } +} + +// Wrap returns an error annotating err with a stack trace +// at the point Wrap is called, and the supplied message. +// If err is nil, Wrap returns nil. +func Wrap(err error, message string) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: message, + } + return &withStack{ + err, + callers(), + } +} + +// Wrapf returns an error annotating err with a stack trace +// at the point Wrapf is call, and the format specifier. +// If err is nil, Wrapf returns nil. +func Wrapf(err error, format string, args ...interface{}) error { + if err == nil { + return nil + } + err = &withMessage{ + cause: err, + msg: fmt.Sprintf(format, args...), + } + return &withStack{ + err, + callers(), + } +} + +// WithMessage annotates err with a new message. +// If err is nil, WithMessage returns nil. +func WithMessage(err error, message string) error { + if err == nil { + return nil + } + return &withMessage{ + cause: err, + msg: message, + } +} + +type withMessage struct { + cause error + msg string +} + +func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } +func (w *withMessage) Cause() error { return w.cause } + +func (w *withMessage) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + if s.Flag('+') { + fmt.Fprintf(s, "%+v\n", w.Cause()) + io.WriteString(s, w.msg) + return + } + fallthrough + case 's', 'q': + io.WriteString(s, w.Error()) + } +} + +// Cause returns the underlying cause of the error, if possible. +// An error value has a cause if it implements the following +// interface: +// +// type causer interface { +// Cause() error +// } +// +// If the error does not implement Cause, the original error will +// be returned. If the error is nil, nil will be returned without further +// investigation. +func Cause(err error) error { + type causer interface { + Cause() error + } + + for err != nil { + cause, ok := err.(causer) + if !ok { + break + } + err = cause.Cause() + } + return err +} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go new file mode 100644 index 00000000..2874a048 --- /dev/null +++ b/vendor/github.com/pkg/errors/stack.go @@ -0,0 +1,147 @@ +package errors + +import ( + "fmt" + "io" + "path" + "runtime" + "strings" +) + +// Frame represents a program counter inside a stack frame. +type Frame uintptr + +// pc returns the program counter for this frame; +// multiple frames may have the same PC value. +func (f Frame) pc() uintptr { return uintptr(f) - 1 } + +// file returns the full path to the file that contains the +// function for this Frame's pc. +func (f Frame) file() string { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return "unknown" + } + file, _ := fn.FileLine(f.pc()) + return file +} + +// line returns the line number of source code of the +// function for this Frame's pc. +func (f Frame) line() int { + fn := runtime.FuncForPC(f.pc()) + if fn == nil { + return 0 + } + _, line := fn.FileLine(f.pc()) + return line +} + +// Format formats the frame according to the fmt.Formatter interface. +// +// %s source file +// %d source line +// %n function name +// %v equivalent to %s:%d +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+s function name and path of source file relative to the compile time +// GOPATH separated by \n\t (\n\t) +// %+v equivalent to %+s:%d +func (f Frame) Format(s fmt.State, verb rune) { + switch verb { + case 's': + switch { + case s.Flag('+'): + pc := f.pc() + fn := runtime.FuncForPC(pc) + if fn == nil { + io.WriteString(s, "unknown") + } else { + file, _ := fn.FileLine(pc) + fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) + } + default: + io.WriteString(s, path.Base(f.file())) + } + case 'd': + fmt.Fprintf(s, "%d", f.line()) + case 'n': + name := runtime.FuncForPC(f.pc()).Name() + io.WriteString(s, funcname(name)) + case 'v': + f.Format(s, 's') + io.WriteString(s, ":") + f.Format(s, 'd') + } +} + +// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). +type StackTrace []Frame + +// Format formats the stack of Frames according to the fmt.Formatter interface. +// +// %s lists source files for each Frame in the stack +// %v lists the source file and line number for each Frame in the stack +// +// Format accepts flags that alter the printing of some verbs, as follows: +// +// %+v Prints filename, function, and line number for each Frame in the stack. +func (st StackTrace) Format(s fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case s.Flag('+'): + for _, f := range st { + fmt.Fprintf(s, "\n%+v", f) + } + case s.Flag('#'): + fmt.Fprintf(s, "%#v", []Frame(st)) + default: + fmt.Fprintf(s, "%v", []Frame(st)) + } + case 's': + fmt.Fprintf(s, "%s", []Frame(st)) + } +} + +// stack represents a stack of program counters. +type stack []uintptr + +func (s *stack) Format(st fmt.State, verb rune) { + switch verb { + case 'v': + switch { + case st.Flag('+'): + for _, pc := range *s { + f := Frame(pc) + fmt.Fprintf(st, "\n%+v", f) + } + } + } +} + +func (s *stack) StackTrace() StackTrace { + f := make([]Frame, len(*s)) + for i := 0; i < len(f); i++ { + f[i] = Frame((*s)[i]) + } + return f +} + +func callers() *stack { + const depth = 32 + var pcs [depth]uintptr + n := runtime.Callers(3, pcs[:]) + var st stack = pcs[0:n] + return &st +} + +// funcname removes the path prefix component of a function's name reported by func.Name(). +func funcname(name string) string { + i := strings.LastIndex(name, "/") + name = name[i+1:] + i = strings.Index(name, ".") + return name[i+1:] +} diff --git a/vendor/github.com/urfave/cli/CHANGELOG.md b/vendor/github.com/urfave/cli/CHANGELOG.md new file mode 100644 index 00000000..401eae5a --- /dev/null +++ b/vendor/github.com/urfave/cli/CHANGELOG.md @@ -0,0 +1,435 @@ +# Change Log + +**ATTN**: This project uses [semantic versioning](http://semver.org/). + +## [Unreleased] + +## 1.20.0 - 2017-08-10 + +### Fixed + +* `HandleExitCoder` is now correctly iterates over all errors in + a `MultiError`. The exit code is the exit code of the last error or `1` if + there are no `ExitCoder`s in the `MultiError`. +* Fixed YAML file loading on Windows (previously would fail validate the file path) +* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly + propogated +* `ErrWriter` is now passed downwards through command structure to avoid the + need to redefine it +* Pass `Command` context into `OnUsageError` rather than parent context so that + all fields are avaiable +* Errors occuring in `Before` funcs are no longer double printed +* Use `UsageText` in the help templates for commands and subcommands if + defined; otherwise build the usage as before (was previously ignoring this + field) +* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if + a program calls `Set` or `GlobalSet` directly after flag parsing (would + previously only return `true` if the flag was set during parsing) + +### Changed + +* No longer exit the program on command/subcommand error if the error raised is + not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was + determined to be a regression in functionality. See [the + PR](https://github.com/urfave/cli/pull/595) for discussion. + +### Added + +* `CommandsByName` type was added to make it easy to sort `Command`s by name, + alphabetically +* `altsrc` now handles loading of string and int arrays from TOML +* Support for definition of custom help templates for `App` via + `CustomAppHelpTemplate` +* Support for arbitrary key/value fields on `App` to be used with + `CustomAppHelpTemplate` via `ExtraInfo` +* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be + `cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag` + interface to be used. + + +## [1.19.1] - 2016-11-21 + +### Fixed + +- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as + the `Action` for a command would cause it to error rather than calling the + function. Should not have a affected declarative cases using `func(c + *cli.Context) err)`. +- Shell completion now handles the case where the user specifies + `--generate-bash-completion` immediately after a flag that takes an argument. + Previously it call the application with `--generate-bash-completion` as the + flag value. + +## [1.19.0] - 2016-11-19 +### Added +- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`) +- A `Description` field was added to `App` for a more detailed description of + the application (similar to the existing `Description` field on `Command`) +- Flag type code generation via `go generate` +- Write to stderr and exit 1 if action returns non-nil error +- Added support for TOML to the `altsrc` loader +- `SkipArgReorder` was added to allow users to skip the argument reordering. + This is useful if you want to consider all "flags" after an argument as + arguments rather than flags (the default behavior of the stdlib `flag` + library). This is backported functionality from the [removal of the flag + reordering](https://github.com/urfave/cli/pull/398) in the unreleased version + 2 +- For formatted errors (those implementing `ErrorFormatter`), the errors will + be formatted during output. Compatible with `pkg/errors`. + +### Changed +- Raise minimum tested/supported Go version to 1.2+ + +### Fixed +- Consider empty environment variables as set (previously environment variables + with the equivalent of `""` would be skipped rather than their value used). +- Return an error if the value in a given environment variable cannot be parsed + as the flag type. Previously these errors were silently swallowed. +- Print full error when an invalid flag is specified (which includes the invalid flag) +- `App.Writer` defaults to `stdout` when `nil` +- If no action is specified on a command or app, the help is now printed instead of `panic`ing +- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized) +- Correctly show help message if `-h` is provided to a subcommand +- `context.(Global)IsSet` now respects environment variables. Previously it + would return `false` if a flag was specified in the environment rather than + as an argument +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user +- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This + fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well + as `altsrc` where Go would complain that the types didn't match + +## [1.18.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported) + +## [1.18.0] - 2016-06-27 +### Added +- `./runtests` test runner with coverage tracking by default +- testing on OS X +- testing on Windows +- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code + +### Changed +- Use spaces for alignment in help/usage output instead of tabs, making the + output alignment consistent regardless of tab width + +### Fixed +- Printing of command aliases in help text +- Printing of visible flags for both struct and struct pointer flags +- Display the `help` subcommand when using `CommandCategories` +- No longer swallows `panic`s that occur within the `Action`s themselves when + detecting the signature of the `Action` field + +## [1.17.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user + +## [1.17.0] - 2016-05-09 +### Added +- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` +- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool` +- Support for hiding commands by setting `Hidden: true` -- this will hide the + commands in help output + +### Changed +- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer + quoted in help text output. +- All flag types now include `(default: {value})` strings following usage when a + default value can be (reasonably) detected. +- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent + with non-slice flag types +- Apps now exit with a code of 3 if an unknown subcommand is specified + (previously they printed "No help topic for...", but still exited 0. This + makes it easier to script around apps built using `cli` since they can trust + that a 0 exit code indicated a successful execution. +- cleanups based on [Go Report Card + feedback](https://goreportcard.com/report/github.com/urfave/cli) + +## [1.16.1] - 2016-08-28 +### Fixed +- Removed deprecation warnings to STDERR to avoid them leaking to the end-user + +## [1.16.0] - 2016-05-02 +### Added +- `Hidden` field on all flag struct types to omit from generated help text + +### Changed +- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from +generated help text via the `Hidden` field + +### Fixed +- handling of error values in `HandleAction` and `HandleExitCoder` + +## [1.15.0] - 2016-04-30 +### Added +- This file! +- Support for placeholders in flag usage strings +- `App.Metadata` map for arbitrary data/state management +- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after +parsing. +- Support for nested lookup of dot-delimited keys in structures loaded from +YAML. + +### Changed +- The `App.Action` and `Command.Action` now prefer a return signature of +`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil +`error` is returned, there may be two outcomes: + - If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called + automatically + - Else the error is bubbled up and returned from `App.Run` +- Specifying an `Action` with the legacy return signature of +`func(*cli.Context)` will produce a deprecation message to stderr +- Specifying an `Action` that is not a `func` type will produce a non-zero exit +from `App.Run` +- Specifying an `Action` func that has an invalid (input) signature will +produce a non-zero exit from `App.Run` + +### Deprecated +- +`cli.App.RunAndExitOnError`, which should now be done by returning an error +that fulfills `cli.ExitCoder` to `cli.App.Run`. +- the legacy signature for +`cli.App.Action` of `func(*cli.Context)`, which should now have a return +signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. + +### Fixed +- Added missing `*cli.Context.GlobalFloat64` method + +## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) +### Added +- Codebeat badge +- Support for categorization via `CategorizedHelp` and `Categories` on app. + +### Changed +- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`. + +### Fixed +- Ensure version is not shown in help text when `HideVersion` set. + +## [1.13.0] - 2016-03-06 (backfilled 2016-04-25) +### Added +- YAML file input support. +- `NArg` method on context. + +## [1.12.0] - 2016-02-17 (backfilled 2016-04-25) +### Added +- Custom usage error handling. +- Custom text support in `USAGE` section of help output. +- Improved help messages for empty strings. +- AppVeyor CI configuration. + +### Changed +- Removed `panic` from default help printer func. +- De-duping and optimizations. + +### Fixed +- Correctly handle `Before`/`After` at command level when no subcommands. +- Case of literal `-` argument causing flag reordering. +- Environment variable hints on Windows. +- Docs updates. + +## [1.11.1] - 2015-12-21 (backfilled 2016-04-25) +### Changed +- Use `path.Base` in `Name` and `HelpName` +- Export `GetName` on flag types. + +### Fixed +- Flag parsing when skipping is enabled. +- Test output cleanup. +- Move completion check to account for empty input case. + +## [1.11.0] - 2015-11-15 (backfilled 2016-04-25) +### Added +- Destination scan support for flags. +- Testing against `tip` in Travis CI config. + +### Changed +- Go version in Travis CI config. + +### Fixed +- Removed redundant tests. +- Use correct example naming in tests. + +## [1.10.2] - 2015-10-29 (backfilled 2016-04-25) +### Fixed +- Remove unused var in bash completion. + +## [1.10.1] - 2015-10-21 (backfilled 2016-04-25) +### Added +- Coverage and reference logos in README. + +### Fixed +- Use specified values in help and version parsing. +- Only display app version and help message once. + +## [1.10.0] - 2015-10-06 (backfilled 2016-04-25) +### Added +- More tests for existing functionality. +- `ArgsUsage` at app and command level for help text flexibility. + +### Fixed +- Honor `HideHelp` and `HideVersion` in `App.Run`. +- Remove juvenile word from README. + +## [1.9.0] - 2015-09-08 (backfilled 2016-04-25) +### Added +- `FullName` on command with accompanying help output update. +- Set default `$PROG` in bash completion. + +### Changed +- Docs formatting. + +### Fixed +- Removed self-referential imports in tests. + +## [1.8.0] - 2015-06-30 (backfilled 2016-04-25) +### Added +- Support for `Copyright` at app level. +- `Parent` func at context level to walk up context lineage. + +### Fixed +- Global flag processing at top level. + +## [1.7.1] - 2015-06-11 (backfilled 2016-04-25) +### Added +- Aggregate errors from `Before`/`After` funcs. +- Doc comments on flag structs. +- Include non-global flags when checking version and help. +- Travis CI config updates. + +### Fixed +- Ensure slice type flags have non-nil values. +- Collect global flags from the full command hierarchy. +- Docs prose. + +## [1.7.0] - 2015-05-03 (backfilled 2016-04-25) +### Changed +- `HelpPrinter` signature includes output writer. + +### Fixed +- Specify go 1.1+ in docs. +- Set `Writer` when running command as app. + +## [1.6.0] - 2015-03-23 (backfilled 2016-04-25) +### Added +- Multiple author support. +- `NumFlags` at context level. +- `Aliases` at command level. + +### Deprecated +- `ShortName` at command level. + +### Fixed +- Subcommand help output. +- Backward compatible support for deprecated `Author` and `Email` fields. +- Docs regarding `Names`/`Aliases`. + +## [1.5.0] - 2015-02-20 (backfilled 2016-04-25) +### Added +- `After` hook func support at app and command level. + +### Fixed +- Use parsed context when running command as subcommand. +- Docs prose. + +## [1.4.1] - 2015-01-09 (backfilled 2016-04-25) +### Added +- Support for hiding `-h / --help` flags, but not `help` subcommand. +- Stop flag parsing after `--`. + +### Fixed +- Help text for generic flags to specify single value. +- Use double quotes in output for defaults. +- Use `ParseInt` instead of `ParseUint` for int environment var values. +- Use `0` as base when parsing int environment var values. + +## [1.4.0] - 2014-12-12 (backfilled 2016-04-25) +### Added +- Support for environment variable lookup "cascade". +- Support for `Stdout` on app for output redirection. + +### Fixed +- Print command help instead of app help in `ShowCommandHelp`. + +## [1.3.1] - 2014-11-13 (backfilled 2016-04-25) +### Added +- Docs and example code updates. + +### Changed +- Default `-v / --version` flag made optional. + +## [1.3.0] - 2014-08-10 (backfilled 2016-04-25) +### Added +- `FlagNames` at context level. +- Exposed `VersionPrinter` var for more control over version output. +- Zsh completion hook. +- `AUTHOR` section in default app help template. +- Contribution guidelines. +- `DurationFlag` type. + +## [1.2.0] - 2014-08-02 +### Added +- Support for environment variable defaults on flags plus tests. + +## [1.1.0] - 2014-07-15 +### Added +- Bash completion. +- Optional hiding of built-in help command. +- Optional skipping of flag parsing at command level. +- `Author`, `Email`, and `Compiled` metadata on app. +- `Before` hook func support at app and command level. +- `CommandNotFound` func support at app level. +- Command reference available on context. +- `GenericFlag` type. +- `Float64Flag` type. +- `BoolTFlag` type. +- `IsSet` flag helper on context. +- More flag lookup funcs at context level. +- More tests & docs. + +### Changed +- Help template updates to account for presence/absence of flags. +- Separated subcommand help template. +- Exposed `HelpPrinter` var for more control over help output. + +## [1.0.0] - 2013-11-01 +### Added +- `help` flag in default app flag set and each command flag set. +- Custom handling of argument parsing errors. +- Command lookup by name at app level. +- `StringSliceFlag` type and supporting `StringSlice` type. +- `IntSliceFlag` type and supporting `IntSlice` type. +- Slice type flag lookups by name at context level. +- Export of app and command help functions. +- More tests & docs. + +## 0.1.0 - 2013-07-22 +### Added +- Initial implementation. + +[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD +[1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0 +[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0 +[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0 +[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0 +[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0 +[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0 +[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0 +[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1 +[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0 +[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2 +[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1 +[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0 +[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0 +[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0 +[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1 +[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0 +[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0 +[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0 +[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1 +[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0 +[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1 +[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0 +[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0 +[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0 +[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0 diff --git a/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md b/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..41ba294f --- /dev/null +++ b/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be +reviewed and investigated and will result in a response that is deemed necessary +and appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/vendor/github.com/urfave/cli/CONTRIBUTING.md b/vendor/github.com/urfave/cli/CONTRIBUTING.md new file mode 100644 index 00000000..329195ee --- /dev/null +++ b/vendor/github.com/urfave/cli/CONTRIBUTING.md @@ -0,0 +1,19 @@ +## Contributing + +**NOTE**: the primary maintainer(s) may be found in +[./MAINTAINERS.md](./MAINTAINERS.md). + +Feel free to put up a pull request to fix a bug or maybe add a feature. I will +give it a code review and make sure that it does not break backwards +compatibility. If I or any other collaborators agree that it is in line with +the vision of the project, we will work with you to get the code into +a mergeable state and merge it into the master branch. + +If you have contributed something significant to the project, we will most +likely add you as a collaborator. As a collaborator you are given the ability +to merge others pull requests. It is very important that new code does not +break existing code, so be careful about what code you do choose to merge. + +If you feel like you have contributed to the project but have not yet been added +as a collaborator, we probably forgot to add you :sweat_smile:. Please open an +issue! diff --git a/vendor/github.com/urfave/cli/LICENSE b/vendor/github.com/urfave/cli/LICENSE new file mode 100644 index 00000000..42a597e2 --- /dev/null +++ b/vendor/github.com/urfave/cli/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Jeremy Saenz & Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/urfave/cli/MAINTAINERS.md b/vendor/github.com/urfave/cli/MAINTAINERS.md new file mode 100644 index 00000000..f6bdd99c --- /dev/null +++ b/vendor/github.com/urfave/cli/MAINTAINERS.md @@ -0,0 +1 @@ +- @meatballhat diff --git a/vendor/github.com/urfave/cli/README.md b/vendor/github.com/urfave/cli/README.md new file mode 100644 index 00000000..f2baef4f --- /dev/null +++ b/vendor/github.com/urfave/cli/README.md @@ -0,0 +1,1526 @@ +cli +=== + +[![Build Status](https://travis-ci.org/urfave/cli.svg?branch=master)](https://travis-ci.org/urfave/cli) +[![Windows Build Status](https://ci.appveyor.com/api/projects/status/rtgk5xufi932pb2v?svg=true)](https://ci.appveyor.com/project/urfave/cli) +[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) +[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) +[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) +[![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / +[![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) + +This is the library formerly known as `github.com/codegangsta/cli` -- Github +will automatically redirect requests to this repository, but we recommend +updating your references for clarity. + +cli is a simple, fast, and fun package for building command line apps in Go. The +goal is to enable developers to write fast and distributable command line +applications in an expressive way. + + + +- [Overview](#overview) +- [Installation](#installation) + * [Supported platforms](#supported-platforms) + * [Using the `v2` branch](#using-the-v2-branch) + * [Pinning to the `v1` releases](#pinning-to-the-v1-releases) +- [Getting Started](#getting-started) +- [Examples](#examples) + * [Arguments](#arguments) + * [Flags](#flags) + + [Placeholder Values](#placeholder-values) + + [Alternate Names](#alternate-names) + + [Ordering](#ordering) + + [Values from the Environment](#values-from-the-environment) + + [Values from files](#values-from-files) + + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) + + [Precedence](#precedence) + * [Subcommands](#subcommands) + * [Subcommands categories](#subcommands-categories) + * [Exit code](#exit-code) + * [Bash Completion](#bash-completion) + + [Enabling](#enabling) + + [Distribution](#distribution) + + [Customization](#customization) + * [Generated Help Text](#generated-help-text) + + [Customization](#customization-1) + * [Version Flag](#version-flag) + + [Customization](#customization-2) + + [Full API Example](#full-api-example) + * [Combining short Bool options](#combining-short-bool-options) +- [Contribution Guidelines](#contribution-guidelines) + + + +## Overview + +Command line apps are usually so tiny that there is absolutely no reason why +your code should *not* be self-documenting. Things like generating help text and +parsing command flags/options should not hinder productivity when writing a +command line app. + +**This is where cli comes into play.** cli makes command line programming fun, +organized, and expressive! + +## Installation + +Make sure you have a working Go environment. Go version 1.2+ is supported. [See +the install instructions for Go](http://golang.org/doc/install.html). + +To install cli, simply run: +``` +$ go get github.com/urfave/cli +``` + +Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can +be easily used: +``` +export PATH=$PATH:$GOPATH/bin +``` + +### Supported platforms + +cli is tested against multiple versions of Go on Linux, and against the latest +released version of Go on OS X and Windows. For full details, see +[`./.travis.yml`](./.travis.yml) and [`./appveyor.yml`](./appveyor.yml). + +### Using the `v2` branch + +**Warning**: The `v2` branch is currently unreleased and considered unstable. + +There is currently a long-lived branch named `v2` that is intended to land as +the new `master` branch once development there has settled down. The current +`master` branch (mirrored as `v1`) is being manually merged into `v2` on +an irregular human-based schedule, but generally if one wants to "upgrade" to +`v2` *now* and accept the volatility (read: "awesomeness") that comes along with +that, please use whatever version pinning of your preference, such as via +`gopkg.in`: + +``` +$ go get gopkg.in/urfave/cli.v2 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v2" // imports as package "cli" +) +... +``` + +### Pinning to the `v1` releases + +Similarly to the section above describing use of the `v2` branch, if one wants +to avoid any unexpected compatibility pains once `v2` becomes `master`, then +pinning to `v1` is an acceptable option, e.g.: + +``` +$ go get gopkg.in/urfave/cli.v1 +``` + +``` go +... +import ( + "gopkg.in/urfave/cli.v1" // imports as package "cli" +) +... +``` + +This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing). + +## Getting Started + +One of the philosophies behind cli is that an API should be playful and full of +discovery. So a cli app can be as little as one line of code in `main()`. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +This app will run and show help text, but is not very useful. Let's give an +action to execute and some help documentation: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "boom" + app.Usage = "make an explosive entrance" + app.Action = func(c *cli.Context) error { + fmt.Println("boom! I say!") + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Running this already gives you a ton of functionality, plus support for things +like subcommands and flags, which are covered below. + +## Examples + +Being a programmer can be a lonely job. Thankfully by the power of automation +that is not the case! Let's create a greeter app to fend off our demons of +loneliness! + +Start by creating a directory named `greet`, and within it, add a file, +`greet.go` with the following code in it: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "greet" + app.Usage = "fight the loneliness!" + app.Action = func(c *cli.Context) error { + fmt.Println("Hello friend!") + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Install our command to the `$GOPATH/bin` directory: + +``` +$ go install +``` + +Finally run our new command: + +``` +$ greet +Hello friend! +``` + +cli also generates neat help text: + +``` +$ greet help +NAME: + greet - fight the loneliness! + +USAGE: + greet [global options] command [command options] [arguments...] + +VERSION: + 0.0.0 + +COMMANDS: + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS + --version Shows version information +``` + +### Arguments + +You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Action = func(c *cli.Context) error { + fmt.Printf("Hello %q", c.Args().Get(0)) + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +### Flags + +Setting and querying flags is simple. + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + }, + } + + app.Action = func(c *cli.Context) error { + name := "Nefertiti" + if c.NArg() > 0 { + name = c.Args().Get(0) + } + if c.String("lang") == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +You can also set a destination variable for a flag, to which the content will be +scanned. + + +``` go +package main + +import ( + "log" + "os" + "fmt" + + "github.com/urfave/cli" +) + +func main() { + var language string + + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang", + Value: "english", + Usage: "language for the greeting", + Destination: &language, + }, + } + + app.Action = func(c *cli.Context) error { + name := "someone" + if c.NArg() > 0 { + name = c.Args()[0] + } + if language == "spanish" { + fmt.Println("Hola", name) + } else { + fmt.Println("Hello", name) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +See full list of flags at http://godoc.org/github.com/urfave/cli + +#### Placeholder Values + +Sometimes it's useful to specify a flag's value within the usage string itself. +Such placeholders are indicated with back quotes. + +For example this: + + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +``` + +Note that only the first placeholder is used. Subsequent back-quoted words will +be left as-is. + +#### Alternate Names + +You can set alternate (or short) names for flags by providing a comma-delimited +list for the `Name`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +That flag can then be set with `--lang spanish` or `-l spanish`. Note that +giving two different forms of the same flag in the same command invocation is an +error. + +#### Ordering + +Flags for the application and commands are shown in the order they are defined. +However, it's possible to sort them from outside this library by using `FlagsByName` +or `CommandsByName` with `sort`. + +For example this: + + +``` go +package main + +import ( + "log" + "os" + "sort" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "Language for the greeting", + }, + cli.StringFlag{ + Name: "config, c", + Usage: "Load configuration from `FILE`", + }, + } + + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + return nil + }, + }, + } + + sort.Sort(cli.FlagsByName(app.Flags)) + sort.Sort(cli.CommandsByName(app.Commands)) + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will result in help output like: + +``` +--config FILE, -c FILE Load configuration from FILE +--lang value, -l value Language for the greeting (default: "english") +``` + +#### Values from the Environment + +You can also have the default value set from the environment via `EnvVar`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "APP_LANG", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +The `EnvVar` may also be given as a comma-delimited "cascade", where the first +environment variable that resolves is used as the default. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "lang, l", + Value: "english", + Usage: "language for the greeting", + EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Values from files + +You can also have the default value set from file via `FilePath`. e.g. + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Flags = []cli.Flag { + cli.StringFlag{ + Name: "password, p", + Usage: "password for the mysql database", + FilePath: "/etc/mysql/password", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Note that default values set from file (e.g. `FilePath`) take precedence over +default values set from the enviornment (e.g. `EnvVar`). + +#### Values from alternate input sources (YAML, TOML, and others) + +There is a separate package altsrc that adds support for getting flag values +from other file input sources. + +Currently supported input source formats: +* YAML +* TOML + +In order to get values for a flag from an alternate input source the following +code would be added to wrap an existing cli.Flag like below: + +``` go + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) +``` + +Initialization must also occur for these flags. Below is an example initializing +getting data from a yaml file below. + +``` go + command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) +``` + +The code above will use the "load" string as a flag name to get the file name of +a yaml file from the cli.Context. It will then use that file name to initialize +the yaml input source for any flags that are defined on that command. As a note +the "load" flag used would also have to be defined on the command flags in order +for this code snipped to work. + +Currently only YAML and JSON files are supported but developers can add support +for other input sources by implementing the altsrc.InputSourceContext for their +given sources. + +Here is a more complete sample of a command using YAML support: + + +``` go +package notmain + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" + "github.com/urfave/cli/altsrc" +) + +func main() { + app := cli.NewApp() + + flags := []cli.Flag{ + altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), + cli.StringFlag{Name: "load"}, + } + + app.Action = func(c *cli.Context) error { + fmt.Println("yaml ist rad") + return nil + } + + app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) + app.Flags = flags + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Precedence + +The precedence for flag value sources is as follows (highest to lowest): + +0. Command line flag value from user +0. Environment variable (if specified) +0. Configuration file (if specified) +0. Default defined on the flag + +### Subcommands + +Subcommands can be defined for a more git-like command line app. + + +```go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "add", + Aliases: []string{"a"}, + Usage: "add a task to the list", + Action: func(c *cli.Context) error { + fmt.Println("added task: ", c.Args().First()) + return nil + }, + }, + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + }, + { + Name: "template", + Aliases: []string{"t"}, + Usage: "options for task templates", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "add a new template", + Action: func(c *cli.Context) error { + fmt.Println("new task template: ", c.Args().First()) + return nil + }, + }, + { + Name: "remove", + Usage: "remove an existing template", + Action: func(c *cli.Context) error { + fmt.Println("removed task template: ", c.Args().First()) + return nil + }, + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +### Subcommands categories + +For additional organization in apps that have many subcommands, you can +associate a category for each command to group them together in the help +output. + +E.g. + +```go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + + app.Commands = []cli.Command{ + { + Name: "noop", + }, + { + Name: "add", + Category: "Template actions", + }, + { + Name: "remove", + Category: "Template actions", + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Will include: + +``` +COMMANDS: + noop + + Template actions: + add + remove +``` + +### Exit code + +Calling `App.Run` will not automatically call `os.Exit`, which means that by +default the exit code will "fall through" to being `0`. An explicit exit code +may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a +`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Flags = []cli.Flag{ + cli.BoolTFlag{ + Name: "ginger-crouton", + Usage: "is it in the soup?", + }, + } + app.Action = func(ctx *cli.Context) error { + if !ctx.Bool("ginger-crouton") { + return cli.NewExitError("it is not in the soup", 86) + } + return nil + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +### Bash Completion + +You can enable completion commands by setting the `EnableBashCompletion` +flag on the `App` object. By default, this setting will only auto-complete to +show an app's subcommands, but you can write your own completion methods for +the App or its subcommands. + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "complete", + Aliases: []string{"c"}, + Usage: "complete a task on the list", + Action: func(c *cli.Context) error { + fmt.Println("completed task: ", c.Args().First()) + return nil + }, + BashComplete: func(c *cli.Context) { + // This will complete if no args are passed + if c.NArg() > 0 { + return + } + for _, t := range tasks { + fmt.Println(t) + } + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Enabling + +Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while +setting the `PROG` variable to the name of your program: + +`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` + +#### Distribution + +Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename +it to the name of the program you wish to add autocomplete support for (or +automatically install it there if you are distributing a package). Don't forget +to source the file to make it active in the current shell. + +``` +sudo cp src/bash_autocomplete /etc/bash_completion.d/ +source /etc/bash_completion.d/ +``` + +Alternatively, you can just document that users should source the generic +`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set +to the name of their program (as above). + +#### Customization + +The default bash completion flag (`--generate-bash-completion`) is defined as +`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.BashCompletionFlag = cli.BoolFlag{ + Name: "compgen", + Hidden: true, + } + + app := cli.NewApp() + app.EnableBashCompletion = true + app.Commands = []cli.Command{ + { + Name: "wat", + }, + } + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +### Generated Help Text + +The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked +by the cli internals in order to print generated help text for the app, command, +or subcommand, and break execution. + +#### Customization + +All of the help text generation may be customized, and at multiple levels. The +templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and +`SubcommandHelpTemplate` which may be reassigned or augmented, and full override +is possible by assigning a compatible func to the `cli.HelpPrinter` variable, +e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "io" + "os" + + "github.com/urfave/cli" +) + +func main() { + // EXAMPLE: Append to an existing template + cli.AppHelpTemplate = fmt.Sprintf(`%s + +WEBSITE: http://awesometown.example.com + +SUPPORT: support@awesometown.example.com + +`, cli.AppHelpTemplate) + + // EXAMPLE: Override a template + cli.AppHelpTemplate = `NAME: + {{.Name}} - {{.Usage}} +USAGE: + {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} + {{if len .Authors}} +AUTHOR: + {{range .Authors}}{{ . }}{{end}} + {{end}}{{if .Commands}} +COMMANDS: +{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} +GLOBAL OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}}{{if .Copyright }} +COPYRIGHT: + {{.Copyright}} + {{end}}{{if .Version}} +VERSION: + {{.Version}} + {{end}} +` + + // EXAMPLE: Replace the `HelpPrinter` func + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Println("Ha HA. I pwnd the help!!1") + } + + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +The default flag may be customized to something other than `-h/--help` by +setting `cli.HelpFlag`, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.HelpFlag = cli.BoolFlag{ + Name: "halp, haaaaalp", + Usage: "HALP", + EnvVar: "SHOW_HALP,HALPPLZ", + } + + err := cli.NewApp().Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +### Version Flag + +The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which +is checked by the cli internals in order to print the `App.Version` via +`cli.VersionPrinter` and break execution. + +#### Customization + +The default flag may be customized to something other than `-v/--version` by +setting `cli.VersionFlag`, e.g.: + + +``` go +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + cli.VersionFlag = cli.BoolFlag{ + Name: "print-version, V", + Usage: "print only the version", + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: + + +``` go +package main + +import ( + "fmt" + "log" + "os" + + "github.com/urfave/cli" +) + +var ( + Revision = "fafafaf" +) + +func main() { + cli.VersionPrinter = func(c *cli.Context) { + fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) + } + + app := cli.NewApp() + app.Name = "partay" + app.Version = "19.99.0" + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} +``` + +#### Full API Example + +**Notice**: This is a contrived (functioning) example meant strictly for API +demonstration purposes. Use of one's imagination is encouraged. + + +``` go +package main + +import ( + "errors" + "flag" + "fmt" + "io" + "io/ioutil" + "os" + "time" + + "github.com/urfave/cli" +) + +func init() { + cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" + cli.CommandHelpTemplate += "\nYMMV\n" + cli.SubcommandHelpTemplate += "\nor something\n" + + cli.HelpFlag = cli.BoolFlag{Name: "halp"} + cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} + cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} + + cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { + fmt.Fprintf(w, "best of luck to you\n") + } + cli.VersionPrinter = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) + } + cli.OsExiter = func(c int) { + fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) + } + cli.ErrWriter = ioutil.Discard + cli.FlagStringer = func(fl cli.Flag) string { + return fmt.Sprintf("\t\t%s", fl.GetName()) + } +} + +type hexWriter struct{} + +func (w *hexWriter) Write(p []byte) (int, error) { + for _, b := range p { + fmt.Printf("%x", b) + } + fmt.Printf("\n") + + return len(p), nil +} + +type genericType struct{ + s string +} + +func (g *genericType) Set(value string) error { + g.s = value + return nil +} + +func (g *genericType) String() string { + return g.s +} + +func main() { + app := cli.NewApp() + app.Name = "kənˈtrīv" + app.Version = "19.99.0" + app.Compiled = time.Now() + app.Authors = []cli.Author{ + cli.Author{ + Name: "Example Human", + Email: "human@example.com", + }, + } + app.Copyright = "(c) 1999 Serious Enterprise" + app.HelpName = "contrive" + app.Usage = "demonstrate available API" + app.UsageText = "contrive - demonstrating the available API" + app.ArgsUsage = "[args and such]" + app.Commands = []cli.Command{ + cli.Command{ + Name: "doo", + Aliases: []string{"do"}, + Category: "motion", + Usage: "do the doo", + UsageText: "doo - does the dooing", + Description: "no really, there is a lot of dooing to be done", + ArgsUsage: "[arrgh]", + Flags: []cli.Flag{ + cli.BoolFlag{Name: "forever, forevvarr"}, + }, + Subcommands: cli.Commands{ + cli.Command{ + Name: "wop", + Action: wopAction, + }, + }, + SkipFlagParsing: false, + HideHelp: false, + Hidden: false, + HelpName: "doo!", + BashComplete: func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "--better\n") + }, + Before: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "brace for impact\n") + return nil + }, + After: func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") + return nil + }, + Action: func(c *cli.Context) error { + c.Command.FullName() + c.Command.HasName("wop") + c.Command.Names() + c.Command.VisibleFlags() + fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") + if c.Bool("forever") { + c.Command.Run(c) + } + return nil + }, + OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { + fmt.Fprintf(c.App.Writer, "for shame\n") + return err + }, + }, + } + app.Flags = []cli.Flag{ + cli.BoolFlag{Name: "fancy"}, + cli.BoolTFlag{Name: "fancier"}, + cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, + cli.Float64Flag{Name: "howmuch"}, + cli.GenericFlag{Name: "wat", Value: &genericType{}}, + cli.Int64Flag{Name: "longdistance"}, + cli.Int64SliceFlag{Name: "intervals"}, + cli.IntFlag{Name: "distance"}, + cli.IntSliceFlag{Name: "times"}, + cli.StringFlag{Name: "dance-move, d"}, + cli.StringSliceFlag{Name: "names, N"}, + cli.UintFlag{Name: "age"}, + cli.Uint64Flag{Name: "bigage"}, + } + app.EnableBashCompletion = true + app.HideHelp = false + app.HideVersion = false + app.BashComplete = func(c *cli.Context) { + fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") + } + app.Before = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") + return nil + } + app.After = func(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, "Phew!\n") + return nil + } + app.CommandNotFound = func(c *cli.Context, command string) { + fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) + } + app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { + if isSubcommand { + return err + } + + fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) + return nil + } + app.Action = func(c *cli.Context) error { + cli.DefaultAppComplete(c) + cli.HandleExitCoder(errors.New("not an exit coder, though")) + cli.ShowAppHelp(c) + cli.ShowCommandCompletions(c, "nope") + cli.ShowCommandHelp(c, "also-nope") + cli.ShowCompletions(c) + cli.ShowSubcommandHelp(c) + cli.ShowVersion(c) + + categories := c.App.Categories() + categories.AddCommand("sounds", cli.Command{ + Name: "bloop", + }) + + for _, category := range c.App.Categories() { + fmt.Fprintf(c.App.Writer, "%s\n", category.Name) + fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) + fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) + } + + fmt.Printf("%#v\n", c.App.Command("doo")) + if c.Bool("infinite") { + c.App.Run([]string{"app", "doo", "wop"}) + } + + if c.Bool("forevar") { + c.App.RunAsSubcommand(c) + } + c.App.Setup() + fmt.Printf("%#v\n", c.App.VisibleCategories()) + fmt.Printf("%#v\n", c.App.VisibleCommands()) + fmt.Printf("%#v\n", c.App.VisibleFlags()) + + fmt.Printf("%#v\n", c.Args().First()) + if len(c.Args()) > 0 { + fmt.Printf("%#v\n", c.Args()[1]) + } + fmt.Printf("%#v\n", c.Args().Present()) + fmt.Printf("%#v\n", c.Args().Tail()) + + set := flag.NewFlagSet("contrive", 0) + nc := cli.NewContext(c.App, set, c) + + fmt.Printf("%#v\n", nc.Args()) + fmt.Printf("%#v\n", nc.Bool("nope")) + fmt.Printf("%#v\n", nc.BoolT("nerp")) + fmt.Printf("%#v\n", nc.Duration("howlong")) + fmt.Printf("%#v\n", nc.Float64("hay")) + fmt.Printf("%#v\n", nc.Generic("bloop")) + fmt.Printf("%#v\n", nc.Int64("bonk")) + fmt.Printf("%#v\n", nc.Int64Slice("burnks")) + fmt.Printf("%#v\n", nc.Int("bips")) + fmt.Printf("%#v\n", nc.IntSlice("blups")) + fmt.Printf("%#v\n", nc.String("snurt")) + fmt.Printf("%#v\n", nc.StringSlice("snurkles")) + fmt.Printf("%#v\n", nc.Uint("flub")) + fmt.Printf("%#v\n", nc.Uint64("florb")) + fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) + fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) + fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) + fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) + fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) + fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) + fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) + fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) + fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) + + fmt.Printf("%#v\n", nc.FlagNames()) + fmt.Printf("%#v\n", nc.GlobalFlagNames()) + fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) + fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) + fmt.Printf("%#v\n", nc.NArg()) + fmt.Printf("%#v\n", nc.NumFlags()) + fmt.Printf("%#v\n", nc.Parent()) + + nc.Set("wat", "also-nope") + + ec := cli.NewExitError("ohwell", 86) + fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) + fmt.Printf("made it!\n") + return nil + } + + if os.Getenv("HEXY") != "" { + app.Writer = &hexWriter{} + app.ErrWriter = &hexWriter{} + } + + app.Metadata = map[string]interface{}{ + "layers": "many", + "explicable": false, + "whatever-values": 19.99, + } + + + // ignore error so we don't exit non-zero and break gfmrun README example tests + _ = app.Run(os.Args) +} + +func wopAction(c *cli.Context) error { + fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") + return nil +} +``` + +### Combining short Bool options + +Traditional use of boolean options using their shortnames look like this: +``` +# cmd foobar -s -o +``` + +Suppose you want users to be able to combine your bool options with their shortname. This +can be done using the **UseShortOptionHandling** bool in your commands. Suppose your program +has a two bool flags such as *serve* and *option* with the short options of *-o* and +*-s* respectively. With **UseShortOptionHandling** set to *true*, a user can use a syntax +like: +``` +# cmd foobar -so +``` + +If you enable the **UseShortOptionHandling*, then you must not use any flags that have a single +leading *-* or this will result in failures. For example, **-option** can no longer be used. Flags +with two leading dashes (such as **--options**) are still valid. + +## Contribution Guidelines + +See [./CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/vendor/github.com/urfave/cli/app.go b/vendor/github.com/urfave/cli/app.go new file mode 100644 index 00000000..9add067b --- /dev/null +++ b/vendor/github.com/urfave/cli/app.go @@ -0,0 +1,508 @@ +package cli + +import ( + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "time" +) + +var ( + changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" + appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) + runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) + + contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." + + errInvalidActionType = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ + fmt.Sprintf("See %s", appActionDeprecationURL), 2) +) + +// App is the main structure of a cli application. It is recommended that +// an app be created with the cli.NewApp() function +type App struct { + // The name of the program. Defaults to path.Base(os.Args[0]) + Name string + // Full name of command for help, defaults to Name + HelpName string + // Description of the program. + Usage string + // Text to override the USAGE section of help + UsageText string + // Description of the program argument format. + ArgsUsage string + // Version of the program + Version string + // Description of the program + Description string + // List of commands to execute + Commands []Command + // List of flags to parse + Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command + HideHelp bool + // Boolean to hide built-in version flag and the VERSION section of help + HideVersion bool + // Populate on app startup, only gettable through method Categories() + categories CommandCategories + // An action to execute when the bash-completion flag is set + BashComplete BashCompleteFunc + // An action to execute before any subcommands are run, but after the context is ready + // If a non-nil error is returned, no subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + + // The action to execute when no subcommands are specified + // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` + // *Note*: support for the deprecated `Action` signature will be removed in a future version + Action interface{} + + // Execute this function if the proper command cannot be found + CommandNotFound CommandNotFoundFunc + // Execute this function if an usage error occurs + OnUsageError OnUsageErrorFunc + // Compilation date + Compiled time.Time + // List of all authors who contributed + Authors []Author + // Copyright of the binary if any + Copyright string + // Name of Author (Note: Use App.Authors, this is deprecated) + Author string + // Email of Author (Note: Use App.Authors, this is deprecated) + Email string + // Writer writer to write output to + Writer io.Writer + // ErrWriter writes error output + ErrWriter io.Writer + // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to + // function as a default, so this is optional. + ExitErrHandler ExitErrHandlerFunc + // Other custom info + Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string + + didSetup bool +} + +// Tries to find out when this binary was compiled. +// Returns the current time if it fails to find it. +func compileTime() time.Time { + info, err := os.Stat(os.Args[0]) + if err != nil { + return time.Now() + } + return info.ModTime() +} + +// NewApp creates a new cli Application with some reasonable defaults for Name, +// Usage, Version and Action. +func NewApp() *App { + return &App{ + Name: filepath.Base(os.Args[0]), + HelpName: filepath.Base(os.Args[0]), + Usage: "A new cli application", + UsageText: "", + Version: "0.0.0", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + Writer: os.Stdout, + } +} + +// Setup runs initialization code to ensure all data structures are ready for +// `Run` or inspection prior to `Run`. It is internally called by `Run`, but +// will return early if setup has already happened. +func (a *App) Setup() { + if a.didSetup { + return + } + + a.didSetup = true + + if a.Author != "" || a.Email != "" { + a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) + } + + newCmds := []Command{} + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCmds = append(newCmds, c) + } + a.Commands = newCmds + + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + if (HelpFlag != BoolFlag{}) { + a.appendFlag(HelpFlag) + } + } + + if !a.HideVersion { + a.appendFlag(VersionFlag) + } + + a.categories = CommandCategories{} + for _, command := range a.Commands { + a.categories = a.categories.AddCommand(command.Category, command) + } + sort.Sort(a.categories) + + if a.Metadata == nil { + a.Metadata = make(map[string]interface{}) + } + + if a.Writer == nil { + a.Writer = os.Stdout + } +} + +// Run is the entry point to the cli app. Parses the arguments slice and routes +// to the proper flag/args combination +func (a *App) Run(arguments []string) (err error) { + a.Setup() + + // handle the completion flag separately from the flagset since + // completion could be attempted after a flag, but before its value was put + // on the command line. this causes the flagset to interpret the completion + // flag name as the value of the flag before it which is undesirable + // note that we can only do this because the shell autocomplete function + // always appends the completion flag at the end of the command + shellComplete, arguments := checkShellCompleteFlag(a, arguments) + + // parse flags + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + + set.SetOutput(ioutil.Discard) + err = set.Parse(arguments[1:]) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, nil) + if nerr != nil { + fmt.Fprintln(a.Writer, nerr) + ShowAppHelp(context) + return nerr + } + context.shellComplete = shellComplete + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err := a.OnUsageError(context, err, false) + a.handleExitCoder(context, err) + return err + } + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + ShowAppHelp(context) + return err + } + + if !a.HideHelp && checkHelp(context) { + ShowAppHelp(context) + return nil + } + + if !a.HideVersion && checkVersion(context) { + ShowVersion(context) + return nil + } + + if a.After != nil { + defer func() { + if afterErr := a.After(context); afterErr != nil { + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + beforeErr := a.Before(context) + if beforeErr != nil { + fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) + ShowAppHelp(context) + a.handleExitCoder(context, beforeErr) + err = beforeErr + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + if a.Action == nil { + a.Action = helpCommand.Action + } + + // Run default Action + err = HandleAction(a.Action, context) + + a.handleExitCoder(context, err) + return err +} + +// RunAndExitOnError calls .Run() and exits non-zero if an error was returned +// +// Deprecated: instead you should return an error that fulfills cli.ExitCoder +// to cli.App.Run. This will cause the application to exit with the given eror +// code in the cli.ExitCoder +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + fmt.Fprintln(a.errWriter(), err) + OsExiter(1) + } +} + +// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to +// generate command-specific flags +func (a *App) RunAsSubcommand(ctx *Context) (err error) { + // append help to commands + if len(a.Commands) > 0 { + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + a.Commands = append(a.Commands, helpCommand) + if (HelpFlag != BoolFlag{}) { + a.appendFlag(HelpFlag) + } + } + } + + newCmds := []Command{} + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCmds = append(newCmds, c) + } + a.Commands = newCmds + + // parse flags + set, err := flagSet(a.Name, a.Flags) + if err != nil { + return err + } + + set.SetOutput(ioutil.Discard) + err = set.Parse(ctx.Args().Tail()) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, ctx) + + if nerr != nil { + fmt.Fprintln(a.Writer, nerr) + fmt.Fprintln(a.Writer) + if len(a.Commands) > 0 { + ShowSubcommandHelp(context) + } else { + ShowCommandHelp(ctx, context.Args().First()) + } + return nerr + } + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err = a.OnUsageError(context, err, true) + a.handleExitCoder(context, err) + return err + } + fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + ShowSubcommandHelp(context) + return err + } + + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } + } + + if a.After != nil { + defer func() { + afterErr := a.After(context) + if afterErr != nil { + a.handleExitCoder(context, err) + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + beforeErr := a.Before(context) + if beforeErr != nil { + a.handleExitCoder(context, beforeErr) + err = beforeErr + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + err = HandleAction(a.Action, context) + + a.handleExitCoder(context, err) + return err +} + +// Command returns the named command on App. Returns nil if the command does not exist +func (a *App) Command(name string) *Command { + for _, c := range a.Commands { + if c.HasName(name) { + return &c + } + } + + return nil +} + +// Categories returns a slice containing all the categories with the commands they contain +func (a *App) Categories() CommandCategories { + return a.categories +} + +// VisibleCategories returns a slice of categories and commands that are +// Hidden=false +func (a *App) VisibleCategories() []*CommandCategory { + ret := []*CommandCategory{} + for _, category := range a.categories { + if visible := func() *CommandCategory { + for _, command := range category.Commands { + if !command.Hidden { + return category + } + } + return nil + }(); visible != nil { + ret = append(ret, visible) + } + } + return ret +} + +// VisibleCommands returns a slice of the Commands with Hidden=false +func (a *App) VisibleCommands() []Command { + ret := []Command{} + for _, command := range a.Commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} + +// VisibleFlags returns a slice of the Flags with Hidden=false +func (a *App) VisibleFlags() []Flag { + return visibleFlags(a.Flags) +} + +func (a *App) hasFlag(flag Flag) bool { + for _, f := range a.Flags { + if flag == f { + return true + } + } + + return false +} + +func (a *App) errWriter() io.Writer { + // When the app ErrWriter is nil use the package level one. + if a.ErrWriter == nil { + return ErrWriter + } + + return a.ErrWriter +} + +func (a *App) appendFlag(flag Flag) { + if !a.hasFlag(flag) { + a.Flags = append(a.Flags, flag) + } +} + +func (a *App) handleExitCoder(context *Context, err error) { + if a.ExitErrHandler != nil { + a.ExitErrHandler(context, err) + } else { + HandleExitCoder(err) + } +} + +// Author represents someone who has contributed to a cli project. +type Author struct { + Name string // The Authors name + Email string // The Authors email +} + +// String makes Author comply to the Stringer interface, to allow an easy print in the templating process +func (a Author) String() string { + e := "" + if a.Email != "" { + e = " <" + a.Email + ">" + } + + return fmt.Sprintf("%v%v", a.Name, e) +} + +// HandleAction attempts to figure out which Action signature was used. If +// it's an ActionFunc or a func with the legacy signature for Action, the func +// is run! +func HandleAction(action interface{}, context *Context) (err error) { + if a, ok := action.(ActionFunc); ok { + return a(context) + } else if a, ok := action.(func(*Context) error); ok { + return a(context) + } else if a, ok := action.(func(*Context)); ok { // deprecated function signature + a(context) + return nil + } + + return errInvalidActionType +} diff --git a/vendor/github.com/urfave/cli/appveyor.yml b/vendor/github.com/urfave/cli/appveyor.yml new file mode 100644 index 00000000..1e1489c3 --- /dev/null +++ b/vendor/github.com/urfave/cli/appveyor.yml @@ -0,0 +1,26 @@ +version: "{build}" + +os: Windows Server 2016 + +image: Visual Studio 2017 + +clone_folder: c:\gopath\src\github.com\urfave\cli + +environment: + GOPATH: C:\gopath + GOVERSION: 1.8.x + PYTHON: C:\Python36-x64 + PYTHON_VERSION: 3.6.x + PYTHON_ARCH: 64 + +install: +- set PATH=%GOPATH%\bin;C:\go\bin;%PATH% +- go version +- go env +- go get github.com/urfave/gfmrun/... +- go get -v -t ./... + +build_script: +- python runtests vet +- python runtests test +- python runtests gfmrun diff --git a/vendor/github.com/urfave/cli/category.go b/vendor/github.com/urfave/cli/category.go new file mode 100644 index 00000000..bf3c73c5 --- /dev/null +++ b/vendor/github.com/urfave/cli/category.go @@ -0,0 +1,44 @@ +package cli + +// CommandCategories is a slice of *CommandCategory. +type CommandCategories []*CommandCategory + +// CommandCategory is a category containing commands. +type CommandCategory struct { + Name string + Commands Commands +} + +func (c CommandCategories) Less(i, j int) bool { + return lexicographicLess(c[i].Name, c[j].Name) +} + +func (c CommandCategories) Len() int { + return len(c) +} + +func (c CommandCategories) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +// AddCommand adds a command to a category. +func (c CommandCategories) AddCommand(category string, command Command) CommandCategories { + for _, commandCategory := range c { + if commandCategory.Name == category { + commandCategory.Commands = append(commandCategory.Commands, command) + return c + } + } + return append(c, &CommandCategory{Name: category, Commands: []Command{command}}) +} + +// VisibleCommands returns a slice of the Commands with Hidden=false +func (c *CommandCategory) VisibleCommands() []Command { + ret := []Command{} + for _, command := range c.Commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} diff --git a/vendor/github.com/urfave/cli/cli.go b/vendor/github.com/urfave/cli/cli.go new file mode 100644 index 00000000..90c07eb8 --- /dev/null +++ b/vendor/github.com/urfave/cli/cli.go @@ -0,0 +1,22 @@ +// Package cli provides a minimal framework for creating and organizing command line +// Go applications. cli is designed to be easy to understand and write, the most simple +// cli application can be written as follows: +// func main() { +// cli.NewApp().Run(os.Args) +// } +// +// Of course this application does not do much, so let's make this an actual application: +// func main() { +// app := cli.NewApp() +// app.Name = "greet" +// app.Usage = "say a greeting" +// app.Action = func(c *cli.Context) error { +// println("Greetings") +// return nil +// } +// +// app.Run(os.Args) +// } +package cli + +//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go diff --git a/vendor/github.com/urfave/cli/command.go b/vendor/github.com/urfave/cli/command.go new file mode 100644 index 00000000..2acb9768 --- /dev/null +++ b/vendor/github.com/urfave/cli/command.go @@ -0,0 +1,336 @@ +package cli + +import ( + "flag" + "fmt" + "io/ioutil" + "sort" + "strings" +) + +// Command is a subcommand for a cli.App. +type Command struct { + // The name of the command + Name string + // short name of the command. Typically one character (deprecated, use `Aliases`) + ShortName string + // A list of aliases for the command + Aliases []string + // A short description of the usage of this command + Usage string + // Custom text to show on USAGE section of help + UsageText string + // A longer explanation of how the command works + Description string + // A short description of the arguments of this command + ArgsUsage string + // The category the command is part of + Category string + // The function to call when checking for bash command completions + BashComplete BashCompleteFunc + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + // The function to call when this command is invoked + Action interface{} + // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind + // of deprecation period has passed, maybe? + + // Execute this function if a usage error occurs. + OnUsageError OnUsageErrorFunc + // List of child commands + Subcommands Commands + // List of flags to parse + Flags []Flag + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Skip argument reordering which attempts to move flags before arguments, + // but only works if all flags appear after all arguments. This behavior was + // removed n version 2 since it only works under specific conditions so we + // backport here by exposing it as an option for compatibility. + SkipArgReorder bool + // Boolean to hide built-in help command + HideHelp bool + // Boolean to hide this command from help or completion + Hidden bool + // Boolean to enable short-option handling so user can combine several + // single-character bool arguements into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool + + // Full name of command for help, defaults to full command name, including parent commands. + HelpName string + commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string +} + +type CommandsByName []Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return lexicographicLess(c[i].Name, c[j].Name) +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +// FullName returns the full name of the command. +// For subcommands this ensures that parent commands are part of the command path +func (c Command) FullName() string { + if c.commandNamePath == nil { + return c.Name + } + return strings.Join(c.commandNamePath, " ") +} + +// Commands is a slice of Command +type Commands []Command + +// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags +func (c Command) Run(ctx *Context) (err error) { + if len(c.Subcommands) > 0 { + return c.startApp(ctx) + } + + if !c.HideHelp && (HelpFlag != BoolFlag{}) { + // append help to flags + c.Flags = append( + c.Flags, + HelpFlag, + ) + } + + set, err := c.parseFlags(ctx.Args().Tail()) + + context := NewContext(ctx.App, set, ctx) + context.Command = c + if checkCommandCompletions(context, c.Name) { + return nil + } + + if err != nil { + if c.OnUsageError != nil { + err := c.OnUsageError(context, err, false) + context.App.handleExitCoder(context, err) + return err + } + fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + fmt.Fprintln(context.App.Writer) + ShowCommandHelp(context, c.Name) + return err + } + + if checkCommandHelp(context, c.Name) { + return nil + } + + if c.After != nil { + defer func() { + afterErr := c.After(context) + if afterErr != nil { + context.App.handleExitCoder(context, err) + if err != nil { + err = NewMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if c.Before != nil { + err = c.Before(context) + if err != nil { + ShowCommandHelp(context, c.Name) + context.App.handleExitCoder(context, err) + return err + } + } + + if c.Action == nil { + c.Action = helpSubcommand.Action + } + + err = HandleAction(c.Action, context) + + if err != nil { + context.App.handleExitCoder(context, err) + } + return err +} + +func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { + set, err := flagSet(c.Name, c.Flags) + if err != nil { + return nil, err + } + set.SetOutput(ioutil.Discard) + + if c.SkipFlagParsing { + return set, set.Parse(append([]string{"--"}, args...)) + } + + if c.UseShortOptionHandling { + args = translateShortOptions(args) + } + + if !c.SkipArgReorder { + args = reorderArgs(args) + } + + err = set.Parse(args) + if err != nil { + return nil, err + } + + err = normalizeFlags(c.Flags, set) + if err != nil { + return nil, err + } + + return set, nil +} + +// reorderArgs moves all flags before arguments as this is what flag expects +func reorderArgs(args []string) []string { + var nonflags, flags []string + + readFlagValue := false + for i, arg := range args { + if arg == "--" { + nonflags = append(nonflags, args[i:]...) + break + } + + if readFlagValue && !strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { + readFlagValue = false + flags = append(flags, arg) + continue + } + readFlagValue = false + + if arg != "-" && strings.HasPrefix(arg, "-") { + flags = append(flags, arg) + + readFlagValue = !strings.Contains(arg, "=") + } else { + nonflags = append(nonflags, arg) + } + } + + return append(flags, nonflags...) +} + +func translateShortOptions(flagArgs Args) []string { + // separate combined flags + var flagArgsSeparated []string + for _, flagArg := range flagArgs { + if strings.HasPrefix(flagArg, "-") && strings.HasPrefix(flagArg, "--") == false && len(flagArg) > 2 { + for _, flagChar := range flagArg[1:] { + flagArgsSeparated = append(flagArgsSeparated, "-"+string(flagChar)) + } + } else { + flagArgsSeparated = append(flagArgsSeparated, flagArg) + } + } + return flagArgsSeparated +} + +// Names returns the names including short names and aliases. +func (c Command) Names() []string { + names := []string{c.Name} + + if c.ShortName != "" { + names = append(names, c.ShortName) + } + + return append(names, c.Aliases...) +} + +// HasName returns true if Command.Name or Command.ShortName matches given name +func (c Command) HasName(name string) bool { + for _, n := range c.Names() { + if n == name { + return true + } + } + return false +} + +func (c Command) startApp(ctx *Context) error { + app := NewApp() + app.Metadata = ctx.App.Metadata + // set the name and usage + app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) + if c.HelpName == "" { + app.HelpName = c.HelpName + } else { + app.HelpName = app.Name + } + + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + app.HideHelp = c.HideHelp + + app.Version = ctx.App.Version + app.HideVersion = ctx.App.HideVersion + app.Compiled = ctx.App.Compiled + app.Author = ctx.App.Author + app.Email = ctx.App.Email + app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter + + app.categories = CommandCategories{} + for _, command := range c.Subcommands { + app.categories = app.categories.AddCommand(command.Category, command) + } + + sort.Sort(app.categories) + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the actions + app.Before = c.Before + app.After = c.After + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + app.OnUsageError = c.OnUsageError + + for index, cc := range app.Commands { + app.Commands[index].commandNamePath = []string{c.Name, cc.Name} + } + + return app.RunAsSubcommand(ctx) +} + +// VisibleFlags returns a slice of the Flags with Hidden=false +func (c Command) VisibleFlags() []Flag { + return visibleFlags(c.Flags) +} diff --git a/vendor/github.com/urfave/cli/context.go b/vendor/github.com/urfave/cli/context.go new file mode 100644 index 00000000..552ee740 --- /dev/null +++ b/vendor/github.com/urfave/cli/context.go @@ -0,0 +1,287 @@ +package cli + +import ( + "errors" + "flag" + "os" + "reflect" + "strings" + "syscall" +) + +// Context is a type that is passed through to +// each Handler action in a cli application. Context +// can be used to retrieve context-specific Args and +// parsed command-line options. +type Context struct { + App *App + Command Command + shellComplete bool + flagSet *flag.FlagSet + setFlags map[string]bool + parentContext *Context +} + +// NewContext creates a new context. For use in when invoking an App or Command action. +func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { + c := &Context{App: app, flagSet: set, parentContext: parentCtx} + + if parentCtx != nil { + c.shellComplete = parentCtx.shellComplete + } + + return c +} + +// NumFlags returns the number of flags set +func (c *Context) NumFlags() int { + return c.flagSet.NFlag() +} + +// Set sets a context flag to a value. +func (c *Context) Set(name, value string) error { + c.setFlags = nil + return c.flagSet.Set(name, value) +} + +// GlobalSet sets a context flag to a value on the global flagset +func (c *Context) GlobalSet(name, value string) error { + globalContext(c).setFlags = nil + return globalContext(c).flagSet.Set(name, value) +} + +// IsSet determines if the flag was actually set +func (c *Context) IsSet(name string) bool { + if c.setFlags == nil { + c.setFlags = make(map[string]bool) + + c.flagSet.Visit(func(f *flag.Flag) { + c.setFlags[f.Name] = true + }) + + c.flagSet.VisitAll(func(f *flag.Flag) { + if _, ok := c.setFlags[f.Name]; ok { + return + } + c.setFlags[f.Name] = false + }) + + // XXX hack to support IsSet for flags with EnvVar + // + // There isn't an easy way to do this with the current implementation since + // whether a flag was set via an environment variable is very difficult to + // determine here. Instead, we intend to introduce a backwards incompatible + // change in version 2 to add `IsSet` to the Flag interface to push the + // responsibility closer to where the information required to determine + // whether a flag is set by non-standard means such as environment + // variables is available. + // + // See https://github.com/urfave/cli/issues/294 for additional discussion + flags := c.Command.Flags + if c.Command.Name == "" { // cannot == Command{} since it contains slice types + if c.App != nil { + flags = c.App.Flags + } + } + for _, f := range flags { + eachName(f.GetName(), func(name string) { + if isSet, ok := c.setFlags[name]; isSet || !ok { + return + } + + val := reflect.ValueOf(f) + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + + filePathValue := val.FieldByName("FilePath") + if filePathValue.IsValid() { + eachName(filePathValue.String(), func(filePath string) { + if _, err := os.Stat(filePath); err == nil { + c.setFlags[name] = true + return + } + }) + } + + envVarValue := val.FieldByName("EnvVar") + if envVarValue.IsValid() { + eachName(envVarValue.String(), func(envVar string) { + envVar = strings.TrimSpace(envVar) + if _, ok := syscall.Getenv(envVar); ok { + c.setFlags[name] = true + return + } + }) + } + }) + } + } + + return c.setFlags[name] +} + +// GlobalIsSet determines if the global flag was actually set +func (c *Context) GlobalIsSet(name string) bool { + ctx := c + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + + for ; ctx != nil; ctx = ctx.parentContext { + if ctx.IsSet(name) { + return true + } + } + return false +} + +// FlagNames returns a slice of flag names used in this context. +func (c *Context) FlagNames() (names []string) { + for _, flag := range c.Command.Flags { + name := strings.Split(flag.GetName(), ",")[0] + if name == "help" { + continue + } + names = append(names, name) + } + return +} + +// GlobalFlagNames returns a slice of global flag names used by the app. +func (c *Context) GlobalFlagNames() (names []string) { + for _, flag := range c.App.Flags { + name := strings.Split(flag.GetName(), ",")[0] + if name == "help" || name == "version" { + continue + } + names = append(names, name) + } + return +} + +// Parent returns the parent context, if any +func (c *Context) Parent() *Context { + return c.parentContext +} + +// value returns the value of the flag coressponding to `name` +func (c *Context) value(name string) interface{} { + return c.flagSet.Lookup(name).Value.(flag.Getter).Get() +} + +// Args contains apps console arguments +type Args []string + +// Args returns the command line arguments associated with the context. +func (c *Context) Args() Args { + args := Args(c.flagSet.Args()) + return args +} + +// NArg returns the number of the command line arguments. +func (c *Context) NArg() int { + return len(c.Args()) +} + +// Get returns the nth argument, or else a blank string +func (a Args) Get(n int) string { + if len(a) > n { + return a[n] + } + return "" +} + +// First returns the first argument, or else a blank string +func (a Args) First() string { + return a.Get(0) +} + +// Tail returns the rest of the arguments (not the first one) +// or else an empty string slice +func (a Args) Tail() []string { + if len(a) >= 2 { + return []string(a)[1:] + } + return []string{} +} + +// Present checks if there are any arguments present +func (a Args) Present() bool { + return len(a) != 0 +} + +// Swap swaps arguments at the given indexes +func (a Args) Swap(from, to int) error { + if from >= len(a) || to >= len(a) { + return errors.New("index out of range") + } + a[from], a[to] = a[to], a[from] + return nil +} + +func globalContext(ctx *Context) *Context { + if ctx == nil { + return nil + } + + for { + if ctx.parentContext == nil { + return ctx + } + ctx = ctx.parentContext + } +} + +func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { + if ctx.parentContext != nil { + ctx = ctx.parentContext + } + for ; ctx != nil; ctx = ctx.parentContext { + if f := ctx.flagSet.Lookup(name); f != nil { + return ctx.flagSet + } + } + return nil +} + +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case *StringSlice: + default: + set.Set(name, ff.Value.String()) + } +} + +func normalizeFlags(flags []Flag, set *flag.FlagSet) error { + visited := make(map[string]bool) + set.Visit(func(f *flag.Flag) { + visited[f.Name] = true + }) + for _, f := range flags { + parts := strings.Split(f.GetName(), ",") + if len(parts) == 1 { + continue + } + var ff *flag.Flag + for _, name := range parts { + name = strings.Trim(name, " ") + if visited[name] { + if ff != nil { + return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) + } + ff = set.Lookup(name) + } + } + if ff == nil { + continue + } + for _, name := range parts { + name = strings.Trim(name, " ") + if !visited[name] { + copyFlag(name, ff, set) + } + } + } + return nil +} diff --git a/vendor/github.com/urfave/cli/errors.go b/vendor/github.com/urfave/cli/errors.go new file mode 100644 index 00000000..562b2953 --- /dev/null +++ b/vendor/github.com/urfave/cli/errors.go @@ -0,0 +1,115 @@ +package cli + +import ( + "fmt" + "io" + "os" + "strings" +) + +// OsExiter is the function used when the app exits. If not set defaults to os.Exit. +var OsExiter = os.Exit + +// ErrWriter is used to write errors to the user. This can be anything +// implementing the io.Writer interface and defaults to os.Stderr. +var ErrWriter io.Writer = os.Stderr + +// MultiError is an error that wraps multiple errors. +type MultiError struct { + Errors []error +} + +// NewMultiError creates a new MultiError. Pass in one or more errors. +func NewMultiError(err ...error) MultiError { + return MultiError{Errors: err} +} + +// Error implements the error interface. +func (m MultiError) Error() string { + errs := make([]string, len(m.Errors)) + for i, err := range m.Errors { + errs[i] = err.Error() + } + + return strings.Join(errs, "\n") +} + +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + +// ExitCoder is the interface checked by `App` and `Command` for a custom exit +// code +type ExitCoder interface { + error + ExitCode() int +} + +// ExitError fulfills both the builtin `error` interface and `ExitCoder` +type ExitError struct { + exitCode int + message interface{} +} + +// NewExitError makes a new *ExitError +func NewExitError(message interface{}, exitCode int) *ExitError { + return &ExitError{ + exitCode: exitCode, + message: message, + } +} + +// Error returns the string message, fulfilling the interface required by +// `error` +func (ee *ExitError) Error() string { + return fmt.Sprintf("%v", ee.message) +} + +// ExitCode returns the exit code, fulfilling the interface required by +// `ExitCoder` +func (ee *ExitError) ExitCode() int { + return ee.exitCode +} + +// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if +// so prints the error to stderr (if it is non-empty) and calls OsExiter with the +// given exit code. If the given error is a MultiError, then this func is +// called on all members of the Errors slice and calls OsExiter with the last exit code. +func HandleExitCoder(err error) { + if err == nil { + return + } + + if exitErr, ok := err.(ExitCoder); ok { + if err.Error() != "" { + if _, ok := exitErr.(ErrorFormatter); ok { + fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + fmt.Fprintln(ErrWriter, err) + } + } + OsExiter(exitErr.ExitCode()) + return + } + + if multiErr, ok := err.(MultiError); ok { + code := handleMultiError(multiErr) + OsExiter(code) + return + } +} + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code +} diff --git a/vendor/github.com/urfave/cli/flag-types.json b/vendor/github.com/urfave/cli/flag-types.json new file mode 100644 index 00000000..12231078 --- /dev/null +++ b/vendor/github.com/urfave/cli/flag-types.json @@ -0,0 +1,93 @@ +[ + { + "name": "Bool", + "type": "bool", + "value": false, + "context_default": "false", + "parser": "strconv.ParseBool(f.Value.String())" + }, + { + "name": "BoolT", + "type": "bool", + "value": false, + "doctail": " that is true by default", + "context_default": "false", + "parser": "strconv.ParseBool(f.Value.String())" + }, + { + "name": "Duration", + "type": "time.Duration", + "doctail": " (see https://golang.org/pkg/time/#ParseDuration)", + "context_default": "0", + "parser": "time.ParseDuration(f.Value.String())" + }, + { + "name": "Float64", + "type": "float64", + "context_default": "0", + "parser": "strconv.ParseFloat(f.Value.String(), 64)" + }, + { + "name": "Generic", + "type": "Generic", + "dest": false, + "context_default": "nil", + "context_type": "interface{}" + }, + { + "name": "Int64", + "type": "int64", + "context_default": "0", + "parser": "strconv.ParseInt(f.Value.String(), 0, 64)" + }, + { + "name": "Int", + "type": "int", + "context_default": "0", + "parser": "strconv.ParseInt(f.Value.String(), 0, 64)", + "parser_cast": "int(parsed)" + }, + { + "name": "IntSlice", + "type": "*IntSlice", + "dest": false, + "context_default": "nil", + "context_type": "[]int", + "parser": "(f.Value.(*IntSlice)).Value(), error(nil)" + }, + { + "name": "Int64Slice", + "type": "*Int64Slice", + "dest": false, + "context_default": "nil", + "context_type": "[]int64", + "parser": "(f.Value.(*Int64Slice)).Value(), error(nil)" + }, + { + "name": "String", + "type": "string", + "context_default": "\"\"", + "parser": "f.Value.String(), error(nil)" + }, + { + "name": "StringSlice", + "type": "*StringSlice", + "dest": false, + "context_default": "nil", + "context_type": "[]string", + "parser": "(f.Value.(*StringSlice)).Value(), error(nil)" + }, + { + "name": "Uint64", + "type": "uint64", + "context_default": "0", + "parser": "strconv.ParseUint(f.Value.String(), 0, 64)" + }, + { + "name": "Uint", + "type": "uint", + "context_default": "0", + "parser": "strconv.ParseUint(f.Value.String(), 0, 64)", + "parser_cast": "uint(parsed)" + } +] diff --git a/vendor/github.com/urfave/cli/flag.go b/vendor/github.com/urfave/cli/flag.go new file mode 100644 index 00000000..b0cffc00 --- /dev/null +++ b/vendor/github.com/urfave/cli/flag.go @@ -0,0 +1,786 @@ +package cli + +import ( + "flag" + "fmt" + "io/ioutil" + "reflect" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +const defaultPlaceholder = "value" + +// BashCompletionFlag enables bash-completion for all commands and subcommands +var BashCompletionFlag Flag = BoolFlag{ + Name: "generate-bash-completion", + Hidden: true, +} + +// VersionFlag prints the version for the application +var VersionFlag Flag = BoolFlag{ + Name: "version, v", + Usage: "print the version", +} + +// HelpFlag prints the help for all commands and subcommands +// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand +// unless HideHelp is set to true) +var HelpFlag Flag = BoolFlag{ + Name: "help, h", + Usage: "show help", +} + +// FlagStringer converts a flag definition to a string. This is used by help +// to display a flag. +var FlagStringer FlagStringFunc = stringifyFlag + +// FlagNamePrefixer converts a full flag name and its placeholder into the help +// message flag prefix. This is used by the default FlagStringer. +var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames + +// FlagEnvHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagEnvHinter FlagEnvHintFunc = withEnvHint + +// FlagFileHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagFileHinter FlagFileHintFunc = withFileHint + +// FlagsByName is a slice of Flag. +type FlagsByName []Flag + +func (f FlagsByName) Len() int { + return len(f) +} + +func (f FlagsByName) Less(i, j int) bool { + return lexicographicLess(f[i].GetName(), f[j].GetName()) +} + +func (f FlagsByName) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + +// Flag is a common interface related to parsing flags in cli. +// For more advanced flag parsing techniques, it is recommended that +// this interface be implemented. +type Flag interface { + fmt.Stringer + // Apply Flag settings to the given flag set + Apply(*flag.FlagSet) + GetName() string +} + +// errorableFlag is an interface that allows us to return errors during apply +// it allows flags defined in this library to return errors in a fashion backwards compatible +// TODO remove in v2 and modify the existing Flag interface to return errors +type errorableFlag interface { + Flag + + ApplyWithError(*flag.FlagSet) error +} + +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { + set := flag.NewFlagSet(name, flag.ContinueOnError) + + for _, f := range flags { + //TODO remove in v2 when errorableFlag is removed + if ef, ok := f.(errorableFlag); ok { + if err := ef.ApplyWithError(set); err != nil { + return nil, err + } + } else { + f.Apply(set) + } + } + return set, nil +} + +func eachName(longName string, fn func(string)) { + parts := strings.Split(longName, ",") + for _, name := range parts { + name = strings.Trim(name, " ") + fn(name) + } +} + +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// Apply takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +// Ignores parsing errors +func (f GenericFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { + val := f.Value + if fileEnvVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if err := val.Set(fileEnvVal); err != nil { + return fmt.Errorf("could not parse %s as value for flag %s: %s", fileEnvVal, f.Name, err) + } + } + + eachName(f.Name, func(name string) { + set.Var(f.Value, name, f.Usage) + }) + + return nil +} + +// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter +type StringSlice []string + +// Set appends the string value to the list of values +func (f *StringSlice) Set(value string) error { + *f = append(*f, value) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *StringSlice) String() string { + return fmt.Sprintf("%s", *f) +} + +// Value returns the slice of strings set by this flag +func (f *StringSlice) Value() []string { + return *f +} + +// Get returns the slice of strings set by this flag +func (f *StringSlice) Get() interface{} { + return *f +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f StringSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &StringSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) + } + } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &StringSlice{} + } + set.Var(f.Value, name, f.Usage) + }) + + return nil +} + +// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter +type IntSlice []int + +// Set parses the value into an integer and appends it to the list of values +func (f *IntSlice) Set(value string) error { + tmp, err := strconv.Atoi(value) + if err != nil { + return err + } + *f = append(*f, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *IntSlice) String() string { + return fmt.Sprintf("%#v", *f) +} + +// Value returns the slice of ints set by this flag +func (f *IntSlice) Value() []int { + return *f +} + +// Get returns the slice of ints set by this flag +func (f *IntSlice) Get() interface{} { + return *f +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f IntSliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &IntSlice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) + } + } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &IntSlice{} + } + set.Var(f.Value, name, f.Usage) + }) + + return nil +} + +// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter +type Int64Slice []int64 + +// Set parses the value into an integer and appends it to the list of values +func (f *Int64Slice) Set(value string) error { + tmp, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return err + } + *f = append(*f, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Int64Slice) String() string { + return fmt.Sprintf("%#v", *f) +} + +// Value returns the slice of ints set by this flag +func (f *Int64Slice) Value() []int64 { + return *f +} + +// Get returns the slice of ints set by this flag +func (f *Int64Slice) Get() interface{} { + return *f +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Int64SliceFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + newVal := &Int64Slice{} + for _, s := range strings.Split(envVal, ",") { + s = strings.TrimSpace(s) + if err := newVal.Set(s); err != nil { + return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) + } + } + if f.Value == nil { + f.Value = newVal + } else { + *f.Value = *newVal + } + } + + eachName(f.Name, func(name string) { + if f.Value == nil { + f.Value = &Int64Slice{} + } + set.Var(f.Value, name, f.Usage) + }) + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f BoolFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { + val := false + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if envVal == "" { + val = false + } else { + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + val = envValBool + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.BoolVar(f.Destination, name, val, f.Usage) + return + } + set.Bool(name, val, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f BoolTFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { + val := true + + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + if envVal == "" { + val = false + } else { + envValBool, err := strconv.ParseBool(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) + } + val = envValBool + } + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.BoolVar(f.Destination, name, val, f.Usage) + return + } + set.Bool(name, val, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f StringFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + f.Value = envVal + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.StringVar(f.Destination, name, f.Value, f.Usage) + return + } + set.String(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f IntFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) + } + f.Value = int(envValInt) + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.IntVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Int(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Int64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseInt(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = envValInt + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Int64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Int64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f UintFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint(envValInt) + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.UintVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Uint64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValInt, err := strconv.ParseUint(envVal, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = uint64(envValInt) + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Uint64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Uint64(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f DurationFlag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValDuration, err := time.ParseDuration(envVal) + if err != nil { + return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) + } + + f.Value = envValDuration + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.DurationVar(f.Destination, name, f.Value, f.Usage) + return + } + set.Duration(name, f.Value, f.Usage) + }) + + return nil +} + +// Apply populates the flag given the flag set and environment +// Ignores errors +func (f Float64Flag) Apply(set *flag.FlagSet) { + f.ApplyWithError(set) +} + +// ApplyWithError populates the flag given the flag set and environment +func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { + if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { + envValFloat, err := strconv.ParseFloat(envVal, 10) + if err != nil { + return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) + } + + f.Value = float64(envValFloat) + } + + eachName(f.Name, func(name string) { + if f.Destination != nil { + set.Float64Var(f.Destination, name, f.Value, f.Usage) + return + } + set.Float64(name, f.Value, f.Usage) + }) + + return nil +} + +func visibleFlags(fl []Flag) []Flag { + visible := []Flag{} + for _, flag := range fl { + field := flagValue(flag).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { + visible = append(visible, flag) + } + } + return visible +} + +func prefixFor(name string) (prefix string) { + if len(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} + +// Returns the placeholder, if any, and the unquoted usage string. +func unquoteUsage(usage string) (string, string) { + for i := 0; i < len(usage); i++ { + if usage[i] == '`' { + for j := i + 1; j < len(usage); j++ { + if usage[j] == '`' { + name := usage[i+1 : j] + usage = usage[:i] + name + usage[j+1:] + return name, usage + } + } + break + } + } + return "", usage +} + +func prefixedNames(fullName, placeholder string) string { + var prefixed string + parts := strings.Split(fullName, ",") + for i, name := range parts { + name = strings.Trim(name, " ") + prefixed += prefixFor(name) + name + if placeholder != "" { + prefixed += " " + placeholder + } + if i < len(parts)-1 { + prefixed += ", " + } + } + return prefixed +} + +func withEnvHint(envVar, str string) string { + envText := "" + if envVar != "" { + prefix := "$" + suffix := "" + sep := ", $" + if runtime.GOOS == "windows" { + prefix = "%" + suffix = "%" + sep = "%, %" + } + envText = " [" + prefix + strings.Join(strings.Split(envVar, ","), sep) + suffix + "]" + } + return str + envText +} + +func withFileHint(filePath, str string) string { + fileText := "" + if filePath != "" { + fileText = fmt.Sprintf(" [%s]", filePath) + } + return str + fileText +} + +func flagValue(f Flag) reflect.Value { + fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +func stringifyFlag(f Flag) string { + fv := flagValue(f) + + switch f.(type) { + case IntSliceFlag: + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyIntSliceFlag(f.(IntSliceFlag)), + ), + ) + case Int64SliceFlag: + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyInt64SliceFlag(f.(Int64SliceFlag)), + ), + ) + case StringSliceFlag: + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + stringifyStringSliceFlag(f.(StringSliceFlag)), + ), + ) + } + + placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) + + needsPlaceholder := false + defaultValueString := "" + + if val := fv.FieldByName("Value"); val.IsValid() { + needsPlaceholder = true + defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) + + if val.Kind() == reflect.String && val.String() != "" { + defaultValueString = fmt.Sprintf(" (default: %q)", val.String()) + } + } + + if defaultValueString == " (default: )" { + defaultValueString = "" + } + + if needsPlaceholder && placeholder == "" { + placeholder = defaultPlaceholder + } + + usageWithDefault := strings.TrimSpace(usage + defaultValueString) + + return FlagFileHinter( + fv.FieldByName("FilePath").String(), + FlagEnvHinter( + fv.FieldByName("EnvVar").String(), + FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder)+"\t"+usageWithDefault, + ), + ) +} + +func stringifyIntSliceFlag(f IntSliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.Itoa(i)) + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + +func stringifyInt64SliceFlag(f Int64SliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + +func stringifyStringSliceFlag(f StringSliceFlag) string { + defaultVals := []string{} + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, s := range f.Value.Value() { + if len(s) > 0 { + defaultVals = append(defaultVals, strconv.Quote(s)) + } + } + } + + return stringifySliceFlag(f.Usage, f.Name, defaultVals) +} + +func stringifySliceFlag(usage, name string, defaultVals []string) string { + placeholder, usage := unquoteUsage(usage) + if placeholder == "" { + placeholder = defaultPlaceholder + } + + defaultVal := "" + if len(defaultVals) > 0 { + defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) + } + + usageWithDefault := strings.TrimSpace(usage + defaultVal) + return FlagNamePrefixer(name, placeholder) + "\t" + usageWithDefault +} + +func flagFromFileEnv(filePath, envName string) (val string, ok bool) { + for _, envVar := range strings.Split(envName, ",") { + envVar = strings.TrimSpace(envVar) + if envVal, ok := syscall.Getenv(envVar); ok { + return envVal, true + } + } + for _, fileVar := range strings.Split(filePath, ",") { + if data, err := ioutil.ReadFile(fileVar); err == nil { + return string(data), true + } + } + return "", false +} diff --git a/vendor/github.com/urfave/cli/flag_generated.go b/vendor/github.com/urfave/cli/flag_generated.go new file mode 100644 index 00000000..001576c8 --- /dev/null +++ b/vendor/github.com/urfave/cli/flag_generated.go @@ -0,0 +1,640 @@ +package cli + +import ( + "flag" + "strconv" + "time" +) + +// WARNING: This file is generated! + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolFlag) GetName() string { + return f.Name +} + +// Bool looks up the value of a local BoolFlag, returns +// false if not found +func (c *Context) Bool(name string) bool { + return lookupBool(name, c.flagSet) +} + +// GlobalBool looks up the value of a global BoolFlag, returns +// false if not found +func (c *Context) GlobalBool(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// BoolTFlag is a flag with type bool that is true by default +type BoolTFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Destination *bool +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f BoolTFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f BoolTFlag) GetName() string { + return f.Name +} + +// BoolT looks up the value of a local BoolTFlag, returns +// false if not found +func (c *Context) BoolT(name string) bool { + return lookupBoolT(name, c.flagSet) +} + +// GlobalBoolT looks up the value of a global BoolTFlag, returns +// false if not found +func (c *Context) GlobalBoolT(name string) bool { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupBoolT(name, fs) + } + return false +} + +func lookupBoolT(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} + +// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) +type DurationFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value time.Duration + Destination *time.Duration +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f DurationFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f DurationFlag) GetName() string { + return f.Name +} + +// Duration looks up the value of a local DurationFlag, returns +// 0 if not found +func (c *Context) Duration(name string) time.Duration { + return lookupDuration(name, c.flagSet) +} + +// GlobalDuration looks up the value of a global DurationFlag, returns +// 0 if not found +func (c *Context) GlobalDuration(name string) time.Duration { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + parsed, err := time.ParseDuration(f.Value.String()) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value float64 + Destination *float64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Float64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Float64Flag) GetName() string { + return f.Name +} + +// Float64 looks up the value of a local Float64Flag, returns +// 0 if not found +func (c *Context) Float64(name string) float64 { + return lookupFloat64(name, c.flagSet) +} + +// GlobalFloat64 looks up the value of a global Float64Flag, returns +// 0 if not found +func (c *Context) GlobalFloat64(name string) float64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupFloat64(name, fs) + } + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value Generic +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f GenericFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f GenericFlag) GetName() string { + return f.Name +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (c *Context) Generic(name string) interface{} { + return lookupGeneric(name, c.flagSet) +} + +// GlobalGeneric looks up the value of a global GenericFlag, returns +// nil if not found +func (c *Context) GlobalGeneric(name string) interface{} { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupGeneric(name, fs) + } + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value, error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value int64 + Destination *int64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64Flag) GetName() string { + return f.Name +} + +// Int64 looks up the value of a local Int64Flag, returns +// 0 if not found +func (c *Context) Int64(name string) int64 { + return lookupInt64(name, c.flagSet) +} + +// GlobalInt64 looks up the value of a global Int64Flag, returns +// 0 if not found +func (c *Context) GlobalInt64(name string) int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64(name, fs) + } + return 0 +} + +func lookupInt64(name string, set *flag.FlagSet) int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value int + Destination *int +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntFlag) GetName() string { + return f.Name +} + +// Int looks up the value of a local IntFlag, returns +// 0 if not found +func (c *Context) Int(name string) int { + return lookupInt(name, c.flagSet) +} + +// GlobalInt looks up the value of a global IntFlag, returns +// 0 if not found +func (c *Context) GlobalInt(name string) int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt(name, fs) + } + return 0 +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return int(parsed) + } + return 0 +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *IntSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f IntSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f IntSliceFlag) GetName() string { + return f.Name +} + +// IntSlice looks up the value of a local IntSliceFlag, returns +// nil if not found +func (c *Context) IntSlice(name string) []int { + return lookupIntSlice(name, c.flagSet) +} + +// GlobalIntSlice looks up the value of a global IntSliceFlag, returns +// nil if not found +func (c *Context) GlobalIntSlice(name string) []int { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupIntSlice(name, fs) + } + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *Int64Slice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Int64SliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Int64SliceFlag) GetName() string { + return f.Name +} + +// Int64Slice looks up the value of a local Int64SliceFlag, returns +// nil if not found +func (c *Context) Int64Slice(name string) []int64 { + return lookupInt64Slice(name, c.flagSet) +} + +// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns +// nil if not found +func (c *Context) GlobalInt64Slice(name string) []int64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupInt64Slice(name, fs) + } + return nil +} + +func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value string + Destination *string +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringFlag) GetName() string { + return f.Name +} + +// String looks up the value of a local StringFlag, returns +// "" if not found +func (c *Context) String(name string) string { + return lookupString(name, c.flagSet) +} + +// GlobalString looks up the value of a global StringFlag, returns +// "" if not found +func (c *Context) GlobalString(name string) string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupString(name, fs) + } + return "" +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value *StringSlice +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f StringSliceFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f StringSliceFlag) GetName() string { + return f.Name +} + +// StringSlice looks up the value of a local StringSliceFlag, returns +// nil if not found +func (c *Context) StringSlice(name string) []string { + return lookupStringSlice(name, c.flagSet) +} + +// GlobalStringSlice looks up the value of a global StringSliceFlag, returns +// nil if not found +func (c *Context) GlobalStringSlice(name string) []string { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupStringSlice(name, fs) + } + return nil +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value uint64 + Destination *uint64 +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f Uint64Flag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f Uint64Flag) GetName() string { + return f.Name +} + +// Uint64 looks up the value of a local Uint64Flag, returns +// 0 if not found +func (c *Context) Uint64(name string) uint64 { + return lookupUint64(name, c.flagSet) +} + +// GlobalUint64 looks up the value of a global Uint64Flag, returns +// 0 if not found +func (c *Context) GlobalUint64(name string) uint64 { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint64(name, fs) + } + return 0 +} + +func lookupUint64(name string, set *flag.FlagSet) uint64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + Value uint + Destination *uint +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f UintFlag) String() string { + return FlagStringer(f) +} + +// GetName returns the name of the flag +func (f UintFlag) GetName() string { + return f.Name +} + +// Uint looks up the value of a local UintFlag, returns +// 0 if not found +func (c *Context) Uint(name string) uint { + return lookupUint(name, c.flagSet) +} + +// GlobalUint looks up the value of a global UintFlag, returns +// 0 if not found +func (c *Context) GlobalUint(name string) uint { + if fs := lookupGlobalFlagSet(name, c); fs != nil { + return lookupUint(name, fs) + } + return 0 +} + +func lookupUint(name string, set *flag.FlagSet) uint { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return uint(parsed) + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/funcs.go b/vendor/github.com/urfave/cli/funcs.go new file mode 100644 index 00000000..0036b113 --- /dev/null +++ b/vendor/github.com/urfave/cli/funcs.go @@ -0,0 +1,44 @@ +package cli + +// BashCompleteFunc is an action to execute when the bash-completion flag is set +type BashCompleteFunc func(*Context) + +// BeforeFunc is an action to execute before any subcommands are run, but after +// the context is ready if a non-nil error is returned, no subcommands are run +type BeforeFunc func(*Context) error + +// AfterFunc is an action to execute after any subcommands are run, but after the +// subcommand has finished it is run even if Action() panics +type AfterFunc func(*Context) error + +// ActionFunc is the action to execute when no subcommands are specified +type ActionFunc func(*Context) error + +// CommandNotFoundFunc is executed if the proper command cannot be found +type CommandNotFoundFunc func(*Context, string) + +// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying +// customized usage error messages. This function is able to replace the +// original error messages. If this function is not set, the "Incorrect usage" +// is displayed and the execution is interrupted. +type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error + +// ExitErrHandlerFunc is executed if provided in order to handle ExitError values +// returned by Actions and Before/After functions. +type ExitErrHandlerFunc func(context *Context, err error) + +// FlagStringFunc is used by the help generation to display a flag, which is +// expected to be a single line. +type FlagStringFunc func(Flag) string + +// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix +// text for a flag's full name. +type FlagNamePrefixFunc func(fullName, placeholder string) string + +// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help +// with the environment variable details. +type FlagEnvHintFunc func(envVar, str string) string + +// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help +// with the file path details. +type FlagFileHintFunc func(filePath, str string) string diff --git a/vendor/github.com/urfave/cli/generate-flag-types b/vendor/github.com/urfave/cli/generate-flag-types new file mode 100755 index 00000000..13588573 --- /dev/null +++ b/vendor/github.com/urfave/cli/generate-flag-types @@ -0,0 +1,256 @@ +#!/usr/bin/env python +""" +The flag types that ship with the cli library have many things in common, and +so we can take advantage of the `go generate` command to create much of the +source code from a list of definitions. These definitions attempt to cover +the parts that vary between flag types, and should evolve as needed. + +An example of the minimum definition needed is: + + { + "name": "SomeType", + "type": "sometype", + "context_default": "nil" + } + +In this example, the code generated for the `cli` package will include a type +named `SomeTypeFlag` that is expected to wrap a value of type `sometype`. +Fetching values by name via `*cli.Context` will default to a value of `nil`. + +A more complete, albeit somewhat redundant, example showing all available +definition keys is: + + { + "name": "VeryMuchType", + "type": "*VeryMuchType", + "value": true, + "dest": false, + "doctail": " which really only wraps a []float64, oh well!", + "context_type": "[]float64", + "context_default": "nil", + "parser": "parseVeryMuchType(f.Value.String())", + "parser_cast": "[]float64(parsed)" + } + +The meaning of each field is as follows: + + name (string) - The type "name", which will be suffixed with + `Flag` when generating the type definition + for `cli` and the wrapper type for `altsrc` + type (string) - The type that the generated `Flag` type for `cli` + is expected to "contain" as its `.Value` member + value (bool) - Should the generated `cli` type have a `Value` + member? + dest (bool) - Should the generated `cli` type support a + destination pointer? + doctail (string) - Additional docs for the `cli` flag type comment + context_type (string) - The literal type used in the `*cli.Context` + reader func signature + context_default (string) - The literal value used as the default by the + `*cli.Context` reader funcs when no value is + present + parser (string) - Literal code used to parse the flag `f`, + expected to have a return signature of + (value, error) + parser_cast (string) - Literal code used to cast the `parsed` value + returned from the `parser` code +""" + +from __future__ import print_function, unicode_literals + +import argparse +import json +import os +import subprocess +import sys +import tempfile +import textwrap + + +class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter, + argparse.RawDescriptionHelpFormatter): + pass + + +def main(sysargs=sys.argv[:]): + parser = argparse.ArgumentParser( + description='Generate flag type code!', + formatter_class=_FancyFormatter) + parser.add_argument( + 'package', + type=str, default='cli', choices=_WRITEFUNCS.keys(), + help='Package for which flag types will be generated' + ) + parser.add_argument( + '-i', '--in-json', + type=argparse.FileType('r'), + default=sys.stdin, + help='Input JSON file which defines each type to be generated' + ) + parser.add_argument( + '-o', '--out-go', + type=argparse.FileType('w'), + default=sys.stdout, + help='Output file/stream to which generated source will be written' + ) + parser.epilog = __doc__ + + args = parser.parse_args(sysargs[1:]) + _generate_flag_types(_WRITEFUNCS[args.package], args.out_go, args.in_json) + return 0 + + +def _generate_flag_types(writefunc, output_go, input_json): + types = json.load(input_json) + + tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False) + writefunc(tmp, types) + tmp.close() + + new_content = subprocess.check_output( + ['goimports', tmp.name] + ).decode('utf-8') + + print(new_content, file=output_go, end='') + output_go.flush() + os.remove(tmp.name) + + +def _set_typedef_defaults(typedef): + typedef.setdefault('doctail', '') + typedef.setdefault('context_type', typedef['type']) + typedef.setdefault('dest', True) + typedef.setdefault('value', True) + typedef.setdefault('parser', 'f.Value, error(nil)') + typedef.setdefault('parser_cast', 'parsed') + + +def _write_cli_flag_types(outfile, types): + _fwrite(outfile, """\ + package cli + + // WARNING: This file is generated! + + """) + + for typedef in types: + _set_typedef_defaults(typedef) + + _fwrite(outfile, """\ + // {name}Flag is a flag with type {type}{doctail} + type {name}Flag struct {{ + Name string + Usage string + EnvVar string + FilePath string + Hidden bool + """.format(**typedef)) + + if typedef['value']: + _fwrite(outfile, """\ + Value {type} + """.format(**typedef)) + + if typedef['dest']: + _fwrite(outfile, """\ + Destination *{type} + """.format(**typedef)) + + _fwrite(outfile, "\n}\n\n") + + _fwrite(outfile, """\ + // String returns a readable representation of this value + // (for usage defaults) + func (f {name}Flag) String() string {{ + return FlagStringer(f) + }} + + // GetName returns the name of the flag + func (f {name}Flag) GetName() string {{ + return f.Name + }} + + // {name} looks up the value of a local {name}Flag, returns + // {context_default} if not found + func (c *Context) {name}(name string) {context_type} {{ + return lookup{name}(name, c.flagSet) + }} + + // Global{name} looks up the value of a global {name}Flag, returns + // {context_default} if not found + func (c *Context) Global{name}(name string) {context_type} {{ + if fs := lookupGlobalFlagSet(name, c); fs != nil {{ + return lookup{name}(name, fs) + }} + return {context_default} + }} + + func lookup{name}(name string, set *flag.FlagSet) {context_type} {{ + f := set.Lookup(name) + if f != nil {{ + parsed, err := {parser} + if err != nil {{ + return {context_default} + }} + return {parser_cast} + }} + return {context_default} + }} + """.format(**typedef)) + + +def _write_altsrc_flag_types(outfile, types): + _fwrite(outfile, """\ + package altsrc + + import ( + "gopkg.in/urfave/cli.v1" + ) + + // WARNING: This file is generated! + + """) + + for typedef in types: + _set_typedef_defaults(typedef) + + _fwrite(outfile, """\ + // {name}Flag is the flag type that wraps cli.{name}Flag to allow + // for other values to be specified + type {name}Flag struct {{ + cli.{name}Flag + set *flag.FlagSet + }} + + // New{name}Flag creates a new {name}Flag + func New{name}Flag(fl cli.{name}Flag) *{name}Flag {{ + return &{name}Flag{{{name}Flag: fl, set: nil}} + }} + + // Apply saves the flagSet for later usage calls, then calls the + // wrapped {name}Flag.Apply + func (f *{name}Flag) Apply(set *flag.FlagSet) {{ + f.set = set + f.{name}Flag.Apply(set) + }} + + // ApplyWithError saves the flagSet for later usage calls, then calls the + // wrapped {name}Flag.ApplyWithError + func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{ + f.set = set + return f.{name}Flag.ApplyWithError(set) + }} + """.format(**typedef)) + + +def _fwrite(outfile, text): + print(textwrap.dedent(text), end='', file=outfile) + + +_WRITEFUNCS = { + 'cli': _write_cli_flag_types, + 'altsrc': _write_altsrc_flag_types +} + +if __name__ == '__main__': + sys.exit(main()) diff --git a/vendor/github.com/urfave/cli/help.go b/vendor/github.com/urfave/cli/help.go new file mode 100644 index 00000000..65874fa2 --- /dev/null +++ b/vendor/github.com/urfave/cli/help.go @@ -0,0 +1,345 @@ +package cli + +import ( + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" +) + +// AppHelpTemplate is the text template for the Default help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var AppHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + + {{.Name}}:{{end}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +GLOBAL OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + +COPYRIGHT: + {{.Copyright}}{{end}} +` + +// CommandHelpTemplate is the text template for the command help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var CommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + +CATEGORY: + {{.Category}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +// SubcommandHelpTemplate is the text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{end}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} +{{end}}{{if .VisibleFlags}} +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +var helpCommand = Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) error { + args := c.Args() + if args.Present() { + return ShowCommandHelp(c, args.First()) + } + + ShowAppHelp(c) + return nil + }, +} + +var helpSubcommand = Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) error { + args := c.Args() + if args.Present() { + return ShowCommandHelp(c, args.First()) + } + + return ShowSubcommandHelp(c) + }, +} + +// Prints help for the App or Command +type helpPrinter func(w io.Writer, templ string, data interface{}) + +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + +// HelpPrinter is a function that writes the help output. If not set a default +// is used. The function signature is: +// func(w io.Writer, templ string, data interface{}) +var HelpPrinter helpPrinter = printHelp + +// HelpPrinterCustom is same as HelpPrinter but +// takes a custom function for template function map. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + +// VersionPrinter prints the version for the App +var VersionPrinter = printVersion + +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + ShowAppHelp(c) + os.Exit(exitCode) +} + +// ShowAppHelp is an action that displays the help. +func ShowAppHelp(c *Context) (err error) { + if c.App.CustomAppHelpTemplate == "" { + HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) + return + } + customAppData := func() map[string]interface{} { + if c.App.ExtraInfo == nil { + return nil + } + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } + } + HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) + return nil +} + +// DefaultAppComplete prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + for _, command := range c.App.Commands { + if command.Hidden { + continue + } + if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { + for _, name := range command.Names() { + fmt.Fprintf(c.App.Writer, "%s:%s\n", name, command.Usage) + } + } else { + for _, name := range command.Names() { + fmt.Fprintf(c.App.Writer, "%s\n", name) + } + } + } +} + +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + ShowCommandHelp(c, command) + os.Exit(code) +} + +// ShowCommandHelp prints help for the given command +func ShowCommandHelp(ctx *Context, command string) error { + // show the subcommand help for a command with subcommands + if command == "" { + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) + return nil + } + + for _, c := range ctx.App.Commands { + if c.HasName(command) { + if c.CustomHelpTemplate != "" { + HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) + } else { + HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) + } + return nil + } + } + + if ctx.App.CommandNotFound == nil { + return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3) + } + + ctx.App.CommandNotFound(ctx, command) + return nil +} + +// ShowSubcommandHelp prints help for the given subcommand +func ShowSubcommandHelp(c *Context) error { + return ShowCommandHelp(c, c.Command.Name) +} + +// ShowVersion prints the version number of the App +func ShowVersion(c *Context) { + VersionPrinter(c) +} + +func printVersion(c *Context) { + fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) +} + +// ShowCompletions prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// ShowCommandCompletions prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil && c.BashComplete != nil { + c.BashComplete(ctx) + } +} + +func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { + funcMap := template.FuncMap{ + "join": strings.Join, + } + if customFunc != nil { + for key, value := range customFunc { + funcMap[key] = value + } + } + + w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) + t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) + err := t.Execute(w, data) + if err != nil { + // If the writer is closed, t.Execute will fail, and there's nothing + // we can do to recover. + if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { + fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) + } + return + } + w.Flush() +} + +func printHelp(out io.Writer, templ string, data interface{}) { + printHelpCustom(out, templ, data, nil) +} + +func checkVersion(c *Context) bool { + found := false + if VersionFlag.GetName() != "" { + eachName(VersionFlag.GetName(), func(name string) { + if c.GlobalBool(name) || c.Bool(name) { + found = true + } + }) + } + return found +} + +func checkHelp(c *Context) bool { + found := false + if HelpFlag.GetName() != "" { + eachName(HelpFlag.GetName(), func(name string) { + if c.GlobalBool(name) || c.Bool(name) { + found = true + } + }) + } + return found +} + +func checkCommandHelp(c *Context, name string) bool { + if c.Bool("h") || c.Bool("help") { + ShowCommandHelp(c, name) + return true + } + + return false +} + +func checkSubcommandHelp(c *Context) bool { + if c.Bool("h") || c.Bool("help") { + ShowSubcommandHelp(c) + return true + } + + return false +} + +func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { + if !a.EnableBashCompletion { + return false, arguments + } + + pos := len(arguments) - 1 + lastArg := arguments[pos] + + if lastArg != "--"+BashCompletionFlag.GetName() { + return false, arguments + } + + return true, arguments[:pos] +} + +func checkCompletions(c *Context) bool { + if !c.shellComplete { + return false + } + + if args := c.Args(); args.Present() { + name := args.First() + if cmd := c.App.Command(name); cmd != nil { + // let the command handle the completion + return false + } + } + + ShowCompletions(c) + return true +} + +func checkCommandCompletions(c *Context, name string) bool { + if !c.shellComplete { + return false + } + + ShowCommandCompletions(c, name) + return true +} diff --git a/vendor/github.com/urfave/cli/runtests b/vendor/github.com/urfave/cli/runtests new file mode 100755 index 00000000..ee22bdee --- /dev/null +++ b/vendor/github.com/urfave/cli/runtests @@ -0,0 +1,122 @@ +#!/usr/bin/env python +from __future__ import print_function + +import argparse +import os +import sys +import tempfile + +from subprocess import check_call, check_output + + +PACKAGE_NAME = os.environ.get( + 'CLI_PACKAGE_NAME', 'github.com/urfave/cli' +) + + +def main(sysargs=sys.argv[:]): + targets = { + 'vet': _vet, + 'test': _test, + 'gfmrun': _gfmrun, + 'toc': _toc, + 'gen': _gen, + } + + parser = argparse.ArgumentParser() + parser.add_argument( + 'target', nargs='?', choices=tuple(targets.keys()), default='test' + ) + args = parser.parse_args(sysargs[1:]) + + targets[args.target]() + return 0 + + +def _test(): + if check_output('go version'.split()).split()[2] < 'go1.2': + _run('go test -v .') + return + + coverprofiles = [] + for subpackage in ['', 'altsrc']: + coverprofile = 'cli.coverprofile' + if subpackage != '': + coverprofile = '{}.coverprofile'.format(subpackage) + + coverprofiles.append(coverprofile) + + _run('go test -v'.split() + [ + '-coverprofile={}'.format(coverprofile), + ('{}/{}'.format(PACKAGE_NAME, subpackage)).rstrip('/') + ]) + + combined_name = _combine_coverprofiles(coverprofiles) + _run('go tool cover -func={}'.format(combined_name)) + os.remove(combined_name) + + +def _gfmrun(): + go_version = check_output('go version'.split()).split()[2] + if go_version < 'go1.3': + print('runtests: skip on {}'.format(go_version), file=sys.stderr) + return + _run(['gfmrun', '-c', str(_gfmrun_count()), '-s', 'README.md']) + + +def _vet(): + _run('go vet ./...') + + +def _toc(): + _run('node_modules/.bin/markdown-toc -i README.md') + _run('git diff --exit-code') + + +def _gen(): + go_version = check_output('go version'.split()).split()[2] + if go_version < 'go1.5': + print('runtests: skip on {}'.format(go_version), file=sys.stderr) + return + + _run('go generate ./...') + _run('git diff --exit-code') + + +def _run(command): + if hasattr(command, 'split'): + command = command.split() + print('runtests: {}'.format(' '.join(command)), file=sys.stderr) + check_call(command) + + +def _gfmrun_count(): + with open('README.md') as infile: + lines = infile.read().splitlines() + return len(filter(_is_go_runnable, lines)) + + +def _is_go_runnable(line): + return line.startswith('package main') + + +def _combine_coverprofiles(coverprofiles): + combined = tempfile.NamedTemporaryFile( + suffix='.coverprofile', delete=False + ) + combined.write('mode: set\n') + + for coverprofile in coverprofiles: + with open(coverprofile, 'r') as infile: + for line in infile.readlines(): + if not line.startswith('mode: '): + combined.write(line) + + combined.flush() + name = combined.name + combined.close() + return name + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/vendor/github.com/urfave/cli/sort.go b/vendor/github.com/urfave/cli/sort.go new file mode 100644 index 00000000..23d1c2f7 --- /dev/null +++ b/vendor/github.com/urfave/cli/sort.go @@ -0,0 +1,29 @@ +package cli + +import "unicode" + +// lexicographicLess compares strings alphabetically considering case. +func lexicographicLess(i, j string) bool { + iRunes := []rune(i) + jRunes := []rune(j) + + lenShared := len(iRunes) + if lenShared > len(jRunes) { + lenShared = len(jRunes) + } + + for index := 0; index < lenShared; index++ { + ir := iRunes[index] + jr := jRunes[index] + + if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { + return lir < ljr + } + + if ir != jr { + return ir < jr + } + } + + return i < j +} diff --git a/vendor/vendor.json b/vendor/vendor.json new file mode 100644 index 00000000..a1e24a8a --- /dev/null +++ b/vendor/vendor.json @@ -0,0 +1,19 @@ +{ + "comment": "", + "ignore": "test", + "package": [ + { + "checksumSHA1": "ljd3FhYRJ91cLZz3wsH9BQQ2JbA=", + "path": "github.com/pkg/errors", + "revision": "816c9085562cd7ee03e7f8188a1cfd942858cded", + "revisionTime": "2018-03-11T21:45:15Z" + }, + { + "checksumSHA1": "9/2qvuFPQxW7ZXzQSOYwqZb5cNg=", + "path": "github.com/urfave/cli", + "revision": "8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff", + "revisionTime": "2018-02-26T03:02:53Z" + } + ], + "rootPath": "github.com/StackExchange/blackbox" +} From 57702a1395543379c5bc8f61fe4658c5fa1d1ad2 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 21 Apr 2018 15:35:21 -0400 Subject: [PATCH 02/69] New command: blackbox: Parses subcommands and calls bash scripts. --- cmd/blackbox/blackbox.go | 146 +++++++++++++++++++++++++++++++++++++++ cmd/blackbox/runbash.go | 23 ++++++ 2 files changed, 169 insertions(+) create mode 100644 cmd/blackbox/blackbox.go create mode 100644 cmd/blackbox/runbash.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go new file mode 100644 index 00000000..4ac81823 --- /dev/null +++ b/cmd/blackbox/blackbox.go @@ -0,0 +1,146 @@ +package main + +import ( + "log" + "os" + + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Version = "2.0.0" + app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" + + app.Commands = []cli.Command{ + { + Name: "initialize", + Aliases: []string{"init"}, + Usage: "Runs blackbox_initialize", + Action: func(c *cli.Context) error { + return RunBash("blackbox_initialize", c.Args().First()) + }, + }, + { + Name: "edit", + Aliases: []string{"e", "ed"}, + Usage: "Runs blackbox_edit ", + Action: func(c *cli.Context) error { + return RunBash("blackbox_edit", c.Args().First()) + }, + }, + { + Name: "decrypt", + Aliases: []string{"de"}, + Usage: "Runs blackbox_edit_start", + Action: func(c *cli.Context) error { + return RunBash("blackbox_edit_start", c.Args().First()) + }, + // TODO(tlim): Add --all flag to run blackbox_decrypt_all_files + // TODO(tlim): Add --non-interactive to run blackbox_postdeploy + }, + { + Name: "encrypt", + Aliases: []string{"en"}, + Usage: "Runs blackbox_edit_end", + Action: func(c *cli.Context) error { + return RunBash("blackbox_edit_end", c.Args().First()) + }, + }, + { + Name: "reencrypt", + Usage: "Runs blackbox_update_all_files", + Action: func(c *cli.Context) error { + return RunBash("blackbox_update_all_files", c.Args().First()) + }, + }, + { + Name: "cat", + Usage: "Runs blackbox_cat", + Action: func(c *cli.Context) error { + return RunBash("blackbox_cat", c.Args().First()) + }, + }, + { + Name: "diff", + Usage: "Runs blackbox_diff", + Action: func(c *cli.Context) error { + return RunBash("blackbox_diff", c.Args().First()) + }, + }, + { + Name: "shredall", + Usage: "Runs blackbox_shred_all_files", + Action: func(c *cli.Context) error { + return RunBash("blackbox_shred_all_files", c.Args().First()) + }, + }, + { + Name: "whatsnew", + Usage: "Runs blackbox_whatsnew", + Action: func(c *cli.Context) error { + return RunBash("blackbox_whatsnew", c.Args().First()) + }, + }, + { + Name: "admin", + Usage: "Maintain the list of administrators", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "Runs blackbox_addadmin", + Action: func(c *cli.Context) error { + return RunBash("blackbox_addadmin", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "Runs blackbox_removeadmin", + Action: func(c *cli.Context) error { + return RunBash("blackbox_removeadmin", c.Args().First()) + }, + }, + { + Name: "list", + Usage: "Runs blackbox_list_admins", + Action: func(c *cli.Context) error { + return RunBash("blackbox_list_admins", c.Args().First()) + }, + }, + }, + }, + { + Name: "register", + Aliases: []string{"reg"}, + Usage: "Maintain the list of files", + Subcommands: []cli.Command{ + { + Name: "add", + Usage: "Runs blackbox_register_new_file", + Action: func(c *cli.Context) error { + return RunBash("blackbox_register_new_file", c.Args().First()) + }, + }, + { + Name: "remove", + Usage: "Runs blackbox_deregister_file", + Action: func(c *cli.Context) error { + return RunBash("blackbox_deregister_file", c.Args().First()) + }, + }, + { + Name: "list", + Usage: "Runs blackbox_list_admins", + Action: func(c *cli.Context) error { + return RunBash("blackbox_list_files", c.Args().First()) + }, + }, + }, + }, + } + + err := app.Run(os.Args) + if err != nil { + log.Fatal(err) + } +} diff --git a/cmd/blackbox/runbash.go b/cmd/blackbox/runbash.go new file mode 100644 index 00000000..86e239ca --- /dev/null +++ b/cmd/blackbox/runbash.go @@ -0,0 +1,23 @@ +package main + +import ( + "log" + "os" + "os/exec" + + "github.com/pkg/errors" +) + +// RunBash runs a Bash command. +func RunBash(command string, args ...string) error { + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + err = cmd.Wait() + return errors.Wrapf(err, "run_bash:") +} From 3a5e4b65cbd5cec47abaed082e6d117632bee428 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 21 Apr 2018 20:39:05 -0400 Subject: [PATCH 03/69] Implement status and nlist commands --- cmd/blackbox/blackbox.go | 74 ++++++++++++++++++++---- cmd/blackbox/cmd_list.go | 22 ++++++++ cmd/blackbox/cmd_status.go | 24 ++++++++ cmd/blackbox/runbash.go | 5 ++ pkg/bbgit/discover.go | 38 +++++++++++++ pkg/bbnone/discover.go | 29 ++++++++++ pkg/bbutil/env.go | 112 +++++++++++++++++++++++++++++++++++++ pkg/bbutil/register.go | 62 ++++++++++++++++++++ 8 files changed, 354 insertions(+), 12 deletions(-) create mode 100644 cmd/blackbox/cmd_list.go create mode 100644 cmd/blackbox/cmd_status.go create mode 100644 pkg/bbgit/discover.go create mode 100644 pkg/bbnone/discover.go create mode 100644 pkg/bbutil/env.go create mode 100644 pkg/bbutil/register.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 4ac81823..24976b32 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -1,22 +1,34 @@ package main import ( + "fmt" "log" "os" "github.com/urfave/cli" ) +var dryRun bool + func main() { app := cli.NewApp() app.Version = "2.0.0" app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" + app.Flags = []cli.Flag{ + cli.BoolFlag{ + Name: "dry-run, n", + Usage: "show what would have been done", + Destination: &dryRun, + }, + } + app.Commands = []cli.Command{ { - Name: "initialize", - Aliases: []string{"init"}, - Usage: "Runs blackbox_initialize", + Name: "initialize", + Aliases: []string{"init"}, + Category: "GETTING STARTED", + Usage: "Runs blackbox_initialize", Action: func(c *cli.Context) error { return RunBash("blackbox_initialize", c.Args().First()) }, @@ -31,17 +43,31 @@ func main() { }, { Name: "decrypt", - Aliases: []string{"de"}, + Aliases: []string{"de", "start"}, Usage: "Runs blackbox_edit_start", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "Runs blackbox_decrypt_all_files", + }, + cli.BoolFlag{ + Name: "non-interactive", + Usage: "Runs blackbox_postdeploy", + }, + }, Action: func(c *cli.Context) error { + if c.Bool("all") { + if c.Bool("non-interactive") { + return RunBash("blackbox_postdeploy", c.Args().First()) + } + return RunBash("blackbox_decrypt_all_files", c.Args().First()) + } return RunBash("blackbox_edit_start", c.Args().First()) }, - // TODO(tlim): Add --all flag to run blackbox_decrypt_all_files - // TODO(tlim): Add --non-interactive to run blackbox_postdeploy }, { Name: "encrypt", - Aliases: []string{"en"}, + Aliases: []string{"en", "end"}, Usage: "Runs blackbox_edit_end", Action: func(c *cli.Context) error { return RunBash("blackbox_edit_end", c.Args().First()) @@ -83,8 +109,9 @@ func main() { }, }, { - Name: "admin", - Usage: "Maintain the list of administrators", + Name: "admin", + Category: "ADMINISTRATIVE", + Usage: "Maintain the list of administrators", Subcommands: []cli.Command{ { Name: "add", @@ -110,9 +137,10 @@ func main() { }, }, { - Name: "register", - Aliases: []string{"reg"}, - Usage: "Maintain the list of files", + Name: "register", + Aliases: []string{"reg"}, + Category: "ADMINISTRATIVE", + Usage: "Maintain the list of files", Subcommands: []cli.Command{ { Name: "add", @@ -135,6 +163,28 @@ func main() { return RunBash("blackbox_list_files", c.Args().First()) }, }, + { + Name: "nlist", + Usage: "Lists the registered files", + Action: func(c *cli.Context) error { + if len(c.Args()) != 0 { + fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + return nil + } + return cmdList() + }, + }, + { + Name: "status", + Usage: "Prints info about registered files", + Action: func(c *cli.Context) error { + if len(c.Args()) != 0 { + fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + return nil + } + return cmdStatus() + }, + }, }, }, } diff --git a/cmd/blackbox/cmd_list.go b/cmd/blackbox/cmd_list.go new file mode 100644 index 00000000..7dbd02b5 --- /dev/null +++ b/cmd/blackbox/cmd_list.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "github.com/StackExchange/blackbox/pkg/bbutil" +) + +func cmdList() error { + bbu, err := bbutil.New() + if err != nil { + return err + } + names, err := bbu.RegisteredFiles() + if err != nil { + return err + } + for _, item := range names { + fmt.Println(item.Name) + } + return nil +} diff --git a/cmd/blackbox/cmd_status.go b/cmd/blackbox/cmd_status.go new file mode 100644 index 00000000..f6bf70d1 --- /dev/null +++ b/cmd/blackbox/cmd_status.go @@ -0,0 +1,24 @@ +package main + +import ( + "fmt" + + "github.com/StackExchange/blackbox/pkg/bbutil" +) + +func cmdStatus() error { + bbu, err := bbutil.New() + if err != nil { + return err + } + names, err := bbu.RegisteredFiles() + if err != nil { + return err + } + + for _, item := range names { + s := bbutil.FileStatus(bbu.RepoBaseDir, item.Name) + fmt.Printf("%s\t%s\n", s, item.Name) + } + return nil +} diff --git a/cmd/blackbox/runbash.go b/cmd/blackbox/runbash.go index 86e239ca..5abdaa6d 100644 --- a/cmd/blackbox/runbash.go +++ b/cmd/blackbox/runbash.go @@ -1,6 +1,7 @@ package main import ( + "fmt" "log" "os" "os/exec" @@ -10,6 +11,10 @@ import ( // RunBash runs a Bash command. func RunBash(command string, args ...string) error { + if dryRun { + fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) + return nil + } cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout diff --git a/pkg/bbgit/discover.go b/pkg/bbgit/discover.go new file mode 100644 index 00000000..08ae7503 --- /dev/null +++ b/pkg/bbgit/discover.go @@ -0,0 +1,38 @@ +package bbgit + +import ( + "os/exec" + "strings" + + "github.com/pkg/errors" +) + +// GitInfo contains Git-specific info about this repository. +type GitInfo struct { + baseDir string +} + +// New is a factory; returns error if this is not a Git repo. +func New() (*GitInfo, error) { + ri := new(GitInfo) + path, err := exec.LookPath("git") + if err != nil { + return nil, nil + } + baseDir, err := exec.Command(path, "rev-parse", "--show-toplevel").Output() + if err != nil { + return nil, errors.Wrap(err, "bbgit:") + } + ri.baseDir = strings.TrimSuffix(string(baseDir), "\n") // remove a single newline. + return ri, nil +} + +// Name returns the name of this type of repo. +func (repo *GitInfo) Name() string { + return "git" +} + +// RepoBaseDir returns +func (repo *GitInfo) RepoBaseDir() string { + return repo.baseDir +} diff --git a/pkg/bbnone/discover.go b/pkg/bbnone/discover.go new file mode 100644 index 00000000..e468500f --- /dev/null +++ b/pkg/bbnone/discover.go @@ -0,0 +1,29 @@ +package bbnone + +import ( + "log" + "os" +) + +// NoneInfo contains Git-specific info about this repository. +type NoneInfo struct { +} + +// New is a factory; returns nil if this is not a Git repo. +func New() (*NoneInfo, error) { + return new(NoneInfo), nil +} + +// Name returns the name of this type of repo. +func (repo *NoneInfo) Name() string { + return "unknown" +} + +// RepoBaseDir returns the current working directory. +func (repo *NoneInfo) RepoBaseDir() string { + d, err := os.Getwd() + if err != nil { + log.Fatal(err) + } + return d +} diff --git a/pkg/bbutil/env.go b/pkg/bbutil/env.go new file mode 100644 index 00000000..0e7dccc4 --- /dev/null +++ b/pkg/bbutil/env.go @@ -0,0 +1,112 @@ +package bbutil + +import ( + "os" + "path/filepath" + + "github.com/StackExchange/blackbox/pkg/bbgit" + "github.com/StackExchange/blackbox/pkg/bbnone" +) + +// Vcser is the interface that defines a plug-in VCS system. +type Vcser interface { + Name() string // Returns the name of this type. + RepoBaseDir() string // Returns the full path leading to this repo. +} + +// RepoInfo stores info about the current repository. +type RepoInfo struct { + Vcs Vcser + // BaseDir specifies the path (from "/") to the base of the VCS repo. + RepoBaseDir string // REPOBASE + BlackboxConfigDir string // BLACKBOXDATA + // KEYRINGDIR="$REPOBASE/$BLACKBOXDATA" + // BB_ADMINS_FILE="blackbox-admins.txt" + // BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}" + // SECRING="${KEYRINGDIR}/secring.gpg" +} + +// New is a factory. +func New() (*RepoInfo, error) { + repo := &RepoInfo{} + + vcs, err := vcsType() + if err != nil { + return nil, err + } + repo.Vcs = vcs + + // What is the base directory of the repo? + base := os.Getenv("BLACKBOX_REPOBASE") + if base == "" { + base = repo.Vcs.RepoBaseDir() + } + repo.RepoBaseDir = base + + // Where are the blackbox config files? + repo.BlackboxConfigDir, err = findConfigDir(repo.RepoBaseDir) + if err != nil { + return nil, err + } + + return repo, nil +} + +// vcsType discovers the VCS type based on the BB_VCSTYPE env variable or by probing. +func vcsType() (Vcser, error) { + switch vcsName := os.Getenv("BB_VCSTYPE"); vcsName { + case "git": + return bbgit.New() + // case "hg": + // case "svn": + default: + break + } + answer, err := bbgit.New() + if err == nil { + return answer, nil + } + // TODO(tlim): Should we print err? + // answer = bbhg.New() + // if answer != nil { + // return answer + // } + // answer = bbsvn.New() + // if answer != nil { + // return answer + // } + return bbnone.New() +} + +// If BLACKBOXDATA is not set, search list this of directory paths. +var configDirCandidates = []string{ + "keyrings/live", + ".blackbox", // Last item is the default. +} + +// findConfigDir returns the configuration directory. It first checks the +// BLACKBOXDATA env variable, then a list of candidates, lastly returning the +// last candidate as the default. +func findConfigDir(repoBase string) (string, error) { + if dir := os.Getenv("BLACKBOXDATA"); dir != "" { + //fmt.Fprintln(os.Stderr, "USING BBENV", dir) + return filepath.Join(repoBase, dir), nil + } + var p string + for _, c := range configDirCandidates { + p = filepath.Join(repoBase, c) + //fmt.Fprintf(os.Stderr, "Trying %q\n", p) + if st, err := os.Stat(p); err == nil { + mode := st.Mode() + if mode.IsDir() { + // FIXME(tlim): We are assuming that "not found" + // and "i/o error" are both reasons to skip the candidates. + // Maybe we should see what kind of error it is an output + // some diagnostics if the problem is more than just "no found"? + //fmt.Fprintf(os.Stderr, "RETURNING %q\n", p) + return p, nil + } + } + } + return p, nil +} diff --git a/pkg/bbutil/register.go b/pkg/bbutil/register.go new file mode 100644 index 00000000..88d6295a --- /dev/null +++ b/pkg/bbutil/register.go @@ -0,0 +1,62 @@ +package bbutil + +import ( + "io/ioutil" + "log" + "os" + "path/filepath" + "sort" + "strings" + + "github.com/pkg/errors" +) + +// RegFile is a description of a registered file. +type RegFile struct { + Name string +} + +// RegisteredFiles returns a list of the registered files. +func (bbu *RepoInfo) RegisteredFiles() ([]RegFile, error) { + blackboxFiles := filepath.Join(bbu.BlackboxConfigDir, "blackbox-files.txt") + d, err := ioutil.ReadFile(blackboxFiles) + if err != nil { + return nil, errors.Wrap(err, "Could not read the list of registered files") + } + + // remove a trailing \n. + // NB(tlim): We can't remove all trailing whitespace because filenames may contain whitespace. + s := strings.TrimSuffix(string(d), "\n") // remove a single newline. + + names := strings.Split(s, "\n") + if !sort.StringsAreSorted(names) { + log.Fatalf("Files list is corrupted. It is not sorted; %q", blackboxFiles) + } + r := make([]RegFile, len(names)) + for i, name := range names { + r[i].Name = name + } + + return r, nil +} + +// FileStatus returns the status of a file. +func FileStatus(basedir, file string) string { + p := filepath.Join(basedir, file) + e := p + ".gpg" + ps, perr := os.Stat(p) + es, eerr := os.Stat(e) + if perr == nil && eerr == nil { + if ps.ModTime().Before(es.ModTime()) { + return "ERROR_MOD" + } + return "EDITING" + } + if eerr == nil { + return "ENCRYPTED" + } + if os.IsExist(perr) { + return "ERROR_NOGPG" + } + return "ERROR_NOEXIST" +} From 568190313c76c4eb449cdde67907d96a88200ffb Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 21 Apr 2018 21:04:01 -0400 Subject: [PATCH 04/69] Add "blackbox admin nlist" command. --- cmd/blackbox/blackbox.go | 15 +++++++++++++-- cmd/blackbox/cmd_admin.go | 22 ++++++++++++++++++++++ pkg/bbutil/admin.go | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 cmd/blackbox/cmd_admin.go create mode 100644 pkg/bbutil/admin.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 24976b32..58f9ff80 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -2,7 +2,6 @@ package main import ( "fmt" - "log" "os" "github.com/urfave/cli" @@ -134,6 +133,17 @@ func main() { return RunBash("blackbox_list_admins", c.Args().First()) }, }, + { + Name: "nlist", + Usage: "Runs blackbox_list_admins", + Action: func(c *cli.Context) error { + if len(c.Args()) != 0 { + fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + return nil + } + return cmdAdminList() + }, + }, }, }, { @@ -191,6 +201,7 @@ func main() { err := app.Run(os.Args) if err != nil { - log.Fatal(err) + fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) + os.Exit(1) } } diff --git a/cmd/blackbox/cmd_admin.go b/cmd/blackbox/cmd_admin.go new file mode 100644 index 00000000..0be8ce76 --- /dev/null +++ b/cmd/blackbox/cmd_admin.go @@ -0,0 +1,22 @@ +package main + +import ( + "fmt" + + "github.com/StackExchange/blackbox/pkg/bbutil" +) + +func cmdAdminList() error { + bbu, err := bbutil.New() + if err != nil { + return err + } + names, err := bbu.Administrators() + if err != nil { + return err + } + for _, item := range names { + fmt.Println(item.Name) + } + return nil +} diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go new file mode 100644 index 00000000..8917ec22 --- /dev/null +++ b/pkg/bbutil/admin.go @@ -0,0 +1,38 @@ +package bbutil + +import ( + "io/ioutil" + "log" + "path/filepath" + "sort" + "strings" + + "github.com/pkg/errors" +) + +// Administrator is a description of the admininstrators. +type Administrator struct { + Name string +} + +// Administrators returns the list of administrators. +func (bbu *RepoInfo) Administrators() ([]Administrator, error) { + adminFilename := filepath.Join(bbu.BlackboxConfigDir, "blackbox-admins.txt") + d, err := ioutil.ReadFile(adminFilename) + if err != nil { + return nil, errors.Wrap(err, "Could not read the list of administrators") + } + + // remove a trailing \n. + s := strings.TrimSuffix(string(d), "\n") // remove a single newline. + names := strings.Split(s, "\n") + if !sort.StringsAreSorted(names) { + log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) + } + r := make([]Administrator, len(names)) + for i, name := range names { + r[i].Name = name + } + + return r, nil +} From d393d7e2e2f5d0fceb191ce6edcb9120c4918e65 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 21 Apr 2018 23:19:24 -0400 Subject: [PATCH 05/69] First draft of ndecrypt --- cmd/blackbox/blackbox.go | 40 ++++++++++++++++++++-- cmd/blackbox/cmd_decrypt.go | 32 +++++++++++++++++ cmd/blackbox/cmd_list.go | 22 ------------ cmd/blackbox/{cmd_status.go => cmd_reg.go} | 17 ++++++++- pkg/bbutil/decrypt.go | 32 +++++++++++++++++ pkg/bbutil/iterator.go | 36 +++++++++++++++++++ pkg/bbutil/{register.go => reg.go} | 0 7 files changed, 154 insertions(+), 25 deletions(-) create mode 100644 cmd/blackbox/cmd_decrypt.go delete mode 100644 cmd/blackbox/cmd_list.go rename cmd/blackbox/{cmd_status.go => cmd_reg.go} (57%) create mode 100644 pkg/bbutil/decrypt.go create mode 100644 pkg/bbutil/iterator.go rename pkg/bbutil/{register.go => reg.go} (100%) diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 58f9ff80..a6bf0486 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -1,6 +1,7 @@ package main import ( + "errors" "fmt" "os" @@ -64,6 +65,41 @@ func main() { return RunBash("blackbox_edit_start", c.Args().First()) }, }, + { + Name: "ndecrypt", + Aliases: []string{"de", "start"}, + Usage: "Runs blackbox_edit_start", + Flags: []cli.Flag{ + cli.BoolFlag{ + Name: "all, a", + Usage: "All registered files", + }, + cli.BoolFlag{ + Name: "non-interactive, b", + Usage: "Do not set up gpg-agent", + }, + cli.StringFlag{ + Name: "set-group, g", + Usage: "Set group ownership", + }, + }, + Action: func(c *cli.Context) error { + if !c.Bool("non-interactive") { + // gpg_agent_notice + } + if c.Bool("all") { + if len(c.Args()) != 0 { + return errors.New("Can't combined --all and filenames") + } + } else { + if len(c.Args()) == 0 { + return errors.New("At least one filename required (or --all)") + } + + } + return cmdDecrypt(c.Bool("all"), c.Args(), c.String("set-group")) + }, + }, { Name: "encrypt", Aliases: []string{"en", "end"}, @@ -181,7 +217,7 @@ func main() { fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") return nil } - return cmdList() + return cmdRegList() }, }, { @@ -192,7 +228,7 @@ func main() { fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") return nil } - return cmdStatus() + return cmdRegStatus() }, }, }, diff --git a/cmd/blackbox/cmd_decrypt.go b/cmd/blackbox/cmd_decrypt.go new file mode 100644 index 00000000..f0cac373 --- /dev/null +++ b/cmd/blackbox/cmd_decrypt.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "os" + + "github.com/StackExchange/blackbox/pkg/bbutil" + "github.com/pkg/errors" +) + +func cmdDecrypt(allFiles bool, filenames []string, group string) error { + bbu, err := bbutil.New() + if err != nil { + return err + } + + // prepare_keychain + + fnames, valid, err := bbu.FileIterator(allFiles, filenames) + if err != nil { + return errors.Wrap(err, "decrypt") + } + for i, filename := range fnames { + if valid[i] { + bbu.DecryptFile(filename, group, true) + } else { + fmt.Fprintf(os.Stderr, "SKIPPING: %q\n", filename) + } + } + + return nil +} diff --git a/cmd/blackbox/cmd_list.go b/cmd/blackbox/cmd_list.go deleted file mode 100644 index 7dbd02b5..00000000 --- a/cmd/blackbox/cmd_list.go +++ /dev/null @@ -1,22 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/StackExchange/blackbox/pkg/bbutil" -) - -func cmdList() error { - bbu, err := bbutil.New() - if err != nil { - return err - } - names, err := bbu.RegisteredFiles() - if err != nil { - return err - } - for _, item := range names { - fmt.Println(item.Name) - } - return nil -} diff --git a/cmd/blackbox/cmd_status.go b/cmd/blackbox/cmd_reg.go similarity index 57% rename from cmd/blackbox/cmd_status.go rename to cmd/blackbox/cmd_reg.go index f6bf70d1..01b2a2ee 100644 --- a/cmd/blackbox/cmd_status.go +++ b/cmd/blackbox/cmd_reg.go @@ -6,7 +6,22 @@ import ( "github.com/StackExchange/blackbox/pkg/bbutil" ) -func cmdStatus() error { +func cmdRegList() error { + bbu, err := bbutil.New() + if err != nil { + return err + } + names, err := bbu.RegisteredFiles() + if err != nil { + return err + } + for _, item := range names { + fmt.Println(item.Name) + } + return nil +} + +func cmdRegStatus() error { bbu, err := bbutil.New() if err != nil { return err diff --git a/pkg/bbutil/decrypt.go b/pkg/bbutil/decrypt.go new file mode 100644 index 00000000..7c092d4f --- /dev/null +++ b/pkg/bbutil/decrypt.go @@ -0,0 +1,32 @@ +package bbutil + +import ( + "fmt" + "os" +) + +// DecryptFile decrypts a single file. +func (bbu *RepoInfo) DecryptFile(filename, group string, overwrite bool) error { + + // change_to_vcs_root + + fmt.Fprintf(os.Stderr, "WOULD DECRYPT: %v %q %q\n", overwrite, group, filename) + + // export PATH=/usr/bin:/bin:"$PATH" + + // # Decrypt: + // echo '========== Decrypting new/changed files: START' + // while IFS= read <&99 -r unencrypted_file; do + // encrypted_file=$(get_encrypted_filename "$unencrypted_file") + // decrypt_file_overwrite "$encrypted_file" "$unencrypted_file" + // cp_permissions "$encrypted_file" "$unencrypted_file" + // if [[ ! -z "$FILE_GROUP" ]]; then + // chmod g+r "$unencrypted_file" + // chgrp "$FILE_GROUP" "$unencrypted_file" + // fi + // done 99<"$BB_FILES" + + // echo '========== Decrypting new/changed files: DONE' + + return nil +} diff --git a/pkg/bbutil/iterator.go b/pkg/bbutil/iterator.go new file mode 100644 index 00000000..b43f1b53 --- /dev/null +++ b/pkg/bbutil/iterator.go @@ -0,0 +1,36 @@ +package bbutil + +import ( + "sort" +) + +// FileIterator return a list of files to process. +func (bbu *RepoInfo) FileIterator(allFiles bool, fnames []string) ([]string, []bool, error) { + regfiles, err := bbu.RegisteredFiles() + if err != nil { + return nil, nil, err + } + + allnames := make([]string, len(regfiles)) + for i, r := range regfiles { + allnames[i] = r.Name + } + + if allFiles { + isvalid := make([]bool, len(allnames)) + for n := range allnames { + isvalid[n] = true + } + return allnames, isvalid, nil + } + + retnames := make([]string, len(fnames)) + isvalid := make([]bool, len(fnames)) + for n, fn := range fnames { + retnames[n] = fn + i := sort.SearchStrings(allnames, fn) + isvalid[n] = i < len(allnames) && allnames[i] == fn + } + + return retnames, isvalid, nil +} diff --git a/pkg/bbutil/register.go b/pkg/bbutil/reg.go similarity index 100% rename from pkg/bbutil/register.go rename to pkg/bbutil/reg.go From 3cc7e46c7d64e351d582809d363cd5e29dbffa37 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 10 Sep 2018 14:52:33 -0400 Subject: [PATCH 06/69] Partial commit --- cmd/blackbox/blackbox.go | 25 +++++++++++++++++++++---- cmd/blackbox/cmd_admin.go | 2 ++ pkg/bbutil/admin.go | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 4 deletions(-) diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index a6bf0486..60ebbb90 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -150,6 +150,23 @@ func main() { Subcommands: []cli.Command{ { Name: "add", + Usage: "Adds to blackbox admins", + Action: func(c *cli.Context) error { + if len(c.Args()) != 0 { + fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") + return nil + } + return cmdAdminList() + }, + { + Name: "add", + Usage: "Adds adminstrator(s)", + Action: func(c *cli.Context) error { + return RunBash("blackbox_addadmin", c.Args().First()) + }, + }, + { + Name: "oadd", Usage: "Runs blackbox_addadmin", Action: func(c *cli.Context) error { return RunBash("blackbox_addadmin", c.Args().First()) @@ -163,18 +180,18 @@ func main() { }, }, { - Name: "list", + Name: "olist", Usage: "Runs blackbox_list_admins", Action: func(c *cli.Context) error { return RunBash("blackbox_list_admins", c.Args().First()) }, }, { - Name: "nlist", - Usage: "Runs blackbox_list_admins", + Name: "list", + Usage: "Lists blackbox admins", Action: func(c *cli.Context) error { if len(c.Args()) != 0 { - fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") return nil } return cmdAdminList() diff --git a/cmd/blackbox/cmd_admin.go b/cmd/blackbox/cmd_admin.go index 0be8ce76..6357165e 100644 --- a/cmd/blackbox/cmd_admin.go +++ b/cmd/blackbox/cmd_admin.go @@ -1,5 +1,7 @@ package main +// All the "blackbox admin" subcommands. + import ( "fmt" diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go index 8917ec22..11d358ae 100644 --- a/pkg/bbutil/admin.go +++ b/pkg/bbutil/admin.go @@ -36,3 +36,28 @@ func (bbu *RepoInfo) Administrators() ([]Administrator, error) { return r, nil } + +// AddAdministrators adds administrators by email address. +func (bbu *RepoInfo) AddAdministrators([]string) (error) { + // If doesn't exist, create it. + :q + + adminFilename := filepath.Join(bbu.BlackboxConfigDir, "blackbox-admins.txt") + d, err := ioutil.ReadFile(adminFilename) + if err != nil { + return nil, errors.Wrap(err, "Could not read the list of administrators") + } + + // remove a trailing \n. + s := strings.TrimSuffix(string(d), "\n") // remove a single newline. + names := strings.Split(s, "\n") + if !sort.StringsAreSorted(names) { + log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) + } + r := make([]Administrator, len(names)) + for i, name := range names { + r[i].Name = name + } + + return r, nil +} From ffde3aee31edf05a0de2a8d2a1caf38726579ed9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 9 Dec 2018 14:22:02 -0500 Subject: [PATCH 07/69] Restructure --- cmd/blackbox/blackbox.go | 179 ++++++++++------------------- cmd/blackbox/cmd_admin.go | 9 +- cmd/blackbox/cmd_info.go | 31 +++++ cmd/blackbox/cmd_reg.go | 16 ++- docs/CODE-LAYOUT.md | 58 ++++++++++ integrationTest/README.txt | 55 +++++++++ pkg/bbutil/admin.go | 60 ++-------- pkg/bbutil/adminplain.go | 62 ++++++++++ pkg/bbutil/iterator.go | 1 + pkg/bbutil/reg.go | 2 +- pkg/bbutil/{env.go => repoinfo.go} | 2 +- 11 files changed, 302 insertions(+), 173 deletions(-) create mode 100644 cmd/blackbox/cmd_info.go create mode 100644 docs/CODE-LAYOUT.md create mode 100644 integrationTest/README.txt create mode 100644 pkg/bbutil/adminplain.go rename pkg/bbutil/{env.go => repoinfo.go} (97%) diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 60ebbb90..b51f9d16 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -24,36 +24,33 @@ func main() { } app.Commands = []cli.Command{ + { + Name: "info", + Category: "DEBUG", + Usage: "Report what we know about this repo", + Action: func(c *cli.Context) error { return cmdInfo(c) }, + }, + { Name: "initialize", Aliases: []string{"init"}, Category: "GETTING STARTED", Usage: "Runs blackbox_initialize", - Action: func(c *cli.Context) error { - return RunBash("blackbox_initialize", c.Args().First()) - }, + Action: func(c *cli.Context) error { return RunBash("blackbox_initialize", c.Args().First()) }, }, { Name: "edit", Aliases: []string{"e", "ed"}, Usage: "Runs blackbox_edit ", - Action: func(c *cli.Context) error { - return RunBash("blackbox_edit", c.Args().First()) - }, + Action: func(c *cli.Context) error { return RunBash("blackbox_edit", c.Args().First()) }, }, { Name: "decrypt", Aliases: []string{"de", "start"}, Usage: "Runs blackbox_edit_start", Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "all, a", - Usage: "Runs blackbox_decrypt_all_files", - }, - cli.BoolFlag{ - Name: "non-interactive", - Usage: "Runs blackbox_postdeploy", - }, + cli.BoolFlag{Name: "all, a", Usage: "Runs blackbox_decrypt_all_files"}, + cli.BoolFlag{Name: "non-interactive", Usage: "Runs blackbox_postdeploy"}, }, Action: func(c *cli.Context) error { if c.Bool("all") { @@ -95,7 +92,6 @@ func main() { if len(c.Args()) == 0 { return errors.New("At least one filename required (or --all)") } - } return cmdDecrypt(c.Bool("all"), c.Args(), c.String("set-group")) }, @@ -104,44 +100,32 @@ func main() { Name: "encrypt", Aliases: []string{"en", "end"}, Usage: "Runs blackbox_edit_end", - Action: func(c *cli.Context) error { - return RunBash("blackbox_edit_end", c.Args().First()) - }, + Action: func(c *cli.Context) error { return RunBash("blackbox_edit_end", c.Args().First()) }, }, { - Name: "reencrypt", - Usage: "Runs blackbox_update_all_files", - Action: func(c *cli.Context) error { - return RunBash("blackbox_update_all_files", c.Args().First()) - }, + Name: "reencrypt", + Usage: "Runs blackbox_update_all_files", + Action: func(c *cli.Context) error { return RunBash("blackbox_update_all_files", c.Args().First()) }, }, { - Name: "cat", - Usage: "Runs blackbox_cat", - Action: func(c *cli.Context) error { - return RunBash("blackbox_cat", c.Args().First()) - }, + Name: "cat", + Usage: "Runs blackbox_cat", + Action: func(c *cli.Context) error { return RunBash("blackbox_cat", c.Args().First()) }, }, { - Name: "diff", - Usage: "Runs blackbox_diff", - Action: func(c *cli.Context) error { - return RunBash("blackbox_diff", c.Args().First()) - }, + Name: "diff", + Usage: "Runs blackbox_diff", + Action: func(c *cli.Context) error { return RunBash("blackbox_diff", c.Args().First()) }, }, { - Name: "shredall", - Usage: "Runs blackbox_shred_all_files", - Action: func(c *cli.Context) error { - return RunBash("blackbox_shred_all_files", c.Args().First()) - }, + Name: "shredall", + Usage: "Runs blackbox_shred_all_files", + Action: func(c *cli.Context) error { return RunBash("blackbox_shred_all_files", c.Args().First()) }, }, { - Name: "whatsnew", - Usage: "Runs blackbox_whatsnew", - Action: func(c *cli.Context) error { - return RunBash("blackbox_whatsnew", c.Args().First()) - }, + Name: "whatsnew", + Usage: "Runs blackbox_whatsnew", + Action: func(c *cli.Context) error { return RunBash("blackbox_whatsnew", c.Args().First()) }, }, { Name: "admin", @@ -149,104 +133,65 @@ func main() { Usage: "Maintain the list of administrators", Subcommands: []cli.Command{ { - Name: "add", - Usage: "Adds to blackbox admins", - Action: func(c *cli.Context) error { - if len(c.Args()) != 0 { - fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") - return nil - } - return cmdAdminList() - }, - { - Name: "add", - Usage: "Adds adminstrator(s)", - Action: func(c *cli.Context) error { - return RunBash("blackbox_addadmin", c.Args().First()) - }, + Name: "nadd", + Aliases: []string{"add"}, + Usage: "Adds adminstrator(s)", + Action: func(c *cli.Context) error { return RunBash("blackbox_addadmin", c.Args().First()) }, }, { - Name: "oadd", - Usage: "Runs blackbox_addadmin", - Action: func(c *cli.Context) error { - return RunBash("blackbox_addadmin", c.Args().First()) - }, + Name: "oadd", + Usage: "Runs blackbox_addadmin", + Action: func(c *cli.Context) error { return RunBash("blackbox_addadmin", c.Args().First()) }, }, { - Name: "remove", - Usage: "Runs blackbox_removeadmin", - Action: func(c *cli.Context) error { - return RunBash("blackbox_removeadmin", c.Args().First()) - }, + Name: "remove", + Usage: "Runs blackbox_removeadmin", + Action: func(c *cli.Context) error { return RunBash("blackbox_removeadmin", c.Args().First()) }, }, { - Name: "olist", - Usage: "Runs blackbox_list_admins", - Action: func(c *cli.Context) error { - return RunBash("blackbox_list_admins", c.Args().First()) - }, + Name: "olist", + Usage: "Runs blackbox_list_admins", + Action: func(c *cli.Context) error { return RunBash("blackbox_list_admins", c.Args().First()) }, }, { - Name: "list", - Usage: "Lists blackbox admins", - Action: func(c *cli.Context) error { - if len(c.Args()) != 0 { - fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") - return nil - } - return cmdAdminList() - }, + Name: "nlist", + Aliases: []string{"list"}, + Usage: "Lists blackbox admins", + Action: func(c *cli.Context) error { return cmdAdminList(c) }, }, }, }, { - Name: "register", - Aliases: []string{"reg"}, + Name: "file", + Aliases: []string{"f"}, Category: "ADMINISTRATIVE", Usage: "Maintain the list of files", Subcommands: []cli.Command{ { - Name: "add", - Usage: "Runs blackbox_register_new_file", - Action: func(c *cli.Context) error { - return RunBash("blackbox_register_new_file", c.Args().First()) - }, + Name: "add", + Usage: "Runs blackbox_register_new_file", + Action: func(c *cli.Context) error { return RunBash("blackbox_register_new_file", c.Args().First()) }, }, { - Name: "remove", - Usage: "Runs blackbox_deregister_file", - Action: func(c *cli.Context) error { - return RunBash("blackbox_deregister_file", c.Args().First()) - }, + Name: "remove", + Usage: "Runs blackbox_deregister_file", + Action: func(c *cli.Context) error { return RunBash("blackbox_deregister_file", c.Args().First()) }, }, { - Name: "list", - Usage: "Runs blackbox_list_admins", - Action: func(c *cli.Context) error { - return RunBash("blackbox_list_files", c.Args().First()) - }, + Name: "olist", + Usage: "Runs blackbox_list_admins", + Action: func(c *cli.Context) error { return RunBash("blackbox_list_files", c.Args().First()) }, }, { - Name: "nlist", - Usage: "Lists the registered files", - Action: func(c *cli.Context) error { - if len(c.Args()) != 0 { - fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") - return nil - } - return cmdRegList() - }, + Name: "nlist", + Aliases: []string{"list"}, + Usage: "Lists the registered files", + Action: func(c *cli.Context) error { return cmdRegList(c) }, }, { - Name: "status", - Usage: "Prints info about registered files", - Action: func(c *cli.Context) error { - if len(c.Args()) != 0 { - fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") - return nil - } - return cmdRegStatus() - }, + Name: "status", + Usage: "Prints info about registered files", + Action: func(c *cli.Context) error { return cmdRegStatus(c) }, }, }, }, diff --git a/cmd/blackbox/cmd_admin.go b/cmd/blackbox/cmd_admin.go index 6357165e..30ac0eab 100644 --- a/cmd/blackbox/cmd_admin.go +++ b/cmd/blackbox/cmd_admin.go @@ -6,9 +6,16 @@ import ( "fmt" "github.com/StackExchange/blackbox/pkg/bbutil" + "github.com/urfave/cli" ) -func cmdAdminList() error { +func cmdAdminList(c *cli.Context) error { + + if len(c.Args()) != 0 { + fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") + return nil + } + bbu, err := bbutil.New() if err != nil { return err diff --git a/cmd/blackbox/cmd_info.go b/cmd/blackbox/cmd_info.go new file mode 100644 index 00000000..9fa96772 --- /dev/null +++ b/cmd/blackbox/cmd_info.go @@ -0,0 +1,31 @@ +package main + +// All the "blackbox admin" subcommands. + +import ( + "fmt" + + "github.com/StackExchange/blackbox/pkg/bbutil" + "github.com/urfave/cli" +) + +func cmdInfo(c *cli.Context) error { + + // GPG version + // VCS name + // keys directory + + bbu, err := bbutil.New() + if err != nil { + return err + } + + fmt.Print("VCS:\n") + fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) + fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) + fmt.Print("REPO:\n") + fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) + fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) + + return nil +} diff --git a/cmd/blackbox/cmd_reg.go b/cmd/blackbox/cmd_reg.go index 01b2a2ee..e6ed14de 100644 --- a/cmd/blackbox/cmd_reg.go +++ b/cmd/blackbox/cmd_reg.go @@ -4,9 +4,15 @@ import ( "fmt" "github.com/StackExchange/blackbox/pkg/bbutil" + "github.com/urfave/cli" ) -func cmdRegList() error { +func cmdRegList(c *cli.Context) error { + if len(c.Args()) != 0 { + fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + return nil + } + bbu, err := bbutil.New() if err != nil { return err @@ -21,7 +27,13 @@ func cmdRegList() error { return nil } -func cmdRegStatus() error { +func cmdRegStatus(c *cli.Context) error { + + if len(c.Args()) != 0 { + fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") + return nil + } + bbu, err := bbutil.New() if err != nil { return err diff --git a/docs/CODE-LAYOUT.md b/docs/CODE-LAYOUT.md new file mode 100644 index 00000000..3fa283d5 --- /dev/null +++ b/docs/CODE-LAYOUT.md @@ -0,0 +1,58 @@ + + +# External access + +blackbox.go + - cmd\_\*() -- Validate command line and call op\_\*() functions to do the work. + - This code deals with all command line flags and ENV variables. + - Prints status messages, errors, warns, etc. + - Calls bbutil.go op\_() operations exclusively. + - Determines VCS in use, GPG version in use. + +pkg/bbutil.go: + - op\_\*() -- Perform operations (can be used from other Go code) + - This code is ignorant of flags and ENV variables. + - This code is silent. Never prints to stdio/stderr. Returns errors for parent to print. + - Decrypt(filename) + - CopyPermissions(src, dst) + - EncryptedFilename(plainfilename string) (encryptedfilename string) + - UnencryptedFilename(plainfilename string) (encryptedfilename string) + +file-level access + +pkg/admin: + - main.go -- generic admin manager + - ListAdmins() + - AddAdmins() + - RemoveAdmins() + - ListFiles() + - FileStatus() + - FileStatusAll() + - IsOnFilelist() + - IsNotOnFilelist() + - plain.go -- read/write blackbox-admins.txt + - listAdminsPlain() + - addAdminsPlain() + - removeAdminsPlain() + - listFilesPlain() + - fileStatusPlain() + - fileStatusAllPlain() + - FUTURE: a json equivalent of each plain function. Functions in main.go decide which to call. + +crypto functions + +models.go: + interface for gpg. + - Decrypt(encrypted, unencrypted) error + - Encrypt(unencrypted, encrypted) error + - GetPubKey(dirname, keyname) (pubkey string) + - ImportPubKey(dirname, pubkey) touchedFiles []string +cryptcmd/bb\_gpg\_v1.go: + - struct Gpgv1 +cryptcmd/bb\_gpg\_v2.go: + - struct Gpgv2 + +vcs/models.go + interface for talk with VCS systems. +vcs/gpgv1/ +vcs/gpgv2/ diff --git a/integrationTest/README.txt b/integrationTest/README.txt new file mode 100644 index 00000000..7c81bb14 --- /dev/null +++ b/integrationTest/README.txt @@ -0,0 +1,55 @@ + +Each test does the following: +1. Copy the files from testdata/NNNN +2. Run the command in test_NNNN.sh +3. + + +TEST ENROLLMENT: + +PHASE 'Alice creates a repo. She creates secret.txt.' +PHASE 'Alice wants to be part of the secret system.' +PHASE 'She creates a GPG key...' +PHASE 'Initializes BB...' +PHASE 'and adds herself as an admin.' +PHASE 'Bob arrives.' +PHASE 'Bob creates a gpg key.' +PHASE 'Alice does the second part to enroll bob.' +PHASE 'She enrolls bob.' +PHASE 'She enrolls secrets.txt.' +PHASE 'She decrypts secrets.txt.' +PHASE 'She edits secrets.txt.' +PHASE 'Alice copies files to a non-repo directory. (NO REPO)' +PHASE 'Alice shreds these non-repo files. (NO REPO)' +PHASE 'Alice decrypts secrets.txt (NO REPO).' +PHASE 'Alice edits secrets.txt. (NO REPO EDIT)' +PHASE 'Alice decrypts secrets.txt (NO REPO EDIT).' +PHASE 'appears.' +#PHASE 'Bob makes sure he has all new keys.' + +TEST INDIVIDUAL COMMANDS: + +PHASE 'Bob postdeploys... default.' +PHASE 'Bob postdeploys... with a GID.' +PHASE 'Bob cleans up the secret.' +PHASE 'Bob removes Alice.' +PHASE 'Bob reencrypts files so alice can not access them.' +PHASE 'Bob decrypts secrets.txt.' +PHASE 'Bob edits secrets.txt.' +PHASE 'Bob decrypts secrets.txt VERSION 3.' +PHASE 'Bob exposes a secret in the repo.' +PHASE 'Bob corrects it by registering it.' +PHASE 'Bob enrolls my/path/to/relsecrets.txt.' +PHASE 'Bob decrypts relsecrets.txt.' +PHASE 'Bob enrolls !important!.txt' +PHASE 'Bob enrolls #andpounds.txt' +PHASE 'Bob enrolls stars*bars?.txt' +PHASE 'Bob enrolls space space.txt' +PHASE 'Bob checks out stars*bars?.txt.' +PHASE 'Bob checks out space space.txt.' +PHASE 'Bob shreds all exposed files.' +PHASE 'Bob updates all files.' +PHASE 'Bob DEregisters mistake.txt' +PHASE 'Bob enrolls multiple files: multi1.txt and multi2.txt' +PHASE 'Alice returns. She should be locked out' +PHASE 'Alice tries to decrypt secret.txt. Is blocked.' diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go index 11d358ae..23585bb9 100644 --- a/pkg/bbutil/admin.go +++ b/pkg/bbutil/admin.go @@ -1,63 +1,21 @@ package bbutil -import ( - "io/ioutil" - "log" - "path/filepath" - "sort" - "strings" - - "github.com/pkg/errors" -) - // Administrator is a description of the admininstrators. type Administrator struct { Name string } -// Administrators returns the list of administrators. +// Administrators returns the administrators of this repo. func (bbu *RepoInfo) Administrators() ([]Administrator, error) { - adminFilename := filepath.Join(bbu.BlackboxConfigDir, "blackbox-admins.txt") - d, err := ioutil.ReadFile(adminFilename) - if err != nil { - return nil, errors.Wrap(err, "Could not read the list of administrators") - } - - // remove a trailing \n. - s := strings.TrimSuffix(string(d), "\n") // remove a single newline. - names := strings.Split(s, "\n") - if !sort.StringsAreSorted(names) { - log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) - } - r := make([]Administrator, len(names)) - for i, name := range names { - r[i].Name = name - } - - return r, nil + return plainListAdmins(bbu.BlackboxConfigDir) } -// AddAdministrators adds administrators by email address. -func (bbu *RepoInfo) AddAdministrators([]string) (error) { - // If doesn't exist, create it. - :q - - adminFilename := filepath.Join(bbu.BlackboxConfigDir, "blackbox-admins.txt") - d, err := ioutil.ReadFile(adminFilename) - if err != nil { - return nil, errors.Wrap(err, "Could not read the list of administrators") - } - - // remove a trailing \n. - s := strings.TrimSuffix(string(d), "\n") // remove a single newline. - names := strings.Split(s, "\n") - if !sort.StringsAreSorted(names) { - log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) - } - r := make([]Administrator, len(names)) - for i, name := range names { - r[i].Name = name - } +// AddAdmin adds an administrator to this repo. +func (bbu *RepoInfo) AddAdmin(admin Administrator) error { + return plainAddAdmin(bbu.BlackboxConfigDir, admin) +} - return r, nil +// RemoveAdminByName removes an administrator from this repo. +func (bbu *RepoInfo) RemoveAdminByName(admin Administrator) error { + return plainRemoveAdmin(bbu.BlackboxConfigDir, admin) } diff --git a/pkg/bbutil/adminplain.go b/pkg/bbutil/adminplain.go new file mode 100644 index 00000000..cc36c3a2 --- /dev/null +++ b/pkg/bbutil/adminplain.go @@ -0,0 +1,62 @@ +package bbutil + +import ( + "io/ioutil" + "log" + "path/filepath" + "sort" + "strings" + + "github.com/pkg/errors" +) + +func plainAdminsFile(dir string) string { + return filepath.Join(dir, "blackbox-admins.txt") +} + +// Administrators returns the list of administrators. +func plainListAdmins(dir string) ([]Administrator, error) { + adminFilename := plainAdminsFile(dir) + d, err := ioutil.ReadFile(adminFilename) + if err != nil { + return nil, errors.Wrap(err, "Could not read the list of administrators") + } + + // remove a trailing \n. + s := strings.TrimSuffix(string(d), "\n") // remove a single newline. + names := strings.Split(s, "\n") + if !sort.StringsAreSorted(names) { + log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) + } + r := make([]Administrator, len(names)) + for i, name := range names { + r[i].Name = name + } + + return r, nil +} + +// plainWriteAdmins rewrites the admins file. +func plainWriteAdmins(dir string, admins []Administrator) error { + return errors.New("UNIMPLEMENTED") +} + +// plainAddAdmins adds one administrator by email address. +func plainAddAdmin(dir string, admin Administrator) error { + admins, err := plainListAdmins(dir) + if err != nil { + return err + } + + // Add it to the list, sort it into position. + admins = append(admins, admin) + sort.Slice(admins, func(i, j int) bool { + return admins[i].Name < admins[j].Name + }) + + return plainWriteAdmins(dir, admins) +} + +func plainRemoveAdmin(dir string, admin Administrator) error { + return errors.New("UNIMPLEMENTED") +} diff --git a/pkg/bbutil/iterator.go b/pkg/bbutil/iterator.go index b43f1b53..863b3d78 100644 --- a/pkg/bbutil/iterator.go +++ b/pkg/bbutil/iterator.go @@ -5,6 +5,7 @@ import ( ) // FileIterator return a list of files to process. + func (bbu *RepoInfo) FileIterator(allFiles bool, fnames []string) ([]string, []bool, error) { regfiles, err := bbu.RegisteredFiles() if err != nil { diff --git a/pkg/bbutil/reg.go b/pkg/bbutil/reg.go index 88d6295a..e9c4289b 100644 --- a/pkg/bbutil/reg.go +++ b/pkg/bbutil/reg.go @@ -48,7 +48,7 @@ func FileStatus(basedir, file string) string { es, eerr := os.Stat(e) if perr == nil && eerr == nil { if ps.ModTime().Before(es.ModTime()) { - return "ERROR_MOD" + return "GPGNEWER" } return "EDITING" } diff --git a/pkg/bbutil/env.go b/pkg/bbutil/repoinfo.go similarity index 97% rename from pkg/bbutil/env.go rename to pkg/bbutil/repoinfo.go index 0e7dccc4..8da87385 100644 --- a/pkg/bbutil/env.go +++ b/pkg/bbutil/repoinfo.go @@ -10,7 +10,7 @@ import ( // Vcser is the interface that defines a plug-in VCS system. type Vcser interface { - Name() string // Returns the name of this type. + Name() string // Returns the name of this VCS type. RepoBaseDir() string // Returns the full path leading to this repo. } From a5cd82962dd9fb9a189a07eeba02a908bab8b3f2 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 18 Dec 2018 18:01:25 -0500 Subject: [PATCH 08/69] wip! --- docs/CODE-LAYOUT.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/CODE-LAYOUT.md b/docs/CODE-LAYOUT.md index 3fa283d5..0d88ff74 100644 --- a/docs/CODE-LAYOUT.md +++ b/docs/CODE-LAYOUT.md @@ -18,10 +18,7 @@ pkg/bbutil.go: - EncryptedFilename(plainfilename string) (encryptedfilename string) - UnencryptedFilename(plainfilename string) (encryptedfilename string) -file-level access - -pkg/admin: - - main.go -- generic admin manager + - admin.go -- generic admin manager - ListAdmins() - AddAdmins() - RemoveAdmins() @@ -30,7 +27,7 @@ pkg/admin: - FileStatusAll() - IsOnFilelist() - IsNotOnFilelist() - - plain.go -- read/write blackbox-admins.txt + - adminplain.go -- read/write blackbox-admins.txt - listAdminsPlain() - addAdminsPlain() - removeAdminsPlain() From 1fb57013170b02ab43bf083ee97ddf81de1f54d5 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 19 Apr 2020 07:56:28 -0400 Subject: [PATCH 09/69] NEW: DESIGN.md --- DESIGN.md | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 DESIGN.md diff --git a/DESIGN.md b/DESIGN.md new file mode 100644 index 00000000..c4ae6623 --- /dev/null +++ b/DESIGN.md @@ -0,0 +1,41 @@ +BlackBox Internals +================== + +The goal of the Go rewrite is to improve the usability and +maintainability of Blackbox, meanwhile make it easier to implement new + +The system is built in distinct layers: view, controller, model. + +Suppose there is a subcommand "`foo`". `blackbox.go` parses the +user's command line args and calls `cmdFoo()`, which is given +everything it needs to do the operation. For example, it is given the +filenames the user specified exactly; even if an empty list means "all +files", at this layer the empty list is passed to the function. + +`cmdFoo()` contains the business logic of how the operation should be +done: usually iterating over filenames and calling verb(s) for each +one. For example if an empty file list means "all files", this is the +layer that enumerates the files. + +`cmdFoo()` is implemented in the file `cmd_foo.go`. The caller of +`cmdFoo()` should provide all data it needs to get the job done. +`cmdFoo()` doesn't refer to global flags, they are passed to the +function as parameters. Therefore the function has zero side-effects +(except possibly logging) and can be called as library functions by +other systems. This is the external (binary) API which should be +relatively stable. + +`cmdFoo()` calls verbs that are in `bbutil/`. Some of those verbs are +actually interfaces. For example, any VCS-related verbs are actually a +Go interface which might be implemented one of many ways (Git, +Subversion, Mercurial), GPG-functions may be implemented by shelling +out to `gpg.exe` or by using Go's gpg library. + +They layers look like this: + +| View | `blackbox.go` | Parses User Commands, calls controller | +| Controller | `cmd_*.go` | The business logic. Iterates and calls verbs | +| Model | `pkg/bbutil` | Verbs | +| Interfaces | `pkg/*` | Interfaces and their implementations | + +At least that's the goal. We'll see how well we can achieve this. From c9bd5872f6220c321720979bdda70fa63aec433a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 10:16:00 -0400 Subject: [PATCH 10/69] tmp --- cmd/blackbox/blackbox.go | 190 +- cmd/blackbox/cmd_admin.go | 31 - cmd/blackbox/cmd_decrypt.go | 32 - cmd/blackbox/cmd_info.go | 31 - cmd/blackbox/cmd_reg.go | 51 - cmd/blackbox/flags.go | 183 ++ cmd/blackbox/parse.go | 227 +++ cmd/blackbox/runbash.go | 55 +- pkg/bbgit/discover.go | 75 +- pkg/bbutil/admin.go | 42 +- pkg/bbutil/adminplain.go | 123 +- pkg/bbutil/decrypt.go | 64 +- pkg/bbutil/iterator.go | 74 +- pkg/bbutil/reg.go | 123 +- pkg/bbutil/repoinfo.go | 224 +-- pkg/box/box.go | 32 + vendor/github.com/pkg/errors/LICENSE | 23 - vendor/github.com/pkg/errors/README.md | 52 - vendor/github.com/pkg/errors/appveyor.yml | 32 - vendor/github.com/pkg/errors/errors.go | 269 --- vendor/github.com/pkg/errors/stack.go | 147 -- vendor/github.com/urfave/cli/CHANGELOG.md | 435 ----- .../github.com/urfave/cli/CODE_OF_CONDUCT.md | 74 - vendor/github.com/urfave/cli/CONTRIBUTING.md | 19 - vendor/github.com/urfave/cli/LICENSE | 21 - vendor/github.com/urfave/cli/MAINTAINERS.md | 1 - vendor/github.com/urfave/cli/README.md | 1526 ----------------- vendor/github.com/urfave/cli/app.go | 508 ------ vendor/github.com/urfave/cli/appveyor.yml | 26 - vendor/github.com/urfave/cli/category.go | 44 - vendor/github.com/urfave/cli/cli.go | 22 - vendor/github.com/urfave/cli/command.go | 336 ---- vendor/github.com/urfave/cli/context.go | 287 ---- vendor/github.com/urfave/cli/errors.go | 115 -- vendor/github.com/urfave/cli/flag-types.json | 93 - vendor/github.com/urfave/cli/flag.go | 786 --------- .../github.com/urfave/cli/flag_generated.go | 640 ------- vendor/github.com/urfave/cli/funcs.go | 44 - .../github.com/urfave/cli/generate-flag-types | 256 --- vendor/github.com/urfave/cli/help.go | 345 ---- vendor/github.com/urfave/cli/runtests | 122 -- vendor/github.com/urfave/cli/sort.go | 29 - vendor/vendor.json | 19 - 43 files changed, 831 insertions(+), 6997 deletions(-) delete mode 100644 cmd/blackbox/cmd_admin.go delete mode 100644 cmd/blackbox/cmd_decrypt.go delete mode 100644 cmd/blackbox/cmd_info.go delete mode 100644 cmd/blackbox/cmd_reg.go create mode 100644 cmd/blackbox/flags.go create mode 100644 cmd/blackbox/parse.go create mode 100644 pkg/box/box.go delete mode 100644 vendor/github.com/pkg/errors/LICENSE delete mode 100644 vendor/github.com/pkg/errors/README.md delete mode 100644 vendor/github.com/pkg/errors/appveyor.yml delete mode 100644 vendor/github.com/pkg/errors/errors.go delete mode 100644 vendor/github.com/pkg/errors/stack.go delete mode 100644 vendor/github.com/urfave/cli/CHANGELOG.md delete mode 100644 vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md delete mode 100644 vendor/github.com/urfave/cli/CONTRIBUTING.md delete mode 100644 vendor/github.com/urfave/cli/LICENSE delete mode 100644 vendor/github.com/urfave/cli/MAINTAINERS.md delete mode 100644 vendor/github.com/urfave/cli/README.md delete mode 100644 vendor/github.com/urfave/cli/app.go delete mode 100644 vendor/github.com/urfave/cli/appveyor.yml delete mode 100644 vendor/github.com/urfave/cli/category.go delete mode 100644 vendor/github.com/urfave/cli/cli.go delete mode 100644 vendor/github.com/urfave/cli/command.go delete mode 100644 vendor/github.com/urfave/cli/context.go delete mode 100644 vendor/github.com/urfave/cli/errors.go delete mode 100644 vendor/github.com/urfave/cli/flag-types.json delete mode 100644 vendor/github.com/urfave/cli/flag.go delete mode 100644 vendor/github.com/urfave/cli/flag_generated.go delete mode 100644 vendor/github.com/urfave/cli/funcs.go delete mode 100755 vendor/github.com/urfave/cli/generate-flag-types delete mode 100644 vendor/github.com/urfave/cli/help.go delete mode 100755 vendor/github.com/urfave/cli/runtests delete mode 100644 vendor/github.com/urfave/cli/sort.go delete mode 100644 vendor/vendor.json diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index b51f9d16..9b3526fa 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -1,202 +1,14 @@ package main import ( - "errors" "fmt" "os" - - "github.com/urfave/cli" ) var dryRun bool func main() { - app := cli.NewApp() - app.Version = "2.0.0" - app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" - - app.Flags = []cli.Flag{ - cli.BoolFlag{ - Name: "dry-run, n", - Usage: "show what would have been done", - Destination: &dryRun, - }, - } - - app.Commands = []cli.Command{ - { - Name: "info", - Category: "DEBUG", - Usage: "Report what we know about this repo", - Action: func(c *cli.Context) error { return cmdInfo(c) }, - }, - - { - Name: "initialize", - Aliases: []string{"init"}, - Category: "GETTING STARTED", - Usage: "Runs blackbox_initialize", - Action: func(c *cli.Context) error { return RunBash("blackbox_initialize", c.Args().First()) }, - }, - { - Name: "edit", - Aliases: []string{"e", "ed"}, - Usage: "Runs blackbox_edit ", - Action: func(c *cli.Context) error { return RunBash("blackbox_edit", c.Args().First()) }, - }, - { - Name: "decrypt", - Aliases: []string{"de", "start"}, - Usage: "Runs blackbox_edit_start", - Flags: []cli.Flag{ - cli.BoolFlag{Name: "all, a", Usage: "Runs blackbox_decrypt_all_files"}, - cli.BoolFlag{Name: "non-interactive", Usage: "Runs blackbox_postdeploy"}, - }, - Action: func(c *cli.Context) error { - if c.Bool("all") { - if c.Bool("non-interactive") { - return RunBash("blackbox_postdeploy", c.Args().First()) - } - return RunBash("blackbox_decrypt_all_files", c.Args().First()) - } - return RunBash("blackbox_edit_start", c.Args().First()) - }, - }, - { - Name: "ndecrypt", - Aliases: []string{"de", "start"}, - Usage: "Runs blackbox_edit_start", - Flags: []cli.Flag{ - cli.BoolFlag{ - Name: "all, a", - Usage: "All registered files", - }, - cli.BoolFlag{ - Name: "non-interactive, b", - Usage: "Do not set up gpg-agent", - }, - cli.StringFlag{ - Name: "set-group, g", - Usage: "Set group ownership", - }, - }, - Action: func(c *cli.Context) error { - if !c.Bool("non-interactive") { - // gpg_agent_notice - } - if c.Bool("all") { - if len(c.Args()) != 0 { - return errors.New("Can't combined --all and filenames") - } - } else { - if len(c.Args()) == 0 { - return errors.New("At least one filename required (or --all)") - } - } - return cmdDecrypt(c.Bool("all"), c.Args(), c.String("set-group")) - }, - }, - { - Name: "encrypt", - Aliases: []string{"en", "end"}, - Usage: "Runs blackbox_edit_end", - Action: func(c *cli.Context) error { return RunBash("blackbox_edit_end", c.Args().First()) }, - }, - { - Name: "reencrypt", - Usage: "Runs blackbox_update_all_files", - Action: func(c *cli.Context) error { return RunBash("blackbox_update_all_files", c.Args().First()) }, - }, - { - Name: "cat", - Usage: "Runs blackbox_cat", - Action: func(c *cli.Context) error { return RunBash("blackbox_cat", c.Args().First()) }, - }, - { - Name: "diff", - Usage: "Runs blackbox_diff", - Action: func(c *cli.Context) error { return RunBash("blackbox_diff", c.Args().First()) }, - }, - { - Name: "shredall", - Usage: "Runs blackbox_shred_all_files", - Action: func(c *cli.Context) error { return RunBash("blackbox_shred_all_files", c.Args().First()) }, - }, - { - Name: "whatsnew", - Usage: "Runs blackbox_whatsnew", - Action: func(c *cli.Context) error { return RunBash("blackbox_whatsnew", c.Args().First()) }, - }, - { - Name: "admin", - Category: "ADMINISTRATIVE", - Usage: "Maintain the list of administrators", - Subcommands: []cli.Command{ - { - Name: "nadd", - Aliases: []string{"add"}, - Usage: "Adds adminstrator(s)", - Action: func(c *cli.Context) error { return RunBash("blackbox_addadmin", c.Args().First()) }, - }, - { - Name: "oadd", - Usage: "Runs blackbox_addadmin", - Action: func(c *cli.Context) error { return RunBash("blackbox_addadmin", c.Args().First()) }, - }, - { - Name: "remove", - Usage: "Runs blackbox_removeadmin", - Action: func(c *cli.Context) error { return RunBash("blackbox_removeadmin", c.Args().First()) }, - }, - { - Name: "olist", - Usage: "Runs blackbox_list_admins", - Action: func(c *cli.Context) error { return RunBash("blackbox_list_admins", c.Args().First()) }, - }, - { - Name: "nlist", - Aliases: []string{"list"}, - Usage: "Lists blackbox admins", - Action: func(c *cli.Context) error { return cmdAdminList(c) }, - }, - }, - }, - { - Name: "file", - Aliases: []string{"f"}, - Category: "ADMINISTRATIVE", - Usage: "Maintain the list of files", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "Runs blackbox_register_new_file", - Action: func(c *cli.Context) error { return RunBash("blackbox_register_new_file", c.Args().First()) }, - }, - { - Name: "remove", - Usage: "Runs blackbox_deregister_file", - Action: func(c *cli.Context) error { return RunBash("blackbox_deregister_file", c.Args().First()) }, - }, - { - Name: "olist", - Usage: "Runs blackbox_list_admins", - Action: func(c *cli.Context) error { return RunBash("blackbox_list_files", c.Args().First()) }, - }, - { - Name: "nlist", - Aliases: []string{"list"}, - Usage: "Lists the registered files", - Action: func(c *cli.Context) error { return cmdRegList(c) }, - }, - { - Name: "status", - Usage: "Prints info about registered files", - Action: func(c *cli.Context) error { return cmdRegStatus(c) }, - }, - }, - }, - } - + app := flags() err := app.Run(os.Args) if err != nil { fmt.Fprintf(os.Stderr, "ERROR: %s\n", err) diff --git a/cmd/blackbox/cmd_admin.go b/cmd/blackbox/cmd_admin.go deleted file mode 100644 index 30ac0eab..00000000 --- a/cmd/blackbox/cmd_admin.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -// All the "blackbox admin" subcommands. - -import ( - "fmt" - - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/urfave/cli" -) - -func cmdAdminList(c *cli.Context) error { - - if len(c.Args()) != 0 { - fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") - return nil - } - - bbu, err := bbutil.New() - if err != nil { - return err - } - names, err := bbu.Administrators() - if err != nil { - return err - } - for _, item := range names { - fmt.Println(item.Name) - } - return nil -} diff --git a/cmd/blackbox/cmd_decrypt.go b/cmd/blackbox/cmd_decrypt.go deleted file mode 100644 index f0cac373..00000000 --- a/cmd/blackbox/cmd_decrypt.go +++ /dev/null @@ -1,32 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/pkg/errors" -) - -func cmdDecrypt(allFiles bool, filenames []string, group string) error { - bbu, err := bbutil.New() - if err != nil { - return err - } - - // prepare_keychain - - fnames, valid, err := bbu.FileIterator(allFiles, filenames) - if err != nil { - return errors.Wrap(err, "decrypt") - } - for i, filename := range fnames { - if valid[i] { - bbu.DecryptFile(filename, group, true) - } else { - fmt.Fprintf(os.Stderr, "SKIPPING: %q\n", filename) - } - } - - return nil -} diff --git a/cmd/blackbox/cmd_info.go b/cmd/blackbox/cmd_info.go deleted file mode 100644 index 9fa96772..00000000 --- a/cmd/blackbox/cmd_info.go +++ /dev/null @@ -1,31 +0,0 @@ -package main - -// All the "blackbox admin" subcommands. - -import ( - "fmt" - - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/urfave/cli" -) - -func cmdInfo(c *cli.Context) error { - - // GPG version - // VCS name - // keys directory - - bbu, err := bbutil.New() - if err != nil { - return err - } - - fmt.Print("VCS:\n") - fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) - fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) - fmt.Print("REPO:\n") - fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) - fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) - - return nil -} diff --git a/cmd/blackbox/cmd_reg.go b/cmd/blackbox/cmd_reg.go deleted file mode 100644 index e6ed14de..00000000 --- a/cmd/blackbox/cmd_reg.go +++ /dev/null @@ -1,51 +0,0 @@ -package main - -import ( - "fmt" - - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/urfave/cli" -) - -func cmdRegList(c *cli.Context) error { - if len(c.Args()) != 0 { - fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") - return nil - } - - bbu, err := bbutil.New() - if err != nil { - return err - } - names, err := bbu.RegisteredFiles() - if err != nil { - return err - } - for _, item := range names { - fmt.Println(item.Name) - } - return nil -} - -func cmdRegStatus(c *cli.Context) error { - - if len(c.Args()) != 0 { - fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") - return nil - } - - bbu, err := bbutil.New() - if err != nil { - return err - } - names, err := bbu.RegisteredFiles() - if err != nil { - return err - } - - for _, item := range names { - s := bbutil.FileStatus(bbu.RepoBaseDir, item.Name) - fmt.Printf("%s\t%s\n", s, item.Name) - } - return nil -} diff --git a/cmd/blackbox/flags.go b/cmd/blackbox/flags.go new file mode 100644 index 00000000..650db688 --- /dev/null +++ b/cmd/blackbox/flags.go @@ -0,0 +1,183 @@ +package main + +import ( + "github.com/urfave/cli/v2" +) + +func flags() *cli.App { + app := cli.NewApp() + app.Version = "2.0.0" + app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" + + // app.Flags = []cli.Flag{ + // &cli.BoolFlag{ + // Name: "dry-run", + // Aliases: []string{"n"}, + // Usage: "show what would have been done", + // Destination: &dryRun, + // }, + // } + + app.Commands = []*cli.Command{ + + // List items in the order they appear in the help menu. + + { + Name: "decrypt", + Aliases: []string{"de", "start"}, + Usage: "Decrypt file(s)", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "all", Usage: "All registered files"}, + &cli.BoolFlag{Name: "bulk", Usage: "Do not prompt to start gpg-agent"}, + &cli.StringFlag{Name: "group", Usage: "Set group ownership"}, + &cli.StringFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"}, + }, + Action: func(c *cli.Context) error { return cmdDecrypt(c) }, + }, + + { + Name: "encrypt", + Aliases: []string{"en", "end"}, + Usage: "Encrypts file(s)", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "leave", Usage: "Do not remove plaintext version"}, + }, + Action: func(c *cli.Context) error { return cmdEncrypt(c) }, + }, + + { + Name: "edit", + Aliases: []string{"vi"}, + Usage: "Runs $EDITOR on file(s) (decrypt if needed)", + Action: func(c *cli.Context) error { return cmdEdit(c) }, + }, + + { + Name: "cat", + Usage: "Output plaintext to stderr (decrypt if needed)", + Action: func(c *cli.Context) error { return cmdCat(c) }, + }, + + { + Name: "diff", + Usage: "Diffs against encrypted version", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "all", Usage: "all files"}, + }, + Action: func(c *cli.Context) error { return cmdDiff(c) }, + }, + + { + Name: "init", + Category: "ADMINISTRATIVE", + Usage: "Initialized blackbox for this repo", + Action: func(c *cli.Context) error { return cmdInit(c) }, + }, + + { + Name: "admin", + Category: "ADMINISTRATIVE", + Usage: "Add/list/remove administrators", + Subcommands: []*cli.Command{ + { + Name: "add", + Usage: "Adds admin(s)", + Action: func(c *cli.Context) error { return cmdAdminAdd(c) }, + }, + { + Name: "list", + Usage: "Lists admins", + Action: func(c *cli.Context) error { return cmdAdminList(c) }, + }, + { + Name: "remove", + Usage: "Remove admin(s)", + Action: func(c *cli.Context) error { return cmdAdminRemove(c) }, + }, + }, + }, + + { + Name: "file", + Category: "ADMINISTRATIVE", + Usage: "Add/list/remove files from the registry", + Subcommands: []*cli.Command{ + { + Name: "add", + Usage: "Registers file with the system", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "leave", Usage: "Do not remove plaintext version"}, + }, + Action: func(c *cli.Context) error { return cmdFileAdd(c) }, + }, + { + Name: "list", + Usage: "Lists the registered files", + Action: func(c *cli.Context) error { return cmdFileList(c) }, + }, + { + Name: "remove", + Usage: "Deregister file from the system", + Action: func(c *cli.Context) error { return cmdFileRemove(c) }, + }, + }, + }, + + { + Name: "info", + Category: "DEBUG", + Usage: "Report what we know about this repo", + Action: func(c *cli.Context) error { return cmdInfo(c) }, + }, + + { + Name: "shred", + Usage: "Shred the plaintext", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "all", Usage: "All registered files"}, + }, + Action: func(c *cli.Context) error { return cmdShred(c) }, + }, + + { + Name: "status", + Category: "ADMINISTRATIVE", + Usage: "Print status of files", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "name-only", Usage: "Show only names of the files"}, + }, + Subcommands: []*cli.Command{ + { + Name: "all", + Usage: "All registered files", + Action: func(c *cli.Context) error { return cmdStatusAll(c) }, + }, + { + Name: "changed", + Usage: "Only include files that are newer than their .gpg file", + Action: func(c *cli.Context) error { return cmdStatusChanged(c) }, + }, + { + Name: "unchanged", + Usage: "Only include files that 'status changed' wouldn't list", + Action: func(c *cli.Context) error { return cmdStatusUnchanged(c) }, + }, + }, + }, + + { + Name: "reencrypt", + Usage: "Decrypt then re-encrypt files", + Category: "ADMINISTRATIVE", + Flags: []cli.Flag{ + &cli.BoolFlag{Name: "all", Usage: "All registered files"}, + }, + Action: func(c *cli.Context) error { return cmdReencrypt(c) }, + }, + + // + + } + + return app +} diff --git a/cmd/blackbox/parse.go b/cmd/blackbox/parse.go new file mode 100644 index 00000000..3c484e7f --- /dev/null +++ b/cmd/blackbox/parse.go @@ -0,0 +1,227 @@ +package main + +// All the "blackbox admin" subcommands. + +import ( + "fmt" + "log" + "os" + + "github.com/StackExchange/blackbox/pkg/box" + "github.com/urfave/cli/v2" +) + +var logErr *log.Logger + +func init() { + if logErr == nil { + logErr = log.New(os.Stderr, "", 0) + } +} + +// Keep these functions in alphabetical order. + +func cmdAdminAdd(c *cli.Context) error { + if c.Args().Present() { + return fmt.Errorf( + "Must specify at least one admin's GnuPG user-id (i.e. email address)") + } + bx := box.NewFromFlags(c) + return bx.AdminAdd(c.Args().Slice()) +} + +func cmdAdminList(c *cli.Context) error { + if !c.Args().Present() { + return fmt.Errorf("This command takes zero arguments") + } + bx := box.NewFromFlags(c) + return bx.AdminList(bx) +} + +func cmdAdminRemove(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdCat(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdDecrypt(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdDiff(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdEdit(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdEncrypt(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdFileAdd(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdFileList(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdFileRemove(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdInfo(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdInit(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdReencrypt(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdShred(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdStatusAll(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdStatusChanged(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +func cmdStatusUnchanged(c *cli.Context) error { + logErr.Println("NOT IMPLEMENTED") + return nil +} + +//func cmdInfo(c *cli.Context) error { +// +// // GPG version +// // VCS name +// // keys directory +// +// bbu, err := bbutil.New() +// if err != nil { +// return err +// } +// +// fmt.Print("VCS:\n") +// fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) +// fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) +// fmt.Print("REPO:\n") +// fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) +// fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) +// +// return nil +//} + +//func cmdAdminList(c *cli.Context) error { +// +// if len(c.Args()) != 0 { +// fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") +// return nil +// } +// +// bbu, err := bbutil.New() +// if err != nil { +// return err +// } +// names, err := bbu.Administrators() +// if err != nil { +// return err +// } +// for _, item := range names { +// fmt.Println(item.Name) +// } +// return nil +//} + +// func cmdDecrypt(allFiles bool, filenames []string, group string) error { +// bbu, err := bbutil.New() +// if err != nil { +// return err +// } +// +// // prepare_keychain +// +// fnames, valid, err := bbu.FileIterator(allFiles, filenames) +// if err != nil { +// return errors.Wrap(err, "decrypt") +// } +// for i, filename := range fnames { +// if valid[i] { +// bbu.DecryptFile(filename, group, true) +// } else { +// fmt.Fprintf(os.Stderr, "SKIPPING: %q\n", filename) +// } +// } +// +// return nil +// } + +// func cmdRegList(c *cli.Context) error { +// if len(c.Args()) != 0 { +// fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") +// return nil +// } +// +// bbu, err := bbutil.New() +// if err != nil { +// return err +// } +// names, err := bbu.RegisteredFiles() +// if err != nil { +// return err +// } +// for _, item := range names { +// fmt.Println(item.Name) +// } +// return nil +// } + +// func cmdRegStatus(c *cli.Context) error { +// +// if len(c.Args()) != 0 { +// fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") +// return nil +// } +// +// bbu, err := bbutil.New() +// if err != nil { +// return err +// } +// names, err := bbu.RegisteredFiles() +// if err != nil { +// return err +// } +// +// for _, item := range names { +// s := bbutil.FileStatus(bbu.RepoBaseDir, item.Name) +// fmt.Printf("%s\t%s\n", s, item.Name) +// } +// return nil +// } diff --git a/cmd/blackbox/runbash.go b/cmd/blackbox/runbash.go index 5abdaa6d..39b980d1 100644 --- a/cmd/blackbox/runbash.go +++ b/cmd/blackbox/runbash.go @@ -1,28 +1,27 @@ -package main - -import ( - "fmt" - "log" - "os" - "os/exec" - - "github.com/pkg/errors" -) - -// RunBash runs a Bash command. -func RunBash(command string, args ...string) error { - if dryRun { - fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) - return nil - } - cmd := exec.Command(command, args...) - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - err := cmd.Start() - if err != nil { - log.Fatal(err) - } - err = cmd.Wait() - return errors.Wrapf(err, "run_bash:") -} +// package main +// +// import ( +// "fmt" +// "log" +// "os" +// "os/exec" +// +// ) +// +// // RunBash runs a Bash command. +// func RunBash(command string, args ...string) error { +// if dryRun { +// fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) +// return nil +// } +// cmd := exec.Command(command, args...) +// cmd.Stdin = os.Stdin +// cmd.Stdout = os.Stdout +// cmd.Stderr = os.Stderr +// err := cmd.Start() +// if err != nil { +// log.Fatal(err) +// } +// err = cmd.Wait() +// return errors.Wrapf(err, "run_bash:") +// } diff --git a/pkg/bbgit/discover.go b/pkg/bbgit/discover.go index 08ae7503..150e11e8 100644 --- a/pkg/bbgit/discover.go +++ b/pkg/bbgit/discover.go @@ -1,38 +1,37 @@ -package bbgit - -import ( - "os/exec" - "strings" - - "github.com/pkg/errors" -) - -// GitInfo contains Git-specific info about this repository. -type GitInfo struct { - baseDir string -} - -// New is a factory; returns error if this is not a Git repo. -func New() (*GitInfo, error) { - ri := new(GitInfo) - path, err := exec.LookPath("git") - if err != nil { - return nil, nil - } - baseDir, err := exec.Command(path, "rev-parse", "--show-toplevel").Output() - if err != nil { - return nil, errors.Wrap(err, "bbgit:") - } - ri.baseDir = strings.TrimSuffix(string(baseDir), "\n") // remove a single newline. - return ri, nil -} - -// Name returns the name of this type of repo. -func (repo *GitInfo) Name() string { - return "git" -} - -// RepoBaseDir returns -func (repo *GitInfo) RepoBaseDir() string { - return repo.baseDir -} +// package bbgit +// +// import ( +// "os/exec" +// "strings" +// +// ) +// +// // GitInfo contains Git-specific info about this repository. +// type GitInfo struct { +// baseDir string +// } +// +// // New is a factory; returns error if this is not a Git repo. +// func New() (*GitInfo, error) { +// ri := new(GitInfo) +// path, err := exec.LookPath("git") +// if err != nil { +// return nil, nil +// } +// baseDir, err := exec.Command(path, "rev-parse", "--show-toplevel").Output() +// if err != nil { +// return nil, errors.Wrap(err, "bbgit:") +// } +// ri.baseDir = strings.TrimSuffix(string(baseDir), "\n") // remove a single newline. +// return ri, nil +// } +// +// // Name returns the name of this type of repo. +// func (repo *GitInfo) Name() string { +// return "git" +// } +// +// // RepoBaseDir returns +// func (repo *GitInfo) RepoBaseDir() string { +// return repo.baseDir +// } diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go index 23585bb9..ea7eb667 100644 --- a/pkg/bbutil/admin.go +++ b/pkg/bbutil/admin.go @@ -1,21 +1,21 @@ -package bbutil - -// Administrator is a description of the admininstrators. -type Administrator struct { - Name string -} - -// Administrators returns the administrators of this repo. -func (bbu *RepoInfo) Administrators() ([]Administrator, error) { - return plainListAdmins(bbu.BlackboxConfigDir) -} - -// AddAdmin adds an administrator to this repo. -func (bbu *RepoInfo) AddAdmin(admin Administrator) error { - return plainAddAdmin(bbu.BlackboxConfigDir, admin) -} - -// RemoveAdminByName removes an administrator from this repo. -func (bbu *RepoInfo) RemoveAdminByName(admin Administrator) error { - return plainRemoveAdmin(bbu.BlackboxConfigDir, admin) -} +// package bbutil +// +// // Administrator is a description of the admininstrators. +// type Administrator struct { +// Name string +// } +// +// // Administrators returns the administrators of this repo. +// func (bbu *RepoInfo) Administrators() ([]Administrator, error) { +// return plainListAdmins(bbu.BlackboxConfigDir) +// } +// +// // AddAdmin adds an administrator to this repo. +// func (bbu *RepoInfo) AddAdmin(admin Administrator) error { +// return plainAddAdmin(bbu.BlackboxConfigDir, admin) +// } +// +// // RemoveAdminByName removes an administrator from this repo. +// func (bbu *RepoInfo) RemoveAdminByName(admin Administrator) error { +// return plainRemoveAdmin(bbu.BlackboxConfigDir, admin) +// } diff --git a/pkg/bbutil/adminplain.go b/pkg/bbutil/adminplain.go index cc36c3a2..5875412e 100644 --- a/pkg/bbutil/adminplain.go +++ b/pkg/bbutil/adminplain.go @@ -1,62 +1,61 @@ -package bbutil - -import ( - "io/ioutil" - "log" - "path/filepath" - "sort" - "strings" - - "github.com/pkg/errors" -) - -func plainAdminsFile(dir string) string { - return filepath.Join(dir, "blackbox-admins.txt") -} - -// Administrators returns the list of administrators. -func plainListAdmins(dir string) ([]Administrator, error) { - adminFilename := plainAdminsFile(dir) - d, err := ioutil.ReadFile(adminFilename) - if err != nil { - return nil, errors.Wrap(err, "Could not read the list of administrators") - } - - // remove a trailing \n. - s := strings.TrimSuffix(string(d), "\n") // remove a single newline. - names := strings.Split(s, "\n") - if !sort.StringsAreSorted(names) { - log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) - } - r := make([]Administrator, len(names)) - for i, name := range names { - r[i].Name = name - } - - return r, nil -} - -// plainWriteAdmins rewrites the admins file. -func plainWriteAdmins(dir string, admins []Administrator) error { - return errors.New("UNIMPLEMENTED") -} - -// plainAddAdmins adds one administrator by email address. -func plainAddAdmin(dir string, admin Administrator) error { - admins, err := plainListAdmins(dir) - if err != nil { - return err - } - - // Add it to the list, sort it into position. - admins = append(admins, admin) - sort.Slice(admins, func(i, j int) bool { - return admins[i].Name < admins[j].Name - }) - - return plainWriteAdmins(dir, admins) -} - -func plainRemoveAdmin(dir string, admin Administrator) error { - return errors.New("UNIMPLEMENTED") -} +// package bbutil +// +// import ( +// "io/ioutil" +// "log" +// "path/filepath" +// "sort" +// "strings" +// +// ) +// +// func plainAdminsFile(dir string) string { +// return filepath.Join(dir, "blackbox-admins.txt") +// } +// +// // Administrators returns the list of administrators. +// func plainListAdmins(dir string) ([]Administrator, error) { +// adminFilename := plainAdminsFile(dir) +// d, err := ioutil.ReadFile(adminFilename) +// if err != nil { +// return nil, errors.Wrap(err, "Could not read the list of administrators") +// } +// +// // remove a trailing \n. +// s := strings.TrimSuffix(string(d), "\n") // remove a single newline. +// names := strings.Split(s, "\n") +// if !sort.StringsAreSorted(names) { +// log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) +// } +// r := make([]Administrator, len(names)) +// for i, name := range names { +// r[i].Name = name +// } +// +// return r, nil +// } +// +// // plainWriteAdmins rewrites the admins file. +// func plainWriteAdmins(dir string, admins []Administrator) error { +// return errors.New("UNIMPLEMENTED") +// } +// +// // plainAddAdmins adds one administrator by email address. +// func plainAddAdmin(dir string, admin Administrator) error { +// admins, err := plainListAdmins(dir) +// if err != nil { +// return err +// } +// +// // Add it to the list, sort it into position. +// admins = append(admins, admin) +// sort.Slice(admins, func(i, j int) bool { +// return admins[i].Name < admins[j].Name +// }) +// +// return plainWriteAdmins(dir, admins) +// } +// +// func plainRemoveAdmin(dir string, admin Administrator) error { +// return errors.New("UNIMPLEMENTED") +// } diff --git a/pkg/bbutil/decrypt.go b/pkg/bbutil/decrypt.go index 7c092d4f..546b761d 100644 --- a/pkg/bbutil/decrypt.go +++ b/pkg/bbutil/decrypt.go @@ -1,32 +1,32 @@ -package bbutil - -import ( - "fmt" - "os" -) - -// DecryptFile decrypts a single file. -func (bbu *RepoInfo) DecryptFile(filename, group string, overwrite bool) error { - - // change_to_vcs_root - - fmt.Fprintf(os.Stderr, "WOULD DECRYPT: %v %q %q\n", overwrite, group, filename) - - // export PATH=/usr/bin:/bin:"$PATH" - - // # Decrypt: - // echo '========== Decrypting new/changed files: START' - // while IFS= read <&99 -r unencrypted_file; do - // encrypted_file=$(get_encrypted_filename "$unencrypted_file") - // decrypt_file_overwrite "$encrypted_file" "$unencrypted_file" - // cp_permissions "$encrypted_file" "$unencrypted_file" - // if [[ ! -z "$FILE_GROUP" ]]; then - // chmod g+r "$unencrypted_file" - // chgrp "$FILE_GROUP" "$unencrypted_file" - // fi - // done 99<"$BB_FILES" - - // echo '========== Decrypting new/changed files: DONE' - - return nil -} +// package bbutil +// +// import ( +// "fmt" +// "os" +// ) +// +// // DecryptFile decrypts a single file. +// func (bbu *RepoInfo) DecryptFile(filename, group string, overwrite bool) error { +// +// // change_to_vcs_root +// +// fmt.Fprintf(os.Stderr, "WOULD DECRYPT: %v %q %q\n", overwrite, group, filename) +// +// // export PATH=/usr/bin:/bin:"$PATH" +// +// // # Decrypt: +// // echo '========== Decrypting new/changed files: START' +// // while IFS= read <&99 -r unencrypted_file; do +// // encrypted_file=$(get_encrypted_filename "$unencrypted_file") +// // decrypt_file_overwrite "$encrypted_file" "$unencrypted_file" +// // cp_permissions "$encrypted_file" "$unencrypted_file" +// // if [[ ! -z "$FILE_GROUP" ]]; then +// // chmod g+r "$unencrypted_file" +// // chgrp "$FILE_GROUP" "$unencrypted_file" +// // fi +// // done 99<"$BB_FILES" +// +// // echo '========== Decrypting new/changed files: DONE' +// +// return nil +// } diff --git a/pkg/bbutil/iterator.go b/pkg/bbutil/iterator.go index 863b3d78..cefd16cd 100644 --- a/pkg/bbutil/iterator.go +++ b/pkg/bbutil/iterator.go @@ -1,37 +1,37 @@ -package bbutil - -import ( - "sort" -) - -// FileIterator return a list of files to process. - -func (bbu *RepoInfo) FileIterator(allFiles bool, fnames []string) ([]string, []bool, error) { - regfiles, err := bbu.RegisteredFiles() - if err != nil { - return nil, nil, err - } - - allnames := make([]string, len(regfiles)) - for i, r := range regfiles { - allnames[i] = r.Name - } - - if allFiles { - isvalid := make([]bool, len(allnames)) - for n := range allnames { - isvalid[n] = true - } - return allnames, isvalid, nil - } - - retnames := make([]string, len(fnames)) - isvalid := make([]bool, len(fnames)) - for n, fn := range fnames { - retnames[n] = fn - i := sort.SearchStrings(allnames, fn) - isvalid[n] = i < len(allnames) && allnames[i] == fn - } - - return retnames, isvalid, nil -} +// package bbutil +// +// import ( +// "sort" +// ) +// +// // FileIterator return a list of files to process. +// +// func (bbu *RepoInfo) FileIterator(allFiles bool, fnames []string) ([]string, []bool, error) { +// regfiles, err := bbu.RegisteredFiles() +// if err != nil { +// return nil, nil, err +// } +// +// allnames := make([]string, len(regfiles)) +// for i, r := range regfiles { +// allnames[i] = r.Name +// } +// +// if allFiles { +// isvalid := make([]bool, len(allnames)) +// for n := range allnames { +// isvalid[n] = true +// } +// return allnames, isvalid, nil +// } +// +// retnames := make([]string, len(fnames)) +// isvalid := make([]bool, len(fnames)) +// for n, fn := range fnames { +// retnames[n] = fn +// i := sort.SearchStrings(allnames, fn) +// isvalid[n] = i < len(allnames) && allnames[i] == fn +// } +// +// return retnames, isvalid, nil +// } diff --git a/pkg/bbutil/reg.go b/pkg/bbutil/reg.go index e9c4289b..b258c989 100644 --- a/pkg/bbutil/reg.go +++ b/pkg/bbutil/reg.go @@ -1,62 +1,61 @@ -package bbutil - -import ( - "io/ioutil" - "log" - "os" - "path/filepath" - "sort" - "strings" - - "github.com/pkg/errors" -) - -// RegFile is a description of a registered file. -type RegFile struct { - Name string -} - -// RegisteredFiles returns a list of the registered files. -func (bbu *RepoInfo) RegisteredFiles() ([]RegFile, error) { - blackboxFiles := filepath.Join(bbu.BlackboxConfigDir, "blackbox-files.txt") - d, err := ioutil.ReadFile(blackboxFiles) - if err != nil { - return nil, errors.Wrap(err, "Could not read the list of registered files") - } - - // remove a trailing \n. - // NB(tlim): We can't remove all trailing whitespace because filenames may contain whitespace. - s := strings.TrimSuffix(string(d), "\n") // remove a single newline. - - names := strings.Split(s, "\n") - if !sort.StringsAreSorted(names) { - log.Fatalf("Files list is corrupted. It is not sorted; %q", blackboxFiles) - } - r := make([]RegFile, len(names)) - for i, name := range names { - r[i].Name = name - } - - return r, nil -} - -// FileStatus returns the status of a file. -func FileStatus(basedir, file string) string { - p := filepath.Join(basedir, file) - e := p + ".gpg" - ps, perr := os.Stat(p) - es, eerr := os.Stat(e) - if perr == nil && eerr == nil { - if ps.ModTime().Before(es.ModTime()) { - return "GPGNEWER" - } - return "EDITING" - } - if eerr == nil { - return "ENCRYPTED" - } - if os.IsExist(perr) { - return "ERROR_NOGPG" - } - return "ERROR_NOEXIST" -} +// package bbutil +// +// import ( +// "io/ioutil" +// "log" +// "os" +// "path/filepath" +// "sort" +// "strings" +// +// ) +// +// // RegFile is a description of a registered file. +// type RegFile struct { +// Name string +// } +// +// // RegisteredFiles returns a list of the registered files. +// func (bbu *RepoInfo) RegisteredFiles() ([]RegFile, error) { +// blackboxFiles := filepath.Join(bbu.BlackboxConfigDir, "blackbox-files.txt") +// d, err := ioutil.ReadFile(blackboxFiles) +// if err != nil { +// return nil, errors.Wrap(err, "Could not read the list of registered files") +// } +// +// // remove a trailing \n. +// // NB(tlim): We can't remove all trailing whitespace because filenames may contain whitespace. +// s := strings.TrimSuffix(string(d), "\n") // remove a single newline. +// +// names := strings.Split(s, "\n") +// if !sort.StringsAreSorted(names) { +// log.Fatalf("Files list is corrupted. It is not sorted; %q", blackboxFiles) +// } +// r := make([]RegFile, len(names)) +// for i, name := range names { +// r[i].Name = name +// } +// +// return r, nil +// } +// +// // FileStatus returns the status of a file. +// func FileStatus(basedir, file string) string { +// p := filepath.Join(basedir, file) +// e := p + ".gpg" +// ps, perr := os.Stat(p) +// es, eerr := os.Stat(e) +// if perr == nil && eerr == nil { +// if ps.ModTime().Before(es.ModTime()) { +// return "GPGNEWER" +// } +// return "EDITING" +// } +// if eerr == nil { +// return "ENCRYPTED" +// } +// if os.IsExist(perr) { +// return "ERROR_NOGPG" +// } +// return "ERROR_NOEXIST" +// } diff --git a/pkg/bbutil/repoinfo.go b/pkg/bbutil/repoinfo.go index 8da87385..bedca191 100644 --- a/pkg/bbutil/repoinfo.go +++ b/pkg/bbutil/repoinfo.go @@ -1,112 +1,112 @@ -package bbutil - -import ( - "os" - "path/filepath" - - "github.com/StackExchange/blackbox/pkg/bbgit" - "github.com/StackExchange/blackbox/pkg/bbnone" -) - -// Vcser is the interface that defines a plug-in VCS system. -type Vcser interface { - Name() string // Returns the name of this VCS type. - RepoBaseDir() string // Returns the full path leading to this repo. -} - -// RepoInfo stores info about the current repository. -type RepoInfo struct { - Vcs Vcser - // BaseDir specifies the path (from "/") to the base of the VCS repo. - RepoBaseDir string // REPOBASE - BlackboxConfigDir string // BLACKBOXDATA - // KEYRINGDIR="$REPOBASE/$BLACKBOXDATA" - // BB_ADMINS_FILE="blackbox-admins.txt" - // BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}" - // SECRING="${KEYRINGDIR}/secring.gpg" -} - -// New is a factory. -func New() (*RepoInfo, error) { - repo := &RepoInfo{} - - vcs, err := vcsType() - if err != nil { - return nil, err - } - repo.Vcs = vcs - - // What is the base directory of the repo? - base := os.Getenv("BLACKBOX_REPOBASE") - if base == "" { - base = repo.Vcs.RepoBaseDir() - } - repo.RepoBaseDir = base - - // Where are the blackbox config files? - repo.BlackboxConfigDir, err = findConfigDir(repo.RepoBaseDir) - if err != nil { - return nil, err - } - - return repo, nil -} - -// vcsType discovers the VCS type based on the BB_VCSTYPE env variable or by probing. -func vcsType() (Vcser, error) { - switch vcsName := os.Getenv("BB_VCSTYPE"); vcsName { - case "git": - return bbgit.New() - // case "hg": - // case "svn": - default: - break - } - answer, err := bbgit.New() - if err == nil { - return answer, nil - } - // TODO(tlim): Should we print err? - // answer = bbhg.New() - // if answer != nil { - // return answer - // } - // answer = bbsvn.New() - // if answer != nil { - // return answer - // } - return bbnone.New() -} - -// If BLACKBOXDATA is not set, search list this of directory paths. -var configDirCandidates = []string{ - "keyrings/live", - ".blackbox", // Last item is the default. -} - -// findConfigDir returns the configuration directory. It first checks the -// BLACKBOXDATA env variable, then a list of candidates, lastly returning the -// last candidate as the default. -func findConfigDir(repoBase string) (string, error) { - if dir := os.Getenv("BLACKBOXDATA"); dir != "" { - //fmt.Fprintln(os.Stderr, "USING BBENV", dir) - return filepath.Join(repoBase, dir), nil - } - var p string - for _, c := range configDirCandidates { - p = filepath.Join(repoBase, c) - //fmt.Fprintf(os.Stderr, "Trying %q\n", p) - if st, err := os.Stat(p); err == nil { - mode := st.Mode() - if mode.IsDir() { - // FIXME(tlim): We are assuming that "not found" - // and "i/o error" are both reasons to skip the candidates. - // Maybe we should see what kind of error it is an output - // some diagnostics if the problem is more than just "no found"? - //fmt.Fprintf(os.Stderr, "RETURNING %q\n", p) - return p, nil - } - } - } - return p, nil -} +// package bbutil +// +// import ( +// "os" +// "path/filepath" +// +// "github.com/StackExchange/blackbox/pkg/bbgit" +// "github.com/StackExchange/blackbox/pkg/bbnone" +// ) +// +// // Vcser is the interface that defines a plug-in VCS system. +// type Vcser interface { +// Name() string // Returns the name of this VCS type. +// RepoBaseDir() string // Returns the full path leading to this repo. +// } +// +// // RepoInfo stores info about the current repository. +// type RepoInfo struct { +// Vcs Vcser +// // BaseDir specifies the path (from "/") to the base of the VCS repo. +// RepoBaseDir string // REPOBASE +// BlackboxConfigDir string // BLACKBOXDATA +// // KEYRINGDIR="$REPOBASE/$BLACKBOXDATA" +// // BB_ADMINS_FILE="blackbox-admins.txt" +// // BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}" +// // SECRING="${KEYRINGDIR}/secring.gpg" +// } +// +// // New is a factory. +// func New() (*RepoInfo, error) { +// repo := &RepoInfo{} +// +// vcs, err := vcsType() +// if err != nil { +// return nil, err +// } +// repo.Vcs = vcs +// +// // What is the base directory of the repo? +// base := os.Getenv("BLACKBOX_REPOBASE") +// if base == "" { +// base = repo.Vcs.RepoBaseDir() +// } +// repo.RepoBaseDir = base +// +// // Where are the blackbox config files? +// repo.BlackboxConfigDir, err = findConfigDir(repo.RepoBaseDir) +// if err != nil { +// return nil, err +// } +// +// return repo, nil +// } +// +// // vcsType discovers the VCS type based on the BB_VCSTYPE env variable or by probing. +// func vcsType() (Vcser, error) { +// switch vcsName := os.Getenv("BB_VCSTYPE"); vcsName { +// case "git": +// return bbgit.New() +// // case "hg": +// // case "svn": +// default: +// break +// } +// answer, err := bbgit.New() +// if err == nil { +// return answer, nil +// } +// // TODO(tlim): Should we print err? +// // answer = bbhg.New() +// // if answer != nil { +// // return answer +// // } +// // answer = bbsvn.New() +// // if answer != nil { +// // return answer +// // } +// return bbnone.New() +// } +// +// // If BLACKBOXDATA is not set, search list this of directory paths. +// var configDirCandidates = []string{ +// "keyrings/live", +// ".blackbox", // Last item is the default. +// } +// +// // findConfigDir returns the configuration directory. It first checks the +// // BLACKBOXDATA env variable, then a list of candidates, lastly returning the +// // last candidate as the default. +// func findConfigDir(repoBase string) (string, error) { +// if dir := os.Getenv("BLACKBOXDATA"); dir != "" { +// //fmt.Fprintln(os.Stderr, "USING BBENV", dir) +// return filepath.Join(repoBase, dir), nil +// } +// var p string +// for _, c := range configDirCandidates { +// p = filepath.Join(repoBase, c) +// //fmt.Fprintf(os.Stderr, "Trying %q\n", p) +// if st, err := os.Stat(p); err == nil { +// mode := st.Mode() +// if mode.IsDir() { +// // FIXME(tlim): We are assuming that "not found" +// // and "i/o error" are both reasons to skip the candidates. +// // Maybe we should see what kind of error it is an output +// // some diagnostics if the problem is more than just "no found"? +// //fmt.Fprintf(os.Stderr, "RETURNING %q\n", p) +// return p, nil +// } +// } +// } +// return p, nil +// } diff --git a/pkg/box/box.go b/pkg/box/box.go new file mode 100644 index 00000000..7986a630 --- /dev/null +++ b/pkg/box/box.go @@ -0,0 +1,32 @@ +package box + +import ( + "fmt" + + "github.com/urfave/cli/v2" +) + +// Box provides access to a Blackbox. +type Boxer interface { + AdminAdd([]string) error + AdminList() error + AdminRemove([]string) error +} + +type box struct { +} + +func (bx *box) NewFromFlags(c *cli.Context) error { +} + +func (bx *box) AdminAdd([]string) error { + return fmt.Errorf("NOT IMPLEMENTED") +} + +func (bx *box) AdminList() error { + return fmt.Errorf("NOT IMPLEMENTED") +} + +func (bx *box) AdminRemove([]string) error { + return fmt.Errorf("NOT IMPLEMENTED") +} diff --git a/vendor/github.com/pkg/errors/LICENSE b/vendor/github.com/pkg/errors/LICENSE deleted file mode 100644 index 835ba3e7..00000000 --- a/vendor/github.com/pkg/errors/LICENSE +++ /dev/null @@ -1,23 +0,0 @@ -Copyright (c) 2015, Dave Cheney -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -* Redistributions of source code must retain the above copyright notice, this - list of conditions and the following disclaimer. - -* Redistributions in binary form must reproduce the above copyright notice, - this list of conditions and the following disclaimer in the documentation - and/or other materials provided with the distribution. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE -FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL -DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR -SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, -OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/pkg/errors/README.md b/vendor/github.com/pkg/errors/README.md deleted file mode 100644 index 6483ba2a..00000000 --- a/vendor/github.com/pkg/errors/README.md +++ /dev/null @@ -1,52 +0,0 @@ -# errors [![Travis-CI](https://travis-ci.org/pkg/errors.svg)](https://travis-ci.org/pkg/errors) [![AppVeyor](https://ci.appveyor.com/api/projects/status/b98mptawhudj53ep/branch/master?svg=true)](https://ci.appveyor.com/project/davecheney/errors/branch/master) [![GoDoc](https://godoc.org/github.com/pkg/errors?status.svg)](http://godoc.org/github.com/pkg/errors) [![Report card](https://goreportcard.com/badge/github.com/pkg/errors)](https://goreportcard.com/report/github.com/pkg/errors) [![Sourcegraph](https://sourcegraph.com/github.com/pkg/errors/-/badge.svg)](https://sourcegraph.com/github.com/pkg/errors?badge) - -Package errors provides simple error handling primitives. - -`go get github.com/pkg/errors` - -The traditional error handling idiom in Go is roughly akin to -```go -if err != nil { - return err -} -``` -which applied recursively up the call stack results in error reports without context or debugging information. The errors package allows programmers to add context to the failure path in their code in a way that does not destroy the original value of the error. - -## Adding context to an error - -The errors.Wrap function returns a new error that adds context to the original error. For example -```go -_, err := ioutil.ReadAll(r) -if err != nil { - return errors.Wrap(err, "read failed") -} -``` -## Retrieving the cause of an error - -Using `errors.Wrap` constructs a stack of errors, adding context to the preceding error. Depending on the nature of the error it may be necessary to reverse the operation of errors.Wrap to retrieve the original error for inspection. Any error value which implements this interface can be inspected by `errors.Cause`. -```go -type causer interface { - Cause() error -} -``` -`errors.Cause` will recursively retrieve the topmost error which does not implement `causer`, which is assumed to be the original cause. For example: -```go -switch err := errors.Cause(err).(type) { -case *MyError: - // handle specifically -default: - // unknown error -} -``` - -[Read the package documentation for more information](https://godoc.org/github.com/pkg/errors). - -## Contributing - -We welcome pull requests, bug fixes and issue reports. With that said, the bar for adding new symbols to this package is intentionally set high. - -Before proposing a change, please discuss your change by raising an issue. - -## License - -BSD-2-Clause diff --git a/vendor/github.com/pkg/errors/appveyor.yml b/vendor/github.com/pkg/errors/appveyor.yml deleted file mode 100644 index a932eade..00000000 --- a/vendor/github.com/pkg/errors/appveyor.yml +++ /dev/null @@ -1,32 +0,0 @@ -version: build-{build}.{branch} - -clone_folder: C:\gopath\src\github.com\pkg\errors -shallow_clone: true # for startup speed - -environment: - GOPATH: C:\gopath - -platform: - - x64 - -# http://www.appveyor.com/docs/installed-software -install: - # some helpful output for debugging builds - - go version - - go env - # pre-installed MinGW at C:\MinGW is 32bit only - # but MSYS2 at C:\msys64 has mingw64 - - set PATH=C:\msys64\mingw64\bin;%PATH% - - gcc --version - - g++ --version - -build_script: - - go install -v ./... - -test_script: - - set PATH=C:\gopath\bin;%PATH% - - go test -v ./... - -#artifacts: -# - path: '%GOPATH%\bin\*.exe' -deploy: off diff --git a/vendor/github.com/pkg/errors/errors.go b/vendor/github.com/pkg/errors/errors.go deleted file mode 100644 index 842ee804..00000000 --- a/vendor/github.com/pkg/errors/errors.go +++ /dev/null @@ -1,269 +0,0 @@ -// Package errors provides simple error handling primitives. -// -// The traditional error handling idiom in Go is roughly akin to -// -// if err != nil { -// return err -// } -// -// which applied recursively up the call stack results in error reports -// without context or debugging information. The errors package allows -// programmers to add context to the failure path in their code in a way -// that does not destroy the original value of the error. -// -// Adding context to an error -// -// The errors.Wrap function returns a new error that adds context to the -// original error by recording a stack trace at the point Wrap is called, -// and the supplied message. For example -// -// _, err := ioutil.ReadAll(r) -// if err != nil { -// return errors.Wrap(err, "read failed") -// } -// -// If additional control is required the errors.WithStack and errors.WithMessage -// functions destructure errors.Wrap into its component operations of annotating -// an error with a stack trace and an a message, respectively. -// -// Retrieving the cause of an error -// -// Using errors.Wrap constructs a stack of errors, adding context to the -// preceding error. Depending on the nature of the error it may be necessary -// to reverse the operation of errors.Wrap to retrieve the original error -// for inspection. Any error value which implements this interface -// -// type causer interface { -// Cause() error -// } -// -// can be inspected by errors.Cause. errors.Cause will recursively retrieve -// the topmost error which does not implement causer, which is assumed to be -// the original cause. For example: -// -// switch err := errors.Cause(err).(type) { -// case *MyError: -// // handle specifically -// default: -// // unknown error -// } -// -// causer interface is not exported by this package, but is considered a part -// of stable public API. -// -// Formatted printing of errors -// -// All error values returned from this package implement fmt.Formatter and can -// be formatted by the fmt package. The following verbs are supported -// -// %s print the error. If the error has a Cause it will be -// printed recursively -// %v see %s -// %+v extended format. Each Frame of the error's StackTrace will -// be printed in detail. -// -// Retrieving the stack trace of an error or wrapper -// -// New, Errorf, Wrap, and Wrapf record a stack trace at the point they are -// invoked. This information can be retrieved with the following interface. -// -// type stackTracer interface { -// StackTrace() errors.StackTrace -// } -// -// Where errors.StackTrace is defined as -// -// type StackTrace []Frame -// -// The Frame type represents a call site in the stack trace. Frame supports -// the fmt.Formatter interface that can be used for printing information about -// the stack trace of this error. For example: -// -// if err, ok := err.(stackTracer); ok { -// for _, f := range err.StackTrace() { -// fmt.Printf("%+s:%d", f) -// } -// } -// -// stackTracer interface is not exported by this package, but is considered a part -// of stable public API. -// -// See the documentation for Frame.Format for more details. -package errors - -import ( - "fmt" - "io" -) - -// New returns an error with the supplied message. -// New also records the stack trace at the point it was called. -func New(message string) error { - return &fundamental{ - msg: message, - stack: callers(), - } -} - -// Errorf formats according to a format specifier and returns the string -// as a value that satisfies error. -// Errorf also records the stack trace at the point it was called. -func Errorf(format string, args ...interface{}) error { - return &fundamental{ - msg: fmt.Sprintf(format, args...), - stack: callers(), - } -} - -// fundamental is an error that has a message and a stack, but no caller. -type fundamental struct { - msg string - *stack -} - -func (f *fundamental) Error() string { return f.msg } - -func (f *fundamental) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - io.WriteString(s, f.msg) - f.stack.Format(s, verb) - return - } - fallthrough - case 's': - io.WriteString(s, f.msg) - case 'q': - fmt.Fprintf(s, "%q", f.msg) - } -} - -// WithStack annotates err with a stack trace at the point WithStack was called. -// If err is nil, WithStack returns nil. -func WithStack(err error) error { - if err == nil { - return nil - } - return &withStack{ - err, - callers(), - } -} - -type withStack struct { - error - *stack -} - -func (w *withStack) Cause() error { return w.error } - -func (w *withStack) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - fmt.Fprintf(s, "%+v", w.Cause()) - w.stack.Format(s, verb) - return - } - fallthrough - case 's': - io.WriteString(s, w.Error()) - case 'q': - fmt.Fprintf(s, "%q", w.Error()) - } -} - -// Wrap returns an error annotating err with a stack trace -// at the point Wrap is called, and the supplied message. -// If err is nil, Wrap returns nil. -func Wrap(err error, message string) error { - if err == nil { - return nil - } - err = &withMessage{ - cause: err, - msg: message, - } - return &withStack{ - err, - callers(), - } -} - -// Wrapf returns an error annotating err with a stack trace -// at the point Wrapf is call, and the format specifier. -// If err is nil, Wrapf returns nil. -func Wrapf(err error, format string, args ...interface{}) error { - if err == nil { - return nil - } - err = &withMessage{ - cause: err, - msg: fmt.Sprintf(format, args...), - } - return &withStack{ - err, - callers(), - } -} - -// WithMessage annotates err with a new message. -// If err is nil, WithMessage returns nil. -func WithMessage(err error, message string) error { - if err == nil { - return nil - } - return &withMessage{ - cause: err, - msg: message, - } -} - -type withMessage struct { - cause error - msg string -} - -func (w *withMessage) Error() string { return w.msg + ": " + w.cause.Error() } -func (w *withMessage) Cause() error { return w.cause } - -func (w *withMessage) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - if s.Flag('+') { - fmt.Fprintf(s, "%+v\n", w.Cause()) - io.WriteString(s, w.msg) - return - } - fallthrough - case 's', 'q': - io.WriteString(s, w.Error()) - } -} - -// Cause returns the underlying cause of the error, if possible. -// An error value has a cause if it implements the following -// interface: -// -// type causer interface { -// Cause() error -// } -// -// If the error does not implement Cause, the original error will -// be returned. If the error is nil, nil will be returned without further -// investigation. -func Cause(err error) error { - type causer interface { - Cause() error - } - - for err != nil { - cause, ok := err.(causer) - if !ok { - break - } - err = cause.Cause() - } - return err -} diff --git a/vendor/github.com/pkg/errors/stack.go b/vendor/github.com/pkg/errors/stack.go deleted file mode 100644 index 2874a048..00000000 --- a/vendor/github.com/pkg/errors/stack.go +++ /dev/null @@ -1,147 +0,0 @@ -package errors - -import ( - "fmt" - "io" - "path" - "runtime" - "strings" -) - -// Frame represents a program counter inside a stack frame. -type Frame uintptr - -// pc returns the program counter for this frame; -// multiple frames may have the same PC value. -func (f Frame) pc() uintptr { return uintptr(f) - 1 } - -// file returns the full path to the file that contains the -// function for this Frame's pc. -func (f Frame) file() string { - fn := runtime.FuncForPC(f.pc()) - if fn == nil { - return "unknown" - } - file, _ := fn.FileLine(f.pc()) - return file -} - -// line returns the line number of source code of the -// function for this Frame's pc. -func (f Frame) line() int { - fn := runtime.FuncForPC(f.pc()) - if fn == nil { - return 0 - } - _, line := fn.FileLine(f.pc()) - return line -} - -// Format formats the frame according to the fmt.Formatter interface. -// -// %s source file -// %d source line -// %n function name -// %v equivalent to %s:%d -// -// Format accepts flags that alter the printing of some verbs, as follows: -// -// %+s function name and path of source file relative to the compile time -// GOPATH separated by \n\t (\n\t) -// %+v equivalent to %+s:%d -func (f Frame) Format(s fmt.State, verb rune) { - switch verb { - case 's': - switch { - case s.Flag('+'): - pc := f.pc() - fn := runtime.FuncForPC(pc) - if fn == nil { - io.WriteString(s, "unknown") - } else { - file, _ := fn.FileLine(pc) - fmt.Fprintf(s, "%s\n\t%s", fn.Name(), file) - } - default: - io.WriteString(s, path.Base(f.file())) - } - case 'd': - fmt.Fprintf(s, "%d", f.line()) - case 'n': - name := runtime.FuncForPC(f.pc()).Name() - io.WriteString(s, funcname(name)) - case 'v': - f.Format(s, 's') - io.WriteString(s, ":") - f.Format(s, 'd') - } -} - -// StackTrace is stack of Frames from innermost (newest) to outermost (oldest). -type StackTrace []Frame - -// Format formats the stack of Frames according to the fmt.Formatter interface. -// -// %s lists source files for each Frame in the stack -// %v lists the source file and line number for each Frame in the stack -// -// Format accepts flags that alter the printing of some verbs, as follows: -// -// %+v Prints filename, function, and line number for each Frame in the stack. -func (st StackTrace) Format(s fmt.State, verb rune) { - switch verb { - case 'v': - switch { - case s.Flag('+'): - for _, f := range st { - fmt.Fprintf(s, "\n%+v", f) - } - case s.Flag('#'): - fmt.Fprintf(s, "%#v", []Frame(st)) - default: - fmt.Fprintf(s, "%v", []Frame(st)) - } - case 's': - fmt.Fprintf(s, "%s", []Frame(st)) - } -} - -// stack represents a stack of program counters. -type stack []uintptr - -func (s *stack) Format(st fmt.State, verb rune) { - switch verb { - case 'v': - switch { - case st.Flag('+'): - for _, pc := range *s { - f := Frame(pc) - fmt.Fprintf(st, "\n%+v", f) - } - } - } -} - -func (s *stack) StackTrace() StackTrace { - f := make([]Frame, len(*s)) - for i := 0; i < len(f); i++ { - f[i] = Frame((*s)[i]) - } - return f -} - -func callers() *stack { - const depth = 32 - var pcs [depth]uintptr - n := runtime.Callers(3, pcs[:]) - var st stack = pcs[0:n] - return &st -} - -// funcname removes the path prefix component of a function's name reported by func.Name(). -func funcname(name string) string { - i := strings.LastIndex(name, "/") - name = name[i+1:] - i = strings.Index(name, ".") - return name[i+1:] -} diff --git a/vendor/github.com/urfave/cli/CHANGELOG.md b/vendor/github.com/urfave/cli/CHANGELOG.md deleted file mode 100644 index 401eae5a..00000000 --- a/vendor/github.com/urfave/cli/CHANGELOG.md +++ /dev/null @@ -1,435 +0,0 @@ -# Change Log - -**ATTN**: This project uses [semantic versioning](http://semver.org/). - -## [Unreleased] - -## 1.20.0 - 2017-08-10 - -### Fixed - -* `HandleExitCoder` is now correctly iterates over all errors in - a `MultiError`. The exit code is the exit code of the last error or `1` if - there are no `ExitCoder`s in the `MultiError`. -* Fixed YAML file loading on Windows (previously would fail validate the file path) -* Subcommand `Usage`, `Description`, `ArgsUsage`, `OnUsageError` correctly - propogated -* `ErrWriter` is now passed downwards through command structure to avoid the - need to redefine it -* Pass `Command` context into `OnUsageError` rather than parent context so that - all fields are avaiable -* Errors occuring in `Before` funcs are no longer double printed -* Use `UsageText` in the help templates for commands and subcommands if - defined; otherwise build the usage as before (was previously ignoring this - field) -* `IsSet` and `GlobalIsSet` now correctly return whether a flag is set if - a program calls `Set` or `GlobalSet` directly after flag parsing (would - previously only return `true` if the flag was set during parsing) - -### Changed - -* No longer exit the program on command/subcommand error if the error raised is - not an `OsExiter`. This exiting behavior was introduced in 1.19.0, but was - determined to be a regression in functionality. See [the - PR](https://github.com/urfave/cli/pull/595) for discussion. - -### Added - -* `CommandsByName` type was added to make it easy to sort `Command`s by name, - alphabetically -* `altsrc` now handles loading of string and int arrays from TOML -* Support for definition of custom help templates for `App` via - `CustomAppHelpTemplate` -* Support for arbitrary key/value fields on `App` to be used with - `CustomAppHelpTemplate` via `ExtraInfo` -* `HelpFlag`, `VersionFlag`, and `BashCompletionFlag` changed to explictly be - `cli.Flag`s allowing for the use of custom flags satisfying the `cli.Flag` - interface to be used. - - -## [1.19.1] - 2016-11-21 - -### Fixed - -- Fixes regression introduced in 1.19.0 where using an `ActionFunc` as - the `Action` for a command would cause it to error rather than calling the - function. Should not have a affected declarative cases using `func(c - *cli.Context) err)`. -- Shell completion now handles the case where the user specifies - `--generate-bash-completion` immediately after a flag that takes an argument. - Previously it call the application with `--generate-bash-completion` as the - flag value. - -## [1.19.0] - 2016-11-19 -### Added -- `FlagsByName` was added to make it easy to sort flags (e.g. `sort.Sort(cli.FlagsByName(app.Flags))`) -- A `Description` field was added to `App` for a more detailed description of - the application (similar to the existing `Description` field on `Command`) -- Flag type code generation via `go generate` -- Write to stderr and exit 1 if action returns non-nil error -- Added support for TOML to the `altsrc` loader -- `SkipArgReorder` was added to allow users to skip the argument reordering. - This is useful if you want to consider all "flags" after an argument as - arguments rather than flags (the default behavior of the stdlib `flag` - library). This is backported functionality from the [removal of the flag - reordering](https://github.com/urfave/cli/pull/398) in the unreleased version - 2 -- For formatted errors (those implementing `ErrorFormatter`), the errors will - be formatted during output. Compatible with `pkg/errors`. - -### Changed -- Raise minimum tested/supported Go version to 1.2+ - -### Fixed -- Consider empty environment variables as set (previously environment variables - with the equivalent of `""` would be skipped rather than their value used). -- Return an error if the value in a given environment variable cannot be parsed - as the flag type. Previously these errors were silently swallowed. -- Print full error when an invalid flag is specified (which includes the invalid flag) -- `App.Writer` defaults to `stdout` when `nil` -- If no action is specified on a command or app, the help is now printed instead of `panic`ing -- `App.Metadata` is initialized automatically now (previously was `nil` unless initialized) -- Correctly show help message if `-h` is provided to a subcommand -- `context.(Global)IsSet` now respects environment variables. Previously it - would return `false` if a flag was specified in the environment rather than - as an argument -- Removed deprecation warnings to STDERR to avoid them leaking to the end-user -- `altsrc`s import paths were updated to use `gopkg.in/urfave/cli.v1`. This - fixes issues that occurred when `gopkg.in/urfave/cli.v1` was imported as well - as `altsrc` where Go would complain that the types didn't match - -## [1.18.1] - 2016-08-28 -### Fixed -- Removed deprecation warnings to STDERR to avoid them leaking to the end-user (backported) - -## [1.18.0] - 2016-06-27 -### Added -- `./runtests` test runner with coverage tracking by default -- testing on OS X -- testing on Windows -- `UintFlag`, `Uint64Flag`, and `Int64Flag` types and supporting code - -### Changed -- Use spaces for alignment in help/usage output instead of tabs, making the - output alignment consistent regardless of tab width - -### Fixed -- Printing of command aliases in help text -- Printing of visible flags for both struct and struct pointer flags -- Display the `help` subcommand when using `CommandCategories` -- No longer swallows `panic`s that occur within the `Action`s themselves when - detecting the signature of the `Action` field - -## [1.17.1] - 2016-08-28 -### Fixed -- Removed deprecation warnings to STDERR to avoid them leaking to the end-user - -## [1.17.0] - 2016-05-09 -### Added -- Pluggable flag-level help text rendering via `cli.DefaultFlagStringFunc` -- `context.GlobalBoolT` was added as an analogue to `context.GlobalBool` -- Support for hiding commands by setting `Hidden: true` -- this will hide the - commands in help output - -### Changed -- `Float64Flag`, `IntFlag`, and `DurationFlag` default values are no longer - quoted in help text output. -- All flag types now include `(default: {value})` strings following usage when a - default value can be (reasonably) detected. -- `IntSliceFlag` and `StringSliceFlag` usage strings are now more consistent - with non-slice flag types -- Apps now exit with a code of 3 if an unknown subcommand is specified - (previously they printed "No help topic for...", but still exited 0. This - makes it easier to script around apps built using `cli` since they can trust - that a 0 exit code indicated a successful execution. -- cleanups based on [Go Report Card - feedback](https://goreportcard.com/report/github.com/urfave/cli) - -## [1.16.1] - 2016-08-28 -### Fixed -- Removed deprecation warnings to STDERR to avoid them leaking to the end-user - -## [1.16.0] - 2016-05-02 -### Added -- `Hidden` field on all flag struct types to omit from generated help text - -### Changed -- `BashCompletionFlag` (`--enable-bash-completion`) is now omitted from -generated help text via the `Hidden` field - -### Fixed -- handling of error values in `HandleAction` and `HandleExitCoder` - -## [1.15.0] - 2016-04-30 -### Added -- This file! -- Support for placeholders in flag usage strings -- `App.Metadata` map for arbitrary data/state management -- `Set` and `GlobalSet` methods on `*cli.Context` for altering values after -parsing. -- Support for nested lookup of dot-delimited keys in structures loaded from -YAML. - -### Changed -- The `App.Action` and `Command.Action` now prefer a return signature of -`func(*cli.Context) error`, as defined by `cli.ActionFunc`. If a non-nil -`error` is returned, there may be two outcomes: - - If the error fulfills `cli.ExitCoder`, then `os.Exit` will be called - automatically - - Else the error is bubbled up and returned from `App.Run` -- Specifying an `Action` with the legacy return signature of -`func(*cli.Context)` will produce a deprecation message to stderr -- Specifying an `Action` that is not a `func` type will produce a non-zero exit -from `App.Run` -- Specifying an `Action` func that has an invalid (input) signature will -produce a non-zero exit from `App.Run` - -### Deprecated -- -`cli.App.RunAndExitOnError`, which should now be done by returning an error -that fulfills `cli.ExitCoder` to `cli.App.Run`. -- the legacy signature for -`cli.App.Action` of `func(*cli.Context)`, which should now have a return -signature of `func(*cli.Context) error`, as defined by `cli.ActionFunc`. - -### Fixed -- Added missing `*cli.Context.GlobalFloat64` method - -## [1.14.0] - 2016-04-03 (backfilled 2016-04-25) -### Added -- Codebeat badge -- Support for categorization via `CategorizedHelp` and `Categories` on app. - -### Changed -- Use `filepath.Base` instead of `path.Base` in `Name` and `HelpName`. - -### Fixed -- Ensure version is not shown in help text when `HideVersion` set. - -## [1.13.0] - 2016-03-06 (backfilled 2016-04-25) -### Added -- YAML file input support. -- `NArg` method on context. - -## [1.12.0] - 2016-02-17 (backfilled 2016-04-25) -### Added -- Custom usage error handling. -- Custom text support in `USAGE` section of help output. -- Improved help messages for empty strings. -- AppVeyor CI configuration. - -### Changed -- Removed `panic` from default help printer func. -- De-duping and optimizations. - -### Fixed -- Correctly handle `Before`/`After` at command level when no subcommands. -- Case of literal `-` argument causing flag reordering. -- Environment variable hints on Windows. -- Docs updates. - -## [1.11.1] - 2015-12-21 (backfilled 2016-04-25) -### Changed -- Use `path.Base` in `Name` and `HelpName` -- Export `GetName` on flag types. - -### Fixed -- Flag parsing when skipping is enabled. -- Test output cleanup. -- Move completion check to account for empty input case. - -## [1.11.0] - 2015-11-15 (backfilled 2016-04-25) -### Added -- Destination scan support for flags. -- Testing against `tip` in Travis CI config. - -### Changed -- Go version in Travis CI config. - -### Fixed -- Removed redundant tests. -- Use correct example naming in tests. - -## [1.10.2] - 2015-10-29 (backfilled 2016-04-25) -### Fixed -- Remove unused var in bash completion. - -## [1.10.1] - 2015-10-21 (backfilled 2016-04-25) -### Added -- Coverage and reference logos in README. - -### Fixed -- Use specified values in help and version parsing. -- Only display app version and help message once. - -## [1.10.0] - 2015-10-06 (backfilled 2016-04-25) -### Added -- More tests for existing functionality. -- `ArgsUsage` at app and command level for help text flexibility. - -### Fixed -- Honor `HideHelp` and `HideVersion` in `App.Run`. -- Remove juvenile word from README. - -## [1.9.0] - 2015-09-08 (backfilled 2016-04-25) -### Added -- `FullName` on command with accompanying help output update. -- Set default `$PROG` in bash completion. - -### Changed -- Docs formatting. - -### Fixed -- Removed self-referential imports in tests. - -## [1.8.0] - 2015-06-30 (backfilled 2016-04-25) -### Added -- Support for `Copyright` at app level. -- `Parent` func at context level to walk up context lineage. - -### Fixed -- Global flag processing at top level. - -## [1.7.1] - 2015-06-11 (backfilled 2016-04-25) -### Added -- Aggregate errors from `Before`/`After` funcs. -- Doc comments on flag structs. -- Include non-global flags when checking version and help. -- Travis CI config updates. - -### Fixed -- Ensure slice type flags have non-nil values. -- Collect global flags from the full command hierarchy. -- Docs prose. - -## [1.7.0] - 2015-05-03 (backfilled 2016-04-25) -### Changed -- `HelpPrinter` signature includes output writer. - -### Fixed -- Specify go 1.1+ in docs. -- Set `Writer` when running command as app. - -## [1.6.0] - 2015-03-23 (backfilled 2016-04-25) -### Added -- Multiple author support. -- `NumFlags` at context level. -- `Aliases` at command level. - -### Deprecated -- `ShortName` at command level. - -### Fixed -- Subcommand help output. -- Backward compatible support for deprecated `Author` and `Email` fields. -- Docs regarding `Names`/`Aliases`. - -## [1.5.0] - 2015-02-20 (backfilled 2016-04-25) -### Added -- `After` hook func support at app and command level. - -### Fixed -- Use parsed context when running command as subcommand. -- Docs prose. - -## [1.4.1] - 2015-01-09 (backfilled 2016-04-25) -### Added -- Support for hiding `-h / --help` flags, but not `help` subcommand. -- Stop flag parsing after `--`. - -### Fixed -- Help text for generic flags to specify single value. -- Use double quotes in output for defaults. -- Use `ParseInt` instead of `ParseUint` for int environment var values. -- Use `0` as base when parsing int environment var values. - -## [1.4.0] - 2014-12-12 (backfilled 2016-04-25) -### Added -- Support for environment variable lookup "cascade". -- Support for `Stdout` on app for output redirection. - -### Fixed -- Print command help instead of app help in `ShowCommandHelp`. - -## [1.3.1] - 2014-11-13 (backfilled 2016-04-25) -### Added -- Docs and example code updates. - -### Changed -- Default `-v / --version` flag made optional. - -## [1.3.0] - 2014-08-10 (backfilled 2016-04-25) -### Added -- `FlagNames` at context level. -- Exposed `VersionPrinter` var for more control over version output. -- Zsh completion hook. -- `AUTHOR` section in default app help template. -- Contribution guidelines. -- `DurationFlag` type. - -## [1.2.0] - 2014-08-02 -### Added -- Support for environment variable defaults on flags plus tests. - -## [1.1.0] - 2014-07-15 -### Added -- Bash completion. -- Optional hiding of built-in help command. -- Optional skipping of flag parsing at command level. -- `Author`, `Email`, and `Compiled` metadata on app. -- `Before` hook func support at app and command level. -- `CommandNotFound` func support at app level. -- Command reference available on context. -- `GenericFlag` type. -- `Float64Flag` type. -- `BoolTFlag` type. -- `IsSet` flag helper on context. -- More flag lookup funcs at context level. -- More tests & docs. - -### Changed -- Help template updates to account for presence/absence of flags. -- Separated subcommand help template. -- Exposed `HelpPrinter` var for more control over help output. - -## [1.0.0] - 2013-11-01 -### Added -- `help` flag in default app flag set and each command flag set. -- Custom handling of argument parsing errors. -- Command lookup by name at app level. -- `StringSliceFlag` type and supporting `StringSlice` type. -- `IntSliceFlag` type and supporting `IntSlice` type. -- Slice type flag lookups by name at context level. -- Export of app and command help functions. -- More tests & docs. - -## 0.1.0 - 2013-07-22 -### Added -- Initial implementation. - -[Unreleased]: https://github.com/urfave/cli/compare/v1.18.0...HEAD -[1.18.0]: https://github.com/urfave/cli/compare/v1.17.0...v1.18.0 -[1.17.0]: https://github.com/urfave/cli/compare/v1.16.0...v1.17.0 -[1.16.0]: https://github.com/urfave/cli/compare/v1.15.0...v1.16.0 -[1.15.0]: https://github.com/urfave/cli/compare/v1.14.0...v1.15.0 -[1.14.0]: https://github.com/urfave/cli/compare/v1.13.0...v1.14.0 -[1.13.0]: https://github.com/urfave/cli/compare/v1.12.0...v1.13.0 -[1.12.0]: https://github.com/urfave/cli/compare/v1.11.1...v1.12.0 -[1.11.1]: https://github.com/urfave/cli/compare/v1.11.0...v1.11.1 -[1.11.0]: https://github.com/urfave/cli/compare/v1.10.2...v1.11.0 -[1.10.2]: https://github.com/urfave/cli/compare/v1.10.1...v1.10.2 -[1.10.1]: https://github.com/urfave/cli/compare/v1.10.0...v1.10.1 -[1.10.0]: https://github.com/urfave/cli/compare/v1.9.0...v1.10.0 -[1.9.0]: https://github.com/urfave/cli/compare/v1.8.0...v1.9.0 -[1.8.0]: https://github.com/urfave/cli/compare/v1.7.1...v1.8.0 -[1.7.1]: https://github.com/urfave/cli/compare/v1.7.0...v1.7.1 -[1.7.0]: https://github.com/urfave/cli/compare/v1.6.0...v1.7.0 -[1.6.0]: https://github.com/urfave/cli/compare/v1.5.0...v1.6.0 -[1.5.0]: https://github.com/urfave/cli/compare/v1.4.1...v1.5.0 -[1.4.1]: https://github.com/urfave/cli/compare/v1.4.0...v1.4.1 -[1.4.0]: https://github.com/urfave/cli/compare/v1.3.1...v1.4.0 -[1.3.1]: https://github.com/urfave/cli/compare/v1.3.0...v1.3.1 -[1.3.0]: https://github.com/urfave/cli/compare/v1.2.0...v1.3.0 -[1.2.0]: https://github.com/urfave/cli/compare/v1.1.0...v1.2.0 -[1.1.0]: https://github.com/urfave/cli/compare/v1.0.0...v1.1.0 -[1.0.0]: https://github.com/urfave/cli/compare/v0.1.0...v1.0.0 diff --git a/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md b/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md deleted file mode 100644 index 41ba294f..00000000 --- a/vendor/github.com/urfave/cli/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, race, -religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be -reviewed and investigated and will result in a response that is deemed necessary -and appropriate to the circumstances. The project team is obligated to maintain -confidentiality with regard to the reporter of an incident. Further details of -specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - diff --git a/vendor/github.com/urfave/cli/CONTRIBUTING.md b/vendor/github.com/urfave/cli/CONTRIBUTING.md deleted file mode 100644 index 329195ee..00000000 --- a/vendor/github.com/urfave/cli/CONTRIBUTING.md +++ /dev/null @@ -1,19 +0,0 @@ -## Contributing - -**NOTE**: the primary maintainer(s) may be found in -[./MAINTAINERS.md](./MAINTAINERS.md). - -Feel free to put up a pull request to fix a bug or maybe add a feature. I will -give it a code review and make sure that it does not break backwards -compatibility. If I or any other collaborators agree that it is in line with -the vision of the project, we will work with you to get the code into -a mergeable state and merge it into the master branch. - -If you have contributed something significant to the project, we will most -likely add you as a collaborator. As a collaborator you are given the ability -to merge others pull requests. It is very important that new code does not -break existing code, so be careful about what code you do choose to merge. - -If you feel like you have contributed to the project but have not yet been added -as a collaborator, we probably forgot to add you :sweat_smile:. Please open an -issue! diff --git a/vendor/github.com/urfave/cli/LICENSE b/vendor/github.com/urfave/cli/LICENSE deleted file mode 100644 index 42a597e2..00000000 --- a/vendor/github.com/urfave/cli/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Jeremy Saenz & Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/urfave/cli/MAINTAINERS.md b/vendor/github.com/urfave/cli/MAINTAINERS.md deleted file mode 100644 index f6bdd99c..00000000 --- a/vendor/github.com/urfave/cli/MAINTAINERS.md +++ /dev/null @@ -1 +0,0 @@ -- @meatballhat diff --git a/vendor/github.com/urfave/cli/README.md b/vendor/github.com/urfave/cli/README.md deleted file mode 100644 index f2baef4f..00000000 --- a/vendor/github.com/urfave/cli/README.md +++ /dev/null @@ -1,1526 +0,0 @@ -cli -=== - -[![Build Status](https://travis-ci.org/urfave/cli.svg?branch=master)](https://travis-ci.org/urfave/cli) -[![Windows Build Status](https://ci.appveyor.com/api/projects/status/rtgk5xufi932pb2v?svg=true)](https://ci.appveyor.com/project/urfave/cli) -[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) -[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) -[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) -[![top level coverage](https://gocover.io/_badge/github.com/urfave/cli?0 "top level coverage")](http://gocover.io/github.com/urfave/cli) / -[![altsrc coverage](https://gocover.io/_badge/github.com/urfave/cli/altsrc?0 "altsrc coverage")](http://gocover.io/github.com/urfave/cli/altsrc) - -This is the library formerly known as `github.com/codegangsta/cli` -- Github -will automatically redirect requests to this repository, but we recommend -updating your references for clarity. - -cli is a simple, fast, and fun package for building command line apps in Go. The -goal is to enable developers to write fast and distributable command line -applications in an expressive way. - - - -- [Overview](#overview) -- [Installation](#installation) - * [Supported platforms](#supported-platforms) - * [Using the `v2` branch](#using-the-v2-branch) - * [Pinning to the `v1` releases](#pinning-to-the-v1-releases) -- [Getting Started](#getting-started) -- [Examples](#examples) - * [Arguments](#arguments) - * [Flags](#flags) - + [Placeholder Values](#placeholder-values) - + [Alternate Names](#alternate-names) - + [Ordering](#ordering) - + [Values from the Environment](#values-from-the-environment) - + [Values from files](#values-from-files) - + [Values from alternate input sources (YAML, TOML, and others)](#values-from-alternate-input-sources-yaml-toml-and-others) - + [Precedence](#precedence) - * [Subcommands](#subcommands) - * [Subcommands categories](#subcommands-categories) - * [Exit code](#exit-code) - * [Bash Completion](#bash-completion) - + [Enabling](#enabling) - + [Distribution](#distribution) - + [Customization](#customization) - * [Generated Help Text](#generated-help-text) - + [Customization](#customization-1) - * [Version Flag](#version-flag) - + [Customization](#customization-2) - + [Full API Example](#full-api-example) - * [Combining short Bool options](#combining-short-bool-options) -- [Contribution Guidelines](#contribution-guidelines) - - - -## Overview - -Command line apps are usually so tiny that there is absolutely no reason why -your code should *not* be self-documenting. Things like generating help text and -parsing command flags/options should not hinder productivity when writing a -command line app. - -**This is where cli comes into play.** cli makes command line programming fun, -organized, and expressive! - -## Installation - -Make sure you have a working Go environment. Go version 1.2+ is supported. [See -the install instructions for Go](http://golang.org/doc/install.html). - -To install cli, simply run: -``` -$ go get github.com/urfave/cli -``` - -Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can -be easily used: -``` -export PATH=$PATH:$GOPATH/bin -``` - -### Supported platforms - -cli is tested against multiple versions of Go on Linux, and against the latest -released version of Go on OS X and Windows. For full details, see -[`./.travis.yml`](./.travis.yml) and [`./appveyor.yml`](./appveyor.yml). - -### Using the `v2` branch - -**Warning**: The `v2` branch is currently unreleased and considered unstable. - -There is currently a long-lived branch named `v2` that is intended to land as -the new `master` branch once development there has settled down. The current -`master` branch (mirrored as `v1`) is being manually merged into `v2` on -an irregular human-based schedule, but generally if one wants to "upgrade" to -`v2` *now* and accept the volatility (read: "awesomeness") that comes along with -that, please use whatever version pinning of your preference, such as via -`gopkg.in`: - -``` -$ go get gopkg.in/urfave/cli.v2 -``` - -``` go -... -import ( - "gopkg.in/urfave/cli.v2" // imports as package "cli" -) -... -``` - -### Pinning to the `v1` releases - -Similarly to the section above describing use of the `v2` branch, if one wants -to avoid any unexpected compatibility pains once `v2` becomes `master`, then -pinning to `v1` is an acceptable option, e.g.: - -``` -$ go get gopkg.in/urfave/cli.v1 -``` - -``` go -... -import ( - "gopkg.in/urfave/cli.v1" // imports as package "cli" -) -... -``` - -This will pull the latest tagged `v1` release (e.g. `v1.18.1` at the time of writing). - -## Getting Started - -One of the philosophies behind cli is that an API should be playful and full of -discovery. So a cli app can be as little as one line of code in `main()`. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -This app will run and show help text, but is not very useful. Let's give an -action to execute and some help documentation: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "boom" - app.Usage = "make an explosive entrance" - app.Action = func(c *cli.Context) error { - fmt.Println("boom! I say!") - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Running this already gives you a ton of functionality, plus support for things -like subcommands and flags, which are covered below. - -## Examples - -Being a programmer can be a lonely job. Thankfully by the power of automation -that is not the case! Let's create a greeter app to fend off our demons of -loneliness! - -Start by creating a directory named `greet`, and within it, add a file, -`greet.go` with the following code in it: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "greet" - app.Usage = "fight the loneliness!" - app.Action = func(c *cli.Context) error { - fmt.Println("Hello friend!") - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Install our command to the `$GOPATH/bin` directory: - -``` -$ go install -``` - -Finally run our new command: - -``` -$ greet -Hello friend! -``` - -cli also generates neat help text: - -``` -$ greet help -NAME: - greet - fight the loneliness! - -USAGE: - greet [global options] command [command options] [arguments...] - -VERSION: - 0.0.0 - -COMMANDS: - help, h Shows a list of commands or help for one command - -GLOBAL OPTIONS - --version Shows version information -``` - -### Arguments - -You can lookup arguments by calling the `Args` function on `cli.Context`, e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Action = func(c *cli.Context) error { - fmt.Printf("Hello %q", c.Args().Get(0)) - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Flags - -Setting and querying flags is simple. - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - }, - } - - app.Action = func(c *cli.Context) error { - name := "Nefertiti" - if c.NArg() > 0 { - name = c.Args().Get(0) - } - if c.String("lang") == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -You can also set a destination variable for a flag, to which the content will be -scanned. - - -``` go -package main - -import ( - "log" - "os" - "fmt" - - "github.com/urfave/cli" -) - -func main() { - var language string - - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang", - Value: "english", - Usage: "language for the greeting", - Destination: &language, - }, - } - - app.Action = func(c *cli.Context) error { - name := "someone" - if c.NArg() > 0 { - name = c.Args()[0] - } - if language == "spanish" { - fmt.Println("Hola", name) - } else { - fmt.Println("Hello", name) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -See full list of flags at http://godoc.org/github.com/urfave/cli - -#### Placeholder Values - -Sometimes it's useful to specify a flag's value within the usage string itself. -Such placeholders are indicated with back quotes. - -For example this: - - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE -``` - -Note that only the first placeholder is used. Subsequent back-quoted words will -be left as-is. - -#### Alternate Names - -You can set alternate (or short) names for flags by providing a comma-delimited -list for the `Name`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -That flag can then be set with `--lang spanish` or `-l spanish`. Note that -giving two different forms of the same flag in the same command invocation is an -error. - -#### Ordering - -Flags for the application and commands are shown in the order they are defined. -However, it's possible to sort them from outside this library by using `FlagsByName` -or `CommandsByName` with `sort`. - -For example this: - - -``` go -package main - -import ( - "log" - "os" - "sort" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "Language for the greeting", - }, - cli.StringFlag{ - Name: "config, c", - Usage: "Load configuration from `FILE`", - }, - } - - app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - return nil - }, - }, - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - return nil - }, - }, - } - - sort.Sort(cli.FlagsByName(app.Flags)) - sort.Sort(cli.CommandsByName(app.Commands)) - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will result in help output like: - -``` ---config FILE, -c FILE Load configuration from FILE ---lang value, -l value Language for the greeting (default: "english") -``` - -#### Values from the Environment - -You can also have the default value set from the environment via `EnvVar`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "APP_LANG", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -The `EnvVar` may also be given as a comma-delimited "cascade", where the first -environment variable that resolves is used as the default. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "lang, l", - Value: "english", - Usage: "language for the greeting", - EnvVar: "LEGACY_COMPAT_LANG,APP_LANG,LANG", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Values from files - -You can also have the default value set from file via `FilePath`. e.g. - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Flags = []cli.Flag { - cli.StringFlag{ - Name: "password, p", - Usage: "password for the mysql database", - FilePath: "/etc/mysql/password", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Note that default values set from file (e.g. `FilePath`) take precedence over -default values set from the enviornment (e.g. `EnvVar`). - -#### Values from alternate input sources (YAML, TOML, and others) - -There is a separate package altsrc that adds support for getting flag values -from other file input sources. - -Currently supported input source formats: -* YAML -* TOML - -In order to get values for a flag from an alternate input source the following -code would be added to wrap an existing cli.Flag like below: - -``` go - altsrc.NewIntFlag(cli.IntFlag{Name: "test"}) -``` - -Initialization must also occur for these flags. Below is an example initializing -getting data from a yaml file below. - -``` go - command.Before = altsrc.InitInputSourceWithContext(command.Flags, NewYamlSourceFromFlagFunc("load")) -``` - -The code above will use the "load" string as a flag name to get the file name of -a yaml file from the cli.Context. It will then use that file name to initialize -the yaml input source for any flags that are defined on that command. As a note -the "load" flag used would also have to be defined on the command flags in order -for this code snipped to work. - -Currently only YAML and JSON files are supported but developers can add support -for other input sources by implementing the altsrc.InputSourceContext for their -given sources. - -Here is a more complete sample of a command using YAML support: - - -``` go -package notmain - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" - "github.com/urfave/cli/altsrc" -) - -func main() { - app := cli.NewApp() - - flags := []cli.Flag{ - altsrc.NewIntFlag(cli.IntFlag{Name: "test"}), - cli.StringFlag{Name: "load"}, - } - - app.Action = func(c *cli.Context) error { - fmt.Println("yaml ist rad") - return nil - } - - app.Before = altsrc.InitInputSourceWithContext(flags, altsrc.NewYamlSourceFromFlagFunc("load")) - app.Flags = flags - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Precedence - -The precedence for flag value sources is as follows (highest to lowest): - -0. Command line flag value from user -0. Environment variable (if specified) -0. Configuration file (if specified) -0. Default defined on the flag - -### Subcommands - -Subcommands can be defined for a more git-like command line app. - - -```go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Commands = []cli.Command{ - { - Name: "add", - Aliases: []string{"a"}, - Usage: "add a task to the list", - Action: func(c *cli.Context) error { - fmt.Println("added task: ", c.Args().First()) - return nil - }, - }, - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - }, - { - Name: "template", - Aliases: []string{"t"}, - Usage: "options for task templates", - Subcommands: []cli.Command{ - { - Name: "add", - Usage: "add a new template", - Action: func(c *cli.Context) error { - fmt.Println("new task template: ", c.Args().First()) - return nil - }, - }, - { - Name: "remove", - Usage: "remove an existing template", - Action: func(c *cli.Context) error { - fmt.Println("removed task template: ", c.Args().First()) - return nil - }, - }, - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Subcommands categories - -For additional organization in apps that have many subcommands, you can -associate a category for each command to group them together in the help -output. - -E.g. - -```go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - - app.Commands = []cli.Command{ - { - Name: "noop", - }, - { - Name: "add", - Category: "Template actions", - }, - { - Name: "remove", - Category: "Template actions", - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Will include: - -``` -COMMANDS: - noop - - Template actions: - add - remove -``` - -### Exit code - -Calling `App.Run` will not automatically call `os.Exit`, which means that by -default the exit code will "fall through" to being `0`. An explicit exit code -may be set by returning a non-nil error that fulfills `cli.ExitCoder`, *or* a -`cli.MultiError` that includes an error that fulfills `cli.ExitCoder`, e.g.: - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Flags = []cli.Flag{ - cli.BoolTFlag{ - Name: "ginger-crouton", - Usage: "is it in the soup?", - }, - } - app.Action = func(ctx *cli.Context) error { - if !ctx.Bool("ginger-crouton") { - return cli.NewExitError("it is not in the soup", 86) - } - return nil - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Bash Completion - -You can enable completion commands by setting the `EnableBashCompletion` -flag on the `App` object. By default, this setting will only auto-complete to -show an app's subcommands, but you can write your own completion methods for -the App or its subcommands. - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - tasks := []string{"cook", "clean", "laundry", "eat", "sleep", "code"} - - app := cli.NewApp() - app.EnableBashCompletion = true - app.Commands = []cli.Command{ - { - Name: "complete", - Aliases: []string{"c"}, - Usage: "complete a task on the list", - Action: func(c *cli.Context) error { - fmt.Println("completed task: ", c.Args().First()) - return nil - }, - BashComplete: func(c *cli.Context) { - // This will complete if no args are passed - if c.NArg() > 0 { - return - } - for _, t := range tasks { - fmt.Println(t) - } - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Enabling - -Source the `autocomplete/bash_autocomplete` file in your `.bashrc` file while -setting the `PROG` variable to the name of your program: - -`PROG=myprogram source /.../cli/autocomplete/bash_autocomplete` - -#### Distribution - -Copy `autocomplete/bash_autocomplete` into `/etc/bash_completion.d/` and rename -it to the name of the program you wish to add autocomplete support for (or -automatically install it there if you are distributing a package). Don't forget -to source the file to make it active in the current shell. - -``` -sudo cp src/bash_autocomplete /etc/bash_completion.d/ -source /etc/bash_completion.d/ -``` - -Alternatively, you can just document that users should source the generic -`autocomplete/bash_autocomplete` in their bash configuration with `$PROG` set -to the name of their program (as above). - -#### Customization - -The default bash completion flag (`--generate-bash-completion`) is defined as -`cli.BashCompletionFlag`, and may be redefined if desired, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.BashCompletionFlag = cli.BoolFlag{ - Name: "compgen", - Hidden: true, - } - - app := cli.NewApp() - app.EnableBashCompletion = true - app.Commands = []cli.Command{ - { - Name: "wat", - }, - } - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Generated Help Text - -The default help flag (`-h/--help`) is defined as `cli.HelpFlag` and is checked -by the cli internals in order to print generated help text for the app, command, -or subcommand, and break execution. - -#### Customization - -All of the help text generation may be customized, and at multiple levels. The -templates are exposed as variables `AppHelpTemplate`, `CommandHelpTemplate`, and -`SubcommandHelpTemplate` which may be reassigned or augmented, and full override -is possible by assigning a compatible func to the `cli.HelpPrinter` variable, -e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "io" - "os" - - "github.com/urfave/cli" -) - -func main() { - // EXAMPLE: Append to an existing template - cli.AppHelpTemplate = fmt.Sprintf(`%s - -WEBSITE: http://awesometown.example.com - -SUPPORT: support@awesometown.example.com - -`, cli.AppHelpTemplate) - - // EXAMPLE: Override a template - cli.AppHelpTemplate = `NAME: - {{.Name}} - {{.Usage}} -USAGE: - {{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}} - {{if len .Authors}} -AUTHOR: - {{range .Authors}}{{ . }}{{end}} - {{end}}{{if .Commands}} -COMMANDS: -{{range .Commands}}{{if not .HideHelp}} {{join .Names ", "}}{{ "\t"}}{{.Usage}}{{ "\n" }}{{end}}{{end}}{{end}}{{if .VisibleFlags}} -GLOBAL OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}}{{if .Copyright }} -COPYRIGHT: - {{.Copyright}} - {{end}}{{if .Version}} -VERSION: - {{.Version}} - {{end}} -` - - // EXAMPLE: Replace the `HelpPrinter` func - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Println("Ha HA. I pwnd the help!!1") - } - - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -The default flag may be customized to something other than `-h/--help` by -setting `cli.HelpFlag`, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.HelpFlag = cli.BoolFlag{ - Name: "halp, haaaaalp", - Usage: "HALP", - EnvVar: "SHOW_HALP,HALPPLZ", - } - - err := cli.NewApp().Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -### Version Flag - -The default version flag (`-v/--version`) is defined as `cli.VersionFlag`, which -is checked by the cli internals in order to print the `App.Version` via -`cli.VersionPrinter` and break execution. - -#### Customization - -The default flag may be customized to something other than `-v/--version` by -setting `cli.VersionFlag`, e.g.: - - -``` go -package main - -import ( - "log" - "os" - - "github.com/urfave/cli" -) - -func main() { - cli.VersionFlag = cli.BoolFlag{ - Name: "print-version, V", - Usage: "print only the version", - } - - app := cli.NewApp() - app.Name = "partay" - app.Version = "19.99.0" - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -Alternatively, the version printer at `cli.VersionPrinter` may be overridden, e.g.: - - -``` go -package main - -import ( - "fmt" - "log" - "os" - - "github.com/urfave/cli" -) - -var ( - Revision = "fafafaf" -) - -func main() { - cli.VersionPrinter = func(c *cli.Context) { - fmt.Printf("version=%s revision=%s\n", c.App.Version, Revision) - } - - app := cli.NewApp() - app.Name = "partay" - app.Version = "19.99.0" - err := app.Run(os.Args) - if err != nil { - log.Fatal(err) - } -} -``` - -#### Full API Example - -**Notice**: This is a contrived (functioning) example meant strictly for API -demonstration purposes. Use of one's imagination is encouraged. - - -``` go -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "io/ioutil" - "os" - "time" - - "github.com/urfave/cli" -) - -func init() { - cli.AppHelpTemplate += "\nCUSTOMIZED: you bet ur muffins\n" - cli.CommandHelpTemplate += "\nYMMV\n" - cli.SubcommandHelpTemplate += "\nor something\n" - - cli.HelpFlag = cli.BoolFlag{Name: "halp"} - cli.BashCompletionFlag = cli.BoolFlag{Name: "compgen", Hidden: true} - cli.VersionFlag = cli.BoolFlag{Name: "print-version, V"} - - cli.HelpPrinter = func(w io.Writer, templ string, data interface{}) { - fmt.Fprintf(w, "best of luck to you\n") - } - cli.VersionPrinter = func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "version=%s\n", c.App.Version) - } - cli.OsExiter = func(c int) { - fmt.Fprintf(cli.ErrWriter, "refusing to exit %d\n", c) - } - cli.ErrWriter = ioutil.Discard - cli.FlagStringer = func(fl cli.Flag) string { - return fmt.Sprintf("\t\t%s", fl.GetName()) - } -} - -type hexWriter struct{} - -func (w *hexWriter) Write(p []byte) (int, error) { - for _, b := range p { - fmt.Printf("%x", b) - } - fmt.Printf("\n") - - return len(p), nil -} - -type genericType struct{ - s string -} - -func (g *genericType) Set(value string) error { - g.s = value - return nil -} - -func (g *genericType) String() string { - return g.s -} - -func main() { - app := cli.NewApp() - app.Name = "kənˈtrīv" - app.Version = "19.99.0" - app.Compiled = time.Now() - app.Authors = []cli.Author{ - cli.Author{ - Name: "Example Human", - Email: "human@example.com", - }, - } - app.Copyright = "(c) 1999 Serious Enterprise" - app.HelpName = "contrive" - app.Usage = "demonstrate available API" - app.UsageText = "contrive - demonstrating the available API" - app.ArgsUsage = "[args and such]" - app.Commands = []cli.Command{ - cli.Command{ - Name: "doo", - Aliases: []string{"do"}, - Category: "motion", - Usage: "do the doo", - UsageText: "doo - does the dooing", - Description: "no really, there is a lot of dooing to be done", - ArgsUsage: "[arrgh]", - Flags: []cli.Flag{ - cli.BoolFlag{Name: "forever, forevvarr"}, - }, - Subcommands: cli.Commands{ - cli.Command{ - Name: "wop", - Action: wopAction, - }, - }, - SkipFlagParsing: false, - HideHelp: false, - Hidden: false, - HelpName: "doo!", - BashComplete: func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "--better\n") - }, - Before: func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "brace for impact\n") - return nil - }, - After: func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "did we lose anyone?\n") - return nil - }, - Action: func(c *cli.Context) error { - c.Command.FullName() - c.Command.HasName("wop") - c.Command.Names() - c.Command.VisibleFlags() - fmt.Fprintf(c.App.Writer, "dodododododoodododddooooododododooo\n") - if c.Bool("forever") { - c.Command.Run(c) - } - return nil - }, - OnUsageError: func(c *cli.Context, err error, isSubcommand bool) error { - fmt.Fprintf(c.App.Writer, "for shame\n") - return err - }, - }, - } - app.Flags = []cli.Flag{ - cli.BoolFlag{Name: "fancy"}, - cli.BoolTFlag{Name: "fancier"}, - cli.DurationFlag{Name: "howlong, H", Value: time.Second * 3}, - cli.Float64Flag{Name: "howmuch"}, - cli.GenericFlag{Name: "wat", Value: &genericType{}}, - cli.Int64Flag{Name: "longdistance"}, - cli.Int64SliceFlag{Name: "intervals"}, - cli.IntFlag{Name: "distance"}, - cli.IntSliceFlag{Name: "times"}, - cli.StringFlag{Name: "dance-move, d"}, - cli.StringSliceFlag{Name: "names, N"}, - cli.UintFlag{Name: "age"}, - cli.Uint64Flag{Name: "bigage"}, - } - app.EnableBashCompletion = true - app.HideHelp = false - app.HideVersion = false - app.BashComplete = func(c *cli.Context) { - fmt.Fprintf(c.App.Writer, "lipstick\nkiss\nme\nlipstick\nringo\n") - } - app.Before = func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "HEEEERE GOES\n") - return nil - } - app.After = func(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, "Phew!\n") - return nil - } - app.CommandNotFound = func(c *cli.Context, command string) { - fmt.Fprintf(c.App.Writer, "Thar be no %q here.\n", command) - } - app.OnUsageError = func(c *cli.Context, err error, isSubcommand bool) error { - if isSubcommand { - return err - } - - fmt.Fprintf(c.App.Writer, "WRONG: %#v\n", err) - return nil - } - app.Action = func(c *cli.Context) error { - cli.DefaultAppComplete(c) - cli.HandleExitCoder(errors.New("not an exit coder, though")) - cli.ShowAppHelp(c) - cli.ShowCommandCompletions(c, "nope") - cli.ShowCommandHelp(c, "also-nope") - cli.ShowCompletions(c) - cli.ShowSubcommandHelp(c) - cli.ShowVersion(c) - - categories := c.App.Categories() - categories.AddCommand("sounds", cli.Command{ - Name: "bloop", - }) - - for _, category := range c.App.Categories() { - fmt.Fprintf(c.App.Writer, "%s\n", category.Name) - fmt.Fprintf(c.App.Writer, "%#v\n", category.Commands) - fmt.Fprintf(c.App.Writer, "%#v\n", category.VisibleCommands()) - } - - fmt.Printf("%#v\n", c.App.Command("doo")) - if c.Bool("infinite") { - c.App.Run([]string{"app", "doo", "wop"}) - } - - if c.Bool("forevar") { - c.App.RunAsSubcommand(c) - } - c.App.Setup() - fmt.Printf("%#v\n", c.App.VisibleCategories()) - fmt.Printf("%#v\n", c.App.VisibleCommands()) - fmt.Printf("%#v\n", c.App.VisibleFlags()) - - fmt.Printf("%#v\n", c.Args().First()) - if len(c.Args()) > 0 { - fmt.Printf("%#v\n", c.Args()[1]) - } - fmt.Printf("%#v\n", c.Args().Present()) - fmt.Printf("%#v\n", c.Args().Tail()) - - set := flag.NewFlagSet("contrive", 0) - nc := cli.NewContext(c.App, set, c) - - fmt.Printf("%#v\n", nc.Args()) - fmt.Printf("%#v\n", nc.Bool("nope")) - fmt.Printf("%#v\n", nc.BoolT("nerp")) - fmt.Printf("%#v\n", nc.Duration("howlong")) - fmt.Printf("%#v\n", nc.Float64("hay")) - fmt.Printf("%#v\n", nc.Generic("bloop")) - fmt.Printf("%#v\n", nc.Int64("bonk")) - fmt.Printf("%#v\n", nc.Int64Slice("burnks")) - fmt.Printf("%#v\n", nc.Int("bips")) - fmt.Printf("%#v\n", nc.IntSlice("blups")) - fmt.Printf("%#v\n", nc.String("snurt")) - fmt.Printf("%#v\n", nc.StringSlice("snurkles")) - fmt.Printf("%#v\n", nc.Uint("flub")) - fmt.Printf("%#v\n", nc.Uint64("florb")) - fmt.Printf("%#v\n", nc.GlobalBool("global-nope")) - fmt.Printf("%#v\n", nc.GlobalBoolT("global-nerp")) - fmt.Printf("%#v\n", nc.GlobalDuration("global-howlong")) - fmt.Printf("%#v\n", nc.GlobalFloat64("global-hay")) - fmt.Printf("%#v\n", nc.GlobalGeneric("global-bloop")) - fmt.Printf("%#v\n", nc.GlobalInt("global-bips")) - fmt.Printf("%#v\n", nc.GlobalIntSlice("global-blups")) - fmt.Printf("%#v\n", nc.GlobalString("global-snurt")) - fmt.Printf("%#v\n", nc.GlobalStringSlice("global-snurkles")) - - fmt.Printf("%#v\n", nc.FlagNames()) - fmt.Printf("%#v\n", nc.GlobalFlagNames()) - fmt.Printf("%#v\n", nc.GlobalIsSet("wat")) - fmt.Printf("%#v\n", nc.GlobalSet("wat", "nope")) - fmt.Printf("%#v\n", nc.NArg()) - fmt.Printf("%#v\n", nc.NumFlags()) - fmt.Printf("%#v\n", nc.Parent()) - - nc.Set("wat", "also-nope") - - ec := cli.NewExitError("ohwell", 86) - fmt.Fprintf(c.App.Writer, "%d", ec.ExitCode()) - fmt.Printf("made it!\n") - return nil - } - - if os.Getenv("HEXY") != "" { - app.Writer = &hexWriter{} - app.ErrWriter = &hexWriter{} - } - - app.Metadata = map[string]interface{}{ - "layers": "many", - "explicable": false, - "whatever-values": 19.99, - } - - - // ignore error so we don't exit non-zero and break gfmrun README example tests - _ = app.Run(os.Args) -} - -func wopAction(c *cli.Context) error { - fmt.Fprintf(c.App.Writer, ":wave: over here, eh\n") - return nil -} -``` - -### Combining short Bool options - -Traditional use of boolean options using their shortnames look like this: -``` -# cmd foobar -s -o -``` - -Suppose you want users to be able to combine your bool options with their shortname. This -can be done using the **UseShortOptionHandling** bool in your commands. Suppose your program -has a two bool flags such as *serve* and *option* with the short options of *-o* and -*-s* respectively. With **UseShortOptionHandling** set to *true*, a user can use a syntax -like: -``` -# cmd foobar -so -``` - -If you enable the **UseShortOptionHandling*, then you must not use any flags that have a single -leading *-* or this will result in failures. For example, **-option** can no longer be used. Flags -with two leading dashes (such as **--options**) are still valid. - -## Contribution Guidelines - -See [./CONTRIBUTING.md](./CONTRIBUTING.md) diff --git a/vendor/github.com/urfave/cli/app.go b/vendor/github.com/urfave/cli/app.go deleted file mode 100644 index 9add067b..00000000 --- a/vendor/github.com/urfave/cli/app.go +++ /dev/null @@ -1,508 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "io/ioutil" - "os" - "path/filepath" - "sort" - "time" -) - -var ( - changeLogURL = "https://github.com/urfave/cli/blob/master/CHANGELOG.md" - appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) - runAndExitOnErrorDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-runandexitonerror", changeLogURL) - - contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." - - errInvalidActionType = NewExitError("ERROR invalid Action type. "+ - fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ - fmt.Sprintf("See %s", appActionDeprecationURL), 2) -) - -// App is the main structure of a cli application. It is recommended that -// an app be created with the cli.NewApp() function -type App struct { - // The name of the program. Defaults to path.Base(os.Args[0]) - Name string - // Full name of command for help, defaults to Name - HelpName string - // Description of the program. - Usage string - // Text to override the USAGE section of help - UsageText string - // Description of the program argument format. - ArgsUsage string - // Version of the program - Version string - // Description of the program - Description string - // List of commands to execute - Commands []Command - // List of flags to parse - Flags []Flag - // Boolean to enable bash completion commands - EnableBashCompletion bool - // Boolean to hide built-in help command - HideHelp bool - // Boolean to hide built-in version flag and the VERSION section of help - HideVersion bool - // Populate on app startup, only gettable through method Categories() - categories CommandCategories - // An action to execute when the bash-completion flag is set - BashComplete BashCompleteFunc - // An action to execute before any subcommands are run, but after the context is ready - // If a non-nil error is returned, no subcommands are run - Before BeforeFunc - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After AfterFunc - - // The action to execute when no subcommands are specified - // Expects a `cli.ActionFunc` but will accept the *deprecated* signature of `func(*cli.Context) {}` - // *Note*: support for the deprecated `Action` signature will be removed in a future version - Action interface{} - - // Execute this function if the proper command cannot be found - CommandNotFound CommandNotFoundFunc - // Execute this function if an usage error occurs - OnUsageError OnUsageErrorFunc - // Compilation date - Compiled time.Time - // List of all authors who contributed - Authors []Author - // Copyright of the binary if any - Copyright string - // Name of Author (Note: Use App.Authors, this is deprecated) - Author string - // Email of Author (Note: Use App.Authors, this is deprecated) - Email string - // Writer writer to write output to - Writer io.Writer - // ErrWriter writes error output - ErrWriter io.Writer - // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to - // function as a default, so this is optional. - ExitErrHandler ExitErrHandlerFunc - // Other custom info - Metadata map[string]interface{} - // Carries a function which returns app specific info. - ExtraInfo func() map[string]string - // CustomAppHelpTemplate the text template for app help topic. - // cli.go uses text/template to render templates. You can - // render custom help text by setting this variable. - CustomAppHelpTemplate string - - didSetup bool -} - -// Tries to find out when this binary was compiled. -// Returns the current time if it fails to find it. -func compileTime() time.Time { - info, err := os.Stat(os.Args[0]) - if err != nil { - return time.Now() - } - return info.ModTime() -} - -// NewApp creates a new cli Application with some reasonable defaults for Name, -// Usage, Version and Action. -func NewApp() *App { - return &App{ - Name: filepath.Base(os.Args[0]), - HelpName: filepath.Base(os.Args[0]), - Usage: "A new cli application", - UsageText: "", - Version: "0.0.0", - BashComplete: DefaultAppComplete, - Action: helpCommand.Action, - Compiled: compileTime(), - Writer: os.Stdout, - } -} - -// Setup runs initialization code to ensure all data structures are ready for -// `Run` or inspection prior to `Run`. It is internally called by `Run`, but -// will return early if setup has already happened. -func (a *App) Setup() { - if a.didSetup { - return - } - - a.didSetup = true - - if a.Author != "" || a.Email != "" { - a.Authors = append(a.Authors, Author{Name: a.Author, Email: a.Email}) - } - - newCmds := []Command{} - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - - if !a.HideVersion { - a.appendFlag(VersionFlag) - } - - a.categories = CommandCategories{} - for _, command := range a.Commands { - a.categories = a.categories.AddCommand(command.Category, command) - } - sort.Sort(a.categories) - - if a.Metadata == nil { - a.Metadata = make(map[string]interface{}) - } - - if a.Writer == nil { - a.Writer = os.Stdout - } -} - -// Run is the entry point to the cli app. Parses the arguments slice and routes -// to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { - a.Setup() - - // handle the completion flag separately from the flagset since - // completion could be attempted after a flag, but before its value was put - // on the command line. this causes the flagset to interpret the completion - // flag name as the value of the flag before it which is undesirable - // note that we can only do this because the shell autocomplete function - // always appends the completion flag at the end of the command - shellComplete, arguments := checkShellCompleteFlag(a, arguments) - - // parse flags - set, err := flagSet(a.Name, a.Flags) - if err != nil { - return err - } - - set.SetOutput(ioutil.Discard) - err = set.Parse(arguments[1:]) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, nil) - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - ShowAppHelp(context) - return nerr - } - context.shellComplete = shellComplete - - if checkCompletions(context) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err := a.OnUsageError(context, err, false) - a.handleExitCoder(context, err) - return err - } - fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - ShowAppHelp(context) - return err - } - - if !a.HideHelp && checkHelp(context) { - ShowAppHelp(context) - return nil - } - - if !a.HideVersion && checkVersion(context) { - ShowVersion(context) - return nil - } - - if a.After != nil { - defer func() { - if afterErr := a.After(context); afterErr != nil { - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - beforeErr := a.Before(context) - if beforeErr != nil { - fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) - ShowAppHelp(context) - a.handleExitCoder(context, beforeErr) - err = beforeErr - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - if a.Action == nil { - a.Action = helpCommand.Action - } - - // Run default Action - err = HandleAction(a.Action, context) - - a.handleExitCoder(context, err) - return err -} - -// RunAndExitOnError calls .Run() and exits non-zero if an error was returned -// -// Deprecated: instead you should return an error that fulfills cli.ExitCoder -// to cli.App.Run. This will cause the application to exit with the given eror -// code in the cli.ExitCoder -func (a *App) RunAndExitOnError() { - if err := a.Run(os.Args); err != nil { - fmt.Fprintln(a.errWriter(), err) - OsExiter(1) - } -} - -// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to -// generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { - // append help to commands - if len(a.Commands) > 0 { - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - a.Commands = append(a.Commands, helpCommand) - if (HelpFlag != BoolFlag{}) { - a.appendFlag(HelpFlag) - } - } - } - - newCmds := []Command{} - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - // parse flags - set, err := flagSet(a.Name, a.Flags) - if err != nil { - return err - } - - set.SetOutput(ioutil.Discard) - err = set.Parse(ctx.Args().Tail()) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, ctx) - - if nerr != nil { - fmt.Fprintln(a.Writer, nerr) - fmt.Fprintln(a.Writer) - if len(a.Commands) > 0 { - ShowSubcommandHelp(context) - } else { - ShowCommandHelp(ctx, context.Args().First()) - } - return nerr - } - - if checkCompletions(context) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err = a.OnUsageError(context, err, true) - a.handleExitCoder(context, err) - return err - } - fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - ShowSubcommandHelp(context) - return err - } - - if len(a.Commands) > 0 { - if checkSubcommandHelp(context) { - return nil - } - } else { - if checkCommandHelp(ctx, context.Args().First()) { - return nil - } - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - a.handleExitCoder(context, err) - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - beforeErr := a.Before(context) - if beforeErr != nil { - a.handleExitCoder(context, beforeErr) - err = beforeErr - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - err = HandleAction(a.Action, context) - - a.handleExitCoder(context, err) - return err -} - -// Command returns the named command on App. Returns nil if the command does not exist -func (a *App) Command(name string) *Command { - for _, c := range a.Commands { - if c.HasName(name) { - return &c - } - } - - return nil -} - -// Categories returns a slice containing all the categories with the commands they contain -func (a *App) Categories() CommandCategories { - return a.categories -} - -// VisibleCategories returns a slice of categories and commands that are -// Hidden=false -func (a *App) VisibleCategories() []*CommandCategory { - ret := []*CommandCategory{} - for _, category := range a.categories { - if visible := func() *CommandCategory { - for _, command := range category.Commands { - if !command.Hidden { - return category - } - } - return nil - }(); visible != nil { - ret = append(ret, visible) - } - } - return ret -} - -// VisibleCommands returns a slice of the Commands with Hidden=false -func (a *App) VisibleCommands() []Command { - ret := []Command{} - for _, command := range a.Commands { - if !command.Hidden { - ret = append(ret, command) - } - } - return ret -} - -// VisibleFlags returns a slice of the Flags with Hidden=false -func (a *App) VisibleFlags() []Flag { - return visibleFlags(a.Flags) -} - -func (a *App) hasFlag(flag Flag) bool { - for _, f := range a.Flags { - if flag == f { - return true - } - } - - return false -} - -func (a *App) errWriter() io.Writer { - // When the app ErrWriter is nil use the package level one. - if a.ErrWriter == nil { - return ErrWriter - } - - return a.ErrWriter -} - -func (a *App) appendFlag(flag Flag) { - if !a.hasFlag(flag) { - a.Flags = append(a.Flags, flag) - } -} - -func (a *App) handleExitCoder(context *Context, err error) { - if a.ExitErrHandler != nil { - a.ExitErrHandler(context, err) - } else { - HandleExitCoder(err) - } -} - -// Author represents someone who has contributed to a cli project. -type Author struct { - Name string // The Authors name - Email string // The Authors email -} - -// String makes Author comply to the Stringer interface, to allow an easy print in the templating process -func (a Author) String() string { - e := "" - if a.Email != "" { - e = " <" + a.Email + ">" - } - - return fmt.Sprintf("%v%v", a.Name, e) -} - -// HandleAction attempts to figure out which Action signature was used. If -// it's an ActionFunc or a func with the legacy signature for Action, the func -// is run! -func HandleAction(action interface{}, context *Context) (err error) { - if a, ok := action.(ActionFunc); ok { - return a(context) - } else if a, ok := action.(func(*Context) error); ok { - return a(context) - } else if a, ok := action.(func(*Context)); ok { // deprecated function signature - a(context) - return nil - } - - return errInvalidActionType -} diff --git a/vendor/github.com/urfave/cli/appveyor.yml b/vendor/github.com/urfave/cli/appveyor.yml deleted file mode 100644 index 1e1489c3..00000000 --- a/vendor/github.com/urfave/cli/appveyor.yml +++ /dev/null @@ -1,26 +0,0 @@ -version: "{build}" - -os: Windows Server 2016 - -image: Visual Studio 2017 - -clone_folder: c:\gopath\src\github.com\urfave\cli - -environment: - GOPATH: C:\gopath - GOVERSION: 1.8.x - PYTHON: C:\Python36-x64 - PYTHON_VERSION: 3.6.x - PYTHON_ARCH: 64 - -install: -- set PATH=%GOPATH%\bin;C:\go\bin;%PATH% -- go version -- go env -- go get github.com/urfave/gfmrun/... -- go get -v -t ./... - -build_script: -- python runtests vet -- python runtests test -- python runtests gfmrun diff --git a/vendor/github.com/urfave/cli/category.go b/vendor/github.com/urfave/cli/category.go deleted file mode 100644 index bf3c73c5..00000000 --- a/vendor/github.com/urfave/cli/category.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli - -// CommandCategories is a slice of *CommandCategory. -type CommandCategories []*CommandCategory - -// CommandCategory is a category containing commands. -type CommandCategory struct { - Name string - Commands Commands -} - -func (c CommandCategories) Less(i, j int) bool { - return lexicographicLess(c[i].Name, c[j].Name) -} - -func (c CommandCategories) Len() int { - return len(c) -} - -func (c CommandCategories) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -// AddCommand adds a command to a category. -func (c CommandCategories) AddCommand(category string, command Command) CommandCategories { - for _, commandCategory := range c { - if commandCategory.Name == category { - commandCategory.Commands = append(commandCategory.Commands, command) - return c - } - } - return append(c, &CommandCategory{Name: category, Commands: []Command{command}}) -} - -// VisibleCommands returns a slice of the Commands with Hidden=false -func (c *CommandCategory) VisibleCommands() []Command { - ret := []Command{} - for _, command := range c.Commands { - if !command.Hidden { - ret = append(ret, command) - } - } - return ret -} diff --git a/vendor/github.com/urfave/cli/cli.go b/vendor/github.com/urfave/cli/cli.go deleted file mode 100644 index 90c07eb8..00000000 --- a/vendor/github.com/urfave/cli/cli.go +++ /dev/null @@ -1,22 +0,0 @@ -// Package cli provides a minimal framework for creating and organizing command line -// Go applications. cli is designed to be easy to understand and write, the most simple -// cli application can be written as follows: -// func main() { -// cli.NewApp().Run(os.Args) -// } -// -// Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := cli.NewApp() -// app.Name = "greet" -// app.Usage = "say a greeting" -// app.Action = func(c *cli.Context) error { -// println("Greetings") -// return nil -// } -// -// app.Run(os.Args) -// } -package cli - -//go:generate python ./generate-flag-types cli -i flag-types.json -o flag_generated.go diff --git a/vendor/github.com/urfave/cli/command.go b/vendor/github.com/urfave/cli/command.go deleted file mode 100644 index 2acb9768..00000000 --- a/vendor/github.com/urfave/cli/command.go +++ /dev/null @@ -1,336 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "io/ioutil" - "sort" - "strings" -) - -// Command is a subcommand for a cli.App. -type Command struct { - // The name of the command - Name string - // short name of the command. Typically one character (deprecated, use `Aliases`) - ShortName string - // A list of aliases for the command - Aliases []string - // A short description of the usage of this command - Usage string - // Custom text to show on USAGE section of help - UsageText string - // A longer explanation of how the command works - Description string - // A short description of the arguments of this command - ArgsUsage string - // The category the command is part of - Category string - // The function to call when checking for bash command completions - BashComplete BashCompleteFunc - // An action to execute before any sub-subcommands are run, but after the context is ready - // If a non-nil error is returned, no sub-subcommands are run - Before BeforeFunc - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After AfterFunc - // The function to call when this command is invoked - Action interface{} - // TODO: replace `Action: interface{}` with `Action: ActionFunc` once some kind - // of deprecation period has passed, maybe? - - // Execute this function if a usage error occurs. - OnUsageError OnUsageErrorFunc - // List of child commands - Subcommands Commands - // List of flags to parse - Flags []Flag - // Treat all flags as normal arguments if true - SkipFlagParsing bool - // Skip argument reordering which attempts to move flags before arguments, - // but only works if all flags appear after all arguments. This behavior was - // removed n version 2 since it only works under specific conditions so we - // backport here by exposing it as an option for compatibility. - SkipArgReorder bool - // Boolean to hide built-in help command - HideHelp bool - // Boolean to hide this command from help or completion - Hidden bool - // Boolean to enable short-option handling so user can combine several - // single-character bool arguements into one - // i.e. foobar -o -v -> foobar -ov - UseShortOptionHandling bool - - // Full name of command for help, defaults to full command name, including parent commands. - HelpName string - commandNamePath []string - - // CustomHelpTemplate the text template for the command help topic. - // cli.go uses text/template to render templates. You can - // render custom help text by setting this variable. - CustomHelpTemplate string -} - -type CommandsByName []Command - -func (c CommandsByName) Len() int { - return len(c) -} - -func (c CommandsByName) Less(i, j int) bool { - return lexicographicLess(c[i].Name, c[j].Name) -} - -func (c CommandsByName) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -// FullName returns the full name of the command. -// For subcommands this ensures that parent commands are part of the command path -func (c Command) FullName() string { - if c.commandNamePath == nil { - return c.Name - } - return strings.Join(c.commandNamePath, " ") -} - -// Commands is a slice of Command -type Commands []Command - -// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c Command) Run(ctx *Context) (err error) { - if len(c.Subcommands) > 0 { - return c.startApp(ctx) - } - - if !c.HideHelp && (HelpFlag != BoolFlag{}) { - // append help to flags - c.Flags = append( - c.Flags, - HelpFlag, - ) - } - - set, err := c.parseFlags(ctx.Args().Tail()) - - context := NewContext(ctx.App, set, ctx) - context.Command = c - if checkCommandCompletions(context, c.Name) { - return nil - } - - if err != nil { - if c.OnUsageError != nil { - err := c.OnUsageError(context, err, false) - context.App.handleExitCoder(context, err) - return err - } - fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) - fmt.Fprintln(context.App.Writer) - ShowCommandHelp(context, c.Name) - return err - } - - if checkCommandHelp(context, c.Name) { - return nil - } - - if c.After != nil { - defer func() { - afterErr := c.After(context) - if afterErr != nil { - context.App.handleExitCoder(context, err) - if err != nil { - err = NewMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if c.Before != nil { - err = c.Before(context) - if err != nil { - ShowCommandHelp(context, c.Name) - context.App.handleExitCoder(context, err) - return err - } - } - - if c.Action == nil { - c.Action = helpSubcommand.Action - } - - err = HandleAction(c.Action, context) - - if err != nil { - context.App.handleExitCoder(context, err) - } - return err -} - -func (c *Command) parseFlags(args Args) (*flag.FlagSet, error) { - set, err := flagSet(c.Name, c.Flags) - if err != nil { - return nil, err - } - set.SetOutput(ioutil.Discard) - - if c.SkipFlagParsing { - return set, set.Parse(append([]string{"--"}, args...)) - } - - if c.UseShortOptionHandling { - args = translateShortOptions(args) - } - - if !c.SkipArgReorder { - args = reorderArgs(args) - } - - err = set.Parse(args) - if err != nil { - return nil, err - } - - err = normalizeFlags(c.Flags, set) - if err != nil { - return nil, err - } - - return set, nil -} - -// reorderArgs moves all flags before arguments as this is what flag expects -func reorderArgs(args []string) []string { - var nonflags, flags []string - - readFlagValue := false - for i, arg := range args { - if arg == "--" { - nonflags = append(nonflags, args[i:]...) - break - } - - if readFlagValue && !strings.HasPrefix(arg, "-") && !strings.HasPrefix(arg, "--") { - readFlagValue = false - flags = append(flags, arg) - continue - } - readFlagValue = false - - if arg != "-" && strings.HasPrefix(arg, "-") { - flags = append(flags, arg) - - readFlagValue = !strings.Contains(arg, "=") - } else { - nonflags = append(nonflags, arg) - } - } - - return append(flags, nonflags...) -} - -func translateShortOptions(flagArgs Args) []string { - // separate combined flags - var flagArgsSeparated []string - for _, flagArg := range flagArgs { - if strings.HasPrefix(flagArg, "-") && strings.HasPrefix(flagArg, "--") == false && len(flagArg) > 2 { - for _, flagChar := range flagArg[1:] { - flagArgsSeparated = append(flagArgsSeparated, "-"+string(flagChar)) - } - } else { - flagArgsSeparated = append(flagArgsSeparated, flagArg) - } - } - return flagArgsSeparated -} - -// Names returns the names including short names and aliases. -func (c Command) Names() []string { - names := []string{c.Name} - - if c.ShortName != "" { - names = append(names, c.ShortName) - } - - return append(names, c.Aliases...) -} - -// HasName returns true if Command.Name or Command.ShortName matches given name -func (c Command) HasName(name string) bool { - for _, n := range c.Names() { - if n == name { - return true - } - } - return false -} - -func (c Command) startApp(ctx *Context) error { - app := NewApp() - app.Metadata = ctx.App.Metadata - // set the name and usage - app.Name = fmt.Sprintf("%s %s", ctx.App.Name, c.Name) - if c.HelpName == "" { - app.HelpName = c.HelpName - } else { - app.HelpName = app.Name - } - - app.Usage = c.Usage - app.Description = c.Description - app.ArgsUsage = c.ArgsUsage - - // set CommandNotFound - app.CommandNotFound = ctx.App.CommandNotFound - app.CustomAppHelpTemplate = c.CustomHelpTemplate - - // set the flags and commands - app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - - app.Version = ctx.App.Version - app.HideVersion = ctx.App.HideVersion - app.Compiled = ctx.App.Compiled - app.Author = ctx.App.Author - app.Email = ctx.App.Email - app.Writer = ctx.App.Writer - app.ErrWriter = ctx.App.ErrWriter - - app.categories = CommandCategories{} - for _, command := range c.Subcommands { - app.categories = app.categories.AddCommand(command.Category, command) - } - - sort.Sort(app.categories) - - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - - // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpSubcommand.Action - } - app.OnUsageError = c.OnUsageError - - for index, cc := range app.Commands { - app.Commands[index].commandNamePath = []string{c.Name, cc.Name} - } - - return app.RunAsSubcommand(ctx) -} - -// VisibleFlags returns a slice of the Flags with Hidden=false -func (c Command) VisibleFlags() []Flag { - return visibleFlags(c.Flags) -} diff --git a/vendor/github.com/urfave/cli/context.go b/vendor/github.com/urfave/cli/context.go deleted file mode 100644 index 552ee740..00000000 --- a/vendor/github.com/urfave/cli/context.go +++ /dev/null @@ -1,287 +0,0 @@ -package cli - -import ( - "errors" - "flag" - "os" - "reflect" - "strings" - "syscall" -) - -// Context is a type that is passed through to -// each Handler action in a cli application. Context -// can be used to retrieve context-specific Args and -// parsed command-line options. -type Context struct { - App *App - Command Command - shellComplete bool - flagSet *flag.FlagSet - setFlags map[string]bool - parentContext *Context -} - -// NewContext creates a new context. For use in when invoking an App or Command action. -func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - c := &Context{App: app, flagSet: set, parentContext: parentCtx} - - if parentCtx != nil { - c.shellComplete = parentCtx.shellComplete - } - - return c -} - -// NumFlags returns the number of flags set -func (c *Context) NumFlags() int { - return c.flagSet.NFlag() -} - -// Set sets a context flag to a value. -func (c *Context) Set(name, value string) error { - c.setFlags = nil - return c.flagSet.Set(name, value) -} - -// GlobalSet sets a context flag to a value on the global flagset -func (c *Context) GlobalSet(name, value string) error { - globalContext(c).setFlags = nil - return globalContext(c).flagSet.Set(name, value) -} - -// IsSet determines if the flag was actually set -func (c *Context) IsSet(name string) bool { - if c.setFlags == nil { - c.setFlags = make(map[string]bool) - - c.flagSet.Visit(func(f *flag.Flag) { - c.setFlags[f.Name] = true - }) - - c.flagSet.VisitAll(func(f *flag.Flag) { - if _, ok := c.setFlags[f.Name]; ok { - return - } - c.setFlags[f.Name] = false - }) - - // XXX hack to support IsSet for flags with EnvVar - // - // There isn't an easy way to do this with the current implementation since - // whether a flag was set via an environment variable is very difficult to - // determine here. Instead, we intend to introduce a backwards incompatible - // change in version 2 to add `IsSet` to the Flag interface to push the - // responsibility closer to where the information required to determine - // whether a flag is set by non-standard means such as environment - // variables is available. - // - // See https://github.com/urfave/cli/issues/294 for additional discussion - flags := c.Command.Flags - if c.Command.Name == "" { // cannot == Command{} since it contains slice types - if c.App != nil { - flags = c.App.Flags - } - } - for _, f := range flags { - eachName(f.GetName(), func(name string) { - if isSet, ok := c.setFlags[name]; isSet || !ok { - return - } - - val := reflect.ValueOf(f) - if val.Kind() == reflect.Ptr { - val = val.Elem() - } - - filePathValue := val.FieldByName("FilePath") - if filePathValue.IsValid() { - eachName(filePathValue.String(), func(filePath string) { - if _, err := os.Stat(filePath); err == nil { - c.setFlags[name] = true - return - } - }) - } - - envVarValue := val.FieldByName("EnvVar") - if envVarValue.IsValid() { - eachName(envVarValue.String(), func(envVar string) { - envVar = strings.TrimSpace(envVar) - if _, ok := syscall.Getenv(envVar); ok { - c.setFlags[name] = true - return - } - }) - } - }) - } - } - - return c.setFlags[name] -} - -// GlobalIsSet determines if the global flag was actually set -func (c *Context) GlobalIsSet(name string) bool { - ctx := c - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - - for ; ctx != nil; ctx = ctx.parentContext { - if ctx.IsSet(name) { - return true - } - } - return false -} - -// FlagNames returns a slice of flag names used in this context. -func (c *Context) FlagNames() (names []string) { - for _, flag := range c.Command.Flags { - name := strings.Split(flag.GetName(), ",")[0] - if name == "help" { - continue - } - names = append(names, name) - } - return -} - -// GlobalFlagNames returns a slice of global flag names used by the app. -func (c *Context) GlobalFlagNames() (names []string) { - for _, flag := range c.App.Flags { - name := strings.Split(flag.GetName(), ",")[0] - if name == "help" || name == "version" { - continue - } - names = append(names, name) - } - return -} - -// Parent returns the parent context, if any -func (c *Context) Parent() *Context { - return c.parentContext -} - -// value returns the value of the flag coressponding to `name` -func (c *Context) value(name string) interface{} { - return c.flagSet.Lookup(name).Value.(flag.Getter).Get() -} - -// Args contains apps console arguments -type Args []string - -// Args returns the command line arguments associated with the context. -func (c *Context) Args() Args { - args := Args(c.flagSet.Args()) - return args -} - -// NArg returns the number of the command line arguments. -func (c *Context) NArg() int { - return len(c.Args()) -} - -// Get returns the nth argument, or else a blank string -func (a Args) Get(n int) string { - if len(a) > n { - return a[n] - } - return "" -} - -// First returns the first argument, or else a blank string -func (a Args) First() string { - return a.Get(0) -} - -// Tail returns the rest of the arguments (not the first one) -// or else an empty string slice -func (a Args) Tail() []string { - if len(a) >= 2 { - return []string(a)[1:] - } - return []string{} -} - -// Present checks if there are any arguments present -func (a Args) Present() bool { - return len(a) != 0 -} - -// Swap swaps arguments at the given indexes -func (a Args) Swap(from, to int) error { - if from >= len(a) || to >= len(a) { - return errors.New("index out of range") - } - a[from], a[to] = a[to], a[from] - return nil -} - -func globalContext(ctx *Context) *Context { - if ctx == nil { - return nil - } - - for { - if ctx.parentContext == nil { - return ctx - } - ctx = ctx.parentContext - } -} - -func lookupGlobalFlagSet(name string, ctx *Context) *flag.FlagSet { - if ctx.parentContext != nil { - ctx = ctx.parentContext - } - for ; ctx != nil; ctx = ctx.parentContext { - if f := ctx.flagSet.Lookup(name); f != nil { - return ctx.flagSet - } - } - return nil -} - -func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { - switch ff.Value.(type) { - case *StringSlice: - default: - set.Set(name, ff.Value.String()) - } -} - -func normalizeFlags(flags []Flag, set *flag.FlagSet) error { - visited := make(map[string]bool) - set.Visit(func(f *flag.Flag) { - visited[f.Name] = true - }) - for _, f := range flags { - parts := strings.Split(f.GetName(), ",") - if len(parts) == 1 { - continue - } - var ff *flag.Flag - for _, name := range parts { - name = strings.Trim(name, " ") - if visited[name] { - if ff != nil { - return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) - } - ff = set.Lookup(name) - } - } - if ff == nil { - continue - } - for _, name := range parts { - name = strings.Trim(name, " ") - if !visited[name] { - copyFlag(name, ff, set) - } - } - } - return nil -} diff --git a/vendor/github.com/urfave/cli/errors.go b/vendor/github.com/urfave/cli/errors.go deleted file mode 100644 index 562b2953..00000000 --- a/vendor/github.com/urfave/cli/errors.go +++ /dev/null @@ -1,115 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "os" - "strings" -) - -// OsExiter is the function used when the app exits. If not set defaults to os.Exit. -var OsExiter = os.Exit - -// ErrWriter is used to write errors to the user. This can be anything -// implementing the io.Writer interface and defaults to os.Stderr. -var ErrWriter io.Writer = os.Stderr - -// MultiError is an error that wraps multiple errors. -type MultiError struct { - Errors []error -} - -// NewMultiError creates a new MultiError. Pass in one or more errors. -func NewMultiError(err ...error) MultiError { - return MultiError{Errors: err} -} - -// Error implements the error interface. -func (m MultiError) Error() string { - errs := make([]string, len(m.Errors)) - for i, err := range m.Errors { - errs[i] = err.Error() - } - - return strings.Join(errs, "\n") -} - -type ErrorFormatter interface { - Format(s fmt.State, verb rune) -} - -// ExitCoder is the interface checked by `App` and `Command` for a custom exit -// code -type ExitCoder interface { - error - ExitCode() int -} - -// ExitError fulfills both the builtin `error` interface and `ExitCoder` -type ExitError struct { - exitCode int - message interface{} -} - -// NewExitError makes a new *ExitError -func NewExitError(message interface{}, exitCode int) *ExitError { - return &ExitError{ - exitCode: exitCode, - message: message, - } -} - -// Error returns the string message, fulfilling the interface required by -// `error` -func (ee *ExitError) Error() string { - return fmt.Sprintf("%v", ee.message) -} - -// ExitCode returns the exit code, fulfilling the interface required by -// `ExitCoder` -func (ee *ExitError) ExitCode() int { - return ee.exitCode -} - -// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if -// so prints the error to stderr (if it is non-empty) and calls OsExiter with the -// given exit code. If the given error is a MultiError, then this func is -// called on all members of the Errors slice and calls OsExiter with the last exit code. -func HandleExitCoder(err error) { - if err == nil { - return - } - - if exitErr, ok := err.(ExitCoder); ok { - if err.Error() != "" { - if _, ok := exitErr.(ErrorFormatter); ok { - fmt.Fprintf(ErrWriter, "%+v\n", err) - } else { - fmt.Fprintln(ErrWriter, err) - } - } - OsExiter(exitErr.ExitCode()) - return - } - - if multiErr, ok := err.(MultiError); ok { - code := handleMultiError(multiErr) - OsExiter(code) - return - } -} - -func handleMultiError(multiErr MultiError) int { - code := 1 - for _, merr := range multiErr.Errors { - if multiErr2, ok := merr.(MultiError); ok { - code = handleMultiError(multiErr2) - } else { - fmt.Fprintln(ErrWriter, merr) - if exitErr, ok := merr.(ExitCoder); ok { - code = exitErr.ExitCode() - } - } - } - return code -} diff --git a/vendor/github.com/urfave/cli/flag-types.json b/vendor/github.com/urfave/cli/flag-types.json deleted file mode 100644 index 12231078..00000000 --- a/vendor/github.com/urfave/cli/flag-types.json +++ /dev/null @@ -1,93 +0,0 @@ -[ - { - "name": "Bool", - "type": "bool", - "value": false, - "context_default": "false", - "parser": "strconv.ParseBool(f.Value.String())" - }, - { - "name": "BoolT", - "type": "bool", - "value": false, - "doctail": " that is true by default", - "context_default": "false", - "parser": "strconv.ParseBool(f.Value.String())" - }, - { - "name": "Duration", - "type": "time.Duration", - "doctail": " (see https://golang.org/pkg/time/#ParseDuration)", - "context_default": "0", - "parser": "time.ParseDuration(f.Value.String())" - }, - { - "name": "Float64", - "type": "float64", - "context_default": "0", - "parser": "strconv.ParseFloat(f.Value.String(), 64)" - }, - { - "name": "Generic", - "type": "Generic", - "dest": false, - "context_default": "nil", - "context_type": "interface{}" - }, - { - "name": "Int64", - "type": "int64", - "context_default": "0", - "parser": "strconv.ParseInt(f.Value.String(), 0, 64)" - }, - { - "name": "Int", - "type": "int", - "context_default": "0", - "parser": "strconv.ParseInt(f.Value.String(), 0, 64)", - "parser_cast": "int(parsed)" - }, - { - "name": "IntSlice", - "type": "*IntSlice", - "dest": false, - "context_default": "nil", - "context_type": "[]int", - "parser": "(f.Value.(*IntSlice)).Value(), error(nil)" - }, - { - "name": "Int64Slice", - "type": "*Int64Slice", - "dest": false, - "context_default": "nil", - "context_type": "[]int64", - "parser": "(f.Value.(*Int64Slice)).Value(), error(nil)" - }, - { - "name": "String", - "type": "string", - "context_default": "\"\"", - "parser": "f.Value.String(), error(nil)" - }, - { - "name": "StringSlice", - "type": "*StringSlice", - "dest": false, - "context_default": "nil", - "context_type": "[]string", - "parser": "(f.Value.(*StringSlice)).Value(), error(nil)" - }, - { - "name": "Uint64", - "type": "uint64", - "context_default": "0", - "parser": "strconv.ParseUint(f.Value.String(), 0, 64)" - }, - { - "name": "Uint", - "type": "uint", - "context_default": "0", - "parser": "strconv.ParseUint(f.Value.String(), 0, 64)", - "parser_cast": "uint(parsed)" - } -] diff --git a/vendor/github.com/urfave/cli/flag.go b/vendor/github.com/urfave/cli/flag.go deleted file mode 100644 index b0cffc00..00000000 --- a/vendor/github.com/urfave/cli/flag.go +++ /dev/null @@ -1,786 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "io/ioutil" - "reflect" - "runtime" - "strconv" - "strings" - "syscall" - "time" -) - -const defaultPlaceholder = "value" - -// BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag Flag = BoolFlag{ - Name: "generate-bash-completion", - Hidden: true, -} - -// VersionFlag prints the version for the application -var VersionFlag Flag = BoolFlag{ - Name: "version, v", - Usage: "print the version", -} - -// HelpFlag prints the help for all commands and subcommands -// Set to the zero value (BoolFlag{}) to disable flag -- keeps subcommand -// unless HideHelp is set to true) -var HelpFlag Flag = BoolFlag{ - Name: "help, h", - Usage: "show help", -} - -// FlagStringer converts a flag definition to a string. This is used by help -// to display a flag. -var FlagStringer FlagStringFunc = stringifyFlag - -// FlagNamePrefixer converts a full flag name and its placeholder into the help -// message flag prefix. This is used by the default FlagStringer. -var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames - -// FlagEnvHinter annotates flag help message with the environment variable -// details. This is used by the default FlagStringer. -var FlagEnvHinter FlagEnvHintFunc = withEnvHint - -// FlagFileHinter annotates flag help message with the environment variable -// details. This is used by the default FlagStringer. -var FlagFileHinter FlagFileHintFunc = withFileHint - -// FlagsByName is a slice of Flag. -type FlagsByName []Flag - -func (f FlagsByName) Len() int { - return len(f) -} - -func (f FlagsByName) Less(i, j int) bool { - return lexicographicLess(f[i].GetName(), f[j].GetName()) -} - -func (f FlagsByName) Swap(i, j int) { - f[i], f[j] = f[j], f[i] -} - -// Flag is a common interface related to parsing flags in cli. -// For more advanced flag parsing techniques, it is recommended that -// this interface be implemented. -type Flag interface { - fmt.Stringer - // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) - GetName() string -} - -// errorableFlag is an interface that allows us to return errors during apply -// it allows flags defined in this library to return errors in a fashion backwards compatible -// TODO remove in v2 and modify the existing Flag interface to return errors -type errorableFlag interface { - Flag - - ApplyWithError(*flag.FlagSet) error -} - -func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { - set := flag.NewFlagSet(name, flag.ContinueOnError) - - for _, f := range flags { - //TODO remove in v2 when errorableFlag is removed - if ef, ok := f.(errorableFlag); ok { - if err := ef.ApplyWithError(set); err != nil { - return nil, err - } - } else { - f.Apply(set) - } - } - return set, nil -} - -func eachName(longName string, fn func(string)) { - parts := strings.Split(longName, ",") - for _, name := range parts { - name = strings.Trim(name, " ") - fn(name) - } -} - -// Generic is a generic parseable type identified by a specific flag -type Generic interface { - Set(value string) error - String() string -} - -// Apply takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -// Ignores parsing errors -func (f GenericFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -func (f GenericFlag) ApplyWithError(set *flag.FlagSet) error { - val := f.Value - if fileEnvVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - if err := val.Set(fileEnvVal); err != nil { - return fmt.Errorf("could not parse %s as value for flag %s: %s", fileEnvVal, f.Name, err) - } - } - - eachName(f.Name, func(name string) { - set.Var(f.Value, name, f.Usage) - }) - - return nil -} - -// StringSlice is an opaque type for []string to satisfy flag.Value and flag.Getter -type StringSlice []string - -// Set appends the string value to the list of values -func (f *StringSlice) Set(value string) error { - *f = append(*f, value) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *StringSlice) String() string { - return fmt.Sprintf("%s", *f) -} - -// Value returns the slice of strings set by this flag -func (f *StringSlice) Value() []string { - return *f -} - -// Get returns the slice of strings set by this flag -func (f *StringSlice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f StringSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f StringSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - newVal := &StringSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as string value for flag %s: %s", envVal, f.Name, err) - } - } - if f.Value == nil { - f.Value = newVal - } else { - *f.Value = *newVal - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &StringSlice{} - } - set.Var(f.Value, name, f.Usage) - }) - - return nil -} - -// IntSlice is an opaque type for []int to satisfy flag.Value and flag.Getter -type IntSlice []int - -// Set parses the value into an integer and appends it to the list of values -func (f *IntSlice) Set(value string) error { - tmp, err := strconv.Atoi(value) - if err != nil { - return err - } - *f = append(*f, tmp) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *IntSlice) String() string { - return fmt.Sprintf("%#v", *f) -} - -// Value returns the slice of ints set by this flag -func (f *IntSlice) Value() []int { - return *f -} - -// Get returns the slice of ints set by this flag -func (f *IntSlice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f IntSliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f IntSliceFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - newVal := &IntSlice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as int slice value for flag %s: %s", envVal, f.Name, err) - } - } - if f.Value == nil { - f.Value = newVal - } else { - *f.Value = *newVal - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &IntSlice{} - } - set.Var(f.Value, name, f.Usage) - }) - - return nil -} - -// Int64Slice is an opaque type for []int to satisfy flag.Value and flag.Getter -type Int64Slice []int64 - -// Set parses the value into an integer and appends it to the list of values -func (f *Int64Slice) Set(value string) error { - tmp, err := strconv.ParseInt(value, 10, 64) - if err != nil { - return err - } - *f = append(*f, tmp) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *Int64Slice) String() string { - return fmt.Sprintf("%#v", *f) -} - -// Value returns the slice of ints set by this flag -func (f *Int64Slice) Value() []int64 { - return *f -} - -// Get returns the slice of ints set by this flag -func (f *Int64Slice) Get() interface{} { - return *f -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f Int64SliceFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Int64SliceFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - newVal := &Int64Slice{} - for _, s := range strings.Split(envVal, ",") { - s = strings.TrimSpace(s) - if err := newVal.Set(s); err != nil { - return fmt.Errorf("could not parse %s as int64 slice value for flag %s: %s", envVal, f.Name, err) - } - } - if f.Value == nil { - f.Value = newVal - } else { - *f.Value = *newVal - } - } - - eachName(f.Name, func(name string) { - if f.Value == nil { - f.Value = &Int64Slice{} - } - set.Var(f.Value, name, f.Usage) - }) - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f BoolFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f BoolFlag) ApplyWithError(set *flag.FlagSet) error { - val := false - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - if envVal == "" { - val = false - } else { - envValBool, err := strconv.ParseBool(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) - } - val = envValBool - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.BoolVar(f.Destination, name, val, f.Usage) - return - } - set.Bool(name, val, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f BoolTFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f BoolTFlag) ApplyWithError(set *flag.FlagSet) error { - val := true - - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - if envVal == "" { - val = false - } else { - envValBool, err := strconv.ParseBool(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as bool value for flag %s: %s", envVal, f.Name, err) - } - val = envValBool - } - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.BoolVar(f.Destination, name, val, f.Usage) - return - } - set.Bool(name, val, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f StringFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f StringFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - f.Value = envVal - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - return - } - set.String(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f IntFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f IntFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) - } - f.Value = int(envValInt) - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.IntVar(f.Destination, name, f.Value, f.Usage) - return - } - set.Int(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f Int64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Int64Flag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValInt, err := strconv.ParseInt(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as int value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = envValInt - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.Int64Var(f.Destination, name, f.Value, f.Usage) - return - } - set.Int64(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f UintFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f UintFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as uint value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint(envValInt) - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.UintVar(f.Destination, name, f.Value, f.Usage) - return - } - set.Uint(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f Uint64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Uint64Flag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValInt, err := strconv.ParseUint(envVal, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %s as uint64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = uint64(envValInt) - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.Uint64Var(f.Destination, name, f.Value, f.Usage) - return - } - set.Uint64(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f DurationFlag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f DurationFlag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValDuration, err := time.ParseDuration(envVal) - if err != nil { - return fmt.Errorf("could not parse %s as duration for flag %s: %s", envVal, f.Name, err) - } - - f.Value = envValDuration - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.DurationVar(f.Destination, name, f.Value, f.Usage) - return - } - set.Duration(name, f.Value, f.Usage) - }) - - return nil -} - -// Apply populates the flag given the flag set and environment -// Ignores errors -func (f Float64Flag) Apply(set *flag.FlagSet) { - f.ApplyWithError(set) -} - -// ApplyWithError populates the flag given the flag set and environment -func (f Float64Flag) ApplyWithError(set *flag.FlagSet) error { - if envVal, ok := flagFromFileEnv(f.FilePath, f.EnvVar); ok { - envValFloat, err := strconv.ParseFloat(envVal, 10) - if err != nil { - return fmt.Errorf("could not parse %s as float64 value for flag %s: %s", envVal, f.Name, err) - } - - f.Value = float64(envValFloat) - } - - eachName(f.Name, func(name string) { - if f.Destination != nil { - set.Float64Var(f.Destination, name, f.Value, f.Usage) - return - } - set.Float64(name, f.Value, f.Usage) - }) - - return nil -} - -func visibleFlags(fl []Flag) []Flag { - visible := []Flag{} - for _, flag := range fl { - field := flagValue(flag).FieldByName("Hidden") - if !field.IsValid() || !field.Bool() { - visible = append(visible, flag) - } - } - return visible -} - -func prefixFor(name string) (prefix string) { - if len(name) == 1 { - prefix = "-" - } else { - prefix = "--" - } - - return -} - -// Returns the placeholder, if any, and the unquoted usage string. -func unquoteUsage(usage string) (string, string) { - for i := 0; i < len(usage); i++ { - if usage[i] == '`' { - for j := i + 1; j < len(usage); j++ { - if usage[j] == '`' { - name := usage[i+1 : j] - usage = usage[:i] + name + usage[j+1:] - return name, usage - } - } - break - } - } - return "", usage -} - -func prefixedNames(fullName, placeholder string) string { - var prefixed string - parts := strings.Split(fullName, ",") - for i, name := range parts { - name = strings.Trim(name, " ") - prefixed += prefixFor(name) + name - if placeholder != "" { - prefixed += " " + placeholder - } - if i < len(parts)-1 { - prefixed += ", " - } - } - return prefixed -} - -func withEnvHint(envVar, str string) string { - envText := "" - if envVar != "" { - prefix := "$" - suffix := "" - sep := ", $" - if runtime.GOOS == "windows" { - prefix = "%" - suffix = "%" - sep = "%, %" - } - envText = " [" + prefix + strings.Join(strings.Split(envVar, ","), sep) + suffix + "]" - } - return str + envText -} - -func withFileHint(filePath, str string) string { - fileText := "" - if filePath != "" { - fileText = fmt.Sprintf(" [%s]", filePath) - } - return str + fileText -} - -func flagValue(f Flag) reflect.Value { - fv := reflect.ValueOf(f) - for fv.Kind() == reflect.Ptr { - fv = reflect.Indirect(fv) - } - return fv -} - -func stringifyFlag(f Flag) string { - fv := flagValue(f) - - switch f.(type) { - case IntSliceFlag: - return FlagFileHinter( - fv.FieldByName("FilePath").String(), - FlagEnvHinter( - fv.FieldByName("EnvVar").String(), - stringifyIntSliceFlag(f.(IntSliceFlag)), - ), - ) - case Int64SliceFlag: - return FlagFileHinter( - fv.FieldByName("FilePath").String(), - FlagEnvHinter( - fv.FieldByName("EnvVar").String(), - stringifyInt64SliceFlag(f.(Int64SliceFlag)), - ), - ) - case StringSliceFlag: - return FlagFileHinter( - fv.FieldByName("FilePath").String(), - FlagEnvHinter( - fv.FieldByName("EnvVar").String(), - stringifyStringSliceFlag(f.(StringSliceFlag)), - ), - ) - } - - placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) - - needsPlaceholder := false - defaultValueString := "" - - if val := fv.FieldByName("Value"); val.IsValid() { - needsPlaceholder = true - defaultValueString = fmt.Sprintf(" (default: %v)", val.Interface()) - - if val.Kind() == reflect.String && val.String() != "" { - defaultValueString = fmt.Sprintf(" (default: %q)", val.String()) - } - } - - if defaultValueString == " (default: )" { - defaultValueString = "" - } - - if needsPlaceholder && placeholder == "" { - placeholder = defaultPlaceholder - } - - usageWithDefault := strings.TrimSpace(usage + defaultValueString) - - return FlagFileHinter( - fv.FieldByName("FilePath").String(), - FlagEnvHinter( - fv.FieldByName("EnvVar").String(), - FlagNamePrefixer(fv.FieldByName("Name").String(), placeholder)+"\t"+usageWithDefault, - ), - ) -} - -func stringifyIntSliceFlag(f IntSliceFlag) string { - defaultVals := []string{} - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.Itoa(i)) - } - } - - return stringifySliceFlag(f.Usage, f.Name, defaultVals) -} - -func stringifyInt64SliceFlag(f Int64SliceFlag) string { - defaultVals := []string{} - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) - } - } - - return stringifySliceFlag(f.Usage, f.Name, defaultVals) -} - -func stringifyStringSliceFlag(f StringSliceFlag) string { - defaultVals := []string{} - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, s := range f.Value.Value() { - if len(s) > 0 { - defaultVals = append(defaultVals, strconv.Quote(s)) - } - } - } - - return stringifySliceFlag(f.Usage, f.Name, defaultVals) -} - -func stringifySliceFlag(usage, name string, defaultVals []string) string { - placeholder, usage := unquoteUsage(usage) - if placeholder == "" { - placeholder = defaultPlaceholder - } - - defaultVal := "" - if len(defaultVals) > 0 { - defaultVal = fmt.Sprintf(" (default: %s)", strings.Join(defaultVals, ", ")) - } - - usageWithDefault := strings.TrimSpace(usage + defaultVal) - return FlagNamePrefixer(name, placeholder) + "\t" + usageWithDefault -} - -func flagFromFileEnv(filePath, envName string) (val string, ok bool) { - for _, envVar := range strings.Split(envName, ",") { - envVar = strings.TrimSpace(envVar) - if envVal, ok := syscall.Getenv(envVar); ok { - return envVal, true - } - } - for _, fileVar := range strings.Split(filePath, ",") { - if data, err := ioutil.ReadFile(fileVar); err == nil { - return string(data), true - } - } - return "", false -} diff --git a/vendor/github.com/urfave/cli/flag_generated.go b/vendor/github.com/urfave/cli/flag_generated.go deleted file mode 100644 index 001576c8..00000000 --- a/vendor/github.com/urfave/cli/flag_generated.go +++ /dev/null @@ -1,640 +0,0 @@ -package cli - -import ( - "flag" - "strconv" - "time" -) - -// WARNING: This file is generated! - -// BoolFlag is a flag with type bool -type BoolFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Destination *bool -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f BoolFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f BoolFlag) GetName() string { - return f.Name -} - -// Bool looks up the value of a local BoolFlag, returns -// false if not found -func (c *Context) Bool(name string) bool { - return lookupBool(name, c.flagSet) -} - -// GlobalBool looks up the value of a global BoolFlag, returns -// false if not found -func (c *Context) GlobalBool(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return parsed - } - return false -} - -// BoolTFlag is a flag with type bool that is true by default -type BoolTFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Destination *bool -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f BoolTFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f BoolTFlag) GetName() string { - return f.Name -} - -// BoolT looks up the value of a local BoolTFlag, returns -// false if not found -func (c *Context) BoolT(name string) bool { - return lookupBoolT(name, c.flagSet) -} - -// GlobalBoolT looks up the value of a global BoolTFlag, returns -// false if not found -func (c *Context) GlobalBoolT(name string) bool { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupBoolT(name, fs) - } - return false -} - -func lookupBoolT(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return parsed - } - return false -} - -// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) -type DurationFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value time.Duration - Destination *time.Duration -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f DurationFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f DurationFlag) GetName() string { - return f.Name -} - -// Duration looks up the value of a local DurationFlag, returns -// 0 if not found -func (c *Context) Duration(name string) time.Duration { - return lookupDuration(name, c.flagSet) -} - -// GlobalDuration looks up the value of a global DurationFlag, returns -// 0 if not found -func (c *Context) GlobalDuration(name string) time.Duration { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - parsed, err := time.ParseDuration(f.Value.String()) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// Float64Flag is a flag with type float64 -type Float64Flag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value float64 - Destination *float64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f Float64Flag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f Float64Flag) GetName() string { - return f.Name -} - -// Float64 looks up the value of a local Float64Flag, returns -// 0 if not found -func (c *Context) Float64(name string) float64 { - return lookupFloat64(name, c.flagSet) -} - -// GlobalFloat64 looks up the value of a global Float64Flag, returns -// 0 if not found -func (c *Context) GlobalFloat64(name string) float64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupFloat64(name, fs) - } - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// GenericFlag is a flag with type Generic -type GenericFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value Generic -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f GenericFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f GenericFlag) GetName() string { - return f.Name -} - -// Generic looks up the value of a local GenericFlag, returns -// nil if not found -func (c *Context) Generic(name string) interface{} { - return lookupGeneric(name, c.flagSet) -} - -// GlobalGeneric looks up the value of a global GenericFlag, returns -// nil if not found -func (c *Context) GlobalGeneric(name string) interface{} { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value, error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Int64Flag is a flag with type int64 -type Int64Flag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value int64 - Destination *int64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f Int64Flag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f Int64Flag) GetName() string { - return f.Name -} - -// Int64 looks up the value of a local Int64Flag, returns -// 0 if not found -func (c *Context) Int64(name string) int64 { - return lookupInt64(name, c.flagSet) -} - -// GlobalInt64 looks up the value of a global Int64Flag, returns -// 0 if not found -func (c *Context) GlobalInt64(name string) int64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt64(name, fs) - } - return 0 -} - -func lookupInt64(name string, set *flag.FlagSet) int64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// IntFlag is a flag with type int -type IntFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value int - Destination *int -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f IntFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f IntFlag) GetName() string { - return f.Name -} - -// Int looks up the value of a local IntFlag, returns -// 0 if not found -func (c *Context) Int(name string) int { - return lookupInt(name, c.flagSet) -} - -// GlobalInt looks up the value of a global IntFlag, returns -// 0 if not found -func (c *Context) GlobalInt(name string) int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt(name, fs) - } - return 0 -} - -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return int(parsed) - } - return 0 -} - -// IntSliceFlag is a flag with type *IntSlice -type IntSliceFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value *IntSlice -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f IntSliceFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f IntSliceFlag) GetName() string { - return f.Name -} - -// IntSlice looks up the value of a local IntSliceFlag, returns -// nil if not found -func (c *Context) IntSlice(name string) []int { - return lookupIntSlice(name, c.flagSet) -} - -// GlobalIntSlice looks up the value of a global IntSliceFlag, returns -// nil if not found -func (c *Context) GlobalIntSlice(name string) []int { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupIntSlice(name, fs) - } - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*IntSlice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Int64SliceFlag is a flag with type *Int64Slice -type Int64SliceFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value *Int64Slice -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f Int64SliceFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f Int64SliceFlag) GetName() string { - return f.Name -} - -// Int64Slice looks up the value of a local Int64SliceFlag, returns -// nil if not found -func (c *Context) Int64Slice(name string) []int64 { - return lookupInt64Slice(name, c.flagSet) -} - -// GlobalInt64Slice looks up the value of a global Int64SliceFlag, returns -// nil if not found -func (c *Context) GlobalInt64Slice(name string) []int64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupInt64Slice(name, fs) - } - return nil -} - -func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*Int64Slice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// StringFlag is a flag with type string -type StringFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value string - Destination *string -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f StringFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f StringFlag) GetName() string { - return f.Name -} - -// String looks up the value of a local StringFlag, returns -// "" if not found -func (c *Context) String(name string) string { - return lookupString(name, c.flagSet) -} - -// GlobalString looks up the value of a global StringFlag, returns -// "" if not found -func (c *Context) GlobalString(name string) string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value.String(), error(nil) - if err != nil { - return "" - } - return parsed - } - return "" -} - -// StringSliceFlag is a flag with type *StringSlice -type StringSliceFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value *StringSlice -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f StringSliceFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f StringSliceFlag) GetName() string { - return f.Name -} - -// StringSlice looks up the value of a local StringSliceFlag, returns -// nil if not found -func (c *Context) StringSlice(name string) []string { - return lookupStringSlice(name, c.flagSet) -} - -// GlobalStringSlice looks up the value of a global StringSliceFlag, returns -// nil if not found -func (c *Context) GlobalStringSlice(name string) []string { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - parsed, err := (f.Value.(*StringSlice)).Value(), error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} - -// Uint64Flag is a flag with type uint64 -type Uint64Flag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value uint64 - Destination *uint64 -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f Uint64Flag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f Uint64Flag) GetName() string { - return f.Name -} - -// Uint64 looks up the value of a local Uint64Flag, returns -// 0 if not found -func (c *Context) Uint64(name string) uint64 { - return lookupUint64(name, c.flagSet) -} - -// GlobalUint64 looks up the value of a global Uint64Flag, returns -// 0 if not found -func (c *Context) GlobalUint64(name string) uint64 { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupUint64(name, fs) - } - return 0 -} - -func lookupUint64(name string, set *flag.FlagSet) uint64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} - -// UintFlag is a flag with type uint -type UintFlag struct { - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - Value uint - Destination *uint -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f UintFlag) String() string { - return FlagStringer(f) -} - -// GetName returns the name of the flag -func (f UintFlag) GetName() string { - return f.Name -} - -// Uint looks up the value of a local UintFlag, returns -// 0 if not found -func (c *Context) Uint(name string) uint { - return lookupUint(name, c.flagSet) -} - -// GlobalUint looks up the value of a global UintFlag, returns -// 0 if not found -func (c *Context) GlobalUint(name string) uint { - if fs := lookupGlobalFlagSet(name, c); fs != nil { - return lookupUint(name, fs) - } - return 0 -} - -func lookupUint(name string, set *flag.FlagSet) uint { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return uint(parsed) - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/funcs.go b/vendor/github.com/urfave/cli/funcs.go deleted file mode 100644 index 0036b113..00000000 --- a/vendor/github.com/urfave/cli/funcs.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli - -// BashCompleteFunc is an action to execute when the bash-completion flag is set -type BashCompleteFunc func(*Context) - -// BeforeFunc is an action to execute before any subcommands are run, but after -// the context is ready if a non-nil error is returned, no subcommands are run -type BeforeFunc func(*Context) error - -// AfterFunc is an action to execute after any subcommands are run, but after the -// subcommand has finished it is run even if Action() panics -type AfterFunc func(*Context) error - -// ActionFunc is the action to execute when no subcommands are specified -type ActionFunc func(*Context) error - -// CommandNotFoundFunc is executed if the proper command cannot be found -type CommandNotFoundFunc func(*Context, string) - -// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying -// customized usage error messages. This function is able to replace the -// original error messages. If this function is not set, the "Incorrect usage" -// is displayed and the execution is interrupted. -type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error - -// ExitErrHandlerFunc is executed if provided in order to handle ExitError values -// returned by Actions and Before/After functions. -type ExitErrHandlerFunc func(context *Context, err error) - -// FlagStringFunc is used by the help generation to display a flag, which is -// expected to be a single line. -type FlagStringFunc func(Flag) string - -// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix -// text for a flag's full name. -type FlagNamePrefixFunc func(fullName, placeholder string) string - -// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help -// with the environment variable details. -type FlagEnvHintFunc func(envVar, str string) string - -// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help -// with the file path details. -type FlagFileHintFunc func(filePath, str string) string diff --git a/vendor/github.com/urfave/cli/generate-flag-types b/vendor/github.com/urfave/cli/generate-flag-types deleted file mode 100755 index 13588573..00000000 --- a/vendor/github.com/urfave/cli/generate-flag-types +++ /dev/null @@ -1,256 +0,0 @@ -#!/usr/bin/env python -""" -The flag types that ship with the cli library have many things in common, and -so we can take advantage of the `go generate` command to create much of the -source code from a list of definitions. These definitions attempt to cover -the parts that vary between flag types, and should evolve as needed. - -An example of the minimum definition needed is: - - { - "name": "SomeType", - "type": "sometype", - "context_default": "nil" - } - -In this example, the code generated for the `cli` package will include a type -named `SomeTypeFlag` that is expected to wrap a value of type `sometype`. -Fetching values by name via `*cli.Context` will default to a value of `nil`. - -A more complete, albeit somewhat redundant, example showing all available -definition keys is: - - { - "name": "VeryMuchType", - "type": "*VeryMuchType", - "value": true, - "dest": false, - "doctail": " which really only wraps a []float64, oh well!", - "context_type": "[]float64", - "context_default": "nil", - "parser": "parseVeryMuchType(f.Value.String())", - "parser_cast": "[]float64(parsed)" - } - -The meaning of each field is as follows: - - name (string) - The type "name", which will be suffixed with - `Flag` when generating the type definition - for `cli` and the wrapper type for `altsrc` - type (string) - The type that the generated `Flag` type for `cli` - is expected to "contain" as its `.Value` member - value (bool) - Should the generated `cli` type have a `Value` - member? - dest (bool) - Should the generated `cli` type support a - destination pointer? - doctail (string) - Additional docs for the `cli` flag type comment - context_type (string) - The literal type used in the `*cli.Context` - reader func signature - context_default (string) - The literal value used as the default by the - `*cli.Context` reader funcs when no value is - present - parser (string) - Literal code used to parse the flag `f`, - expected to have a return signature of - (value, error) - parser_cast (string) - Literal code used to cast the `parsed` value - returned from the `parser` code -""" - -from __future__ import print_function, unicode_literals - -import argparse -import json -import os -import subprocess -import sys -import tempfile -import textwrap - - -class _FancyFormatter(argparse.ArgumentDefaultsHelpFormatter, - argparse.RawDescriptionHelpFormatter): - pass - - -def main(sysargs=sys.argv[:]): - parser = argparse.ArgumentParser( - description='Generate flag type code!', - formatter_class=_FancyFormatter) - parser.add_argument( - 'package', - type=str, default='cli', choices=_WRITEFUNCS.keys(), - help='Package for which flag types will be generated' - ) - parser.add_argument( - '-i', '--in-json', - type=argparse.FileType('r'), - default=sys.stdin, - help='Input JSON file which defines each type to be generated' - ) - parser.add_argument( - '-o', '--out-go', - type=argparse.FileType('w'), - default=sys.stdout, - help='Output file/stream to which generated source will be written' - ) - parser.epilog = __doc__ - - args = parser.parse_args(sysargs[1:]) - _generate_flag_types(_WRITEFUNCS[args.package], args.out_go, args.in_json) - return 0 - - -def _generate_flag_types(writefunc, output_go, input_json): - types = json.load(input_json) - - tmp = tempfile.NamedTemporaryFile(suffix='.go', delete=False) - writefunc(tmp, types) - tmp.close() - - new_content = subprocess.check_output( - ['goimports', tmp.name] - ).decode('utf-8') - - print(new_content, file=output_go, end='') - output_go.flush() - os.remove(tmp.name) - - -def _set_typedef_defaults(typedef): - typedef.setdefault('doctail', '') - typedef.setdefault('context_type', typedef['type']) - typedef.setdefault('dest', True) - typedef.setdefault('value', True) - typedef.setdefault('parser', 'f.Value, error(nil)') - typedef.setdefault('parser_cast', 'parsed') - - -def _write_cli_flag_types(outfile, types): - _fwrite(outfile, """\ - package cli - - // WARNING: This file is generated! - - """) - - for typedef in types: - _set_typedef_defaults(typedef) - - _fwrite(outfile, """\ - // {name}Flag is a flag with type {type}{doctail} - type {name}Flag struct {{ - Name string - Usage string - EnvVar string - FilePath string - Hidden bool - """.format(**typedef)) - - if typedef['value']: - _fwrite(outfile, """\ - Value {type} - """.format(**typedef)) - - if typedef['dest']: - _fwrite(outfile, """\ - Destination *{type} - """.format(**typedef)) - - _fwrite(outfile, "\n}\n\n") - - _fwrite(outfile, """\ - // String returns a readable representation of this value - // (for usage defaults) - func (f {name}Flag) String() string {{ - return FlagStringer(f) - }} - - // GetName returns the name of the flag - func (f {name}Flag) GetName() string {{ - return f.Name - }} - - // {name} looks up the value of a local {name}Flag, returns - // {context_default} if not found - func (c *Context) {name}(name string) {context_type} {{ - return lookup{name}(name, c.flagSet) - }} - - // Global{name} looks up the value of a global {name}Flag, returns - // {context_default} if not found - func (c *Context) Global{name}(name string) {context_type} {{ - if fs := lookupGlobalFlagSet(name, c); fs != nil {{ - return lookup{name}(name, fs) - }} - return {context_default} - }} - - func lookup{name}(name string, set *flag.FlagSet) {context_type} {{ - f := set.Lookup(name) - if f != nil {{ - parsed, err := {parser} - if err != nil {{ - return {context_default} - }} - return {parser_cast} - }} - return {context_default} - }} - """.format(**typedef)) - - -def _write_altsrc_flag_types(outfile, types): - _fwrite(outfile, """\ - package altsrc - - import ( - "gopkg.in/urfave/cli.v1" - ) - - // WARNING: This file is generated! - - """) - - for typedef in types: - _set_typedef_defaults(typedef) - - _fwrite(outfile, """\ - // {name}Flag is the flag type that wraps cli.{name}Flag to allow - // for other values to be specified - type {name}Flag struct {{ - cli.{name}Flag - set *flag.FlagSet - }} - - // New{name}Flag creates a new {name}Flag - func New{name}Flag(fl cli.{name}Flag) *{name}Flag {{ - return &{name}Flag{{{name}Flag: fl, set: nil}} - }} - - // Apply saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.Apply - func (f *{name}Flag) Apply(set *flag.FlagSet) {{ - f.set = set - f.{name}Flag.Apply(set) - }} - - // ApplyWithError saves the flagSet for later usage calls, then calls the - // wrapped {name}Flag.ApplyWithError - func (f *{name}Flag) ApplyWithError(set *flag.FlagSet) error {{ - f.set = set - return f.{name}Flag.ApplyWithError(set) - }} - """.format(**typedef)) - - -def _fwrite(outfile, text): - print(textwrap.dedent(text), end='', file=outfile) - - -_WRITEFUNCS = { - 'cli': _write_cli_flag_types, - 'altsrc': _write_altsrc_flag_types -} - -if __name__ == '__main__': - sys.exit(main()) diff --git a/vendor/github.com/urfave/cli/help.go b/vendor/github.com/urfave/cli/help.go deleted file mode 100644 index 65874fa2..00000000 --- a/vendor/github.com/urfave/cli/help.go +++ /dev/null @@ -1,345 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - "text/template" -) - -// AppHelpTemplate is the text template for the Default help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} - -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} - -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} - -COMMANDS:{{range .VisibleCategories}}{{if .Name}} - - {{.Name}}:{{end}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} - -GLOBAL OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} - -COPYRIGHT: - {{.Copyright}}{{end}} -` - -// CommandHelpTemplate is the text template for the command help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} - -CATEGORY: - {{.Category}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if .VisibleFlags}} - -OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} -` - -// SubcommandHelpTemplate is the text template for the subcommand help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{if .Description}}{{.Description}}{{else}}{{.Usage}}{{end}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}} - -COMMANDS:{{range .VisibleCategories}}{{if .Name}} - {{.Name}}:{{end}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}} -{{end}}{{if .VisibleFlags}} -OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} -` - -var helpCommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() - if args.Present() { - return ShowCommandHelp(c, args.First()) - } - - ShowAppHelp(c) - return nil - }, -} - -var helpSubcommand = Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() - if args.Present() { - return ShowCommandHelp(c, args.First()) - } - - return ShowSubcommandHelp(c) - }, -} - -// Prints help for the App or Command -type helpPrinter func(w io.Writer, templ string, data interface{}) - -// Prints help for the App or Command with custom template function. -type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) - -// HelpPrinter is a function that writes the help output. If not set a default -// is used. The function signature is: -// func(w io.Writer, templ string, data interface{}) -var HelpPrinter helpPrinter = printHelp - -// HelpPrinterCustom is same as HelpPrinter but -// takes a custom function for template function map. -var HelpPrinterCustom helpPrinterCustom = printHelpCustom - -// VersionPrinter prints the version for the App -var VersionPrinter = printVersion - -// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowAppHelpAndExit(c *Context, exitCode int) { - ShowAppHelp(c) - os.Exit(exitCode) -} - -// ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) (err error) { - if c.App.CustomAppHelpTemplate == "" { - HelpPrinter(c.App.Writer, AppHelpTemplate, c.App) - return - } - customAppData := func() map[string]interface{} { - if c.App.ExtraInfo == nil { - return nil - } - return map[string]interface{}{ - "ExtraInfo": c.App.ExtraInfo, - } - } - HelpPrinterCustom(c.App.Writer, c.App.CustomAppHelpTemplate, c.App, customAppData()) - return nil -} - -// DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - for _, command := range c.App.Commands { - if command.Hidden { - continue - } - if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { - for _, name := range command.Names() { - fmt.Fprintf(c.App.Writer, "%s:%s\n", name, command.Usage) - } - } else { - for _, name := range command.Names() { - fmt.Fprintf(c.App.Writer, "%s\n", name) - } - } - } -} - -// ShowCommandHelpAndExit - exits with code after showing help -func ShowCommandHelpAndExit(c *Context, command string, code int) { - ShowCommandHelp(c, command) - os.Exit(code) -} - -// ShowCommandHelp prints help for the given command -func ShowCommandHelp(ctx *Context, command string) error { - // show the subcommand help for a command with subcommands - if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return nil - } - - for _, c := range ctx.App.Commands { - if c.HasName(command) { - if c.CustomHelpTemplate != "" { - HelpPrinterCustom(ctx.App.Writer, c.CustomHelpTemplate, c, nil) - } else { - HelpPrinter(ctx.App.Writer, CommandHelpTemplate, c) - } - return nil - } - } - - if ctx.App.CommandNotFound == nil { - return NewExitError(fmt.Sprintf("No help topic for '%v'", command), 3) - } - - ctx.App.CommandNotFound(ctx, command) - return nil -} - -// ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelp(c *Context) error { - return ShowCommandHelp(c, c.Command.Name) -} - -// ShowVersion prints the version number of the App -func ShowVersion(c *Context) { - VersionPrinter(c) -} - -func printVersion(c *Context) { - fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) -} - -// ShowCompletions prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App - if a != nil && a.BashComplete != nil { - a.BashComplete(c) - } -} - -// ShowCommandCompletions prints the custom completions for a given command -func ShowCommandCompletions(ctx *Context, command string) { - c := ctx.App.Command(command) - if c != nil && c.BashComplete != nil { - c.BashComplete(ctx) - } -} - -func printHelpCustom(out io.Writer, templ string, data interface{}, customFunc map[string]interface{}) { - funcMap := template.FuncMap{ - "join": strings.Join, - } - if customFunc != nil { - for key, value := range customFunc { - funcMap[key] = value - } - } - - w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) - t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) - err := t.Execute(w, data) - if err != nil { - // If the writer is closed, t.Execute will fail, and there's nothing - // we can do to recover. - if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { - fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) - } - return - } - w.Flush() -} - -func printHelp(out io.Writer, templ string, data interface{}) { - printHelpCustom(out, templ, data, nil) -} - -func checkVersion(c *Context) bool { - found := false - if VersionFlag.GetName() != "" { - eachName(VersionFlag.GetName(), func(name string) { - if c.GlobalBool(name) || c.Bool(name) { - found = true - } - }) - } - return found -} - -func checkHelp(c *Context) bool { - found := false - if HelpFlag.GetName() != "" { - eachName(HelpFlag.GetName(), func(name string) { - if c.GlobalBool(name) || c.Bool(name) { - found = true - } - }) - } - return found -} - -func checkCommandHelp(c *Context, name string) bool { - if c.Bool("h") || c.Bool("help") { - ShowCommandHelp(c, name) - return true - } - - return false -} - -func checkSubcommandHelp(c *Context) bool { - if c.Bool("h") || c.Bool("help") { - ShowSubcommandHelp(c) - return true - } - - return false -} - -func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { - if !a.EnableBashCompletion { - return false, arguments - } - - pos := len(arguments) - 1 - lastArg := arguments[pos] - - if lastArg != "--"+BashCompletionFlag.GetName() { - return false, arguments - } - - return true, arguments[:pos] -} - -func checkCompletions(c *Context) bool { - if !c.shellComplete { - return false - } - - if args := c.Args(); args.Present() { - name := args.First() - if cmd := c.App.Command(name); cmd != nil { - // let the command handle the completion - return false - } - } - - ShowCompletions(c) - return true -} - -func checkCommandCompletions(c *Context, name string) bool { - if !c.shellComplete { - return false - } - - ShowCommandCompletions(c, name) - return true -} diff --git a/vendor/github.com/urfave/cli/runtests b/vendor/github.com/urfave/cli/runtests deleted file mode 100755 index ee22bdee..00000000 --- a/vendor/github.com/urfave/cli/runtests +++ /dev/null @@ -1,122 +0,0 @@ -#!/usr/bin/env python -from __future__ import print_function - -import argparse -import os -import sys -import tempfile - -from subprocess import check_call, check_output - - -PACKAGE_NAME = os.environ.get( - 'CLI_PACKAGE_NAME', 'github.com/urfave/cli' -) - - -def main(sysargs=sys.argv[:]): - targets = { - 'vet': _vet, - 'test': _test, - 'gfmrun': _gfmrun, - 'toc': _toc, - 'gen': _gen, - } - - parser = argparse.ArgumentParser() - parser.add_argument( - 'target', nargs='?', choices=tuple(targets.keys()), default='test' - ) - args = parser.parse_args(sysargs[1:]) - - targets[args.target]() - return 0 - - -def _test(): - if check_output('go version'.split()).split()[2] < 'go1.2': - _run('go test -v .') - return - - coverprofiles = [] - for subpackage in ['', 'altsrc']: - coverprofile = 'cli.coverprofile' - if subpackage != '': - coverprofile = '{}.coverprofile'.format(subpackage) - - coverprofiles.append(coverprofile) - - _run('go test -v'.split() + [ - '-coverprofile={}'.format(coverprofile), - ('{}/{}'.format(PACKAGE_NAME, subpackage)).rstrip('/') - ]) - - combined_name = _combine_coverprofiles(coverprofiles) - _run('go tool cover -func={}'.format(combined_name)) - os.remove(combined_name) - - -def _gfmrun(): - go_version = check_output('go version'.split()).split()[2] - if go_version < 'go1.3': - print('runtests: skip on {}'.format(go_version), file=sys.stderr) - return - _run(['gfmrun', '-c', str(_gfmrun_count()), '-s', 'README.md']) - - -def _vet(): - _run('go vet ./...') - - -def _toc(): - _run('node_modules/.bin/markdown-toc -i README.md') - _run('git diff --exit-code') - - -def _gen(): - go_version = check_output('go version'.split()).split()[2] - if go_version < 'go1.5': - print('runtests: skip on {}'.format(go_version), file=sys.stderr) - return - - _run('go generate ./...') - _run('git diff --exit-code') - - -def _run(command): - if hasattr(command, 'split'): - command = command.split() - print('runtests: {}'.format(' '.join(command)), file=sys.stderr) - check_call(command) - - -def _gfmrun_count(): - with open('README.md') as infile: - lines = infile.read().splitlines() - return len(filter(_is_go_runnable, lines)) - - -def _is_go_runnable(line): - return line.startswith('package main') - - -def _combine_coverprofiles(coverprofiles): - combined = tempfile.NamedTemporaryFile( - suffix='.coverprofile', delete=False - ) - combined.write('mode: set\n') - - for coverprofile in coverprofiles: - with open(coverprofile, 'r') as infile: - for line in infile.readlines(): - if not line.startswith('mode: '): - combined.write(line) - - combined.flush() - name = combined.name - combined.close() - return name - - -if __name__ == '__main__': - sys.exit(main()) diff --git a/vendor/github.com/urfave/cli/sort.go b/vendor/github.com/urfave/cli/sort.go deleted file mode 100644 index 23d1c2f7..00000000 --- a/vendor/github.com/urfave/cli/sort.go +++ /dev/null @@ -1,29 +0,0 @@ -package cli - -import "unicode" - -// lexicographicLess compares strings alphabetically considering case. -func lexicographicLess(i, j string) bool { - iRunes := []rune(i) - jRunes := []rune(j) - - lenShared := len(iRunes) - if lenShared > len(jRunes) { - lenShared = len(jRunes) - } - - for index := 0; index < lenShared; index++ { - ir := iRunes[index] - jr := jRunes[index] - - if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { - return lir < ljr - } - - if ir != jr { - return ir < jr - } - } - - return i < j -} diff --git a/vendor/vendor.json b/vendor/vendor.json deleted file mode 100644 index a1e24a8a..00000000 --- a/vendor/vendor.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "comment": "", - "ignore": "test", - "package": [ - { - "checksumSHA1": "ljd3FhYRJ91cLZz3wsH9BQQ2JbA=", - "path": "github.com/pkg/errors", - "revision": "816c9085562cd7ee03e7f8188a1cfd942858cded", - "revisionTime": "2018-03-11T21:45:15Z" - }, - { - "checksumSHA1": "9/2qvuFPQxW7ZXzQSOYwqZb5cNg=", - "path": "github.com/urfave/cli", - "revision": "8e01ec4cd3e2d84ab2fe90d8210528ffbb06d8ff", - "revisionTime": "2018-02-26T03:02:53Z" - } - ], - "rootPath": "github.com/StackExchange/blackbox" -} From a630c6b433bf79d21847550dcf333a80fabea56d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 11:41:20 -0400 Subject: [PATCH 11/69] flags.go and parse.go complete --- Version2-Ideas.md | 30 +++++-- cmd/blackbox/flags.go | 20 +---- cmd/blackbox/parse.go | 173 +++++++++++++++++++++++++++++---------- cmd/blackbox/runbash.go | 3 +- go.mod | 5 ++ go.sum | 13 +++ pkg/bbgit/discover.go | 3 +- pkg/bbutil/admin.go | 3 +- pkg/bbutil/adminplain.go | 3 +- pkg/bbutil/decrypt.go | 3 +- pkg/bbutil/iterator.go | 4 +- pkg/bbutil/reg.go | 3 +- pkg/bbutil/repoinfo.go | 4 +- pkg/box/box.go | 79 +++++++++++++++--- 14 files changed, 262 insertions(+), 84 deletions(-) create mode 100644 go.mod create mode 100644 go.sum diff --git a/Version2-Ideas.md b/Version2-Ideas.md index 25e1b69a..ec688ca9 100644 --- a/Version2-Ideas.md +++ b/Version2-Ideas.md @@ -19,6 +19,7 @@ These are the things I'd like to change someday. There should be one program, with subcommands that have names that make more sense: * `blackbox init` +* `blackbox info` * `blackbox admin add ` * `blackbox admin remove ` * `blackbox admin list` @@ -27,14 +28,33 @@ There should be one program, with subcommands that have names that make more sen * `blackbox files remove` * `blackbox encrypt ...` * `blackbox decrypt ...` -* `blackbox decrypt_all` * `blackbox cat ...` * `blackbox edit ...` -* `blackbox reencrypt_all` -* `blackbox shred_all` +* `blackbox reencrypt` +* `blackbox shred` * `blackbox diff ...` -* `blackbox files unchanged` -* `blackbox files changed` +* `blackbox files list-unchanged` +* `blackbox files list-changed` + +* `blackbox admin add ` +* `blackbox admin list` +* `blackbox admin remove ` +* `blackbox cat ...` +* `blackbox decrypt ...` +* `blackbox diff ...` +* `blackbox edit ...` +* `blackbox encrypt ...` +* `blackbox file add` +* `blackbox file list` +* `blackbox file remove` +* `blackbox info` +* `blackbox init` +* `blackbox reencrypt` +* `blackbox shred` +* `blackbox status all` +* `blackbox status changed` +* `blackbox status unchanged` + Flags where appropriate diff --git a/cmd/blackbox/flags.go b/cmd/blackbox/flags.go index 650db688..dbe139b1 100644 --- a/cmd/blackbox/flags.go +++ b/cmd/blackbox/flags.go @@ -145,23 +145,9 @@ func flags() *cli.App { Usage: "Print status of files", Flags: []cli.Flag{ &cli.BoolFlag{Name: "name-only", Usage: "Show only names of the files"}, - }, - Subcommands: []*cli.Command{ - { - Name: "all", - Usage: "All registered files", - Action: func(c *cli.Context) error { return cmdStatusAll(c) }, - }, - { - Name: "changed", - Usage: "Only include files that are newer than their .gpg file", - Action: func(c *cli.Context) error { return cmdStatusChanged(c) }, - }, - { - Name: "unchanged", - Usage: "Only include files that 'status changed' wouldn't list", - Action: func(c *cli.Context) error { return cmdStatusUnchanged(c) }, - }, + &cli.BoolFlag{Name: "all", Usage: "All registered files"}, + &cli.BoolFlag{Name: "changed", Usage: "Only files newer than their .gpg"}, + &cli.BoolFlag{Name: "unchanged", Usage: "Only files older than their .gpg"}, }, }, diff --git a/cmd/blackbox/parse.go b/cmd/blackbox/parse.go index 3c484e7f..6b660cd2 100644 --- a/cmd/blackbox/parse.go +++ b/cmd/blackbox/parse.go @@ -1,6 +1,7 @@ package main -// All the "blackbox admin" subcommands. +// Processes the flags and arguments and calls the appropriate +// business logic. import ( "fmt" @@ -19,10 +20,23 @@ func init() { } } +func allOrSomeFiles(c *cli.Context) error { + if c.Bool("all") { + if c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --all") + } + } else { + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one file name") + } + } + return nil +} + // Keep these functions in alphabetical order. func cmdAdminAdd(c *cli.Context) error { - if c.Args().Present() { + if !c.Args().Present() { return fmt.Errorf( "Must specify at least one admin's GnuPG user-id (i.e. email address)") } @@ -31,91 +45,168 @@ func cmdAdminAdd(c *cli.Context) error { } func cmdAdminList(c *cli.Context) error { - if !c.Args().Present() { + if c.Args().Present() { return fmt.Errorf("This command takes zero arguments") } bx := box.NewFromFlags(c) - return bx.AdminList(bx) + return bx.AdminList() } func cmdAdminRemove(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one admin's GnuPG user-id (i.e. email address)") + } + bx := box.NewFromFlags(c) + return bx.AdminRemove(c.Args().Slice()) } func cmdCat(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one file name") + } + bx := box.NewFromFlags(c) + return bx.Cat(c.Args().Slice()) } func cmdDecrypt(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if err := allOrSomeFiles(c); err != nil { + return err + } + bulk := false + if c.Bool("all") { + bulk = c.Bool("bulk") // Only applies to --all + } + bx := box.NewFromFlags(c) + return bx.Decrypt(c.Args().Slice(), + c.Bool("overwrite"), + bulk, + c.String("group"), + ) } func cmdDiff(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if err := allOrSomeFiles(c); err != nil { + return err + } + bx := box.NewFromFlags(c) + return bx.Diff(c.Args().Slice()) } func cmdEdit(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one file name") + } + bx := box.NewFromFlags(c) + return bx.Edit(c.Args().Slice()) } func cmdEncrypt(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if err := allOrSomeFiles(c); err != nil { + return err + } + bulk := false + if c.Bool("all") { + bulk = c.Bool("bulk") // Only applies to --all + } + bx := box.NewFromFlags(c) + return bx.Encrypt(c.Args().Slice(), bulk, c.String("group"), c.Bool("overwrite")) } func cmdFileAdd(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one file name") + } + bx := box.NewFromFlags(c) + return bx.FileAdd(c.Args().Slice(), c.Bool("overwrite")) } func cmdFileList(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if c.Args().Present() { + return fmt.Errorf("This command takes zero arguments") + } + bx := box.NewFromFlags(c) + return bx.FileList() } func cmdFileRemove(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if !c.Args().Present() { + return fmt.Errorf("Must specify at least one file name") + } + bx := box.NewFromFlags(c) + return bx.FileRemove(c.Args().Slice()) } func cmdInfo(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if c.Args().Present() { + return fmt.Errorf("This command takes zero arguments") + } + bx := box.NewFromFlags(c) + return bx.Info() } func cmdInit(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if c.Args().Present() { + return fmt.Errorf("This command takes zero arguments") + } + bx := box.NewFromFlags(c) + return bx.Init() } func cmdReencrypt(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if err := allOrSomeFiles(c); err != nil { + return err + } + bx := box.NewFromFlags(c) + return bx.Reencrypt(c.Args().Slice()) } func cmdShred(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if err := allOrSomeFiles(c); err != nil { + return err + } + bx := box.NewFromFlags(c) + return bx.Shred(c.Args().Slice()) } -func cmdStatusAll(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil -} +func cmdStatus(c *cli.Context) error { + if err := allOrSomeFiles(c); err != nil { + return err + } -func cmdStatusChanged(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil -} + mode := box.Itemized -func cmdStatusUnchanged(c *cli.Context) error { - logErr.Println("NOT IMPLEMENTED") - return nil + if c.Bool("all") { + if c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --all") + } + if mode != box.Itemized { + return fmt.Errorf("--all can not be mixed with other flags") + } + mode = box.All + } + + if c.Bool("changed") { + if c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --changed") + } + if mode != box.Itemized { + return fmt.Errorf("--changed can not be mixed with other flags") + } + mode = box.Changed + } + + if c.Bool("unchanged") { + if c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --unchanged") + } + if mode != box.Itemized { + return fmt.Errorf("--unchanged can not be mixed with other flags") + } + mode = box.Unchanged + } + + bx := box.NewFromFlags(c) + return bx.Status(c.Args().Slice(), mode, c.Bool("name-only")) } //func cmdInfo(c *cli.Context) error { diff --git a/cmd/blackbox/runbash.go b/cmd/blackbox/runbash.go index 39b980d1..43549977 100644 --- a/cmd/blackbox/runbash.go +++ b/cmd/blackbox/runbash.go @@ -1,4 +1,5 @@ -// package main +package main + // // import ( // "fmt" diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..0fec8471 --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module github.com/StackExchange/blackbox + +go 1.14 + +require github.com/urfave/cli/v2 v2.2.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..033734fb --- /dev/null +++ b/go.sum @@ -0,0 +1,13 @@ +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= +github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/bbgit/discover.go b/pkg/bbgit/discover.go index 150e11e8..07880f39 100644 --- a/pkg/bbgit/discover.go +++ b/pkg/bbgit/discover.go @@ -1,4 +1,5 @@ -// package bbgit +package bbgit + // // import ( // "os/exec" diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go index ea7eb667..36ea1179 100644 --- a/pkg/bbutil/admin.go +++ b/pkg/bbutil/admin.go @@ -1,4 +1,5 @@ -// package bbutil +package bbutil + // // // Administrator is a description of the admininstrators. // type Administrator struct { diff --git a/pkg/bbutil/adminplain.go b/pkg/bbutil/adminplain.go index 5875412e..e0895e49 100644 --- a/pkg/bbutil/adminplain.go +++ b/pkg/bbutil/adminplain.go @@ -1,4 +1,5 @@ -// package bbutil +package bbutil + // // import ( // "io/ioutil" diff --git a/pkg/bbutil/decrypt.go b/pkg/bbutil/decrypt.go index 546b761d..434357ff 100644 --- a/pkg/bbutil/decrypt.go +++ b/pkg/bbutil/decrypt.go @@ -1,4 +1,5 @@ -// package bbutil +package bbutil + // // import ( // "fmt" diff --git a/pkg/bbutil/iterator.go b/pkg/bbutil/iterator.go index cefd16cd..8d993b80 100644 --- a/pkg/bbutil/iterator.go +++ b/pkg/bbutil/iterator.go @@ -1,5 +1,5 @@ -// package bbutil -// +package bbutil + // import ( // "sort" // ) diff --git a/pkg/bbutil/reg.go b/pkg/bbutil/reg.go index b258c989..c30a53d0 100644 --- a/pkg/bbutil/reg.go +++ b/pkg/bbutil/reg.go @@ -1,4 +1,5 @@ -// package bbutil +package bbutil + // // import ( // "io/ioutil" diff --git a/pkg/bbutil/repoinfo.go b/pkg/bbutil/repoinfo.go index bedca191..3d333642 100644 --- a/pkg/bbutil/repoinfo.go +++ b/pkg/bbutil/repoinfo.go @@ -1,5 +1,5 @@ -// package bbutil -// +package bbutil + // import ( // "os" // "path/filepath" diff --git a/pkg/box/box.go b/pkg/box/box.go index 7986a630..abb86b93 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -1,32 +1,89 @@ package box +// box implements the business logic for all operations on the blackbox. + import ( "fmt" "github.com/urfave/cli/v2" ) -// Box provides access to a Blackbox. -type Boxer interface { - AdminAdd([]string) error - AdminList() error - AdminRemove([]string) error -} - type box struct { } -func (bx *box) NewFromFlags(c *cli.Context) error { +func NewFromFlags(c *cli.Context) *box { + return &box{} } +type StatusMode int + +const ( + Itemized StatusMode = iota + All + Unchanged + Changed +) + func (bx *box) AdminAdd([]string) error { - return fmt.Errorf("NOT IMPLEMENTED") + return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") } func (bx *box) AdminList() error { - return fmt.Errorf("NOT IMPLEMENTED") + return fmt.Errorf("NOT IMPLEMENTED: AdminList") } func (bx *box) AdminRemove([]string) error { - return fmt.Errorf("NOT IMPLEMENTED") + return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") +} + +func (bx *box) Cat([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Cat") +} + +func (bx *box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { + return fmt.Errorf("NOT IMPLEMENTED: Decrypt") +} + +func (bx *box) Diff([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Diff") +} + +func (bx *box) Edit([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Edit") +} + +func (bx *box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { + return fmt.Errorf("NOT IMPLEMENTED: Encrypt") +} + +func (bx *box) FileAdd(names []string, overwrite bool) error { + return fmt.Errorf("NOT IMPLEMENTED: FileAdd") +} + +func (bx *box) FileList() error { + return fmt.Errorf("NOT IMPLEMENTED: FileList") +} + +func (bx *box) FileRemove(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: FileRemove") +} + +func (bx *box) Info() error { + return fmt.Errorf("NOT IMPLEMENTED: Info") +} + +func (bx *box) Init() error { + return fmt.Errorf("NOT IMPLEMENTED: Init") +} + +func (bx *box) Reencrypt(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") +} + +func (bx *box) Shred(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: Shred") +} + +func (bx *box) Status(names []string, mode StatusMode, nameOnly bool) error { + return fmt.Errorf("NOT IMPLEMENTED: Status") } From 0bdffb39f9cb743cd71c08a50c038a7057684dd1 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 12:07:41 -0400 Subject: [PATCH 12/69] Draft --- pkg/box/box.go | 70 ++++++---------------------------------- pkg/box/verbs.go | 83 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 92 insertions(+), 61 deletions(-) create mode 100644 pkg/box/verbs.go diff --git a/pkg/box/box.go b/pkg/box/box.go index abb86b93..f8fe1ff0 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -1,6 +1,6 @@ package box -// box implements the business logic for all operations on the blackbox. +// box implements the box model. import ( "fmt" @@ -9,6 +9,7 @@ import ( ) type box struct { + Admins []string } func NewFromFlags(c *cli.Context) *box { @@ -24,66 +25,13 @@ const ( Changed ) -func (bx *box) AdminAdd([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") -} - -func (bx *box) AdminList() error { - return fmt.Errorf("NOT IMPLEMENTED: AdminList") -} - -func (bx *box) AdminRemove([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") -} - -func (bx *box) Cat([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: Cat") -} - -func (bx *box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { - return fmt.Errorf("NOT IMPLEMENTED: Decrypt") -} - -func (bx *box) Diff([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: Diff") -} - -func (bx *box) Edit([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: Edit") -} - -func (bx *box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { - return fmt.Errorf("NOT IMPLEMENTED: Encrypt") -} - -func (bx *box) FileAdd(names []string, overwrite bool) error { - return fmt.Errorf("NOT IMPLEMENTED: FileAdd") -} - -func (bx *box) FileList() error { - return fmt.Errorf("NOT IMPLEMENTED: FileList") -} - -func (bx *box) FileRemove(names []string) error { - return fmt.Errorf("NOT IMPLEMENTED: FileRemove") -} +func (bx *box) getAdmins() ([]string, error) { + if len(bx.Admins) != 0 { + return bx.Admins, nil + } -func (bx *box) Info() error { - return fmt.Errorf("NOT IMPLEMENTED: Info") -} - -func (bx *box) Init() error { - return fmt.Errorf("NOT IMPLEMENTED: Init") -} - -func (bx *box) Reencrypt(names []string) error { - return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") -} - -func (bx *box) Shred(names []string) error { - return fmt.Errorf("NOT IMPLEMENTED: Shred") -} + fmt.Printf("Would load\n") + bx.Admins = nil -func (bx *box) Status(names []string, mode StatusMode, nameOnly bool) error { - return fmt.Errorf("NOT IMPLEMENTED: Status") + return bx.Admins, nil } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go new file mode 100644 index 00000000..49f6c7e7 --- /dev/null +++ b/pkg/box/verbs.go @@ -0,0 +1,83 @@ +package box + +// This file implements the business logic related to a black box. + +import "fmt" + +func (bx *box) AdminAdd([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") +} + +func (bx *box) AdminList() error { + admins, err := bx.GetAdmins() + if err != nil { + return err + } + for _, v := range admins { + fmt.Println(v) + } + return nil +} + +func (bx *box) AdminRemove([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") +} + +func (bx *box) Cat([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Cat") +} + +func (bx *box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { + return fmt.Errorf("NOT IMPLEMENTED: Decrypt") +} + +func (bx *box) Diff([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Diff") +} + +func (bx *box) Edit([]string) error { + return fmt.Errorf("NOT IMPLEMENTED: Edit") +} + +func (bx *box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { + return fmt.Errorf("NOT IMPLEMENTED: Encrypt") +} + +func (bx *box) FileAdd(names []string, overwrite bool) error { + return fmt.Errorf("NOT IMPLEMENTED: FileAdd") +} + +func (bx *box) FileList() error { + return fmt.Errorf("NOT IMPLEMENTED: FileList") +} + +func (bx *box) FileRemove(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: FileRemove") +} + +func (bx *box) Info() error { + fmt.Println("VCS:") + //fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) + //fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) + //fmt.Print("REPO:\n") + //fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) + //fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) + + return nil +} + +func (bx *box) Init() error { + return fmt.Errorf("NOT IMPLEMENTED: Init") +} + +func (bx *box) Reencrypt(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") +} + +func (bx *box) Shred(names []string) error { + return fmt.Errorf("NOT IMPLEMENTED: Shred") +} + +func (bx *box) Status(names []string, mode StatusMode, nameOnly bool) error { + return fmt.Errorf("NOT IMPLEMENTED: Status") +} From a6ec716067745b11b3b3024b78ff9674f85a9579 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 16:46:08 -0400 Subject: [PATCH 13/69] Box discovers admins and files. --- pkg/box/box.go | 143 ++++++++++++++++++++++++++++++++++++++++++++--- pkg/box/verbs.go | 27 ++++++--- 2 files changed, 155 insertions(+), 15 deletions(-) diff --git a/pkg/box/box.go b/pkg/box/box.go index f8fe1ff0..73ac124b 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -4,16 +4,22 @@ package box import ( "fmt" + "io/ioutil" + "log" + "os" + "path/filepath" + "strings" "github.com/urfave/cli/v2" ) type box struct { - Admins []string -} - -func NewFromFlags(c *cli.Context) *box { - return &box{} + // + RepoBaseDir string // Base directory of the repo. + ConfigDir string // Path to the .blackbox config directory. + // + Admins []string // If non-empty, the list of admins. + Files []string // If non-empty, the list of files. } type StatusMode int @@ -25,13 +31,134 @@ const ( Changed ) +var logErr *log.Logger + +func init() { + logErr = log.New(os.Stderr, "", 0) +} + +func NewFromFlags(c *cli.Context) *box { + bx := &box{} + + repoBaseDir, configDir, err := findBaseAndConfigDir() + if err != nil { + logErr.Println(err) + return bx + } + bx.RepoBaseDir = repoBaseDir + bx.ConfigDir = configDir + + return bx +} + +func dirExists(path string) (bool, error) { + stat, err := os.Stat(path) + if err == nil { + return stat.IsDir(), nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} + +func findBaseAndConfigDir() (repodir, configdir string, err error) { + + // If BLACKBOXDATA/BLACKBOX_CONFIGDIR is set, that is the config dir. + d := os.Getenv("BLACKBOXDATA") + c := os.Getenv("BLACKBOX_CONFIGDIR") + r := os.Getenv("BLACKBOX_REPOBASEDIR") + // If any of those are used, r must be set and one or both of d & c + // must be set. d is used before c. + if d != "" { + logErr.Printf("BLACKBOXDATA deprecated. Please use BLACKBOX_CONFIGDIR") + } + if (d != "") || (c != "") || (r != "") { + if (d != "") && (r != "") { + return r, d, nil + } + if (c != "") && (r != "") { + return r, c, nil + } + return c, r, fmt.Errorf("if BLACKBOX_REPOBASEDIR or BLACKBOX_REPOBASEDIR is used, BLACKBOX_REPOBASEDIR must be set") + } + + // Otherwise, search up the tree for the config dir. + + candidates := []string{} + if team := os.Getenv("BLACKBOX_TEAM"); team != "" { + candidates = append([]string{".blackbox-" + team}, candidates...) + } + candidates = append(candidates, ".blackbox") + candidates = append(candidates, "keyrings/live") + + // Prevent an infinite loop by only doing "cd .." this many times + maxDirLevels := 100 + + relpath := "" + for i := 0; i < maxDirLevels; i++ { + // Does relpath contain any of our directory names? + for _, c := range candidates { + t := filepath.Join(relpath, c) + d, err := dirExists(t) + if err != nil { + return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) + } + if d { + return relpath, t, nil + } + } + // If we are at the root, stop. + if abs, _ := filepath.Abs(relpath); abs == "/" { + break + } + // Try one directory up + relpath = filepath.Join("..", relpath) + } + + return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") +} + func (bx *box) getAdmins() ([]string, error) { if len(bx.Admins) != 0 { return bx.Admins, nil } - fmt.Printf("Would load\n") - bx.Admins = nil + // TODO(tlim): Try the json file. + + // Try the legacy file: + fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") + b, err := ioutil.ReadFile(fn) + c := strings.TrimSpace(string(b)) + if err == nil { + bx.Admins = strings.Split(c, "\n") + return bx.Admins, nil + } + if !os.IsNotExist(err) { + return nil, fmt.Errorf("getAdmins can't open %q: %v", fn, err) + } + + return nil, fmt.Errorf("getAdmins can't load admin list") +} + +func (bx *box) getFiles() ([]string, error) { + if len(bx.Files) != 0 { + return bx.Files, nil + } + + // TODO(tlim): Try the json file. + + // Try the legacy file: + fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") + b, err := ioutil.ReadFile(fn) + c := strings.TrimSpace(string(b)) + if err == nil { + bx.Files = strings.Split(c, "\n") + return bx.Files, nil + } + if !os.IsNotExist(err) { + return nil, fmt.Errorf("getFiles can't open %q: %v", fn, err) + } - return bx.Admins, nil + return nil, fmt.Errorf("getFiles can't load file list") } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 49f6c7e7..93fc602f 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -9,10 +9,12 @@ func (bx *box) AdminAdd([]string) error { } func (bx *box) AdminList() error { - admins, err := bx.GetAdmins() + + admins, err := bx.getAdmins() if err != nil { return err } + for _, v := range admins { fmt.Println(v) } @@ -56,12 +58,23 @@ func (bx *box) FileRemove(names []string) error { } func (bx *box) Info() error { - fmt.Println("VCS:") - //fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) - //fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) - //fmt.Print("REPO:\n") - //fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) - //fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) + + _, err := bx.getAdmins() + if err != nil { + logErr.Printf("getAdmins error: %v", err) + } + + _, err = bx.getFiles() + if err != nil { + logErr.Printf("getFiles error: %v", err) + } + + fmt.Println("BLACKBOX:") + fmt.Printf("bx.ConfigDir=%q\n", bx.ConfigDir) + //fmt.Printf("bx.Admins=%q\n", bx.Admins) + fmt.Printf("len(bx.Admins)=%v\n", len(bx.Admins)) + //fmt.Printf("bx.Files=%q\n", bx.Files) + fmt.Printf("len(bx.Files)=%v\n", len(bx.Files)) return nil } From 50fa2e515f2901be7c1a16aaf4101213976f24f9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 22:34:24 -0400 Subject: [PATCH 14/69] Snapshot --- cmd/blackbox/blackbox.go | 3 +++ cmd/blackbox/runbash.go | 28 ---------------------------- models/vcs.go | 6 ++++++ pkg/box/box.go | 28 ++++++++++++++++++++++------ pkg/box/verbs.go | 32 ++++++++++++++++---------------- vcs/_all/all.go | 6 ++++++ vcs/git/git.go | 22 ++++++++++++++++++++++ vcs/none/none.go | 23 +++++++++++++++++++++++ vcs/vcs.go | 38 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 136 insertions(+), 50 deletions(-) delete mode 100644 cmd/blackbox/runbash.go create mode 100644 models/vcs.go create mode 100644 vcs/_all/all.go create mode 100644 vcs/git/git.go create mode 100644 vcs/none/none.go create mode 100644 vcs/vcs.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 9b3526fa..2e8eb967 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -3,6 +3,9 @@ package main import ( "fmt" "os" + + _ "github.com/StackExchange/blackbox/vcs" + _ "github.com/StackExchange/blackbox/vcs/_all" ) var dryRun bool diff --git a/cmd/blackbox/runbash.go b/cmd/blackbox/runbash.go deleted file mode 100644 index 43549977..00000000 --- a/cmd/blackbox/runbash.go +++ /dev/null @@ -1,28 +0,0 @@ -package main - -// -// import ( -// "fmt" -// "log" -// "os" -// "os/exec" -// -// ) -// -// // RunBash runs a Bash command. -// func RunBash(command string, args ...string) error { -// if dryRun { -// fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) -// return nil -// } -// cmd := exec.Command(command, args...) -// cmd.Stdin = os.Stdin -// cmd.Stdout = os.Stdout -// cmd.Stderr = os.Stderr -// err := cmd.Start() -// if err != nil { -// log.Fatal(err) -// } -// err = cmd.Wait() -// return errors.Wrapf(err, "run_bash:") -// } diff --git a/models/vcs.go b/models/vcs.go new file mode 100644 index 00000000..e2cafbfe --- /dev/null +++ b/models/vcs.go @@ -0,0 +1,6 @@ +package models + +// Vcs is git/hg/etc. +type Vcs interface { + Discover() bool +} diff --git a/pkg/box/box.go b/pkg/box/box.go index 73ac124b..c22fdd05 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -10,10 +10,13 @@ import ( "path/filepath" "strings" + // "github.com/StackExchange/blackbox/vcs" + "github.com/StackExchange/blackbox/vcs" "github.com/urfave/cli/v2" ) -type box struct { +// Box describes what we know about a box. +type Box struct { // RepoBaseDir string // Base directory of the repo. ConfigDir string // Path to the .blackbox config directory. @@ -22,12 +25,17 @@ type box struct { Files []string // If non-empty, the list of files. } +// StatusMode is a type of query. type StatusMode int const ( - Itemized StatusMode = iota + // Itemized is blah + Itemized StatusMode = iota // Individual files by name + // All files is blah All + // Unchanged is blah Unchanged + // Changed is blah Changed ) @@ -37,8 +45,9 @@ func init() { logErr = log.New(os.Stderr, "", 0) } -func NewFromFlags(c *cli.Context) *box { - bx := &box{} +// NewFromFlags creates a box using items from flags. +func NewFromFlags(c *cli.Context) *Box { + bx := &Box{} repoBaseDir, configDir, err := findBaseAndConfigDir() if err != nil { @@ -48,6 +57,13 @@ func NewFromFlags(c *cli.Context) *box { bx.RepoBaseDir = repoBaseDir bx.ConfigDir = configDir + // Discover which kind of VCS is in use. + // vcsName, vcsHandle := discoverVCS() + fmt.Printf("VCS len = %v\n", len(vcs.Catalog)) + for i, v := range vcs.Catalog { + fmt.Printf("VCS[%v] = %q %q", i, v.Name, v.Priority) + } + return bx } @@ -119,7 +135,7 @@ func findBaseAndConfigDir() (repodir, configdir string, err error) { return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") } -func (bx *box) getAdmins() ([]string, error) { +func (bx *Box) getAdmins() ([]string, error) { if len(bx.Admins) != 0 { return bx.Admins, nil } @@ -141,7 +157,7 @@ func (bx *box) getAdmins() ([]string, error) { return nil, fmt.Errorf("getAdmins can't load admin list") } -func (bx *box) getFiles() ([]string, error) { +func (bx *Box) getFiles() ([]string, error) { if len(bx.Files) != 0 { return bx.Files, nil } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 93fc602f..74c0422f 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -4,11 +4,11 @@ package box import "fmt" -func (bx *box) AdminAdd([]string) error { +func (bx *Box) AdminAdd([]string) error { return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") } -func (bx *box) AdminList() error { +func (bx *Box) AdminList() error { admins, err := bx.getAdmins() if err != nil { @@ -21,43 +21,43 @@ func (bx *box) AdminList() error { return nil } -func (bx *box) AdminRemove([]string) error { +func (bx *Box) AdminRemove([]string) error { return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") } -func (bx *box) Cat([]string) error { +func (bx *Box) Cat([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Cat") } -func (bx *box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { +func (bx *Box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { return fmt.Errorf("NOT IMPLEMENTED: Decrypt") } -func (bx *box) Diff([]string) error { +func (bx *Box) Diff([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Diff") } -func (bx *box) Edit([]string) error { +func (bx *Box) Edit([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Edit") } -func (bx *box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { +func (bx *Box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { return fmt.Errorf("NOT IMPLEMENTED: Encrypt") } -func (bx *box) FileAdd(names []string, overwrite bool) error { +func (bx *Box) FileAdd(names []string, overwrite bool) error { return fmt.Errorf("NOT IMPLEMENTED: FileAdd") } -func (bx *box) FileList() error { +func (bx *Box) FileList() error { return fmt.Errorf("NOT IMPLEMENTED: FileList") } -func (bx *box) FileRemove(names []string) error { +func (bx *Box) FileRemove(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: FileRemove") } -func (bx *box) Info() error { +func (bx *Box) Info() error { _, err := bx.getAdmins() if err != nil { @@ -79,18 +79,18 @@ func (bx *box) Info() error { return nil } -func (bx *box) Init() error { +func (bx *Box) Init() error { return fmt.Errorf("NOT IMPLEMENTED: Init") } -func (bx *box) Reencrypt(names []string) error { +func (bx *Box) Reencrypt(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") } -func (bx *box) Shred(names []string) error { +func (bx *Box) Shred(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Shred") } -func (bx *box) Status(names []string, mode StatusMode, nameOnly bool) error { +func (bx *Box) Status(names []string, mode StatusMode, nameOnly bool) error { return fmt.Errorf("NOT IMPLEMENTED: Status") } diff --git a/vcs/_all/all.go b/vcs/_all/all.go new file mode 100644 index 00000000..f6512648 --- /dev/null +++ b/vcs/_all/all.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/StackExchange/blackbox/vcs/git" + _ "github.com/StackExchange/blackbox/vcs/none" +) diff --git a/vcs/git/git.go b/vcs/git/git.go new file mode 100644 index 00000000..a821b1d9 --- /dev/null +++ b/vcs/git/git.go @@ -0,0 +1,22 @@ +package git + +import ( + "github.com/StackExchange/blackbox/vcs" +) + +func init() { + vcs.Register("GIT", 1, newGit) +} + +// VcsHandle is the handle +type VcsHandle struct { +} + +func newGit() (*VcsHandle, error) { + return &VcsHandle{}, nil +} + +// Discover returns false. +func (v VcsHandle) Discover() bool { + return false +} diff --git a/vcs/none/none.go b/vcs/none/none.go new file mode 100644 index 00000000..84745f98 --- /dev/null +++ b/vcs/none/none.go @@ -0,0 +1,23 @@ +package none + +import ( + "github.com/StackExchange/blackbox/vcs" +) + +func init() { + vcs.Register("NONE", 0, newNone) +} + +// VcsHandle is +type VcsHandle struct { + Age int +} + +func newNone() (*VcsHandle, error) { + return &VcsHandle{}, nil +} + +// Discover returns false +func (v VcsHandle) Discover() bool { + return false +} diff --git a/vcs/vcs.go b/vcs/vcs.go new file mode 100644 index 00000000..e5ded167 --- /dev/null +++ b/vcs/vcs.go @@ -0,0 +1,38 @@ +package vcs + +import ( + "sort" + + "github.com/StackExchange/blackbox/models" +) + +// Vcs is the handle +type Vcs interface { + models.Vcs +} + +// NewFnSig function signature needed by reg. +type NewFnSig func(int) (Vcs, error) + +// Item stores one item +type Item struct { + Name string + New NewFnSig + Priority int +} + +// Catalog is the list of registered vcs's. +var Catalog []*Item + +// Register a new VCS. +func Register(name string, priority int, newfn NewFnSig) { + item := &Item{ + Name: name, + New: newfn, + Priority: priority, + } + Catalog = append(Catalog, item) + + // Keep the list sorted. + sort.Slice(Catalog, func(i, j int) bool { return Catalog[i].Priority < Catalog[j].Priority }) +} From d7bdd49cced9a8ba5e5925941889b118382d20c7 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 6 Jun 2020 22:46:11 -0400 Subject: [PATCH 15/69] Fixed the signature mismatch problem!!! --- vcs/git/git.go | 2 +- vcs/none/none.go | 2 +- vcs/vcs.go | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/vcs/git/git.go b/vcs/git/git.go index a821b1d9..1307d23b 100644 --- a/vcs/git/git.go +++ b/vcs/git/git.go @@ -12,7 +12,7 @@ func init() { type VcsHandle struct { } -func newGit() (*VcsHandle, error) { +func newGit() (vcs.Vcs, error) { return &VcsHandle{}, nil } diff --git a/vcs/none/none.go b/vcs/none/none.go index 84745f98..5741e93e 100644 --- a/vcs/none/none.go +++ b/vcs/none/none.go @@ -13,7 +13,7 @@ type VcsHandle struct { Age int } -func newNone() (*VcsHandle, error) { +func newNone() (vcs.Vcs, error) { return &VcsHandle{}, nil } diff --git a/vcs/vcs.go b/vcs/vcs.go index e5ded167..c292b705 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -12,7 +12,7 @@ type Vcs interface { } // NewFnSig function signature needed by reg. -type NewFnSig func(int) (Vcs, error) +type NewFnSig func() (Vcs, error) // Item stores one item type Item struct { From ac8105c9162d7fdd49bd8d85912187fac15a734d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 00:00:38 -0400 Subject: [PATCH 16/69] linting --- pkg/box/box.go | 21 +++++++++++++++------ pkg/box/verbs.go | 18 ++++++++++++++++++ vcs/git/git.go | 2 +- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/pkg/box/box.go b/pkg/box/box.go index c22fdd05..2e3b59a5 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -21,8 +21,10 @@ type Box struct { RepoBaseDir string // Base directory of the repo. ConfigDir string // Path to the .blackbox config directory. // - Admins []string // If non-empty, the list of admins. - Files []string // If non-empty, the list of files. + Admins []string // If non-empty, the list of admins. + Files []string // If non-empty, the list of files. + Vcs vcs.Vcs // Handle for VCS access. + VcsName string // name of the VCS } // StatusMode is a type of query. @@ -58,10 +60,17 @@ func NewFromFlags(c *cli.Context) *Box { bx.ConfigDir = configDir // Discover which kind of VCS is in use. - // vcsName, vcsHandle := discoverVCS() - fmt.Printf("VCS len = %v\n", len(vcs.Catalog)) - for i, v := range vcs.Catalog { - fmt.Printf("VCS[%v] = %q %q", i, v.Name, v.Priority) + var h vcs.Vcs + for _, v := range vcs.Catalog { + h, err = v.New() + if err != nil { + return nil + } + if h.Discover() { + bx.Vcs = h + bx.VcsName = v.Name + break + } } return bx diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 74c0422f..b92be538 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -4,10 +4,12 @@ package box import "fmt" +// AdminAdd adds admins. func (bx *Box) AdminAdd([]string) error { return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") } +// AdminList lists the admin id's. func (bx *Box) AdminList() error { admins, err := bx.getAdmins() @@ -21,42 +23,52 @@ func (bx *Box) AdminList() error { return nil } +// AdminRemove removes an id from the admin list. func (bx *Box) AdminRemove([]string) error { return fmt.Errorf("NOT IMPLEMENTED: AdminRemove") } +// Cat outputs a file, unencrypting if needed. func (bx *Box) Cat([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Cat") } +// Decrypt decrypts a file. func (bx *Box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { return fmt.Errorf("NOT IMPLEMENTED: Decrypt") } +// Diff ... func (bx *Box) Diff([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Diff") } +// Edit unencrypts, calls editor, calls encrypt. func (bx *Box) Edit([]string) error { return fmt.Errorf("NOT IMPLEMENTED: Edit") } +// Encrypt encrypts a file. func (bx *Box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { return fmt.Errorf("NOT IMPLEMENTED: Encrypt") } +// FileAdd enrolls files. func (bx *Box) FileAdd(names []string, overwrite bool) error { return fmt.Errorf("NOT IMPLEMENTED: FileAdd") } +// FileList lists the files. func (bx *Box) FileList() error { return fmt.Errorf("NOT IMPLEMENTED: FileList") } +// FileRemove de-enrolls files. func (bx *Box) FileRemove(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: FileRemove") } +// Info prints debugging info. func (bx *Box) Info() error { _, err := bx.getAdmins() @@ -75,22 +87,28 @@ func (bx *Box) Info() error { fmt.Printf("len(bx.Admins)=%v\n", len(bx.Admins)) //fmt.Printf("bx.Files=%q\n", bx.Files) fmt.Printf("len(bx.Files)=%v\n", len(bx.Files)) + fmt.Printf("bx.Vcs=%v\n", bx.Vcs) + fmt.Printf("bx.VcsName=%q\n", bx.VcsName) return nil } +// Init initializes a repo. func (bx *Box) Init() error { return fmt.Errorf("NOT IMPLEMENTED: Init") } +// Reencrypt decrypts and reencrypts files. func (bx *Box) Reencrypt(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") } +// Shred shreds files. func (bx *Box) Shred(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Shred") } +// Status prints the status of files. func (bx *Box) Status(names []string, mode StatusMode, nameOnly bool) error { return fmt.Errorf("NOT IMPLEMENTED: Status") } diff --git a/vcs/git/git.go b/vcs/git/git.go index 1307d23b..9c3f6923 100644 --- a/vcs/git/git.go +++ b/vcs/git/git.go @@ -18,5 +18,5 @@ func newGit() (vcs.Vcs, error) { // Discover returns false. func (v VcsHandle) Discover() bool { - return false + return true } From 4979113a3c1d78bff0397df6f114011128c118f6 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 00:26:41 -0400 Subject: [PATCH 17/69] Implement Discover() --- models/vcs.go | 2 +- pkg/bbutil/admin.go | 22 ---------------------- pkg/bbutil/decrypt.go | 33 --------------------------------- pkg/box/box.go | 17 +++-------------- vcs/git/git.go | 14 +++++++++++--- vcs/none/none.go | 6 +++--- vcs/vcs.go | 2 +- 7 files changed, 19 insertions(+), 77 deletions(-) delete mode 100644 pkg/bbutil/admin.go delete mode 100644 pkg/bbutil/decrypt.go diff --git a/models/vcs.go b/models/vcs.go index e2cafbfe..c8018d40 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -2,5 +2,5 @@ package models // Vcs is git/hg/etc. type Vcs interface { - Discover() bool + Discover(repobasedir string) bool } diff --git a/pkg/bbutil/admin.go b/pkg/bbutil/admin.go deleted file mode 100644 index 36ea1179..00000000 --- a/pkg/bbutil/admin.go +++ /dev/null @@ -1,22 +0,0 @@ -package bbutil - -// -// // Administrator is a description of the admininstrators. -// type Administrator struct { -// Name string -// } -// -// // Administrators returns the administrators of this repo. -// func (bbu *RepoInfo) Administrators() ([]Administrator, error) { -// return plainListAdmins(bbu.BlackboxConfigDir) -// } -// -// // AddAdmin adds an administrator to this repo. -// func (bbu *RepoInfo) AddAdmin(admin Administrator) error { -// return plainAddAdmin(bbu.BlackboxConfigDir, admin) -// } -// -// // RemoveAdminByName removes an administrator from this repo. -// func (bbu *RepoInfo) RemoveAdminByName(admin Administrator) error { -// return plainRemoveAdmin(bbu.BlackboxConfigDir, admin) -// } diff --git a/pkg/bbutil/decrypt.go b/pkg/bbutil/decrypt.go deleted file mode 100644 index 434357ff..00000000 --- a/pkg/bbutil/decrypt.go +++ /dev/null @@ -1,33 +0,0 @@ -package bbutil - -// -// import ( -// "fmt" -// "os" -// ) -// -// // DecryptFile decrypts a single file. -// func (bbu *RepoInfo) DecryptFile(filename, group string, overwrite bool) error { -// -// // change_to_vcs_root -// -// fmt.Fprintf(os.Stderr, "WOULD DECRYPT: %v %q %q\n", overwrite, group, filename) -// -// // export PATH=/usr/bin:/bin:"$PATH" -// -// // # Decrypt: -// // echo '========== Decrypting new/changed files: START' -// // while IFS= read <&99 -r unencrypted_file; do -// // encrypted_file=$(get_encrypted_filename "$unencrypted_file") -// // decrypt_file_overwrite "$encrypted_file" "$unencrypted_file" -// // cp_permissions "$encrypted_file" "$unencrypted_file" -// // if [[ ! -z "$FILE_GROUP" ]]; then -// // chmod g+r "$unencrypted_file" -// // chgrp "$FILE_GROUP" "$unencrypted_file" -// // fi -// // done 99<"$BB_FILES" -// -// // echo '========== Decrypting new/changed files: DONE' -// -// return nil -// } diff --git a/pkg/box/box.go b/pkg/box/box.go index 2e3b59a5..5fd8f60b 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -10,7 +10,7 @@ import ( "path/filepath" "strings" - // "github.com/StackExchange/blackbox/vcs" + "github.com/StackExchange/blackbox/pkg/bbutil" "github.com/StackExchange/blackbox/vcs" "github.com/urfave/cli/v2" ) @@ -66,7 +66,7 @@ func NewFromFlags(c *cli.Context) *Box { if err != nil { return nil } - if h.Discover() { + if h.Discover(bx.RepoBaseDir) { bx.Vcs = h bx.VcsName = v.Name break @@ -76,17 +76,6 @@ func NewFromFlags(c *cli.Context) *Box { return bx } -func dirExists(path string) (bool, error) { - stat, err := os.Stat(path) - if err == nil { - return stat.IsDir(), nil - } - if os.IsNotExist(err) { - return false, nil - } - return true, err -} - func findBaseAndConfigDir() (repodir, configdir string, err error) { // If BLACKBOXDATA/BLACKBOX_CONFIGDIR is set, that is the config dir. @@ -125,7 +114,7 @@ func findBaseAndConfigDir() (repodir, configdir string, err error) { // Does relpath contain any of our directory names? for _, c := range candidates { t := filepath.Join(relpath, c) - d, err := dirExists(t) + d, err := bbutil.DirExists(t) if err != nil { return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) } diff --git a/vcs/git/git.go b/vcs/git/git.go index 9c3f6923..60389988 100644 --- a/vcs/git/git.go +++ b/vcs/git/git.go @@ -1,11 +1,14 @@ package git import ( + "path/filepath" + + "github.com/StackExchange/blackbox/pkg/bbutil" "github.com/StackExchange/blackbox/vcs" ) func init() { - vcs.Register("GIT", 1, newGit) + vcs.Register("GIT", 100, newGit) } // VcsHandle is the handle @@ -17,6 +20,11 @@ func newGit() (vcs.Vcs, error) { } // Discover returns false. -func (v VcsHandle) Discover() bool { - return true +func (v VcsHandle) Discover(repobasedir string) bool { + n := filepath.Join(repobasedir, ".git") + found, err := bbutil.DirExists(n) + if err != nil { + return false + } + return found } diff --git a/vcs/none/none.go b/vcs/none/none.go index 5741e93e..faa115f4 100644 --- a/vcs/none/none.go +++ b/vcs/none/none.go @@ -17,7 +17,7 @@ func newNone() (vcs.Vcs, error) { return &VcsHandle{}, nil } -// Discover returns false -func (v VcsHandle) Discover() bool { - return false +// Discover returns true +func (v VcsHandle) Discover(repobasedir string) bool { + return true } diff --git a/vcs/vcs.go b/vcs/vcs.go index c292b705..960ce722 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -34,5 +34,5 @@ func Register(name string, priority int, newfn NewFnSig) { Catalog = append(Catalog, item) // Keep the list sorted. - sort.Slice(Catalog, func(i, j int) bool { return Catalog[i].Priority < Catalog[j].Priority }) + sort.Slice(Catalog, func(i, j int) bool { return Catalog[j].Priority < Catalog[i].Priority }) } From bc5dab761dd516e752f178b831c268c59460c944 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 00:30:59 -0400 Subject: [PATCH 18/69] Implement: files list --- pkg/bbutil/filestats.go | 14 ++++++++++++++ pkg/box/verbs.go | 9 ++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 pkg/bbutil/filestats.go diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go new file mode 100644 index 00000000..1ae832d6 --- /dev/null +++ b/pkg/bbutil/filestats.go @@ -0,0 +1,14 @@ +package bbutil + +import "os" + +func DirExists(path string) (bool, error) { + stat, err := os.Stat(path) + if err == nil { + return stat.IsDir(), nil + } + if os.IsNotExist(err) { + return false, nil + } + return true, err +} diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index b92be538..df0989d4 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -60,7 +60,14 @@ func (bx *Box) FileAdd(names []string, overwrite bool) error { // FileList lists the files. func (bx *Box) FileList() error { - return fmt.Errorf("NOT IMPLEMENTED: FileList") + files, err := bx.getFiles() + if err != nil { + return err + } + for _, v := range files { + fmt.Println(v) + } + return nil } // FileRemove de-enrolls files. From 2425c0fa49b66be9eb9c7ebd9e2be48bc03b01f6 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 14:58:50 -0400 Subject: [PATCH 19/69] "blackbox status" works. --- .gitignore | 3 - cmd/blackbox/flags.go | 4 +- cmd/blackbox/parse.go | 82 ++-------------------- go.mod | 6 +- go.sum | 6 ++ pkg/bbutil/filestats.go | 11 +++ pkg/bbutil/reg.go | 21 ------ pkg/bbutil/runbash.go | 26 +++++++ pkg/box/verbs.go | 146 +++++++++++++++++++++++++++++++++++++++- 9 files changed, 197 insertions(+), 108 deletions(-) create mode 100644 pkg/bbutil/runbash.go diff --git a/.gitignore b/.gitignore index f2d9497a..03b21364 100644 --- a/.gitignore +++ b/.gitignore @@ -5,9 +5,6 @@ __pycache__/ # C extensions *.so -# backup shell files -*~ - # Distribution / packaging .Python env/ diff --git a/cmd/blackbox/flags.go b/cmd/blackbox/flags.go index dbe139b1..d2a5f6c7 100644 --- a/cmd/blackbox/flags.go +++ b/cmd/blackbox/flags.go @@ -146,9 +146,9 @@ func flags() *cli.App { Flags: []cli.Flag{ &cli.BoolFlag{Name: "name-only", Usage: "Show only names of the files"}, &cli.BoolFlag{Name: "all", Usage: "All registered files"}, - &cli.BoolFlag{Name: "changed", Usage: "Only files newer than their .gpg"}, - &cli.BoolFlag{Name: "unchanged", Usage: "Only files older than their .gpg"}, + &cli.StringFlag{Name: "type", Usage: "only list if status matching this string"}, }, + Action: func(c *cli.Context) error { return cmdStatus(c) }, }, { diff --git a/cmd/blackbox/parse.go b/cmd/blackbox/parse.go index 6b660cd2..fbb6eaea 100644 --- a/cmd/blackbox/parse.go +++ b/cmd/blackbox/parse.go @@ -27,7 +27,7 @@ func allOrSomeFiles(c *cli.Context) error { } } else { if !c.Args().Present() { - return fmt.Errorf("Must specify at least one file name") + return fmt.Errorf("Must specify at least one file name or --all") } } return nil @@ -169,88 +169,14 @@ func cmdShred(c *cli.Context) error { } func cmdStatus(c *cli.Context) error { - if err := allOrSomeFiles(c); err != nil { - return err - } - - mode := box.Itemized - - if c.Bool("all") { - if c.Args().Present() { - return fmt.Errorf("Can not specify filenames and --all") - } - if mode != box.Itemized { - return fmt.Errorf("--all can not be mixed with other flags") - } - mode = box.All - } - - if c.Bool("changed") { - if c.Args().Present() { - return fmt.Errorf("Can not specify filenames and --changed") - } - if mode != box.Itemized { - return fmt.Errorf("--changed can not be mixed with other flags") - } - mode = box.Changed - } - if c.Bool("unchanged") { - if c.Args().Present() { - return fmt.Errorf("Can not specify filenames and --unchanged") - } - if mode != box.Itemized { - return fmt.Errorf("--unchanged can not be mixed with other flags") - } - mode = box.Unchanged + if c.Bool("all") && c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --all") } - bx := box.NewFromFlags(c) - return bx.Status(c.Args().Slice(), mode, c.Bool("name-only")) + return bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type")) } -//func cmdInfo(c *cli.Context) error { -// -// // GPG version -// // VCS name -// // keys directory -// -// bbu, err := bbutil.New() -// if err != nil { -// return err -// } -// -// fmt.Print("VCS:\n") -// fmt.Printf("\tName: %q\n", bbu.Vcs.Name()) -// fmt.Printf("\tRepoBaseDir: %q\n", bbu.Vcs.RepoBaseDir()) -// fmt.Print("REPO:\n") -// fmt.Printf("\tRepoBaseDir: %q\n", bbu.RepoBaseDir) -// fmt.Printf("\tBlackboxConfigDir: %q\n", bbu.BlackboxConfigDir) -// -// return nil -//} - -//func cmdAdminList(c *cli.Context) error { -// -// if len(c.Args()) != 0 { -// fmt.Fprintln(c.App.Writer, "ERROR: 'blackbox admin list' does not take any arguments") -// return nil -// } -// -// bbu, err := bbutil.New() -// if err != nil { -// return err -// } -// names, err := bbu.Administrators() -// if err != nil { -// return err -// } -// for _, item := range names { -// fmt.Println(item.Name) -// } -// return nil -//} - // func cmdDecrypt(allFiles bool, filenames []string, group string) error { // bbu, err := bbutil.New() // if err != nil { diff --git a/go.mod b/go.mod index 0fec8471..06587bbf 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,8 @@ module github.com/StackExchange/blackbox go 1.14 -require github.com/urfave/cli/v2 v2.2.0 +require ( + github.com/mattn/go-runewidth v0.0.9 // indirect + github.com/olekukonko/tablewriter v0.0.4 + github.com/urfave/cli/v2 v2.2.0 +) diff --git a/go.sum b/go.sum index 033734fb..460d92fb 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,12 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= +github.com/olekukonko/tablewriter v0.0.4 h1:vHD/YYe1Wolo78koG299f7V/VAS08c6IpCLn+Ejf/w8= +github.com/olekukonko/tablewriter v0.0.4/go.mod h1:zq6QwlOf5SlnkVbMSr5EoBv3636FWnp+qbPhuoO21uA= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 1ae832d6..f36f908f 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -12,3 +12,14 @@ func DirExists(path string) (bool, error) { } return true, err } + +//func FileExists(path string) (bool, error) { +// _, err := os.Stat(path) +// if err == nil { +// return true, nil +// } +// if os.IsNotExist(err) { +// return false, nil +// } +// return false, err +//} diff --git a/pkg/bbutil/reg.go b/pkg/bbutil/reg.go index c30a53d0..d5b88793 100644 --- a/pkg/bbutil/reg.go +++ b/pkg/bbutil/reg.go @@ -39,24 +39,3 @@ package bbutil // // return r, nil // } -// -// // FileStatus returns the status of a file. -// func FileStatus(basedir, file string) string { -// p := filepath.Join(basedir, file) -// e := p + ".gpg" -// ps, perr := os.Stat(p) -// es, eerr := os.Stat(e) -// if perr == nil && eerr == nil { -// if ps.ModTime().Before(es.ModTime()) { -// return "GPGNEWER" -// } -// return "EDITING" -// } -// if eerr == nil { -// return "ENCRYPTED" -// } -// if os.IsExist(perr) { -// return "ERROR_NOGPG" -// } -// return "ERROR_NOEXIST" -// } diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go new file mode 100644 index 00000000..fcacd69c --- /dev/null +++ b/pkg/bbutil/runbash.go @@ -0,0 +1,26 @@ +package bbutil + +import ( + "fmt" + "log" + "os" + "os/exec" +) + +// RunBash runs a Bash command. +func RunBash(command string, args ...string) error { + //if dryRun { + // fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) + // return nil + //} + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Start() + if err != nil { + log.Fatal(err) + } + err = cmd.Wait() + return fmt.Errorf("run_bash: %w", err) +} diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index df0989d4..c0488611 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -2,7 +2,13 @@ package box // This file implements the business logic related to a black box. -import "fmt" +import ( + "fmt" + "os" + "strings" + + "github.com/olekukonko/tablewriter" +) // AdminAdd adds admins. func (bx *Box) AdminAdd([]string) error { @@ -115,7 +121,141 @@ func (bx *Box) Shred(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Shred") } +// func isChanged(pname string) (bool, error) { +// // if .gpg exists but not plainfile: unchanged +// // if plaintext exists but not .gpg: changed +// // if plainfile < .gpg: unchanged +// // if plainfile > .gpg: don't know, need to try diff + +// // Gather info about the files: + +// pstat, perr := os.Stat(pname) +// if perr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr) +// } +// gname := pname + ".gpg" +// gstat, gerr := os.Stat(gname) +// if gerr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr) +// } + +// pexists := perr == nil +// gexists := gerr == nil + +// // Use the above rules: + +// // if .gpg exists but not plainfile: unchanged +// if gexists && !pexists { +// return false, nil +// } + +// // if plaintext exists but not .gpg: changed +// if pexists && !gexists { +// return true, nil +// } + +// // At this point we can conclude that both p and g exist. +// // Can't hurt to test that assertion. +// if (!pexists) && (!gexists) { +// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname) +// } + +// pmodtime := pstat.ModTime() +// gmodtime := gstat.ModTime() +// // if plainfile < .gpg: unchanged +// if pmodtime.Before(gmodtime) { +// return false, nil +// } +// // if plainfile > .gpg: don't know, need to try diff +// return false, fmt.Errorf("Can not know for sure. Try git diff?") +// } + +// FileStatus returns the status of a file. +func FileStatus(name string) (string, error) { + /* + DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). + ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. + SHREDDED: Plaintext is missing. + GPGMISSING: The .gpg file is missing. Oops? + PLAINERROR: Can't access the plaintext file to determine status. + GPGERROR: Can't access .gpg file to determine status. + */ + + p := name + e := p + ".gpg" + ps, perr := os.Stat(p) + es, eerr := os.Stat(e) + if perr == nil && eerr == nil { + if ps.ModTime().Before(es.ModTime()) { + return "ENCRYPTED", nil + } + return "DECRYPTED", nil + } + + if eerr != nil { + if os.IsNotExist(eerr) { + return "GPGMISSING", nil + } + return "GPGERROR", eerr + } + + if perr != nil { + if os.IsNotExist(perr) { + return "SHREDDED", nil + } + } + return "PLAINERROR", perr +} + // Status prints the status of files. -func (bx *Box) Status(names []string, mode StatusMode, nameOnly bool) error { - return fmt.Errorf("NOT IMPLEMENTED: Status") +func (bx *Box) Status(names []string, nameOnly bool, match string) error { + + _, err := bx.getFiles() + if err != nil { + return err + } + + var flist []string + if len(names) == 0 { + flist = bx.Files + } else { + flist = names + } + + var data [][]string + var onlylist []string + thirdColumn := false + var tcData bool + + for _, name := range flist { + stat, err := FileStatus(name) + if (match == "") || (stat == match) { + if err == nil { + data = append(data, []string{stat, name}) + onlylist = append(onlylist, name) + } else { + thirdColumn = tcData + data = append(data, []string{stat, name, fmt.Sprintf("%v", err)}) + onlylist = append(onlylist, fmt.Sprintf("%v: %v", name, err)) + } + } + } + + if nameOnly { + fmt.Println(strings.Join(onlylist, "\n")) + } else { + table := tablewriter.NewWriter(os.Stdout) + table.SetAutoWrapText(false) + if thirdColumn { + table.SetHeader([]string{"Status", "Name", "Error"}) + } else { + table.SetHeader([]string{"Status", "Name"}) + } + for _, v := range data { + table.Append(v) + } + table.Render() // Send output + } + + return nil } From bf1f1e243931a37aafb52518b4082b37020d05e7 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 15:00:24 -0400 Subject: [PATCH 20/69] Unvendor --- .../cpuguy83/go-md2man/v2/LICENSE.md | 21 + .../cpuguy83/go-md2man/v2/md2man/md2man.go | 14 + .../cpuguy83/go-md2man/v2/md2man/roff.go | 345 ++++ .../github.com/mattn/go-runewidth/.travis.yml | 16 + vendor/github.com/mattn/go-runewidth/LICENSE | 21 + .../github.com/mattn/go-runewidth/README.md | 27 + vendor/github.com/mattn/go-runewidth/go.mod | 3 + .../github.com/mattn/go-runewidth/go.test.sh | 12 + .../mattn/go-runewidth/runewidth.go | 257 +++ .../mattn/go-runewidth/runewidth_appengine.go | 8 + .../mattn/go-runewidth/runewidth_js.go | 9 + .../mattn/go-runewidth/runewidth_posix.go | 82 + .../mattn/go-runewidth/runewidth_table.go | 437 +++++ .../mattn/go-runewidth/runewidth_windows.go | 28 + .../olekukonko/tablewriter/.gitignore | 15 + .../olekukonko/tablewriter/.travis.yml | 14 + .../olekukonko/tablewriter/LICENSE.md | 19 + .../olekukonko/tablewriter/README.md | 396 ++++ .../github.com/olekukonko/tablewriter/csv.go | 52 + .../github.com/olekukonko/tablewriter/go.mod | 5 + .../github.com/olekukonko/tablewriter/go.sum | 2 + .../olekukonko/tablewriter/table.go | 941 ++++++++++ .../tablewriter/table_with_color.go | 136 ++ .../github.com/olekukonko/tablewriter/util.go | 93 + .../github.com/olekukonko/tablewriter/wrap.go | 99 + .../russross/blackfriday/v2/.gitignore | 8 + .../russross/blackfriday/v2/.travis.yml | 17 + .../russross/blackfriday/v2/LICENSE.txt | 29 + .../russross/blackfriday/v2/README.md | 291 +++ .../russross/blackfriday/v2/block.go | 1590 +++++++++++++++++ .../github.com/russross/blackfriday/v2/doc.go | 18 + .../github.com/russross/blackfriday/v2/esc.go | 34 + .../github.com/russross/blackfriday/v2/go.mod | 1 + .../russross/blackfriday/v2/html.go | 949 ++++++++++ .../russross/blackfriday/v2/inline.go | 1228 +++++++++++++ .../russross/blackfriday/v2/markdown.go | 950 ++++++++++ .../russross/blackfriday/v2/node.go | 354 ++++ .../russross/blackfriday/v2/smartypants.go | 457 +++++ .../sanitized_anchor_name/.travis.yml | 16 + .../shurcooL/sanitized_anchor_name/LICENSE | 21 + .../shurcooL/sanitized_anchor_name/README.md | 36 + .../shurcooL/sanitized_anchor_name/go.mod | 1 + .../shurcooL/sanitized_anchor_name/main.go | 29 + vendor/github.com/urfave/cli/v2/.flake8 | 2 + vendor/github.com/urfave/cli/v2/.gitignore | 7 + .../urfave/cli/v2/CODE_OF_CONDUCT.md | 74 + vendor/github.com/urfave/cli/v2/LICENSE | 21 + vendor/github.com/urfave/cli/v2/README.md | 66 + vendor/github.com/urfave/cli/v2/app.go | 542 ++++++ vendor/github.com/urfave/cli/v2/args.go | 54 + vendor/github.com/urfave/cli/v2/category.go | 79 + vendor/github.com/urfave/cli/v2/cli.go | 23 + vendor/github.com/urfave/cli/v2/command.go | 301 ++++ vendor/github.com/urfave/cli/v2/context.go | 273 +++ vendor/github.com/urfave/cli/v2/docs.go | 148 ++ vendor/github.com/urfave/cli/v2/errors.go | 131 ++ vendor/github.com/urfave/cli/v2/fish.go | 192 ++ vendor/github.com/urfave/cli/v2/flag.go | 388 ++++ vendor/github.com/urfave/cli/v2/flag_bool.go | 106 ++ .../github.com/urfave/cli/v2/flag_duration.go | 105 ++ .../github.com/urfave/cli/v2/flag_float64.go | 106 ++ .../urfave/cli/v2/flag_float64_slice.go | 163 ++ .../github.com/urfave/cli/v2/flag_generic.go | 108 ++ vendor/github.com/urfave/cli/v2/flag_int.go | 106 ++ vendor/github.com/urfave/cli/v2/flag_int64.go | 105 ++ .../urfave/cli/v2/flag_int64_slice.go | 159 ++ .../urfave/cli/v2/flag_int_slice.go | 173 ++ vendor/github.com/urfave/cli/v2/flag_path.go | 95 + .../github.com/urfave/cli/v2/flag_string.go | 95 + .../urfave/cli/v2/flag_string_slice.go | 171 ++ .../urfave/cli/v2/flag_timestamp.go | 152 ++ vendor/github.com/urfave/cli/v2/flag_uint.go | 105 ++ .../github.com/urfave/cli/v2/flag_uint64.go | 105 ++ vendor/github.com/urfave/cli/v2/funcs.go | 44 + vendor/github.com/urfave/cli/v2/go.mod | 9 + vendor/github.com/urfave/cli/v2/go.sum | 14 + vendor/github.com/urfave/cli/v2/help.go | 368 ++++ vendor/github.com/urfave/cli/v2/parse.go | 94 + vendor/github.com/urfave/cli/v2/sort.go | 29 + vendor/github.com/urfave/cli/v2/template.go | 120 ++ vendor/modules.txt | 15 + 81 files changed, 13929 insertions(+) create mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md create mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go create mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go create mode 100644 vendor/github.com/mattn/go-runewidth/.travis.yml create mode 100644 vendor/github.com/mattn/go-runewidth/LICENSE create mode 100644 vendor/github.com/mattn/go-runewidth/README.md create mode 100644 vendor/github.com/mattn/go-runewidth/go.mod create mode 100644 vendor/github.com/mattn/go-runewidth/go.test.sh create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_appengine.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_js.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_posix.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_table.go create mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_windows.go create mode 100644 vendor/github.com/olekukonko/tablewriter/.gitignore create mode 100644 vendor/github.com/olekukonko/tablewriter/.travis.yml create mode 100644 vendor/github.com/olekukonko/tablewriter/LICENSE.md create mode 100644 vendor/github.com/olekukonko/tablewriter/README.md create mode 100644 vendor/github.com/olekukonko/tablewriter/csv.go create mode 100644 vendor/github.com/olekukonko/tablewriter/go.mod create mode 100644 vendor/github.com/olekukonko/tablewriter/go.sum create mode 100644 vendor/github.com/olekukonko/tablewriter/table.go create mode 100644 vendor/github.com/olekukonko/tablewriter/table_with_color.go create mode 100644 vendor/github.com/olekukonko/tablewriter/util.go create mode 100644 vendor/github.com/olekukonko/tablewriter/wrap.go create mode 100644 vendor/github.com/russross/blackfriday/v2/.gitignore create mode 100644 vendor/github.com/russross/blackfriday/v2/.travis.yml create mode 100644 vendor/github.com/russross/blackfriday/v2/LICENSE.txt create mode 100644 vendor/github.com/russross/blackfriday/v2/README.md create mode 100644 vendor/github.com/russross/blackfriday/v2/block.go create mode 100644 vendor/github.com/russross/blackfriday/v2/doc.go create mode 100644 vendor/github.com/russross/blackfriday/v2/esc.go create mode 100644 vendor/github.com/russross/blackfriday/v2/go.mod create mode 100644 vendor/github.com/russross/blackfriday/v2/html.go create mode 100644 vendor/github.com/russross/blackfriday/v2/inline.go create mode 100644 vendor/github.com/russross/blackfriday/v2/markdown.go create mode 100644 vendor/github.com/russross/blackfriday/v2/node.go create mode 100644 vendor/github.com/russross/blackfriday/v2/smartypants.go create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/README.md create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/go.mod create mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/main.go create mode 100644 vendor/github.com/urfave/cli/v2/.flake8 create mode 100644 vendor/github.com/urfave/cli/v2/.gitignore create mode 100644 vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md create mode 100644 vendor/github.com/urfave/cli/v2/LICENSE create mode 100644 vendor/github.com/urfave/cli/v2/README.md create mode 100644 vendor/github.com/urfave/cli/v2/app.go create mode 100644 vendor/github.com/urfave/cli/v2/args.go create mode 100644 vendor/github.com/urfave/cli/v2/category.go create mode 100644 vendor/github.com/urfave/cli/v2/cli.go create mode 100644 vendor/github.com/urfave/cli/v2/command.go create mode 100644 vendor/github.com/urfave/cli/v2/context.go create mode 100644 vendor/github.com/urfave/cli/v2/docs.go create mode 100644 vendor/github.com/urfave/cli/v2/errors.go create mode 100644 vendor/github.com/urfave/cli/v2/fish.go create mode 100644 vendor/github.com/urfave/cli/v2/flag.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_bool.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_duration.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_float64.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_float64_slice.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_generic.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_int.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_int64.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_int64_slice.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_int_slice.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_path.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_string.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_string_slice.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_timestamp.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_uint.go create mode 100644 vendor/github.com/urfave/cli/v2/flag_uint64.go create mode 100644 vendor/github.com/urfave/cli/v2/funcs.go create mode 100644 vendor/github.com/urfave/cli/v2/go.mod create mode 100644 vendor/github.com/urfave/cli/v2/go.sum create mode 100644 vendor/github.com/urfave/cli/v2/help.go create mode 100644 vendor/github.com/urfave/cli/v2/parse.go create mode 100644 vendor/github.com/urfave/cli/v2/sort.go create mode 100644 vendor/github.com/urfave/cli/v2/template.go create mode 100644 vendor/modules.txt diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md b/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md new file mode 100644 index 00000000..1cade6ce --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Brian Goff + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go new file mode 100644 index 00000000..b4800567 --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go @@ -0,0 +1,14 @@ +package md2man + +import ( + "github.com/russross/blackfriday/v2" +) + +// Render converts a markdown document into a roff formatted document. +func Render(doc []byte) []byte { + renderer := NewRoffRenderer() + + return blackfriday.Run(doc, + []blackfriday.Option{blackfriday.WithRenderer(renderer), + blackfriday.WithExtensions(renderer.GetExtensions())}...) +} diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go new file mode 100644 index 00000000..0668a66c --- /dev/null +++ b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go @@ -0,0 +1,345 @@ +package md2man + +import ( + "fmt" + "io" + "os" + "strings" + + "github.com/russross/blackfriday/v2" +) + +// roffRenderer implements the blackfriday.Renderer interface for creating +// roff format (manpages) from markdown text +type roffRenderer struct { + extensions blackfriday.Extensions + listCounters []int + firstHeader bool + defineTerm bool + listDepth int +} + +const ( + titleHeader = ".TH " + topLevelHeader = "\n\n.SH " + secondLevelHdr = "\n.SH " + otherHeader = "\n.SS " + crTag = "\n" + emphTag = "\\fI" + emphCloseTag = "\\fP" + strongTag = "\\fB" + strongCloseTag = "\\fP" + breakTag = "\n.br\n" + paraTag = "\n.PP\n" + hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" + linkTag = "\n\\[la]" + linkCloseTag = "\\[ra]" + codespanTag = "\\fB\\fC" + codespanCloseTag = "\\fR" + codeTag = "\n.PP\n.RS\n\n.nf\n" + codeCloseTag = "\n.fi\n.RE\n" + quoteTag = "\n.PP\n.RS\n" + quoteCloseTag = "\n.RE\n" + listTag = "\n.RS\n" + listCloseTag = "\n.RE\n" + arglistTag = "\n.TP\n" + tableStart = "\n.TS\nallbox;\n" + tableEnd = ".TE\n" + tableCellStart = "T{\n" + tableCellEnd = "\nT}\n" +) + +// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents +// from markdown +func NewRoffRenderer() *roffRenderer { // nolint: golint + var extensions blackfriday.Extensions + + extensions |= blackfriday.NoIntraEmphasis + extensions |= blackfriday.Tables + extensions |= blackfriday.FencedCode + extensions |= blackfriday.SpaceHeadings + extensions |= blackfriday.Footnotes + extensions |= blackfriday.Titleblock + extensions |= blackfriday.DefinitionLists + return &roffRenderer{ + extensions: extensions, + } +} + +// GetExtensions returns the list of extensions used by this renderer implementation +func (r *roffRenderer) GetExtensions() blackfriday.Extensions { + return r.extensions +} + +// RenderHeader handles outputting the header at document start +func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { + // disable hyphenation + out(w, ".nh\n") +} + +// RenderFooter handles outputting the footer at the document end; the roff +// renderer has no footer information +func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { +} + +// RenderNode is called for each node in a markdown document; based on the node +// type the equivalent roff output is sent to the writer +func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + + var walkAction = blackfriday.GoToNext + + switch node.Type { + case blackfriday.Text: + r.handleText(w, node, entering) + case blackfriday.Softbreak: + out(w, crTag) + case blackfriday.Hardbreak: + out(w, breakTag) + case blackfriday.Emph: + if entering { + out(w, emphTag) + } else { + out(w, emphCloseTag) + } + case blackfriday.Strong: + if entering { + out(w, strongTag) + } else { + out(w, strongCloseTag) + } + case blackfriday.Link: + if !entering { + out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag) + } + case blackfriday.Image: + // ignore images + walkAction = blackfriday.SkipChildren + case blackfriday.Code: + out(w, codespanTag) + escapeSpecialChars(w, node.Literal) + out(w, codespanCloseTag) + case blackfriday.Document: + break + case blackfriday.Paragraph: + // roff .PP markers break lists + if r.listDepth > 0 { + return blackfriday.GoToNext + } + if entering { + out(w, paraTag) + } else { + out(w, crTag) + } + case blackfriday.BlockQuote: + if entering { + out(w, quoteTag) + } else { + out(w, quoteCloseTag) + } + case blackfriday.Heading: + r.handleHeading(w, node, entering) + case blackfriday.HorizontalRule: + out(w, hruleTag) + case blackfriday.List: + r.handleList(w, node, entering) + case blackfriday.Item: + r.handleItem(w, node, entering) + case blackfriday.CodeBlock: + out(w, codeTag) + escapeSpecialChars(w, node.Literal) + out(w, codeCloseTag) + case blackfriday.Table: + r.handleTable(w, node, entering) + case blackfriday.TableCell: + r.handleTableCell(w, node, entering) + case blackfriday.TableHead: + case blackfriday.TableBody: + case blackfriday.TableRow: + // no action as cell entries do all the nroff formatting + return blackfriday.GoToNext + default: + fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) + } + return walkAction +} + +func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) { + var ( + start, end string + ) + // handle special roff table cell text encapsulation + if node.Parent.Type == blackfriday.TableCell { + if len(node.Literal) > 30 { + start = tableCellStart + end = tableCellEnd + } else { + // end rows that aren't terminated by "tableCellEnd" with a cr if end of row + if node.Parent.Next == nil && !node.Parent.IsHeader { + end = crTag + } + } + } + out(w, start) + escapeSpecialChars(w, node.Literal) + out(w, end) +} + +func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) { + if entering { + switch node.Level { + case 1: + if !r.firstHeader { + out(w, titleHeader) + r.firstHeader = true + break + } + out(w, topLevelHeader) + case 2: + out(w, secondLevelHdr) + default: + out(w, otherHeader) + } + } +} + +func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) { + openTag := listTag + closeTag := listCloseTag + if node.ListFlags&blackfriday.ListTypeDefinition != 0 { + // tags for definition lists handled within Item node + openTag = "" + closeTag = "" + } + if entering { + r.listDepth++ + if node.ListFlags&blackfriday.ListTypeOrdered != 0 { + r.listCounters = append(r.listCounters, 1) + } + out(w, openTag) + } else { + if node.ListFlags&blackfriday.ListTypeOrdered != 0 { + r.listCounters = r.listCounters[:len(r.listCounters)-1] + } + out(w, closeTag) + r.listDepth-- + } +} + +func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) { + if entering { + if node.ListFlags&blackfriday.ListTypeOrdered != 0 { + out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1])) + r.listCounters[len(r.listCounters)-1]++ + } else if node.ListFlags&blackfriday.ListTypeDefinition != 0 { + // state machine for handling terms and following definitions + // since blackfriday does not distinguish them properly, nor + // does it seperate them into separate lists as it should + if !r.defineTerm { + out(w, arglistTag) + r.defineTerm = true + } else { + r.defineTerm = false + } + } else { + out(w, ".IP \\(bu 2\n") + } + } else { + out(w, "\n") + } +} + +func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) { + if entering { + out(w, tableStart) + //call walker to count cells (and rows?) so format section can be produced + columns := countColumns(node) + out(w, strings.Repeat("l ", columns)+"\n") + out(w, strings.Repeat("l ", columns)+".\n") + } else { + out(w, tableEnd) + } +} + +func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) { + var ( + start, end string + ) + if node.IsHeader { + start = codespanTag + end = codespanCloseTag + } + if entering { + if node.Prev != nil && node.Prev.Type == blackfriday.TableCell { + out(w, "\t"+start) + } else { + out(w, start) + } + } else { + // need to carriage return if we are at the end of the header row + if node.IsHeader && node.Next == nil { + end = end + crTag + } + out(w, end) + } +} + +// because roff format requires knowing the column count before outputting any table +// data we need to walk a table tree and count the columns +func countColumns(node *blackfriday.Node) int { + var columns int + + node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { + switch node.Type { + case blackfriday.TableRow: + if !entering { + return blackfriday.Terminate + } + case blackfriday.TableCell: + if entering { + columns++ + } + default: + } + return blackfriday.GoToNext + }) + return columns +} + +func out(w io.Writer, output string) { + io.WriteString(w, output) // nolint: errcheck +} + +func needsBackslash(c byte) bool { + for _, r := range []byte("-_&\\~") { + if c == r { + return true + } + } + return false +} + +func escapeSpecialChars(w io.Writer, text []byte) { + for i := 0; i < len(text); i++ { + // escape initial apostrophe or period + if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { + out(w, "\\&") + } + + // directly copy normal characters + org := i + + for i < len(text) && !needsBackslash(text[i]) { + i++ + } + if i > org { + w.Write(text[org:i]) // nolint: errcheck + } + + // escape a character + if i >= len(text) { + break + } + + w.Write([]byte{'\\', text[i]}) // nolint: errcheck + } +} diff --git a/vendor/github.com/mattn/go-runewidth/.travis.yml b/vendor/github.com/mattn/go-runewidth/.travis.yml new file mode 100644 index 00000000..6a21813a --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/.travis.yml @@ -0,0 +1,16 @@ +language: go +sudo: false +go: + - 1.13.x + - tip + +before_install: + - go get -t -v ./... + +script: + - go generate + - git diff --cached --exit-code + - ./go.test.sh + +after_success: + - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/mattn/go-runewidth/LICENSE b/vendor/github.com/mattn/go-runewidth/LICENSE new file mode 100644 index 00000000..91b5cef3 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Yasuhiro Matsumoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/mattn/go-runewidth/README.md b/vendor/github.com/mattn/go-runewidth/README.md new file mode 100644 index 00000000..aa56ab96 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/README.md @@ -0,0 +1,27 @@ +go-runewidth +============ + +[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth) +[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth) +[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth) +[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth) + +Provides functions to get fixed width of the character or string. + +Usage +----- + +```go +runewidth.StringWidth("つのだ☆HIRO") == 12 +``` + + +Author +------ + +Yasuhiro Matsumoto + +License +------- + +under the MIT License: http://mattn.mit-license.org/2013 diff --git a/vendor/github.com/mattn/go-runewidth/go.mod b/vendor/github.com/mattn/go-runewidth/go.mod new file mode 100644 index 00000000..fa7f4d86 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/go.mod @@ -0,0 +1,3 @@ +module github.com/mattn/go-runewidth + +go 1.9 diff --git a/vendor/github.com/mattn/go-runewidth/go.test.sh b/vendor/github.com/mattn/go-runewidth/go.test.sh new file mode 100644 index 00000000..012162b0 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/go.test.sh @@ -0,0 +1,12 @@ +#!/usr/bin/env bash + +set -e +echo "" > coverage.txt + +for d in $(go list ./... | grep -v vendor); do + go test -race -coverprofile=profile.out -covermode=atomic "$d" + if [ -f profile.out ]; then + cat profile.out >> coverage.txt + rm profile.out + fi +done diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go new file mode 100644 index 00000000..19f8e044 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth.go @@ -0,0 +1,257 @@ +package runewidth + +import ( + "os" +) + +//go:generate go run script/generate.go + +var ( + // EastAsianWidth will be set true if the current locale is CJK + EastAsianWidth bool + + // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ + ZeroWidthJoiner bool + + // DefaultCondition is a condition in current locale + DefaultCondition = &Condition{} +) + +func init() { + handleEnv() +} + +func handleEnv() { + env := os.Getenv("RUNEWIDTH_EASTASIAN") + if env == "" { + EastAsianWidth = IsEastAsian() + } else { + EastAsianWidth = env == "1" + } + // update DefaultCondition + DefaultCondition.EastAsianWidth = EastAsianWidth + DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner +} + +type interval struct { + first rune + last rune +} + +type table []interval + +func inTables(r rune, ts ...table) bool { + for _, t := range ts { + if inTable(r, t) { + return true + } + } + return false +} + +func inTable(r rune, t table) bool { + if r < t[0].first { + return false + } + + bot := 0 + top := len(t) - 1 + for top >= bot { + mid := (bot + top) >> 1 + + switch { + case t[mid].last < r: + bot = mid + 1 + case t[mid].first > r: + top = mid - 1 + default: + return true + } + } + + return false +} + +var private = table{ + {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD}, +} + +var nonprint = table{ + {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, + {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, + {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, + {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, +} + +// Condition have flag EastAsianWidth whether the current locale is CJK or not. +type Condition struct { + EastAsianWidth bool + ZeroWidthJoiner bool +} + +// NewCondition return new instance of Condition which is current locale. +func NewCondition() *Condition { + return &Condition{ + EastAsianWidth: EastAsianWidth, + ZeroWidthJoiner: ZeroWidthJoiner, + } +} + +// RuneWidth returns the number of cells in r. +// See http://www.unicode.org/reports/tr11/ +func (c *Condition) RuneWidth(r rune) int { + switch { + case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned): + return 0 + case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth): + return 2 + default: + return 1 + } +} + +func (c *Condition) stringWidth(s string) (width int) { + for _, r := range []rune(s) { + width += c.RuneWidth(r) + } + return width +} + +func (c *Condition) stringWidthZeroJoiner(s string) (width int) { + r1, r2 := rune(0), rune(0) + for _, r := range []rune(s) { + if r == 0xFE0E || r == 0xFE0F { + continue + } + w := c.RuneWidth(r) + if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) { + if width < w { + width = w + } + } else { + width += w + } + r1, r2 = r2, r + } + return width +} + +// StringWidth return width as you can see +func (c *Condition) StringWidth(s string) (width int) { + if c.ZeroWidthJoiner { + return c.stringWidthZeroJoiner(s) + } + return c.stringWidth(s) +} + +// Truncate return string truncated with w cells +func (c *Condition) Truncate(s string, w int, tail string) string { + if c.StringWidth(s) <= w { + return s + } + r := []rune(s) + tw := c.StringWidth(tail) + w -= tw + width := 0 + i := 0 + for ; i < len(r); i++ { + cw := c.RuneWidth(r[i]) + if width+cw > w { + break + } + width += cw + } + return string(r[0:i]) + tail +} + +// Wrap return string wrapped with w cells +func (c *Condition) Wrap(s string, w int) string { + width := 0 + out := "" + for _, r := range []rune(s) { + cw := RuneWidth(r) + if r == '\n' { + out += string(r) + width = 0 + continue + } else if width+cw > w { + out += "\n" + width = 0 + out += string(r) + width += cw + continue + } + out += string(r) + width += cw + } + return out +} + +// FillLeft return string filled in left by spaces in w cells +func (c *Condition) FillLeft(s string, w int) string { + width := c.StringWidth(s) + count := w - width + if count > 0 { + b := make([]byte, count) + for i := range b { + b[i] = ' ' + } + return string(b) + s + } + return s +} + +// FillRight return string filled in left by spaces in w cells +func (c *Condition) FillRight(s string, w int) string { + width := c.StringWidth(s) + count := w - width + if count > 0 { + b := make([]byte, count) + for i := range b { + b[i] = ' ' + } + return s + string(b) + } + return s +} + +// RuneWidth returns the number of cells in r. +// See http://www.unicode.org/reports/tr11/ +func RuneWidth(r rune) int { + return DefaultCondition.RuneWidth(r) +} + +// IsAmbiguousWidth returns whether is ambiguous width or not. +func IsAmbiguousWidth(r rune) bool { + return inTables(r, private, ambiguous) +} + +// IsNeutralWidth returns whether is neutral width or not. +func IsNeutralWidth(r rune) bool { + return inTable(r, neutral) +} + +// StringWidth return width as you can see +func StringWidth(s string) (width int) { + return DefaultCondition.StringWidth(s) +} + +// Truncate return string truncated with w cells +func Truncate(s string, w int, tail string) string { + return DefaultCondition.Truncate(s, w, tail) +} + +// Wrap return string wrapped with w cells +func Wrap(s string, w int) string { + return DefaultCondition.Wrap(s, w) +} + +// FillLeft return string filled in left by spaces in w cells +func FillLeft(s string, w int) string { + return DefaultCondition.FillLeft(s, w) +} + +// FillRight return string filled in left by spaces in w cells +func FillRight(s string, w int) string { + return DefaultCondition.FillRight(s, w) +} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go b/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go new file mode 100644 index 00000000..7d99f6e5 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go @@ -0,0 +1,8 @@ +// +build appengine + +package runewidth + +// IsEastAsian return true if the current locale is CJK +func IsEastAsian() bool { + return false +} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_js.go b/vendor/github.com/mattn/go-runewidth/runewidth_js.go new file mode 100644 index 00000000..c5fdf40b --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth_js.go @@ -0,0 +1,9 @@ +// +build js +// +build !appengine + +package runewidth + +func IsEastAsian() bool { + // TODO: Implement this for the web. Detect east asian in a compatible way, and return true. + return false +} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_posix.go b/vendor/github.com/mattn/go-runewidth/runewidth_posix.go new file mode 100644 index 00000000..480ad748 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth_posix.go @@ -0,0 +1,82 @@ +// +build !windows +// +build !js +// +build !appengine + +package runewidth + +import ( + "os" + "regexp" + "strings" +) + +var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`) + +var mblenTable = map[string]int{ + "utf-8": 6, + "utf8": 6, + "jis": 8, + "eucjp": 3, + "euckr": 2, + "euccn": 2, + "sjis": 2, + "cp932": 2, + "cp51932": 2, + "cp936": 2, + "cp949": 2, + "cp950": 2, + "big5": 2, + "gbk": 2, + "gb2312": 2, +} + +func isEastAsian(locale string) bool { + charset := strings.ToLower(locale) + r := reLoc.FindStringSubmatch(locale) + if len(r) == 2 { + charset = strings.ToLower(r[1]) + } + + if strings.HasSuffix(charset, "@cjk_narrow") { + return false + } + + for pos, b := range []byte(charset) { + if b == '@' { + charset = charset[:pos] + break + } + } + max := 1 + if m, ok := mblenTable[charset]; ok { + max = m + } + if max > 1 && (charset[0] != 'u' || + strings.HasPrefix(locale, "ja") || + strings.HasPrefix(locale, "ko") || + strings.HasPrefix(locale, "zh")) { + return true + } + return false +} + +// IsEastAsian return true if the current locale is CJK +func IsEastAsian() bool { + locale := os.Getenv("LC_ALL") + if locale == "" { + locale = os.Getenv("LC_CTYPE") + } + if locale == "" { + locale = os.Getenv("LANG") + } + + // ignore C locale + if locale == "POSIX" || locale == "C" { + return false + } + if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') { + return false + } + + return isEastAsian(locale) +} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_table.go b/vendor/github.com/mattn/go-runewidth/runewidth_table.go new file mode 100644 index 00000000..b27d77d8 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth_table.go @@ -0,0 +1,437 @@ +// Code generated by script/generate.go. DO NOT EDIT. + +package runewidth + +var combining = table{ + {0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3}, + {0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01}, + {0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1AC0}, + {0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF}, + {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF}, + {0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D}, + {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1}, + {0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A}, + {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x11300, 0x11301}, + {0x1133B, 0x1133C}, {0x11366, 0x1136C}, {0x11370, 0x11374}, + {0x16AF0, 0x16AF4}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172}, + {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, + {0x1D242, 0x1D244}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018}, + {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, + {0x1E8D0, 0x1E8D6}, +} + +var doublewidth = table{ + {0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A}, + {0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3}, + {0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653}, + {0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1}, + {0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5}, + {0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA}, + {0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA}, + {0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B}, + {0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E}, + {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797}, + {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C}, + {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99}, + {0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB}, + {0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF}, + {0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3}, + {0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF}, + {0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C}, + {0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19}, + {0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B}, + {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4}, + {0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5}, + {0x18D00, 0x18D08}, {0x1B000, 0x1B11E}, {0x1B150, 0x1B152}, + {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004}, + {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, + {0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, + {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, + {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, + {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, + {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, + {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, + {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, + {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, + {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7}, + {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB}, + {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F978}, + {0x1F97A, 0x1F9CB}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA74}, + {0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAA8}, + {0x1FAB0, 0x1FAB6}, {0x1FAC0, 0x1FAC2}, {0x1FAD0, 0x1FAD6}, + {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, +} + +var ambiguous = table{ + {0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8}, + {0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4}, + {0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6}, + {0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1}, + {0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED}, + {0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA}, + {0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101}, + {0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B}, + {0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133}, + {0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144}, + {0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153}, + {0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE}, + {0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4}, + {0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA}, + {0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261}, + {0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB}, + {0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB}, + {0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F}, + {0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1}, + {0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F}, + {0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016}, + {0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022}, + {0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033}, + {0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E}, + {0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084}, + {0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105}, + {0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116}, + {0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B}, + {0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B}, + {0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199}, + {0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4}, + {0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203}, + {0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F}, + {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A}, + {0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, + {0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237}, + {0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C}, + {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267}, + {0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283}, + {0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299}, + {0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312}, + {0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573}, + {0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1}, + {0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7}, + {0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8}, + {0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5}, + {0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609}, + {0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E}, + {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661}, + {0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D}, + {0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF}, + {0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1}, + {0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1}, + {0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC}, + {0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F}, + {0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF}, + {0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A}, + {0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, + {0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF}, + {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD}, +} +var notassigned = table{ + {0x27E6, 0x27ED}, {0x2985, 0x2986}, +} + +var neutral = table{ + {0x0000, 0x001F}, {0x007F, 0x00A0}, {0x00A9, 0x00A9}, + {0x00AB, 0x00AB}, {0x00B5, 0x00B5}, {0x00BB, 0x00BB}, + {0x00C0, 0x00C5}, {0x00C7, 0x00CF}, {0x00D1, 0x00D6}, + {0x00D9, 0x00DD}, {0x00E2, 0x00E5}, {0x00E7, 0x00E7}, + {0x00EB, 0x00EB}, {0x00EE, 0x00EF}, {0x00F1, 0x00F1}, + {0x00F4, 0x00F6}, {0x00FB, 0x00FB}, {0x00FD, 0x00FD}, + {0x00FF, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112}, + {0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A}, + {0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E}, + {0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C}, + {0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A}, + {0x016C, 0x01CD}, {0x01CF, 0x01CF}, {0x01D1, 0x01D1}, + {0x01D3, 0x01D3}, {0x01D5, 0x01D5}, {0x01D7, 0x01D7}, + {0x01D9, 0x01D9}, {0x01DB, 0x01DB}, {0x01DD, 0x0250}, + {0x0252, 0x0260}, {0x0262, 0x02C3}, {0x02C5, 0x02C6}, + {0x02C8, 0x02C8}, {0x02CC, 0x02CC}, {0x02CE, 0x02CF}, + {0x02D1, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE}, + {0x02E0, 0x02FF}, {0x0370, 0x0377}, {0x037A, 0x037F}, + {0x0384, 0x038A}, {0x038C, 0x038C}, {0x038E, 0x0390}, + {0x03AA, 0x03B0}, {0x03C2, 0x03C2}, {0x03CA, 0x0400}, + {0x0402, 0x040F}, {0x0450, 0x0450}, {0x0452, 0x052F}, + {0x0531, 0x0556}, {0x0559, 0x058A}, {0x058D, 0x058F}, + {0x0591, 0x05C7}, {0x05D0, 0x05EA}, {0x05EF, 0x05F4}, + {0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A}, + {0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D}, + {0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E}, + {0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08C7}, + {0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990}, + {0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2}, + {0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8}, + {0x09CB, 0x09CE}, {0x09D7, 0x09D7}, {0x09DC, 0x09DD}, + {0x09DF, 0x09E3}, {0x09E6, 0x09FE}, {0x0A01, 0x0A03}, + {0x0A05, 0x0A0A}, {0x0A0F, 0x0A10}, {0x0A13, 0x0A28}, + {0x0A2A, 0x0A30}, {0x0A32, 0x0A33}, {0x0A35, 0x0A36}, + {0x0A38, 0x0A39}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42}, + {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51}, + {0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A76}, + {0x0A81, 0x0A83}, {0x0A85, 0x0A8D}, {0x0A8F, 0x0A91}, + {0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0}, {0x0AB2, 0x0AB3}, + {0x0AB5, 0x0AB9}, {0x0ABC, 0x0AC5}, {0x0AC7, 0x0AC9}, + {0x0ACB, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE3}, + {0x0AE6, 0x0AF1}, {0x0AF9, 0x0AFF}, {0x0B01, 0x0B03}, + {0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28}, + {0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39}, + {0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D}, + {0x0B55, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63}, + {0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A}, + {0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A}, + {0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4}, + {0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9}, {0x0BBE, 0x0BC2}, + {0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCD}, {0x0BD0, 0x0BD0}, + {0x0BD7, 0x0BD7}, {0x0BE6, 0x0BFA}, {0x0C00, 0x0C0C}, + {0x0C0E, 0x0C10}, {0x0C12, 0x0C28}, {0x0C2A, 0x0C39}, + {0x0C3D, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D}, + {0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C63}, + {0x0C66, 0x0C6F}, {0x0C77, 0x0C8C}, {0x0C8E, 0x0C90}, + {0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9}, + {0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD}, + {0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3}, + {0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D0C}, + {0x0D0E, 0x0D10}, {0x0D12, 0x0D44}, {0x0D46, 0x0D48}, + {0x0D4A, 0x0D4F}, {0x0D54, 0x0D63}, {0x0D66, 0x0D7F}, + {0x0D81, 0x0D83}, {0x0D85, 0x0D96}, {0x0D9A, 0x0DB1}, + {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD}, {0x0DC0, 0x0DC6}, + {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4}, {0x0DD6, 0x0DD6}, + {0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF4}, + {0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B}, {0x0E81, 0x0E82}, + {0x0E84, 0x0E84}, {0x0E86, 0x0E8A}, {0x0E8C, 0x0EA3}, + {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD}, {0x0EC0, 0x0EC4}, + {0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9}, + {0x0EDC, 0x0EDF}, {0x0F00, 0x0F47}, {0x0F49, 0x0F6C}, + {0x0F71, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FBE, 0x0FCC}, + {0x0FCE, 0x0FDA}, {0x1000, 0x10C5}, {0x10C7, 0x10C7}, + {0x10CD, 0x10CD}, {0x10D0, 0x10FF}, {0x1160, 0x1248}, + {0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258}, + {0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D}, + {0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE}, + {0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6}, + {0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A}, + {0x135D, 0x137C}, {0x1380, 0x1399}, {0x13A0, 0x13F5}, + {0x13F8, 0x13FD}, {0x1400, 0x169C}, {0x16A0, 0x16F8}, + {0x1700, 0x170C}, {0x170E, 0x1714}, {0x1720, 0x1736}, + {0x1740, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770}, + {0x1772, 0x1773}, {0x1780, 0x17DD}, {0x17E0, 0x17E9}, + {0x17F0, 0x17F9}, {0x1800, 0x180E}, {0x1810, 0x1819}, + {0x1820, 0x1878}, {0x1880, 0x18AA}, {0x18B0, 0x18F5}, + {0x1900, 0x191E}, {0x1920, 0x192B}, {0x1930, 0x193B}, + {0x1940, 0x1940}, {0x1944, 0x196D}, {0x1970, 0x1974}, + {0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x19D0, 0x19DA}, + {0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E}, {0x1A60, 0x1A7C}, + {0x1A7F, 0x1A89}, {0x1A90, 0x1A99}, {0x1AA0, 0x1AAD}, + {0x1AB0, 0x1AC0}, {0x1B00, 0x1B4B}, {0x1B50, 0x1B7C}, + {0x1B80, 0x1BF3}, {0x1BFC, 0x1C37}, {0x1C3B, 0x1C49}, + {0x1C4D, 0x1C88}, {0x1C90, 0x1CBA}, {0x1CBD, 0x1CC7}, + {0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9}, {0x1DFB, 0x1F15}, + {0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D}, + {0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B}, + {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4}, + {0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3}, {0x1FD6, 0x1FDB}, + {0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFE}, + {0x2000, 0x200F}, {0x2011, 0x2012}, {0x2017, 0x2017}, + {0x201A, 0x201B}, {0x201E, 0x201F}, {0x2023, 0x2023}, + {0x2028, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034}, + {0x2036, 0x203A}, {0x203C, 0x203D}, {0x203F, 0x2064}, + {0x2066, 0x2071}, {0x2075, 0x207E}, {0x2080, 0x2080}, + {0x2085, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8}, + {0x20AA, 0x20AB}, {0x20AD, 0x20BF}, {0x20D0, 0x20F0}, + {0x2100, 0x2102}, {0x2104, 0x2104}, {0x2106, 0x2108}, + {0x210A, 0x2112}, {0x2114, 0x2115}, {0x2117, 0x2120}, + {0x2123, 0x2125}, {0x2127, 0x212A}, {0x212C, 0x2152}, + {0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F}, + {0x217A, 0x2188}, {0x218A, 0x218B}, {0x219A, 0x21B7}, + {0x21BA, 0x21D1}, {0x21D3, 0x21D3}, {0x21D5, 0x21E6}, + {0x21E8, 0x21FF}, {0x2201, 0x2201}, {0x2204, 0x2206}, + {0x2209, 0x220A}, {0x220C, 0x220E}, {0x2210, 0x2210}, + {0x2212, 0x2214}, {0x2216, 0x2219}, {0x221B, 0x221C}, + {0x2221, 0x2222}, {0x2224, 0x2224}, {0x2226, 0x2226}, + {0x222D, 0x222D}, {0x222F, 0x2233}, {0x2238, 0x223B}, + {0x223E, 0x2247}, {0x2249, 0x224B}, {0x224D, 0x2251}, + {0x2253, 0x225F}, {0x2262, 0x2263}, {0x2268, 0x2269}, + {0x226C, 0x226D}, {0x2270, 0x2281}, {0x2284, 0x2285}, + {0x2288, 0x2294}, {0x2296, 0x2298}, {0x229A, 0x22A4}, + {0x22A6, 0x22BE}, {0x22C0, 0x2311}, {0x2313, 0x2319}, + {0x231C, 0x2328}, {0x232B, 0x23E8}, {0x23ED, 0x23EF}, + {0x23F1, 0x23F2}, {0x23F4, 0x2426}, {0x2440, 0x244A}, + {0x24EA, 0x24EA}, {0x254C, 0x254F}, {0x2574, 0x257F}, + {0x2590, 0x2591}, {0x2596, 0x259F}, {0x25A2, 0x25A2}, + {0x25AA, 0x25B1}, {0x25B4, 0x25B5}, {0x25B8, 0x25BB}, + {0x25BE, 0x25BF}, {0x25C2, 0x25C5}, {0x25C9, 0x25CA}, + {0x25CC, 0x25CD}, {0x25D2, 0x25E1}, {0x25E6, 0x25EE}, + {0x25F0, 0x25FC}, {0x25FF, 0x2604}, {0x2607, 0x2608}, + {0x260A, 0x260D}, {0x2610, 0x2613}, {0x2616, 0x261B}, + {0x261D, 0x261D}, {0x261F, 0x263F}, {0x2641, 0x2641}, + {0x2643, 0x2647}, {0x2654, 0x265F}, {0x2662, 0x2662}, + {0x2666, 0x2666}, {0x266B, 0x266B}, {0x266E, 0x266E}, + {0x2670, 0x267E}, {0x2680, 0x2692}, {0x2694, 0x269D}, + {0x26A0, 0x26A0}, {0x26A2, 0x26A9}, {0x26AC, 0x26BC}, + {0x26C0, 0x26C3}, {0x26E2, 0x26E2}, {0x26E4, 0x26E7}, + {0x2700, 0x2704}, {0x2706, 0x2709}, {0x270C, 0x2727}, + {0x2729, 0x273C}, {0x273E, 0x274B}, {0x274D, 0x274D}, + {0x274F, 0x2752}, {0x2756, 0x2756}, {0x2758, 0x2775}, + {0x2780, 0x2794}, {0x2798, 0x27AF}, {0x27B1, 0x27BE}, + {0x27C0, 0x27E5}, {0x27EE, 0x2984}, {0x2987, 0x2B1A}, + {0x2B1D, 0x2B4F}, {0x2B51, 0x2B54}, {0x2B5A, 0x2B73}, + {0x2B76, 0x2B95}, {0x2B97, 0x2C2E}, {0x2C30, 0x2C5E}, + {0x2C60, 0x2CF3}, {0x2CF9, 0x2D25}, {0x2D27, 0x2D27}, + {0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D70}, + {0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE}, + {0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6}, + {0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE}, + {0x2DE0, 0x2E52}, {0x303F, 0x303F}, {0x4DC0, 0x4DFF}, + {0xA4D0, 0xA62B}, {0xA640, 0xA6F7}, {0xA700, 0xA7BF}, + {0xA7C2, 0xA7CA}, {0xA7F5, 0xA82C}, {0xA830, 0xA839}, + {0xA840, 0xA877}, {0xA880, 0xA8C5}, {0xA8CE, 0xA8D9}, + {0xA8E0, 0xA953}, {0xA95F, 0xA95F}, {0xA980, 0xA9CD}, + {0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE}, {0xAA00, 0xAA36}, + {0xAA40, 0xAA4D}, {0xAA50, 0xAA59}, {0xAA5C, 0xAAC2}, + {0xAADB, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E}, + {0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E}, + {0xAB30, 0xAB6B}, {0xAB70, 0xABED}, {0xABF0, 0xABF9}, + {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDFFF}, + {0xFB00, 0xFB06}, {0xFB13, 0xFB17}, {0xFB1D, 0xFB36}, + {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41}, + {0xFB43, 0xFB44}, {0xFB46, 0xFBC1}, {0xFBD3, 0xFD3F}, + {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, {0xFDF0, 0xFDFD}, + {0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC}, + {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC}, {0x10000, 0x1000B}, + {0x1000D, 0x10026}, {0x10028, 0x1003A}, {0x1003C, 0x1003D}, + {0x1003F, 0x1004D}, {0x10050, 0x1005D}, {0x10080, 0x100FA}, + {0x10100, 0x10102}, {0x10107, 0x10133}, {0x10137, 0x1018E}, + {0x10190, 0x1019C}, {0x101A0, 0x101A0}, {0x101D0, 0x101FD}, + {0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x102E0, 0x102FB}, + {0x10300, 0x10323}, {0x1032D, 0x1034A}, {0x10350, 0x1037A}, + {0x10380, 0x1039D}, {0x1039F, 0x103C3}, {0x103C8, 0x103D5}, + {0x10400, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3}, + {0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563}, + {0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755}, + {0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808}, + {0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C}, + {0x1083F, 0x10855}, {0x10857, 0x1089E}, {0x108A7, 0x108AF}, + {0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x1091B}, + {0x1091F, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x109B7}, + {0x109BC, 0x109CF}, {0x109D2, 0x10A03}, {0x10A05, 0x10A06}, + {0x10A0C, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A35}, + {0x10A38, 0x10A3A}, {0x10A3F, 0x10A48}, {0x10A50, 0x10A58}, + {0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6}, {0x10AEB, 0x10AF6}, + {0x10B00, 0x10B35}, {0x10B39, 0x10B55}, {0x10B58, 0x10B72}, + {0x10B78, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF}, + {0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2}, + {0x10CFA, 0x10D27}, {0x10D30, 0x10D39}, {0x10E60, 0x10E7E}, + {0x10E80, 0x10EA9}, {0x10EAB, 0x10EAD}, {0x10EB0, 0x10EB1}, + {0x10F00, 0x10F27}, {0x10F30, 0x10F59}, {0x10FB0, 0x10FCB}, + {0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F}, + {0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8}, + {0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11147}, + {0x11150, 0x11176}, {0x11180, 0x111DF}, {0x111E1, 0x111F4}, + {0x11200, 0x11211}, {0x11213, 0x1123E}, {0x11280, 0x11286}, + {0x11288, 0x11288}, {0x1128A, 0x1128D}, {0x1128F, 0x1129D}, + {0x1129F, 0x112A9}, {0x112B0, 0x112EA}, {0x112F0, 0x112F9}, + {0x11300, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310}, + {0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333}, + {0x11335, 0x11339}, {0x1133B, 0x11344}, {0x11347, 0x11348}, + {0x1134B, 0x1134D}, {0x11350, 0x11350}, {0x11357, 0x11357}, + {0x1135D, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374}, + {0x11400, 0x1145B}, {0x1145D, 0x11461}, {0x11480, 0x114C7}, + {0x114D0, 0x114D9}, {0x11580, 0x115B5}, {0x115B8, 0x115DD}, + {0x11600, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C}, + {0x11680, 0x116B8}, {0x116C0, 0x116C9}, {0x11700, 0x1171A}, + {0x1171D, 0x1172B}, {0x11730, 0x1173F}, {0x11800, 0x1183B}, + {0x118A0, 0x118F2}, {0x118FF, 0x11906}, {0x11909, 0x11909}, + {0x1190C, 0x11913}, {0x11915, 0x11916}, {0x11918, 0x11935}, + {0x11937, 0x11938}, {0x1193B, 0x11946}, {0x11950, 0x11959}, + {0x119A0, 0x119A7}, {0x119AA, 0x119D7}, {0x119DA, 0x119E4}, + {0x11A00, 0x11A47}, {0x11A50, 0x11AA2}, {0x11AC0, 0x11AF8}, + {0x11C00, 0x11C08}, {0x11C0A, 0x11C36}, {0x11C38, 0x11C45}, + {0x11C50, 0x11C6C}, {0x11C70, 0x11C8F}, {0x11C92, 0x11CA7}, + {0x11CA9, 0x11CB6}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09}, + {0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, + {0x11D3F, 0x11D47}, {0x11D50, 0x11D59}, {0x11D60, 0x11D65}, + {0x11D67, 0x11D68}, {0x11D6A, 0x11D8E}, {0x11D90, 0x11D91}, + {0x11D93, 0x11D98}, {0x11DA0, 0x11DA9}, {0x11EE0, 0x11EF8}, + {0x11FB0, 0x11FB0}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399}, + {0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543}, + {0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646}, + {0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69}, + {0x16A6E, 0x16A6F}, {0x16AD0, 0x16AED}, {0x16AF0, 0x16AF5}, + {0x16B00, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61}, + {0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16E40, 0x16E9A}, + {0x16F00, 0x16F4A}, {0x16F4F, 0x16F87}, {0x16F8F, 0x16F9F}, + {0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88}, + {0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BCA3}, {0x1D000, 0x1D0F5}, + {0x1D100, 0x1D126}, {0x1D129, 0x1D1E8}, {0x1D200, 0x1D245}, + {0x1D2E0, 0x1D2F3}, {0x1D300, 0x1D356}, {0x1D360, 0x1D378}, + {0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F}, + {0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC}, + {0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3}, + {0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514}, + {0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E}, + {0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550}, + {0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D7CB}, {0x1D7CE, 0x1DA8B}, + {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, + {0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, + {0x1E026, 0x1E02A}, {0x1E100, 0x1E12C}, {0x1E130, 0x1E13D}, + {0x1E140, 0x1E149}, {0x1E14E, 0x1E14F}, {0x1E2C0, 0x1E2F9}, + {0x1E2FF, 0x1E2FF}, {0x1E800, 0x1E8C4}, {0x1E8C7, 0x1E8D6}, + {0x1E900, 0x1E94B}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F}, + {0x1EC71, 0x1ECB4}, {0x1ED01, 0x1ED3D}, {0x1EE00, 0x1EE03}, + {0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, {0x1EE24, 0x1EE24}, + {0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, {0x1EE34, 0x1EE37}, + {0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, {0x1EE42, 0x1EE42}, + {0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, {0x1EE4B, 0x1EE4B}, + {0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, {0x1EE54, 0x1EE54}, + {0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, {0x1EE5B, 0x1EE5B}, + {0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, {0x1EE61, 0x1EE62}, + {0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, {0x1EE6C, 0x1EE72}, + {0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, {0x1EE7E, 0x1EE7E}, + {0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, {0x1EEA1, 0x1EEA3}, + {0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1}, + {0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093}, + {0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE}, + {0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10F}, {0x1F12E, 0x1F12F}, + {0x1F16A, 0x1F16F}, {0x1F1AD, 0x1F1AD}, {0x1F1E6, 0x1F1FF}, + {0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D}, + {0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF}, + {0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F}, + {0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A}, + {0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594}, + {0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F}, + {0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4}, + {0x1F6E0, 0x1F6EA}, {0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773}, + {0x1F780, 0x1F7D8}, {0x1F800, 0x1F80B}, {0x1F810, 0x1F847}, + {0x1F850, 0x1F859}, {0x1F860, 0x1F887}, {0x1F890, 0x1F8AD}, + {0x1F8B0, 0x1F8B1}, {0x1F900, 0x1F90B}, {0x1F93B, 0x1F93B}, + {0x1F946, 0x1F946}, {0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D}, + {0x1FB00, 0x1FB92}, {0x1FB94, 0x1FBCA}, {0x1FBF0, 0x1FBF9}, + {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, +} + +var emoji = table{ + {0x203C, 0x203C}, {0x2049, 0x2049}, {0x2122, 0x2122}, + {0x2139, 0x2139}, {0x2194, 0x2199}, {0x21A9, 0x21AA}, + {0x231A, 0x231B}, {0x2328, 0x2328}, {0x2388, 0x2388}, + {0x23CF, 0x23CF}, {0x23E9, 0x23F3}, {0x23F8, 0x23FA}, + {0x24C2, 0x24C2}, {0x25AA, 0x25AB}, {0x25B6, 0x25B6}, + {0x25C0, 0x25C0}, {0x25FB, 0x25FE}, {0x2600, 0x2605}, + {0x2607, 0x2612}, {0x2614, 0x2685}, {0x2690, 0x2705}, + {0x2708, 0x2712}, {0x2714, 0x2714}, {0x2716, 0x2716}, + {0x271D, 0x271D}, {0x2721, 0x2721}, {0x2728, 0x2728}, + {0x2733, 0x2734}, {0x2744, 0x2744}, {0x2747, 0x2747}, + {0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755}, + {0x2757, 0x2757}, {0x2763, 0x2767}, {0x2795, 0x2797}, + {0x27A1, 0x27A1}, {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, + {0x2934, 0x2935}, {0x2B05, 0x2B07}, {0x2B1B, 0x2B1C}, + {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x3030, 0x3030}, + {0x303D, 0x303D}, {0x3297, 0x3297}, {0x3299, 0x3299}, + {0x1F000, 0x1F0FF}, {0x1F10D, 0x1F10F}, {0x1F12F, 0x1F12F}, + {0x1F16C, 0x1F171}, {0x1F17E, 0x1F17F}, {0x1F18E, 0x1F18E}, + {0x1F191, 0x1F19A}, {0x1F1AD, 0x1F1E5}, {0x1F201, 0x1F20F}, + {0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F23A}, + {0x1F23C, 0x1F23F}, {0x1F249, 0x1F3FA}, {0x1F400, 0x1F53D}, + {0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F}, + {0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F}, + {0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF}, + {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FAFF}, + {0x1FC00, 0x1FFFD}, +} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go new file mode 100644 index 00000000..d6a61777 --- /dev/null +++ b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go @@ -0,0 +1,28 @@ +// +build windows +// +build !appengine + +package runewidth + +import ( + "syscall" +) + +var ( + kernel32 = syscall.NewLazyDLL("kernel32") + procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP") +) + +// IsEastAsian return true if the current locale is CJK +func IsEastAsian() bool { + r1, _, _ := procGetConsoleOutputCP.Call() + if r1 == 0 { + return false + } + + switch int(r1) { + case 932, 51932, 936, 949, 950: + return true + } + + return false +} diff --git a/vendor/github.com/olekukonko/tablewriter/.gitignore b/vendor/github.com/olekukonko/tablewriter/.gitignore new file mode 100644 index 00000000..b66cec63 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/.gitignore @@ -0,0 +1,15 @@ +# Created by .ignore support plugin (hsz.mobi) +### Go template +# Binaries for programs and plugins +*.exe +*.exe~ +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + diff --git a/vendor/github.com/olekukonko/tablewriter/.travis.yml b/vendor/github.com/olekukonko/tablewriter/.travis.yml new file mode 100644 index 00000000..9c64270e --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/.travis.yml @@ -0,0 +1,14 @@ +language: go + +go: + - 1.1 + - 1.2 + - 1.3 + - 1.4 + - 1.5 + - 1.6 + - 1.7 + - 1.8 + - 1.9 + - "1.10" + - tip diff --git a/vendor/github.com/olekukonko/tablewriter/LICENSE.md b/vendor/github.com/olekukonko/tablewriter/LICENSE.md new file mode 100644 index 00000000..a0769b5c --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/LICENSE.md @@ -0,0 +1,19 @@ +Copyright (C) 2014 by Oleku Konko + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md new file mode 100644 index 00000000..cb9b2ef4 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/README.md @@ -0,0 +1,396 @@ +ASCII Table Writer +========= + +[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) +[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter) +[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter) + +Generate ASCII table on the fly ... Installation is simple as + + go get github.com/olekukonko/tablewriter + + +#### Features +- Automatic Padding +- Support Multiple Lines +- Supports Alignment +- Support Custom Separators +- Automatic Alignment of numbers & percentage +- Write directly to http , file etc via `io.Writer` +- Read directly from CSV file +- Optional row line via `SetRowLine` +- Normalise table header +- Make CSV Headers optional +- Enable or disable table border +- Set custom footer support +- Optional identical cells merging +- Set custom caption +- Optional reflowing of paragrpahs in multi-line cells. + +#### Example 1 - Basic +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +##### Output 1 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +``` + +#### Example 2 - Without Border / Footer / Bulk Append +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 2 +``` + + DATE | DESCRIPTION | CV2 | AMOUNT +-----------+--------------------------+-------+---------- + 1/1/2014 | Domain name | 2233 | $10.98 + 1/1/2014 | January Hosting | 2233 | $54.95 + 1/4/2014 | February Hosting | 2233 | $51.00 + 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 +-----------+--------------------------+-------+---------- + TOTAL | $146 93 + --------+---------- + +``` + + +#### Example 3 - CSV +```go +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true) +table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment +table.Render() +``` + +##### Output 3 +``` ++----------+--------------+------+-----+---------+----------------+ +| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA | ++----------+--------------+------+-----+---------+----------------+ +| user_id | smallint(5) | NO | PRI | NULL | auto_increment | +| username | varchar(10) | NO | | NULL | | +| password | varchar(100) | NO | | NULL | | ++----------+--------------+------+-----+---------+----------------+ +``` + +#### Example 4 - Custom Separator +```go +table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true) +table.SetRowLine(true) // Enable row line + +// Change table lines +table.SetCenterSeparator("*") +table.SetColumnSeparator("╪") +table.SetRowSeparator("-") + +table.SetAlignment(tablewriter.ALIGN_LEFT) +table.Render() +``` + +##### Output 4 +``` +*------------*-----------*---------* +╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪ +*------------*-----------*---------* +╪ John ╪ Barry ╪ 123456 ╪ +*------------*-----------*---------* +╪ Kathy ╪ Smith ╪ 687987 ╪ +*------------*-----------*---------* +╪ Bob ╪ McCornick ╪ 3979870 ╪ +*------------*-----------*---------* +``` + +#### Example 5 - Markdown Format +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) +table.SetCenterSeparator("|") +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 5 +``` +| DATE | DESCRIPTION | CV2 | AMOUNT | +|----------|--------------------------|------|--------| +| 1/1/2014 | Domain name | 2233 | $10.98 | +| 1/1/2014 | January Hosting | 2233 | $54.95 | +| 1/4/2014 | February Hosting | 2233 | $51.00 | +| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | +``` + +#### Example 6 - Identical cells merging +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "1234", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2345", "$54.95"}, + []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) +table.SetAutoMergeCells(true) +table.SetRowLine(true) +table.AppendBulk(data) +table.Render() +``` + +##### Output 6 +``` ++----------+--------------------------+-------+---------+ +| DATE | DESCRIPTION | CV2 | AMOUNT | ++----------+--------------------------+-------+---------+ +| 1/1/2014 | Domain name | 1234 | $10.98 | ++ +--------------------------+-------+---------+ +| | January Hosting | 2345 | $54.95 | ++----------+--------------------------+-------+---------+ +| 1/4/2014 | February Hosting | 3456 | $51.00 | ++ +--------------------------+-------+---------+ +| | February Extra Bandwidth | 4567 | $30.00 | ++----------+--------------------------+-------+---------+ +| TOTAL | $146 93 | ++----------+--------------------------+-------+---------+ +``` + + +#### Table with color +```go +data := [][]string{ + []string{"1/1/2014", "Domain name", "2233", "$10.98"}, + []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, + []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, + []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) +table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer +table.SetBorder(false) // Set Border to false + +table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, + tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, + tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) + +table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) + +table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.FgHiRedColor}) + +table.AppendBulk(data) +table.Render() +``` + +#### Table with color Output +![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png) + +#### Example - 7 Table Cells with Color + +Individual Cell Colors from `func Rich` take precedence over Column Colors + +```go +data := [][]string{ + []string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"}, + []string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"}, + []string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"}, + []string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"}, + []string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"}, + []string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"}, + []string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"}, + []string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"}, + []string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"}, + []string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"}) +table.SetFooter([]string{"", "", "Footer3", "Footer4"}) +table.SetBorder(false) + +table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, + tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, + tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, + tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) + +table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, + tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) + +table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, + tablewriter.Colors{tablewriter.Bold}, + tablewriter.Colors{tablewriter.FgHiRedColor}) + +colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"} +colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"} + +for i, row := range data { + if i == 4 { + table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}}) + table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}}) + } + table.Append(row) +} + +table.SetAutoMergeCells(true) +table.Render() + +``` + +##### Table cells with color Output +![Table cells with Color](https://user-images.githubusercontent.com/9064687/63969376-bcd88d80-ca6f-11e9-9466-c3d954700b25.png) + +#### Example 8 - Set table caption +```go +data := [][]string{ + []string{"A", "The Good", "500"}, + []string{"B", "The Very very Bad Man", "288"}, + []string{"C", "The Ugly", "120"}, + []string{"D", "The Gopher", "800"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Sign", "Rating"}) +table.SetCaption(true, "Movie ratings.") + +for _, v := range data { + table.Append(v) +} +table.Render() // Send output +``` + +Note: Caption text will wrap with total width of rendered table. + +##### Output 7 +``` ++------+-----------------------+--------+ +| NAME | SIGN | RATING | ++------+-----------------------+--------+ +| A | The Good | 500 | +| B | The Very very Bad Man | 288 | +| C | The Ugly | 120 | +| D | The Gopher | 800 | ++------+-----------------------+--------+ +Movie ratings. +``` + +#### Example 8 - Set NoWhiteSpace and TablePadding option +```go +data := [][]string{ + {"node1.example.com", "Ready", "compute", "1.11"}, + {"node2.example.com", "Ready", "compute", "1.11"}, + {"node3.example.com", "Ready", "compute", "1.11"}, + {"node4.example.com", "NotReady", "compute", "1.11"}, +} + +table := tablewriter.NewWriter(os.Stdout) +table.SetHeader([]string{"Name", "Status", "Role", "Version"}) +table.SetAutoWrapText(false) +table.SetAutoFormatHeaders(true) +table.SetHeaderAlignment(ALIGN_LEFT) +table.SetAlignment(ALIGN_LEFT) +table.SetCenterSeparator("") +table.SetColumnSeparator("") +table.SetRowSeparator("") +table.SetHeaderLine(false) +table.SetBorder(false) +table.SetTablePadding("\t") // pad with tabs +table.SetNoWhiteSpace(true) +table.AppendBulk(data) // Add Bulk Data +table.Render() +``` + +##### Output 8 +``` +NAME STATUS ROLE VERSION +node1.example.com Ready compute 1.11 +node2.example.com Ready compute 1.11 +node3.example.com Ready compute 1.11 +node4.example.com NotReady compute 1.11 +``` + +#### Render table into a string + +Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the `strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example: + +```go +package main + +import ( + "strings" + "fmt" + + "github.com/olekukonko/tablewriter" +) + +func main() { + tableString := &strings.Builder{} + table := tablewriter.NewWriter(tableString) + + /* + * Code to fill the table + */ + + table.Render() + + fmt.Println(tableString.String()) +} +``` + +#### TODO +- ~~Import Directly from CSV~~ - `done` +- ~~Support for `SetFooter`~~ - `done` +- ~~Support for `SetBorder`~~ - `done` +- ~~Support table with uneven rows~~ - `done` +- ~~Support custom alignment~~ +- General Improvement & Optimisation +- `NewHTML` Parse table from HTML diff --git a/vendor/github.com/olekukonko/tablewriter/csv.go b/vendor/github.com/olekukonko/tablewriter/csv.go new file mode 100644 index 00000000..98878303 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/csv.go @@ -0,0 +1,52 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "encoding/csv" + "io" + "os" +) + +// Start A new table by importing from a CSV file +// Takes io.Writer and csv File name +func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) { + file, err := os.Open(fileName) + if err != nil { + return &Table{}, err + } + defer file.Close() + csvReader := csv.NewReader(file) + t, err := NewCSVReader(writer, csvReader, hasHeader) + return t, err +} + +// Start a New Table Writer with csv.Reader +// This enables customisation such as reader.Comma = ';' +// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94 +func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) { + t := NewWriter(writer) + if hasHeader { + // Read the first row + headers, err := csvReader.Read() + if err != nil { + return &Table{}, err + } + t.SetHeader(headers) + } + for { + record, err := csvReader.Read() + if err == io.EOF { + break + } else if err != nil { + return &Table{}, err + } + t.Append(record) + } + return t, nil +} diff --git a/vendor/github.com/olekukonko/tablewriter/go.mod b/vendor/github.com/olekukonko/tablewriter/go.mod new file mode 100644 index 00000000..0430d99b --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/go.mod @@ -0,0 +1,5 @@ +module github.com/olekukonko/tablewriter + +go 1.12 + +require github.com/mattn/go-runewidth v0.0.7 diff --git a/vendor/github.com/olekukonko/tablewriter/go.sum b/vendor/github.com/olekukonko/tablewriter/go.sum new file mode 100644 index 00000000..1e7b9aab --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/go.sum @@ -0,0 +1,2 @@ +github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go new file mode 100644 index 00000000..cf63eadf --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table.go @@ -0,0 +1,941 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +// Create & Generate text based table +package tablewriter + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strings" +) + +const ( + MAX_ROW_WIDTH = 30 +) + +const ( + CENTER = "+" + ROW = "-" + COLUMN = "|" + SPACE = " " + NEWLINE = "\n" +) + +const ( + ALIGN_DEFAULT = iota + ALIGN_CENTER + ALIGN_RIGHT + ALIGN_LEFT +) + +var ( + decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`) + percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`) +) + +type Border struct { + Left bool + Right bool + Top bool + Bottom bool +} + +type Table struct { + out io.Writer + rows [][]string + lines [][][]string + cs map[int]int + rs map[int]int + headers [][]string + footers [][]string + caption bool + captionText string + autoFmt bool + autoWrap bool + reflowText bool + mW int + pCenter string + pRow string + pColumn string + tColumn int + tRow int + hAlign int + fAlign int + align int + newLine string + rowLine bool + autoMergeCells bool + noWhiteSpace bool + tablePadding string + hdrLine bool + borders Border + colSize int + headerParams []string + columnsParams []string + footerParams []string + columnsAlign []int +} + +// Start New Table +// Take io.Writer Directly +func NewWriter(writer io.Writer) *Table { + t := &Table{ + out: writer, + rows: [][]string{}, + lines: [][][]string{}, + cs: make(map[int]int), + rs: make(map[int]int), + headers: [][]string{}, + footers: [][]string{}, + caption: false, + captionText: "Table caption.", + autoFmt: true, + autoWrap: true, + reflowText: true, + mW: MAX_ROW_WIDTH, + pCenter: CENTER, + pRow: ROW, + pColumn: COLUMN, + tColumn: -1, + tRow: -1, + hAlign: ALIGN_DEFAULT, + fAlign: ALIGN_DEFAULT, + align: ALIGN_DEFAULT, + newLine: NEWLINE, + rowLine: false, + hdrLine: true, + borders: Border{Left: true, Right: true, Bottom: true, Top: true}, + colSize: -1, + headerParams: []string{}, + columnsParams: []string{}, + footerParams: []string{}, + columnsAlign: []int{}} + return t +} + +// Render table output +func (t *Table) Render() { + if t.borders.Top { + t.printLine(true) + } + t.printHeading() + if t.autoMergeCells { + t.printRowsMergeCells() + } else { + t.printRows() + } + if !t.rowLine && t.borders.Bottom { + t.printLine(true) + } + t.printFooter() + + if t.caption { + t.printCaption() + } +} + +const ( + headerRowIdx = -1 + footerRowIdx = -2 +) + +// Set table header +func (t *Table) SetHeader(keys []string) { + t.colSize = len(keys) + for i, v := range keys { + lines := t.parseDimension(v, i, headerRowIdx) + t.headers = append(t.headers, lines) + } +} + +// Set table Footer +func (t *Table) SetFooter(keys []string) { + //t.colSize = len(keys) + for i, v := range keys { + lines := t.parseDimension(v, i, footerRowIdx) + t.footers = append(t.footers, lines) + } +} + +// Set table Caption +func (t *Table) SetCaption(caption bool, captionText ...string) { + t.caption = caption + if len(captionText) == 1 { + t.captionText = captionText[0] + } +} + +// Turn header autoformatting on/off. Default is on (true). +func (t *Table) SetAutoFormatHeaders(auto bool) { + t.autoFmt = auto +} + +// Turn automatic multiline text adjustment on/off. Default is on (true). +func (t *Table) SetAutoWrapText(auto bool) { + t.autoWrap = auto +} + +// Turn automatic reflowing of multiline text when rewrapping. Default is on (true). +func (t *Table) SetReflowDuringAutoWrap(auto bool) { + t.reflowText = auto +} + +// Set the Default column width +func (t *Table) SetColWidth(width int) { + t.mW = width +} + +// Set the minimal width for a column +func (t *Table) SetColMinWidth(column int, width int) { + t.cs[column] = width +} + +// Set the Column Separator +func (t *Table) SetColumnSeparator(sep string) { + t.pColumn = sep +} + +// Set the Row Separator +func (t *Table) SetRowSeparator(sep string) { + t.pRow = sep +} + +// Set the center Separator +func (t *Table) SetCenterSeparator(sep string) { + t.pCenter = sep +} + +// Set Header Alignment +func (t *Table) SetHeaderAlignment(hAlign int) { + t.hAlign = hAlign +} + +// Set Footer Alignment +func (t *Table) SetFooterAlignment(fAlign int) { + t.fAlign = fAlign +} + +// Set Table Alignment +func (t *Table) SetAlignment(align int) { + t.align = align +} + +// Set No White Space +func (t *Table) SetNoWhiteSpace(allow bool) { + t.noWhiteSpace = allow +} + +// Set Table Padding +func (t *Table) SetTablePadding(padding string) { + t.tablePadding = padding +} + +func (t *Table) SetColumnAlignment(keys []int) { + for _, v := range keys { + switch v { + case ALIGN_CENTER: + break + case ALIGN_LEFT: + break + case ALIGN_RIGHT: + break + default: + v = ALIGN_DEFAULT + } + t.columnsAlign = append(t.columnsAlign, v) + } +} + +// Set New Line +func (t *Table) SetNewLine(nl string) { + t.newLine = nl +} + +// Set Header Line +// This would enable / disable a line after the header +func (t *Table) SetHeaderLine(line bool) { + t.hdrLine = line +} + +// Set Row Line +// This would enable / disable a line on each row of the table +func (t *Table) SetRowLine(line bool) { + t.rowLine = line +} + +// Set Auto Merge Cells +// This would enable / disable the merge of cells with identical values +func (t *Table) SetAutoMergeCells(auto bool) { + t.autoMergeCells = auto +} + +// Set Table Border +// This would enable / disable line around the table +func (t *Table) SetBorder(border bool) { + t.SetBorders(Border{border, border, border, border}) +} + +func (t *Table) SetBorders(border Border) { + t.borders = border +} + +// Append row to table +func (t *Table) Append(row []string) { + rowSize := len(t.headers) + if rowSize > t.colSize { + t.colSize = rowSize + } + + n := len(t.lines) + line := [][]string{} + for i, v := range row { + + // Detect string width + // Detect String height + // Break strings into words + out := t.parseDimension(v, i, n) + + // Append broken words + line = append(line, out) + } + t.lines = append(t.lines, line) +} + +// Append row to table with color attributes +func (t *Table) Rich(row []string, colors []Colors) { + rowSize := len(t.headers) + if rowSize > t.colSize { + t.colSize = rowSize + } + + n := len(t.lines) + line := [][]string{} + for i, v := range row { + + // Detect string width + // Detect String height + // Break strings into words + out := t.parseDimension(v, i, n) + + if len(colors) > i { + color := colors[i] + out[0] = format(out[0], color) + } + + // Append broken words + line = append(line, out) + } + t.lines = append(t.lines, line) +} + +// Allow Support for Bulk Append +// Eliminates repeated for loops +func (t *Table) AppendBulk(rows [][]string) { + for _, row := range rows { + t.Append(row) + } +} + +// NumLines to get the number of lines +func (t *Table) NumLines() int { + return len(t.lines) +} + +// Clear rows +func (t *Table) ClearRows() { + t.lines = [][][]string{} +} + +// Clear footer +func (t *Table) ClearFooter() { + t.footers = [][]string{} +} + +// Center based on position and border. +func (t *Table) center(i int) string { + if i == -1 && !t.borders.Left { + return t.pRow + } + + if i == len(t.cs)-1 && !t.borders.Right { + return t.pRow + } + + return t.pCenter +} + +// Print line based on row width +func (t *Table) printLine(nl bool) { + fmt.Fprint(t.out, t.center(-1)) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.center(i)) + } + if nl { + fmt.Fprint(t.out, t.newLine) + } +} + +// Print line based on row width with our without cell separator +func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { + fmt.Fprint(t.out, t.pCenter) + for i := 0; i < len(t.cs); i++ { + v := t.cs[i] + if i > len(displayCellSeparator) || displayCellSeparator[i] { + // Display the cell separator + fmt.Fprintf(t.out, "%s%s%s%s", + t.pRow, + strings.Repeat(string(t.pRow), v), + t.pRow, + t.pCenter) + } else { + // Don't display the cell separator for this cell + fmt.Fprintf(t.out, "%s%s", + strings.Repeat(" ", v+2), + t.pCenter) + } + } + if nl { + fmt.Fprint(t.out, t.newLine) + } +} + +// Return the PadRight function if align is left, PadLeft if align is right, +// and Pad by default +func pad(align int) func(string, string, int) string { + padFunc := Pad + switch align { + case ALIGN_LEFT: + padFunc = PadRight + case ALIGN_RIGHT: + padFunc = PadLeft + } + return padFunc +} + +// Print heading information +func (t *Table) printHeading() { + // Check if headers is available + if len(t.headers) < 1 { + return + } + + // Identify last column + end := len(t.cs) - 1 + + // Get pad function + padFunc := pad(t.hAlign) + + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.headerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[headerRowIdx] + + // Print Heading + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + } + + for y := 0; y <= end; y++ { + v := t.cs[y] + h := "" + + if y < len(t.headers) && x < len(t.headers[y]) { + h = t.headers[y][x] + } + if t.autoFmt { + h = Title(h) + } + pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn) + if t.noWhiteSpace { + pad = ConditionString((y == end && !t.borders.Left), SPACE, t.tablePadding) + } + if is_esc_seq { + if !t.noWhiteSpace { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } else { + fmt.Fprintf(t.out, "%s %s", + format(padFunc(h, SPACE, v), + t.headerParams[y]), pad) + } + } else { + if !t.noWhiteSpace { + fmt.Fprintf(t.out, " %s %s", + padFunc(h, SPACE, v), + pad) + } else { + // the spaces between breaks the kube formatting + fmt.Fprintf(t.out, "%s%s", + padFunc(h, SPACE, v), + pad) + } + } + } + // Next line + fmt.Fprint(t.out, t.newLine) + } + if t.hdrLine { + t.printLine(true) + } +} + +// Print heading information +func (t *Table) printFooter() { + // Check if headers is available + if len(t.footers) < 1 { + return + } + + // Only print line if border is not set + if !t.borders.Bottom { + t.printLine(true) + } + + // Identify last column + end := len(t.cs) - 1 + + // Get pad function + padFunc := pad(t.fAlign) + + // Checking for ANSI escape sequences for header + is_esc_seq := false + if len(t.footerParams) > 0 { + is_esc_seq = true + } + + // Maximum height. + max := t.rs[footerRowIdx] + + // Print Footer + erasePad := make([]bool, len(t.footers)) + for x := 0; x < max; x++ { + // Check if border is set + // Replace with space if not set + fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) + + for y := 0; y <= end; y++ { + v := t.cs[y] + f := "" + if y < len(t.footers) && x < len(t.footers[y]) { + f = t.footers[y][x] + } + if t.autoFmt { + f = Title(f) + } + pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn) + + if erasePad[y] || (x == 0 && len(f) == 0) { + pad = SPACE + erasePad[y] = true + } + + if is_esc_seq { + fmt.Fprintf(t.out, " %s %s", + format(padFunc(f, SPACE, v), + t.footerParams[y]), pad) + } else { + fmt.Fprintf(t.out, " %s %s", + padFunc(f, SPACE, v), + pad) + } + + //fmt.Fprintf(t.out, " %s %s", + // padFunc(f, SPACE, v), + // pad) + } + // Next line + fmt.Fprint(t.out, t.newLine) + //t.printLine(true) + } + + hasPrinted := false + + for i := 0; i <= end; i++ { + v := t.cs[i] + pad := t.pRow + center := t.pCenter + length := len(t.footers[i][0]) + + if length > 0 { + hasPrinted = true + } + + // Set center to be space if length is 0 + if length == 0 && !t.borders.Right { + center = SPACE + } + + // Print first junction + if i == 0 { + if length > 0 && !t.borders.Left { + center = t.pRow + } + fmt.Fprint(t.out, center) + } + + // Pad With space of length is 0 + if length == 0 { + pad = SPACE + } + // Ignore left space as it has printed before + if hasPrinted || t.borders.Left { + pad = t.pRow + center = t.pCenter + } + + // Change Center end position + if center != SPACE { + if i == end && !t.borders.Right { + center = t.pRow + } + } + + // Change Center start position + if center == SPACE { + if i < end && len(t.footers[i+1][0]) != 0 { + if !t.borders.Left { + center = t.pRow + } else { + center = t.pCenter + } + } + } + + // Print the footer + fmt.Fprintf(t.out, "%s%s%s%s", + pad, + strings.Repeat(string(pad), v), + pad, + center) + + } + + fmt.Fprint(t.out, t.newLine) +} + +// Print caption text +func (t Table) printCaption() { + width := t.getTableWidth() + paragraph, _ := WrapString(t.captionText, width) + for linecount := 0; linecount < len(paragraph); linecount++ { + fmt.Fprintln(t.out, paragraph[linecount]) + } +} + +// Calculate the total number of characters in a row +func (t Table) getTableWidth() int { + var chars int + for _, v := range t.cs { + chars += v + } + + // Add chars, spaces, seperators to calculate the total width of the table. + // ncols := t.colSize + // spaces := ncols * 2 + // seps := ncols + 1 + + return (chars + (3 * t.colSize) + 2) +} + +func (t Table) printRows() { + for i, lines := range t.lines { + t.printRow(lines, i) + } +} + +func (t *Table) fillAlignment(num int) { + if len(t.columnsAlign) < num { + t.columnsAlign = make([]int, num) + for i := range t.columnsAlign { + t.columnsAlign[i] = t.align + } + } +} + +// Print Row Information +// Adjust column alignment based on type + +func (t *Table) printRow(columns [][]string, rowIdx int) { + // Get Maximum Height + max := t.rs[rowIdx] + total := len(columns) + + // TODO Fix uneven col size + // if total < t.colSize { + // for n := t.colSize - total; n < t.colSize ; n++ { + // columns = append(columns, []string{SPACE}) + // t.cs[n] = t.mW + // } + //} + + // Pad Each Height + pads := []int{} + + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + t.fillAlignment(total) + + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + //fmt.Println(max, "\n") + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) + fmt.Fprintf(t.out, SPACE) + } + + str := columns[y][x] + + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + + // This would print alignment + // Default alignment would use multiple configuration + switch t.columnsAlign[y] { + case ALIGN_CENTER: // + fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + + // TODO Custom alignment per column + //if max == 1 || pads[y] > 0 { + // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) + //} else { + // fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) + //} + + } + } + if !t.noWhiteSpace { + fmt.Fprintf(t.out, SPACE) + } else { + fmt.Fprintf(t.out, t.tablePadding) + } + } + // Check if border is set + // Replace with space if not set + if !t.noWhiteSpace { + fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) + } + fmt.Fprint(t.out, t.newLine) + } + + if t.rowLine { + t.printLine(true) + } +} + +// Print the rows of the table and merge the cells that are identical +func (t *Table) printRowsMergeCells() { + var previousLine []string + var displayCellBorder []bool + var tmpWriter bytes.Buffer + for i, lines := range t.lines { + // We store the display of the current line in a tmp writer, as we need to know which border needs to be print above + previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine) + if i > 0 { //We don't need to print borders above first line + if t.rowLine { + t.printLineOptionalCellSeparators(true, displayCellBorder) + } + } + tmpWriter.WriteTo(t.out) + } + //Print the end of the table + if t.rowLine { + t.printLine(true) + } +} + +// Print Row Information to a writer and merge identical cells. +// Adjust column alignment based on type + +func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) { + // Get Maximum Height + max := t.rs[rowIdx] + total := len(columns) + + // Pad Each Height + pads := []int{} + + // Checking for ANSI escape sequences for columns + is_esc_seq := false + if len(t.columnsParams) > 0 { + is_esc_seq = true + } + for i, line := range columns { + length := len(line) + pad := max - length + pads = append(pads, pad) + for n := 0; n < pad; n++ { + columns[i] = append(columns[i], " ") + } + } + + var displayCellBorder []bool + t.fillAlignment(total) + for x := 0; x < max; x++ { + for y := 0; y < total; y++ { + + // Check if border is set + fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) + + fmt.Fprintf(writer, SPACE) + + str := columns[y][x] + + // Embedding escape sequence with column value + if is_esc_seq { + str = format(str, t.columnsParams[y]) + } + + if t.autoMergeCells { + //Store the full line to merge mutli-lines cells + fullLine := strings.TrimRight(strings.Join(columns[y], " "), " ") + if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" { + // If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty. + displayCellBorder = append(displayCellBorder, false) + str = "" + } else { + // First line or different content, keep the content and print the cell border + displayCellBorder = append(displayCellBorder, true) + } + } + + // This would print alignment + // Default alignment would use multiple configuration + switch t.columnsAlign[y] { + case ALIGN_CENTER: // + fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y])) + case ALIGN_RIGHT: + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + case ALIGN_LEFT: + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + default: + if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { + fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) + } else { + fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) + } + } + fmt.Fprintf(writer, SPACE) + } + // Check if border is set + // Replace with space if not set + fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE)) + fmt.Fprint(writer, t.newLine) + } + + //The new previous line is the current one + previousLine = make([]string, total) + for y := 0; y < total; y++ { + previousLine[y] = strings.TrimRight(strings.Join(columns[y], " "), " ") //Store the full line for multi-lines cells + } + //Returns the newly added line and wether or not a border should be displayed above. + return previousLine, displayCellBorder +} + +func (t *Table) parseDimension(str string, colKey, rowKey int) []string { + var ( + raw []string + maxWidth int + ) + + raw = getLines(str) + maxWidth = 0 + for _, line := range raw { + if w := DisplayWidth(line); w > maxWidth { + maxWidth = w + } + } + + // If wrapping, ensure that all paragraphs in the cell fit in the + // specified width. + if t.autoWrap { + // If there's a maximum allowed width for wrapping, use that. + if maxWidth > t.mW { + maxWidth = t.mW + } + + // In the process of doing so, we need to recompute maxWidth. This + // is because perhaps a word in the cell is longer than the + // allowed maximum width in t.mW. + newMaxWidth := maxWidth + newRaw := make([]string, 0, len(raw)) + + if t.reflowText { + // Make a single paragraph of everything. + raw = []string{strings.Join(raw, " ")} + } + for i, para := range raw { + paraLines, _ := WrapString(para, maxWidth) + for _, line := range paraLines { + if w := DisplayWidth(line); w > newMaxWidth { + newMaxWidth = w + } + } + if i > 0 { + newRaw = append(newRaw, " ") + } + newRaw = append(newRaw, paraLines...) + } + raw = newRaw + maxWidth = newMaxWidth + } + + // Store the new known maximum width. + v, ok := t.cs[colKey] + if !ok || v < maxWidth || v == 0 { + t.cs[colKey] = maxWidth + } + + // Remember the number of lines for the row printer. + h := len(raw) + v, ok = t.rs[rowKey] + + if !ok || v < h || v == 0 { + t.rs[rowKey] = h + } + //fmt.Printf("Raw %+v %d\n", raw, len(raw)) + return raw +} diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go new file mode 100644 index 00000000..ae7a364a --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/table_with_color.go @@ -0,0 +1,136 @@ +package tablewriter + +import ( + "fmt" + "strconv" + "strings" +) + +const ESC = "\033" +const SEP = ";" + +const ( + BgBlackColor int = iota + 40 + BgRedColor + BgGreenColor + BgYellowColor + BgBlueColor + BgMagentaColor + BgCyanColor + BgWhiteColor +) + +const ( + FgBlackColor int = iota + 30 + FgRedColor + FgGreenColor + FgYellowColor + FgBlueColor + FgMagentaColor + FgCyanColor + FgWhiteColor +) + +const ( + BgHiBlackColor int = iota + 100 + BgHiRedColor + BgHiGreenColor + BgHiYellowColor + BgHiBlueColor + BgHiMagentaColor + BgHiCyanColor + BgHiWhiteColor +) + +const ( + FgHiBlackColor int = iota + 90 + FgHiRedColor + FgHiGreenColor + FgHiYellowColor + FgHiBlueColor + FgHiMagentaColor + FgHiCyanColor + FgHiWhiteColor +) + +const ( + Normal = 0 + Bold = 1 + UnderlineSingle = 4 + Italic +) + +type Colors []int + +func startFormat(seq string) string { + return fmt.Sprintf("%s[%sm", ESC, seq) +} + +func stopFormat() string { + return fmt.Sprintf("%s[%dm", ESC, Normal) +} + +// Making the SGR (Select Graphic Rendition) sequence. +func makeSequence(codes []int) string { + codesInString := []string{} + for _, code := range codes { + codesInString = append(codesInString, strconv.Itoa(code)) + } + return strings.Join(codesInString, SEP) +} + +// Adding ANSI escape sequences before and after string +func format(s string, codes interface{}) string { + var seq string + + switch v := codes.(type) { + + case string: + seq = v + case []int: + seq = makeSequence(v) + case Colors: + seq = makeSequence(v) + default: + return s + } + + if len(seq) == 0 { + return s + } + return startFormat(seq) + s + stopFormat() +} + +// Adding header colors (ANSI codes) +func (t *Table) SetHeaderColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of header colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.headerParams = append(t.headerParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetColumnColor(colors ...Colors) { + if t.colSize != len(colors) { + panic("Number of column colors must be equal to number of headers.") + } + for i := 0; i < len(colors); i++ { + t.columnsParams = append(t.columnsParams, makeSequence(colors[i])) + } +} + +// Adding column colors (ANSI codes) +func (t *Table) SetFooterColor(colors ...Colors) { + if len(t.footers) != len(colors) { + panic("Number of footer colors must be equal to number of footer.") + } + for i := 0; i < len(colors); i++ { + t.footerParams = append(t.footerParams, makeSequence(colors[i])) + } +} + +func Color(colors ...int) []int { + return colors +} diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go new file mode 100644 index 00000000..380e7ab3 --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/util.go @@ -0,0 +1,93 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "regexp" + "strings" + + "github.com/mattn/go-runewidth" +) + +var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]") + +func DisplayWidth(str string) int { + return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, "")) +} + +// Simple Condition for string +// Returns value based on condition +func ConditionString(cond bool, valid, inValid string) string { + if cond { + return valid + } + return inValid +} + +func isNumOrSpace(r rune) bool { + return ('0' <= r && r <= '9') || r == ' ' +} + +// Format Table Header +// Replace _ , . and spaces +func Title(name string) string { + origLen := len(name) + rs := []rune(name) + for i, r := range rs { + switch r { + case '_': + rs[i] = ' ' + case '.': + // ignore floating number 0.0 + if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) { + rs[i] = ' ' + } + } + } + name = string(rs) + name = strings.TrimSpace(name) + if len(name) == 0 && origLen > 0 { + // Keep at least one character. This is important to preserve + // empty lines in multi-line headers/footers. + name = " " + } + return strings.ToUpper(name) +} + +// Pad String +// Attempts to place string in the center +func Pad(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + gapLeft := int(math.Ceil(float64(gap / 2))) + gapRight := gap - gapLeft + return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight) + } + return s +} + +// Pad String Right position +// This would place string at the left side of the screen +func PadRight(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return s + strings.Repeat(string(pad), gap) + } + return s +} + +// Pad String Left position +// This would place string at the right side of the screen +func PadLeft(s, pad string, width int) string { + gap := width - DisplayWidth(s) + if gap > 0 { + return strings.Repeat(string(pad), gap) + s + } + return s +} diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go new file mode 100644 index 00000000..a092ee1f --- /dev/null +++ b/vendor/github.com/olekukonko/tablewriter/wrap.go @@ -0,0 +1,99 @@ +// Copyright 2014 Oleku Konko All rights reserved. +// Use of this source code is governed by a MIT +// license that can be found in the LICENSE file. + +// This module is a Table Writer API for the Go Programming Language. +// The protocols were written in pure Go and works on windows and unix systems + +package tablewriter + +import ( + "math" + "strings" + + "github.com/mattn/go-runewidth" +) + +var ( + nl = "\n" + sp = " " +) + +const defaultPenalty = 1e5 + +// Wrap wraps s into a paragraph of lines of length lim, with minimal +// raggedness. +func WrapString(s string, lim int) ([]string, int) { + words := strings.Split(strings.Replace(s, nl, sp, -1), sp) + var lines []string + max := 0 + for _, v := range words { + max = runewidth.StringWidth(v) + if max > lim { + lim = max + } + } + for _, line := range WrapWords(words, 1, lim, defaultPenalty) { + lines = append(lines, strings.Join(line, sp)) + } + return lines, lim +} + +// WrapWords is the low-level line-breaking algorithm, useful if you need more +// control over the details of the text wrapping process. For most uses, +// WrapString will be sufficient and more convenient. +// +// WrapWords splits a list of words into lines with minimal "raggedness", +// treating each rune as one unit, accounting for spc units between adjacent +// words on each line, and attempting to limit lines to lim units. Raggedness +// is the total error over all lines, where error is the square of the +// difference of the length of the line and lim. Too-long lines (which only +// happen when a single word is longer than lim units) have pen penalty units +// added to the error. +func WrapWords(words []string, spc, lim, pen int) [][]string { + n := len(words) + + length := make([][]int, n) + for i := 0; i < n; i++ { + length[i] = make([]int, n) + length[i][i] = runewidth.StringWidth(words[i]) + for j := i + 1; j < n; j++ { + length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j]) + } + } + nbrk := make([]int, n) + cost := make([]int, n) + for i := range cost { + cost[i] = math.MaxInt32 + } + for i := n - 1; i >= 0; i-- { + if length[i][n-1] <= lim { + cost[i] = 0 + nbrk[i] = n + } else { + for j := i + 1; j < n; j++ { + d := lim - length[i][j-1] + c := d*d + cost[j] + if length[i][j-1] > lim { + c += pen // too-long lines get a worse penalty + } + if c < cost[i] { + cost[i] = c + nbrk[i] = j + } + } + } + } + var lines [][]string + i := 0 + for i < n { + lines = append(lines, words[i:nbrk[i]]) + i = nbrk[i] + } + return lines +} + +// getLines decomposes a multiline string into a slice of strings. +func getLines(s string) []string { + return strings.Split(s, nl) +} diff --git a/vendor/github.com/russross/blackfriday/v2/.gitignore b/vendor/github.com/russross/blackfriday/v2/.gitignore new file mode 100644 index 00000000..75623dcc --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/.gitignore @@ -0,0 +1,8 @@ +*.out +*.swp +*.8 +*.6 +_obj +_test* +markdown +tags diff --git a/vendor/github.com/russross/blackfriday/v2/.travis.yml b/vendor/github.com/russross/blackfriday/v2/.travis.yml new file mode 100644 index 00000000..b0b525a5 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/.travis.yml @@ -0,0 +1,17 @@ +sudo: false +language: go +go: + - "1.10.x" + - "1.11.x" + - tip +matrix: + fast_finish: true + allow_failures: + - go: tip +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v ./... diff --git a/vendor/github.com/russross/blackfriday/v2/LICENSE.txt b/vendor/github.com/russross/blackfriday/v2/LICENSE.txt new file mode 100644 index 00000000..2885af36 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/LICENSE.txt @@ -0,0 +1,29 @@ +Blackfriday is distributed under the Simplified BSD License: + +> Copyright © 2011 Russ Ross +> All rights reserved. +> +> Redistribution and use in source and binary forms, with or without +> modification, are permitted provided that the following conditions +> are met: +> +> 1. Redistributions of source code must retain the above copyright +> notice, this list of conditions and the following disclaimer. +> +> 2. Redistributions in binary form must reproduce the above +> copyright notice, this list of conditions and the following +> disclaimer in the documentation and/or other materials provided with +> the distribution. +> +> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS +> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE +> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN +> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/russross/blackfriday/v2/README.md b/vendor/github.com/russross/blackfriday/v2/README.md new file mode 100644 index 00000000..d5a8649b --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/README.md @@ -0,0 +1,291 @@ +Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) +=========== + +Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It +is paranoid about its input (so you can safely feed it user-supplied +data), it is fast, it supports common extensions (tables, smart +punctuation substitutions, etc.), and it is safe for all utf-8 +(unicode) input. + +HTML output is currently supported, along with Smartypants +extensions. + +It started as a translation from C of [Sundown][3]. + + +Installation +------------ + +Blackfriday is compatible with any modern Go release. With Go 1.7 and git +installed: + + go get gopkg.in/russross/blackfriday.v2 + +will download, compile, and install the package into your `$GOPATH` +directory hierarchy. Alternatively, you can achieve the same if you +import it into a project: + + import "gopkg.in/russross/blackfriday.v2" + +and `go get` without parameters. + + +Versions +-------- + +Currently maintained and recommended version of Blackfriday is `v2`. It's being +developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the +documentation is available at +https://godoc.org/gopkg.in/russross/blackfriday.v2. + +It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, +but we highly recommend using package management tool like [dep][7] or +[Glide][8] and make use of semantic versioning. With package management you +should import `github.com/russross/blackfriday` and specify that you're using +version 2.0.0. + +Version 2 offers a number of improvements over v1: + +* Cleaned up API +* A separate call to [`Parse`][4], which produces an abstract syntax tree for + the document +* Latest bug fixes +* Flexibility to easily add your own rendering extensions + +Potential drawbacks: + +* Our benchmarks show v2 to be slightly slower than v1. Currently in the + ballpark of around 15%. +* API breakage. If you can't afford modifying your code to adhere to the new API + and don't care too much about the new features, v2 is probably not for you. +* Several bug fixes are trailing behind and still need to be forward-ported to + v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for + tracking. + +Usage +----- + +For the most sensible markdown processing, it is as simple as getting your input +into a byte slice and calling: + +```go +output := blackfriday.Run(input) +``` + +Your input will be parsed and the output rendered with a set of most popular +extensions enabled. If you want the most basic feature set, corresponding with +the bare Markdown specification, use: + +```go +output := blackfriday.Run(input, blackfriday.WithNoExtensions()) +``` + +### Sanitize untrusted content + +Blackfriday itself does nothing to protect against malicious content. If you are +dealing with user-supplied markdown, we recommend running Blackfriday's output +through HTML sanitizer such as [Bluemonday][5]. + +Here's an example of simple usage of Blackfriday together with Bluemonday: + +```go +import ( + "github.com/microcosm-cc/bluemonday" + "github.com/russross/blackfriday" +) + +// ... +unsafe := blackfriday.Run(input) +html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) +``` + +### Custom options + +If you want to customize the set of options, use `blackfriday.WithExtensions`, +`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. + +You can also check out `blackfriday-tool` for a more complete example +of how to use it. Download and install it using: + + go get github.com/russross/blackfriday-tool + +This is a simple command-line tool that allows you to process a +markdown file using a standalone program. You can also browse the +source directly on github if you are just looking for some example +code: + +* + +Note that if you have not already done so, installing +`blackfriday-tool` will be sufficient to download and install +blackfriday in addition to the tool itself. The tool binary will be +installed in `$GOPATH/bin`. This is a statically-linked binary that +can be copied to wherever you need it without worrying about +dependencies and library versions. + + +Features +-------- + +All features of Sundown are supported, including: + +* **Compatibility**. The Markdown v1.0.3 test suite passes with + the `--tidy` option. Without `--tidy`, the differences are + mostly in whitespace and entity escaping, where blackfriday is + more consistent and cleaner. + +* **Common extensions**, including table support, fenced code + blocks, autolinks, strikethroughs, non-strict emphasis, etc. + +* **Safety**. Blackfriday is paranoid when parsing, making it safe + to feed untrusted user input without fear of bad things + happening. The test suite stress tests this and there are no + known inputs that make it crash. If you find one, please let me + know and send me the input that does it. + + NOTE: "safety" in this context means *runtime safety only*. In order to + protect yourself against JavaScript injection in untrusted content, see + [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). + +* **Fast processing**. It is fast enough to render on-demand in + most web applications without having to cache the output. + +* **Thread safety**. You can run multiple parsers in different + goroutines without ill effect. There is no dependence on global + shared state. + +* **Minimal dependencies**. Blackfriday only depends on standard + library packages in Go. The source code is pretty + self-contained, so it is easy to add to any project, including + Google App Engine projects. + +* **Standards compliant**. Output successfully validates using the + W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. + + +Extensions +---------- + +In addition to the standard markdown syntax, this package +implements the following extensions: + +* **Intra-word emphasis supression**. The `_` character is + commonly used inside words when discussing code, so having + markdown interpret it as an emphasis command is usually the + wrong thing. Blackfriday lets you treat all emphasis markers as + normal characters when they occur inside a word. + +* **Tables**. Tables can be created by drawing them in the input + using a simple syntax: + + ``` + Name | Age + --------|------ + Bob | 27 + Alice | 23 + ``` + +* **Fenced code blocks**. In addition to the normal 4-space + indentation to mark code blocks, you can explicitly mark them + and supply a language (to make syntax highlighting simple). Just + mark it like this: + + ```go + func getTrue() bool { + return true + } + ``` + + You can use 3 or more backticks to mark the beginning of the + block, and the same number to mark the end of the block. + +* **Definition lists**. A simple definition list is made of a single-line + term followed by a colon and the definition for that term. + + Cat + : Fluffy animal everyone likes + + Internet + : Vector of transmission for pictures of cats + + Terms must be separated from the previous definition by a blank line. + +* **Footnotes**. A marker in the text that will become a superscript number; + a footnote definition that will be placed in a list of footnotes at the + end of the document. A footnote looks like this: + + This is a footnote.[^1] + + [^1]: the footnote text. + +* **Autolinking**. Blackfriday can find URLs that have not been + explicitly marked as links and turn them into links. + +* **Strikethrough**. Use two tildes (`~~`) to mark text that + should be crossed out. + +* **Hard line breaks**. With this extension enabled newlines in the input + translate into line breaks in the output. This extension is off by default. + +* **Smart quotes**. Smartypants-style punctuation substitution is + supported, turning normal double- and single-quote marks into + curly quotes, etc. + +* **LaTeX-style dash parsing** is an additional option, where `--` + is translated into `–`, and `---` is translated into + `—`. This differs from most smartypants processors, which + turn a single hyphen into an ndash and a double hyphen into an + mdash. + +* **Smart fractions**, where anything that looks like a fraction + is translated into suitable HTML (instead of just a few special + cases like most smartypant processors). For example, `4/5` + becomes `45`, which renders as + 45. + + +Other renderers +--------------- + +Blackfriday is structured to allow alternative rendering engines. Here +are a few of note: + +* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): + provides a GitHub Flavored Markdown renderer with fenced code block + highlighting, clickable heading anchor links. + + It's not customizable, and its goal is to produce HTML output + equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), + except the rendering is performed locally. + +* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, + but for markdown. + +* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX): + renders output as LaTeX. + +* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. + + +Todo +---- + +* More unit testing +* Improve unicode support. It does not understand all unicode + rules (about what constitutes a letter, a punctuation symbol, + etc.), so it may fail to detect word boundaries correctly in + some instances. It is safe on all utf-8 input. + + +License +------- + +[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) + + + [1]: https://daringfireball.net/projects/markdown/ "Markdown" + [2]: https://golang.org/ "Go Language" + [3]: https://github.com/vmg/sundown "Sundown" + [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" + [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" + [6]: https://labix.org/gopkg.in "gopkg.in" diff --git a/vendor/github.com/russross/blackfriday/v2/block.go b/vendor/github.com/russross/blackfriday/v2/block.go new file mode 100644 index 00000000..b8607474 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/block.go @@ -0,0 +1,1590 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse block-level elements. +// + +package blackfriday + +import ( + "bytes" + "html" + "regexp" + "strings" + + "github.com/shurcooL/sanitized_anchor_name" +) + +const ( + charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" + escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" +) + +var ( + reBackslashOrAmp = regexp.MustCompile("[\\&]") + reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) +) + +// Parse block-level data. +// Note: this function and many that it calls assume that +// the input buffer ends with a newline. +func (p *Markdown) block(data []byte) { + // this is called recursively: enforce a maximum depth + if p.nesting >= p.maxNesting { + return + } + p.nesting++ + + // parse out one block-level construct at a time + for len(data) > 0 { + // prefixed heading: + // + // # Heading 1 + // ## Heading 2 + // ... + // ###### Heading 6 + if p.isPrefixHeading(data) { + data = data[p.prefixHeading(data):] + continue + } + + // block of preformatted HTML: + // + //
+ // ... + //
+ if data[0] == '<' { + if i := p.html(data, true); i > 0 { + data = data[i:] + continue + } + } + + // title block + // + // % stuff + // % more stuff + // % even more stuff + if p.extensions&Titleblock != 0 { + if data[0] == '%' { + if i := p.titleBlock(data, true); i > 0 { + data = data[i:] + continue + } + } + } + + // blank lines. note: returns the # of bytes to skip + if i := p.isEmpty(data); i > 0 { + data = data[i:] + continue + } + + // indented code block: + // + // func max(a, b int) int { + // if a > b { + // return a + // } + // return b + // } + if p.codePrefix(data) > 0 { + data = data[p.code(data):] + continue + } + + // fenced code block: + // + // ``` go + // func fact(n int) int { + // if n <= 1 { + // return n + // } + // return n * fact(n-1) + // } + // ``` + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data, true); i > 0 { + data = data[i:] + continue + } + } + + // horizontal rule: + // + // ------ + // or + // ****** + // or + // ______ + if p.isHRule(data) { + p.addBlock(HorizontalRule, nil) + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + } + data = data[i:] + continue + } + + // block quote: + // + // > A big quote I found somewhere + // > on the web + if p.quotePrefix(data) > 0 { + data = data[p.quote(data):] + continue + } + + // table: + // + // Name | Age | Phone + // ------|-----|--------- + // Bob | 31 | 555-1234 + // Alice | 27 | 555-4321 + if p.extensions&Tables != 0 { + if i := p.table(data); i > 0 { + data = data[i:] + continue + } + } + + // an itemized/unordered list: + // + // * Item 1 + // * Item 2 + // + // also works with + or - + if p.uliPrefix(data) > 0 { + data = data[p.list(data, 0):] + continue + } + + // a numbered/ordered list: + // + // 1. Item 1 + // 2. Item 2 + if p.oliPrefix(data) > 0 { + data = data[p.list(data, ListTypeOrdered):] + continue + } + + // definition lists: + // + // Term 1 + // : Definition a + // : Definition b + // + // Term 2 + // : Definition c + if p.extensions&DefinitionLists != 0 { + if p.dliPrefix(data) > 0 { + data = data[p.list(data, ListTypeDefinition):] + continue + } + } + + // anything else must look like a normal paragraph + // note: this finds underlined headings, too + data = data[p.paragraph(data):] + } + + p.nesting-- +} + +func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { + p.closeUnmatchedBlocks() + container := p.addChild(typ, 0) + container.content = content + return container +} + +func (p *Markdown) isPrefixHeading(data []byte) bool { + if data[0] != '#' { + return false + } + + if p.extensions&SpaceHeadings != 0 { + level := 0 + for level < 6 && level < len(data) && data[level] == '#' { + level++ + } + if level == len(data) || data[level] != ' ' { + return false + } + } + return true +} + +func (p *Markdown) prefixHeading(data []byte) int { + level := 0 + for level < 6 && level < len(data) && data[level] == '#' { + level++ + } + i := skipChar(data, level, ' ') + end := skipUntilChar(data, i, '\n') + skip := end + id := "" + if p.extensions&HeadingIDs != 0 { + j, k := 0, 0 + // find start/end of heading id + for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { + } + for k = j + 1; k < end && data[k] != '}'; k++ { + } + // extract heading id iff found + if j < end && k < end { + id = string(data[j+2 : k]) + end = j + skip = k + 1 + for end > 0 && data[end-1] == ' ' { + end-- + } + } + } + for end > 0 && data[end-1] == '#' { + if isBackslashEscaped(data, end-1) { + break + } + end-- + } + for end > 0 && data[end-1] == ' ' { + end-- + } + if end > i { + if id == "" && p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[i:end])) + } + block := p.addBlock(Heading, data[i:end]) + block.HeadingID = id + block.Level = level + } + return skip +} + +func (p *Markdown) isUnderlinedHeading(data []byte) int { + // test of level 1 heading + if data[0] == '=' { + i := skipChar(data, 1, '=') + i = skipChar(data, i, ' ') + if i < len(data) && data[i] == '\n' { + return 1 + } + return 0 + } + + // test of level 2 heading + if data[0] == '-' { + i := skipChar(data, 1, '-') + i = skipChar(data, i, ' ') + if i < len(data) && data[i] == '\n' { + return 2 + } + return 0 + } + + return 0 +} + +func (p *Markdown) titleBlock(data []byte, doRender bool) int { + if data[0] != '%' { + return 0 + } + splitData := bytes.Split(data, []byte("\n")) + var i int + for idx, b := range splitData { + if !bytes.HasPrefix(b, []byte("%")) { + i = idx // - 1 + break + } + } + + data = bytes.Join(splitData[0:i], []byte("\n")) + consumed := len(data) + data = bytes.TrimPrefix(data, []byte("% ")) + data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) + block := p.addBlock(Heading, data) + block.Level = 1 + block.IsTitleblock = true + + return consumed +} + +func (p *Markdown) html(data []byte, doRender bool) int { + var i, j int + + // identify the opening tag + if data[0] != '<' { + return 0 + } + curtag, tagfound := p.htmlFindTag(data[1:]) + + // handle special cases + if !tagfound { + // check for an HTML comment + if size := p.htmlComment(data, doRender); size > 0 { + return size + } + + // check for an
tag + if size := p.htmlHr(data, doRender); size > 0 { + return size + } + + // no special case recognized + return 0 + } + + // look for an unindented matching closing tag + // followed by a blank line + found := false + /* + closetag := []byte("\n") + j = len(curtag) + 1 + for !found { + // scan for a closing tag at the beginning of a line + if skip := bytes.Index(data[j:], closetag); skip >= 0 { + j += skip + len(closetag) + } else { + break + } + + // see if it is the only thing on the line + if skip := p.isEmpty(data[j:]); skip > 0 { + // see if it is followed by a blank line/eof + j += skip + if j >= len(data) { + found = true + i = j + } else { + if skip := p.isEmpty(data[j:]); skip > 0 { + j += skip + found = true + i = j + } + } + } + } + */ + + // if not found, try a second pass looking for indented match + // but not if tag is "ins" or "del" (following original Markdown.pl) + if !found && curtag != "ins" && curtag != "del" { + i = 1 + for i < len(data) { + i++ + for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { + i++ + } + + if i+2+len(curtag) >= len(data) { + break + } + + j = p.htmlFindEnd(curtag, data[i-1:]) + + if j > 0 { + i += j - 1 + found = true + break + } + } + } + + if !found { + return 0 + } + + // the end of the block has been found + if doRender { + // trim newlines + end := i + for end > 0 && data[end-1] == '\n' { + end-- + } + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + } + + return i +} + +func finalizeHTMLBlock(block *Node) { + block.Literal = block.content + block.content = nil +} + +// HTML comment, lax form +func (p *Markdown) htmlComment(data []byte, doRender bool) int { + i := p.inlineHTMLComment(data) + // needs to end with a blank line + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim trailing newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + block := p.addBlock(HTMLBlock, data[:end]) + finalizeHTMLBlock(block) + } + return size + } + return 0 +} + +// HR, which is the only self-closing block tag considered +func (p *Markdown) htmlHr(data []byte, doRender bool) int { + if len(data) < 4 { + return 0 + } + if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { + return 0 + } + if data[3] != ' ' && data[3] != '/' && data[3] != '>' { + // not an
tag after all; at least not a valid one + return 0 + } + i := 3 + for i < len(data) && data[i] != '>' && data[i] != '\n' { + i++ + } + if i < len(data) && data[i] == '>' { + i++ + if j := p.isEmpty(data[i:]); j > 0 { + size := i + j + if doRender { + // trim newlines + end := size + for end > 0 && data[end-1] == '\n' { + end-- + } + finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) + } + return size + } + } + return 0 +} + +func (p *Markdown) htmlFindTag(data []byte) (string, bool) { + i := 0 + for i < len(data) && isalnum(data[i]) { + i++ + } + key := string(data[:i]) + if _, ok := blockTags[key]; ok { + return key, true + } + return "", false +} + +func (p *Markdown) htmlFindEnd(tag string, data []byte) int { + // assume data[0] == '<' && data[1] == '/' already tested + if tag == "hr" { + return 2 + } + // check if tag is a match + closetag := []byte("") + if !bytes.HasPrefix(data, closetag) { + return 0 + } + i := len(closetag) + + // check that the rest of the line is blank + skip := 0 + if skip = p.isEmpty(data[i:]); skip == 0 { + return 0 + } + i += skip + skip = 0 + + if i >= len(data) { + return i + } + + if p.extensions&LaxHTMLBlocks != 0 { + return i + } + if skip = p.isEmpty(data[i:]); skip == 0 { + // following line must be blank + return 0 + } + + return i + skip +} + +func (*Markdown) isEmpty(data []byte) int { + // it is okay to call isEmpty on an empty buffer + if len(data) == 0 { + return 0 + } + + var i int + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] != ' ' && data[i] != '\t' { + return 0 + } + } + if i < len(data) && data[i] == '\n' { + i++ + } + return i +} + +func (*Markdown) isHRule(data []byte) bool { + i := 0 + + // skip up to three spaces + for i < 3 && data[i] == ' ' { + i++ + } + + // look at the hrule char + if data[i] != '*' && data[i] != '-' && data[i] != '_' { + return false + } + c := data[i] + + // the whole line must be the char or whitespace + n := 0 + for i < len(data) && data[i] != '\n' { + switch { + case data[i] == c: + n++ + case data[i] != ' ': + return false + } + i++ + } + + return n >= 3 +} + +// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, +// and returns the end index if so, or 0 otherwise. It also returns the marker found. +// If info is not nil, it gets set to the syntax specified in the fence line. +func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { + i, size := 0, 0 + + // skip up to three spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + + // check for the marker characters: ~ or ` + if i >= len(data) { + return 0, "" + } + if data[i] != '~' && data[i] != '`' { + return 0, "" + } + + c := data[i] + + // the whole line must be the same char or whitespace + for i < len(data) && data[i] == c { + size++ + i++ + } + + // the marker char must occur at least 3 times + if size < 3 { + return 0, "" + } + marker = string(data[i-size : i]) + + // if this is the end marker, it must match the beginning marker + if oldmarker != "" && marker != oldmarker { + return 0, "" + } + + // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here + // into one, always get the info string, and discard it if the caller doesn't care. + if info != nil { + infoLength := 0 + i = skipChar(data, i, ' ') + + if i >= len(data) { + if i == len(data) { + return i, marker + } + return 0, "" + } + + infoStart := i + + if data[i] == '{' { + i++ + infoStart++ + + for i < len(data) && data[i] != '}' && data[i] != '\n' { + infoLength++ + i++ + } + + if i >= len(data) || data[i] != '}' { + return 0, "" + } + + // strip all whitespace at the beginning and the end + // of the {} block + for infoLength > 0 && isspace(data[infoStart]) { + infoStart++ + infoLength-- + } + + for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { + infoLength-- + } + i++ + i = skipChar(data, i, ' ') + } else { + for i < len(data) && !isverticalspace(data[i]) { + infoLength++ + i++ + } + } + + *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) + } + + if i == len(data) { + return i, marker + } + if i > len(data) || data[i] != '\n' { + return 0, "" + } + return i + 1, marker // Take newline into account. +} + +// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, +// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. +// If doRender is true, a final newline is mandatory to recognize the fenced code block. +func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { + var info string + beg, marker := isFenceLine(data, &info, "") + if beg == 0 || beg >= len(data) { + return 0 + } + + var work bytes.Buffer + work.Write([]byte(info)) + work.WriteByte('\n') + + for { + // safe to assume beg < len(data) + + // check for the end of the code block + fenceEnd, _ := isFenceLine(data[beg:], nil, marker) + if fenceEnd != 0 { + beg += fenceEnd + break + } + + // copy the current line + end := skipUntilChar(data, beg, '\n') + 1 + + // did we reach the end of the buffer without a closing marker? + if end >= len(data) { + return 0 + } + + // verbatim copy to the working buffer + if doRender { + work.Write(data[beg:end]) + } + beg = end + } + + if doRender { + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = true + finalizeCodeBlock(block) + } + + return beg +} + +func unescapeChar(str []byte) []byte { + if str[0] == '\\' { + return []byte{str[1]} + } + return []byte(html.UnescapeString(string(str))) +} + +func unescapeString(str []byte) []byte { + if reBackslashOrAmp.Match(str) { + return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) + } + return str +} + +func finalizeCodeBlock(block *Node) { + if block.IsFenced { + newlinePos := bytes.IndexByte(block.content, '\n') + firstLine := block.content[:newlinePos] + rest := block.content[newlinePos+1:] + block.Info = unescapeString(bytes.Trim(firstLine, "\n")) + block.Literal = rest + } else { + block.Literal = block.content + } + block.content = nil +} + +func (p *Markdown) table(data []byte) int { + table := p.addBlock(Table, nil) + i, columns := p.tableHeader(data) + if i == 0 { + p.tip = table.Parent + table.Unlink() + return 0 + } + + p.addBlock(TableBody, nil) + + for i < len(data) { + pipes, rowStart := 0, i + for ; i < len(data) && data[i] != '\n'; i++ { + if data[i] == '|' { + pipes++ + } + } + + if pipes == 0 { + i = rowStart + break + } + + // include the newline in data sent to tableRow + if i < len(data) && data[i] == '\n' { + i++ + } + p.tableRow(data[rowStart:i], columns, false) + } + + return i +} + +// check if the specified position is preceded by an odd number of backslashes +func isBackslashEscaped(data []byte, i int) bool { + backslashes := 0 + for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { + backslashes++ + } + return backslashes&1 == 1 +} + +func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { + i := 0 + colCount := 1 + for i = 0; i < len(data) && data[i] != '\n'; i++ { + if data[i] == '|' && !isBackslashEscaped(data, i) { + colCount++ + } + } + + // doesn't look like a table header + if colCount == 1 { + return + } + + // include the newline in the data sent to tableRow + j := i + if j < len(data) && data[j] == '\n' { + j++ + } + header := data[:j] + + // column count ignores pipes at beginning or end of line + if data[0] == '|' { + colCount-- + } + if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { + colCount-- + } + + columns = make([]CellAlignFlags, colCount) + + // move on to the header underline + i++ + if i >= len(data) { + return + } + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + i = skipChar(data, i, ' ') + + // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 + // and trailing | optional on last column + col := 0 + for i < len(data) && data[i] != '\n' { + dashes := 0 + + if data[i] == ':' { + i++ + columns[col] |= TableAlignmentLeft + dashes++ + } + for i < len(data) && data[i] == '-' { + i++ + dashes++ + } + if i < len(data) && data[i] == ':' { + i++ + columns[col] |= TableAlignmentRight + dashes++ + } + for i < len(data) && data[i] == ' ' { + i++ + } + if i == len(data) { + return + } + // end of column test is messy + switch { + case dashes < 3: + // not a valid column + return + + case data[i] == '|' && !isBackslashEscaped(data, i): + // marker found, now skip past trailing whitespace + col++ + i++ + for i < len(data) && data[i] == ' ' { + i++ + } + + // trailing junk found after last column + if col >= colCount && i < len(data) && data[i] != '\n' { + return + } + + case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: + // something else found where marker was required + return + + case data[i] == '\n': + // marker is optional for the last column + col++ + + default: + // trailing junk found after last column + return + } + } + if col != colCount { + return + } + + p.addBlock(TableHead, nil) + p.tableRow(header, columns, true) + size = i + if size < len(data) && data[size] == '\n' { + size++ + } + return +} + +func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { + p.addBlock(TableRow, nil) + i, col := 0, 0 + + if data[i] == '|' && !isBackslashEscaped(data, i) { + i++ + } + + for col = 0; col < len(columns) && i < len(data); col++ { + for i < len(data) && data[i] == ' ' { + i++ + } + + cellStart := i + + for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { + i++ + } + + cellEnd := i + + // skip the end-of-cell marker, possibly taking us past end of buffer + i++ + + for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { + cellEnd-- + } + + cell := p.addBlock(TableCell, data[cellStart:cellEnd]) + cell.IsHeader = header + cell.Align = columns[col] + } + + // pad it out with empty columns to get the right number + for ; col < len(columns); col++ { + cell := p.addBlock(TableCell, nil) + cell.IsHeader = header + cell.Align = columns[col] + } + + // silently ignore rows with too many cells +} + +// returns blockquote prefix length +func (p *Markdown) quotePrefix(data []byte) int { + i := 0 + for i < 3 && i < len(data) && data[i] == ' ' { + i++ + } + if i < len(data) && data[i] == '>' { + if i+1 < len(data) && data[i+1] == ' ' { + return i + 2 + } + return i + 1 + } + return 0 +} + +// blockquote ends with at least one blank line +// followed by something without a blockquote prefix +func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { + if p.isEmpty(data[beg:]) <= 0 { + return false + } + if end >= len(data) { + return true + } + return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 +} + +// parse a blockquote fragment +func (p *Markdown) quote(data []byte) int { + block := p.addBlock(BlockQuote, nil) + var raw bytes.Buffer + beg, end := 0, 0 + for beg < len(data) { + end = beg + // Step over whole lines, collecting them. While doing that, check for + // fenced code and if one's found, incorporate it altogether, + // irregardless of any contents inside it + for end < len(data) && data[end] != '\n' { + if p.extensions&FencedCode != 0 { + if i := p.fencedCodeBlock(data[end:], false); i > 0 { + // -1 to compensate for the extra end++ after the loop: + end += i - 1 + break + } + } + end++ + } + if end < len(data) && data[end] == '\n' { + end++ + } + if pre := p.quotePrefix(data[beg:]); pre > 0 { + // skip the prefix + beg += pre + } else if p.terminateBlockquote(data, beg, end) { + break + } + // this line is part of the blockquote + raw.Write(data[beg:end]) + beg = end + } + p.block(raw.Bytes()) + p.finalize(block) + return end +} + +// returns prefix length for block code +func (p *Markdown) codePrefix(data []byte) int { + if len(data) >= 1 && data[0] == '\t' { + return 1 + } + if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { + return 4 + } + return 0 +} + +func (p *Markdown) code(data []byte) int { + var work bytes.Buffer + + i := 0 + for i < len(data) { + beg := i + for i < len(data) && data[i] != '\n' { + i++ + } + if i < len(data) && data[i] == '\n' { + i++ + } + + blankline := p.isEmpty(data[beg:i]) > 0 + if pre := p.codePrefix(data[beg:i]); pre > 0 { + beg += pre + } else if !blankline { + // non-empty, non-prefixed line breaks the pre + i = beg + break + } + + // verbatim copy to the working buffer + if blankline { + work.WriteByte('\n') + } else { + work.Write(data[beg:i]) + } + } + + // trim all the \n off the end of work + workbytes := work.Bytes() + eol := len(workbytes) + for eol > 0 && workbytes[eol-1] == '\n' { + eol-- + } + if eol != len(workbytes) { + work.Truncate(eol) + } + + work.WriteByte('\n') + + block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer + block.IsFenced = false + finalizeCodeBlock(block) + + return i +} + +// returns unordered list item prefix +func (p *Markdown) uliPrefix(data []byte) int { + i := 0 + // start with up to 3 spaces + for i < len(data) && i < 3 && data[i] == ' ' { + i++ + } + if i >= len(data)-1 { + return 0 + } + // need one of {'*', '+', '-'} followed by a space or a tab + if (data[i] != '*' && data[i] != '+' && data[i] != '-') || + (data[i+1] != ' ' && data[i+1] != '\t') { + return 0 + } + return i + 2 +} + +// returns ordered list item prefix +func (p *Markdown) oliPrefix(data []byte) int { + i := 0 + + // start with up to 3 spaces + for i < 3 && i < len(data) && data[i] == ' ' { + i++ + } + + // count the digits + start := i + for i < len(data) && data[i] >= '0' && data[i] <= '9' { + i++ + } + if start == i || i >= len(data)-1 { + return 0 + } + + // we need >= 1 digits followed by a dot and a space or a tab + if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { + return 0 + } + return i + 2 +} + +// returns definition list item prefix +func (p *Markdown) dliPrefix(data []byte) int { + if len(data) < 2 { + return 0 + } + i := 0 + // need a ':' followed by a space or a tab + if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { + return 0 + } + for i < len(data) && data[i] == ' ' { + i++ + } + return i + 2 +} + +// parse ordered or unordered list block +func (p *Markdown) list(data []byte, flags ListType) int { + i := 0 + flags |= ListItemBeginningOfList + block := p.addBlock(List, nil) + block.ListFlags = flags + block.Tight = true + + for i < len(data) { + skip := p.listItem(data[i:], &flags) + if flags&ListItemContainsBlock != 0 { + block.ListData.Tight = false + } + i += skip + if skip == 0 || flags&ListItemEndOfList != 0 { + break + } + flags &= ^ListItemBeginningOfList + } + + above := block.Parent + finalizeList(block) + p.tip = above + return i +} + +// Returns true if the list item is not the same type as its parent list +func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool { + if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 { + return true + } else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 { + return true + } else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) { + return true + } + return false +} + +// Returns true if block ends with a blank line, descending if needed +// into lists and sublists. +func endsWithBlankLine(block *Node) bool { + // TODO: figure this out. Always false now. + for block != nil { + //if block.lastLineBlank { + //return true + //} + t := block.Type + if t == List || t == Item { + block = block.LastChild + } else { + break + } + } + return false +} + +func finalizeList(block *Node) { + block.open = false + item := block.FirstChild + for item != nil { + // check for non-final list item ending with blank line: + if endsWithBlankLine(item) && item.Next != nil { + block.ListData.Tight = false + break + } + // recurse into children of list item, to see if there are spaces + // between any of them: + subItem := item.FirstChild + for subItem != nil { + if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { + block.ListData.Tight = false + break + } + subItem = subItem.Next + } + item = item.Next + } +} + +// Parse a single list item. +// Assumes initial prefix is already removed if this is a sublist. +func (p *Markdown) listItem(data []byte, flags *ListType) int { + // keep track of the indentation of the first line + itemIndent := 0 + if data[0] == '\t' { + itemIndent += 4 + } else { + for itemIndent < 3 && data[itemIndent] == ' ' { + itemIndent++ + } + } + + var bulletChar byte = '*' + i := p.uliPrefix(data) + if i == 0 { + i = p.oliPrefix(data) + } else { + bulletChar = data[i-2] + } + if i == 0 { + i = p.dliPrefix(data) + // reset definition term flag + if i > 0 { + *flags &= ^ListTypeTerm + } + } + if i == 0 { + // if in definition list, set term flag and continue + if *flags&ListTypeDefinition != 0 { + *flags |= ListTypeTerm + } else { + return 0 + } + } + + // skip leading whitespace on first line + for i < len(data) && data[i] == ' ' { + i++ + } + + // find the end of the line + line := i + for i > 0 && i < len(data) && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[line:i]) + line = i + + // process the following lines + containsBlankLine := false + sublist := 0 + codeBlockMarker := "" + +gatherlines: + for line < len(data) { + i++ + + // find the end of this line + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[line:i]) > 0 { + containsBlankLine = true + line = i + continue + } + + // calculate the indentation + indent := 0 + indentIndex := 0 + if data[line] == '\t' { + indentIndex++ + indent += 4 + } else { + for indent < 4 && line+indent < i && data[line+indent] == ' ' { + indent++ + indentIndex++ + } + } + + chunk := data[line+indentIndex : i] + + if p.extensions&FencedCode != 0 { + // determine if in or out of codeblock + // if in codeblock, ignore normal list processing + _, marker := isFenceLine(chunk, nil, codeBlockMarker) + if marker != "" { + if codeBlockMarker == "" { + // start of codeblock + codeBlockMarker = marker + } else { + // end of codeblock. + codeBlockMarker = "" + } + } + // we are in a codeblock, write line, and continue + if codeBlockMarker != "" || marker != "" { + raw.Write(data[line+indentIndex : i]) + line = i + continue gatherlines + } + } + + // evaluate how this line fits in + switch { + // is this a nested list item? + case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || + p.oliPrefix(chunk) > 0 || + p.dliPrefix(chunk) > 0: + + // to be a nested list, it must be indented more + // if not, it is either a different kind of list + // or the next item in the same list + if indent <= itemIndent { + if p.listTypeChanged(chunk, flags) { + *flags |= ListItemEndOfList + } else if containsBlankLine { + *flags |= ListItemContainsBlock + } + + break gatherlines + } + + if containsBlankLine { + *flags |= ListItemContainsBlock + } + + // is this the first item in the nested list? + if sublist == 0 { + sublist = raw.Len() + } + + // is this a nested prefix heading? + case p.isPrefixHeading(chunk): + // if the heading is not indented, it is not nested in the list + // and thus ends the list + if containsBlankLine && indent < 4 { + *flags |= ListItemEndOfList + break gatherlines + } + *flags |= ListItemContainsBlock + + // anything following an empty line is only part + // of this item if it is indented 4 spaces + // (regardless of the indentation of the beginning of the item) + case containsBlankLine && indent < 4: + if *flags&ListTypeDefinition != 0 && i < len(data)-1 { + // is the next item still a part of this list? + next := i + for next < len(data) && data[next] != '\n' { + next++ + } + for next < len(data)-1 && data[next] == '\n' { + next++ + } + if i < len(data)-1 && data[i] != ':' && data[next] != ':' { + *flags |= ListItemEndOfList + } + } else { + *flags |= ListItemEndOfList + } + break gatherlines + + // a blank line means this should be parsed as a block + case containsBlankLine: + raw.WriteByte('\n') + *flags |= ListItemContainsBlock + } + + // if this line was preceded by one or more blanks, + // re-introduce the blank into the buffer + if containsBlankLine { + containsBlankLine = false + raw.WriteByte('\n') + } + + // add the line into the working buffer without prefix + raw.Write(data[line+indentIndex : i]) + + line = i + } + + rawBytes := raw.Bytes() + + block := p.addBlock(Item, nil) + block.ListFlags = *flags + block.Tight = false + block.BulletChar = bulletChar + block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark + + // render the contents of the list item + if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { + // intermediate render of block item, except for definition term + if sublist > 0 { + p.block(rawBytes[:sublist]) + p.block(rawBytes[sublist:]) + } else { + p.block(rawBytes) + } + } else { + // intermediate render of inline item + if sublist > 0 { + child := p.addChild(Paragraph, 0) + child.content = rawBytes[:sublist] + p.block(rawBytes[sublist:]) + } else { + child := p.addChild(Paragraph, 0) + child.content = rawBytes + } + } + return line +} + +// render a single paragraph that has already been parsed out +func (p *Markdown) renderParagraph(data []byte) { + if len(data) == 0 { + return + } + + // trim leading spaces + beg := 0 + for data[beg] == ' ' { + beg++ + } + + end := len(data) + // trim trailing newline + if data[len(data)-1] == '\n' { + end-- + } + + // trim trailing spaces + for end > beg && data[end-1] == ' ' { + end-- + } + + p.addBlock(Paragraph, data[beg:end]) +} + +func (p *Markdown) paragraph(data []byte) int { + // prev: index of 1st char of previous line + // line: index of 1st char of current line + // i: index of cursor/end of current line + var prev, line, i int + tabSize := TabSizeDefault + if p.extensions&TabSizeEight != 0 { + tabSize = TabSizeDouble + } + // keep going until we find something to mark the end of the paragraph + for i < len(data) { + // mark the beginning of the current line + prev = line + current := data[i:] + line = i + + // did we find a reference or a footnote? If so, end a paragraph + // preceding it and report that we have consumed up to the end of that + // reference: + if refEnd := isReference(p, current, tabSize); refEnd > 0 { + p.renderParagraph(data[:i]) + return i + refEnd + } + + // did we find a blank line marking the end of the paragraph? + if n := p.isEmpty(current); n > 0 { + // did this blank line followed by a definition list item? + if p.extensions&DefinitionLists != 0 { + if i < len(data)-1 && data[i+1] == ':' { + return p.list(data[prev:], ListTypeDefinition) + } + } + + p.renderParagraph(data[:i]) + return i + n + } + + // an underline under some text marks a heading, so our paragraph ended on prev line + if i > 0 { + if level := p.isUnderlinedHeading(current); level > 0 { + // render the paragraph + p.renderParagraph(data[:prev]) + + // ignore leading and trailing whitespace + eol := i - 1 + for prev < eol && data[prev] == ' ' { + prev++ + } + for eol > prev && data[eol-1] == ' ' { + eol-- + } + + id := "" + if p.extensions&AutoHeadingIDs != 0 { + id = sanitized_anchor_name.Create(string(data[prev:eol])) + } + + block := p.addBlock(Heading, data[prev:eol]) + block.Level = level + block.HeadingID = id + + // find the end of the underline + for i < len(data) && data[i] != '\n' { + i++ + } + return i + } + } + + // if the next line starts a block of HTML, then the paragraph ends here + if p.extensions&LaxHTMLBlocks != 0 { + if data[i] == '<' && p.html(current, false) > 0 { + // rewind to before the HTML block + p.renderParagraph(data[:i]) + return i + } + } + + // if there's a prefixed heading or a horizontal rule after this, paragraph is over + if p.isPrefixHeading(current) || p.isHRule(current) { + p.renderParagraph(data[:i]) + return i + } + + // if there's a fenced code block, paragraph is over + if p.extensions&FencedCode != 0 { + if p.fencedCodeBlock(current, false) > 0 { + p.renderParagraph(data[:i]) + return i + } + } + + // if there's a definition list item, prev line is a definition term + if p.extensions&DefinitionLists != 0 { + if p.dliPrefix(current) != 0 { + ret := p.list(data[prev:], ListTypeDefinition) + return ret + } + } + + // if there's a list after this, paragraph is over + if p.extensions&NoEmptyLineBeforeBlock != 0 { + if p.uliPrefix(current) != 0 || + p.oliPrefix(current) != 0 || + p.quotePrefix(current) != 0 || + p.codePrefix(current) != 0 { + p.renderParagraph(data[:i]) + return i + } + } + + // otherwise, scan to the beginning of the next line + nl := bytes.IndexByte(data[i:], '\n') + if nl >= 0 { + i += nl + 1 + } else { + i += len(data[i:]) + } + } + + p.renderParagraph(data[:i]) + return i +} + +func skipChar(data []byte, start int, char byte) int { + i := start + for i < len(data) && data[i] == char { + i++ + } + return i +} + +func skipUntilChar(text []byte, start int, char byte) int { + i := start + for i < len(text) && text[i] != char { + i++ + } + return i +} diff --git a/vendor/github.com/russross/blackfriday/v2/doc.go b/vendor/github.com/russross/blackfriday/v2/doc.go new file mode 100644 index 00000000..5b3fa987 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/doc.go @@ -0,0 +1,18 @@ +// Package blackfriday is a markdown processor. +// +// It translates plain text with simple formatting rules into an AST, which can +// then be further processed to HTML (provided by Blackfriday itself) or other +// formats (provided by the community). +// +// The simplest way to invoke Blackfriday is to call the Run function. It will +// take a text input and produce a text output in HTML (or other format). +// +// A slightly more sophisticated way to use Blackfriday is to create a Markdown +// processor and to call Parse, which returns a syntax tree for the input +// document. You can leverage Blackfriday's parsing for content extraction from +// markdown documents. You can assign a custom renderer and set various options +// to the Markdown processor. +// +// If you're interested in calling Blackfriday from command line, see +// https://github.com/russross/blackfriday-tool. +package blackfriday diff --git a/vendor/github.com/russross/blackfriday/v2/esc.go b/vendor/github.com/russross/blackfriday/v2/esc.go new file mode 100644 index 00000000..6385f27c --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/esc.go @@ -0,0 +1,34 @@ +package blackfriday + +import ( + "html" + "io" +) + +var htmlEscaper = [256][]byte{ + '&': []byte("&"), + '<': []byte("<"), + '>': []byte(">"), + '"': []byte("""), +} + +func escapeHTML(w io.Writer, s []byte) { + var start, end int + for end < len(s) { + escSeq := htmlEscaper[s[end]] + if escSeq != nil { + w.Write(s[start:end]) + w.Write(escSeq) + start = end + 1 + } + end++ + } + if start < len(s) && end <= len(s) { + w.Write(s[start:end]) + } +} + +func escLink(w io.Writer, text []byte) { + unesc := html.UnescapeString(string(text)) + escapeHTML(w, []byte(unesc)) +} diff --git a/vendor/github.com/russross/blackfriday/v2/go.mod b/vendor/github.com/russross/blackfriday/v2/go.mod new file mode 100644 index 00000000..620b74e0 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/go.mod @@ -0,0 +1 @@ +module github.com/russross/blackfriday/v2 diff --git a/vendor/github.com/russross/blackfriday/v2/html.go b/vendor/github.com/russross/blackfriday/v2/html.go new file mode 100644 index 00000000..284c8718 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/html.go @@ -0,0 +1,949 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// HTML rendering backend +// +// + +package blackfriday + +import ( + "bytes" + "fmt" + "io" + "regexp" + "strings" +) + +// HTMLFlags control optional behavior of HTML renderer. +type HTMLFlags int + +// HTML renderer configuration options. +const ( + HTMLFlagsNone HTMLFlags = 0 + SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks + SkipImages // Skip embedded images + SkipLinks // Skip all links + Safelink // Only link to trusted protocols + NofollowLinks // Only link with rel="nofollow" + NoreferrerLinks // Only link with rel="noreferrer" + NoopenerLinks // Only link with rel="noopener" + HrefTargetBlank // Add a blank target + CompletePage // Generate a complete HTML page + UseXHTML // Generate XHTML output instead of HTML + FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source + Smartypants // Enable smart punctuation substitutions + SmartypantsFractions // Enable smart fractions (with Smartypants) + SmartypantsDashes // Enable smart dashes (with Smartypants) + SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) + SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering + SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) + TOC // Generate a table of contents +) + +var ( + htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) +) + +const ( + htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + + processingInstruction + "|" + declaration + "|" + cdata + ")" + closeTag = "]" + openTag = "<" + tagName + attribute + "*" + "\\s*/?>" + attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" + attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" + attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" + attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" + cdata = "" + declaration = "]*>" + doubleQuotedValue = "\"[^\"]*\"" + htmlComment = "|" + processingInstruction = "[<][?].*?[?][>]" + singleQuotedValue = "'[^']*'" + tagName = "[A-Za-z][A-Za-z0-9-]*" + unquotedValue = "[^\"'=<>`\\x00-\\x20]+" +) + +// HTMLRendererParameters is a collection of supplementary parameters tweaking +// the behavior of various parts of HTML renderer. +type HTMLRendererParameters struct { + // Prepend this text to each relative URL. + AbsolutePrefix string + // Add this text to each footnote anchor, to ensure uniqueness. + FootnoteAnchorPrefix string + // Show this text inside the tag for a footnote return link, if the + // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string + // [return] is used. + FootnoteReturnLinkContents string + // If set, add this text to the front of each Heading ID, to ensure + // uniqueness. + HeadingIDPrefix string + // If set, add this text to the back of each Heading ID, to ensure uniqueness. + HeadingIDSuffix string + // Increase heading levels: if the offset is 1,

becomes

etc. + // Negative offset is also valid. + // Resulting levels are clipped between 1 and 6. + HeadingLevelOffset int + + Title string // Document title (used if CompletePage is set) + CSS string // Optional CSS file URL (used if CompletePage is set) + Icon string // Optional icon file URL (used if CompletePage is set) + + Flags HTMLFlags // Flags allow customizing this renderer's behavior +} + +// HTMLRenderer is a type that implements the Renderer interface for HTML output. +// +// Do not create this directly, instead use the NewHTMLRenderer function. +type HTMLRenderer struct { + HTMLRendererParameters + + closeTag string // how to end singleton tags: either " />" or ">" + + // Track heading IDs to prevent ID collision in a single generation. + headingIDs map[string]int + + lastOutputLen int + disableTags int + + sr *SPRenderer +} + +const ( + xhtmlClose = " />" + htmlClose = ">" +) + +// NewHTMLRenderer creates and configures an HTMLRenderer object, which +// satisfies the Renderer interface. +func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { + // configure the rendering engine + closeTag := htmlClose + if params.Flags&UseXHTML != 0 { + closeTag = xhtmlClose + } + + if params.FootnoteReturnLinkContents == "" { + params.FootnoteReturnLinkContents = `[return]` + } + + return &HTMLRenderer{ + HTMLRendererParameters: params, + + closeTag: closeTag, + headingIDs: make(map[string]int), + + sr: NewSmartypantsRenderer(params.Flags), + } +} + +func isHTMLTag(tag []byte, tagname string) bool { + found, _ := findHTMLTagPos(tag, tagname) + return found +} + +// Look for a character, but ignore it when it's in any kind of quotes, it +// might be JavaScript +func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { + inSingleQuote := false + inDoubleQuote := false + inGraveQuote := false + i := start + for i < len(html) { + switch { + case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: + return i + case html[i] == '\'': + inSingleQuote = !inSingleQuote + case html[i] == '"': + inDoubleQuote = !inDoubleQuote + case html[i] == '`': + inGraveQuote = !inGraveQuote + } + i++ + } + return start +} + +func findHTMLTagPos(tag []byte, tagname string) (bool, int) { + i := 0 + if i < len(tag) && tag[0] != '<' { + return false, -1 + } + i++ + i = skipSpace(tag, i) + + if i < len(tag) && tag[i] == '/' { + i++ + } + + i = skipSpace(tag, i) + j := 0 + for ; i < len(tag); i, j = i+1, j+1 { + if j >= len(tagname) { + break + } + + if strings.ToLower(string(tag[i]))[0] != tagname[j] { + return false, -1 + } + } + + if i == len(tag) { + return false, -1 + } + + rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') + if rightAngle >= i { + return true, rightAngle + } + + return false, -1 +} + +func skipSpace(tag []byte, i int) int { + for i < len(tag) && isspace(tag[i]) { + i++ + } + return i +} + +func isRelativeLink(link []byte) (yes bool) { + // a tag begin with '#' + if link[0] == '#' { + return true + } + + // link begin with '/' but not '//', the second maybe a protocol relative link + if len(link) >= 2 && link[0] == '/' && link[1] != '/' { + return true + } + + // only the root '/' + if len(link) == 1 && link[0] == '/' { + return true + } + + // current directory : begin with "./" + if bytes.HasPrefix(link, []byte("./")) { + return true + } + + // parent directory : begin with "../" + if bytes.HasPrefix(link, []byte("../")) { + return true + } + + return false +} + +func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { + for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { + tmp := fmt.Sprintf("%s-%d", id, count+1) + + if _, tmpFound := r.headingIDs[tmp]; !tmpFound { + r.headingIDs[id] = count + 1 + id = tmp + } else { + id = id + "-1" + } + } + + if _, found := r.headingIDs[id]; !found { + r.headingIDs[id] = 0 + } + + return id +} + +func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { + if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { + newDest := r.AbsolutePrefix + if link[0] != '/' { + newDest += "/" + } + newDest += string(link) + return []byte(newDest) + } + return link +} + +func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { + if isRelativeLink(link) { + return attrs + } + val := []string{} + if flags&NofollowLinks != 0 { + val = append(val, "nofollow") + } + if flags&NoreferrerLinks != 0 { + val = append(val, "noreferrer") + } + if flags&NoopenerLinks != 0 { + val = append(val, "noopener") + } + if flags&HrefTargetBlank != 0 { + attrs = append(attrs, "target=\"_blank\"") + } + if len(val) == 0 { + return attrs + } + attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) + return append(attrs, attr) +} + +func isMailto(link []byte) bool { + return bytes.HasPrefix(link, []byte("mailto:")) +} + +func needSkipLink(flags HTMLFlags, dest []byte) bool { + if flags&SkipLinks != 0 { + return true + } + return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) +} + +func isSmartypantable(node *Node) bool { + pt := node.Parent.Type + return pt != Link && pt != CodeBlock && pt != Code +} + +func appendLanguageAttr(attrs []string, info []byte) []string { + if len(info) == 0 { + return attrs + } + endOfLang := bytes.IndexAny(info, "\t ") + if endOfLang < 0 { + endOfLang = len(info) + } + return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) +} + +func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { + w.Write(name) + if len(attrs) > 0 { + w.Write(spaceBytes) + w.Write([]byte(strings.Join(attrs, " "))) + } + w.Write(gtBytes) + r.lastOutputLen = 1 +} + +func footnoteRef(prefix string, node *Node) []byte { + urlFrag := prefix + string(slugify(node.Destination)) + anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) + return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) +} + +func footnoteItem(prefix string, slug []byte) []byte { + return []byte(fmt.Sprintf(`
  • `, prefix, slug)) +} + +func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { + const format = ` %s` + return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) +} + +func itemOpenCR(node *Node) bool { + if node.Prev == nil { + return false + } + ld := node.Parent.ListData + return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 +} + +func skipParagraphTags(node *Node) bool { + grandparent := node.Parent.Parent + if grandparent == nil || grandparent.Type != List { + return false + } + tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 + return grandparent.Type == List && tightOrTerm +} + +func cellAlignment(align CellAlignFlags) string { + switch align { + case TableAlignmentLeft: + return "left" + case TableAlignmentRight: + return "right" + case TableAlignmentCenter: + return "center" + default: + return "" + } +} + +func (r *HTMLRenderer) out(w io.Writer, text []byte) { + if r.disableTags > 0 { + w.Write(htmlTagRe.ReplaceAll(text, []byte{})) + } else { + w.Write(text) + } + r.lastOutputLen = len(text) +} + +func (r *HTMLRenderer) cr(w io.Writer) { + if r.lastOutputLen > 0 { + r.out(w, nlBytes) + } +} + +var ( + nlBytes = []byte{'\n'} + gtBytes = []byte{'>'} + spaceBytes = []byte{' '} +) + +var ( + brTag = []byte("
    ") + brXHTMLTag = []byte("
    ") + emTag = []byte("") + emCloseTag = []byte("") + strongTag = []byte("") + strongCloseTag = []byte("") + delTag = []byte("") + delCloseTag = []byte("") + ttTag = []byte("") + ttCloseTag = []byte("") + aTag = []byte("") + preTag = []byte("
    ")
    +	preCloseTag        = []byte("
    ") + codeTag = []byte("") + codeCloseTag = []byte("") + pTag = []byte("

    ") + pCloseTag = []byte("

    ") + blockquoteTag = []byte("
    ") + blockquoteCloseTag = []byte("
    ") + hrTag = []byte("
    ") + hrXHTMLTag = []byte("
    ") + ulTag = []byte("
      ") + ulCloseTag = []byte("
    ") + olTag = []byte("
      ") + olCloseTag = []byte("
    ") + dlTag = []byte("
    ") + dlCloseTag = []byte("
    ") + liTag = []byte("
  • ") + liCloseTag = []byte("
  • ") + ddTag = []byte("
    ") + ddCloseTag = []byte("
    ") + dtTag = []byte("
    ") + dtCloseTag = []byte("
    ") + tableTag = []byte("") + tableCloseTag = []byte("
    ") + tdTag = []byte("") + thTag = []byte("") + theadTag = []byte("") + theadCloseTag = []byte("") + tbodyTag = []byte("") + tbodyCloseTag = []byte("") + trTag = []byte("") + trCloseTag = []byte("") + h1Tag = []byte("") + h2Tag = []byte("") + h3Tag = []byte("") + h4Tag = []byte("") + h5Tag = []byte("") + h6Tag = []byte("") + + footnotesDivBytes = []byte("\n
    \n\n") + footnotesCloseDivBytes = []byte("\n
    \n") +) + +func headingTagsFromLevel(level int) ([]byte, []byte) { + if level <= 1 { + return h1Tag, h1CloseTag + } + switch level { + case 2: + return h2Tag, h2CloseTag + case 3: + return h3Tag, h3CloseTag + case 4: + return h4Tag, h4CloseTag + case 5: + return h5Tag, h5CloseTag + } + return h6Tag, h6CloseTag +} + +func (r *HTMLRenderer) outHRTag(w io.Writer) { + if r.Flags&UseXHTML == 0 { + r.out(w, hrTag) + } else { + r.out(w, hrXHTMLTag) + } +} + +// RenderNode is a default renderer of a single node of a syntax tree. For +// block nodes it will be called twice: first time with entering=true, second +// time with entering=false, so that it could know when it's working on an open +// tag and when on close. It writes the result to w. +// +// The return value is a way to tell the calling walker to adjust its walk +// pattern: e.g. it can terminate the traversal by returning Terminate. Or it +// can ask the walker to skip a subtree of this node by returning SkipChildren. +// The typical behavior is to return GoToNext, which asks for the usual +// traversal to the next node. +func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { + attrs := []string{} + switch node.Type { + case Text: + if r.Flags&Smartypants != 0 { + var tmp bytes.Buffer + escapeHTML(&tmp, node.Literal) + r.sr.Process(w, tmp.Bytes()) + } else { + if node.Parent.Type == Link { + escLink(w, node.Literal) + } else { + escapeHTML(w, node.Literal) + } + } + case Softbreak: + r.cr(w) + // TODO: make it configurable via out(renderer.softbreak) + case Hardbreak: + if r.Flags&UseXHTML == 0 { + r.out(w, brTag) + } else { + r.out(w, brXHTMLTag) + } + r.cr(w) + case Emph: + if entering { + r.out(w, emTag) + } else { + r.out(w, emCloseTag) + } + case Strong: + if entering { + r.out(w, strongTag) + } else { + r.out(w, strongCloseTag) + } + case Del: + if entering { + r.out(w, delTag) + } else { + r.out(w, delCloseTag) + } + case HTMLSpan: + if r.Flags&SkipHTML != 0 { + break + } + r.out(w, node.Literal) + case Link: + // mark it but don't link it if it is not a safe link: no smartypants + dest := node.LinkData.Destination + if needSkipLink(r.Flags, dest) { + if entering { + r.out(w, ttTag) + } else { + r.out(w, ttCloseTag) + } + } else { + if entering { + dest = r.addAbsPrefix(dest) + var hrefBuf bytes.Buffer + hrefBuf.WriteString("href=\"") + escLink(&hrefBuf, dest) + hrefBuf.WriteByte('"') + attrs = append(attrs, hrefBuf.String()) + if node.NoteID != 0 { + r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) + break + } + attrs = appendLinkAttrs(attrs, r.Flags, dest) + if len(node.LinkData.Title) > 0 { + var titleBuff bytes.Buffer + titleBuff.WriteString("title=\"") + escapeHTML(&titleBuff, node.LinkData.Title) + titleBuff.WriteByte('"') + attrs = append(attrs, titleBuff.String()) + } + r.tag(w, aTag, attrs) + } else { + if node.NoteID != 0 { + break + } + r.out(w, aCloseTag) + } + } + case Image: + if r.Flags&SkipImages != 0 { + return SkipChildren + } + if entering { + dest := node.LinkData.Destination + dest = r.addAbsPrefix(dest) + if r.disableTags == 0 { + //if options.safe && potentiallyUnsafe(dest) { + //out(w, ``)
+				//} else {
+				r.out(w, []byte(`<img src=`)) + } + } + case Code: + r.out(w, codeTag) + escapeHTML(w, node.Literal) + r.out(w, codeCloseTag) + case Document: + break + case Paragraph: + if skipParagraphTags(node) { + break + } + if entering { + // TODO: untangle this clusterfuck about when the newlines need + // to be added and when not. + if node.Prev != nil { + switch node.Prev.Type { + case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: + r.cr(w) + } + } + if node.Parent.Type == BlockQuote && node.Prev == nil { + r.cr(w) + } + r.out(w, pTag) + } else { + r.out(w, pCloseTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case BlockQuote: + if entering { + r.cr(w) + r.out(w, blockquoteTag) + } else { + r.out(w, blockquoteCloseTag) + r.cr(w) + } + case HTMLBlock: + if r.Flags&SkipHTML != 0 { + break + } + r.cr(w) + r.out(w, node.Literal) + r.cr(w) + case Heading: + headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level + openTag, closeTag := headingTagsFromLevel(headingLevel) + if entering { + if node.IsTitleblock { + attrs = append(attrs, `class="title"`) + } + if node.HeadingID != "" { + id := r.ensureUniqueHeadingID(node.HeadingID) + if r.HeadingIDPrefix != "" { + id = r.HeadingIDPrefix + id + } + if r.HeadingIDSuffix != "" { + id = id + r.HeadingIDSuffix + } + attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) + } + r.cr(w) + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + if !(node.Parent.Type == Item && node.Next == nil) { + r.cr(w) + } + } + case HorizontalRule: + r.cr(w) + r.outHRTag(w) + r.cr(w) + case List: + openTag := ulTag + closeTag := ulCloseTag + if node.ListFlags&ListTypeOrdered != 0 { + openTag = olTag + closeTag = olCloseTag + } + if node.ListFlags&ListTypeDefinition != 0 { + openTag = dlTag + closeTag = dlCloseTag + } + if entering { + if node.IsFootnotesList { + r.out(w, footnotesDivBytes) + r.outHRTag(w) + r.cr(w) + } + r.cr(w) + if node.Parent.Type == Item && node.Parent.Parent.Tight { + r.cr(w) + } + r.tag(w, openTag[:len(openTag)-1], attrs) + r.cr(w) + } else { + r.out(w, closeTag) + //cr(w) + //if node.parent.Type != Item { + // cr(w) + //} + if node.Parent.Type == Item && node.Next != nil { + r.cr(w) + } + if node.Parent.Type == Document || node.Parent.Type == BlockQuote { + r.cr(w) + } + if node.IsFootnotesList { + r.out(w, footnotesCloseDivBytes) + } + } + case Item: + openTag := liTag + closeTag := liCloseTag + if node.ListFlags&ListTypeDefinition != 0 { + openTag = ddTag + closeTag = ddCloseTag + } + if node.ListFlags&ListTypeTerm != 0 { + openTag = dtTag + closeTag = dtCloseTag + } + if entering { + if itemOpenCR(node) { + r.cr(w) + } + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) + break + } + r.out(w, openTag) + } else { + if node.ListData.RefLink != nil { + slug := slugify(node.ListData.RefLink) + if r.Flags&FootnoteReturnLinks != 0 { + r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) + } + } + r.out(w, closeTag) + r.cr(w) + } + case CodeBlock: + attrs = appendLanguageAttr(attrs, node.Info) + r.cr(w) + r.out(w, preTag) + r.tag(w, codeTag[:len(codeTag)-1], attrs) + escapeHTML(w, node.Literal) + r.out(w, codeCloseTag) + r.out(w, preCloseTag) + if node.Parent.Type != Item { + r.cr(w) + } + case Table: + if entering { + r.cr(w) + r.out(w, tableTag) + } else { + r.out(w, tableCloseTag) + r.cr(w) + } + case TableCell: + openTag := tdTag + closeTag := tdCloseTag + if node.IsHeader { + openTag = thTag + closeTag = thCloseTag + } + if entering { + align := cellAlignment(node.Align) + if align != "" { + attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) + } + if node.Prev == nil { + r.cr(w) + } + r.tag(w, openTag, attrs) + } else { + r.out(w, closeTag) + r.cr(w) + } + case TableHead: + if entering { + r.cr(w) + r.out(w, theadTag) + } else { + r.out(w, theadCloseTag) + r.cr(w) + } + case TableBody: + if entering { + r.cr(w) + r.out(w, tbodyTag) + // XXX: this is to adhere to a rather silly test. Should fix test. + if node.FirstChild == nil { + r.cr(w) + } + } else { + r.out(w, tbodyCloseTag) + r.cr(w) + } + case TableRow: + if entering { + r.cr(w) + r.out(w, trTag) + } else { + r.out(w, trCloseTag) + r.cr(w) + } + default: + panic("Unknown node type " + node.Type.String()) + } + return GoToNext +} + +// RenderHeader writes HTML document preamble and TOC if requested. +func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { + r.writeDocumentHeader(w) + if r.Flags&TOC != 0 { + r.writeTOC(w, ast) + } +} + +// RenderFooter writes HTML document footer. +func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { + if r.Flags&CompletePage == 0 { + return + } + io.WriteString(w, "\n\n\n") +} + +func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { + if r.Flags&CompletePage == 0 { + return + } + ending := "" + if r.Flags&UseXHTML != 0 { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + ending = " /" + } else { + io.WriteString(w, "\n") + io.WriteString(w, "\n") + } + io.WriteString(w, "\n") + io.WriteString(w, " ") + if r.Flags&Smartypants != 0 { + r.sr.Process(w, []byte(r.Title)) + } else { + escapeHTML(w, []byte(r.Title)) + } + io.WriteString(w, "\n") + io.WriteString(w, " \n") + io.WriteString(w, " \n") + if r.CSS != "" { + io.WriteString(w, " \n") + } + if r.Icon != "" { + io.WriteString(w, " \n") + } + io.WriteString(w, "\n") + io.WriteString(w, "\n\n") +} + +func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { + buf := bytes.Buffer{} + + inHeading := false + tocLevel := 0 + headingCount := 0 + + ast.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Heading && !node.HeadingData.IsTitleblock { + inHeading = entering + if entering { + node.HeadingID = fmt.Sprintf("toc_%d", headingCount) + if node.Level == tocLevel { + buf.WriteString("\n\n
  • ") + } else if node.Level < tocLevel { + for node.Level < tocLevel { + tocLevel-- + buf.WriteString("
  • \n") + } + buf.WriteString("\n\n
  • ") + } else { + for node.Level > tocLevel { + tocLevel++ + buf.WriteString("\n") + } + + if buf.Len() > 0 { + io.WriteString(w, "\n") + } + r.lastOutputLen = buf.Len() +} diff --git a/vendor/github.com/russross/blackfriday/v2/inline.go b/vendor/github.com/russross/blackfriday/v2/inline.go new file mode 100644 index 00000000..4ed29079 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/inline.go @@ -0,0 +1,1228 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// Functions to parse inline elements. +// + +package blackfriday + +import ( + "bytes" + "regexp" + "strconv" +) + +var ( + urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` + anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) + + // https://www.w3.org/TR/html5/syntax.html#character-references + // highest unicode code point in 17 planes (2^20): 1,114,112d = + // 7 dec digits or 6 hex digits + // named entity references can be 2-31 characters with stuff like < + // at one end and ∳ at the other. There + // are also sometimes numbers at the end, although this isn't inherent + // in the specification; there are never numbers anywhere else in + // current character references, though; see ¾ and ▒, etc. + // https://www.w3.org/TR/html5/syntax.html#named-character-references + // + // entity := "&" (named group | number ref) ";" + // named group := [a-zA-Z]{2,31}[0-9]{0,2} + // number ref := "#" (dec ref | hex ref) + // dec ref := [0-9]{1,7} + // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} + htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) +) + +// Functions to parse text within a block +// Each function returns the number of chars taken care of +// data is the complete block being rendered +// offset is the number of valid chars before the current cursor + +func (p *Markdown) inline(currBlock *Node, data []byte) { + // handlers might call us recursively: enforce a maximum depth + if p.nesting >= p.maxNesting || len(data) == 0 { + return + } + p.nesting++ + beg, end := 0, 0 + for end < len(data) { + handler := p.inlineCallback[data[end]] + if handler != nil { + if consumed, node := handler(p, data, end); consumed == 0 { + // No action from the callback. + end++ + } else { + // Copy inactive chars into the output. + currBlock.AppendChild(text(data[beg:end])) + if node != nil { + currBlock.AppendChild(node) + } + // Skip past whatever the callback used. + beg = end + consumed + end = beg + } + } else { + end++ + } + } + if beg < len(data) { + if data[end-1] == '\n' { + end-- + } + currBlock.AppendChild(text(data[beg:end])) + } + p.nesting-- +} + +// single and double emphasis parsing +func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + c := data[0] + + if len(data) > 2 && data[1] != c { + // whitespace cannot follow an opening emphasis; + // strikethrough only takes two characters '~~' + if c == '~' || isspace(data[1]) { + return 0, nil + } + ret, node := helperEmphasis(p, data[1:], c) + if ret == 0 { + return 0, nil + } + + return ret + 1, node + } + + if len(data) > 3 && data[1] == c && data[2] != c { + if isspace(data[2]) { + return 0, nil + } + ret, node := helperDoubleEmphasis(p, data[2:], c) + if ret == 0 { + return 0, nil + } + + return ret + 2, node + } + + if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { + if c == '~' || isspace(data[3]) { + return 0, nil + } + ret, node := helperTripleEmphasis(p, data, 3, c) + if ret == 0 { + return 0, nil + } + + return ret + 3, node + } + + return 0, nil +} + +func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + nb := 0 + + // count the number of backticks in the delimiter + for nb < len(data) && data[nb] == '`' { + nb++ + } + + // find the next delimiter + i, end := 0, 0 + for end = nb; end < len(data) && i < nb; end++ { + if data[end] == '`' { + i++ + } else { + i = 0 + } + } + + // no matching delimiter? + if i < nb && end >= len(data) { + return 0, nil + } + + // trim outside whitespace + fBegin := nb + for fBegin < end && data[fBegin] == ' ' { + fBegin++ + } + + fEnd := end - nb + for fEnd > fBegin && data[fEnd-1] == ' ' { + fEnd-- + } + + // render the code span + if fBegin != fEnd { + code := NewNode(Code) + code.Literal = data[fBegin:fEnd] + return end, code + } + + return end, nil +} + +// newline preceded by two spaces becomes
    +func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + origOffset := offset + for offset < len(data) && data[offset] == ' ' { + offset++ + } + + if offset < len(data) && data[offset] == '\n' { + if offset-origOffset >= 2 { + return offset - origOffset + 1, NewNode(Hardbreak) + } + return offset - origOffset, nil + } + return 0, nil +} + +// newline without two spaces works when HardLineBreak is enabled +func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { + if p.extensions&HardLineBreak != 0 { + return 1, NewNode(Hardbreak) + } + return 0, nil +} + +type linkType int + +const ( + linkNormal linkType = iota + linkImg + linkDeferredFootnote + linkInlineFootnote +) + +func isReferenceStyleLink(data []byte, pos int, t linkType) bool { + if t == linkDeferredFootnote { + return false + } + return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' +} + +func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { + if offset < len(data)-1 && data[offset+1] == '[' { + return link(p, data, offset) + } + return 0, nil +} + +// '[': parse a link or an image or a footnote +func link(p *Markdown, data []byte, offset int) (int, *Node) { + // no links allowed inside regular links, footnote, and deferred footnotes + if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { + return 0, nil + } + + var t linkType + switch { + // special case: ![^text] == deferred footnote (that follows something with + // an exclamation point) + case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': + t = linkDeferredFootnote + // ![alt] == image + case offset >= 0 && data[offset] == '!': + t = linkImg + offset++ + // ^[text] == inline footnote + // [^refId] == deferred footnote + case p.extensions&Footnotes != 0: + if offset >= 0 && data[offset] == '^' { + t = linkInlineFootnote + offset++ + } else if len(data)-1 > offset && data[offset+1] == '^' { + t = linkDeferredFootnote + } + // [text] == regular link + default: + t = linkNormal + } + + data = data[offset:] + + var ( + i = 1 + noteID int + title, link, altContent []byte + textHasNl = false + ) + + if t == linkDeferredFootnote { + i++ + } + + // look for the matching closing bracket + for level := 1; level > 0 && i < len(data); i++ { + switch { + case data[i] == '\n': + textHasNl = true + + case data[i-1] == '\\': + continue + + case data[i] == '[': + level++ + + case data[i] == ']': + level-- + if level <= 0 { + i-- // compensate for extra i++ in for loop + } + } + } + + if i >= len(data) { + return 0, nil + } + + txtE := i + i++ + var footnoteNode *Node + + // skip any amount of whitespace or newline + // (this is much more lax than original markdown syntax) + for i < len(data) && isspace(data[i]) { + i++ + } + + // inline style link + switch { + case i < len(data) && data[i] == '(': + // skip initial whitespace + i++ + + for i < len(data) && isspace(data[i]) { + i++ + } + + linkB := i + + // look for link end: ' " ) + findlinkend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')' || data[i] == '\'' || data[i] == '"': + break findlinkend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + linkE := i + + // look for title end if present + titleB, titleE := 0, 0 + if data[i] == '\'' || data[i] == '"' { + i++ + titleB = i + + findtitleend: + for i < len(data) { + switch { + case data[i] == '\\': + i += 2 + + case data[i] == ')': + break findtitleend + + default: + i++ + } + } + + if i >= len(data) { + return 0, nil + } + + // skip whitespace after title + titleE = i - 1 + for titleE > titleB && isspace(data[titleE]) { + titleE-- + } + + // check for closing quote presence + if data[titleE] != '\'' && data[titleE] != '"' { + titleB, titleE = 0, 0 + linkE = i + } + } + + // remove whitespace at the end of the link + for linkE > linkB && isspace(data[linkE-1]) { + linkE-- + } + + // remove optional angle brackets around the link + if data[linkB] == '<' { + linkB++ + } + if data[linkE-1] == '>' { + linkE-- + } + + // build escaped link and title + if linkE > linkB { + link = data[linkB:linkE] + } + + if titleE > titleB { + title = data[titleB:titleE] + } + + i++ + + // reference style link + case isReferenceStyleLink(data, i, t): + var id []byte + altContentConsidered := false + + // look for the id + i++ + linkB := i + for i < len(data) && data[i] != ']' { + i++ + } + if i >= len(data) { + return 0, nil + } + linkE := i + + // find the reference + if linkB == linkE { + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + id = data[1:txtE] + altContentConsidered = true + } + } else { + id = data[linkB:linkE] + } + + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + // keep link and title from reference + link = lr.link + title = lr.title + if altContentConsidered { + altContent = lr.text + } + i++ + + // shortcut reference style link or reference or inline footnote + default: + var id []byte + + // craft the id + if textHasNl { + var b bytes.Buffer + + for j := 1; j < txtE; j++ { + switch { + case data[j] != '\n': + b.WriteByte(data[j]) + case data[j-1] != ' ': + b.WriteByte(' ') + } + } + + id = b.Bytes() + } else { + if t == linkDeferredFootnote { + id = data[2:txtE] // get rid of the ^ + } else { + id = data[1:txtE] + } + } + + footnoteNode = NewNode(Item) + if t == linkInlineFootnote { + // create a new reference + noteID = len(p.notes) + 1 + + var fragment []byte + if len(id) > 0 { + if len(id) < 16 { + fragment = make([]byte, len(id)) + } else { + fragment = make([]byte, 16) + } + copy(fragment, slugify(id)) + } else { + fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) + } + + ref := &reference{ + noteID: noteID, + hasBlock: false, + link: fragment, + title: id, + footnote: footnoteNode, + } + + p.notes = append(p.notes, ref) + + link = ref.link + title = ref.title + } else { + // find the reference with matching id + lr, ok := p.getRef(string(id)) + if !ok { + return 0, nil + } + + if t == linkDeferredFootnote { + lr.noteID = len(p.notes) + 1 + lr.footnote = footnoteNode + p.notes = append(p.notes, lr) + } + + // keep link and title from reference + link = lr.link + // if inline footnote, title == footnote contents + title = lr.title + noteID = lr.noteID + } + + // rewind the whitespace + i = txtE + 1 + } + + var uLink []byte + if t == linkNormal || t == linkImg { + if len(link) > 0 { + var uLinkBuf bytes.Buffer + unescapeText(&uLinkBuf, link) + uLink = uLinkBuf.Bytes() + } + + // links need something to click on and somewhere to go + if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { + return 0, nil + } + } + + // call the relevant rendering function + var linkNode *Node + switch t { + case linkNormal: + linkNode = NewNode(Link) + linkNode.Destination = normalizeURI(uLink) + linkNode.Title = title + if len(altContent) > 0 { + linkNode.AppendChild(text(altContent)) + } else { + // links cannot contain other links, so turn off link parsing + // temporarily and recurse + insideLink := p.insideLink + p.insideLink = true + p.inline(linkNode, data[1:txtE]) + p.insideLink = insideLink + } + + case linkImg: + linkNode = NewNode(Image) + linkNode.Destination = uLink + linkNode.Title = title + linkNode.AppendChild(text(data[1:txtE])) + i++ + + case linkInlineFootnote, linkDeferredFootnote: + linkNode = NewNode(Link) + linkNode.Destination = link + linkNode.Title = title + linkNode.NoteID = noteID + linkNode.Footnote = footnoteNode + if t == linkInlineFootnote { + i++ + } + + default: + return 0, nil + } + + return i, linkNode +} + +func (p *Markdown) inlineHTMLComment(data []byte) int { + if len(data) < 5 { + return 0 + } + if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { + return 0 + } + i := 5 + // scan for an end-of-comment marker, across lines if necessary + for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { + i++ + } + // no end-of-comment marker + if i >= len(data) { + return 0 + } + return i + 1 +} + +func stripMailto(link []byte) []byte { + if bytes.HasPrefix(link, []byte("mailto://")) { + return link[9:] + } else if bytes.HasPrefix(link, []byte("mailto:")) { + return link[7:] + } else { + return link + } +} + +// autolinkType specifies a kind of autolink that gets detected. +type autolinkType int + +// These are the possible flag values for the autolink renderer. +const ( + notAutolink autolinkType = iota + normalAutolink + emailAutolink +) + +// '<' when tags or autolinks are allowed +func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + altype, end := tagLength(data) + if size := p.inlineHTMLComment(data); size > 0 { + end = size + } + if end > 2 { + if altype != notAutolink { + var uLink bytes.Buffer + unescapeText(&uLink, data[1:end+1-2]) + if uLink.Len() > 0 { + link := uLink.Bytes() + node := NewNode(Link) + node.Destination = link + if altype == emailAutolink { + node.Destination = append([]byte("mailto:"), link...) + } + node.AppendChild(text(stripMailto(link))) + return end, node + } + } else { + htmlTag := NewNode(HTMLSpan) + htmlTag.Literal = data[:end] + return end, htmlTag + } + } + + return end, nil +} + +// '\\' backslash escape +var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") + +func escape(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + if len(data) > 1 { + if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { + return 2, NewNode(Hardbreak) + } + if bytes.IndexByte(escapeChars, data[1]) < 0 { + return 0, nil + } + + return 2, text(data[1:2]) + } + + return 2, nil +} + +func unescapeText(ob *bytes.Buffer, src []byte) { + i := 0 + for i < len(src) { + org := i + for i < len(src) && src[i] != '\\' { + i++ + } + + if i > org { + ob.Write(src[org:i]) + } + + if i+1 >= len(src) { + break + } + + ob.WriteByte(src[i+1]) + i += 2 + } +} + +// '&' escaped when it doesn't belong to an entity +// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; +func entity(p *Markdown, data []byte, offset int) (int, *Node) { + data = data[offset:] + + end := 1 + + if end < len(data) && data[end] == '#' { + end++ + } + + for end < len(data) && isalnum(data[end]) { + end++ + } + + if end < len(data) && data[end] == ';' { + end++ // real entity + } else { + return 0, nil // lone '&' + } + + ent := data[:end] + // undo & escaping or it will be converted to &amp; by another + // escaper in the renderer + if bytes.Equal(ent, []byte("&")) { + ent = []byte{'&'} + } + + return end, text(ent) +} + +func linkEndsWithEntity(data []byte, linkEnd int) bool { + entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) + return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd +} + +// hasPrefixCaseInsensitive is a custom implementation of +// strings.HasPrefix(strings.ToLower(s), prefix) +// we rolled our own because ToLower pulls in a huge machinery of lowercasing +// anything from Unicode and that's very slow. Since this func will only be +// used on ASCII protocol prefixes, we can take shortcuts. +func hasPrefixCaseInsensitive(s, prefix []byte) bool { + if len(s) < len(prefix) { + return false + } + delta := byte('a' - 'A') + for i, b := range prefix { + if b != s[i] && b != s[i]+delta { + return false + } + } + return true +} + +var protocolPrefixes = [][]byte{ + []byte("http://"), + []byte("https://"), + []byte("ftp://"), + []byte("file://"), + []byte("mailto:"), +} + +const shortestPrefix = 6 // len("ftp://"), the shortest of the above + +func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // quick check to rule out most false hits + if p.insideLink || len(data) < offset+shortestPrefix { + return 0, nil + } + for _, prefix := range protocolPrefixes { + endOfHead := offset + 8 // 8 is the len() of the longest prefix + if endOfHead > len(data) { + endOfHead = len(data) + } + if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { + return autoLink(p, data, offset) + } + } + return 0, nil +} + +func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { + // Now a more expensive check to see if we're not inside an anchor element + anchorStart := offset + offsetFromAnchor := 0 + for anchorStart > 0 && data[anchorStart] != '<' { + anchorStart-- + offsetFromAnchor++ + } + + anchorStr := anchorRe.Find(data[anchorStart:]) + if anchorStr != nil { + anchorClose := NewNode(HTMLSpan) + anchorClose.Literal = anchorStr[offsetFromAnchor:] + return len(anchorStr) - offsetFromAnchor, anchorClose + } + + // scan backward for a word boundary + rewind := 0 + for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) { + rewind++ + } + if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters + return 0, nil + } + + origData := data + data = data[offset-rewind:] + + if !isSafeLink(data) { + return 0, nil + } + + linkEnd := 0 + for linkEnd < len(data) && !isEndOfLink(data[linkEnd]) { + linkEnd++ + } + + // Skip punctuation at the end of the link + if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',') && data[linkEnd-2] != '\\' { + linkEnd-- + } + + // But don't skip semicolon if it's a part of escaped entity: + if data[linkEnd-1] == ';' && data[linkEnd-2] != '\\' && !linkEndsWithEntity(data, linkEnd) { + linkEnd-- + } + + // See if the link finishes with a punctuation sign that can be closed. + var copen byte + switch data[linkEnd-1] { + case '"': + copen = '"' + case '\'': + copen = '\'' + case ')': + copen = '(' + case ']': + copen = '[' + case '}': + copen = '{' + default: + copen = 0 + } + + if copen != 0 { + bufEnd := offset - rewind + linkEnd - 2 + + openDelim := 1 + + /* Try to close the final punctuation sign in this same line; + * if we managed to close it outside of the URL, that means that it's + * not part of the URL. If it closes inside the URL, that means it + * is part of the URL. + * + * Examples: + * + * foo http://www.pokemon.com/Pikachu_(Electric) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo (http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric) + * + * foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => http://www.pokemon.com/Pikachu_(Electric)) + * + * (foo http://www.pokemon.com/Pikachu_(Electric)) bar + * => foo http://www.pokemon.com/Pikachu_(Electric) + */ + + for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 { + if origData[bufEnd] == data[linkEnd-1] { + openDelim++ + } + + if origData[bufEnd] == copen { + openDelim-- + } + + bufEnd-- + } + + if openDelim == 0 { + linkEnd-- + } + } + + var uLink bytes.Buffer + unescapeText(&uLink, data[:linkEnd]) + + if uLink.Len() > 0 { + node := NewNode(Link) + node.Destination = uLink.Bytes() + node.AppendChild(text(uLink.Bytes())) + return linkEnd, node + } + + return linkEnd, nil +} + +func isEndOfLink(char byte) bool { + return isspace(char) || char == '<' +} + +var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} +var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} + +func isSafeLink(link []byte) bool { + for _, path := range validPaths { + if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { + if len(link) == len(path) { + return true + } else if isalnum(link[len(path)]) { + return true + } + } + } + + for _, prefix := range validUris { + // TODO: handle unicode here + // case-insensitive prefix test + if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) { + return true + } + } + + return false +} + +// return the length of the given tag, or 0 is it's not valid +func tagLength(data []byte) (autolink autolinkType, end int) { + var i, j int + + // a valid tag can't be shorter than 3 chars + if len(data) < 3 { + return notAutolink, 0 + } + + // begins with a '<' optionally followed by '/', followed by letter or number + if data[0] != '<' { + return notAutolink, 0 + } + if data[1] == '/' { + i = 2 + } else { + i = 1 + } + + if !isalnum(data[i]) { + return notAutolink, 0 + } + + // scheme test + autolink = notAutolink + + // try to find the beginning of an URI + for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { + i++ + } + + if i > 1 && i < len(data) && data[i] == '@' { + if j = isMailtoAutoLink(data[i:]); j != 0 { + return emailAutolink, i + j + } + } + + if i > 2 && i < len(data) && data[i] == ':' { + autolink = normalAutolink + i++ + } + + // complete autolink test: no whitespace or ' or " + switch { + case i >= len(data): + autolink = notAutolink + case autolink != notAutolink: + j = i + + for i < len(data) { + if data[i] == '\\' { + i += 2 + } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) { + break + } else { + i++ + } + + } + + if i >= len(data) { + return autolink, 0 + } + if i > j && data[i] == '>' { + return autolink, i + 1 + } + + // one of the forbidden chars has been found + autolink = notAutolink + } + i += bytes.IndexByte(data[i:], '>') + if i < 0 { + return autolink, 0 + } + return autolink, i + 1 +} + +// look for the address part of a mail autolink and '>' +// this is less strict than the original markdown e-mail address matching +func isMailtoAutoLink(data []byte) int { + nb := 0 + + // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' + for i := 0; i < len(data); i++ { + if isalnum(data[i]) { + continue + } + + switch data[i] { + case '@': + nb++ + + case '-', '.', '_': + break + + case '>': + if nb == 1 { + return i + 1 + } + return 0 + default: + return 0 + } + } + + return 0 +} + +// look for the next emph char, skipping other constructs +func helperFindEmphChar(data []byte, c byte) int { + i := 0 + + for i < len(data) { + for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' { + i++ + } + if i >= len(data) { + return 0 + } + // do not count escaped chars + if i != 0 && data[i-1] == '\\' { + i++ + continue + } + if data[i] == c { + return i + } + + if data[i] == '`' { + // skip a code span + tmpI := 0 + i++ + for i < len(data) && data[i] != '`' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } else if data[i] == '[' { + // skip a link + tmpI := 0 + i++ + for i < len(data) && data[i] != ']' { + if tmpI == 0 && data[i] == c { + tmpI = i + } + i++ + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\n') { + i++ + } + if i >= len(data) { + return tmpI + } + if data[i] != '[' && data[i] != '(' { // not a link + if tmpI > 0 { + return tmpI + } + continue + } + cc := data[i] + i++ + for i < len(data) && data[i] != cc { + if tmpI == 0 && data[i] == c { + return i + } + i++ + } + if i >= len(data) { + return tmpI + } + i++ + } + } + return 0 +} + +func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + // skip one symbol if coming from emph3 + if len(data) > 1 && data[0] == c && data[1] == c { + i = 1 + } + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + if i >= len(data) { + return 0, nil + } + + if i+1 < len(data) && data[i+1] == c { + i++ + continue + } + + if data[i] == c && !isspace(data[i-1]) { + + if p.extensions&NoIntraEmphasis != 0 { + if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { + continue + } + } + + emph := NewNode(Emph) + p.inline(emph, data[:i]) + return i + 1, emph + } + } + + return 0, nil +} + +func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { + i := 0 + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { + nodeType := Strong + if c == '~' { + nodeType = Del + } + node := NewNode(nodeType) + p.inline(node, data[:i]) + return i + 2, node + } + i++ + } + return 0, nil +} + +func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { + i := 0 + origData := data + data = data[offset:] + + for i < len(data) { + length := helperFindEmphChar(data[i:], c) + if length == 0 { + return 0, nil + } + i += length + + // skip whitespace preceded symbols + if data[i] != c || isspace(data[i-1]) { + continue + } + + switch { + case i+2 < len(data) && data[i+1] == c && data[i+2] == c: + // triple symbol found + strong := NewNode(Strong) + em := NewNode(Emph) + strong.AppendChild(em) + p.inline(em, data[:i]) + return i + 3, strong + case (i+1 < len(data) && data[i+1] == c): + // double symbol found, hand over to emph1 + length, node := helperEmphasis(p, origData[offset-2:], c) + if length == 0 { + return 0, nil + } + return length - 2, node + default: + // single symbol found, hand over to emph2 + length, node := helperDoubleEmphasis(p, origData[offset-1:], c) + if length == 0 { + return 0, nil + } + return length - 1, node + } + } + return 0, nil +} + +func text(s []byte) *Node { + node := NewNode(Text) + node.Literal = s + return node +} + +func normalizeURI(s []byte) []byte { + return s // TODO: implement +} diff --git a/vendor/github.com/russross/blackfriday/v2/markdown.go b/vendor/github.com/russross/blackfriday/v2/markdown.go new file mode 100644 index 00000000..58d2e453 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/markdown.go @@ -0,0 +1,950 @@ +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. + +package blackfriday + +import ( + "bytes" + "fmt" + "io" + "strings" + "unicode/utf8" +) + +// +// Markdown parsing and processing +// + +// Version string of the package. Appears in the rendered document when +// CompletePage flag is on. +const Version = "2.0" + +// Extensions is a bitwise or'ed collection of enabled Blackfriday's +// extensions. +type Extensions int + +// These are the supported markdown parsing extensions. +// OR these values together to select multiple extensions. +const ( + NoExtensions Extensions = 0 + NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words + Tables // Render tables + FencedCode // Render fenced code blocks + Autolink // Detect embedded URLs that are not explicitly marked + Strikethrough // Strikethrough text using ~~test~~ + LaxHTMLBlocks // Loosen up HTML block parsing rules + SpaceHeadings // Be strict about prefix heading rules + HardLineBreak // Translate newlines into line breaks + TabSizeEight // Expand tabs to eight spaces instead of four + Footnotes // Pandoc-style footnotes + NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block + HeadingIDs // specify heading IDs with {#id} + Titleblock // Titleblock ala pandoc + AutoHeadingIDs // Create the heading ID from the text + BackslashLineBreak // Translate trailing backslashes into line breaks + DefinitionLists // Render definition lists + + CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | + SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes + + CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | + Autolink | Strikethrough | SpaceHeadings | HeadingIDs | + BackslashLineBreak | DefinitionLists +) + +// ListType contains bitwise or'ed flags for list and list item objects. +type ListType int + +// These are the possible flag values for the ListItem renderer. +// Multiple flag values may be ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + ListTypeOrdered ListType = 1 << iota + ListTypeDefinition + ListTypeTerm + + ListItemContainsBlock + ListItemBeginningOfList // TODO: figure out if this is of any use now + ListItemEndOfList +) + +// CellAlignFlags holds a type of alignment in a table cell. +type CellAlignFlags int + +// These are the possible flag values for the table cell renderer. +// Only a single one of these values will be used; they are not ORed together. +// These are mostly of interest if you are writing a new output format. +const ( + TableAlignmentLeft CellAlignFlags = 1 << iota + TableAlignmentRight + TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) +) + +// The size of a tab stop. +const ( + TabSizeDefault = 4 + TabSizeDouble = 8 +) + +// blockTags is a set of tags that are recognized as HTML block tags. +// Any of these can be included in markdown text without special escaping. +var blockTags = map[string]struct{}{ + "blockquote": {}, + "del": {}, + "div": {}, + "dl": {}, + "fieldset": {}, + "form": {}, + "h1": {}, + "h2": {}, + "h3": {}, + "h4": {}, + "h5": {}, + "h6": {}, + "iframe": {}, + "ins": {}, + "math": {}, + "noscript": {}, + "ol": {}, + "pre": {}, + "p": {}, + "script": {}, + "style": {}, + "table": {}, + "ul": {}, + + // HTML5 + "address": {}, + "article": {}, + "aside": {}, + "canvas": {}, + "figcaption": {}, + "figure": {}, + "footer": {}, + "header": {}, + "hgroup": {}, + "main": {}, + "nav": {}, + "output": {}, + "progress": {}, + "section": {}, + "video": {}, +} + +// Renderer is the rendering interface. This is mostly of interest if you are +// implementing a new rendering format. +// +// Only an HTML implementation is provided in this repository, see the README +// for external implementations. +type Renderer interface { + // RenderNode is the main rendering method. It will be called once for + // every leaf node and twice for every non-leaf node (first with + // entering=true, then with entering=false). The method should write its + // rendition of the node to the supplied writer w. + RenderNode(w io.Writer, node *Node, entering bool) WalkStatus + + // RenderHeader is a method that allows the renderer to produce some + // content preceding the main body of the output document. The header is + // understood in the broad sense here. For example, the default HTML + // renderer will write not only the HTML document preamble, but also the + // table of contents if it was requested. + // + // The method will be passed an entire document tree, in case a particular + // implementation needs to inspect it to produce output. + // + // The output should be written to the supplied writer w. If your + // implementation has no header to write, supply an empty implementation. + RenderHeader(w io.Writer, ast *Node) + + // RenderFooter is a symmetric counterpart of RenderHeader. + RenderFooter(w io.Writer, ast *Node) +} + +// Callback functions for inline parsing. One such function is defined +// for each character that triggers a response when parsing inline data. +type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) + +// Markdown is a type that holds extensions and the runtime state used by +// Parse, and the renderer. You can not use it directly, construct it with New. +type Markdown struct { + renderer Renderer + referenceOverride ReferenceOverrideFunc + refs map[string]*reference + inlineCallback [256]inlineParser + extensions Extensions + nesting int + maxNesting int + insideLink bool + + // Footnotes need to be ordered as well as available to quickly check for + // presence. If a ref is also a footnote, it's stored both in refs and here + // in notes. Slice is nil if footnotes not enabled. + notes []*reference + + doc *Node + tip *Node // = doc + oldTip *Node + lastMatchedContainer *Node // = doc + allClosed bool +} + +func (p *Markdown) getRef(refid string) (ref *reference, found bool) { + if p.referenceOverride != nil { + r, overridden := p.referenceOverride(refid) + if overridden { + if r == nil { + return nil, false + } + return &reference{ + link: []byte(r.Link), + title: []byte(r.Title), + noteID: 0, + hasBlock: false, + text: []byte(r.Text)}, true + } + } + // refs are case insensitive + ref, found = p.refs[strings.ToLower(refid)] + return ref, found +} + +func (p *Markdown) finalize(block *Node) { + above := block.Parent + block.open = false + p.tip = above +} + +func (p *Markdown) addChild(node NodeType, offset uint32) *Node { + return p.addExistingChild(NewNode(node), offset) +} + +func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { + for !p.tip.canContain(node.Type) { + p.finalize(p.tip) + } + p.tip.AppendChild(node) + p.tip = node + return node +} + +func (p *Markdown) closeUnmatchedBlocks() { + if !p.allClosed { + for p.oldTip != p.lastMatchedContainer { + parent := p.oldTip.Parent + p.finalize(p.oldTip) + p.oldTip = parent + } + p.allClosed = true + } +} + +// +// +// Public interface +// +// + +// Reference represents the details of a link. +// See the documentation in Options for more details on use-case. +type Reference struct { + // Link is usually the URL the reference points to. + Link string + // Title is the alternate text describing the link in more detail. + Title string + // Text is the optional text to override the ref with if the syntax used was + // [refid][] + Text string +} + +// ReferenceOverrideFunc is expected to be called with a reference string and +// return either a valid Reference type that the reference string maps to or +// nil. If overridden is false, the default reference logic will be executed. +// See the documentation in Options for more details on use-case. +type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) + +// New constructs a Markdown processor. You can use the same With* functions as +// for Run() to customize parser's behavior and the renderer. +func New(opts ...Option) *Markdown { + var p Markdown + for _, opt := range opts { + opt(&p) + } + p.refs = make(map[string]*reference) + p.maxNesting = 16 + p.insideLink = false + docNode := NewNode(Document) + p.doc = docNode + p.tip = docNode + p.oldTip = docNode + p.lastMatchedContainer = docNode + p.allClosed = true + // register inline parsers + p.inlineCallback[' '] = maybeLineBreak + p.inlineCallback['*'] = emphasis + p.inlineCallback['_'] = emphasis + if p.extensions&Strikethrough != 0 { + p.inlineCallback['~'] = emphasis + } + p.inlineCallback['`'] = codeSpan + p.inlineCallback['\n'] = lineBreak + p.inlineCallback['['] = link + p.inlineCallback['<'] = leftAngle + p.inlineCallback['\\'] = escape + p.inlineCallback['&'] = entity + p.inlineCallback['!'] = maybeImage + p.inlineCallback['^'] = maybeInlineFootnote + if p.extensions&Autolink != 0 { + p.inlineCallback['h'] = maybeAutoLink + p.inlineCallback['m'] = maybeAutoLink + p.inlineCallback['f'] = maybeAutoLink + p.inlineCallback['H'] = maybeAutoLink + p.inlineCallback['M'] = maybeAutoLink + p.inlineCallback['F'] = maybeAutoLink + } + if p.extensions&Footnotes != 0 { + p.notes = make([]*reference, 0) + } + return &p +} + +// Option customizes the Markdown processor's default behavior. +type Option func(*Markdown) + +// WithRenderer allows you to override the default renderer. +func WithRenderer(r Renderer) Option { + return func(p *Markdown) { + p.renderer = r + } +} + +// WithExtensions allows you to pick some of the many extensions provided by +// Blackfriday. You can bitwise OR them. +func WithExtensions(e Extensions) Option { + return func(p *Markdown) { + p.extensions = e + } +} + +// WithNoExtensions turns off all extensions and custom behavior. +func WithNoExtensions() Option { + return func(p *Markdown) { + p.extensions = NoExtensions + p.renderer = NewHTMLRenderer(HTMLRendererParameters{ + Flags: HTMLFlagsNone, + }) + } +} + +// WithRefOverride sets an optional function callback that is called every +// time a reference is resolved. +// +// In Markdown, the link reference syntax can be made to resolve a link to +// a reference instead of an inline URL, in one of the following ways: +// +// * [link text][refid] +// * [refid][] +// +// Usually, the refid is defined at the bottom of the Markdown document. If +// this override function is provided, the refid is passed to the override +// function first, before consulting the defined refids at the bottom. If +// the override function indicates an override did not occur, the refids at +// the bottom will be used to fill in the link details. +func WithRefOverride(o ReferenceOverrideFunc) Option { + return func(p *Markdown) { + p.referenceOverride = o + } +} + +// Run is the main entry point to Blackfriday. It parses and renders a +// block of markdown-encoded text. +// +// The simplest invocation of Run takes one argument, input: +// output := Run(input) +// This will parse the input with CommonExtensions enabled and render it with +// the default HTMLRenderer (with CommonHTMLFlags). +// +// Variadic arguments opts can customize the default behavior. Since Markdown +// type does not contain exported fields, you can not use it directly. Instead, +// use the With* functions. For example, this will call the most basic +// functionality, with no extensions: +// output := Run(input, WithNoExtensions()) +// +// You can use any number of With* arguments, even contradicting ones. They +// will be applied in order of appearance and the latter will override the +// former: +// output := Run(input, WithNoExtensions(), WithExtensions(exts), +// WithRenderer(yourRenderer)) +func Run(input []byte, opts ...Option) []byte { + r := NewHTMLRenderer(HTMLRendererParameters{ + Flags: CommonHTMLFlags, + }) + optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} + optList = append(optList, opts...) + parser := New(optList...) + ast := parser.Parse(input) + var buf bytes.Buffer + parser.renderer.RenderHeader(&buf, ast) + ast.Walk(func(node *Node, entering bool) WalkStatus { + return parser.renderer.RenderNode(&buf, node, entering) + }) + parser.renderer.RenderFooter(&buf, ast) + return buf.Bytes() +} + +// Parse is an entry point to the parsing part of Blackfriday. It takes an +// input markdown document and produces a syntax tree for its contents. This +// tree can then be rendered with a default or custom renderer, or +// analyzed/transformed by the caller to whatever non-standard needs they have. +// The return value is the root node of the syntax tree. +func (p *Markdown) Parse(input []byte) *Node { + p.block(input) + // Walk the tree and finish up some of unfinished blocks + for p.tip != nil { + p.finalize(p.tip) + } + // Walk the tree again and process inline markdown in each block + p.doc.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) + p.parseRefsToAST() + return p.doc +} + +func (p *Markdown) parseRefsToAST() { + if p.extensions&Footnotes == 0 || len(p.notes) == 0 { + return + } + p.tip = p.doc + block := p.addBlock(List, nil) + block.IsFootnotesList = true + block.ListFlags = ListTypeOrdered + flags := ListItemBeginningOfList + // Note: this loop is intentionally explicit, not range-form. This is + // because the body of the loop will append nested footnotes to p.notes and + // we need to process those late additions. Range form would only walk over + // the fixed initial set. + for i := 0; i < len(p.notes); i++ { + ref := p.notes[i] + p.addExistingChild(ref.footnote, 0) + block := ref.footnote + block.ListFlags = flags | ListTypeOrdered + block.RefLink = ref.link + if ref.hasBlock { + flags |= ListItemContainsBlock + p.block(ref.title) + } else { + p.inline(block, ref.title) + } + flags &^= ListItemBeginningOfList | ListItemContainsBlock + } + above := block.Parent + finalizeList(block) + p.tip = above + block.Walk(func(node *Node, entering bool) WalkStatus { + if node.Type == Paragraph || node.Type == Heading { + p.inline(node, node.content) + node.content = nil + } + return GoToNext + }) +} + +// +// Link references +// +// This section implements support for references that (usually) appear +// as footnotes in a document, and can be referenced anywhere in the document. +// The basic format is: +// +// [1]: http://www.google.com/ "Google" +// [2]: http://www.github.com/ "Github" +// +// Anywhere in the document, the reference can be linked by referring to its +// label, i.e., 1 and 2 in this example, as in: +// +// This library is hosted on [Github][2], a git hosting site. +// +// Actual footnotes as specified in Pandoc and supported by some other Markdown +// libraries such as php-markdown are also taken care of. They look like this: +// +// This sentence needs a bit of further explanation.[^note] +// +// [^note]: This is the explanation. +// +// Footnotes should be placed at the end of the document in an ordered list. +// Finally, there are inline footnotes such as: +// +// Inline footnotes^[Also supported.] provide a quick inline explanation, +// but are rendered at the bottom of the document. +// + +// reference holds all information necessary for a reference-style links or +// footnotes. +// +// Consider this markdown with reference-style links: +// +// [link][ref] +// +// [ref]: /url/ "tooltip title" +// +// It will be ultimately converted to this HTML: +// +//

    link

    +// +// And a reference structure will be populated as follows: +// +// p.refs["ref"] = &reference{ +// link: "/url/", +// title: "tooltip title", +// } +// +// Alternatively, reference can contain information about a footnote. Consider +// this markdown: +// +// Text needing a footnote.[^a] +// +// [^a]: This is the note +// +// A reference structure will be populated as follows: +// +// p.refs["a"] = &reference{ +// link: "a", +// title: "This is the note", +// noteID: , +// } +// +// TODO: As you can see, it begs for splitting into two dedicated structures +// for refs and for footnotes. +type reference struct { + link []byte + title []byte + noteID int // 0 if not a footnote ref + hasBlock bool + footnote *Node // a link to the Item node within a list of footnotes + + text []byte // only gets populated by refOverride feature with Reference.Text +} + +func (r *reference) String() string { + return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", + r.link, r.title, r.text, r.noteID, r.hasBlock) +} + +// Check whether or not data starts with a reference link. +// If so, it is parsed and stored in the list of references +// (in the render struct). +// Returns the number of bytes to skip to move past it, +// or zero if the first line is not a reference. +func isReference(p *Markdown, data []byte, tabSize int) int { + // up to 3 optional leading spaces + if len(data) < 4 { + return 0 + } + i := 0 + for i < 3 && data[i] == ' ' { + i++ + } + + noteID := 0 + + // id part: anything but a newline between brackets + if data[i] != '[' { + return 0 + } + i++ + if p.extensions&Footnotes != 0 { + if i < len(data) && data[i] == '^' { + // we can set it to anything here because the proper noteIds will + // be assigned later during the second pass. It just has to be != 0 + noteID = 1 + i++ + } + } + idOffset := i + for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { + i++ + } + if i >= len(data) || data[i] != ']' { + return 0 + } + idEnd := i + // footnotes can have empty ID, like this: [^], but a reference can not be + // empty like this: []. Break early if it's not a footnote and there's no ID + if noteID == 0 && idOffset == idEnd { + return 0 + } + // spacer: colon (space | tab)* newline? (space | tab)* + i++ + if i >= len(data) || data[i] != ':' { + return 0 + } + i++ + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && (data[i] == '\n' || data[i] == '\r') { + i++ + if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { + i++ + } + } + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i >= len(data) { + return 0 + } + + var ( + linkOffset, linkEnd int + titleOffset, titleEnd int + lineEnd int + raw []byte + hasBlock bool + ) + + if p.extensions&Footnotes != 0 && noteID != 0 { + linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) + lineEnd = linkEnd + } else { + linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) + } + if lineEnd == 0 { + return 0 + } + + // a valid ref has been found + + ref := &reference{ + noteID: noteID, + hasBlock: hasBlock, + } + + if noteID > 0 { + // reusing the link field for the id since footnotes don't have links + ref.link = data[idOffset:idEnd] + // if footnote, it's not really a title, it's the contained text + ref.title = raw + } else { + ref.link = data[linkOffset:linkEnd] + ref.title = data[titleOffset:titleEnd] + } + + // id matches are case-insensitive + id := string(bytes.ToLower(data[idOffset:idEnd])) + + p.refs[id] = ref + + return lineEnd +} + +func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { + // link: whitespace-free sequence, optionally between angle brackets + if data[i] == '<' { + i++ + } + linkOffset = i + for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { + i++ + } + linkEnd = i + if data[linkOffset] == '<' && data[linkEnd-1] == '>' { + linkOffset++ + linkEnd-- + } + + // optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { + return + } + + // compute end-of-line + if i >= len(data) || data[i] == '\r' || data[i] == '\n' { + lineEnd = i + } + if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { + lineEnd++ + } + + // optional (space|tab)* spacer after a newline + if lineEnd > 0 { + i = lineEnd + 1 + for i < len(data) && (data[i] == ' ' || data[i] == '\t') { + i++ + } + } + + // optional title: any non-newline sequence enclosed in '"() alone on its line + if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { + i++ + titleOffset = i + + // look for EOL + for i < len(data) && data[i] != '\n' && data[i] != '\r' { + i++ + } + if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { + titleEnd = i + 1 + } else { + titleEnd = i + } + + // step back + i-- + for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { + i-- + } + if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { + lineEnd = titleEnd + titleEnd = i + } + } + + return +} + +// The first bit of this logic is the same as Parser.listItem, but the rest +// is much simpler. This function simply finds the entire block and shifts it +// over by one tab if it is indeed a block (just returns the line if it's not). +// blockEnd is the end of the section in the input buffer, and contents is the +// extracted text that was shifted over one tab. It will need to be rendered at +// the end of the document. +func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { + if i == 0 || len(data) == 0 { + return + } + + // skip leading whitespace on first line + for i < len(data) && data[i] == ' ' { + i++ + } + + blockStart = i + + // find the end of the line + blockEnd = i + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // get working buffer + var raw bytes.Buffer + + // put the first line into the working buffer + raw.Write(data[blockEnd:i]) + blockEnd = i + + // process the following lines + containsBlankLine := false + +gatherLines: + for blockEnd < len(data) { + i++ + + // find the end of this line + for i < len(data) && data[i-1] != '\n' { + i++ + } + + // if it is an empty line, guess that it is part of this item + // and move on to the next line + if p.isEmpty(data[blockEnd:i]) > 0 { + containsBlankLine = true + blockEnd = i + continue + } + + n := 0 + if n = isIndented(data[blockEnd:i], indentSize); n == 0 { + // this is the end of the block. + // we don't want to include this last line in the index. + break gatherLines + } + + // if there were blank lines before this one, insert a new one now + if containsBlankLine { + raw.WriteByte('\n') + containsBlankLine = false + } + + // get rid of that first tab, write to buffer + raw.Write(data[blockEnd+n : i]) + hasBlock = true + + blockEnd = i + } + + if data[blockEnd-1] != '\n' { + raw.WriteByte('\n') + } + + contents = raw.Bytes() + + return +} + +// +// +// Miscellaneous helper functions +// +// + +// Test if a character is a punctuation symbol. +// Taken from a private function in regexp in the stdlib. +func ispunct(c byte) bool { + for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { + if c == r { + return true + } + } + return false +} + +// Test if a character is a whitespace character. +func isspace(c byte) bool { + return ishorizontalspace(c) || isverticalspace(c) +} + +// Test if a character is a horizontal whitespace character. +func ishorizontalspace(c byte) bool { + return c == ' ' || c == '\t' +} + +// Test if a character is a vertical character. +func isverticalspace(c byte) bool { + return c == '\n' || c == '\r' || c == '\f' || c == '\v' +} + +// Test if a character is letter. +func isletter(c byte) bool { + return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') +} + +// Test if a character is a letter or a digit. +// TODO: check when this is looking for ASCII alnum and when it should use unicode +func isalnum(c byte) bool { + return (c >= '0' && c <= '9') || isletter(c) +} + +// Replace tab characters with spaces, aligning to the next TAB_SIZE column. +// always ends output with a newline +func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { + // first, check for common cases: no tabs, or only tabs at beginning of line + i, prefix := 0, 0 + slowcase := false + for i = 0; i < len(line); i++ { + if line[i] == '\t' { + if prefix == i { + prefix++ + } else { + slowcase = true + break + } + } + } + + // no need to decode runes if all tabs are at the beginning of the line + if !slowcase { + for i = 0; i < prefix*tabSize; i++ { + out.WriteByte(' ') + } + out.Write(line[prefix:]) + return + } + + // the slow case: we need to count runes to figure out how + // many spaces to insert for each tab + column := 0 + i = 0 + for i < len(line) { + start := i + for i < len(line) && line[i] != '\t' { + _, size := utf8.DecodeRune(line[i:]) + i += size + column++ + } + + if i > start { + out.Write(line[start:i]) + } + + if i >= len(line) { + break + } + + for { + out.WriteByte(' ') + column++ + if column%tabSize == 0 { + break + } + } + + i++ + } +} + +// Find if a line counts as indented or not. +// Returns number of characters the indent is (0 = not indented). +func isIndented(data []byte, indentSize int) int { + if len(data) == 0 { + return 0 + } + if data[0] == '\t' { + return 1 + } + if len(data) < indentSize { + return 0 + } + for i := 0; i < indentSize; i++ { + if data[i] != ' ' { + return 0 + } + } + return indentSize +} + +// Create a url-safe slug for fragments +func slugify(in []byte) []byte { + if len(in) == 0 { + return in + } + out := make([]byte, 0, len(in)) + sym := false + + for _, ch := range in { + if isalnum(ch) { + sym = false + out = append(out, ch) + } else if sym { + continue + } else { + out = append(out, '-') + sym = true + } + } + var a, b int + var ch byte + for a, ch = range out { + if ch != '-' { + break + } + } + for b = len(out) - 1; b > 0; b-- { + if out[b] != '-' { + break + } + } + return out[a : b+1] +} diff --git a/vendor/github.com/russross/blackfriday/v2/node.go b/vendor/github.com/russross/blackfriday/v2/node.go new file mode 100644 index 00000000..51b9e8c1 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/node.go @@ -0,0 +1,354 @@ +package blackfriday + +import ( + "bytes" + "fmt" +) + +// NodeType specifies a type of a single node of a syntax tree. Usually one +// node (and its type) corresponds to a single markdown feature, e.g. emphasis +// or code block. +type NodeType int + +// Constants for identifying different types of nodes. See NodeType. +const ( + Document NodeType = iota + BlockQuote + List + Item + Paragraph + Heading + HorizontalRule + Emph + Strong + Del + Link + Image + Text + HTMLBlock + CodeBlock + Softbreak + Hardbreak + Code + HTMLSpan + Table + TableCell + TableHead + TableBody + TableRow +) + +var nodeTypeNames = []string{ + Document: "Document", + BlockQuote: "BlockQuote", + List: "List", + Item: "Item", + Paragraph: "Paragraph", + Heading: "Heading", + HorizontalRule: "HorizontalRule", + Emph: "Emph", + Strong: "Strong", + Del: "Del", + Link: "Link", + Image: "Image", + Text: "Text", + HTMLBlock: "HTMLBlock", + CodeBlock: "CodeBlock", + Softbreak: "Softbreak", + Hardbreak: "Hardbreak", + Code: "Code", + HTMLSpan: "HTMLSpan", + Table: "Table", + TableCell: "TableCell", + TableHead: "TableHead", + TableBody: "TableBody", + TableRow: "TableRow", +} + +func (t NodeType) String() string { + return nodeTypeNames[t] +} + +// ListData contains fields relevant to a List and Item node type. +type ListData struct { + ListFlags ListType + Tight bool // Skip

    s around list item data if true + BulletChar byte // '*', '+' or '-' in bullet lists + Delimiter byte // '.' or ')' after the number in ordered lists + RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering + IsFootnotesList bool // This is a list of footnotes +} + +// LinkData contains fields relevant to a Link node type. +type LinkData struct { + Destination []byte // Destination is what goes into a href + Title []byte // Title is the tooltip thing that goes in a title attribute + NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote + Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. +} + +// CodeBlockData contains fields relevant to a CodeBlock node type. +type CodeBlockData struct { + IsFenced bool // Specifies whether it's a fenced code block or an indented one + Info []byte // This holds the info string + FenceChar byte + FenceLength int + FenceOffset int +} + +// TableCellData contains fields relevant to a TableCell node type. +type TableCellData struct { + IsHeader bool // This tells if it's under the header row + Align CellAlignFlags // This holds the value for align attribute +} + +// HeadingData contains fields relevant to a Heading node type. +type HeadingData struct { + Level int // This holds the heading level number + HeadingID string // This might hold heading ID, if present + IsTitleblock bool // Specifies whether it's a title block +} + +// Node is a single element in the abstract syntax tree of the parsed document. +// It holds connections to the structurally neighboring nodes and, for certain +// types of nodes, additional information that might be needed when rendering. +type Node struct { + Type NodeType // Determines the type of the node + Parent *Node // Points to the parent + FirstChild *Node // Points to the first child, if any + LastChild *Node // Points to the last child, if any + Prev *Node // Previous sibling; nil if it's the first child + Next *Node // Next sibling; nil if it's the last child + + Literal []byte // Text contents of the leaf nodes + + HeadingData // Populated if Type is Heading + ListData // Populated if Type is List + CodeBlockData // Populated if Type is CodeBlock + LinkData // Populated if Type is Link + TableCellData // Populated if Type is TableCell + + content []byte // Markdown content of the block nodes + open bool // Specifies an open block node that has not been finished to process yet +} + +// NewNode allocates a node of a specified type. +func NewNode(typ NodeType) *Node { + return &Node{ + Type: typ, + open: true, + } +} + +func (n *Node) String() string { + ellipsis := "" + snippet := n.Literal + if len(snippet) > 16 { + snippet = snippet[:16] + ellipsis = "..." + } + return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) +} + +// Unlink removes node 'n' from the tree. +// It panics if the node is nil. +func (n *Node) Unlink() { + if n.Prev != nil { + n.Prev.Next = n.Next + } else if n.Parent != nil { + n.Parent.FirstChild = n.Next + } + if n.Next != nil { + n.Next.Prev = n.Prev + } else if n.Parent != nil { + n.Parent.LastChild = n.Prev + } + n.Parent = nil + n.Next = nil + n.Prev = nil +} + +// AppendChild adds a node 'child' as a child of 'n'. +// It panics if either node is nil. +func (n *Node) AppendChild(child *Node) { + child.Unlink() + child.Parent = n + if n.LastChild != nil { + n.LastChild.Next = child + child.Prev = n.LastChild + n.LastChild = child + } else { + n.FirstChild = child + n.LastChild = child + } +} + +// InsertBefore inserts 'sibling' immediately before 'n'. +// It panics if either node is nil. +func (n *Node) InsertBefore(sibling *Node) { + sibling.Unlink() + sibling.Prev = n.Prev + if sibling.Prev != nil { + sibling.Prev.Next = sibling + } + sibling.Next = n + n.Prev = sibling + sibling.Parent = n.Parent + if sibling.Prev == nil { + sibling.Parent.FirstChild = sibling + } +} + +func (n *Node) isContainer() bool { + switch n.Type { + case Document: + fallthrough + case BlockQuote: + fallthrough + case List: + fallthrough + case Item: + fallthrough + case Paragraph: + fallthrough + case Heading: + fallthrough + case Emph: + fallthrough + case Strong: + fallthrough + case Del: + fallthrough + case Link: + fallthrough + case Image: + fallthrough + case Table: + fallthrough + case TableHead: + fallthrough + case TableBody: + fallthrough + case TableRow: + fallthrough + case TableCell: + return true + default: + return false + } +} + +func (n *Node) canContain(t NodeType) bool { + if n.Type == List { + return t == Item + } + if n.Type == Document || n.Type == BlockQuote || n.Type == Item { + return t != Item + } + if n.Type == Table { + return t == TableHead || t == TableBody + } + if n.Type == TableHead || n.Type == TableBody { + return t == TableRow + } + if n.Type == TableRow { + return t == TableCell + } + return false +} + +// WalkStatus allows NodeVisitor to have some control over the tree traversal. +// It is returned from NodeVisitor and different values allow Node.Walk to +// decide which node to go to next. +type WalkStatus int + +const ( + // GoToNext is the default traversal of every node. + GoToNext WalkStatus = iota + // SkipChildren tells walker to skip all children of current node. + SkipChildren + // Terminate tells walker to terminate the traversal. + Terminate +) + +// NodeVisitor is a callback to be called when traversing the syntax tree. +// Called twice for every node: once with entering=true when the branch is +// first visited, then with entering=false after all the children are done. +type NodeVisitor func(node *Node, entering bool) WalkStatus + +// Walk is a convenience method that instantiates a walker and starts a +// traversal of subtree rooted at n. +func (n *Node) Walk(visitor NodeVisitor) { + w := newNodeWalker(n) + for w.current != nil { + status := visitor(w.current, w.entering) + switch status { + case GoToNext: + w.next() + case SkipChildren: + w.entering = false + w.next() + case Terminate: + return + } + } +} + +type nodeWalker struct { + current *Node + root *Node + entering bool +} + +func newNodeWalker(root *Node) *nodeWalker { + return &nodeWalker{ + current: root, + root: root, + entering: true, + } +} + +func (nw *nodeWalker) next() { + if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { + nw.current = nil + return + } + if nw.entering && nw.current.isContainer() { + if nw.current.FirstChild != nil { + nw.current = nw.current.FirstChild + nw.entering = true + } else { + nw.entering = false + } + } else if nw.current.Next == nil { + nw.current = nw.current.Parent + nw.entering = false + } else { + nw.current = nw.current.Next + nw.entering = true + } +} + +func dump(ast *Node) { + fmt.Println(dumpString(ast)) +} + +func dumpR(ast *Node, depth int) string { + if ast == nil { + return "" + } + indent := bytes.Repeat([]byte("\t"), depth) + content := ast.Literal + if content == nil { + content = ast.content + } + result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) + for n := ast.FirstChild; n != nil; n = n.Next { + result += dumpR(n, depth+1) + } + return result +} + +func dumpString(ast *Node) string { + return dumpR(ast, 0) +} diff --git a/vendor/github.com/russross/blackfriday/v2/smartypants.go b/vendor/github.com/russross/blackfriday/v2/smartypants.go new file mode 100644 index 00000000..3a220e94 --- /dev/null +++ b/vendor/github.com/russross/blackfriday/v2/smartypants.go @@ -0,0 +1,457 @@ +// +// Blackfriday Markdown Processor +// Available at http://github.com/russross/blackfriday +// +// Copyright © 2011 Russ Ross . +// Distributed under the Simplified BSD License. +// See README.md for details. +// + +// +// +// SmartyPants rendering +// +// + +package blackfriday + +import ( + "bytes" + "io" +) + +// SPRenderer is a struct containing state of a Smartypants renderer. +type SPRenderer struct { + inSingleQuote bool + inDoubleQuote bool + callbacks [256]smartCallback +} + +func wordBoundary(c byte) bool { + return c == 0 || isspace(c) || ispunct(c) +} + +func tolower(c byte) byte { + if c >= 'A' && c <= 'Z' { + return c - 'A' + 'a' + } + return c +} + +func isdigit(c byte) bool { + return c >= '0' && c <= '9' +} + +func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { + // edge of the buffer is likely to be a tag that we don't get to see, + // so we treat it like text sometimes + + // enumerate all sixteen possibilities for (previousChar, nextChar) + // each can be one of {0, space, punct, other} + switch { + case previousChar == 0 && nextChar == 0: + // context is not any help here, so toggle + *isOpen = !*isOpen + case isspace(previousChar) && nextChar == 0: + // [ "] might be [ "foo...] + *isOpen = true + case ispunct(previousChar) && nextChar == 0: + // [!"] hmm... could be [Run!"] or [("...] + *isOpen = false + case /* isnormal(previousChar) && */ nextChar == 0: + // [a"] is probably a close + *isOpen = false + case previousChar == 0 && isspace(nextChar): + // [" ] might be [...foo" ] + *isOpen = false + case isspace(previousChar) && isspace(nextChar): + // [ " ] context is not any help here, so toggle + *isOpen = !*isOpen + case ispunct(previousChar) && isspace(nextChar): + // [!" ] is probably a close + *isOpen = false + case /* isnormal(previousChar) && */ isspace(nextChar): + // [a" ] this is one of the easy cases + *isOpen = false + case previousChar == 0 && ispunct(nextChar): + // ["!] hmm... could be ["$1.95] or ["!...] + *isOpen = false + case isspace(previousChar) && ispunct(nextChar): + // [ "!] looks more like [ "$1.95] + *isOpen = true + case ispunct(previousChar) && ispunct(nextChar): + // [!"!] context is not any help here, so toggle + *isOpen = !*isOpen + case /* isnormal(previousChar) && */ ispunct(nextChar): + // [a"!] is probably a close + *isOpen = false + case previousChar == 0 /* && isnormal(nextChar) */ : + // ["a] is probably an open + *isOpen = true + case isspace(previousChar) /* && isnormal(nextChar) */ : + // [ "a] this is one of the easy cases + *isOpen = true + case ispunct(previousChar) /* && isnormal(nextChar) */ : + // [!"a] is probably an open + *isOpen = true + default: + // [a'b] maybe a contraction? + *isOpen = false + } + + // Note that with the limited lookahead, this non-breaking + // space will also be appended to single double quotes. + if addNBSP && !*isOpen { + out.WriteString(" ") + } + + out.WriteByte('&') + if *isOpen { + out.WriteByte('l') + } else { + out.WriteByte('r') + } + out.WriteByte(quote) + out.WriteString("quo;") + + if addNBSP && *isOpen { + out.WriteString(" ") + } + + return true +} + +func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + t1 := tolower(text[1]) + + if t1 == '\'' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { + out.WriteString("’") + return 0 + } + + if len(text) >= 3 { + t2 := tolower(text[2]) + + if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && + (len(text) < 4 || wordBoundary(text[3])) { + out.WriteString("’") + return 0 + } + } + } + + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { + return 0 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 { + t1 := tolower(text[1]) + t2 := tolower(text[2]) + + if t1 == 'c' && t2 == ')' { + out.WriteString("©") + return 2 + } + + if t1 == 'r' && t2 == ')' { + out.WriteString("®") + return 2 + } + + if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { + out.WriteString("™") + return 3 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 { + if text[1] == '-' { + out.WriteString("—") + return 1 + } + + if wordBoundary(previousChar) && wordBoundary(text[1]) { + out.WriteString("–") + return 0 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '-' && text[2] == '-' { + out.WriteString("—") + return 2 + } + if len(text) >= 2 && text[1] == '-' { + out.WriteString("–") + return 1 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { + if bytes.HasPrefix(text, []byte(""")) { + nextChar := byte(0) + if len(text) >= 7 { + nextChar = text[6] + } + if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { + return 5 + } + } + + if bytes.HasPrefix(text, []byte("�")) { + return 3 + } + + out.WriteByte('&') + return 0 +} + +func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { + var quote byte = 'd' + if angledQuotes { + quote = 'a' + } + + return func(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) + } +} + +func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 3 && text[1] == '.' && text[2] == '.' { + out.WriteString("…") + return 2 + } + + if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { + out.WriteString("…") + return 4 + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { + if len(text) >= 2 && text[1] == '`' { + nextChar := byte(0) + if len(text) >= 3 { + nextChar = text[2] + } + if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { + return 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b + // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) + // and avoid changing dates like 1/23/2005 into fractions. + numEnd := 0 + for len(text) > numEnd && isdigit(text[numEnd]) { + numEnd++ + } + if numEnd == 0 { + out.WriteByte(text[0]) + return 0 + } + denStart := numEnd + 1 + if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { + denStart = numEnd + 3 + } else if len(text) < numEnd+2 || text[numEnd] != '/' { + out.WriteByte(text[0]) + return 0 + } + denEnd := denStart + for len(text) > denEnd && isdigit(text[denEnd]) { + denEnd++ + } + if denEnd == denStart { + out.WriteByte(text[0]) + return 0 + } + if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { + out.WriteString("") + out.Write(text[:numEnd]) + out.WriteString("") + out.Write(text[denStart:denEnd]) + out.WriteString("") + return denEnd - 1 + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { + if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { + if text[0] == '1' && text[1] == '/' && text[2] == '2' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { + out.WriteString("½") + return 2 + } + } + + if text[0] == '1' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { + out.WriteString("¼") + return 2 + } + } + + if text[0] == '3' && text[1] == '/' && text[2] == '4' { + if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { + out.WriteString("¾") + return 2 + } + } + } + + out.WriteByte(text[0]) + return 0 +} + +func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { + nextChar := byte(0) + if len(text) > 1 { + nextChar = text[1] + } + if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { + out.WriteString(""") + } + + return 0 +} + +func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') +} + +func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { + return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') +} + +func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { + i := 0 + + for i < len(text) && text[i] != '>' { + i++ + } + + out.Write(text[:i+1]) + return i +} + +type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int + +// NewSmartypantsRenderer constructs a Smartypants renderer object. +func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { + var ( + r SPRenderer + + smartAmpAngled = r.smartAmp(true, false) + smartAmpAngledNBSP = r.smartAmp(true, true) + smartAmpRegular = r.smartAmp(false, false) + smartAmpRegularNBSP = r.smartAmp(false, true) + + addNBSP = flags&SmartypantsQuotesNBSP != 0 + ) + + if flags&SmartypantsAngledQuotes == 0 { + r.callbacks['"'] = r.smartDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpRegular + } else { + r.callbacks['&'] = smartAmpRegularNBSP + } + } else { + r.callbacks['"'] = r.smartAngledDoubleQuote + if !addNBSP { + r.callbacks['&'] = smartAmpAngled + } else { + r.callbacks['&'] = smartAmpAngledNBSP + } + } + r.callbacks['\''] = r.smartSingleQuote + r.callbacks['('] = r.smartParens + if flags&SmartypantsDashes != 0 { + if flags&SmartypantsLatexDashes == 0 { + r.callbacks['-'] = r.smartDash + } else { + r.callbacks['-'] = r.smartDashLatex + } + } + r.callbacks['.'] = r.smartPeriod + if flags&SmartypantsFractions == 0 { + r.callbacks['1'] = r.smartNumber + r.callbacks['3'] = r.smartNumber + } else { + for ch := '1'; ch <= '9'; ch++ { + r.callbacks[ch] = r.smartNumberGeneric + } + } + r.callbacks['<'] = r.smartLeftAngle + r.callbacks['`'] = r.smartBacktick + return &r +} + +// Process is the entry point of the Smartypants renderer. +func (r *SPRenderer) Process(w io.Writer, text []byte) { + mark := 0 + for i := 0; i < len(text); i++ { + if action := r.callbacks[text[i]]; action != nil { + if i > mark { + w.Write(text[mark:i]) + } + previousChar := byte(0) + if i > 0 { + previousChar = text[i-1] + } + var tmp bytes.Buffer + i += action(&tmp, previousChar, text[i:]) + w.Write(tmp.Bytes()) + mark = i + 1 + } + } + if mark < len(text) { + w.Write(text[mark:]) + } +} diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml new file mode 100644 index 00000000..93b1fcdb --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml @@ -0,0 +1,16 @@ +sudo: false +language: go +go: + - 1.x + - master +matrix: + allow_failures: + - go: master + fast_finish: true +install: + - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). +script: + - go get -t -v ./... + - diff -u <(echo -n) <(gofmt -d -s .) + - go tool vet . + - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE new file mode 100644 index 00000000..c35c17af --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 Dmitri Shuralyov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md new file mode 100644 index 00000000..670bf0fe --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md @@ -0,0 +1,36 @@ +sanitized_anchor_name +===================== + +[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) + +Package sanitized_anchor_name provides a func to create sanitized anchor names. + +Its logic can be reused by multiple packages to create interoperable anchor names +and links to those anchors. + +At this time, it does not try to ensure that generated anchor names +are unique, that responsibility falls on the caller. + +Installation +------------ + +```bash +go get -u github.com/shurcooL/sanitized_anchor_name +``` + +Example +------- + +```Go +anchorName := sanitized_anchor_name.Create("This is a header") + +fmt.Println(anchorName) + +// Output: +// this-is-a-header +``` + +License +------- + +- [MIT License](LICENSE) diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod new file mode 100644 index 00000000..1e255347 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod @@ -0,0 +1 @@ +module github.com/shurcooL/sanitized_anchor_name diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go new file mode 100644 index 00000000..6a77d124 --- /dev/null +++ b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go @@ -0,0 +1,29 @@ +// Package sanitized_anchor_name provides a func to create sanitized anchor names. +// +// Its logic can be reused by multiple packages to create interoperable anchor names +// and links to those anchors. +// +// At this time, it does not try to ensure that generated anchor names +// are unique, that responsibility falls on the caller. +package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" + +import "unicode" + +// Create returns a sanitized anchor name for the given text. +func Create(text string) string { + var anchorName []rune + var futureDash = false + for _, r := range text { + switch { + case unicode.IsLetter(r) || unicode.IsNumber(r): + if futureDash && len(anchorName) > 0 { + anchorName = append(anchorName, '-') + } + futureDash = false + anchorName = append(anchorName, unicode.ToLower(r)) + default: + futureDash = true + } + } + return string(anchorName) +} diff --git a/vendor/github.com/urfave/cli/v2/.flake8 b/vendor/github.com/urfave/cli/v2/.flake8 new file mode 100644 index 00000000..6deafc26 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 120 diff --git a/vendor/github.com/urfave/cli/v2/.gitignore b/vendor/github.com/urfave/cli/v2/.gitignore new file mode 100644 index 00000000..2d5e149b --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/.gitignore @@ -0,0 +1,7 @@ +*.coverprofile +*.orig +node_modules/ +vendor +.idea +internal/*/built-example +coverage.txt diff --git a/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md b/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md new file mode 100644 index 00000000..41ba294f --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md @@ -0,0 +1,74 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be +reviewed and investigated and will result in a response that is deemed necessary +and appropriate to the circumstances. The project team is obligated to maintain +confidentiality with regard to the reporter of an incident. Further details of +specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + diff --git a/vendor/github.com/urfave/cli/v2/LICENSE b/vendor/github.com/urfave/cli/v2/LICENSE new file mode 100644 index 00000000..42a597e2 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2016 Jeremy Saenz & Contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/urfave/cli/v2/README.md b/vendor/github.com/urfave/cli/v2/README.md new file mode 100644 index 00000000..c9237fbc --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/README.md @@ -0,0 +1,66 @@ +cli +=== + +[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) +[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) +[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) +[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) + +cli is a simple, fast, and fun package for building command line apps in Go. The +goal is to enable developers to write fast and distributable command line +applications in an expressive way. + +## Usage Documentation + +Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `master` branch, which is currently `v2`. + +- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) +- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) + +## Installation + +Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html). + +Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). + +### Using `v2` releases + +``` +$ GO111MODULE=on go get github.com/urfave/cli/v2 +``` + +```go +... +import ( + "github.com/urfave/cli/v2" // imports as package "cli" +) +... +``` + +### Using `v1` releases + +``` +$ GO111MODULE=on go get github.com/urfave/cli +``` + +```go +... +import ( + "github.com/urfave/cli" +) +... +``` + +### GOPATH + +Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can +be easily used: +``` +export PATH=$PATH:$GOPATH/bin +``` + +### Supported platforms + +cli is tested against multiple versions of Go on Linux, and against the latest +released version of Go on OS X and Windows. This project uses Github Actions for +builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). diff --git a/vendor/github.com/urfave/cli/v2/app.go b/vendor/github.com/urfave/cli/v2/app.go new file mode 100644 index 00000000..d0c8f84e --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/app.go @@ -0,0 +1,542 @@ +package cli + +import ( + "context" + "flag" + "fmt" + "io" + "os" + "path/filepath" + "sort" + "time" +) + +var ( + changeLogURL = "https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md" + appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) + contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." + errInvalidActionType = NewExitError("ERROR invalid Action type. "+ + fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ + fmt.Sprintf("See %s", appActionDeprecationURL), 2) +) + +// App is the main structure of a cli application. It is recommended that +// an app be created with the cli.NewApp() function +type App struct { + // The name of the program. Defaults to path.Base(os.Args[0]) + Name string + // Full name of command for help, defaults to Name + HelpName string + // Description of the program. + Usage string + // Text to override the USAGE section of help + UsageText string + // Description of the program argument format. + ArgsUsage string + // Version of the program + Version string + // Description of the program + Description string + // List of commands to execute + Commands []*Command + // List of flags to parse + Flags []Flag + // Boolean to enable bash completion commands + EnableBashCompletion bool + // Boolean to hide built-in help command and help flag + HideHelp bool + // Boolean to hide built-in help command but keep help flag. + // Ignored if HideHelp is true. + HideHelpCommand bool + // Boolean to hide built-in version flag and the VERSION section of help + HideVersion bool + // categories contains the categorized commands and is populated on app startup + categories CommandCategories + // An action to execute when the shell completion flag is set + BashComplete BashCompleteFunc + // An action to execute before any subcommands are run, but after the context is ready + // If a non-nil error is returned, no subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + // The action to execute when no subcommands are specified + Action ActionFunc + // Execute this function if the proper command cannot be found + CommandNotFound CommandNotFoundFunc + // Execute this function if an usage error occurs + OnUsageError OnUsageErrorFunc + // Compilation date + Compiled time.Time + // List of all authors who contributed + Authors []*Author + // Copyright of the binary if any + Copyright string + // Writer writer to write output to + Writer io.Writer + // ErrWriter writes error output + ErrWriter io.Writer + // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to + // function as a default, so this is optional. + ExitErrHandler ExitErrHandlerFunc + // Other custom info + Metadata map[string]interface{} + // Carries a function which returns app specific info. + ExtraInfo func() map[string]string + // CustomAppHelpTemplate the text template for app help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomAppHelpTemplate string + // Boolean to enable short-option handling so user can combine several + // single-character bool arguments into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool + + didSetup bool +} + +// Tries to find out when this binary was compiled. +// Returns the current time if it fails to find it. +func compileTime() time.Time { + info, err := os.Stat(os.Args[0]) + if err != nil { + return time.Now() + } + return info.ModTime() +} + +// NewApp creates a new cli Application with some reasonable defaults for Name, +// Usage, Version and Action. +func NewApp() *App { + return &App{ + Name: filepath.Base(os.Args[0]), + HelpName: filepath.Base(os.Args[0]), + Usage: "A new cli application", + UsageText: "", + BashComplete: DefaultAppComplete, + Action: helpCommand.Action, + Compiled: compileTime(), + Writer: os.Stdout, + } +} + +// Setup runs initialization code to ensure all data structures are ready for +// `Run` or inspection prior to `Run`. It is internally called by `Run`, but +// will return early if setup has already happened. +func (a *App) Setup() { + if a.didSetup { + return + } + + a.didSetup = true + + if a.Name == "" { + a.Name = filepath.Base(os.Args[0]) + } + + if a.HelpName == "" { + a.HelpName = filepath.Base(os.Args[0]) + } + + if a.Usage == "" { + a.Usage = "A new cli application" + } + + if a.Version == "" { + a.HideVersion = true + } + + if a.BashComplete == nil { + a.BashComplete = DefaultAppComplete + } + + if a.Action == nil { + a.Action = helpCommand.Action + } + + if a.Compiled == (time.Time{}) { + a.Compiled = compileTime() + } + + if a.Writer == nil { + a.Writer = os.Stdout + } + + var newCommands []*Command + + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCommands = append(newCommands, c) + } + a.Commands = newCommands + + if a.Command(helpCommand.Name) == nil && !a.HideHelp { + if !a.HideHelpCommand { + a.appendCommand(helpCommand) + } + + if HelpFlag != nil { + a.appendFlag(HelpFlag) + } + } + + if !a.HideVersion { + a.appendFlag(VersionFlag) + } + + a.categories = newCommandCategories() + for _, command := range a.Commands { + a.categories.AddCommand(command.Category, command) + } + sort.Sort(a.categories.(*commandCategories)) + + if a.Metadata == nil { + a.Metadata = make(map[string]interface{}) + } + + if a.Writer == nil { + a.Writer = os.Stdout + } +} + +func (a *App) newFlagSet() (*flag.FlagSet, error) { + return flagSet(a.Name, a.Flags) +} + +func (a *App) useShortOptionHandling() bool { + return a.UseShortOptionHandling +} + +// Run is the entry point to the cli app. Parses the arguments slice and routes +// to the proper flag/args combination +func (a *App) Run(arguments []string) (err error) { + return a.RunContext(context.Background(), arguments) +} + +// RunContext is like Run except it takes a Context that will be +// passed to its commands and sub-commands. Through this, you can +// propagate timeouts and cancellation requests +func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { + a.Setup() + + // handle the completion flag separately from the flagset since + // completion could be attempted after a flag, but before its value was put + // on the command line. this causes the flagset to interpret the completion + // flag name as the value of the flag before it which is undesirable + // note that we can only do this because the shell autocomplete function + // always appends the completion flag at the end of the command + shellComplete, arguments := checkShellCompleteFlag(a, arguments) + + set, err := a.newFlagSet() + if err != nil { + return err + } + + err = parseIter(set, a, arguments[1:], shellComplete) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, &Context{Context: ctx}) + if nerr != nil { + _, _ = fmt.Fprintln(a.Writer, nerr) + _ = ShowAppHelp(context) + return nerr + } + context.shellComplete = shellComplete + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err := a.OnUsageError(context, err, false) + a.handleExitCoder(context, err) + return err + } + _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + _ = ShowAppHelp(context) + return err + } + + if !a.HideHelp && checkHelp(context) { + _ = ShowAppHelp(context) + return nil + } + + if !a.HideVersion && checkVersion(context) { + ShowVersion(context) + return nil + } + + cerr := checkRequiredFlags(a.Flags, context) + if cerr != nil { + _ = ShowAppHelp(context) + return cerr + } + + if a.After != nil { + defer func() { + if afterErr := a.After(context); afterErr != nil { + if err != nil { + err = newMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + beforeErr := a.Before(context) + if beforeErr != nil { + _, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) + _ = ShowAppHelp(context) + a.handleExitCoder(context, beforeErr) + err = beforeErr + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + if a.Action == nil { + a.Action = helpCommand.Action + } + + // Run default Action + err = a.Action(context) + + a.handleExitCoder(context, err) + return err +} + +// RunAndExitOnError calls .Run() and exits non-zero if an error was returned +// +// Deprecated: instead you should return an error that fulfills cli.ExitCoder +// to cli.App.Run. This will cause the application to exit with the given eror +// code in the cli.ExitCoder +func (a *App) RunAndExitOnError() { + if err := a.Run(os.Args); err != nil { + _, _ = fmt.Fprintln(a.errWriter(), err) + OsExiter(1) + } +} + +// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to +// generate command-specific flags +func (a *App) RunAsSubcommand(ctx *Context) (err error) { + // Setup also handles HideHelp and HideHelpCommand + a.Setup() + + var newCmds []*Command + for _, c := range a.Commands { + if c.HelpName == "" { + c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) + } + newCmds = append(newCmds, c) + } + a.Commands = newCmds + + set, err := a.newFlagSet() + if err != nil { + return err + } + + err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) + nerr := normalizeFlags(a.Flags, set) + context := NewContext(a, set, ctx) + + if nerr != nil { + _, _ = fmt.Fprintln(a.Writer, nerr) + _, _ = fmt.Fprintln(a.Writer) + if len(a.Commands) > 0 { + _ = ShowSubcommandHelp(context) + } else { + _ = ShowCommandHelp(ctx, context.Args().First()) + } + return nerr + } + + if checkCompletions(context) { + return nil + } + + if err != nil { + if a.OnUsageError != nil { + err = a.OnUsageError(context, err, true) + a.handleExitCoder(context, err) + return err + } + _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) + _ = ShowSubcommandHelp(context) + return err + } + + if len(a.Commands) > 0 { + if checkSubcommandHelp(context) { + return nil + } + } else { + if checkCommandHelp(ctx, context.Args().First()) { + return nil + } + } + + cerr := checkRequiredFlags(a.Flags, context) + if cerr != nil { + _ = ShowSubcommandHelp(context) + return cerr + } + + if a.After != nil { + defer func() { + afterErr := a.After(context) + if afterErr != nil { + a.handleExitCoder(context, err) + if err != nil { + err = newMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if a.Before != nil { + beforeErr := a.Before(context) + if beforeErr != nil { + a.handleExitCoder(context, beforeErr) + err = beforeErr + return err + } + } + + args := context.Args() + if args.Present() { + name := args.First() + c := a.Command(name) + if c != nil { + return c.Run(context) + } + } + + // Run default Action + err = a.Action(context) + + a.handleExitCoder(context, err) + return err +} + +// Command returns the named command on App. Returns nil if the command does not exist +func (a *App) Command(name string) *Command { + for _, c := range a.Commands { + if c.HasName(name) { + return c + } + } + + return nil +} + +// VisibleCategories returns a slice of categories and commands that are +// Hidden=false +func (a *App) VisibleCategories() []CommandCategory { + ret := []CommandCategory{} + for _, category := range a.categories.Categories() { + if visible := func() CommandCategory { + if len(category.VisibleCommands()) > 0 { + return category + } + return nil + }(); visible != nil { + ret = append(ret, visible) + } + } + return ret +} + +// VisibleCommands returns a slice of the Commands with Hidden=false +func (a *App) VisibleCommands() []*Command { + var ret []*Command + for _, command := range a.Commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} + +// VisibleFlags returns a slice of the Flags with Hidden=false +func (a *App) VisibleFlags() []Flag { + return visibleFlags(a.Flags) +} + +func (a *App) errWriter() io.Writer { + // When the app ErrWriter is nil use the package level one. + if a.ErrWriter == nil { + return ErrWriter + } + + return a.ErrWriter +} + +func (a *App) appendFlag(fl Flag) { + if !hasFlag(a.Flags, fl) { + a.Flags = append(a.Flags, fl) + } +} + +func (a *App) appendCommand(c *Command) { + if !hasCommand(a.Commands, c) { + a.Commands = append(a.Commands, c) + } +} + +func (a *App) handleExitCoder(context *Context, err error) { + if a.ExitErrHandler != nil { + a.ExitErrHandler(context, err) + } else { + HandleExitCoder(err) + } +} + +// Author represents someone who has contributed to a cli project. +type Author struct { + Name string // The Authors name + Email string // The Authors email +} + +// String makes Author comply to the Stringer interface, to allow an easy print in the templating process +func (a *Author) String() string { + e := "" + if a.Email != "" { + e = " <" + a.Email + ">" + } + + return fmt.Sprintf("%v%v", a.Name, e) +} + +// HandleAction attempts to figure out which Action signature was used. If +// it's an ActionFunc or a func with the legacy signature for Action, the func +// is run! +func HandleAction(action interface{}, context *Context) (err error) { + switch a := action.(type) { + case ActionFunc: + return a(context) + case func(*Context) error: + return a(context) + case func(*Context): // deprecated function signature + a(context) + return nil + } + + return errInvalidActionType +} diff --git a/vendor/github.com/urfave/cli/v2/args.go b/vendor/github.com/urfave/cli/v2/args.go new file mode 100644 index 00000000..bd65c17b --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/args.go @@ -0,0 +1,54 @@ +package cli + +type Args interface { + // Get returns the nth argument, or else a blank string + Get(n int) string + // First returns the first argument, or else a blank string + First() string + // Tail returns the rest of the arguments (not the first one) + // or else an empty string slice + Tail() []string + // Len returns the length of the wrapped slice + Len() int + // Present checks if there are any arguments present + Present() bool + // Slice returns a copy of the internal slice + Slice() []string +} + +type args []string + +func (a *args) Get(n int) string { + if len(*a) > n { + return (*a)[n] + } + return "" +} + +func (a *args) First() string { + return a.Get(0) +} + +func (a *args) Tail() []string { + if a.Len() >= 2 { + tail := []string((*a)[1:]) + ret := make([]string, len(tail)) + copy(ret, tail) + return ret + } + return []string{} +} + +func (a *args) Len() int { + return len(*a) +} + +func (a *args) Present() bool { + return a.Len() != 0 +} + +func (a *args) Slice() []string { + ret := make([]string, len(*a)) + copy(ret, *a) + return ret +} diff --git a/vendor/github.com/urfave/cli/v2/category.go b/vendor/github.com/urfave/cli/v2/category.go new file mode 100644 index 00000000..867e3908 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/category.go @@ -0,0 +1,79 @@ +package cli + +// CommandCategories interface allows for category manipulation +type CommandCategories interface { + // AddCommand adds a command to a category, creating a new category if necessary. + AddCommand(category string, command *Command) + // categories returns a copy of the category slice + Categories() []CommandCategory +} + +type commandCategories []*commandCategory + +func newCommandCategories() CommandCategories { + ret := commandCategories([]*commandCategory{}) + return &ret +} + +func (c *commandCategories) Less(i, j int) bool { + return lexicographicLess((*c)[i].Name(), (*c)[j].Name()) +} + +func (c *commandCategories) Len() int { + return len(*c) +} + +func (c *commandCategories) Swap(i, j int) { + (*c)[i], (*c)[j] = (*c)[j], (*c)[i] +} + +func (c *commandCategories) AddCommand(category string, command *Command) { + for _, commandCategory := range []*commandCategory(*c) { + if commandCategory.name == category { + commandCategory.commands = append(commandCategory.commands, command) + return + } + } + newVal := append(*c, + &commandCategory{name: category, commands: []*Command{command}}) + *c = newVal +} + +func (c *commandCategories) Categories() []CommandCategory { + ret := make([]CommandCategory, len(*c)) + for i, cat := range *c { + ret[i] = cat + } + return ret +} + +// CommandCategory is a category containing commands. +type CommandCategory interface { + // Name returns the category name string + Name() string + // VisibleCommands returns a slice of the Commands with Hidden=false + VisibleCommands() []*Command +} + +type commandCategory struct { + name string + commands []*Command +} + +func (c *commandCategory) Name() string { + return c.name +} + +func (c *commandCategory) VisibleCommands() []*Command { + if c.commands == nil { + c.commands = []*Command{} + } + + var ret []*Command + for _, command := range c.commands { + if !command.Hidden { + ret = append(ret, command) + } + } + return ret +} diff --git a/vendor/github.com/urfave/cli/v2/cli.go b/vendor/github.com/urfave/cli/v2/cli.go new file mode 100644 index 00000000..62a5bc22 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/cli.go @@ -0,0 +1,23 @@ +// Package cli provides a minimal framework for creating and organizing command line +// Go applications. cli is designed to be easy to understand and write, the most simple +// cli application can be written as follows: +// func main() { +// (&cli.App{}).Run(os.Args) +// } +// +// Of course this application does not do much, so let's make this an actual application: +// func main() { +// app := &cli.App{ +// Name: "greet", +// Usage: "say a greeting", +// Action: func(c *cli.Context) error { +// fmt.Println("Greetings") +// return nil +// }, +// } +// +// app.Run(os.Args) +// } +package cli + +//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go diff --git a/vendor/github.com/urfave/cli/v2/command.go b/vendor/github.com/urfave/cli/v2/command.go new file mode 100644 index 00000000..95840f32 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/command.go @@ -0,0 +1,301 @@ +package cli + +import ( + "flag" + "fmt" + "sort" + "strings" +) + +// Command is a subcommand for a cli.App. +type Command struct { + // The name of the command + Name string + // A list of aliases for the command + Aliases []string + // A short description of the usage of this command + Usage string + // Custom text to show on USAGE section of help + UsageText string + // A longer explanation of how the command works + Description string + // A short description of the arguments of this command + ArgsUsage string + // The category the command is part of + Category string + // The function to call when checking for bash command completions + BashComplete BashCompleteFunc + // An action to execute before any sub-subcommands are run, but after the context is ready + // If a non-nil error is returned, no sub-subcommands are run + Before BeforeFunc + // An action to execute after any subcommands are run, but after the subcommand has finished + // It is run even if Action() panics + After AfterFunc + // The function to call when this command is invoked + Action ActionFunc + // Execute this function if a usage error occurs. + OnUsageError OnUsageErrorFunc + // List of child commands + Subcommands []*Command + // List of flags to parse + Flags []Flag + // Treat all flags as normal arguments if true + SkipFlagParsing bool + // Boolean to hide built-in help command and help flag + HideHelp bool + // Boolean to hide built-in help command but keep help flag + // Ignored if HideHelp is true. + HideHelpCommand bool + // Boolean to hide this command from help or completion + Hidden bool + // Boolean to enable short-option handling so user can combine several + // single-character bool arguments into one + // i.e. foobar -o -v -> foobar -ov + UseShortOptionHandling bool + + // Full name of command for help, defaults to full command name, including parent commands. + HelpName string + commandNamePath []string + + // CustomHelpTemplate the text template for the command help topic. + // cli.go uses text/template to render templates. You can + // render custom help text by setting this variable. + CustomHelpTemplate string +} + +type Commands []*Command + +type CommandsByName []*Command + +func (c CommandsByName) Len() int { + return len(c) +} + +func (c CommandsByName) Less(i, j int) bool { + return lexicographicLess(c[i].Name, c[j].Name) +} + +func (c CommandsByName) Swap(i, j int) { + c[i], c[j] = c[j], c[i] +} + +// FullName returns the full name of the command. +// For subcommands this ensures that parent commands are part of the command path +func (c *Command) FullName() string { + if c.commandNamePath == nil { + return c.Name + } + return strings.Join(c.commandNamePath, " ") +} + +// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags +func (c *Command) Run(ctx *Context) (err error) { + if len(c.Subcommands) > 0 { + return c.startApp(ctx) + } + + if !c.HideHelp && HelpFlag != nil { + // append help to flags + c.appendFlag(HelpFlag) + } + + if ctx.App.UseShortOptionHandling { + c.UseShortOptionHandling = true + } + + set, err := c.parseFlags(ctx.Args(), ctx.shellComplete) + + context := NewContext(ctx.App, set, ctx) + context.Command = c + if checkCommandCompletions(context, c.Name) { + return nil + } + + if err != nil { + if c.OnUsageError != nil { + err = c.OnUsageError(context, err, false) + context.App.handleExitCoder(context, err) + return err + } + _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) + _, _ = fmt.Fprintln(context.App.Writer) + _ = ShowCommandHelp(context, c.Name) + return err + } + + if checkCommandHelp(context, c.Name) { + return nil + } + + cerr := checkRequiredFlags(c.Flags, context) + if cerr != nil { + _ = ShowCommandHelp(context, c.Name) + return cerr + } + + if c.After != nil { + defer func() { + afterErr := c.After(context) + if afterErr != nil { + context.App.handleExitCoder(context, err) + if err != nil { + err = newMultiError(err, afterErr) + } else { + err = afterErr + } + } + }() + } + + if c.Before != nil { + err = c.Before(context) + if err != nil { + _ = ShowCommandHelp(context, c.Name) + context.App.handleExitCoder(context, err) + return err + } + } + + if c.Action == nil { + c.Action = helpSubcommand.Action + } + + context.Command = c + err = c.Action(context) + + if err != nil { + context.App.handleExitCoder(context, err) + } + return err +} + +func (c *Command) newFlagSet() (*flag.FlagSet, error) { + return flagSet(c.Name, c.Flags) +} + +func (c *Command) useShortOptionHandling() bool { + return c.UseShortOptionHandling +} + +func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { + set, err := c.newFlagSet() + if err != nil { + return nil, err + } + + if c.SkipFlagParsing { + return set, set.Parse(append([]string{"--"}, args.Tail()...)) + } + + err = parseIter(set, c, args.Tail(), shellComplete) + if err != nil { + return nil, err + } + + err = normalizeFlags(c.Flags, set) + if err != nil { + return nil, err + } + + return set, nil +} + +// Names returns the names including short names and aliases. +func (c *Command) Names() []string { + return append([]string{c.Name}, c.Aliases...) +} + +// HasName returns true if Command.Name matches given name +func (c *Command) HasName(name string) bool { + for _, n := range c.Names() { + if n == name { + return true + } + } + return false +} + +func (c *Command) startApp(ctx *Context) error { + app := &App{ + Metadata: ctx.App.Metadata, + Name: fmt.Sprintf("%s %s", ctx.App.Name, c.Name), + } + + if c.HelpName == "" { + app.HelpName = c.HelpName + } else { + app.HelpName = app.Name + } + + app.Usage = c.Usage + app.Description = c.Description + app.ArgsUsage = c.ArgsUsage + + // set CommandNotFound + app.CommandNotFound = ctx.App.CommandNotFound + app.CustomAppHelpTemplate = c.CustomHelpTemplate + + // set the flags and commands + app.Commands = c.Subcommands + app.Flags = c.Flags + app.HideHelp = c.HideHelp + app.HideHelpCommand = c.HideHelpCommand + + app.Version = ctx.App.Version + app.HideVersion = ctx.App.HideVersion + app.Compiled = ctx.App.Compiled + app.Writer = ctx.App.Writer + app.ErrWriter = ctx.App.ErrWriter + app.ExitErrHandler = ctx.App.ExitErrHandler + app.UseShortOptionHandling = ctx.App.UseShortOptionHandling + + app.categories = newCommandCategories() + for _, command := range c.Subcommands { + app.categories.AddCommand(command.Category, command) + } + + sort.Sort(app.categories.(*commandCategories)) + + // bash completion + app.EnableBashCompletion = ctx.App.EnableBashCompletion + if c.BashComplete != nil { + app.BashComplete = c.BashComplete + } + + // set the actions + app.Before = c.Before + app.After = c.After + if c.Action != nil { + app.Action = c.Action + } else { + app.Action = helpSubcommand.Action + } + app.OnUsageError = c.OnUsageError + + for index, cc := range app.Commands { + app.Commands[index].commandNamePath = []string{c.Name, cc.Name} + } + + return app.RunAsSubcommand(ctx) +} + +// VisibleFlags returns a slice of the Flags with Hidden=false +func (c *Command) VisibleFlags() []Flag { + return visibleFlags(c.Flags) +} + +func (c *Command) appendFlag(fl Flag) { + if !hasFlag(c.Flags, fl) { + c.Flags = append(c.Flags, fl) + } +} + +func hasCommand(commands []*Command, command *Command) bool { + for _, existing := range commands { + if command == existing { + return true + } + } + + return false +} diff --git a/vendor/github.com/urfave/cli/v2/context.go b/vendor/github.com/urfave/cli/v2/context.go new file mode 100644 index 00000000..74ed5191 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/context.go @@ -0,0 +1,273 @@ +package cli + +import ( + "context" + "errors" + "flag" + "fmt" + "strings" +) + +// Context is a type that is passed through to +// each Handler action in a cli application. Context +// can be used to retrieve context-specific args and +// parsed command-line options. +type Context struct { + context.Context + App *App + Command *Command + shellComplete bool + flagSet *flag.FlagSet + parentContext *Context +} + +// NewContext creates a new context. For use in when invoking an App or Command action. +func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { + c := &Context{App: app, flagSet: set, parentContext: parentCtx} + if parentCtx != nil { + c.Context = parentCtx.Context + c.shellComplete = parentCtx.shellComplete + if parentCtx.flagSet == nil { + parentCtx.flagSet = &flag.FlagSet{} + } + } + + c.Command = &Command{} + + if c.Context == nil { + c.Context = context.Background() + } + + return c +} + +// NumFlags returns the number of flags set +func (c *Context) NumFlags() int { + return c.flagSet.NFlag() +} + +// Set sets a context flag to a value. +func (c *Context) Set(name, value string) error { + return c.flagSet.Set(name, value) +} + +// IsSet determines if the flag was actually set +func (c *Context) IsSet(name string) bool { + if fs := lookupFlagSet(name, c); fs != nil { + if fs := lookupFlagSet(name, c); fs != nil { + isSet := false + fs.Visit(func(f *flag.Flag) { + if f.Name == name { + isSet = true + } + }) + if isSet { + return true + } + } + + f := lookupFlag(name, c) + if f == nil { + return false + } + + return f.IsSet() + } + + return false +} + +// LocalFlagNames returns a slice of flag names used in this context. +func (c *Context) LocalFlagNames() []string { + var names []string + c.flagSet.Visit(makeFlagNameVisitor(&names)) + return names +} + +// FlagNames returns a slice of flag names used by the this context and all of +// its parent contexts. +func (c *Context) FlagNames() []string { + var names []string + for _, ctx := range c.Lineage() { + ctx.flagSet.Visit(makeFlagNameVisitor(&names)) + } + return names +} + +// Lineage returns *this* context and all of its ancestor contexts in order from +// child to parent +func (c *Context) Lineage() []*Context { + var lineage []*Context + + for cur := c; cur != nil; cur = cur.parentContext { + lineage = append(lineage, cur) + } + + return lineage +} + +// Value returns the value of the flag corresponding to `name` +func (c *Context) Value(name string) interface{} { + return c.flagSet.Lookup(name).Value.(flag.Getter).Get() +} + +// Args returns the command line arguments associated with the context. +func (c *Context) Args() Args { + ret := args(c.flagSet.Args()) + return &ret +} + +// NArg returns the number of the command line arguments. +func (c *Context) NArg() int { + return c.Args().Len() +} + +func lookupFlag(name string, ctx *Context) Flag { + for _, c := range ctx.Lineage() { + if c.Command == nil { + continue + } + + for _, f := range c.Command.Flags { + for _, n := range f.Names() { + if n == name { + return f + } + } + } + } + + if ctx.App != nil { + for _, f := range ctx.App.Flags { + for _, n := range f.Names() { + if n == name { + return f + } + } + } + } + + return nil +} + +func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { + for _, c := range ctx.Lineage() { + if f := c.flagSet.Lookup(name); f != nil { + return c.flagSet + } + } + + return nil +} + +func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { + switch ff.Value.(type) { + case Serializer: + _ = set.Set(name, ff.Value.(Serializer).Serialize()) + default: + _ = set.Set(name, ff.Value.String()) + } +} + +func normalizeFlags(flags []Flag, set *flag.FlagSet) error { + visited := make(map[string]bool) + set.Visit(func(f *flag.Flag) { + visited[f.Name] = true + }) + for _, f := range flags { + parts := f.Names() + if len(parts) == 1 { + continue + } + var ff *flag.Flag + for _, name := range parts { + name = strings.Trim(name, " ") + if visited[name] { + if ff != nil { + return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) + } + ff = set.Lookup(name) + } + } + if ff == nil { + continue + } + for _, name := range parts { + name = strings.Trim(name, " ") + if !visited[name] { + copyFlag(name, ff, set) + } + } + } + return nil +} + +func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { + return func(f *flag.Flag) { + nameParts := strings.Split(f.Name, ",") + name := strings.TrimSpace(nameParts[0]) + + for _, part := range nameParts { + part = strings.TrimSpace(part) + if len(part) > len(name) { + name = part + } + } + + if name != "" { + *names = append(*names, name) + } + } +} + +type requiredFlagsErr interface { + error + getMissingFlags() []string +} + +type errRequiredFlags struct { + missingFlags []string +} + +func (e *errRequiredFlags) Error() string { + numberOfMissingFlags := len(e.missingFlags) + if numberOfMissingFlags == 1 { + return fmt.Sprintf("Required flag %q not set", e.missingFlags[0]) + } + joinedMissingFlags := strings.Join(e.missingFlags, ", ") + return fmt.Sprintf("Required flags %q not set", joinedMissingFlags) +} + +func (e *errRequiredFlags) getMissingFlags() []string { + return e.missingFlags +} + +func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr { + var missingFlags []string + for _, f := range flags { + if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { + var flagPresent bool + var flagName string + + for _, key := range f.Names() { + if len(key) > 1 { + flagName = key + } + + if context.IsSet(strings.TrimSpace(key)) { + flagPresent = true + } + } + + if !flagPresent && flagName != "" { + missingFlags = append(missingFlags, flagName) + } + } + } + + if len(missingFlags) != 0 { + return &errRequiredFlags{missingFlags: missingFlags} + } + + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/docs.go b/vendor/github.com/urfave/cli/v2/docs.go new file mode 100644 index 00000000..dc16fc82 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/docs.go @@ -0,0 +1,148 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "sort" + "strings" + "text/template" + + "github.com/cpuguy83/go-md2man/v2/md2man" +) + +// ToMarkdown creates a markdown string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToMarkdown() (string, error) { + var w bytes.Buffer + if err := a.writeDocTemplate(&w); err != nil { + return "", err + } + return w.String(), nil +} + +// ToMan creates a man page string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToMan() (string, error) { + var w bytes.Buffer + if err := a.writeDocTemplate(&w); err != nil { + return "", err + } + man := md2man.Render(w.Bytes()) + return string(man), nil +} + +type cliTemplate struct { + App *App + Commands []string + GlobalArgs []string + SynopsisArgs []string +} + +func (a *App) writeDocTemplate(w io.Writer) error { + const name = "cli" + t, err := template.New(name).Parse(MarkdownDocTemplate) + if err != nil { + return err + } + return t.ExecuteTemplate(w, name, &cliTemplate{ + App: a, + Commands: prepareCommands(a.Commands, 0), + GlobalArgs: prepareArgsWithValues(a.VisibleFlags()), + SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()), + }) +} + +func prepareCommands(commands []*Command, level int) []string { + var coms []string + for _, command := range commands { + if command.Hidden { + continue + } + usage := "" + if command.Usage != "" { + usage = command.Usage + } + + prepared := fmt.Sprintf("%s %s\n\n%s\n", + strings.Repeat("#", level+2), + strings.Join(command.Names(), ", "), + usage, + ) + + flags := prepareArgsWithValues(command.Flags) + if len(flags) > 0 { + prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) + } + + coms = append(coms, prepared) + + // recursevly iterate subcommands + if len(command.Subcommands) > 0 { + coms = append( + coms, + prepareCommands(command.Subcommands, level+1)..., + ) + } + } + + return coms +} + +func prepareArgsWithValues(flags []Flag) []string { + return prepareFlags(flags, ", ", "**", "**", `""`, true) +} + +func prepareArgsSynopsis(flags []Flag) []string { + return prepareFlags(flags, "|", "[", "]", "[value]", false) +} + +func prepareFlags( + flags []Flag, + sep, opener, closer, value string, + addDetails bool, +) []string { + args := []string{} + for _, f := range flags { + flag, ok := f.(DocGenerationFlag) + if !ok { + continue + } + modifiedArg := opener + + for _, s := range flag.Names() { + trimmed := strings.TrimSpace(s) + if len(modifiedArg) > len(opener) { + modifiedArg += sep + } + if len(trimmed) > 1 { + modifiedArg += fmt.Sprintf("--%s", trimmed) + } else { + modifiedArg += fmt.Sprintf("-%s", trimmed) + } + } + modifiedArg += closer + if flag.TakesValue() { + modifiedArg += fmt.Sprintf("=%s", value) + } + + if addDetails { + modifiedArg += flagDetails(flag) + } + + args = append(args, modifiedArg+"\n") + + } + sort.Strings(args) + return args +} + +// flagDetails returns a string containing the flags metadata +func flagDetails(flag DocGenerationFlag) string { + description := flag.GetUsage() + value := flag.GetValue() + if value != "" { + description += " (default: " + value + ")" + } + return ": " + description +} diff --git a/vendor/github.com/urfave/cli/v2/errors.go b/vendor/github.com/urfave/cli/v2/errors.go new file mode 100644 index 00000000..344b4361 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/errors.go @@ -0,0 +1,131 @@ +package cli + +import ( + "fmt" + "io" + "os" + "strings" +) + +// OsExiter is the function used when the app exits. If not set defaults to os.Exit. +var OsExiter = os.Exit + +// ErrWriter is used to write errors to the user. This can be anything +// implementing the io.Writer interface and defaults to os.Stderr. +var ErrWriter io.Writer = os.Stderr + +// MultiError is an error that wraps multiple errors. +type MultiError interface { + error + // Errors returns a copy of the errors slice + Errors() []error +} + +// NewMultiError creates a new MultiError. Pass in one or more errors. +func newMultiError(err ...error) MultiError { + ret := multiError(err) + return &ret +} + +type multiError []error + +// Error implements the error interface. +func (m *multiError) Error() string { + errs := make([]string, len(*m)) + for i, err := range *m { + errs[i] = err.Error() + } + + return strings.Join(errs, "\n") +} + +// Errors returns a copy of the errors slice +func (m *multiError) Errors() []error { + errs := make([]error, len(*m)) + for _, err := range *m { + errs = append(errs, err) + } + return errs +} + +// ErrorFormatter is the interface that will suitably format the error output +type ErrorFormatter interface { + Format(s fmt.State, verb rune) +} + +// ExitCoder is the interface checked by `App` and `Command` for a custom exit +// code +type ExitCoder interface { + error + ExitCode() int +} + +type exitError struct { + exitCode int + message interface{} +} + +// NewExitError makes a new *exitError +func NewExitError(message interface{}, exitCode int) ExitCoder { + return Exit(message, exitCode) +} + +// Exit wraps a message and exit code into an ExitCoder suitable for handling by +// HandleExitCoder +func Exit(message interface{}, exitCode int) ExitCoder { + return &exitError{ + message: message, + exitCode: exitCode, + } +} + +func (ee *exitError) Error() string { + return fmt.Sprintf("%v", ee.message) +} + +func (ee *exitError) ExitCode() int { + return ee.exitCode +} + +// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if +// so prints the error to stderr (if it is non-empty) and calls OsExiter with the +// given exit code. If the given error is a MultiError, then this func is +// called on all members of the Errors slice and calls OsExiter with the last exit code. +func HandleExitCoder(err error) { + if err == nil { + return + } + + if exitErr, ok := err.(ExitCoder); ok { + if err.Error() != "" { + if _, ok := exitErr.(ErrorFormatter); ok { + _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) + } else { + _, _ = fmt.Fprintln(ErrWriter, err) + } + } + OsExiter(exitErr.ExitCode()) + return + } + + if multiErr, ok := err.(MultiError); ok { + code := handleMultiError(multiErr) + OsExiter(code) + return + } +} + +func handleMultiError(multiErr MultiError) int { + code := 1 + for _, merr := range multiErr.Errors() { + if multiErr2, ok := merr.(MultiError); ok { + code = handleMultiError(multiErr2) + } else if merr != nil { + fmt.Fprintln(ErrWriter, merr) + if exitErr, ok := merr.(ExitCoder); ok { + code = exitErr.ExitCode() + } + } + } + return code +} diff --git a/vendor/github.com/urfave/cli/v2/fish.go b/vendor/github.com/urfave/cli/v2/fish.go new file mode 100644 index 00000000..67122c9f --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/fish.go @@ -0,0 +1,192 @@ +package cli + +import ( + "bytes" + "fmt" + "io" + "strings" + "text/template" +) + +// ToFishCompletion creates a fish completion string for the `*App` +// The function errors if either parsing or writing of the string fails. +func (a *App) ToFishCompletion() (string, error) { + var w bytes.Buffer + if err := a.writeFishCompletionTemplate(&w); err != nil { + return "", err + } + return w.String(), nil +} + +type fishCompletionTemplate struct { + App *App + Completions []string + AllCommands []string +} + +func (a *App) writeFishCompletionTemplate(w io.Writer) error { + const name = "cli" + t, err := template.New(name).Parse(FishCompletionTemplate) + if err != nil { + return err + } + allCommands := []string{} + + // Add global flags + completions := a.prepareFishFlags(a.VisibleFlags(), allCommands) + + // Add help flag + if !a.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, allCommands)..., + ) + } + + // Add version flag + if !a.HideVersion { + completions = append( + completions, + a.prepareFishFlags([]Flag{VersionFlag}, allCommands)..., + ) + } + + // Add commands and their flags + completions = append( + completions, + a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})..., + ) + + return t.ExecuteTemplate(w, name, &fishCompletionTemplate{ + App: a, + Completions: completions, + AllCommands: allCommands, + }) +} + +func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, previousCommands []string) []string { + completions := []string{} + for _, command := range commands { + if command.Hidden { + continue + } + + var completion strings.Builder + completion.WriteString(fmt.Sprintf( + "complete -r -c %s -n '%s' -a '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + strings.Join(command.Names(), " "), + )) + + if command.Usage != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", + escapeSingleQuotes(command.Usage))) + } + + if !command.HideHelp { + completions = append( + completions, + a.prepareFishFlags([]Flag{HelpFlag}, command.Names())..., + ) + } + + *allCommands = append(*allCommands, command.Names()...) + completions = append(completions, completion.String()) + completions = append( + completions, + a.prepareFishFlags(command.Flags, command.Names())..., + ) + + // recursevly iterate subcommands + if len(command.Subcommands) > 0 { + completions = append( + completions, + a.prepareFishCommands( + command.Subcommands, allCommands, command.Names(), + )..., + ) + } + } + + return completions +} + +func (a *App) prepareFishFlags(flags []Flag, previousCommands []string) []string { + completions := []string{} + for _, f := range flags { + flag, ok := f.(DocGenerationFlag) + if !ok { + continue + } + + completion := &strings.Builder{} + completion.WriteString(fmt.Sprintf( + "complete -c %s -n '%s'", + a.Name, + a.fishSubcommandHelper(previousCommands), + )) + + fishAddFileFlag(f, completion) + + for idx, opt := range flag.Names() { + if idx == 0 { + completion.WriteString(fmt.Sprintf( + " -l %s", strings.TrimSpace(opt), + )) + } else { + completion.WriteString(fmt.Sprintf( + " -s %s", strings.TrimSpace(opt), + )) + + } + } + + if flag.TakesValue() { + completion.WriteString(" -r") + } + + if flag.GetUsage() != "" { + completion.WriteString(fmt.Sprintf(" -d '%s'", + escapeSingleQuotes(flag.GetUsage()))) + } + + completions = append(completions, completion.String()) + } + + return completions +} + +func fishAddFileFlag(flag Flag, completion *strings.Builder) { + switch f := flag.(type) { + case *GenericFlag: + if f.TakesFile { + return + } + case *StringFlag: + if f.TakesFile { + return + } + case *StringSliceFlag: + if f.TakesFile { + return + } + } + completion.WriteString(" -f") +} + +func (a *App) fishSubcommandHelper(allCommands []string) string { + fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name) + if len(allCommands) > 0 { + fishHelper = fmt.Sprintf( + "__fish_seen_subcommand_from %s", + strings.Join(allCommands, " "), + ) + } + return fishHelper + +} + +func escapeSingleQuotes(input string) string { + return strings.Replace(input, `'`, `\'`, -1) +} diff --git a/vendor/github.com/urfave/cli/v2/flag.go b/vendor/github.com/urfave/cli/v2/flag.go new file mode 100644 index 00000000..ad97c2d0 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag.go @@ -0,0 +1,388 @@ +package cli + +import ( + "flag" + "fmt" + "io/ioutil" + "reflect" + "regexp" + "runtime" + "strconv" + "strings" + "syscall" + "time" +) + +const defaultPlaceholder = "value" + +var ( + slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) + + commaWhitespace = regexp.MustCompile("[, ]+.*") +) + +// BashCompletionFlag enables bash-completion for all commands and subcommands +var BashCompletionFlag Flag = &BoolFlag{ + Name: "generate-bash-completion", + Hidden: true, +} + +// VersionFlag prints the version for the application +var VersionFlag Flag = &BoolFlag{ + Name: "version", + Aliases: []string{"v"}, + Usage: "print the version", +} + +// HelpFlag prints the help for all commands and subcommands. +// Set to nil to disable the flag. The subcommand +// will still be added unless HideHelp or HideHelpCommand is set to true. +var HelpFlag Flag = &BoolFlag{ + Name: "help", + Aliases: []string{"h"}, + Usage: "show help", +} + +// FlagStringer converts a flag definition to a string. This is used by help +// to display a flag. +var FlagStringer FlagStringFunc = stringifyFlag + +// Serializer is used to circumvent the limitations of flag.FlagSet.Set +type Serializer interface { + Serialize() string +} + +// FlagNamePrefixer converts a full flag name and its placeholder into the help +// message flag prefix. This is used by the default FlagStringer. +var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames + +// FlagEnvHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagEnvHinter FlagEnvHintFunc = withEnvHint + +// FlagFileHinter annotates flag help message with the environment variable +// details. This is used by the default FlagStringer. +var FlagFileHinter FlagFileHintFunc = withFileHint + +// FlagsByName is a slice of Flag. +type FlagsByName []Flag + +func (f FlagsByName) Len() int { + return len(f) +} + +func (f FlagsByName) Less(i, j int) bool { + if len(f[j].Names()) == 0 { + return false + } else if len(f[i].Names()) == 0 { + return true + } + return lexicographicLess(f[i].Names()[0], f[j].Names()[0]) +} + +func (f FlagsByName) Swap(i, j int) { + f[i], f[j] = f[j], f[i] +} + +// Flag is a common interface related to parsing flags in cli. +// For more advanced flag parsing techniques, it is recommended that +// this interface be implemented. +type Flag interface { + fmt.Stringer + // Apply Flag settings to the given flag set + Apply(*flag.FlagSet) error + Names() []string + IsSet() bool +} + +// RequiredFlag is an interface that allows us to mark flags as required +// it allows flags required flags to be backwards compatible with the Flag interface +type RequiredFlag interface { + Flag + + IsRequired() bool +} + +// DocGenerationFlag is an interface that allows documentation generation for the flag +type DocGenerationFlag interface { + Flag + + // TakesValue returns true if the flag takes a value, otherwise false + TakesValue() bool + + // GetUsage returns the usage string for the flag + GetUsage() string + + // GetValue returns the flags value as string representation and an empty + // string if the flag takes no value at all. + GetValue() string +} + +func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { + set := flag.NewFlagSet(name, flag.ContinueOnError) + + for _, f := range flags { + if err := f.Apply(set); err != nil { + return nil, err + } + } + set.SetOutput(ioutil.Discard) + return set, nil +} + +func visibleFlags(fl []Flag) []Flag { + var visible []Flag + for _, f := range fl { + field := flagValue(f).FieldByName("Hidden") + if !field.IsValid() || !field.Bool() { + visible = append(visible, f) + } + } + return visible +} + +func prefixFor(name string) (prefix string) { + if len(name) == 1 { + prefix = "-" + } else { + prefix = "--" + } + + return +} + +// Returns the placeholder, if any, and the unquoted usage string. +func unquoteUsage(usage string) (string, string) { + for i := 0; i < len(usage); i++ { + if usage[i] == '`' { + for j := i + 1; j < len(usage); j++ { + if usage[j] == '`' { + name := usage[i+1 : j] + usage = usage[:i] + name + usage[j+1:] + return name, usage + } + } + break + } + } + return "", usage +} + +func prefixedNames(names []string, placeholder string) string { + var prefixed string + for i, name := range names { + if name == "" { + continue + } + + prefixed += prefixFor(name) + name + if placeholder != "" { + prefixed += " " + placeholder + } + if i < len(names)-1 { + prefixed += ", " + } + } + return prefixed +} + +func withEnvHint(envVars []string, str string) string { + envText := "" + if envVars != nil && len(envVars) > 0 { + prefix := "$" + suffix := "" + sep := ", $" + if runtime.GOOS == "windows" { + prefix = "%" + suffix = "%" + sep = "%, %" + } + + envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) + } + return str + envText +} + +func flagNames(name string, aliases []string) []string { + var ret []string + + for _, part := range append([]string{name}, aliases...) { + // v1 -> v2 migration warning zone: + // Strip off anything after the first found comma or space, which + // *hopefully* makes it a tiny bit more obvious that unexpected behavior is + // caused by using the v1 form of stringly typed "Name". + ret = append(ret, commaWhitespace.ReplaceAllString(part, "")) + } + + return ret +} + +func flagStringSliceField(f Flag, name string) []string { + fv := flagValue(f) + field := fv.FieldByName(name) + + if field.IsValid() { + return field.Interface().([]string) + } + + return []string{} +} + +func withFileHint(filePath, str string) string { + fileText := "" + if filePath != "" { + fileText = fmt.Sprintf(" [%s]", filePath) + } + return str + fileText +} + +func flagValue(f Flag) reflect.Value { + fv := reflect.ValueOf(f) + for fv.Kind() == reflect.Ptr { + fv = reflect.Indirect(fv) + } + return fv +} + +func formatDefault(format string) string { + return " (default: " + format + ")" +} + +func stringifyFlag(f Flag) string { + fv := flagValue(f) + + switch f := f.(type) { + case *IntSliceFlag: + return withEnvHint(flagStringSliceField(f, "EnvVars"), + stringifyIntSliceFlag(f)) + case *Int64SliceFlag: + return withEnvHint(flagStringSliceField(f, "EnvVars"), + stringifyInt64SliceFlag(f)) + case *Float64SliceFlag: + return withEnvHint(flagStringSliceField(f, "EnvVars"), + stringifyFloat64SliceFlag(f)) + case *StringSliceFlag: + return withEnvHint(flagStringSliceField(f, "EnvVars"), + stringifyStringSliceFlag(f)) + } + + placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) + + needsPlaceholder := false + defaultValueString := "" + val := fv.FieldByName("Value") + if val.IsValid() { + needsPlaceholder = val.Kind() != reflect.Bool + defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface()) + + if val.Kind() == reflect.String && val.String() != "" { + defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String()) + } + } + + helpText := fv.FieldByName("DefaultText") + if helpText.IsValid() && helpText.String() != "" { + needsPlaceholder = val.Kind() != reflect.Bool + defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String()) + } + + if defaultValueString == formatDefault("") { + defaultValueString = "" + } + + if needsPlaceholder && placeholder == "" { + placeholder = defaultPlaceholder + } + + usageWithDefault := strings.TrimSpace(usage + defaultValueString) + + return withEnvHint(flagStringSliceField(f, "EnvVars"), + fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) +} + +func stringifyIntSliceFlag(f *IntSliceFlag) string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.Itoa(i)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +func stringifyInt64SliceFlag(f *Int64SliceFlag) string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { + var defaultVals []string + + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, i := range f.Value.Value() { + defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +func stringifyStringSliceFlag(f *StringSliceFlag) string { + var defaultVals []string + if f.Value != nil && len(f.Value.Value()) > 0 { + for _, s := range f.Value.Value() { + if len(s) > 0 { + defaultVals = append(defaultVals, strconv.Quote(s)) + } + } + } + + return stringifySliceFlag(f.Usage, f.Names(), defaultVals) +} + +func stringifySliceFlag(usage string, names, defaultVals []string) string { + placeholder, usage := unquoteUsage(usage) + if placeholder == "" { + placeholder = defaultPlaceholder + } + + defaultVal := "" + if len(defaultVals) > 0 { + defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", ")) + } + + usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) + return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) +} + +func hasFlag(flags []Flag, fl Flag) bool { + for _, existing := range flags { + if fl == existing { + return true + } + } + + return false +} + +func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { + for _, envVar := range envVars { + envVar = strings.TrimSpace(envVar) + if val, ok := syscall.Getenv(envVar); ok { + return val, true + } + } + for _, fileVar := range strings.Split(filePath, ",") { + if data, err := ioutil.ReadFile(fileVar); err == nil { + return string(data), true + } + } + return "", false +} diff --git a/vendor/github.com/urfave/cli/v2/flag_bool.go b/vendor/github.com/urfave/cli/v2/flag_bool.go new file mode 100644 index 00000000..bc9ea35d --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_bool.go @@ -0,0 +1,106 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// BoolFlag is a flag with type bool +type BoolFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value bool + DefaultText string + Destination *bool + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *BoolFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *BoolFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *BoolFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *BoolFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *BoolFlag) TakesValue() bool { + return false +} + +// GetUsage returns the usage string for the flag +func (f *BoolFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *BoolFlag) GetValue() string { + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *BoolFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valBool, err := strconv.ParseBool(val) + + if err != nil { + return fmt.Errorf("could not parse %q as bool value for flag %s: %s", val, f.Name, err) + } + + f.Value = valBool + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.BoolVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Bool(name, f.Value, f.Usage) + } + + return nil +} + +// Bool looks up the value of a local BoolFlag, returns +// false if not found +func (c *Context) Bool(name string) bool { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupBool(name, fs) + } + return false +} + +func lookupBool(name string, set *flag.FlagSet) bool { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseBool(f.Value.String()) + if err != nil { + return false + } + return parsed + } + return false +} diff --git a/vendor/github.com/urfave/cli/v2/flag_duration.go b/vendor/github.com/urfave/cli/v2/flag_duration.go new file mode 100644 index 00000000..22a2e672 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_duration.go @@ -0,0 +1,105 @@ +package cli + +import ( + "flag" + "fmt" + "time" +) + +// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) +type DurationFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value time.Duration + DefaultText string + Destination *time.Duration + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *DurationFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *DurationFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *DurationFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *DurationFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *DurationFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *DurationFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *DurationFlag) GetValue() string { + return f.Value.String() +} + +// Apply populates the flag given the flag set and environment +func (f *DurationFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valDuration, err := time.ParseDuration(val) + + if err != nil { + return fmt.Errorf("could not parse %q as duration value for flag %s: %s", val, f.Name, err) + } + + f.Value = valDuration + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.DurationVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Duration(name, f.Value, f.Usage) + } + return nil +} + +// Duration looks up the value of a local DurationFlag, returns +// 0 if not found +func (c *Context) Duration(name string) time.Duration { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupDuration(name, fs) + } + return 0 +} + +func lookupDuration(name string, set *flag.FlagSet) time.Duration { + f := set.Lookup(name) + if f != nil { + parsed, err := time.ParseDuration(f.Value.String()) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/flag_float64.go b/vendor/github.com/urfave/cli/v2/flag_float64.go new file mode 100644 index 00000000..91c778c8 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_float64.go @@ -0,0 +1,106 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Float64Flag is a flag with type float64 +type Float64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value float64 + DefaultText string + Destination *float64 + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Float64Flag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Float64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Float64Flag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Float64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Float64Flag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Float64Flag) GetValue() string { + return fmt.Sprintf("%f", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *Float64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valFloat, err := strconv.ParseFloat(val, 10) + + if err != nil { + return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) + } + + f.Value = valFloat + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Float64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Float64(name, f.Value, f.Usage) + } + + return nil +} + +// Float64 looks up the value of a local Float64Flag, returns +// 0 if not found +func (c *Context) Float64(name string) float64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupFloat64(name, fs) + } + return 0 +} + +func lookupFloat64(name string, set *flag.FlagSet) float64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseFloat(f.Value.String(), 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/flag_float64_slice.go b/vendor/github.com/urfave/cli/v2/flag_float64_slice.go new file mode 100644 index 00000000..706ee6cd --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_float64_slice.go @@ -0,0 +1,163 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// Float64Slice wraps []float64 to satisfy flag.Value +type Float64Slice struct { + slice []float64 + hasBeenSet bool +} + +// NewFloat64Slice makes a *Float64Slice with default values +func NewFloat64Slice(defaults ...float64) *Float64Slice { + return &Float64Slice{slice: append([]float64{}, defaults...)} +} + +// Set parses the value into a float64 and appends it to the list of values +func (f *Float64Slice) Set(value string) error { + if !f.hasBeenSet { + f.slice = []float64{} + f.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) + f.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseFloat(value, 64) + if err != nil { + return err + } + + f.slice = append(f.slice, tmp) + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (f *Float64Slice) String() string { + return fmt.Sprintf("%#v", f.slice) +} + +// Serialize allows Float64Slice to fulfill Serializer +func (f *Float64Slice) Serialize() string { + jsonBytes, _ := json.Marshal(f.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of float64s set by this flag +func (f *Float64Slice) Value() []float64 { + return f.slice +} + +// Get returns the slice of float64s set by this flag +func (f *Float64Slice) Get() interface{} { + return *f +} + +// Float64SliceFlag is a flag with type *Float64Slice +type Float64SliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *Float64Slice + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Float64SliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Float64SliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Float64SliceFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Float64SliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true if the flag takes a value, otherwise false +func (f *Float64SliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Float64SliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Float64SliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + f.Value = &Float64Slice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err) + } + } + + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &Float64Slice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Float64Slice looks up the value of a local Float64SliceFlag, returns +// nil if not found +func (c *Context) Float64Slice(name string) []float64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupFloat64Slice(name, fs) + } + return nil +} + +func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { + f := set.Lookup(name) + if f != nil { + if slice, ok := f.Value.(*Float64Slice); ok { + return slice.Value() + } + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_generic.go b/vendor/github.com/urfave/cli/v2/flag_generic.go new file mode 100644 index 00000000..b0c8ff44 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_generic.go @@ -0,0 +1,108 @@ +package cli + +import ( + "flag" + "fmt" +) + +// Generic is a generic parseable type identified by a specific flag +type Generic interface { + Set(value string) error + String() string +} + +// GenericFlag is a flag with type Generic +type GenericFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value Generic + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *GenericFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *GenericFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *GenericFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *GenericFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *GenericFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *GenericFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *GenericFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply takes the flagset and calls Set on the generic flag with the value +// provided by the user for parsing by the flag +func (f GenericFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + if err := f.Value.Set(val); err != nil { + return fmt.Errorf("could not parse %q as value for flag %s: %s", val, f.Name, err) + } + + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Generic looks up the value of a local GenericFlag, returns +// nil if not found +func (c *Context) Generic(name string) interface{} { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupGeneric(name, fs) + } + return nil +} + +func lookupGeneric(name string, set *flag.FlagSet) interface{} { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value, error(nil) + if err != nil { + return nil + } + return parsed + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_int.go b/vendor/github.com/urfave/cli/v2/flag_int.go new file mode 100644 index 00000000..ac39d4a9 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_int.go @@ -0,0 +1,106 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// IntFlag is a flag with type int +type IntFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value int + DefaultText string + Destination *int + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *IntFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *IntFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *IntFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *IntFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *IntFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *IntFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *IntFlag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *IntFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseInt(val, 0, 64) + + if err != nil { + return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) + } + + f.Value = int(valInt) + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.IntVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Int(name, f.Value, f.Usage) + } + + return nil +} + +// Int looks up the value of a local IntFlag, returns +// 0 if not found +func (c *Context) Int(name string) int { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupInt(name, fs) + } + return 0 +} + +func lookupInt(name string, set *flag.FlagSet) int { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return int(parsed) + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/flag_int64.go b/vendor/github.com/urfave/cli/v2/flag_int64.go new file mode 100644 index 00000000..e0999126 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_int64.go @@ -0,0 +1,105 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Int64Flag is a flag with type int64 +type Int64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value int64 + DefaultText string + Destination *int64 + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Int64Flag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Int64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Int64Flag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Int64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Int64Flag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Int64Flag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Apply populates the flag given the flag set and environment +func (f *Int64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseInt(val, 0, 64) + + if err != nil { + return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) + } + + f.Value = valInt + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Int64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Int64(name, f.Value, f.Usage) + } + return nil +} + +// Int64 looks up the value of a local Int64Flag, returns +// 0 if not found +func (c *Context) Int64(name string) int64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupInt64(name, fs) + } + return 0 +} + +func lookupInt64(name string, set *flag.FlagSet) int64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/flag_int64_slice.go b/vendor/github.com/urfave/cli/v2/flag_int64_slice.go new file mode 100644 index 00000000..6c7fd937 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_int64_slice.go @@ -0,0 +1,159 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// Int64Slice wraps []int64 to satisfy flag.Value +type Int64Slice struct { + slice []int64 + hasBeenSet bool +} + +// NewInt64Slice makes an *Int64Slice with default values +func NewInt64Slice(defaults ...int64) *Int64Slice { + return &Int64Slice{slice: append([]int64{}, defaults...)} +} + +// Set parses the value into an integer and appends it to the list of values +func (i *Int64Slice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []int64{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + + i.slice = append(i.slice, tmp) + + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *Int64Slice) String() string { + return fmt.Sprintf("%#v", i.slice) +} + +// Serialize allows Int64Slice to fulfill Serializer +func (i *Int64Slice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *Int64Slice) Value() []int64 { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *Int64Slice) Get() interface{} { + return *i +} + +// Int64SliceFlag is a flag with type *Int64Slice +type Int64SliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *Int64Slice + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Int64SliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Int64SliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Int64SliceFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Int64SliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Int64SliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f Int64SliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Int64SliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &Int64Slice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err) + } + } + + f.HasBeenSet = true + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &Int64Slice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// Int64Slice looks up the value of a local Int64SliceFlag, returns +// nil if not found +func (c *Context) Int64Slice(name string) []int64 { + return lookupInt64Slice(name, c.flagSet) +} + +func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { + f := set.Lookup(name) + if f != nil { + if slice, ok := f.Value.(*Int64Slice); ok { + return slice.Value() + } + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_int_slice.go b/vendor/github.com/urfave/cli/v2/flag_int_slice.go new file mode 100644 index 00000000..4e0afc02 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_int_slice.go @@ -0,0 +1,173 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strconv" + "strings" +) + +// IntSlice wraps []int to satisfy flag.Value +type IntSlice struct { + slice []int + hasBeenSet bool +} + +// NewIntSlice makes an *IntSlice with default values +func NewIntSlice(defaults ...int) *IntSlice { + return &IntSlice{slice: append([]int{}, defaults...)} +} + +// TODO: Consistently have specific Set function for Int64 and Float64 ? +// SetInt directly adds an integer to the list of values +func (i *IntSlice) SetInt(value int) { + if !i.hasBeenSet { + i.slice = []int{} + i.hasBeenSet = true + } + + i.slice = append(i.slice, value) +} + +// Set parses the value into an integer and appends it to the list of values +func (i *IntSlice) Set(value string) error { + if !i.hasBeenSet { + i.slice = []int{} + i.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) + i.hasBeenSet = true + return nil + } + + tmp, err := strconv.ParseInt(value, 0, 64) + if err != nil { + return err + } + + i.slice = append(i.slice, int(tmp)) + + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (i *IntSlice) String() string { + return fmt.Sprintf("%#v", i.slice) +} + +// Serialize allows IntSlice to fulfill Serializer +func (i *IntSlice) Serialize() string { + jsonBytes, _ := json.Marshal(i.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of ints set by this flag +func (i *IntSlice) Value() []int { + return i.slice +} + +// Get returns the slice of ints set by this flag +func (i *IntSlice) Get() interface{} { + return *i +} + +// IntSliceFlag is a flag with type *IntSlice +type IntSliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value *IntSlice + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *IntSliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *IntSliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *IntSliceFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *IntSliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *IntSliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f IntSliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *IntSliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &IntSlice{} + + for _, s := range strings.Split(val, ",") { + if err := f.Value.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err) + } + } + + f.HasBeenSet = true + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &IntSlice{} + } + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// IntSlice looks up the value of a local IntSliceFlag, returns +// nil if not found +func (c *Context) IntSlice(name string) []int { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupIntSlice(name, c.flagSet) + } + return nil +} + +func lookupIntSlice(name string, set *flag.FlagSet) []int { + f := set.Lookup(name) + if f != nil { + if slice, ok := f.Value.(*IntSlice); ok { + return slice.Value() + } + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_path.go b/vendor/github.com/urfave/cli/v2/flag_path.go new file mode 100644 index 00000000..8070dc4b --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_path.go @@ -0,0 +1,95 @@ +package cli + +import "flag" + +type PathFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value string + DefaultText string + Destination *string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *PathFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *PathFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *PathFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *PathFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *PathFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *PathFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *PathFlag) GetValue() string { + return f.Value +} + +// Apply populates the flag given the flag set and environment +func (f *PathFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = val + f.HasBeenSet = true + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.StringVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.String(name, f.Value, f.Usage) + } + + return nil +} + +// Path looks up the value of a local PathFlag, returns +// "" if not found +func (c *Context) Path(name string) string { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupPath(name, fs) + } + + return "" +} + +func lookupPath(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} diff --git a/vendor/github.com/urfave/cli/v2/flag_string.go b/vendor/github.com/urfave/cli/v2/flag_string.go new file mode 100644 index 00000000..400bb532 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_string.go @@ -0,0 +1,95 @@ +package cli + +import "flag" + +// StringFlag is a flag with type string +type StringFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value string + DefaultText string + Destination *string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *StringFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *StringFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *StringFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *StringFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *StringFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *StringFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *StringFlag) GetValue() string { + return f.Value +} + +// Apply populates the flag given the flag set and environment +func (f *StringFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = val + f.HasBeenSet = true + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.StringVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.String(name, f.Value, f.Usage) + } + + return nil +} + +// String looks up the value of a local StringFlag, returns +// "" if not found +func (c *Context) String(name string) string { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupString(name, fs) + } + return "" +} + +func lookupString(name string, set *flag.FlagSet) string { + f := set.Lookup(name) + if f != nil { + parsed, err := f.Value.String(), error(nil) + if err != nil { + return "" + } + return parsed + } + return "" +} diff --git a/vendor/github.com/urfave/cli/v2/flag_string_slice.go b/vendor/github.com/urfave/cli/v2/flag_string_slice.go new file mode 100644 index 00000000..ac363bf6 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_string_slice.go @@ -0,0 +1,171 @@ +package cli + +import ( + "encoding/json" + "flag" + "fmt" + "strings" +) + +// StringSlice wraps a []string to satisfy flag.Value +type StringSlice struct { + slice []string + hasBeenSet bool +} + +// NewStringSlice creates a *StringSlice with default values +func NewStringSlice(defaults ...string) *StringSlice { + return &StringSlice{slice: append([]string{}, defaults...)} +} + +// Set appends the string value to the list of values +func (s *StringSlice) Set(value string) error { + if !s.hasBeenSet { + s.slice = []string{} + s.hasBeenSet = true + } + + if strings.HasPrefix(value, slPfx) { + // Deserializing assumes overwrite + _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &s.slice) + s.hasBeenSet = true + return nil + } + + s.slice = append(s.slice, value) + + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (s *StringSlice) String() string { + return fmt.Sprintf("%s", s.slice) +} + +// Serialize allows StringSlice to fulfill Serializer +func (s *StringSlice) Serialize() string { + jsonBytes, _ := json.Marshal(s.slice) + return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) +} + +// Value returns the slice of strings set by this flag +func (s *StringSlice) Value() []string { + return s.slice +} + +// Get returns the slice of strings set by this flag +func (s *StringSlice) Get() interface{} { + return *s +} + +// StringSliceFlag is a flag with type *StringSlice +type StringSliceFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + TakesFile bool + Value *StringSlice + DefaultText string + HasBeenSet bool + Destination *StringSlice +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *StringSliceFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *StringSliceFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *StringSliceFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *StringSliceFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *StringSliceFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *StringSliceFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *StringSliceFlag) GetValue() string { + if f.Value != nil { + return f.Value.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + f.Value = &StringSlice{} + destination := f.Value + if f.Destination != nil { + destination = f.Destination + } + + for _, s := range strings.Split(val, ",") { + if err := destination.Set(strings.TrimSpace(s)); err != nil { + return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err) + } + } + + // Set this to false so that we reset the slice if we then set values from + // flags that have already been set by the environment. + destination.hasBeenSet = false + f.HasBeenSet = true + } + + for _, name := range f.Names() { + if f.Value == nil { + f.Value = &StringSlice{} + } + + if f.Destination != nil { + set.Var(f.Destination, name, f.Usage) + continue + } + + set.Var(f.Value, name, f.Usage) + } + + return nil +} + +// StringSlice looks up the value of a local StringSliceFlag, returns +// nil if not found +func (c *Context) StringSlice(name string) []string { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupStringSlice(name, fs) + } + return nil +} + +func lookupStringSlice(name string, set *flag.FlagSet) []string { + f := set.Lookup(name) + if f != nil { + if slice, ok := f.Value.(*StringSlice); ok { + return slice.Value() + } + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_timestamp.go b/vendor/github.com/urfave/cli/v2/flag_timestamp.go new file mode 100644 index 00000000..9fac1d1e --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_timestamp.go @@ -0,0 +1,152 @@ +package cli + +import ( + "flag" + "fmt" + "time" +) + +// Timestamp wrap to satisfy golang's flag interface. +type Timestamp struct { + timestamp *time.Time + hasBeenSet bool + layout string +} + +// Timestamp constructor +func NewTimestamp(timestamp time.Time) *Timestamp { + return &Timestamp{timestamp: ×tamp} +} + +// Set the timestamp value directly +func (t *Timestamp) SetTimestamp(value time.Time) { + if !t.hasBeenSet { + t.timestamp = &value + t.hasBeenSet = true + } +} + +// Set the timestamp string layout for future parsing +func (t *Timestamp) SetLayout(layout string) { + t.layout = layout +} + +// Parses the string value to timestamp +func (t *Timestamp) Set(value string) error { + timestamp, err := time.Parse(t.layout, value) + if err != nil { + return err + } + + t.timestamp = ×tamp + t.hasBeenSet = true + return nil +} + +// String returns a readable representation of this value (for usage defaults) +func (t *Timestamp) String() string { + return fmt.Sprintf("%#v", t.timestamp) +} + +// Value returns the timestamp value stored in the flag +func (t *Timestamp) Value() *time.Time { + return t.timestamp +} + +// Get returns the flag structure +func (t *Timestamp) Get() interface{} { + return *t +} + +// TimestampFlag is a flag with type time +type TimestampFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Layout string + Value *Timestamp + DefaultText string + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *TimestampFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *TimestampFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *TimestampFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *TimestampFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *TimestampFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *TimestampFlag) GetUsage() string { + return f.Usage +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *TimestampFlag) GetValue() string { + if f.Value != nil { + return f.Value.timestamp.String() + } + return "" +} + +// Apply populates the flag given the flag set and environment +func (f *TimestampFlag) Apply(set *flag.FlagSet) error { + if f.Layout == "" { + return fmt.Errorf("timestamp Layout is required") + } + f.Value = &Timestamp{} + f.Value.SetLayout(f.Layout) + + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if err := f.Value.Set(val); err != nil { + return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) + } + f.HasBeenSet = true + } + + for _, name := range f.Names() { + set.Var(f.Value, name, f.Usage) + } + return nil +} + +// Timestamp gets the timestamp from a flag name +func (c *Context) Timestamp(name string) *time.Time { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupTimestamp(name, fs) + } + return nil +} + +// Fetches the timestamp value from the local timestampWrap +func lookupTimestamp(name string, set *flag.FlagSet) *time.Time { + f := set.Lookup(name) + if f != nil { + return (f.Value.(*Timestamp)).Value() + } + return nil +} diff --git a/vendor/github.com/urfave/cli/v2/flag_uint.go b/vendor/github.com/urfave/cli/v2/flag_uint.go new file mode 100644 index 00000000..2e5e76b0 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_uint.go @@ -0,0 +1,105 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// UintFlag is a flag with type uint +type UintFlag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value uint + DefaultText string + Destination *uint + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *UintFlag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *UintFlag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *UintFlag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *UintFlag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *UintFlag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *UintFlag) GetUsage() string { + return f.Usage +} + +// Apply populates the flag given the flag set and environment +func (f *UintFlag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseUint(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %q as uint value for flag %s: %s", val, f.Name, err) + } + + f.Value = uint(valInt) + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.UintVar(f.Destination, name, f.Value, f.Usage) + continue + } + set.Uint(name, f.Value, f.Usage) + } + + return nil +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *UintFlag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Uint looks up the value of a local UintFlag, returns +// 0 if not found +func (c *Context) Uint(name string) uint { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupUint(name, fs) + } + return 0 +} + +func lookupUint(name string, set *flag.FlagSet) uint { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return uint(parsed) + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/flag_uint64.go b/vendor/github.com/urfave/cli/v2/flag_uint64.go new file mode 100644 index 00000000..8fc3289d --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/flag_uint64.go @@ -0,0 +1,105 @@ +package cli + +import ( + "flag" + "fmt" + "strconv" +) + +// Uint64Flag is a flag with type uint64 +type Uint64Flag struct { + Name string + Aliases []string + Usage string + EnvVars []string + FilePath string + Required bool + Hidden bool + Value uint64 + DefaultText string + Destination *uint64 + HasBeenSet bool +} + +// IsSet returns whether or not the flag has been set through env or file +func (f *Uint64Flag) IsSet() bool { + return f.HasBeenSet +} + +// String returns a readable representation of this value +// (for usage defaults) +func (f *Uint64Flag) String() string { + return FlagStringer(f) +} + +// Names returns the names of the flag +func (f *Uint64Flag) Names() []string { + return flagNames(f.Name, f.Aliases) +} + +// IsRequired returns whether or not the flag is required +func (f *Uint64Flag) IsRequired() bool { + return f.Required +} + +// TakesValue returns true of the flag takes a value, otherwise false +func (f *Uint64Flag) TakesValue() bool { + return true +} + +// GetUsage returns the usage string for the flag +func (f *Uint64Flag) GetUsage() string { + return f.Usage +} + +// Apply populates the flag given the flag set and environment +func (f *Uint64Flag) Apply(set *flag.FlagSet) error { + if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { + if val != "" { + valInt, err := strconv.ParseUint(val, 0, 64) + if err != nil { + return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", val, f.Name, err) + } + + f.Value = valInt + f.HasBeenSet = true + } + } + + for _, name := range f.Names() { + if f.Destination != nil { + set.Uint64Var(f.Destination, name, f.Value, f.Usage) + continue + } + set.Uint64(name, f.Value, f.Usage) + } + + return nil +} + +// GetValue returns the flags value as string representation and an empty +// string if the flag takes no value at all. +func (f *Uint64Flag) GetValue() string { + return fmt.Sprintf("%d", f.Value) +} + +// Uint64 looks up the value of a local Uint64Flag, returns +// 0 if not found +func (c *Context) Uint64(name string) uint64 { + if fs := lookupFlagSet(name, c); fs != nil { + return lookupUint64(name, fs) + } + return 0 +} + +func lookupUint64(name string, set *flag.FlagSet) uint64 { + f := set.Lookup(name) + if f != nil { + parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) + if err != nil { + return 0 + } + return parsed + } + return 0 +} diff --git a/vendor/github.com/urfave/cli/v2/funcs.go b/vendor/github.com/urfave/cli/v2/funcs.go new file mode 100644 index 00000000..474c48fa --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/funcs.go @@ -0,0 +1,44 @@ +package cli + +// BashCompleteFunc is an action to execute when the shell completion flag is set +type BashCompleteFunc func(*Context) + +// BeforeFunc is an action to execute before any subcommands are run, but after +// the context is ready if a non-nil error is returned, no subcommands are run +type BeforeFunc func(*Context) error + +// AfterFunc is an action to execute after any subcommands are run, but after the +// subcommand has finished it is run even if Action() panics +type AfterFunc func(*Context) error + +// ActionFunc is the action to execute when no subcommands are specified +type ActionFunc func(*Context) error + +// CommandNotFoundFunc is executed if the proper command cannot be found +type CommandNotFoundFunc func(*Context, string) + +// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying +// customized usage error messages. This function is able to replace the +// original error messages. If this function is not set, the "Incorrect usage" +// is displayed and the execution is interrupted. +type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error + +// ExitErrHandlerFunc is executed if provided in order to handle exitError values +// returned by Actions and Before/After functions. +type ExitErrHandlerFunc func(context *Context, err error) + +// FlagStringFunc is used by the help generation to display a flag, which is +// expected to be a single line. +type FlagStringFunc func(Flag) string + +// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix +// text for a flag's full name. +type FlagNamePrefixFunc func(fullName []string, placeholder string) string + +// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help +// with the environment variable details. +type FlagEnvHintFunc func(envVars []string, str string) string + +// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help +// with the file path details. +type FlagFileHintFunc func(filePath, str string) string diff --git a/vendor/github.com/urfave/cli/v2/go.mod b/vendor/github.com/urfave/cli/v2/go.mod new file mode 100644 index 00000000..c38d41c1 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/go.mod @@ -0,0 +1,9 @@ +module github.com/urfave/cli/v2 + +go 1.11 + +require ( + github.com/BurntSushi/toml v0.3.1 + github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/vendor/github.com/urfave/cli/v2/go.sum b/vendor/github.com/urfave/cli/v2/go.sum new file mode 100644 index 00000000..ef121ff5 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/go.sum @@ -0,0 +1,14 @@ +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/urfave/cli/v2/help.go b/vendor/github.com/urfave/cli/v2/help.go new file mode 100644 index 00000000..c1e974a4 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/help.go @@ -0,0 +1,368 @@ +package cli + +import ( + "fmt" + "io" + "os" + "strings" + "text/tabwriter" + "text/template" + "unicode/utf8" +) + +var helpCommand = &Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) error { + args := c.Args() + if args.Present() { + return ShowCommandHelp(c, args.First()) + } + + _ = ShowAppHelp(c) + return nil + }, +} + +var helpSubcommand = &Command{ + Name: "help", + Aliases: []string{"h"}, + Usage: "Shows a list of commands or help for one command", + ArgsUsage: "[command]", + Action: func(c *Context) error { + args := c.Args() + if args.Present() { + return ShowCommandHelp(c, args.First()) + } + + return ShowSubcommandHelp(c) + }, +} + +// Prints help for the App or Command +type helpPrinter func(w io.Writer, templ string, data interface{}) + +// Prints help for the App or Command with custom template function. +type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) + +// HelpPrinter is a function that writes the help output. If not set explicitly, +// this calls HelpPrinterCustom using only the default template functions. +// +// If custom logic for printing help is required, this function can be +// overridden. If the ExtraInfo field is defined on an App, this function +// should not be modified, as HelpPrinterCustom will be used directly in order +// to capture the extra information. +var HelpPrinter helpPrinter = printHelp + +// HelpPrinterCustom is a function that writes the help output. It is used as +// the default implementation of HelpPrinter, and may be called directly if +// the ExtraInfo field is set on an App. +var HelpPrinterCustom helpPrinterCustom = printHelpCustom + +// VersionPrinter prints the version for the App +var VersionPrinter = printVersion + +// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. +func ShowAppHelpAndExit(c *Context, exitCode int) { + _ = ShowAppHelp(c) + os.Exit(exitCode) +} + +// ShowAppHelp is an action that displays the help. +func ShowAppHelp(c *Context) error { + template := c.App.CustomAppHelpTemplate + if template == "" { + template = AppHelpTemplate + } + + if c.App.ExtraInfo == nil { + HelpPrinter(c.App.Writer, template, c.App) + return nil + } + + customAppData := func() map[string]interface{} { + return map[string]interface{}{ + "ExtraInfo": c.App.ExtraInfo, + } + } + HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) + + return nil +} + +// DefaultAppComplete prints the list of subcommands as the default app completion method +func DefaultAppComplete(c *Context) { + DefaultCompleteWithFlags(nil)(c) +} + +func printCommandSuggestions(commands []*Command, writer io.Writer) { + for _, command := range commands { + if command.Hidden { + continue + } + if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { + for _, name := range command.Names() { + _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) + } + } else { + for _, name := range command.Names() { + _, _ = fmt.Fprintf(writer, "%s\n", name) + } + } + } +} + +func cliArgContains(flagName string) bool { + for _, name := range strings.Split(flagName, ",") { + name = strings.TrimSpace(name) + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 + } + flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + for _, a := range os.Args { + if a == flag { + return true + } + } + } + return false +} + +func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { + cur := strings.TrimPrefix(lastArg, "-") + cur = strings.TrimPrefix(cur, "-") + for _, flag := range flags { + if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { + continue + } + for _, name := range flag.Names() { + name = strings.TrimSpace(name) + // this will get total count utf8 letters in flag name + count := utf8.RuneCountInString(name) + if count > 2 { + count = 2 // resuse this count to generate single - or -- in flag completion + } + // if flag name has more than one utf8 letter and last argument in cli has -- prefix then + // skip flag completion for short flags example -v or -x + if strings.HasPrefix(lastArg, "--") && count == 1 { + continue + } + // match if last argument matches this flag and it is not repeated + if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { + flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) + _, _ = fmt.Fprintln(writer, flagCompletion) + } + } + } +} + +func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { + return func(c *Context) { + if len(os.Args) > 2 { + lastArg := os.Args[len(os.Args)-2] + if strings.HasPrefix(lastArg, "-") { + printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer) + if cmd != nil { + printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) + } + return + } + } + if cmd != nil { + printCommandSuggestions(cmd.Subcommands, c.App.Writer) + } else { + printCommandSuggestions(c.App.Commands, c.App.Writer) + } + } +} + +// ShowCommandHelpAndExit - exits with code after showing help +func ShowCommandHelpAndExit(c *Context, command string, code int) { + _ = ShowCommandHelp(c, command) + os.Exit(code) +} + +// ShowCommandHelp prints help for the given command +func ShowCommandHelp(ctx *Context, command string) error { + // show the subcommand help for a command with subcommands + if command == "" { + HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) + return nil + } + + for _, c := range ctx.App.Commands { + if c.HasName(command) { + templ := c.CustomHelpTemplate + if templ == "" { + templ = CommandHelpTemplate + } + + HelpPrinter(ctx.App.Writer, templ, c) + + return nil + } + } + + if ctx.App.CommandNotFound == nil { + return Exit(fmt.Sprintf("No help topic for '%v'", command), 3) + } + + ctx.App.CommandNotFound(ctx, command) + return nil +} + +// ShowSubcommandHelp prints help for the given subcommand +func ShowSubcommandHelp(c *Context) error { + if c == nil { + return nil + } + + if c.Command != nil { + return ShowCommandHelp(c, c.Command.Name) + } + + return ShowCommandHelp(c, "") +} + +// ShowVersion prints the version number of the App +func ShowVersion(c *Context) { + VersionPrinter(c) +} + +func printVersion(c *Context) { + _, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) +} + +// ShowCompletions prints the lists of commands within a given context +func ShowCompletions(c *Context) { + a := c.App + if a != nil && a.BashComplete != nil { + a.BashComplete(c) + } +} + +// ShowCommandCompletions prints the custom completions for a given command +func ShowCommandCompletions(ctx *Context, command string) { + c := ctx.App.Command(command) + if c != nil { + if c.BashComplete != nil { + c.BashComplete(ctx) + } else { + DefaultCompleteWithFlags(c)(ctx) + } + } + +} + +// printHelpCustom is the default implementation of HelpPrinterCustom. +// +// The customFuncs map will be combined with a default template.FuncMap to +// allow using arbitrary functions in template rendering. +func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { + funcMap := template.FuncMap{ + "join": strings.Join, + } + for key, value := range customFuncs { + funcMap[key] = value + } + + w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) + t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) + + err := t.Execute(w, data) + if err != nil { + // If the writer is closed, t.Execute will fail, and there's nothing + // we can do to recover. + if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { + _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) + } + return + } + _ = w.Flush() +} + +func printHelp(out io.Writer, templ string, data interface{}) { + HelpPrinterCustom(out, templ, data, nil) +} + +func checkVersion(c *Context) bool { + found := false + for _, name := range VersionFlag.Names() { + if c.Bool(name) { + found = true + } + } + return found +} + +func checkHelp(c *Context) bool { + found := false + for _, name := range HelpFlag.Names() { + if c.Bool(name) { + found = true + } + } + return found +} + +func checkCommandHelp(c *Context, name string) bool { + if c.Bool("h") || c.Bool("help") { + _ = ShowCommandHelp(c, name) + return true + } + + return false +} + +func checkSubcommandHelp(c *Context) bool { + if c.Bool("h") || c.Bool("help") { + _ = ShowSubcommandHelp(c) + return true + } + + return false +} + +func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { + if !a.EnableBashCompletion { + return false, arguments + } + + pos := len(arguments) - 1 + lastArg := arguments[pos] + + if lastArg != "--generate-bash-completion" { + return false, arguments + } + + return true, arguments[:pos] +} + +func checkCompletions(c *Context) bool { + if !c.shellComplete { + return false + } + + if args := c.Args(); args.Present() { + name := args.First() + if cmd := c.App.Command(name); cmd != nil { + // let the command handle the completion + return false + } + } + + ShowCompletions(c) + return true +} + +func checkCommandCompletions(c *Context, name string) bool { + if !c.shellComplete { + return false + } + + ShowCommandCompletions(c, name) + return true +} diff --git a/vendor/github.com/urfave/cli/v2/parse.go b/vendor/github.com/urfave/cli/v2/parse.go new file mode 100644 index 00000000..7df17296 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/parse.go @@ -0,0 +1,94 @@ +package cli + +import ( + "flag" + "strings" +) + +type iterativeParser interface { + newFlagSet() (*flag.FlagSet, error) + useShortOptionHandling() bool +} + +// To enable short-option handling (e.g., "-it" vs "-i -t") we have to +// iteratively catch parsing errors. This way we achieve LR parsing without +// transforming any arguments. Otherwise, there is no way we can discriminate +// combined short options from common arguments that should be left untouched. +// Pass `shellComplete` to continue parsing options on failure during shell +// completion when, the user-supplied options may be incomplete. +func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error { + for { + err := set.Parse(args) + if !ip.useShortOptionHandling() || err == nil { + if shellComplete { + return nil + } + return err + } + + errStr := err.Error() + trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -") + if errStr == trimmed { + return err + } + + // regenerate the initial args with the split short opts + argsWereSplit := false + for i, arg := range args { + // skip args that are not part of the error message + if name := strings.TrimLeft(arg, "-"); name != trimmed { + continue + } + + // if we can't split, the error was accurate + shortOpts := splitShortOptions(set, arg) + if len(shortOpts) == 1 { + return err + } + + // swap current argument with the split version + args = append(args[:i], append(shortOpts, args[i+1:]...)...) + argsWereSplit = true + break + } + + // This should be an impossible to reach code path, but in case the arg + // splitting failed to happen, this will prevent infinite loops + if !argsWereSplit { + return err + } + + // Since custom parsing failed, replace the flag set before retrying + newSet, err := ip.newFlagSet() + if err != nil { + return err + } + *set = *newSet + } +} + +func splitShortOptions(set *flag.FlagSet, arg string) []string { + shortFlagsExist := func(s string) bool { + for _, c := range s[1:] { + if f := set.Lookup(string(c)); f == nil { + return false + } + } + return true + } + + if !isSplittable(arg) || !shortFlagsExist(arg) { + return []string{arg} + } + + separated := make([]string, 0, len(arg)-1) + for _, flagChar := range arg[1:] { + separated = append(separated, "-"+string(flagChar)) + } + + return separated +} + +func isSplittable(flagArg string) bool { + return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2 +} diff --git a/vendor/github.com/urfave/cli/v2/sort.go b/vendor/github.com/urfave/cli/v2/sort.go new file mode 100644 index 00000000..23d1c2f7 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/sort.go @@ -0,0 +1,29 @@ +package cli + +import "unicode" + +// lexicographicLess compares strings alphabetically considering case. +func lexicographicLess(i, j string) bool { + iRunes := []rune(i) + jRunes := []rune(j) + + lenShared := len(iRunes) + if lenShared > len(jRunes) { + lenShared = len(jRunes) + } + + for index := 0; index < lenShared; index++ { + ir := iRunes[index] + jr := jRunes[index] + + if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { + return lir < ljr + } + + if ir != jr { + return ir < jr + } + } + + return i < j +} diff --git a/vendor/github.com/urfave/cli/v2/template.go b/vendor/github.com/urfave/cli/v2/template.go new file mode 100644 index 00000000..aee3e049 --- /dev/null +++ b/vendor/github.com/urfave/cli/v2/template.go @@ -0,0 +1,120 @@ +package cli + +// AppHelpTemplate is the text template for the Default help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var AppHelpTemplate = `NAME: + {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} + +VERSION: + {{.Version}}{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if len .Authors}} + +AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: + {{range $index, $author := .Authors}}{{if $index}} + {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +GLOBAL OPTIONS: + {{range $index, $option := .VisibleFlags}}{{if $index}} + {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} + +COPYRIGHT: + {{.Copyright}}{{end}} +` + +// CommandHelpTemplate is the text template for the command help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var CommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} + +CATEGORY: + {{.Category}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +// SubcommandHelpTemplate is the text template for the subcommand help topic. +// cli.go uses text/template to render templates. You can +// render custom help text by setting this variable. +var SubcommandHelpTemplate = `NAME: + {{.HelpName}} - {{.Usage}} + +USAGE: + {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} + +DESCRIPTION: + {{.Description}}{{end}} + +COMMANDS:{{range .VisibleCategories}}{{if .Name}} + {{.Name}}:{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} + {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} + +OPTIONS: + {{range .VisibleFlags}}{{.}} + {{end}}{{end}} +` + +var MarkdownDocTemplate = `% {{ .App.Name }} 8 + +# NAME + +{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} + +# SYNOPSIS + +{{ .App.Name }} +{{ if .SynopsisArgs }} +` + "```" + ` +{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` +{{ end }}{{ if .App.UsageText }} +# DESCRIPTION + +{{ .App.UsageText }} +{{ end }} +**Usage**: + +` + "```" + ` +{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] +` + "```" + ` +{{ if .GlobalArgs }} +# GLOBAL OPTIONS +{{ range $v := .GlobalArgs }} +{{ $v }}{{ end }} +{{ end }}{{ if .Commands }} +# COMMANDS +{{ range $v := .Commands }} +{{ $v }}{{ end }}{{ end }}` + +var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion + +function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' + for i in (commandline -opc) + if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} + return 1 + end + end + return 0 +end + +{{ range $v := .Completions }}{{ $v }} +{{ end }}` diff --git a/vendor/modules.txt b/vendor/modules.txt new file mode 100644 index 00000000..6651fd33 --- /dev/null +++ b/vendor/modules.txt @@ -0,0 +1,15 @@ +# github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d +github.com/cpuguy83/go-md2man/v2/md2man +# github.com/mattn/go-runewidth v0.0.9 +## explicit +github.com/mattn/go-runewidth +# github.com/olekukonko/tablewriter v0.0.4 +## explicit +github.com/olekukonko/tablewriter +# github.com/russross/blackfriday/v2 v2.0.1 +github.com/russross/blackfriday/v2 +# github.com/shurcooL/sanitized_anchor_name v1.0.0 +github.com/shurcooL/sanitized_anchor_name +# github.com/urfave/cli/v2 v2.2.0 +## explicit +github.com/urfave/cli/v2 From 148aa3d97af38232bc5b43f65bc9cfbe7ed5bd06 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 15:01:33 -0400 Subject: [PATCH 21/69] Unvendor --- .../cpuguy83/go-md2man/v2/LICENSE.md | 21 - .../cpuguy83/go-md2man/v2/md2man/md2man.go | 14 - .../cpuguy83/go-md2man/v2/md2man/roff.go | 345 ---- .../github.com/mattn/go-runewidth/.travis.yml | 16 - vendor/github.com/mattn/go-runewidth/LICENSE | 21 - .../github.com/mattn/go-runewidth/README.md | 27 - vendor/github.com/mattn/go-runewidth/go.mod | 3 - .../github.com/mattn/go-runewidth/go.test.sh | 12 - .../mattn/go-runewidth/runewidth.go | 257 --- .../mattn/go-runewidth/runewidth_appengine.go | 8 - .../mattn/go-runewidth/runewidth_js.go | 9 - .../mattn/go-runewidth/runewidth_posix.go | 82 - .../mattn/go-runewidth/runewidth_table.go | 437 ----- .../mattn/go-runewidth/runewidth_windows.go | 28 - .../olekukonko/tablewriter/.gitignore | 15 - .../olekukonko/tablewriter/.travis.yml | 14 - .../olekukonko/tablewriter/LICENSE.md | 19 - .../olekukonko/tablewriter/README.md | 396 ---- .../github.com/olekukonko/tablewriter/csv.go | 52 - .../github.com/olekukonko/tablewriter/go.mod | 5 - .../github.com/olekukonko/tablewriter/go.sum | 2 - .../olekukonko/tablewriter/table.go | 941 ---------- .../tablewriter/table_with_color.go | 136 -- .../github.com/olekukonko/tablewriter/util.go | 93 - .../github.com/olekukonko/tablewriter/wrap.go | 99 - .../russross/blackfriday/v2/.gitignore | 8 - .../russross/blackfriday/v2/.travis.yml | 17 - .../russross/blackfriday/v2/LICENSE.txt | 29 - .../russross/blackfriday/v2/README.md | 291 --- .../russross/blackfriday/v2/block.go | 1590 ----------------- .../github.com/russross/blackfriday/v2/doc.go | 18 - .../github.com/russross/blackfriday/v2/esc.go | 34 - .../github.com/russross/blackfriday/v2/go.mod | 1 - .../russross/blackfriday/v2/html.go | 949 ---------- .../russross/blackfriday/v2/inline.go | 1228 ------------- .../russross/blackfriday/v2/markdown.go | 950 ---------- .../russross/blackfriday/v2/node.go | 354 ---- .../russross/blackfriday/v2/smartypants.go | 457 ----- .../sanitized_anchor_name/.travis.yml | 16 - .../shurcooL/sanitized_anchor_name/LICENSE | 21 - .../shurcooL/sanitized_anchor_name/README.md | 36 - .../shurcooL/sanitized_anchor_name/go.mod | 1 - .../shurcooL/sanitized_anchor_name/main.go | 29 - vendor/github.com/urfave/cli/v2/.flake8 | 2 - vendor/github.com/urfave/cli/v2/.gitignore | 7 - .../urfave/cli/v2/CODE_OF_CONDUCT.md | 74 - vendor/github.com/urfave/cli/v2/LICENSE | 21 - vendor/github.com/urfave/cli/v2/README.md | 66 - vendor/github.com/urfave/cli/v2/app.go | 542 ------ vendor/github.com/urfave/cli/v2/args.go | 54 - vendor/github.com/urfave/cli/v2/category.go | 79 - vendor/github.com/urfave/cli/v2/cli.go | 23 - vendor/github.com/urfave/cli/v2/command.go | 301 ---- vendor/github.com/urfave/cli/v2/context.go | 273 --- vendor/github.com/urfave/cli/v2/docs.go | 148 -- vendor/github.com/urfave/cli/v2/errors.go | 131 -- vendor/github.com/urfave/cli/v2/fish.go | 192 -- vendor/github.com/urfave/cli/v2/flag.go | 388 ---- vendor/github.com/urfave/cli/v2/flag_bool.go | 106 -- .../github.com/urfave/cli/v2/flag_duration.go | 105 -- .../github.com/urfave/cli/v2/flag_float64.go | 106 -- .../urfave/cli/v2/flag_float64_slice.go | 163 -- .../github.com/urfave/cli/v2/flag_generic.go | 108 -- vendor/github.com/urfave/cli/v2/flag_int.go | 106 -- vendor/github.com/urfave/cli/v2/flag_int64.go | 105 -- .../urfave/cli/v2/flag_int64_slice.go | 159 -- .../urfave/cli/v2/flag_int_slice.go | 173 -- vendor/github.com/urfave/cli/v2/flag_path.go | 95 - .../github.com/urfave/cli/v2/flag_string.go | 95 - .../urfave/cli/v2/flag_string_slice.go | 171 -- .../urfave/cli/v2/flag_timestamp.go | 152 -- vendor/github.com/urfave/cli/v2/flag_uint.go | 105 -- .../github.com/urfave/cli/v2/flag_uint64.go | 105 -- vendor/github.com/urfave/cli/v2/funcs.go | 44 - vendor/github.com/urfave/cli/v2/go.mod | 9 - vendor/github.com/urfave/cli/v2/go.sum | 14 - vendor/github.com/urfave/cli/v2/help.go | 368 ---- vendor/github.com/urfave/cli/v2/parse.go | 94 - vendor/github.com/urfave/cli/v2/sort.go | 29 - vendor/github.com/urfave/cli/v2/template.go | 120 -- vendor/modules.txt | 15 - 81 files changed, 13929 deletions(-) delete mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md delete mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go delete mode 100644 vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go delete mode 100644 vendor/github.com/mattn/go-runewidth/.travis.yml delete mode 100644 vendor/github.com/mattn/go-runewidth/LICENSE delete mode 100644 vendor/github.com/mattn/go-runewidth/README.md delete mode 100644 vendor/github.com/mattn/go-runewidth/go.mod delete mode 100644 vendor/github.com/mattn/go-runewidth/go.test.sh delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth.go delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_appengine.go delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_js.go delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_posix.go delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_table.go delete mode 100644 vendor/github.com/mattn/go-runewidth/runewidth_windows.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/.gitignore delete mode 100644 vendor/github.com/olekukonko/tablewriter/.travis.yml delete mode 100644 vendor/github.com/olekukonko/tablewriter/LICENSE.md delete mode 100644 vendor/github.com/olekukonko/tablewriter/README.md delete mode 100644 vendor/github.com/olekukonko/tablewriter/csv.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/go.mod delete mode 100644 vendor/github.com/olekukonko/tablewriter/go.sum delete mode 100644 vendor/github.com/olekukonko/tablewriter/table.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/table_with_color.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/util.go delete mode 100644 vendor/github.com/olekukonko/tablewriter/wrap.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/.gitignore delete mode 100644 vendor/github.com/russross/blackfriday/v2/.travis.yml delete mode 100644 vendor/github.com/russross/blackfriday/v2/LICENSE.txt delete mode 100644 vendor/github.com/russross/blackfriday/v2/README.md delete mode 100644 vendor/github.com/russross/blackfriday/v2/block.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/doc.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/esc.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/go.mod delete mode 100644 vendor/github.com/russross/blackfriday/v2/html.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/inline.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/markdown.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/node.go delete mode 100644 vendor/github.com/russross/blackfriday/v2/smartypants.go delete mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml delete mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE delete mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/README.md delete mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/go.mod delete mode 100644 vendor/github.com/shurcooL/sanitized_anchor_name/main.go delete mode 100644 vendor/github.com/urfave/cli/v2/.flake8 delete mode 100644 vendor/github.com/urfave/cli/v2/.gitignore delete mode 100644 vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md delete mode 100644 vendor/github.com/urfave/cli/v2/LICENSE delete mode 100644 vendor/github.com/urfave/cli/v2/README.md delete mode 100644 vendor/github.com/urfave/cli/v2/app.go delete mode 100644 vendor/github.com/urfave/cli/v2/args.go delete mode 100644 vendor/github.com/urfave/cli/v2/category.go delete mode 100644 vendor/github.com/urfave/cli/v2/cli.go delete mode 100644 vendor/github.com/urfave/cli/v2/command.go delete mode 100644 vendor/github.com/urfave/cli/v2/context.go delete mode 100644 vendor/github.com/urfave/cli/v2/docs.go delete mode 100644 vendor/github.com/urfave/cli/v2/errors.go delete mode 100644 vendor/github.com/urfave/cli/v2/fish.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_bool.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_duration.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_float64.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_float64_slice.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_generic.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_int.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_int64.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_int64_slice.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_int_slice.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_path.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_string.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_string_slice.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_timestamp.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_uint.go delete mode 100644 vendor/github.com/urfave/cli/v2/flag_uint64.go delete mode 100644 vendor/github.com/urfave/cli/v2/funcs.go delete mode 100644 vendor/github.com/urfave/cli/v2/go.mod delete mode 100644 vendor/github.com/urfave/cli/v2/go.sum delete mode 100644 vendor/github.com/urfave/cli/v2/help.go delete mode 100644 vendor/github.com/urfave/cli/v2/parse.go delete mode 100644 vendor/github.com/urfave/cli/v2/sort.go delete mode 100644 vendor/github.com/urfave/cli/v2/template.go delete mode 100644 vendor/modules.txt diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md b/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md deleted file mode 100644 index 1cade6ce..00000000 --- a/vendor/github.com/cpuguy83/go-md2man/v2/LICENSE.md +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2014 Brian Goff - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go deleted file mode 100644 index b4800567..00000000 --- a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/md2man.go +++ /dev/null @@ -1,14 +0,0 @@ -package md2man - -import ( - "github.com/russross/blackfriday/v2" -) - -// Render converts a markdown document into a roff formatted document. -func Render(doc []byte) []byte { - renderer := NewRoffRenderer() - - return blackfriday.Run(doc, - []blackfriday.Option{blackfriday.WithRenderer(renderer), - blackfriday.WithExtensions(renderer.GetExtensions())}...) -} diff --git a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go b/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go deleted file mode 100644 index 0668a66c..00000000 --- a/vendor/github.com/cpuguy83/go-md2man/v2/md2man/roff.go +++ /dev/null @@ -1,345 +0,0 @@ -package md2man - -import ( - "fmt" - "io" - "os" - "strings" - - "github.com/russross/blackfriday/v2" -) - -// roffRenderer implements the blackfriday.Renderer interface for creating -// roff format (manpages) from markdown text -type roffRenderer struct { - extensions blackfriday.Extensions - listCounters []int - firstHeader bool - defineTerm bool - listDepth int -} - -const ( - titleHeader = ".TH " - topLevelHeader = "\n\n.SH " - secondLevelHdr = "\n.SH " - otherHeader = "\n.SS " - crTag = "\n" - emphTag = "\\fI" - emphCloseTag = "\\fP" - strongTag = "\\fB" - strongCloseTag = "\\fP" - breakTag = "\n.br\n" - paraTag = "\n.PP\n" - hruleTag = "\n.ti 0\n\\l'\\n(.lu'\n" - linkTag = "\n\\[la]" - linkCloseTag = "\\[ra]" - codespanTag = "\\fB\\fC" - codespanCloseTag = "\\fR" - codeTag = "\n.PP\n.RS\n\n.nf\n" - codeCloseTag = "\n.fi\n.RE\n" - quoteTag = "\n.PP\n.RS\n" - quoteCloseTag = "\n.RE\n" - listTag = "\n.RS\n" - listCloseTag = "\n.RE\n" - arglistTag = "\n.TP\n" - tableStart = "\n.TS\nallbox;\n" - tableEnd = ".TE\n" - tableCellStart = "T{\n" - tableCellEnd = "\nT}\n" -) - -// NewRoffRenderer creates a new blackfriday Renderer for generating roff documents -// from markdown -func NewRoffRenderer() *roffRenderer { // nolint: golint - var extensions blackfriday.Extensions - - extensions |= blackfriday.NoIntraEmphasis - extensions |= blackfriday.Tables - extensions |= blackfriday.FencedCode - extensions |= blackfriday.SpaceHeadings - extensions |= blackfriday.Footnotes - extensions |= blackfriday.Titleblock - extensions |= blackfriday.DefinitionLists - return &roffRenderer{ - extensions: extensions, - } -} - -// GetExtensions returns the list of extensions used by this renderer implementation -func (r *roffRenderer) GetExtensions() blackfriday.Extensions { - return r.extensions -} - -// RenderHeader handles outputting the header at document start -func (r *roffRenderer) RenderHeader(w io.Writer, ast *blackfriday.Node) { - // disable hyphenation - out(w, ".nh\n") -} - -// RenderFooter handles outputting the footer at the document end; the roff -// renderer has no footer information -func (r *roffRenderer) RenderFooter(w io.Writer, ast *blackfriday.Node) { -} - -// RenderNode is called for each node in a markdown document; based on the node -// type the equivalent roff output is sent to the writer -func (r *roffRenderer) RenderNode(w io.Writer, node *blackfriday.Node, entering bool) blackfriday.WalkStatus { - - var walkAction = blackfriday.GoToNext - - switch node.Type { - case blackfriday.Text: - r.handleText(w, node, entering) - case blackfriday.Softbreak: - out(w, crTag) - case blackfriday.Hardbreak: - out(w, breakTag) - case blackfriday.Emph: - if entering { - out(w, emphTag) - } else { - out(w, emphCloseTag) - } - case blackfriday.Strong: - if entering { - out(w, strongTag) - } else { - out(w, strongCloseTag) - } - case blackfriday.Link: - if !entering { - out(w, linkTag+string(node.LinkData.Destination)+linkCloseTag) - } - case blackfriday.Image: - // ignore images - walkAction = blackfriday.SkipChildren - case blackfriday.Code: - out(w, codespanTag) - escapeSpecialChars(w, node.Literal) - out(w, codespanCloseTag) - case blackfriday.Document: - break - case blackfriday.Paragraph: - // roff .PP markers break lists - if r.listDepth > 0 { - return blackfriday.GoToNext - } - if entering { - out(w, paraTag) - } else { - out(w, crTag) - } - case blackfriday.BlockQuote: - if entering { - out(w, quoteTag) - } else { - out(w, quoteCloseTag) - } - case blackfriday.Heading: - r.handleHeading(w, node, entering) - case blackfriday.HorizontalRule: - out(w, hruleTag) - case blackfriday.List: - r.handleList(w, node, entering) - case blackfriday.Item: - r.handleItem(w, node, entering) - case blackfriday.CodeBlock: - out(w, codeTag) - escapeSpecialChars(w, node.Literal) - out(w, codeCloseTag) - case blackfriday.Table: - r.handleTable(w, node, entering) - case blackfriday.TableCell: - r.handleTableCell(w, node, entering) - case blackfriday.TableHead: - case blackfriday.TableBody: - case blackfriday.TableRow: - // no action as cell entries do all the nroff formatting - return blackfriday.GoToNext - default: - fmt.Fprintln(os.Stderr, "WARNING: go-md2man does not handle node type "+node.Type.String()) - } - return walkAction -} - -func (r *roffRenderer) handleText(w io.Writer, node *blackfriday.Node, entering bool) { - var ( - start, end string - ) - // handle special roff table cell text encapsulation - if node.Parent.Type == blackfriday.TableCell { - if len(node.Literal) > 30 { - start = tableCellStart - end = tableCellEnd - } else { - // end rows that aren't terminated by "tableCellEnd" with a cr if end of row - if node.Parent.Next == nil && !node.Parent.IsHeader { - end = crTag - } - } - } - out(w, start) - escapeSpecialChars(w, node.Literal) - out(w, end) -} - -func (r *roffRenderer) handleHeading(w io.Writer, node *blackfriday.Node, entering bool) { - if entering { - switch node.Level { - case 1: - if !r.firstHeader { - out(w, titleHeader) - r.firstHeader = true - break - } - out(w, topLevelHeader) - case 2: - out(w, secondLevelHdr) - default: - out(w, otherHeader) - } - } -} - -func (r *roffRenderer) handleList(w io.Writer, node *blackfriday.Node, entering bool) { - openTag := listTag - closeTag := listCloseTag - if node.ListFlags&blackfriday.ListTypeDefinition != 0 { - // tags for definition lists handled within Item node - openTag = "" - closeTag = "" - } - if entering { - r.listDepth++ - if node.ListFlags&blackfriday.ListTypeOrdered != 0 { - r.listCounters = append(r.listCounters, 1) - } - out(w, openTag) - } else { - if node.ListFlags&blackfriday.ListTypeOrdered != 0 { - r.listCounters = r.listCounters[:len(r.listCounters)-1] - } - out(w, closeTag) - r.listDepth-- - } -} - -func (r *roffRenderer) handleItem(w io.Writer, node *blackfriday.Node, entering bool) { - if entering { - if node.ListFlags&blackfriday.ListTypeOrdered != 0 { - out(w, fmt.Sprintf(".IP \"%3d.\" 5\n", r.listCounters[len(r.listCounters)-1])) - r.listCounters[len(r.listCounters)-1]++ - } else if node.ListFlags&blackfriday.ListTypeDefinition != 0 { - // state machine for handling terms and following definitions - // since blackfriday does not distinguish them properly, nor - // does it seperate them into separate lists as it should - if !r.defineTerm { - out(w, arglistTag) - r.defineTerm = true - } else { - r.defineTerm = false - } - } else { - out(w, ".IP \\(bu 2\n") - } - } else { - out(w, "\n") - } -} - -func (r *roffRenderer) handleTable(w io.Writer, node *blackfriday.Node, entering bool) { - if entering { - out(w, tableStart) - //call walker to count cells (and rows?) so format section can be produced - columns := countColumns(node) - out(w, strings.Repeat("l ", columns)+"\n") - out(w, strings.Repeat("l ", columns)+".\n") - } else { - out(w, tableEnd) - } -} - -func (r *roffRenderer) handleTableCell(w io.Writer, node *blackfriday.Node, entering bool) { - var ( - start, end string - ) - if node.IsHeader { - start = codespanTag - end = codespanCloseTag - } - if entering { - if node.Prev != nil && node.Prev.Type == blackfriday.TableCell { - out(w, "\t"+start) - } else { - out(w, start) - } - } else { - // need to carriage return if we are at the end of the header row - if node.IsHeader && node.Next == nil { - end = end + crTag - } - out(w, end) - } -} - -// because roff format requires knowing the column count before outputting any table -// data we need to walk a table tree and count the columns -func countColumns(node *blackfriday.Node) int { - var columns int - - node.Walk(func(node *blackfriday.Node, entering bool) blackfriday.WalkStatus { - switch node.Type { - case blackfriday.TableRow: - if !entering { - return blackfriday.Terminate - } - case blackfriday.TableCell: - if entering { - columns++ - } - default: - } - return blackfriday.GoToNext - }) - return columns -} - -func out(w io.Writer, output string) { - io.WriteString(w, output) // nolint: errcheck -} - -func needsBackslash(c byte) bool { - for _, r := range []byte("-_&\\~") { - if c == r { - return true - } - } - return false -} - -func escapeSpecialChars(w io.Writer, text []byte) { - for i := 0; i < len(text); i++ { - // escape initial apostrophe or period - if len(text) >= 1 && (text[0] == '\'' || text[0] == '.') { - out(w, "\\&") - } - - // directly copy normal characters - org := i - - for i < len(text) && !needsBackslash(text[i]) { - i++ - } - if i > org { - w.Write(text[org:i]) // nolint: errcheck - } - - // escape a character - if i >= len(text) { - break - } - - w.Write([]byte{'\\', text[i]}) // nolint: errcheck - } -} diff --git a/vendor/github.com/mattn/go-runewidth/.travis.yml b/vendor/github.com/mattn/go-runewidth/.travis.yml deleted file mode 100644 index 6a21813a..00000000 --- a/vendor/github.com/mattn/go-runewidth/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -language: go -sudo: false -go: - - 1.13.x - - tip - -before_install: - - go get -t -v ./... - -script: - - go generate - - git diff --cached --exit-code - - ./go.test.sh - -after_success: - - bash <(curl -s https://codecov.io/bash) diff --git a/vendor/github.com/mattn/go-runewidth/LICENSE b/vendor/github.com/mattn/go-runewidth/LICENSE deleted file mode 100644 index 91b5cef3..00000000 --- a/vendor/github.com/mattn/go-runewidth/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -The MIT License (MIT) - -Copyright (c) 2016 Yasuhiro Matsumoto - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/mattn/go-runewidth/README.md b/vendor/github.com/mattn/go-runewidth/README.md deleted file mode 100644 index aa56ab96..00000000 --- a/vendor/github.com/mattn/go-runewidth/README.md +++ /dev/null @@ -1,27 +0,0 @@ -go-runewidth -============ - -[![Build Status](https://travis-ci.org/mattn/go-runewidth.png?branch=master)](https://travis-ci.org/mattn/go-runewidth) -[![Codecov](https://codecov.io/gh/mattn/go-runewidth/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-runewidth) -[![GoDoc](https://godoc.org/github.com/mattn/go-runewidth?status.svg)](http://godoc.org/github.com/mattn/go-runewidth) -[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-runewidth)](https://goreportcard.com/report/github.com/mattn/go-runewidth) - -Provides functions to get fixed width of the character or string. - -Usage ------ - -```go -runewidth.StringWidth("つのだ☆HIRO") == 12 -``` - - -Author ------- - -Yasuhiro Matsumoto - -License -------- - -under the MIT License: http://mattn.mit-license.org/2013 diff --git a/vendor/github.com/mattn/go-runewidth/go.mod b/vendor/github.com/mattn/go-runewidth/go.mod deleted file mode 100644 index fa7f4d86..00000000 --- a/vendor/github.com/mattn/go-runewidth/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/mattn/go-runewidth - -go 1.9 diff --git a/vendor/github.com/mattn/go-runewidth/go.test.sh b/vendor/github.com/mattn/go-runewidth/go.test.sh deleted file mode 100644 index 012162b0..00000000 --- a/vendor/github.com/mattn/go-runewidth/go.test.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash - -set -e -echo "" > coverage.txt - -for d in $(go list ./... | grep -v vendor); do - go test -race -coverprofile=profile.out -covermode=atomic "$d" - if [ -f profile.out ]; then - cat profile.out >> coverage.txt - rm profile.out - fi -done diff --git a/vendor/github.com/mattn/go-runewidth/runewidth.go b/vendor/github.com/mattn/go-runewidth/runewidth.go deleted file mode 100644 index 19f8e044..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth.go +++ /dev/null @@ -1,257 +0,0 @@ -package runewidth - -import ( - "os" -) - -//go:generate go run script/generate.go - -var ( - // EastAsianWidth will be set true if the current locale is CJK - EastAsianWidth bool - - // ZeroWidthJoiner is flag to set to use UTR#51 ZWJ - ZeroWidthJoiner bool - - // DefaultCondition is a condition in current locale - DefaultCondition = &Condition{} -) - -func init() { - handleEnv() -} - -func handleEnv() { - env := os.Getenv("RUNEWIDTH_EASTASIAN") - if env == "" { - EastAsianWidth = IsEastAsian() - } else { - EastAsianWidth = env == "1" - } - // update DefaultCondition - DefaultCondition.EastAsianWidth = EastAsianWidth - DefaultCondition.ZeroWidthJoiner = ZeroWidthJoiner -} - -type interval struct { - first rune - last rune -} - -type table []interval - -func inTables(r rune, ts ...table) bool { - for _, t := range ts { - if inTable(r, t) { - return true - } - } - return false -} - -func inTable(r rune, t table) bool { - if r < t[0].first { - return false - } - - bot := 0 - top := len(t) - 1 - for top >= bot { - mid := (bot + top) >> 1 - - switch { - case t[mid].last < r: - bot = mid + 1 - case t[mid].first > r: - top = mid - 1 - default: - return true - } - } - - return false -} - -var private = table{ - {0x00E000, 0x00F8FF}, {0x0F0000, 0x0FFFFD}, {0x100000, 0x10FFFD}, -} - -var nonprint = table{ - {0x0000, 0x001F}, {0x007F, 0x009F}, {0x00AD, 0x00AD}, - {0x070F, 0x070F}, {0x180B, 0x180E}, {0x200B, 0x200F}, - {0x2028, 0x202E}, {0x206A, 0x206F}, {0xD800, 0xDFFF}, - {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFB}, {0xFFFE, 0xFFFF}, -} - -// Condition have flag EastAsianWidth whether the current locale is CJK or not. -type Condition struct { - EastAsianWidth bool - ZeroWidthJoiner bool -} - -// NewCondition return new instance of Condition which is current locale. -func NewCondition() *Condition { - return &Condition{ - EastAsianWidth: EastAsianWidth, - ZeroWidthJoiner: ZeroWidthJoiner, - } -} - -// RuneWidth returns the number of cells in r. -// See http://www.unicode.org/reports/tr11/ -func (c *Condition) RuneWidth(r rune) int { - switch { - case r < 0 || r > 0x10FFFF || inTables(r, nonprint, combining, notassigned): - return 0 - case (c.EastAsianWidth && IsAmbiguousWidth(r)) || inTables(r, doublewidth): - return 2 - default: - return 1 - } -} - -func (c *Condition) stringWidth(s string) (width int) { - for _, r := range []rune(s) { - width += c.RuneWidth(r) - } - return width -} - -func (c *Condition) stringWidthZeroJoiner(s string) (width int) { - r1, r2 := rune(0), rune(0) - for _, r := range []rune(s) { - if r == 0xFE0E || r == 0xFE0F { - continue - } - w := c.RuneWidth(r) - if r2 == 0x200D && inTables(r, emoji) && inTables(r1, emoji) { - if width < w { - width = w - } - } else { - width += w - } - r1, r2 = r2, r - } - return width -} - -// StringWidth return width as you can see -func (c *Condition) StringWidth(s string) (width int) { - if c.ZeroWidthJoiner { - return c.stringWidthZeroJoiner(s) - } - return c.stringWidth(s) -} - -// Truncate return string truncated with w cells -func (c *Condition) Truncate(s string, w int, tail string) string { - if c.StringWidth(s) <= w { - return s - } - r := []rune(s) - tw := c.StringWidth(tail) - w -= tw - width := 0 - i := 0 - for ; i < len(r); i++ { - cw := c.RuneWidth(r[i]) - if width+cw > w { - break - } - width += cw - } - return string(r[0:i]) + tail -} - -// Wrap return string wrapped with w cells -func (c *Condition) Wrap(s string, w int) string { - width := 0 - out := "" - for _, r := range []rune(s) { - cw := RuneWidth(r) - if r == '\n' { - out += string(r) - width = 0 - continue - } else if width+cw > w { - out += "\n" - width = 0 - out += string(r) - width += cw - continue - } - out += string(r) - width += cw - } - return out -} - -// FillLeft return string filled in left by spaces in w cells -func (c *Condition) FillLeft(s string, w int) string { - width := c.StringWidth(s) - count := w - width - if count > 0 { - b := make([]byte, count) - for i := range b { - b[i] = ' ' - } - return string(b) + s - } - return s -} - -// FillRight return string filled in left by spaces in w cells -func (c *Condition) FillRight(s string, w int) string { - width := c.StringWidth(s) - count := w - width - if count > 0 { - b := make([]byte, count) - for i := range b { - b[i] = ' ' - } - return s + string(b) - } - return s -} - -// RuneWidth returns the number of cells in r. -// See http://www.unicode.org/reports/tr11/ -func RuneWidth(r rune) int { - return DefaultCondition.RuneWidth(r) -} - -// IsAmbiguousWidth returns whether is ambiguous width or not. -func IsAmbiguousWidth(r rune) bool { - return inTables(r, private, ambiguous) -} - -// IsNeutralWidth returns whether is neutral width or not. -func IsNeutralWidth(r rune) bool { - return inTable(r, neutral) -} - -// StringWidth return width as you can see -func StringWidth(s string) (width int) { - return DefaultCondition.StringWidth(s) -} - -// Truncate return string truncated with w cells -func Truncate(s string, w int, tail string) string { - return DefaultCondition.Truncate(s, w, tail) -} - -// Wrap return string wrapped with w cells -func Wrap(s string, w int) string { - return DefaultCondition.Wrap(s, w) -} - -// FillLeft return string filled in left by spaces in w cells -func FillLeft(s string, w int) string { - return DefaultCondition.FillLeft(s, w) -} - -// FillRight return string filled in left by spaces in w cells -func FillRight(s string, w int) string { - return DefaultCondition.FillRight(s, w) -} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go b/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go deleted file mode 100644 index 7d99f6e5..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth_appengine.go +++ /dev/null @@ -1,8 +0,0 @@ -// +build appengine - -package runewidth - -// IsEastAsian return true if the current locale is CJK -func IsEastAsian() bool { - return false -} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_js.go b/vendor/github.com/mattn/go-runewidth/runewidth_js.go deleted file mode 100644 index c5fdf40b..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth_js.go +++ /dev/null @@ -1,9 +0,0 @@ -// +build js -// +build !appengine - -package runewidth - -func IsEastAsian() bool { - // TODO: Implement this for the web. Detect east asian in a compatible way, and return true. - return false -} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_posix.go b/vendor/github.com/mattn/go-runewidth/runewidth_posix.go deleted file mode 100644 index 480ad748..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth_posix.go +++ /dev/null @@ -1,82 +0,0 @@ -// +build !windows -// +build !js -// +build !appengine - -package runewidth - -import ( - "os" - "regexp" - "strings" -) - -var reLoc = regexp.MustCompile(`^[a-z][a-z][a-z]?(?:_[A-Z][A-Z])?\.(.+)`) - -var mblenTable = map[string]int{ - "utf-8": 6, - "utf8": 6, - "jis": 8, - "eucjp": 3, - "euckr": 2, - "euccn": 2, - "sjis": 2, - "cp932": 2, - "cp51932": 2, - "cp936": 2, - "cp949": 2, - "cp950": 2, - "big5": 2, - "gbk": 2, - "gb2312": 2, -} - -func isEastAsian(locale string) bool { - charset := strings.ToLower(locale) - r := reLoc.FindStringSubmatch(locale) - if len(r) == 2 { - charset = strings.ToLower(r[1]) - } - - if strings.HasSuffix(charset, "@cjk_narrow") { - return false - } - - for pos, b := range []byte(charset) { - if b == '@' { - charset = charset[:pos] - break - } - } - max := 1 - if m, ok := mblenTable[charset]; ok { - max = m - } - if max > 1 && (charset[0] != 'u' || - strings.HasPrefix(locale, "ja") || - strings.HasPrefix(locale, "ko") || - strings.HasPrefix(locale, "zh")) { - return true - } - return false -} - -// IsEastAsian return true if the current locale is CJK -func IsEastAsian() bool { - locale := os.Getenv("LC_ALL") - if locale == "" { - locale = os.Getenv("LC_CTYPE") - } - if locale == "" { - locale = os.Getenv("LANG") - } - - // ignore C locale - if locale == "POSIX" || locale == "C" { - return false - } - if len(locale) > 1 && locale[0] == 'C' && (locale[1] == '.' || locale[1] == '-') { - return false - } - - return isEastAsian(locale) -} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_table.go b/vendor/github.com/mattn/go-runewidth/runewidth_table.go deleted file mode 100644 index b27d77d8..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth_table.go +++ /dev/null @@ -1,437 +0,0 @@ -// Code generated by script/generate.go. DO NOT EDIT. - -package runewidth - -var combining = table{ - {0x0300, 0x036F}, {0x0483, 0x0489}, {0x07EB, 0x07F3}, - {0x0C00, 0x0C00}, {0x0C04, 0x0C04}, {0x0D00, 0x0D01}, - {0x135D, 0x135F}, {0x1A7F, 0x1A7F}, {0x1AB0, 0x1AC0}, - {0x1B6B, 0x1B73}, {0x1DC0, 0x1DF9}, {0x1DFB, 0x1DFF}, - {0x20D0, 0x20F0}, {0x2CEF, 0x2CF1}, {0x2DE0, 0x2DFF}, - {0x3099, 0x309A}, {0xA66F, 0xA672}, {0xA674, 0xA67D}, - {0xA69E, 0xA69F}, {0xA6F0, 0xA6F1}, {0xA8E0, 0xA8F1}, - {0xFE20, 0xFE2F}, {0x101FD, 0x101FD}, {0x10376, 0x1037A}, - {0x10EAB, 0x10EAC}, {0x10F46, 0x10F50}, {0x11300, 0x11301}, - {0x1133B, 0x1133C}, {0x11366, 0x1136C}, {0x11370, 0x11374}, - {0x16AF0, 0x16AF4}, {0x1D165, 0x1D169}, {0x1D16D, 0x1D172}, - {0x1D17B, 0x1D182}, {0x1D185, 0x1D18B}, {0x1D1AA, 0x1D1AD}, - {0x1D242, 0x1D244}, {0x1E000, 0x1E006}, {0x1E008, 0x1E018}, - {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, {0x1E026, 0x1E02A}, - {0x1E8D0, 0x1E8D6}, -} - -var doublewidth = table{ - {0x1100, 0x115F}, {0x231A, 0x231B}, {0x2329, 0x232A}, - {0x23E9, 0x23EC}, {0x23F0, 0x23F0}, {0x23F3, 0x23F3}, - {0x25FD, 0x25FE}, {0x2614, 0x2615}, {0x2648, 0x2653}, - {0x267F, 0x267F}, {0x2693, 0x2693}, {0x26A1, 0x26A1}, - {0x26AA, 0x26AB}, {0x26BD, 0x26BE}, {0x26C4, 0x26C5}, - {0x26CE, 0x26CE}, {0x26D4, 0x26D4}, {0x26EA, 0x26EA}, - {0x26F2, 0x26F3}, {0x26F5, 0x26F5}, {0x26FA, 0x26FA}, - {0x26FD, 0x26FD}, {0x2705, 0x2705}, {0x270A, 0x270B}, - {0x2728, 0x2728}, {0x274C, 0x274C}, {0x274E, 0x274E}, - {0x2753, 0x2755}, {0x2757, 0x2757}, {0x2795, 0x2797}, - {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, {0x2B1B, 0x2B1C}, - {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x2E80, 0x2E99}, - {0x2E9B, 0x2EF3}, {0x2F00, 0x2FD5}, {0x2FF0, 0x2FFB}, - {0x3000, 0x303E}, {0x3041, 0x3096}, {0x3099, 0x30FF}, - {0x3105, 0x312F}, {0x3131, 0x318E}, {0x3190, 0x31E3}, - {0x31F0, 0x321E}, {0x3220, 0x3247}, {0x3250, 0x4DBF}, - {0x4E00, 0xA48C}, {0xA490, 0xA4C6}, {0xA960, 0xA97C}, - {0xAC00, 0xD7A3}, {0xF900, 0xFAFF}, {0xFE10, 0xFE19}, - {0xFE30, 0xFE52}, {0xFE54, 0xFE66}, {0xFE68, 0xFE6B}, - {0xFF01, 0xFF60}, {0xFFE0, 0xFFE6}, {0x16FE0, 0x16FE4}, - {0x16FF0, 0x16FF1}, {0x17000, 0x187F7}, {0x18800, 0x18CD5}, - {0x18D00, 0x18D08}, {0x1B000, 0x1B11E}, {0x1B150, 0x1B152}, - {0x1B164, 0x1B167}, {0x1B170, 0x1B2FB}, {0x1F004, 0x1F004}, - {0x1F0CF, 0x1F0CF}, {0x1F18E, 0x1F18E}, {0x1F191, 0x1F19A}, - {0x1F200, 0x1F202}, {0x1F210, 0x1F23B}, {0x1F240, 0x1F248}, - {0x1F250, 0x1F251}, {0x1F260, 0x1F265}, {0x1F300, 0x1F320}, - {0x1F32D, 0x1F335}, {0x1F337, 0x1F37C}, {0x1F37E, 0x1F393}, - {0x1F3A0, 0x1F3CA}, {0x1F3CF, 0x1F3D3}, {0x1F3E0, 0x1F3F0}, - {0x1F3F4, 0x1F3F4}, {0x1F3F8, 0x1F43E}, {0x1F440, 0x1F440}, - {0x1F442, 0x1F4FC}, {0x1F4FF, 0x1F53D}, {0x1F54B, 0x1F54E}, - {0x1F550, 0x1F567}, {0x1F57A, 0x1F57A}, {0x1F595, 0x1F596}, - {0x1F5A4, 0x1F5A4}, {0x1F5FB, 0x1F64F}, {0x1F680, 0x1F6C5}, - {0x1F6CC, 0x1F6CC}, {0x1F6D0, 0x1F6D2}, {0x1F6D5, 0x1F6D7}, - {0x1F6EB, 0x1F6EC}, {0x1F6F4, 0x1F6FC}, {0x1F7E0, 0x1F7EB}, - {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1F978}, - {0x1F97A, 0x1F9CB}, {0x1F9CD, 0x1F9FF}, {0x1FA70, 0x1FA74}, - {0x1FA78, 0x1FA7A}, {0x1FA80, 0x1FA86}, {0x1FA90, 0x1FAA8}, - {0x1FAB0, 0x1FAB6}, {0x1FAC0, 0x1FAC2}, {0x1FAD0, 0x1FAD6}, - {0x20000, 0x2FFFD}, {0x30000, 0x3FFFD}, -} - -var ambiguous = table{ - {0x00A1, 0x00A1}, {0x00A4, 0x00A4}, {0x00A7, 0x00A8}, - {0x00AA, 0x00AA}, {0x00AD, 0x00AE}, {0x00B0, 0x00B4}, - {0x00B6, 0x00BA}, {0x00BC, 0x00BF}, {0x00C6, 0x00C6}, - {0x00D0, 0x00D0}, {0x00D7, 0x00D8}, {0x00DE, 0x00E1}, - {0x00E6, 0x00E6}, {0x00E8, 0x00EA}, {0x00EC, 0x00ED}, - {0x00F0, 0x00F0}, {0x00F2, 0x00F3}, {0x00F7, 0x00FA}, - {0x00FC, 0x00FC}, {0x00FE, 0x00FE}, {0x0101, 0x0101}, - {0x0111, 0x0111}, {0x0113, 0x0113}, {0x011B, 0x011B}, - {0x0126, 0x0127}, {0x012B, 0x012B}, {0x0131, 0x0133}, - {0x0138, 0x0138}, {0x013F, 0x0142}, {0x0144, 0x0144}, - {0x0148, 0x014B}, {0x014D, 0x014D}, {0x0152, 0x0153}, - {0x0166, 0x0167}, {0x016B, 0x016B}, {0x01CE, 0x01CE}, - {0x01D0, 0x01D0}, {0x01D2, 0x01D2}, {0x01D4, 0x01D4}, - {0x01D6, 0x01D6}, {0x01D8, 0x01D8}, {0x01DA, 0x01DA}, - {0x01DC, 0x01DC}, {0x0251, 0x0251}, {0x0261, 0x0261}, - {0x02C4, 0x02C4}, {0x02C7, 0x02C7}, {0x02C9, 0x02CB}, - {0x02CD, 0x02CD}, {0x02D0, 0x02D0}, {0x02D8, 0x02DB}, - {0x02DD, 0x02DD}, {0x02DF, 0x02DF}, {0x0300, 0x036F}, - {0x0391, 0x03A1}, {0x03A3, 0x03A9}, {0x03B1, 0x03C1}, - {0x03C3, 0x03C9}, {0x0401, 0x0401}, {0x0410, 0x044F}, - {0x0451, 0x0451}, {0x2010, 0x2010}, {0x2013, 0x2016}, - {0x2018, 0x2019}, {0x201C, 0x201D}, {0x2020, 0x2022}, - {0x2024, 0x2027}, {0x2030, 0x2030}, {0x2032, 0x2033}, - {0x2035, 0x2035}, {0x203B, 0x203B}, {0x203E, 0x203E}, - {0x2074, 0x2074}, {0x207F, 0x207F}, {0x2081, 0x2084}, - {0x20AC, 0x20AC}, {0x2103, 0x2103}, {0x2105, 0x2105}, - {0x2109, 0x2109}, {0x2113, 0x2113}, {0x2116, 0x2116}, - {0x2121, 0x2122}, {0x2126, 0x2126}, {0x212B, 0x212B}, - {0x2153, 0x2154}, {0x215B, 0x215E}, {0x2160, 0x216B}, - {0x2170, 0x2179}, {0x2189, 0x2189}, {0x2190, 0x2199}, - {0x21B8, 0x21B9}, {0x21D2, 0x21D2}, {0x21D4, 0x21D4}, - {0x21E7, 0x21E7}, {0x2200, 0x2200}, {0x2202, 0x2203}, - {0x2207, 0x2208}, {0x220B, 0x220B}, {0x220F, 0x220F}, - {0x2211, 0x2211}, {0x2215, 0x2215}, {0x221A, 0x221A}, - {0x221D, 0x2220}, {0x2223, 0x2223}, {0x2225, 0x2225}, - {0x2227, 0x222C}, {0x222E, 0x222E}, {0x2234, 0x2237}, - {0x223C, 0x223D}, {0x2248, 0x2248}, {0x224C, 0x224C}, - {0x2252, 0x2252}, {0x2260, 0x2261}, {0x2264, 0x2267}, - {0x226A, 0x226B}, {0x226E, 0x226F}, {0x2282, 0x2283}, - {0x2286, 0x2287}, {0x2295, 0x2295}, {0x2299, 0x2299}, - {0x22A5, 0x22A5}, {0x22BF, 0x22BF}, {0x2312, 0x2312}, - {0x2460, 0x24E9}, {0x24EB, 0x254B}, {0x2550, 0x2573}, - {0x2580, 0x258F}, {0x2592, 0x2595}, {0x25A0, 0x25A1}, - {0x25A3, 0x25A9}, {0x25B2, 0x25B3}, {0x25B6, 0x25B7}, - {0x25BC, 0x25BD}, {0x25C0, 0x25C1}, {0x25C6, 0x25C8}, - {0x25CB, 0x25CB}, {0x25CE, 0x25D1}, {0x25E2, 0x25E5}, - {0x25EF, 0x25EF}, {0x2605, 0x2606}, {0x2609, 0x2609}, - {0x260E, 0x260F}, {0x261C, 0x261C}, {0x261E, 0x261E}, - {0x2640, 0x2640}, {0x2642, 0x2642}, {0x2660, 0x2661}, - {0x2663, 0x2665}, {0x2667, 0x266A}, {0x266C, 0x266D}, - {0x266F, 0x266F}, {0x269E, 0x269F}, {0x26BF, 0x26BF}, - {0x26C6, 0x26CD}, {0x26CF, 0x26D3}, {0x26D5, 0x26E1}, - {0x26E3, 0x26E3}, {0x26E8, 0x26E9}, {0x26EB, 0x26F1}, - {0x26F4, 0x26F4}, {0x26F6, 0x26F9}, {0x26FB, 0x26FC}, - {0x26FE, 0x26FF}, {0x273D, 0x273D}, {0x2776, 0x277F}, - {0x2B56, 0x2B59}, {0x3248, 0x324F}, {0xE000, 0xF8FF}, - {0xFE00, 0xFE0F}, {0xFFFD, 0xFFFD}, {0x1F100, 0x1F10A}, - {0x1F110, 0x1F12D}, {0x1F130, 0x1F169}, {0x1F170, 0x1F18D}, - {0x1F18F, 0x1F190}, {0x1F19B, 0x1F1AC}, {0xE0100, 0xE01EF}, - {0xF0000, 0xFFFFD}, {0x100000, 0x10FFFD}, -} -var notassigned = table{ - {0x27E6, 0x27ED}, {0x2985, 0x2986}, -} - -var neutral = table{ - {0x0000, 0x001F}, {0x007F, 0x00A0}, {0x00A9, 0x00A9}, - {0x00AB, 0x00AB}, {0x00B5, 0x00B5}, {0x00BB, 0x00BB}, - {0x00C0, 0x00C5}, {0x00C7, 0x00CF}, {0x00D1, 0x00D6}, - {0x00D9, 0x00DD}, {0x00E2, 0x00E5}, {0x00E7, 0x00E7}, - {0x00EB, 0x00EB}, {0x00EE, 0x00EF}, {0x00F1, 0x00F1}, - {0x00F4, 0x00F6}, {0x00FB, 0x00FB}, {0x00FD, 0x00FD}, - {0x00FF, 0x0100}, {0x0102, 0x0110}, {0x0112, 0x0112}, - {0x0114, 0x011A}, {0x011C, 0x0125}, {0x0128, 0x012A}, - {0x012C, 0x0130}, {0x0134, 0x0137}, {0x0139, 0x013E}, - {0x0143, 0x0143}, {0x0145, 0x0147}, {0x014C, 0x014C}, - {0x014E, 0x0151}, {0x0154, 0x0165}, {0x0168, 0x016A}, - {0x016C, 0x01CD}, {0x01CF, 0x01CF}, {0x01D1, 0x01D1}, - {0x01D3, 0x01D3}, {0x01D5, 0x01D5}, {0x01D7, 0x01D7}, - {0x01D9, 0x01D9}, {0x01DB, 0x01DB}, {0x01DD, 0x0250}, - {0x0252, 0x0260}, {0x0262, 0x02C3}, {0x02C5, 0x02C6}, - {0x02C8, 0x02C8}, {0x02CC, 0x02CC}, {0x02CE, 0x02CF}, - {0x02D1, 0x02D7}, {0x02DC, 0x02DC}, {0x02DE, 0x02DE}, - {0x02E0, 0x02FF}, {0x0370, 0x0377}, {0x037A, 0x037F}, - {0x0384, 0x038A}, {0x038C, 0x038C}, {0x038E, 0x0390}, - {0x03AA, 0x03B0}, {0x03C2, 0x03C2}, {0x03CA, 0x0400}, - {0x0402, 0x040F}, {0x0450, 0x0450}, {0x0452, 0x052F}, - {0x0531, 0x0556}, {0x0559, 0x058A}, {0x058D, 0x058F}, - {0x0591, 0x05C7}, {0x05D0, 0x05EA}, {0x05EF, 0x05F4}, - {0x0600, 0x061C}, {0x061E, 0x070D}, {0x070F, 0x074A}, - {0x074D, 0x07B1}, {0x07C0, 0x07FA}, {0x07FD, 0x082D}, - {0x0830, 0x083E}, {0x0840, 0x085B}, {0x085E, 0x085E}, - {0x0860, 0x086A}, {0x08A0, 0x08B4}, {0x08B6, 0x08C7}, - {0x08D3, 0x0983}, {0x0985, 0x098C}, {0x098F, 0x0990}, - {0x0993, 0x09A8}, {0x09AA, 0x09B0}, {0x09B2, 0x09B2}, - {0x09B6, 0x09B9}, {0x09BC, 0x09C4}, {0x09C7, 0x09C8}, - {0x09CB, 0x09CE}, {0x09D7, 0x09D7}, {0x09DC, 0x09DD}, - {0x09DF, 0x09E3}, {0x09E6, 0x09FE}, {0x0A01, 0x0A03}, - {0x0A05, 0x0A0A}, {0x0A0F, 0x0A10}, {0x0A13, 0x0A28}, - {0x0A2A, 0x0A30}, {0x0A32, 0x0A33}, {0x0A35, 0x0A36}, - {0x0A38, 0x0A39}, {0x0A3C, 0x0A3C}, {0x0A3E, 0x0A42}, - {0x0A47, 0x0A48}, {0x0A4B, 0x0A4D}, {0x0A51, 0x0A51}, - {0x0A59, 0x0A5C}, {0x0A5E, 0x0A5E}, {0x0A66, 0x0A76}, - {0x0A81, 0x0A83}, {0x0A85, 0x0A8D}, {0x0A8F, 0x0A91}, - {0x0A93, 0x0AA8}, {0x0AAA, 0x0AB0}, {0x0AB2, 0x0AB3}, - {0x0AB5, 0x0AB9}, {0x0ABC, 0x0AC5}, {0x0AC7, 0x0AC9}, - {0x0ACB, 0x0ACD}, {0x0AD0, 0x0AD0}, {0x0AE0, 0x0AE3}, - {0x0AE6, 0x0AF1}, {0x0AF9, 0x0AFF}, {0x0B01, 0x0B03}, - {0x0B05, 0x0B0C}, {0x0B0F, 0x0B10}, {0x0B13, 0x0B28}, - {0x0B2A, 0x0B30}, {0x0B32, 0x0B33}, {0x0B35, 0x0B39}, - {0x0B3C, 0x0B44}, {0x0B47, 0x0B48}, {0x0B4B, 0x0B4D}, - {0x0B55, 0x0B57}, {0x0B5C, 0x0B5D}, {0x0B5F, 0x0B63}, - {0x0B66, 0x0B77}, {0x0B82, 0x0B83}, {0x0B85, 0x0B8A}, - {0x0B8E, 0x0B90}, {0x0B92, 0x0B95}, {0x0B99, 0x0B9A}, - {0x0B9C, 0x0B9C}, {0x0B9E, 0x0B9F}, {0x0BA3, 0x0BA4}, - {0x0BA8, 0x0BAA}, {0x0BAE, 0x0BB9}, {0x0BBE, 0x0BC2}, - {0x0BC6, 0x0BC8}, {0x0BCA, 0x0BCD}, {0x0BD0, 0x0BD0}, - {0x0BD7, 0x0BD7}, {0x0BE6, 0x0BFA}, {0x0C00, 0x0C0C}, - {0x0C0E, 0x0C10}, {0x0C12, 0x0C28}, {0x0C2A, 0x0C39}, - {0x0C3D, 0x0C44}, {0x0C46, 0x0C48}, {0x0C4A, 0x0C4D}, - {0x0C55, 0x0C56}, {0x0C58, 0x0C5A}, {0x0C60, 0x0C63}, - {0x0C66, 0x0C6F}, {0x0C77, 0x0C8C}, {0x0C8E, 0x0C90}, - {0x0C92, 0x0CA8}, {0x0CAA, 0x0CB3}, {0x0CB5, 0x0CB9}, - {0x0CBC, 0x0CC4}, {0x0CC6, 0x0CC8}, {0x0CCA, 0x0CCD}, - {0x0CD5, 0x0CD6}, {0x0CDE, 0x0CDE}, {0x0CE0, 0x0CE3}, - {0x0CE6, 0x0CEF}, {0x0CF1, 0x0CF2}, {0x0D00, 0x0D0C}, - {0x0D0E, 0x0D10}, {0x0D12, 0x0D44}, {0x0D46, 0x0D48}, - {0x0D4A, 0x0D4F}, {0x0D54, 0x0D63}, {0x0D66, 0x0D7F}, - {0x0D81, 0x0D83}, {0x0D85, 0x0D96}, {0x0D9A, 0x0DB1}, - {0x0DB3, 0x0DBB}, {0x0DBD, 0x0DBD}, {0x0DC0, 0x0DC6}, - {0x0DCA, 0x0DCA}, {0x0DCF, 0x0DD4}, {0x0DD6, 0x0DD6}, - {0x0DD8, 0x0DDF}, {0x0DE6, 0x0DEF}, {0x0DF2, 0x0DF4}, - {0x0E01, 0x0E3A}, {0x0E3F, 0x0E5B}, {0x0E81, 0x0E82}, - {0x0E84, 0x0E84}, {0x0E86, 0x0E8A}, {0x0E8C, 0x0EA3}, - {0x0EA5, 0x0EA5}, {0x0EA7, 0x0EBD}, {0x0EC0, 0x0EC4}, - {0x0EC6, 0x0EC6}, {0x0EC8, 0x0ECD}, {0x0ED0, 0x0ED9}, - {0x0EDC, 0x0EDF}, {0x0F00, 0x0F47}, {0x0F49, 0x0F6C}, - {0x0F71, 0x0F97}, {0x0F99, 0x0FBC}, {0x0FBE, 0x0FCC}, - {0x0FCE, 0x0FDA}, {0x1000, 0x10C5}, {0x10C7, 0x10C7}, - {0x10CD, 0x10CD}, {0x10D0, 0x10FF}, {0x1160, 0x1248}, - {0x124A, 0x124D}, {0x1250, 0x1256}, {0x1258, 0x1258}, - {0x125A, 0x125D}, {0x1260, 0x1288}, {0x128A, 0x128D}, - {0x1290, 0x12B0}, {0x12B2, 0x12B5}, {0x12B8, 0x12BE}, - {0x12C0, 0x12C0}, {0x12C2, 0x12C5}, {0x12C8, 0x12D6}, - {0x12D8, 0x1310}, {0x1312, 0x1315}, {0x1318, 0x135A}, - {0x135D, 0x137C}, {0x1380, 0x1399}, {0x13A0, 0x13F5}, - {0x13F8, 0x13FD}, {0x1400, 0x169C}, {0x16A0, 0x16F8}, - {0x1700, 0x170C}, {0x170E, 0x1714}, {0x1720, 0x1736}, - {0x1740, 0x1753}, {0x1760, 0x176C}, {0x176E, 0x1770}, - {0x1772, 0x1773}, {0x1780, 0x17DD}, {0x17E0, 0x17E9}, - {0x17F0, 0x17F9}, {0x1800, 0x180E}, {0x1810, 0x1819}, - {0x1820, 0x1878}, {0x1880, 0x18AA}, {0x18B0, 0x18F5}, - {0x1900, 0x191E}, {0x1920, 0x192B}, {0x1930, 0x193B}, - {0x1940, 0x1940}, {0x1944, 0x196D}, {0x1970, 0x1974}, - {0x1980, 0x19AB}, {0x19B0, 0x19C9}, {0x19D0, 0x19DA}, - {0x19DE, 0x1A1B}, {0x1A1E, 0x1A5E}, {0x1A60, 0x1A7C}, - {0x1A7F, 0x1A89}, {0x1A90, 0x1A99}, {0x1AA0, 0x1AAD}, - {0x1AB0, 0x1AC0}, {0x1B00, 0x1B4B}, {0x1B50, 0x1B7C}, - {0x1B80, 0x1BF3}, {0x1BFC, 0x1C37}, {0x1C3B, 0x1C49}, - {0x1C4D, 0x1C88}, {0x1C90, 0x1CBA}, {0x1CBD, 0x1CC7}, - {0x1CD0, 0x1CFA}, {0x1D00, 0x1DF9}, {0x1DFB, 0x1F15}, - {0x1F18, 0x1F1D}, {0x1F20, 0x1F45}, {0x1F48, 0x1F4D}, - {0x1F50, 0x1F57}, {0x1F59, 0x1F59}, {0x1F5B, 0x1F5B}, - {0x1F5D, 0x1F5D}, {0x1F5F, 0x1F7D}, {0x1F80, 0x1FB4}, - {0x1FB6, 0x1FC4}, {0x1FC6, 0x1FD3}, {0x1FD6, 0x1FDB}, - {0x1FDD, 0x1FEF}, {0x1FF2, 0x1FF4}, {0x1FF6, 0x1FFE}, - {0x2000, 0x200F}, {0x2011, 0x2012}, {0x2017, 0x2017}, - {0x201A, 0x201B}, {0x201E, 0x201F}, {0x2023, 0x2023}, - {0x2028, 0x202F}, {0x2031, 0x2031}, {0x2034, 0x2034}, - {0x2036, 0x203A}, {0x203C, 0x203D}, {0x203F, 0x2064}, - {0x2066, 0x2071}, {0x2075, 0x207E}, {0x2080, 0x2080}, - {0x2085, 0x208E}, {0x2090, 0x209C}, {0x20A0, 0x20A8}, - {0x20AA, 0x20AB}, {0x20AD, 0x20BF}, {0x20D0, 0x20F0}, - {0x2100, 0x2102}, {0x2104, 0x2104}, {0x2106, 0x2108}, - {0x210A, 0x2112}, {0x2114, 0x2115}, {0x2117, 0x2120}, - {0x2123, 0x2125}, {0x2127, 0x212A}, {0x212C, 0x2152}, - {0x2155, 0x215A}, {0x215F, 0x215F}, {0x216C, 0x216F}, - {0x217A, 0x2188}, {0x218A, 0x218B}, {0x219A, 0x21B7}, - {0x21BA, 0x21D1}, {0x21D3, 0x21D3}, {0x21D5, 0x21E6}, - {0x21E8, 0x21FF}, {0x2201, 0x2201}, {0x2204, 0x2206}, - {0x2209, 0x220A}, {0x220C, 0x220E}, {0x2210, 0x2210}, - {0x2212, 0x2214}, {0x2216, 0x2219}, {0x221B, 0x221C}, - {0x2221, 0x2222}, {0x2224, 0x2224}, {0x2226, 0x2226}, - {0x222D, 0x222D}, {0x222F, 0x2233}, {0x2238, 0x223B}, - {0x223E, 0x2247}, {0x2249, 0x224B}, {0x224D, 0x2251}, - {0x2253, 0x225F}, {0x2262, 0x2263}, {0x2268, 0x2269}, - {0x226C, 0x226D}, {0x2270, 0x2281}, {0x2284, 0x2285}, - {0x2288, 0x2294}, {0x2296, 0x2298}, {0x229A, 0x22A4}, - {0x22A6, 0x22BE}, {0x22C0, 0x2311}, {0x2313, 0x2319}, - {0x231C, 0x2328}, {0x232B, 0x23E8}, {0x23ED, 0x23EF}, - {0x23F1, 0x23F2}, {0x23F4, 0x2426}, {0x2440, 0x244A}, - {0x24EA, 0x24EA}, {0x254C, 0x254F}, {0x2574, 0x257F}, - {0x2590, 0x2591}, {0x2596, 0x259F}, {0x25A2, 0x25A2}, - {0x25AA, 0x25B1}, {0x25B4, 0x25B5}, {0x25B8, 0x25BB}, - {0x25BE, 0x25BF}, {0x25C2, 0x25C5}, {0x25C9, 0x25CA}, - {0x25CC, 0x25CD}, {0x25D2, 0x25E1}, {0x25E6, 0x25EE}, - {0x25F0, 0x25FC}, {0x25FF, 0x2604}, {0x2607, 0x2608}, - {0x260A, 0x260D}, {0x2610, 0x2613}, {0x2616, 0x261B}, - {0x261D, 0x261D}, {0x261F, 0x263F}, {0x2641, 0x2641}, - {0x2643, 0x2647}, {0x2654, 0x265F}, {0x2662, 0x2662}, - {0x2666, 0x2666}, {0x266B, 0x266B}, {0x266E, 0x266E}, - {0x2670, 0x267E}, {0x2680, 0x2692}, {0x2694, 0x269D}, - {0x26A0, 0x26A0}, {0x26A2, 0x26A9}, {0x26AC, 0x26BC}, - {0x26C0, 0x26C3}, {0x26E2, 0x26E2}, {0x26E4, 0x26E7}, - {0x2700, 0x2704}, {0x2706, 0x2709}, {0x270C, 0x2727}, - {0x2729, 0x273C}, {0x273E, 0x274B}, {0x274D, 0x274D}, - {0x274F, 0x2752}, {0x2756, 0x2756}, {0x2758, 0x2775}, - {0x2780, 0x2794}, {0x2798, 0x27AF}, {0x27B1, 0x27BE}, - {0x27C0, 0x27E5}, {0x27EE, 0x2984}, {0x2987, 0x2B1A}, - {0x2B1D, 0x2B4F}, {0x2B51, 0x2B54}, {0x2B5A, 0x2B73}, - {0x2B76, 0x2B95}, {0x2B97, 0x2C2E}, {0x2C30, 0x2C5E}, - {0x2C60, 0x2CF3}, {0x2CF9, 0x2D25}, {0x2D27, 0x2D27}, - {0x2D2D, 0x2D2D}, {0x2D30, 0x2D67}, {0x2D6F, 0x2D70}, - {0x2D7F, 0x2D96}, {0x2DA0, 0x2DA6}, {0x2DA8, 0x2DAE}, - {0x2DB0, 0x2DB6}, {0x2DB8, 0x2DBE}, {0x2DC0, 0x2DC6}, - {0x2DC8, 0x2DCE}, {0x2DD0, 0x2DD6}, {0x2DD8, 0x2DDE}, - {0x2DE0, 0x2E52}, {0x303F, 0x303F}, {0x4DC0, 0x4DFF}, - {0xA4D0, 0xA62B}, {0xA640, 0xA6F7}, {0xA700, 0xA7BF}, - {0xA7C2, 0xA7CA}, {0xA7F5, 0xA82C}, {0xA830, 0xA839}, - {0xA840, 0xA877}, {0xA880, 0xA8C5}, {0xA8CE, 0xA8D9}, - {0xA8E0, 0xA953}, {0xA95F, 0xA95F}, {0xA980, 0xA9CD}, - {0xA9CF, 0xA9D9}, {0xA9DE, 0xA9FE}, {0xAA00, 0xAA36}, - {0xAA40, 0xAA4D}, {0xAA50, 0xAA59}, {0xAA5C, 0xAAC2}, - {0xAADB, 0xAAF6}, {0xAB01, 0xAB06}, {0xAB09, 0xAB0E}, - {0xAB11, 0xAB16}, {0xAB20, 0xAB26}, {0xAB28, 0xAB2E}, - {0xAB30, 0xAB6B}, {0xAB70, 0xABED}, {0xABF0, 0xABF9}, - {0xD7B0, 0xD7C6}, {0xD7CB, 0xD7FB}, {0xD800, 0xDFFF}, - {0xFB00, 0xFB06}, {0xFB13, 0xFB17}, {0xFB1D, 0xFB36}, - {0xFB38, 0xFB3C}, {0xFB3E, 0xFB3E}, {0xFB40, 0xFB41}, - {0xFB43, 0xFB44}, {0xFB46, 0xFBC1}, {0xFBD3, 0xFD3F}, - {0xFD50, 0xFD8F}, {0xFD92, 0xFDC7}, {0xFDF0, 0xFDFD}, - {0xFE20, 0xFE2F}, {0xFE70, 0xFE74}, {0xFE76, 0xFEFC}, - {0xFEFF, 0xFEFF}, {0xFFF9, 0xFFFC}, {0x10000, 0x1000B}, - {0x1000D, 0x10026}, {0x10028, 0x1003A}, {0x1003C, 0x1003D}, - {0x1003F, 0x1004D}, {0x10050, 0x1005D}, {0x10080, 0x100FA}, - {0x10100, 0x10102}, {0x10107, 0x10133}, {0x10137, 0x1018E}, - {0x10190, 0x1019C}, {0x101A0, 0x101A0}, {0x101D0, 0x101FD}, - {0x10280, 0x1029C}, {0x102A0, 0x102D0}, {0x102E0, 0x102FB}, - {0x10300, 0x10323}, {0x1032D, 0x1034A}, {0x10350, 0x1037A}, - {0x10380, 0x1039D}, {0x1039F, 0x103C3}, {0x103C8, 0x103D5}, - {0x10400, 0x1049D}, {0x104A0, 0x104A9}, {0x104B0, 0x104D3}, - {0x104D8, 0x104FB}, {0x10500, 0x10527}, {0x10530, 0x10563}, - {0x1056F, 0x1056F}, {0x10600, 0x10736}, {0x10740, 0x10755}, - {0x10760, 0x10767}, {0x10800, 0x10805}, {0x10808, 0x10808}, - {0x1080A, 0x10835}, {0x10837, 0x10838}, {0x1083C, 0x1083C}, - {0x1083F, 0x10855}, {0x10857, 0x1089E}, {0x108A7, 0x108AF}, - {0x108E0, 0x108F2}, {0x108F4, 0x108F5}, {0x108FB, 0x1091B}, - {0x1091F, 0x10939}, {0x1093F, 0x1093F}, {0x10980, 0x109B7}, - {0x109BC, 0x109CF}, {0x109D2, 0x10A03}, {0x10A05, 0x10A06}, - {0x10A0C, 0x10A13}, {0x10A15, 0x10A17}, {0x10A19, 0x10A35}, - {0x10A38, 0x10A3A}, {0x10A3F, 0x10A48}, {0x10A50, 0x10A58}, - {0x10A60, 0x10A9F}, {0x10AC0, 0x10AE6}, {0x10AEB, 0x10AF6}, - {0x10B00, 0x10B35}, {0x10B39, 0x10B55}, {0x10B58, 0x10B72}, - {0x10B78, 0x10B91}, {0x10B99, 0x10B9C}, {0x10BA9, 0x10BAF}, - {0x10C00, 0x10C48}, {0x10C80, 0x10CB2}, {0x10CC0, 0x10CF2}, - {0x10CFA, 0x10D27}, {0x10D30, 0x10D39}, {0x10E60, 0x10E7E}, - {0x10E80, 0x10EA9}, {0x10EAB, 0x10EAD}, {0x10EB0, 0x10EB1}, - {0x10F00, 0x10F27}, {0x10F30, 0x10F59}, {0x10FB0, 0x10FCB}, - {0x10FE0, 0x10FF6}, {0x11000, 0x1104D}, {0x11052, 0x1106F}, - {0x1107F, 0x110C1}, {0x110CD, 0x110CD}, {0x110D0, 0x110E8}, - {0x110F0, 0x110F9}, {0x11100, 0x11134}, {0x11136, 0x11147}, - {0x11150, 0x11176}, {0x11180, 0x111DF}, {0x111E1, 0x111F4}, - {0x11200, 0x11211}, {0x11213, 0x1123E}, {0x11280, 0x11286}, - {0x11288, 0x11288}, {0x1128A, 0x1128D}, {0x1128F, 0x1129D}, - {0x1129F, 0x112A9}, {0x112B0, 0x112EA}, {0x112F0, 0x112F9}, - {0x11300, 0x11303}, {0x11305, 0x1130C}, {0x1130F, 0x11310}, - {0x11313, 0x11328}, {0x1132A, 0x11330}, {0x11332, 0x11333}, - {0x11335, 0x11339}, {0x1133B, 0x11344}, {0x11347, 0x11348}, - {0x1134B, 0x1134D}, {0x11350, 0x11350}, {0x11357, 0x11357}, - {0x1135D, 0x11363}, {0x11366, 0x1136C}, {0x11370, 0x11374}, - {0x11400, 0x1145B}, {0x1145D, 0x11461}, {0x11480, 0x114C7}, - {0x114D0, 0x114D9}, {0x11580, 0x115B5}, {0x115B8, 0x115DD}, - {0x11600, 0x11644}, {0x11650, 0x11659}, {0x11660, 0x1166C}, - {0x11680, 0x116B8}, {0x116C0, 0x116C9}, {0x11700, 0x1171A}, - {0x1171D, 0x1172B}, {0x11730, 0x1173F}, {0x11800, 0x1183B}, - {0x118A0, 0x118F2}, {0x118FF, 0x11906}, {0x11909, 0x11909}, - {0x1190C, 0x11913}, {0x11915, 0x11916}, {0x11918, 0x11935}, - {0x11937, 0x11938}, {0x1193B, 0x11946}, {0x11950, 0x11959}, - {0x119A0, 0x119A7}, {0x119AA, 0x119D7}, {0x119DA, 0x119E4}, - {0x11A00, 0x11A47}, {0x11A50, 0x11AA2}, {0x11AC0, 0x11AF8}, - {0x11C00, 0x11C08}, {0x11C0A, 0x11C36}, {0x11C38, 0x11C45}, - {0x11C50, 0x11C6C}, {0x11C70, 0x11C8F}, {0x11C92, 0x11CA7}, - {0x11CA9, 0x11CB6}, {0x11D00, 0x11D06}, {0x11D08, 0x11D09}, - {0x11D0B, 0x11D36}, {0x11D3A, 0x11D3A}, {0x11D3C, 0x11D3D}, - {0x11D3F, 0x11D47}, {0x11D50, 0x11D59}, {0x11D60, 0x11D65}, - {0x11D67, 0x11D68}, {0x11D6A, 0x11D8E}, {0x11D90, 0x11D91}, - {0x11D93, 0x11D98}, {0x11DA0, 0x11DA9}, {0x11EE0, 0x11EF8}, - {0x11FB0, 0x11FB0}, {0x11FC0, 0x11FF1}, {0x11FFF, 0x12399}, - {0x12400, 0x1246E}, {0x12470, 0x12474}, {0x12480, 0x12543}, - {0x13000, 0x1342E}, {0x13430, 0x13438}, {0x14400, 0x14646}, - {0x16800, 0x16A38}, {0x16A40, 0x16A5E}, {0x16A60, 0x16A69}, - {0x16A6E, 0x16A6F}, {0x16AD0, 0x16AED}, {0x16AF0, 0x16AF5}, - {0x16B00, 0x16B45}, {0x16B50, 0x16B59}, {0x16B5B, 0x16B61}, - {0x16B63, 0x16B77}, {0x16B7D, 0x16B8F}, {0x16E40, 0x16E9A}, - {0x16F00, 0x16F4A}, {0x16F4F, 0x16F87}, {0x16F8F, 0x16F9F}, - {0x1BC00, 0x1BC6A}, {0x1BC70, 0x1BC7C}, {0x1BC80, 0x1BC88}, - {0x1BC90, 0x1BC99}, {0x1BC9C, 0x1BCA3}, {0x1D000, 0x1D0F5}, - {0x1D100, 0x1D126}, {0x1D129, 0x1D1E8}, {0x1D200, 0x1D245}, - {0x1D2E0, 0x1D2F3}, {0x1D300, 0x1D356}, {0x1D360, 0x1D378}, - {0x1D400, 0x1D454}, {0x1D456, 0x1D49C}, {0x1D49E, 0x1D49F}, - {0x1D4A2, 0x1D4A2}, {0x1D4A5, 0x1D4A6}, {0x1D4A9, 0x1D4AC}, - {0x1D4AE, 0x1D4B9}, {0x1D4BB, 0x1D4BB}, {0x1D4BD, 0x1D4C3}, - {0x1D4C5, 0x1D505}, {0x1D507, 0x1D50A}, {0x1D50D, 0x1D514}, - {0x1D516, 0x1D51C}, {0x1D51E, 0x1D539}, {0x1D53B, 0x1D53E}, - {0x1D540, 0x1D544}, {0x1D546, 0x1D546}, {0x1D54A, 0x1D550}, - {0x1D552, 0x1D6A5}, {0x1D6A8, 0x1D7CB}, {0x1D7CE, 0x1DA8B}, - {0x1DA9B, 0x1DA9F}, {0x1DAA1, 0x1DAAF}, {0x1E000, 0x1E006}, - {0x1E008, 0x1E018}, {0x1E01B, 0x1E021}, {0x1E023, 0x1E024}, - {0x1E026, 0x1E02A}, {0x1E100, 0x1E12C}, {0x1E130, 0x1E13D}, - {0x1E140, 0x1E149}, {0x1E14E, 0x1E14F}, {0x1E2C0, 0x1E2F9}, - {0x1E2FF, 0x1E2FF}, {0x1E800, 0x1E8C4}, {0x1E8C7, 0x1E8D6}, - {0x1E900, 0x1E94B}, {0x1E950, 0x1E959}, {0x1E95E, 0x1E95F}, - {0x1EC71, 0x1ECB4}, {0x1ED01, 0x1ED3D}, {0x1EE00, 0x1EE03}, - {0x1EE05, 0x1EE1F}, {0x1EE21, 0x1EE22}, {0x1EE24, 0x1EE24}, - {0x1EE27, 0x1EE27}, {0x1EE29, 0x1EE32}, {0x1EE34, 0x1EE37}, - {0x1EE39, 0x1EE39}, {0x1EE3B, 0x1EE3B}, {0x1EE42, 0x1EE42}, - {0x1EE47, 0x1EE47}, {0x1EE49, 0x1EE49}, {0x1EE4B, 0x1EE4B}, - {0x1EE4D, 0x1EE4F}, {0x1EE51, 0x1EE52}, {0x1EE54, 0x1EE54}, - {0x1EE57, 0x1EE57}, {0x1EE59, 0x1EE59}, {0x1EE5B, 0x1EE5B}, - {0x1EE5D, 0x1EE5D}, {0x1EE5F, 0x1EE5F}, {0x1EE61, 0x1EE62}, - {0x1EE64, 0x1EE64}, {0x1EE67, 0x1EE6A}, {0x1EE6C, 0x1EE72}, - {0x1EE74, 0x1EE77}, {0x1EE79, 0x1EE7C}, {0x1EE7E, 0x1EE7E}, - {0x1EE80, 0x1EE89}, {0x1EE8B, 0x1EE9B}, {0x1EEA1, 0x1EEA3}, - {0x1EEA5, 0x1EEA9}, {0x1EEAB, 0x1EEBB}, {0x1EEF0, 0x1EEF1}, - {0x1F000, 0x1F003}, {0x1F005, 0x1F02B}, {0x1F030, 0x1F093}, - {0x1F0A0, 0x1F0AE}, {0x1F0B1, 0x1F0BF}, {0x1F0C1, 0x1F0CE}, - {0x1F0D1, 0x1F0F5}, {0x1F10B, 0x1F10F}, {0x1F12E, 0x1F12F}, - {0x1F16A, 0x1F16F}, {0x1F1AD, 0x1F1AD}, {0x1F1E6, 0x1F1FF}, - {0x1F321, 0x1F32C}, {0x1F336, 0x1F336}, {0x1F37D, 0x1F37D}, - {0x1F394, 0x1F39F}, {0x1F3CB, 0x1F3CE}, {0x1F3D4, 0x1F3DF}, - {0x1F3F1, 0x1F3F3}, {0x1F3F5, 0x1F3F7}, {0x1F43F, 0x1F43F}, - {0x1F441, 0x1F441}, {0x1F4FD, 0x1F4FE}, {0x1F53E, 0x1F54A}, - {0x1F54F, 0x1F54F}, {0x1F568, 0x1F579}, {0x1F57B, 0x1F594}, - {0x1F597, 0x1F5A3}, {0x1F5A5, 0x1F5FA}, {0x1F650, 0x1F67F}, - {0x1F6C6, 0x1F6CB}, {0x1F6CD, 0x1F6CF}, {0x1F6D3, 0x1F6D4}, - {0x1F6E0, 0x1F6EA}, {0x1F6F0, 0x1F6F3}, {0x1F700, 0x1F773}, - {0x1F780, 0x1F7D8}, {0x1F800, 0x1F80B}, {0x1F810, 0x1F847}, - {0x1F850, 0x1F859}, {0x1F860, 0x1F887}, {0x1F890, 0x1F8AD}, - {0x1F8B0, 0x1F8B1}, {0x1F900, 0x1F90B}, {0x1F93B, 0x1F93B}, - {0x1F946, 0x1F946}, {0x1FA00, 0x1FA53}, {0x1FA60, 0x1FA6D}, - {0x1FB00, 0x1FB92}, {0x1FB94, 0x1FBCA}, {0x1FBF0, 0x1FBF9}, - {0xE0001, 0xE0001}, {0xE0020, 0xE007F}, -} - -var emoji = table{ - {0x203C, 0x203C}, {0x2049, 0x2049}, {0x2122, 0x2122}, - {0x2139, 0x2139}, {0x2194, 0x2199}, {0x21A9, 0x21AA}, - {0x231A, 0x231B}, {0x2328, 0x2328}, {0x2388, 0x2388}, - {0x23CF, 0x23CF}, {0x23E9, 0x23F3}, {0x23F8, 0x23FA}, - {0x24C2, 0x24C2}, {0x25AA, 0x25AB}, {0x25B6, 0x25B6}, - {0x25C0, 0x25C0}, {0x25FB, 0x25FE}, {0x2600, 0x2605}, - {0x2607, 0x2612}, {0x2614, 0x2685}, {0x2690, 0x2705}, - {0x2708, 0x2712}, {0x2714, 0x2714}, {0x2716, 0x2716}, - {0x271D, 0x271D}, {0x2721, 0x2721}, {0x2728, 0x2728}, - {0x2733, 0x2734}, {0x2744, 0x2744}, {0x2747, 0x2747}, - {0x274C, 0x274C}, {0x274E, 0x274E}, {0x2753, 0x2755}, - {0x2757, 0x2757}, {0x2763, 0x2767}, {0x2795, 0x2797}, - {0x27A1, 0x27A1}, {0x27B0, 0x27B0}, {0x27BF, 0x27BF}, - {0x2934, 0x2935}, {0x2B05, 0x2B07}, {0x2B1B, 0x2B1C}, - {0x2B50, 0x2B50}, {0x2B55, 0x2B55}, {0x3030, 0x3030}, - {0x303D, 0x303D}, {0x3297, 0x3297}, {0x3299, 0x3299}, - {0x1F000, 0x1F0FF}, {0x1F10D, 0x1F10F}, {0x1F12F, 0x1F12F}, - {0x1F16C, 0x1F171}, {0x1F17E, 0x1F17F}, {0x1F18E, 0x1F18E}, - {0x1F191, 0x1F19A}, {0x1F1AD, 0x1F1E5}, {0x1F201, 0x1F20F}, - {0x1F21A, 0x1F21A}, {0x1F22F, 0x1F22F}, {0x1F232, 0x1F23A}, - {0x1F23C, 0x1F23F}, {0x1F249, 0x1F3FA}, {0x1F400, 0x1F53D}, - {0x1F546, 0x1F64F}, {0x1F680, 0x1F6FF}, {0x1F774, 0x1F77F}, - {0x1F7D5, 0x1F7FF}, {0x1F80C, 0x1F80F}, {0x1F848, 0x1F84F}, - {0x1F85A, 0x1F85F}, {0x1F888, 0x1F88F}, {0x1F8AE, 0x1F8FF}, - {0x1F90C, 0x1F93A}, {0x1F93C, 0x1F945}, {0x1F947, 0x1FAFF}, - {0x1FC00, 0x1FFFD}, -} diff --git a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go b/vendor/github.com/mattn/go-runewidth/runewidth_windows.go deleted file mode 100644 index d6a61777..00000000 --- a/vendor/github.com/mattn/go-runewidth/runewidth_windows.go +++ /dev/null @@ -1,28 +0,0 @@ -// +build windows -// +build !appengine - -package runewidth - -import ( - "syscall" -) - -var ( - kernel32 = syscall.NewLazyDLL("kernel32") - procGetConsoleOutputCP = kernel32.NewProc("GetConsoleOutputCP") -) - -// IsEastAsian return true if the current locale is CJK -func IsEastAsian() bool { - r1, _, _ := procGetConsoleOutputCP.Call() - if r1 == 0 { - return false - } - - switch int(r1) { - case 932, 51932, 936, 949, 950: - return true - } - - return false -} diff --git a/vendor/github.com/olekukonko/tablewriter/.gitignore b/vendor/github.com/olekukonko/tablewriter/.gitignore deleted file mode 100644 index b66cec63..00000000 --- a/vendor/github.com/olekukonko/tablewriter/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -# Created by .ignore support plugin (hsz.mobi) -### Go template -# Binaries for programs and plugins -*.exe -*.exe~ -*.dll -*.so -*.dylib - -# Test binary, build with `go test -c` -*.test - -# Output of the go coverage tool, specifically when used with LiteIDE -*.out - diff --git a/vendor/github.com/olekukonko/tablewriter/.travis.yml b/vendor/github.com/olekukonko/tablewriter/.travis.yml deleted file mode 100644 index 9c64270e..00000000 --- a/vendor/github.com/olekukonko/tablewriter/.travis.yml +++ /dev/null @@ -1,14 +0,0 @@ -language: go - -go: - - 1.1 - - 1.2 - - 1.3 - - 1.4 - - 1.5 - - 1.6 - - 1.7 - - 1.8 - - 1.9 - - "1.10" - - tip diff --git a/vendor/github.com/olekukonko/tablewriter/LICENSE.md b/vendor/github.com/olekukonko/tablewriter/LICENSE.md deleted file mode 100644 index a0769b5c..00000000 --- a/vendor/github.com/olekukonko/tablewriter/LICENSE.md +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2014 by Oleku Konko - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. diff --git a/vendor/github.com/olekukonko/tablewriter/README.md b/vendor/github.com/olekukonko/tablewriter/README.md deleted file mode 100644 index cb9b2ef4..00000000 --- a/vendor/github.com/olekukonko/tablewriter/README.md +++ /dev/null @@ -1,396 +0,0 @@ -ASCII Table Writer -========= - -[![Build Status](https://travis-ci.org/olekukonko/tablewriter.png?branch=master)](https://travis-ci.org/olekukonko/tablewriter) -[![Total views](https://img.shields.io/sourcegraph/rrc/github.com/olekukonko/tablewriter.svg)](https://sourcegraph.com/github.com/olekukonko/tablewriter) -[![Godoc](https://godoc.org/github.com/olekukonko/tablewriter?status.svg)](https://godoc.org/github.com/olekukonko/tablewriter) - -Generate ASCII table on the fly ... Installation is simple as - - go get github.com/olekukonko/tablewriter - - -#### Features -- Automatic Padding -- Support Multiple Lines -- Supports Alignment -- Support Custom Separators -- Automatic Alignment of numbers & percentage -- Write directly to http , file etc via `io.Writer` -- Read directly from CSV file -- Optional row line via `SetRowLine` -- Normalise table header -- Make CSV Headers optional -- Enable or disable table border -- Set custom footer support -- Optional identical cells merging -- Set custom caption -- Optional reflowing of paragrpahs in multi-line cells. - -#### Example 1 - Basic -```go -data := [][]string{ - []string{"A", "The Good", "500"}, - []string{"B", "The Very very Bad Man", "288"}, - []string{"C", "The Ugly", "120"}, - []string{"D", "The Gopher", "800"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Name", "Sign", "Rating"}) - -for _, v := range data { - table.Append(v) -} -table.Render() // Send output -``` - -##### Output 1 -``` -+------+-----------------------+--------+ -| NAME | SIGN | RATING | -+------+-----------------------+--------+ -| A | The Good | 500 | -| B | The Very very Bad Man | 288 | -| C | The Ugly | 120 | -| D | The Gopher | 800 | -+------+-----------------------+--------+ -``` - -#### Example 2 - Without Border / Footer / Bulk Append -```go -data := [][]string{ - []string{"1/1/2014", "Domain name", "2233", "$10.98"}, - []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, - []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, - []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) -table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer -table.SetBorder(false) // Set Border to false -table.AppendBulk(data) // Add Bulk Data -table.Render() -``` - -##### Output 2 -``` - - DATE | DESCRIPTION | CV2 | AMOUNT ------------+--------------------------+-------+---------- - 1/1/2014 | Domain name | 2233 | $10.98 - 1/1/2014 | January Hosting | 2233 | $54.95 - 1/4/2014 | February Hosting | 2233 | $51.00 - 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 ------------+--------------------------+-------+---------- - TOTAL | $146 93 - --------+---------- - -``` - - -#### Example 3 - CSV -```go -table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test_info.csv", true) -table.SetAlignment(tablewriter.ALIGN_LEFT) // Set Alignment -table.Render() -``` - -##### Output 3 -``` -+----------+--------------+------+-----+---------+----------------+ -| FIELD | TYPE | NULL | KEY | DEFAULT | EXTRA | -+----------+--------------+------+-----+---------+----------------+ -| user_id | smallint(5) | NO | PRI | NULL | auto_increment | -| username | varchar(10) | NO | | NULL | | -| password | varchar(100) | NO | | NULL | | -+----------+--------------+------+-----+---------+----------------+ -``` - -#### Example 4 - Custom Separator -```go -table, _ := tablewriter.NewCSV(os.Stdout, "testdata/test.csv", true) -table.SetRowLine(true) // Enable row line - -// Change table lines -table.SetCenterSeparator("*") -table.SetColumnSeparator("╪") -table.SetRowSeparator("-") - -table.SetAlignment(tablewriter.ALIGN_LEFT) -table.Render() -``` - -##### Output 4 -``` -*------------*-----------*---------* -╪ FIRST NAME ╪ LAST NAME ╪ SSN ╪ -*------------*-----------*---------* -╪ John ╪ Barry ╪ 123456 ╪ -*------------*-----------*---------* -╪ Kathy ╪ Smith ╪ 687987 ╪ -*------------*-----------*---------* -╪ Bob ╪ McCornick ╪ 3979870 ╪ -*------------*-----------*---------* -``` - -#### Example 5 - Markdown Format -```go -data := [][]string{ - []string{"1/1/2014", "Domain name", "2233", "$10.98"}, - []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, - []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, - []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) -table.SetBorders(tablewriter.Border{Left: true, Top: false, Right: true, Bottom: false}) -table.SetCenterSeparator("|") -table.AppendBulk(data) // Add Bulk Data -table.Render() -``` - -##### Output 5 -``` -| DATE | DESCRIPTION | CV2 | AMOUNT | -|----------|--------------------------|------|--------| -| 1/1/2014 | Domain name | 2233 | $10.98 | -| 1/1/2014 | January Hosting | 2233 | $54.95 | -| 1/4/2014 | February Hosting | 2233 | $51.00 | -| 1/4/2014 | February Extra Bandwidth | 2233 | $30.00 | -``` - -#### Example 6 - Identical cells merging -```go -data := [][]string{ - []string{"1/1/2014", "Domain name", "1234", "$10.98"}, - []string{"1/1/2014", "January Hosting", "2345", "$54.95"}, - []string{"1/4/2014", "February Hosting", "3456", "$51.00"}, - []string{"1/4/2014", "February Extra Bandwidth", "4567", "$30.00"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) -table.SetFooter([]string{"", "", "Total", "$146.93"}) -table.SetAutoMergeCells(true) -table.SetRowLine(true) -table.AppendBulk(data) -table.Render() -``` - -##### Output 6 -``` -+----------+--------------------------+-------+---------+ -| DATE | DESCRIPTION | CV2 | AMOUNT | -+----------+--------------------------+-------+---------+ -| 1/1/2014 | Domain name | 1234 | $10.98 | -+ +--------------------------+-------+---------+ -| | January Hosting | 2345 | $54.95 | -+----------+--------------------------+-------+---------+ -| 1/4/2014 | February Hosting | 3456 | $51.00 | -+ +--------------------------+-------+---------+ -| | February Extra Bandwidth | 4567 | $30.00 | -+----------+--------------------------+-------+---------+ -| TOTAL | $146 93 | -+----------+--------------------------+-------+---------+ -``` - - -#### Table with color -```go -data := [][]string{ - []string{"1/1/2014", "Domain name", "2233", "$10.98"}, - []string{"1/1/2014", "January Hosting", "2233", "$54.95"}, - []string{"1/4/2014", "February Hosting", "2233", "$51.00"}, - []string{"1/4/2014", "February Extra Bandwidth", "2233", "$30.00"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Date", "Description", "CV2", "Amount"}) -table.SetFooter([]string{"", "", "Total", "$146.93"}) // Add Footer -table.SetBorder(false) // Set Border to false - -table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, - tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, - tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) - -table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) - -table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, - tablewriter.Colors{tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiRedColor}) - -table.AppendBulk(data) -table.Render() -``` - -#### Table with color Output -![Table with Color](https://cloud.githubusercontent.com/assets/6460392/21101956/bbc7b356-c0a1-11e6-9f36-dba694746efc.png) - -#### Example - 7 Table Cells with Color - -Individual Cell Colors from `func Rich` take precedence over Column Colors - -```go -data := [][]string{ - []string{"Test1Merge", "HelloCol2 - 1", "HelloCol3 - 1", "HelloCol4 - 1"}, - []string{"Test1Merge", "HelloCol2 - 2", "HelloCol3 - 2", "HelloCol4 - 2"}, - []string{"Test1Merge", "HelloCol2 - 3", "HelloCol3 - 3", "HelloCol4 - 3"}, - []string{"Test2Merge", "HelloCol2 - 4", "HelloCol3 - 4", "HelloCol4 - 4"}, - []string{"Test2Merge", "HelloCol2 - 5", "HelloCol3 - 5", "HelloCol4 - 5"}, - []string{"Test2Merge", "HelloCol2 - 6", "HelloCol3 - 6", "HelloCol4 - 6"}, - []string{"Test2Merge", "HelloCol2 - 7", "HelloCol3 - 7", "HelloCol4 - 7"}, - []string{"Test3Merge", "HelloCol2 - 8", "HelloCol3 - 8", "HelloCol4 - 8"}, - []string{"Test3Merge", "HelloCol2 - 9", "HelloCol3 - 9", "HelloCol4 - 9"}, - []string{"Test3Merge", "HelloCol2 - 10", "HelloCol3 -10", "HelloCol4 - 10"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Col1", "Col2", "Col3", "Col4"}) -table.SetFooter([]string{"", "", "Footer3", "Footer4"}) -table.SetBorder(false) - -table.SetHeaderColor(tablewriter.Colors{tablewriter.Bold, tablewriter.BgGreenColor}, - tablewriter.Colors{tablewriter.FgHiRedColor, tablewriter.Bold, tablewriter.BgBlackColor}, - tablewriter.Colors{tablewriter.BgRedColor, tablewriter.FgWhiteColor}, - tablewriter.Colors{tablewriter.BgCyanColor, tablewriter.FgWhiteColor}) - -table.SetColumnColor(tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiRedColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgHiBlackColor}, - tablewriter.Colors{tablewriter.Bold, tablewriter.FgBlackColor}) - -table.SetFooterColor(tablewriter.Colors{}, tablewriter.Colors{}, - tablewriter.Colors{tablewriter.Bold}, - tablewriter.Colors{tablewriter.FgHiRedColor}) - -colorData1 := []string{"TestCOLOR1Merge", "HelloCol2 - COLOR1", "HelloCol3 - COLOR1", "HelloCol4 - COLOR1"} -colorData2 := []string{"TestCOLOR2Merge", "HelloCol2 - COLOR2", "HelloCol3 - COLOR2", "HelloCol4 - COLOR2"} - -for i, row := range data { - if i == 4 { - table.Rich(colorData1, []tablewriter.Colors{tablewriter.Colors{}, tablewriter.Colors{tablewriter.Normal, tablewriter.FgCyanColor}, tablewriter.Colors{tablewriter.Bold, tablewriter.FgWhiteColor}, tablewriter.Colors{}}) - table.Rich(colorData2, []tablewriter.Colors{tablewriter.Colors{tablewriter.Normal, tablewriter.FgMagentaColor}, tablewriter.Colors{}, tablewriter.Colors{tablewriter.Bold, tablewriter.BgRedColor}, tablewriter.Colors{tablewriter.FgHiGreenColor, tablewriter.Italic, tablewriter.BgHiCyanColor}}) - } - table.Append(row) -} - -table.SetAutoMergeCells(true) -table.Render() - -``` - -##### Table cells with color Output -![Table cells with Color](https://user-images.githubusercontent.com/9064687/63969376-bcd88d80-ca6f-11e9-9466-c3d954700b25.png) - -#### Example 8 - Set table caption -```go -data := [][]string{ - []string{"A", "The Good", "500"}, - []string{"B", "The Very very Bad Man", "288"}, - []string{"C", "The Ugly", "120"}, - []string{"D", "The Gopher", "800"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Name", "Sign", "Rating"}) -table.SetCaption(true, "Movie ratings.") - -for _, v := range data { - table.Append(v) -} -table.Render() // Send output -``` - -Note: Caption text will wrap with total width of rendered table. - -##### Output 7 -``` -+------+-----------------------+--------+ -| NAME | SIGN | RATING | -+------+-----------------------+--------+ -| A | The Good | 500 | -| B | The Very very Bad Man | 288 | -| C | The Ugly | 120 | -| D | The Gopher | 800 | -+------+-----------------------+--------+ -Movie ratings. -``` - -#### Example 8 - Set NoWhiteSpace and TablePadding option -```go -data := [][]string{ - {"node1.example.com", "Ready", "compute", "1.11"}, - {"node2.example.com", "Ready", "compute", "1.11"}, - {"node3.example.com", "Ready", "compute", "1.11"}, - {"node4.example.com", "NotReady", "compute", "1.11"}, -} - -table := tablewriter.NewWriter(os.Stdout) -table.SetHeader([]string{"Name", "Status", "Role", "Version"}) -table.SetAutoWrapText(false) -table.SetAutoFormatHeaders(true) -table.SetHeaderAlignment(ALIGN_LEFT) -table.SetAlignment(ALIGN_LEFT) -table.SetCenterSeparator("") -table.SetColumnSeparator("") -table.SetRowSeparator("") -table.SetHeaderLine(false) -table.SetBorder(false) -table.SetTablePadding("\t") // pad with tabs -table.SetNoWhiteSpace(true) -table.AppendBulk(data) // Add Bulk Data -table.Render() -``` - -##### Output 8 -``` -NAME STATUS ROLE VERSION -node1.example.com Ready compute 1.11 -node2.example.com Ready compute 1.11 -node3.example.com Ready compute 1.11 -node4.example.com NotReady compute 1.11 -``` - -#### Render table into a string - -Instead of rendering the table to `io.Stdout` you can also render it into a string. Go 1.10 introduced the `strings.Builder` type which implements the `io.Writer` interface and can therefore be used for this task. Example: - -```go -package main - -import ( - "strings" - "fmt" - - "github.com/olekukonko/tablewriter" -) - -func main() { - tableString := &strings.Builder{} - table := tablewriter.NewWriter(tableString) - - /* - * Code to fill the table - */ - - table.Render() - - fmt.Println(tableString.String()) -} -``` - -#### TODO -- ~~Import Directly from CSV~~ - `done` -- ~~Support for `SetFooter`~~ - `done` -- ~~Support for `SetBorder`~~ - `done` -- ~~Support table with uneven rows~~ - `done` -- ~~Support custom alignment~~ -- General Improvement & Optimisation -- `NewHTML` Parse table from HTML diff --git a/vendor/github.com/olekukonko/tablewriter/csv.go b/vendor/github.com/olekukonko/tablewriter/csv.go deleted file mode 100644 index 98878303..00000000 --- a/vendor/github.com/olekukonko/tablewriter/csv.go +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright 2014 Oleku Konko All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -// This module is a Table Writer API for the Go Programming Language. -// The protocols were written in pure Go and works on windows and unix systems - -package tablewriter - -import ( - "encoding/csv" - "io" - "os" -) - -// Start A new table by importing from a CSV file -// Takes io.Writer and csv File name -func NewCSV(writer io.Writer, fileName string, hasHeader bool) (*Table, error) { - file, err := os.Open(fileName) - if err != nil { - return &Table{}, err - } - defer file.Close() - csvReader := csv.NewReader(file) - t, err := NewCSVReader(writer, csvReader, hasHeader) - return t, err -} - -// Start a New Table Writer with csv.Reader -// This enables customisation such as reader.Comma = ';' -// See http://golang.org/src/pkg/encoding/csv/reader.go?s=3213:3671#L94 -func NewCSVReader(writer io.Writer, csvReader *csv.Reader, hasHeader bool) (*Table, error) { - t := NewWriter(writer) - if hasHeader { - // Read the first row - headers, err := csvReader.Read() - if err != nil { - return &Table{}, err - } - t.SetHeader(headers) - } - for { - record, err := csvReader.Read() - if err == io.EOF { - break - } else if err != nil { - return &Table{}, err - } - t.Append(record) - } - return t, nil -} diff --git a/vendor/github.com/olekukonko/tablewriter/go.mod b/vendor/github.com/olekukonko/tablewriter/go.mod deleted file mode 100644 index 0430d99b..00000000 --- a/vendor/github.com/olekukonko/tablewriter/go.mod +++ /dev/null @@ -1,5 +0,0 @@ -module github.com/olekukonko/tablewriter - -go 1.12 - -require github.com/mattn/go-runewidth v0.0.7 diff --git a/vendor/github.com/olekukonko/tablewriter/go.sum b/vendor/github.com/olekukonko/tablewriter/go.sum deleted file mode 100644 index 1e7b9aab..00000000 --- a/vendor/github.com/olekukonko/tablewriter/go.sum +++ /dev/null @@ -1,2 +0,0 @@ -github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= -github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= diff --git a/vendor/github.com/olekukonko/tablewriter/table.go b/vendor/github.com/olekukonko/tablewriter/table.go deleted file mode 100644 index cf63eadf..00000000 --- a/vendor/github.com/olekukonko/tablewriter/table.go +++ /dev/null @@ -1,941 +0,0 @@ -// Copyright 2014 Oleku Konko All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -// This module is a Table Writer API for the Go Programming Language. -// The protocols were written in pure Go and works on windows and unix systems - -// Create & Generate text based table -package tablewriter - -import ( - "bytes" - "fmt" - "io" - "regexp" - "strings" -) - -const ( - MAX_ROW_WIDTH = 30 -) - -const ( - CENTER = "+" - ROW = "-" - COLUMN = "|" - SPACE = " " - NEWLINE = "\n" -) - -const ( - ALIGN_DEFAULT = iota - ALIGN_CENTER - ALIGN_RIGHT - ALIGN_LEFT -) - -var ( - decimal = regexp.MustCompile(`^-?(?:\d{1,3}(?:,\d{3})*|\d+)(?:\.\d+)?$`) - percent = regexp.MustCompile(`^-?\d+\.?\d*$%$`) -) - -type Border struct { - Left bool - Right bool - Top bool - Bottom bool -} - -type Table struct { - out io.Writer - rows [][]string - lines [][][]string - cs map[int]int - rs map[int]int - headers [][]string - footers [][]string - caption bool - captionText string - autoFmt bool - autoWrap bool - reflowText bool - mW int - pCenter string - pRow string - pColumn string - tColumn int - tRow int - hAlign int - fAlign int - align int - newLine string - rowLine bool - autoMergeCells bool - noWhiteSpace bool - tablePadding string - hdrLine bool - borders Border - colSize int - headerParams []string - columnsParams []string - footerParams []string - columnsAlign []int -} - -// Start New Table -// Take io.Writer Directly -func NewWriter(writer io.Writer) *Table { - t := &Table{ - out: writer, - rows: [][]string{}, - lines: [][][]string{}, - cs: make(map[int]int), - rs: make(map[int]int), - headers: [][]string{}, - footers: [][]string{}, - caption: false, - captionText: "Table caption.", - autoFmt: true, - autoWrap: true, - reflowText: true, - mW: MAX_ROW_WIDTH, - pCenter: CENTER, - pRow: ROW, - pColumn: COLUMN, - tColumn: -1, - tRow: -1, - hAlign: ALIGN_DEFAULT, - fAlign: ALIGN_DEFAULT, - align: ALIGN_DEFAULT, - newLine: NEWLINE, - rowLine: false, - hdrLine: true, - borders: Border{Left: true, Right: true, Bottom: true, Top: true}, - colSize: -1, - headerParams: []string{}, - columnsParams: []string{}, - footerParams: []string{}, - columnsAlign: []int{}} - return t -} - -// Render table output -func (t *Table) Render() { - if t.borders.Top { - t.printLine(true) - } - t.printHeading() - if t.autoMergeCells { - t.printRowsMergeCells() - } else { - t.printRows() - } - if !t.rowLine && t.borders.Bottom { - t.printLine(true) - } - t.printFooter() - - if t.caption { - t.printCaption() - } -} - -const ( - headerRowIdx = -1 - footerRowIdx = -2 -) - -// Set table header -func (t *Table) SetHeader(keys []string) { - t.colSize = len(keys) - for i, v := range keys { - lines := t.parseDimension(v, i, headerRowIdx) - t.headers = append(t.headers, lines) - } -} - -// Set table Footer -func (t *Table) SetFooter(keys []string) { - //t.colSize = len(keys) - for i, v := range keys { - lines := t.parseDimension(v, i, footerRowIdx) - t.footers = append(t.footers, lines) - } -} - -// Set table Caption -func (t *Table) SetCaption(caption bool, captionText ...string) { - t.caption = caption - if len(captionText) == 1 { - t.captionText = captionText[0] - } -} - -// Turn header autoformatting on/off. Default is on (true). -func (t *Table) SetAutoFormatHeaders(auto bool) { - t.autoFmt = auto -} - -// Turn automatic multiline text adjustment on/off. Default is on (true). -func (t *Table) SetAutoWrapText(auto bool) { - t.autoWrap = auto -} - -// Turn automatic reflowing of multiline text when rewrapping. Default is on (true). -func (t *Table) SetReflowDuringAutoWrap(auto bool) { - t.reflowText = auto -} - -// Set the Default column width -func (t *Table) SetColWidth(width int) { - t.mW = width -} - -// Set the minimal width for a column -func (t *Table) SetColMinWidth(column int, width int) { - t.cs[column] = width -} - -// Set the Column Separator -func (t *Table) SetColumnSeparator(sep string) { - t.pColumn = sep -} - -// Set the Row Separator -func (t *Table) SetRowSeparator(sep string) { - t.pRow = sep -} - -// Set the center Separator -func (t *Table) SetCenterSeparator(sep string) { - t.pCenter = sep -} - -// Set Header Alignment -func (t *Table) SetHeaderAlignment(hAlign int) { - t.hAlign = hAlign -} - -// Set Footer Alignment -func (t *Table) SetFooterAlignment(fAlign int) { - t.fAlign = fAlign -} - -// Set Table Alignment -func (t *Table) SetAlignment(align int) { - t.align = align -} - -// Set No White Space -func (t *Table) SetNoWhiteSpace(allow bool) { - t.noWhiteSpace = allow -} - -// Set Table Padding -func (t *Table) SetTablePadding(padding string) { - t.tablePadding = padding -} - -func (t *Table) SetColumnAlignment(keys []int) { - for _, v := range keys { - switch v { - case ALIGN_CENTER: - break - case ALIGN_LEFT: - break - case ALIGN_RIGHT: - break - default: - v = ALIGN_DEFAULT - } - t.columnsAlign = append(t.columnsAlign, v) - } -} - -// Set New Line -func (t *Table) SetNewLine(nl string) { - t.newLine = nl -} - -// Set Header Line -// This would enable / disable a line after the header -func (t *Table) SetHeaderLine(line bool) { - t.hdrLine = line -} - -// Set Row Line -// This would enable / disable a line on each row of the table -func (t *Table) SetRowLine(line bool) { - t.rowLine = line -} - -// Set Auto Merge Cells -// This would enable / disable the merge of cells with identical values -func (t *Table) SetAutoMergeCells(auto bool) { - t.autoMergeCells = auto -} - -// Set Table Border -// This would enable / disable line around the table -func (t *Table) SetBorder(border bool) { - t.SetBorders(Border{border, border, border, border}) -} - -func (t *Table) SetBorders(border Border) { - t.borders = border -} - -// Append row to table -func (t *Table) Append(row []string) { - rowSize := len(t.headers) - if rowSize > t.colSize { - t.colSize = rowSize - } - - n := len(t.lines) - line := [][]string{} - for i, v := range row { - - // Detect string width - // Detect String height - // Break strings into words - out := t.parseDimension(v, i, n) - - // Append broken words - line = append(line, out) - } - t.lines = append(t.lines, line) -} - -// Append row to table with color attributes -func (t *Table) Rich(row []string, colors []Colors) { - rowSize := len(t.headers) - if rowSize > t.colSize { - t.colSize = rowSize - } - - n := len(t.lines) - line := [][]string{} - for i, v := range row { - - // Detect string width - // Detect String height - // Break strings into words - out := t.parseDimension(v, i, n) - - if len(colors) > i { - color := colors[i] - out[0] = format(out[0], color) - } - - // Append broken words - line = append(line, out) - } - t.lines = append(t.lines, line) -} - -// Allow Support for Bulk Append -// Eliminates repeated for loops -func (t *Table) AppendBulk(rows [][]string) { - for _, row := range rows { - t.Append(row) - } -} - -// NumLines to get the number of lines -func (t *Table) NumLines() int { - return len(t.lines) -} - -// Clear rows -func (t *Table) ClearRows() { - t.lines = [][][]string{} -} - -// Clear footer -func (t *Table) ClearFooter() { - t.footers = [][]string{} -} - -// Center based on position and border. -func (t *Table) center(i int) string { - if i == -1 && !t.borders.Left { - return t.pRow - } - - if i == len(t.cs)-1 && !t.borders.Right { - return t.pRow - } - - return t.pCenter -} - -// Print line based on row width -func (t *Table) printLine(nl bool) { - fmt.Fprint(t.out, t.center(-1)) - for i := 0; i < len(t.cs); i++ { - v := t.cs[i] - fmt.Fprintf(t.out, "%s%s%s%s", - t.pRow, - strings.Repeat(string(t.pRow), v), - t.pRow, - t.center(i)) - } - if nl { - fmt.Fprint(t.out, t.newLine) - } -} - -// Print line based on row width with our without cell separator -func (t *Table) printLineOptionalCellSeparators(nl bool, displayCellSeparator []bool) { - fmt.Fprint(t.out, t.pCenter) - for i := 0; i < len(t.cs); i++ { - v := t.cs[i] - if i > len(displayCellSeparator) || displayCellSeparator[i] { - // Display the cell separator - fmt.Fprintf(t.out, "%s%s%s%s", - t.pRow, - strings.Repeat(string(t.pRow), v), - t.pRow, - t.pCenter) - } else { - // Don't display the cell separator for this cell - fmt.Fprintf(t.out, "%s%s", - strings.Repeat(" ", v+2), - t.pCenter) - } - } - if nl { - fmt.Fprint(t.out, t.newLine) - } -} - -// Return the PadRight function if align is left, PadLeft if align is right, -// and Pad by default -func pad(align int) func(string, string, int) string { - padFunc := Pad - switch align { - case ALIGN_LEFT: - padFunc = PadRight - case ALIGN_RIGHT: - padFunc = PadLeft - } - return padFunc -} - -// Print heading information -func (t *Table) printHeading() { - // Check if headers is available - if len(t.headers) < 1 { - return - } - - // Identify last column - end := len(t.cs) - 1 - - // Get pad function - padFunc := pad(t.hAlign) - - // Checking for ANSI escape sequences for header - is_esc_seq := false - if len(t.headerParams) > 0 { - is_esc_seq = true - } - - // Maximum height. - max := t.rs[headerRowIdx] - - // Print Heading - for x := 0; x < max; x++ { - // Check if border is set - // Replace with space if not set - if !t.noWhiteSpace { - fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) - } - - for y := 0; y <= end; y++ { - v := t.cs[y] - h := "" - - if y < len(t.headers) && x < len(t.headers[y]) { - h = t.headers[y][x] - } - if t.autoFmt { - h = Title(h) - } - pad := ConditionString((y == end && !t.borders.Left), SPACE, t.pColumn) - if t.noWhiteSpace { - pad = ConditionString((y == end && !t.borders.Left), SPACE, t.tablePadding) - } - if is_esc_seq { - if !t.noWhiteSpace { - fmt.Fprintf(t.out, " %s %s", - format(padFunc(h, SPACE, v), - t.headerParams[y]), pad) - } else { - fmt.Fprintf(t.out, "%s %s", - format(padFunc(h, SPACE, v), - t.headerParams[y]), pad) - } - } else { - if !t.noWhiteSpace { - fmt.Fprintf(t.out, " %s %s", - padFunc(h, SPACE, v), - pad) - } else { - // the spaces between breaks the kube formatting - fmt.Fprintf(t.out, "%s%s", - padFunc(h, SPACE, v), - pad) - } - } - } - // Next line - fmt.Fprint(t.out, t.newLine) - } - if t.hdrLine { - t.printLine(true) - } -} - -// Print heading information -func (t *Table) printFooter() { - // Check if headers is available - if len(t.footers) < 1 { - return - } - - // Only print line if border is not set - if !t.borders.Bottom { - t.printLine(true) - } - - // Identify last column - end := len(t.cs) - 1 - - // Get pad function - padFunc := pad(t.fAlign) - - // Checking for ANSI escape sequences for header - is_esc_seq := false - if len(t.footerParams) > 0 { - is_esc_seq = true - } - - // Maximum height. - max := t.rs[footerRowIdx] - - // Print Footer - erasePad := make([]bool, len(t.footers)) - for x := 0; x < max; x++ { - // Check if border is set - // Replace with space if not set - fmt.Fprint(t.out, ConditionString(t.borders.Bottom, t.pColumn, SPACE)) - - for y := 0; y <= end; y++ { - v := t.cs[y] - f := "" - if y < len(t.footers) && x < len(t.footers[y]) { - f = t.footers[y][x] - } - if t.autoFmt { - f = Title(f) - } - pad := ConditionString((y == end && !t.borders.Top), SPACE, t.pColumn) - - if erasePad[y] || (x == 0 && len(f) == 0) { - pad = SPACE - erasePad[y] = true - } - - if is_esc_seq { - fmt.Fprintf(t.out, " %s %s", - format(padFunc(f, SPACE, v), - t.footerParams[y]), pad) - } else { - fmt.Fprintf(t.out, " %s %s", - padFunc(f, SPACE, v), - pad) - } - - //fmt.Fprintf(t.out, " %s %s", - // padFunc(f, SPACE, v), - // pad) - } - // Next line - fmt.Fprint(t.out, t.newLine) - //t.printLine(true) - } - - hasPrinted := false - - for i := 0; i <= end; i++ { - v := t.cs[i] - pad := t.pRow - center := t.pCenter - length := len(t.footers[i][0]) - - if length > 0 { - hasPrinted = true - } - - // Set center to be space if length is 0 - if length == 0 && !t.borders.Right { - center = SPACE - } - - // Print first junction - if i == 0 { - if length > 0 && !t.borders.Left { - center = t.pRow - } - fmt.Fprint(t.out, center) - } - - // Pad With space of length is 0 - if length == 0 { - pad = SPACE - } - // Ignore left space as it has printed before - if hasPrinted || t.borders.Left { - pad = t.pRow - center = t.pCenter - } - - // Change Center end position - if center != SPACE { - if i == end && !t.borders.Right { - center = t.pRow - } - } - - // Change Center start position - if center == SPACE { - if i < end && len(t.footers[i+1][0]) != 0 { - if !t.borders.Left { - center = t.pRow - } else { - center = t.pCenter - } - } - } - - // Print the footer - fmt.Fprintf(t.out, "%s%s%s%s", - pad, - strings.Repeat(string(pad), v), - pad, - center) - - } - - fmt.Fprint(t.out, t.newLine) -} - -// Print caption text -func (t Table) printCaption() { - width := t.getTableWidth() - paragraph, _ := WrapString(t.captionText, width) - for linecount := 0; linecount < len(paragraph); linecount++ { - fmt.Fprintln(t.out, paragraph[linecount]) - } -} - -// Calculate the total number of characters in a row -func (t Table) getTableWidth() int { - var chars int - for _, v := range t.cs { - chars += v - } - - // Add chars, spaces, seperators to calculate the total width of the table. - // ncols := t.colSize - // spaces := ncols * 2 - // seps := ncols + 1 - - return (chars + (3 * t.colSize) + 2) -} - -func (t Table) printRows() { - for i, lines := range t.lines { - t.printRow(lines, i) - } -} - -func (t *Table) fillAlignment(num int) { - if len(t.columnsAlign) < num { - t.columnsAlign = make([]int, num) - for i := range t.columnsAlign { - t.columnsAlign[i] = t.align - } - } -} - -// Print Row Information -// Adjust column alignment based on type - -func (t *Table) printRow(columns [][]string, rowIdx int) { - // Get Maximum Height - max := t.rs[rowIdx] - total := len(columns) - - // TODO Fix uneven col size - // if total < t.colSize { - // for n := t.colSize - total; n < t.colSize ; n++ { - // columns = append(columns, []string{SPACE}) - // t.cs[n] = t.mW - // } - //} - - // Pad Each Height - pads := []int{} - - // Checking for ANSI escape sequences for columns - is_esc_seq := false - if len(t.columnsParams) > 0 { - is_esc_seq = true - } - t.fillAlignment(total) - - for i, line := range columns { - length := len(line) - pad := max - length - pads = append(pads, pad) - for n := 0; n < pad; n++ { - columns[i] = append(columns[i], " ") - } - } - //fmt.Println(max, "\n") - for x := 0; x < max; x++ { - for y := 0; y < total; y++ { - - // Check if border is set - if !t.noWhiteSpace { - fmt.Fprint(t.out, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) - fmt.Fprintf(t.out, SPACE) - } - - str := columns[y][x] - - // Embedding escape sequence with column value - if is_esc_seq { - str = format(str, t.columnsParams[y]) - } - - // This would print alignment - // Default alignment would use multiple configuration - switch t.columnsAlign[y] { - case ALIGN_CENTER: // - fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) - case ALIGN_RIGHT: - fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) - case ALIGN_LEFT: - fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) - default: - if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { - fmt.Fprintf(t.out, "%s", PadLeft(str, SPACE, t.cs[y])) - } else { - fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) - - // TODO Custom alignment per column - //if max == 1 || pads[y] > 0 { - // fmt.Fprintf(t.out, "%s", Pad(str, SPACE, t.cs[y])) - //} else { - // fmt.Fprintf(t.out, "%s", PadRight(str, SPACE, t.cs[y])) - //} - - } - } - if !t.noWhiteSpace { - fmt.Fprintf(t.out, SPACE) - } else { - fmt.Fprintf(t.out, t.tablePadding) - } - } - // Check if border is set - // Replace with space if not set - if !t.noWhiteSpace { - fmt.Fprint(t.out, ConditionString(t.borders.Left, t.pColumn, SPACE)) - } - fmt.Fprint(t.out, t.newLine) - } - - if t.rowLine { - t.printLine(true) - } -} - -// Print the rows of the table and merge the cells that are identical -func (t *Table) printRowsMergeCells() { - var previousLine []string - var displayCellBorder []bool - var tmpWriter bytes.Buffer - for i, lines := range t.lines { - // We store the display of the current line in a tmp writer, as we need to know which border needs to be print above - previousLine, displayCellBorder = t.printRowMergeCells(&tmpWriter, lines, i, previousLine) - if i > 0 { //We don't need to print borders above first line - if t.rowLine { - t.printLineOptionalCellSeparators(true, displayCellBorder) - } - } - tmpWriter.WriteTo(t.out) - } - //Print the end of the table - if t.rowLine { - t.printLine(true) - } -} - -// Print Row Information to a writer and merge identical cells. -// Adjust column alignment based on type - -func (t *Table) printRowMergeCells(writer io.Writer, columns [][]string, rowIdx int, previousLine []string) ([]string, []bool) { - // Get Maximum Height - max := t.rs[rowIdx] - total := len(columns) - - // Pad Each Height - pads := []int{} - - // Checking for ANSI escape sequences for columns - is_esc_seq := false - if len(t.columnsParams) > 0 { - is_esc_seq = true - } - for i, line := range columns { - length := len(line) - pad := max - length - pads = append(pads, pad) - for n := 0; n < pad; n++ { - columns[i] = append(columns[i], " ") - } - } - - var displayCellBorder []bool - t.fillAlignment(total) - for x := 0; x < max; x++ { - for y := 0; y < total; y++ { - - // Check if border is set - fmt.Fprint(writer, ConditionString((!t.borders.Left && y == 0), SPACE, t.pColumn)) - - fmt.Fprintf(writer, SPACE) - - str := columns[y][x] - - // Embedding escape sequence with column value - if is_esc_seq { - str = format(str, t.columnsParams[y]) - } - - if t.autoMergeCells { - //Store the full line to merge mutli-lines cells - fullLine := strings.TrimRight(strings.Join(columns[y], " "), " ") - if len(previousLine) > y && fullLine == previousLine[y] && fullLine != "" { - // If this cell is identical to the one above but not empty, we don't display the border and keep the cell empty. - displayCellBorder = append(displayCellBorder, false) - str = "" - } else { - // First line or different content, keep the content and print the cell border - displayCellBorder = append(displayCellBorder, true) - } - } - - // This would print alignment - // Default alignment would use multiple configuration - switch t.columnsAlign[y] { - case ALIGN_CENTER: // - fmt.Fprintf(writer, "%s", Pad(str, SPACE, t.cs[y])) - case ALIGN_RIGHT: - fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) - case ALIGN_LEFT: - fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) - default: - if decimal.MatchString(strings.TrimSpace(str)) || percent.MatchString(strings.TrimSpace(str)) { - fmt.Fprintf(writer, "%s", PadLeft(str, SPACE, t.cs[y])) - } else { - fmt.Fprintf(writer, "%s", PadRight(str, SPACE, t.cs[y])) - } - } - fmt.Fprintf(writer, SPACE) - } - // Check if border is set - // Replace with space if not set - fmt.Fprint(writer, ConditionString(t.borders.Left, t.pColumn, SPACE)) - fmt.Fprint(writer, t.newLine) - } - - //The new previous line is the current one - previousLine = make([]string, total) - for y := 0; y < total; y++ { - previousLine[y] = strings.TrimRight(strings.Join(columns[y], " "), " ") //Store the full line for multi-lines cells - } - //Returns the newly added line and wether or not a border should be displayed above. - return previousLine, displayCellBorder -} - -func (t *Table) parseDimension(str string, colKey, rowKey int) []string { - var ( - raw []string - maxWidth int - ) - - raw = getLines(str) - maxWidth = 0 - for _, line := range raw { - if w := DisplayWidth(line); w > maxWidth { - maxWidth = w - } - } - - // If wrapping, ensure that all paragraphs in the cell fit in the - // specified width. - if t.autoWrap { - // If there's a maximum allowed width for wrapping, use that. - if maxWidth > t.mW { - maxWidth = t.mW - } - - // In the process of doing so, we need to recompute maxWidth. This - // is because perhaps a word in the cell is longer than the - // allowed maximum width in t.mW. - newMaxWidth := maxWidth - newRaw := make([]string, 0, len(raw)) - - if t.reflowText { - // Make a single paragraph of everything. - raw = []string{strings.Join(raw, " ")} - } - for i, para := range raw { - paraLines, _ := WrapString(para, maxWidth) - for _, line := range paraLines { - if w := DisplayWidth(line); w > newMaxWidth { - newMaxWidth = w - } - } - if i > 0 { - newRaw = append(newRaw, " ") - } - newRaw = append(newRaw, paraLines...) - } - raw = newRaw - maxWidth = newMaxWidth - } - - // Store the new known maximum width. - v, ok := t.cs[colKey] - if !ok || v < maxWidth || v == 0 { - t.cs[colKey] = maxWidth - } - - // Remember the number of lines for the row printer. - h := len(raw) - v, ok = t.rs[rowKey] - - if !ok || v < h || v == 0 { - t.rs[rowKey] = h - } - //fmt.Printf("Raw %+v %d\n", raw, len(raw)) - return raw -} diff --git a/vendor/github.com/olekukonko/tablewriter/table_with_color.go b/vendor/github.com/olekukonko/tablewriter/table_with_color.go deleted file mode 100644 index ae7a364a..00000000 --- a/vendor/github.com/olekukonko/tablewriter/table_with_color.go +++ /dev/null @@ -1,136 +0,0 @@ -package tablewriter - -import ( - "fmt" - "strconv" - "strings" -) - -const ESC = "\033" -const SEP = ";" - -const ( - BgBlackColor int = iota + 40 - BgRedColor - BgGreenColor - BgYellowColor - BgBlueColor - BgMagentaColor - BgCyanColor - BgWhiteColor -) - -const ( - FgBlackColor int = iota + 30 - FgRedColor - FgGreenColor - FgYellowColor - FgBlueColor - FgMagentaColor - FgCyanColor - FgWhiteColor -) - -const ( - BgHiBlackColor int = iota + 100 - BgHiRedColor - BgHiGreenColor - BgHiYellowColor - BgHiBlueColor - BgHiMagentaColor - BgHiCyanColor - BgHiWhiteColor -) - -const ( - FgHiBlackColor int = iota + 90 - FgHiRedColor - FgHiGreenColor - FgHiYellowColor - FgHiBlueColor - FgHiMagentaColor - FgHiCyanColor - FgHiWhiteColor -) - -const ( - Normal = 0 - Bold = 1 - UnderlineSingle = 4 - Italic -) - -type Colors []int - -func startFormat(seq string) string { - return fmt.Sprintf("%s[%sm", ESC, seq) -} - -func stopFormat() string { - return fmt.Sprintf("%s[%dm", ESC, Normal) -} - -// Making the SGR (Select Graphic Rendition) sequence. -func makeSequence(codes []int) string { - codesInString := []string{} - for _, code := range codes { - codesInString = append(codesInString, strconv.Itoa(code)) - } - return strings.Join(codesInString, SEP) -} - -// Adding ANSI escape sequences before and after string -func format(s string, codes interface{}) string { - var seq string - - switch v := codes.(type) { - - case string: - seq = v - case []int: - seq = makeSequence(v) - case Colors: - seq = makeSequence(v) - default: - return s - } - - if len(seq) == 0 { - return s - } - return startFormat(seq) + s + stopFormat() -} - -// Adding header colors (ANSI codes) -func (t *Table) SetHeaderColor(colors ...Colors) { - if t.colSize != len(colors) { - panic("Number of header colors must be equal to number of headers.") - } - for i := 0; i < len(colors); i++ { - t.headerParams = append(t.headerParams, makeSequence(colors[i])) - } -} - -// Adding column colors (ANSI codes) -func (t *Table) SetColumnColor(colors ...Colors) { - if t.colSize != len(colors) { - panic("Number of column colors must be equal to number of headers.") - } - for i := 0; i < len(colors); i++ { - t.columnsParams = append(t.columnsParams, makeSequence(colors[i])) - } -} - -// Adding column colors (ANSI codes) -func (t *Table) SetFooterColor(colors ...Colors) { - if len(t.footers) != len(colors) { - panic("Number of footer colors must be equal to number of footer.") - } - for i := 0; i < len(colors); i++ { - t.footerParams = append(t.footerParams, makeSequence(colors[i])) - } -} - -func Color(colors ...int) []int { - return colors -} diff --git a/vendor/github.com/olekukonko/tablewriter/util.go b/vendor/github.com/olekukonko/tablewriter/util.go deleted file mode 100644 index 380e7ab3..00000000 --- a/vendor/github.com/olekukonko/tablewriter/util.go +++ /dev/null @@ -1,93 +0,0 @@ -// Copyright 2014 Oleku Konko All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -// This module is a Table Writer API for the Go Programming Language. -// The protocols were written in pure Go and works on windows and unix systems - -package tablewriter - -import ( - "math" - "regexp" - "strings" - - "github.com/mattn/go-runewidth" -) - -var ansi = regexp.MustCompile("\033\\[(?:[0-9]{1,3}(?:;[0-9]{1,3})*)?[m|K]") - -func DisplayWidth(str string) int { - return runewidth.StringWidth(ansi.ReplaceAllLiteralString(str, "")) -} - -// Simple Condition for string -// Returns value based on condition -func ConditionString(cond bool, valid, inValid string) string { - if cond { - return valid - } - return inValid -} - -func isNumOrSpace(r rune) bool { - return ('0' <= r && r <= '9') || r == ' ' -} - -// Format Table Header -// Replace _ , . and spaces -func Title(name string) string { - origLen := len(name) - rs := []rune(name) - for i, r := range rs { - switch r { - case '_': - rs[i] = ' ' - case '.': - // ignore floating number 0.0 - if (i != 0 && !isNumOrSpace(rs[i-1])) || (i != len(rs)-1 && !isNumOrSpace(rs[i+1])) { - rs[i] = ' ' - } - } - } - name = string(rs) - name = strings.TrimSpace(name) - if len(name) == 0 && origLen > 0 { - // Keep at least one character. This is important to preserve - // empty lines in multi-line headers/footers. - name = " " - } - return strings.ToUpper(name) -} - -// Pad String -// Attempts to place string in the center -func Pad(s, pad string, width int) string { - gap := width - DisplayWidth(s) - if gap > 0 { - gapLeft := int(math.Ceil(float64(gap / 2))) - gapRight := gap - gapLeft - return strings.Repeat(string(pad), gapLeft) + s + strings.Repeat(string(pad), gapRight) - } - return s -} - -// Pad String Right position -// This would place string at the left side of the screen -func PadRight(s, pad string, width int) string { - gap := width - DisplayWidth(s) - if gap > 0 { - return s + strings.Repeat(string(pad), gap) - } - return s -} - -// Pad String Left position -// This would place string at the right side of the screen -func PadLeft(s, pad string, width int) string { - gap := width - DisplayWidth(s) - if gap > 0 { - return strings.Repeat(string(pad), gap) + s - } - return s -} diff --git a/vendor/github.com/olekukonko/tablewriter/wrap.go b/vendor/github.com/olekukonko/tablewriter/wrap.go deleted file mode 100644 index a092ee1f..00000000 --- a/vendor/github.com/olekukonko/tablewriter/wrap.go +++ /dev/null @@ -1,99 +0,0 @@ -// Copyright 2014 Oleku Konko All rights reserved. -// Use of this source code is governed by a MIT -// license that can be found in the LICENSE file. - -// This module is a Table Writer API for the Go Programming Language. -// The protocols were written in pure Go and works on windows and unix systems - -package tablewriter - -import ( - "math" - "strings" - - "github.com/mattn/go-runewidth" -) - -var ( - nl = "\n" - sp = " " -) - -const defaultPenalty = 1e5 - -// Wrap wraps s into a paragraph of lines of length lim, with minimal -// raggedness. -func WrapString(s string, lim int) ([]string, int) { - words := strings.Split(strings.Replace(s, nl, sp, -1), sp) - var lines []string - max := 0 - for _, v := range words { - max = runewidth.StringWidth(v) - if max > lim { - lim = max - } - } - for _, line := range WrapWords(words, 1, lim, defaultPenalty) { - lines = append(lines, strings.Join(line, sp)) - } - return lines, lim -} - -// WrapWords is the low-level line-breaking algorithm, useful if you need more -// control over the details of the text wrapping process. For most uses, -// WrapString will be sufficient and more convenient. -// -// WrapWords splits a list of words into lines with minimal "raggedness", -// treating each rune as one unit, accounting for spc units between adjacent -// words on each line, and attempting to limit lines to lim units. Raggedness -// is the total error over all lines, where error is the square of the -// difference of the length of the line and lim. Too-long lines (which only -// happen when a single word is longer than lim units) have pen penalty units -// added to the error. -func WrapWords(words []string, spc, lim, pen int) [][]string { - n := len(words) - - length := make([][]int, n) - for i := 0; i < n; i++ { - length[i] = make([]int, n) - length[i][i] = runewidth.StringWidth(words[i]) - for j := i + 1; j < n; j++ { - length[i][j] = length[i][j-1] + spc + runewidth.StringWidth(words[j]) - } - } - nbrk := make([]int, n) - cost := make([]int, n) - for i := range cost { - cost[i] = math.MaxInt32 - } - for i := n - 1; i >= 0; i-- { - if length[i][n-1] <= lim { - cost[i] = 0 - nbrk[i] = n - } else { - for j := i + 1; j < n; j++ { - d := lim - length[i][j-1] - c := d*d + cost[j] - if length[i][j-1] > lim { - c += pen // too-long lines get a worse penalty - } - if c < cost[i] { - cost[i] = c - nbrk[i] = j - } - } - } - } - var lines [][]string - i := 0 - for i < n { - lines = append(lines, words[i:nbrk[i]]) - i = nbrk[i] - } - return lines -} - -// getLines decomposes a multiline string into a slice of strings. -func getLines(s string) []string { - return strings.Split(s, nl) -} diff --git a/vendor/github.com/russross/blackfriday/v2/.gitignore b/vendor/github.com/russross/blackfriday/v2/.gitignore deleted file mode 100644 index 75623dcc..00000000 --- a/vendor/github.com/russross/blackfriday/v2/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -*.out -*.swp -*.8 -*.6 -_obj -_test* -markdown -tags diff --git a/vendor/github.com/russross/blackfriday/v2/.travis.yml b/vendor/github.com/russross/blackfriday/v2/.travis.yml deleted file mode 100644 index b0b525a5..00000000 --- a/vendor/github.com/russross/blackfriday/v2/.travis.yml +++ /dev/null @@ -1,17 +0,0 @@ -sudo: false -language: go -go: - - "1.10.x" - - "1.11.x" - - tip -matrix: - fast_finish: true - allow_failures: - - go: tip -install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . - - go test -v ./... diff --git a/vendor/github.com/russross/blackfriday/v2/LICENSE.txt b/vendor/github.com/russross/blackfriday/v2/LICENSE.txt deleted file mode 100644 index 2885af36..00000000 --- a/vendor/github.com/russross/blackfriday/v2/LICENSE.txt +++ /dev/null @@ -1,29 +0,0 @@ -Blackfriday is distributed under the Simplified BSD License: - -> Copyright © 2011 Russ Ross -> All rights reserved. -> -> Redistribution and use in source and binary forms, with or without -> modification, are permitted provided that the following conditions -> are met: -> -> 1. Redistributions of source code must retain the above copyright -> notice, this list of conditions and the following disclaimer. -> -> 2. Redistributions in binary form must reproduce the above -> copyright notice, this list of conditions and the following -> disclaimer in the documentation and/or other materials provided with -> the distribution. -> -> THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -> "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -> LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS -> FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE -> COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, -> INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, -> BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -> LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER -> CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT -> LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN -> ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -> POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/russross/blackfriday/v2/README.md b/vendor/github.com/russross/blackfriday/v2/README.md deleted file mode 100644 index d5a8649b..00000000 --- a/vendor/github.com/russross/blackfriday/v2/README.md +++ /dev/null @@ -1,291 +0,0 @@ -Blackfriday [![Build Status](https://travis-ci.org/russross/blackfriday.svg?branch=master)](https://travis-ci.org/russross/blackfriday) -=========== - -Blackfriday is a [Markdown][1] processor implemented in [Go][2]. It -is paranoid about its input (so you can safely feed it user-supplied -data), it is fast, it supports common extensions (tables, smart -punctuation substitutions, etc.), and it is safe for all utf-8 -(unicode) input. - -HTML output is currently supported, along with Smartypants -extensions. - -It started as a translation from C of [Sundown][3]. - - -Installation ------------- - -Blackfriday is compatible with any modern Go release. With Go 1.7 and git -installed: - - go get gopkg.in/russross/blackfriday.v2 - -will download, compile, and install the package into your `$GOPATH` -directory hierarchy. Alternatively, you can achieve the same if you -import it into a project: - - import "gopkg.in/russross/blackfriday.v2" - -and `go get` without parameters. - - -Versions --------- - -Currently maintained and recommended version of Blackfriday is `v2`. It's being -developed on its own branch: https://github.com/russross/blackfriday/tree/v2 and the -documentation is available at -https://godoc.org/gopkg.in/russross/blackfriday.v2. - -It is `go get`-able via via [gopkg.in][6] at `gopkg.in/russross/blackfriday.v2`, -but we highly recommend using package management tool like [dep][7] or -[Glide][8] and make use of semantic versioning. With package management you -should import `github.com/russross/blackfriday` and specify that you're using -version 2.0.0. - -Version 2 offers a number of improvements over v1: - -* Cleaned up API -* A separate call to [`Parse`][4], which produces an abstract syntax tree for - the document -* Latest bug fixes -* Flexibility to easily add your own rendering extensions - -Potential drawbacks: - -* Our benchmarks show v2 to be slightly slower than v1. Currently in the - ballpark of around 15%. -* API breakage. If you can't afford modifying your code to adhere to the new API - and don't care too much about the new features, v2 is probably not for you. -* Several bug fixes are trailing behind and still need to be forward-ported to - v2. See issue [#348](https://github.com/russross/blackfriday/issues/348) for - tracking. - -Usage ------ - -For the most sensible markdown processing, it is as simple as getting your input -into a byte slice and calling: - -```go -output := blackfriday.Run(input) -``` - -Your input will be parsed and the output rendered with a set of most popular -extensions enabled. If you want the most basic feature set, corresponding with -the bare Markdown specification, use: - -```go -output := blackfriday.Run(input, blackfriday.WithNoExtensions()) -``` - -### Sanitize untrusted content - -Blackfriday itself does nothing to protect against malicious content. If you are -dealing with user-supplied markdown, we recommend running Blackfriday's output -through HTML sanitizer such as [Bluemonday][5]. - -Here's an example of simple usage of Blackfriday together with Bluemonday: - -```go -import ( - "github.com/microcosm-cc/bluemonday" - "github.com/russross/blackfriday" -) - -// ... -unsafe := blackfriday.Run(input) -html := bluemonday.UGCPolicy().SanitizeBytes(unsafe) -``` - -### Custom options - -If you want to customize the set of options, use `blackfriday.WithExtensions`, -`blackfriday.WithRenderer` and `blackfriday.WithRefOverride`. - -You can also check out `blackfriday-tool` for a more complete example -of how to use it. Download and install it using: - - go get github.com/russross/blackfriday-tool - -This is a simple command-line tool that allows you to process a -markdown file using a standalone program. You can also browse the -source directly on github if you are just looking for some example -code: - -* - -Note that if you have not already done so, installing -`blackfriday-tool` will be sufficient to download and install -blackfriday in addition to the tool itself. The tool binary will be -installed in `$GOPATH/bin`. This is a statically-linked binary that -can be copied to wherever you need it without worrying about -dependencies and library versions. - - -Features --------- - -All features of Sundown are supported, including: - -* **Compatibility**. The Markdown v1.0.3 test suite passes with - the `--tidy` option. Without `--tidy`, the differences are - mostly in whitespace and entity escaping, where blackfriday is - more consistent and cleaner. - -* **Common extensions**, including table support, fenced code - blocks, autolinks, strikethroughs, non-strict emphasis, etc. - -* **Safety**. Blackfriday is paranoid when parsing, making it safe - to feed untrusted user input without fear of bad things - happening. The test suite stress tests this and there are no - known inputs that make it crash. If you find one, please let me - know and send me the input that does it. - - NOTE: "safety" in this context means *runtime safety only*. In order to - protect yourself against JavaScript injection in untrusted content, see - [this example](https://github.com/russross/blackfriday#sanitize-untrusted-content). - -* **Fast processing**. It is fast enough to render on-demand in - most web applications without having to cache the output. - -* **Thread safety**. You can run multiple parsers in different - goroutines without ill effect. There is no dependence on global - shared state. - -* **Minimal dependencies**. Blackfriday only depends on standard - library packages in Go. The source code is pretty - self-contained, so it is easy to add to any project, including - Google App Engine projects. - -* **Standards compliant**. Output successfully validates using the - W3C validation tool for HTML 4.01 and XHTML 1.0 Transitional. - - -Extensions ----------- - -In addition to the standard markdown syntax, this package -implements the following extensions: - -* **Intra-word emphasis supression**. The `_` character is - commonly used inside words when discussing code, so having - markdown interpret it as an emphasis command is usually the - wrong thing. Blackfriday lets you treat all emphasis markers as - normal characters when they occur inside a word. - -* **Tables**. Tables can be created by drawing them in the input - using a simple syntax: - - ``` - Name | Age - --------|------ - Bob | 27 - Alice | 23 - ``` - -* **Fenced code blocks**. In addition to the normal 4-space - indentation to mark code blocks, you can explicitly mark them - and supply a language (to make syntax highlighting simple). Just - mark it like this: - - ```go - func getTrue() bool { - return true - } - ``` - - You can use 3 or more backticks to mark the beginning of the - block, and the same number to mark the end of the block. - -* **Definition lists**. A simple definition list is made of a single-line - term followed by a colon and the definition for that term. - - Cat - : Fluffy animal everyone likes - - Internet - : Vector of transmission for pictures of cats - - Terms must be separated from the previous definition by a blank line. - -* **Footnotes**. A marker in the text that will become a superscript number; - a footnote definition that will be placed in a list of footnotes at the - end of the document. A footnote looks like this: - - This is a footnote.[^1] - - [^1]: the footnote text. - -* **Autolinking**. Blackfriday can find URLs that have not been - explicitly marked as links and turn them into links. - -* **Strikethrough**. Use two tildes (`~~`) to mark text that - should be crossed out. - -* **Hard line breaks**. With this extension enabled newlines in the input - translate into line breaks in the output. This extension is off by default. - -* **Smart quotes**. Smartypants-style punctuation substitution is - supported, turning normal double- and single-quote marks into - curly quotes, etc. - -* **LaTeX-style dash parsing** is an additional option, where `--` - is translated into `–`, and `---` is translated into - `—`. This differs from most smartypants processors, which - turn a single hyphen into an ndash and a double hyphen into an - mdash. - -* **Smart fractions**, where anything that looks like a fraction - is translated into suitable HTML (instead of just a few special - cases like most smartypant processors). For example, `4/5` - becomes `45`, which renders as - 45. - - -Other renderers ---------------- - -Blackfriday is structured to allow alternative rendering engines. Here -are a few of note: - -* [github_flavored_markdown](https://godoc.org/github.com/shurcooL/github_flavored_markdown): - provides a GitHub Flavored Markdown renderer with fenced code block - highlighting, clickable heading anchor links. - - It's not customizable, and its goal is to produce HTML output - equivalent to the [GitHub Markdown API endpoint](https://developer.github.com/v3/markdown/#render-a-markdown-document-in-raw-mode), - except the rendering is performed locally. - -* [markdownfmt](https://github.com/shurcooL/markdownfmt): like gofmt, - but for markdown. - -* [LaTeX output](https://github.com/Ambrevar/Blackfriday-LaTeX): - renders output as LaTeX. - -* [Blackfriday-Confluence](https://github.com/kentaro-m/blackfriday-confluence): provides a [Confluence Wiki Markup](https://confluence.atlassian.com/doc/confluence-wiki-markup-251003035.html) renderer. - - -Todo ----- - -* More unit testing -* Improve unicode support. It does not understand all unicode - rules (about what constitutes a letter, a punctuation symbol, - etc.), so it may fail to detect word boundaries correctly in - some instances. It is safe on all utf-8 input. - - -License -------- - -[Blackfriday is distributed under the Simplified BSD License](LICENSE.txt) - - - [1]: https://daringfireball.net/projects/markdown/ "Markdown" - [2]: https://golang.org/ "Go Language" - [3]: https://github.com/vmg/sundown "Sundown" - [4]: https://godoc.org/gopkg.in/russross/blackfriday.v2#Parse "Parse func" - [5]: https://github.com/microcosm-cc/bluemonday "Bluemonday" - [6]: https://labix.org/gopkg.in "gopkg.in" diff --git a/vendor/github.com/russross/blackfriday/v2/block.go b/vendor/github.com/russross/blackfriday/v2/block.go deleted file mode 100644 index b8607474..00000000 --- a/vendor/github.com/russross/blackfriday/v2/block.go +++ /dev/null @@ -1,1590 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// Functions to parse block-level elements. -// - -package blackfriday - -import ( - "bytes" - "html" - "regexp" - "strings" - - "github.com/shurcooL/sanitized_anchor_name" -) - -const ( - charEntity = "&(?:#x[a-f0-9]{1,8}|#[0-9]{1,8}|[a-z][a-z0-9]{1,31});" - escapable = "[!\"#$%&'()*+,./:;<=>?@[\\\\\\]^_`{|}~-]" -) - -var ( - reBackslashOrAmp = regexp.MustCompile("[\\&]") - reEntityOrEscapedChar = regexp.MustCompile("(?i)\\\\" + escapable + "|" + charEntity) -) - -// Parse block-level data. -// Note: this function and many that it calls assume that -// the input buffer ends with a newline. -func (p *Markdown) block(data []byte) { - // this is called recursively: enforce a maximum depth - if p.nesting >= p.maxNesting { - return - } - p.nesting++ - - // parse out one block-level construct at a time - for len(data) > 0 { - // prefixed heading: - // - // # Heading 1 - // ## Heading 2 - // ... - // ###### Heading 6 - if p.isPrefixHeading(data) { - data = data[p.prefixHeading(data):] - continue - } - - // block of preformatted HTML: - // - //

    - // ... - //
    - if data[0] == '<' { - if i := p.html(data, true); i > 0 { - data = data[i:] - continue - } - } - - // title block - // - // % stuff - // % more stuff - // % even more stuff - if p.extensions&Titleblock != 0 { - if data[0] == '%' { - if i := p.titleBlock(data, true); i > 0 { - data = data[i:] - continue - } - } - } - - // blank lines. note: returns the # of bytes to skip - if i := p.isEmpty(data); i > 0 { - data = data[i:] - continue - } - - // indented code block: - // - // func max(a, b int) int { - // if a > b { - // return a - // } - // return b - // } - if p.codePrefix(data) > 0 { - data = data[p.code(data):] - continue - } - - // fenced code block: - // - // ``` go - // func fact(n int) int { - // if n <= 1 { - // return n - // } - // return n * fact(n-1) - // } - // ``` - if p.extensions&FencedCode != 0 { - if i := p.fencedCodeBlock(data, true); i > 0 { - data = data[i:] - continue - } - } - - // horizontal rule: - // - // ------ - // or - // ****** - // or - // ______ - if p.isHRule(data) { - p.addBlock(HorizontalRule, nil) - var i int - for i = 0; i < len(data) && data[i] != '\n'; i++ { - } - data = data[i:] - continue - } - - // block quote: - // - // > A big quote I found somewhere - // > on the web - if p.quotePrefix(data) > 0 { - data = data[p.quote(data):] - continue - } - - // table: - // - // Name | Age | Phone - // ------|-----|--------- - // Bob | 31 | 555-1234 - // Alice | 27 | 555-4321 - if p.extensions&Tables != 0 { - if i := p.table(data); i > 0 { - data = data[i:] - continue - } - } - - // an itemized/unordered list: - // - // * Item 1 - // * Item 2 - // - // also works with + or - - if p.uliPrefix(data) > 0 { - data = data[p.list(data, 0):] - continue - } - - // a numbered/ordered list: - // - // 1. Item 1 - // 2. Item 2 - if p.oliPrefix(data) > 0 { - data = data[p.list(data, ListTypeOrdered):] - continue - } - - // definition lists: - // - // Term 1 - // : Definition a - // : Definition b - // - // Term 2 - // : Definition c - if p.extensions&DefinitionLists != 0 { - if p.dliPrefix(data) > 0 { - data = data[p.list(data, ListTypeDefinition):] - continue - } - } - - // anything else must look like a normal paragraph - // note: this finds underlined headings, too - data = data[p.paragraph(data):] - } - - p.nesting-- -} - -func (p *Markdown) addBlock(typ NodeType, content []byte) *Node { - p.closeUnmatchedBlocks() - container := p.addChild(typ, 0) - container.content = content - return container -} - -func (p *Markdown) isPrefixHeading(data []byte) bool { - if data[0] != '#' { - return false - } - - if p.extensions&SpaceHeadings != 0 { - level := 0 - for level < 6 && level < len(data) && data[level] == '#' { - level++ - } - if level == len(data) || data[level] != ' ' { - return false - } - } - return true -} - -func (p *Markdown) prefixHeading(data []byte) int { - level := 0 - for level < 6 && level < len(data) && data[level] == '#' { - level++ - } - i := skipChar(data, level, ' ') - end := skipUntilChar(data, i, '\n') - skip := end - id := "" - if p.extensions&HeadingIDs != 0 { - j, k := 0, 0 - // find start/end of heading id - for j = i; j < end-1 && (data[j] != '{' || data[j+1] != '#'); j++ { - } - for k = j + 1; k < end && data[k] != '}'; k++ { - } - // extract heading id iff found - if j < end && k < end { - id = string(data[j+2 : k]) - end = j - skip = k + 1 - for end > 0 && data[end-1] == ' ' { - end-- - } - } - } - for end > 0 && data[end-1] == '#' { - if isBackslashEscaped(data, end-1) { - break - } - end-- - } - for end > 0 && data[end-1] == ' ' { - end-- - } - if end > i { - if id == "" && p.extensions&AutoHeadingIDs != 0 { - id = sanitized_anchor_name.Create(string(data[i:end])) - } - block := p.addBlock(Heading, data[i:end]) - block.HeadingID = id - block.Level = level - } - return skip -} - -func (p *Markdown) isUnderlinedHeading(data []byte) int { - // test of level 1 heading - if data[0] == '=' { - i := skipChar(data, 1, '=') - i = skipChar(data, i, ' ') - if i < len(data) && data[i] == '\n' { - return 1 - } - return 0 - } - - // test of level 2 heading - if data[0] == '-' { - i := skipChar(data, 1, '-') - i = skipChar(data, i, ' ') - if i < len(data) && data[i] == '\n' { - return 2 - } - return 0 - } - - return 0 -} - -func (p *Markdown) titleBlock(data []byte, doRender bool) int { - if data[0] != '%' { - return 0 - } - splitData := bytes.Split(data, []byte("\n")) - var i int - for idx, b := range splitData { - if !bytes.HasPrefix(b, []byte("%")) { - i = idx // - 1 - break - } - } - - data = bytes.Join(splitData[0:i], []byte("\n")) - consumed := len(data) - data = bytes.TrimPrefix(data, []byte("% ")) - data = bytes.Replace(data, []byte("\n% "), []byte("\n"), -1) - block := p.addBlock(Heading, data) - block.Level = 1 - block.IsTitleblock = true - - return consumed -} - -func (p *Markdown) html(data []byte, doRender bool) int { - var i, j int - - // identify the opening tag - if data[0] != '<' { - return 0 - } - curtag, tagfound := p.htmlFindTag(data[1:]) - - // handle special cases - if !tagfound { - // check for an HTML comment - if size := p.htmlComment(data, doRender); size > 0 { - return size - } - - // check for an
    tag - if size := p.htmlHr(data, doRender); size > 0 { - return size - } - - // no special case recognized - return 0 - } - - // look for an unindented matching closing tag - // followed by a blank line - found := false - /* - closetag := []byte("\n") - j = len(curtag) + 1 - for !found { - // scan for a closing tag at the beginning of a line - if skip := bytes.Index(data[j:], closetag); skip >= 0 { - j += skip + len(closetag) - } else { - break - } - - // see if it is the only thing on the line - if skip := p.isEmpty(data[j:]); skip > 0 { - // see if it is followed by a blank line/eof - j += skip - if j >= len(data) { - found = true - i = j - } else { - if skip := p.isEmpty(data[j:]); skip > 0 { - j += skip - found = true - i = j - } - } - } - } - */ - - // if not found, try a second pass looking for indented match - // but not if tag is "ins" or "del" (following original Markdown.pl) - if !found && curtag != "ins" && curtag != "del" { - i = 1 - for i < len(data) { - i++ - for i < len(data) && !(data[i-1] == '<' && data[i] == '/') { - i++ - } - - if i+2+len(curtag) >= len(data) { - break - } - - j = p.htmlFindEnd(curtag, data[i-1:]) - - if j > 0 { - i += j - 1 - found = true - break - } - } - } - - if !found { - return 0 - } - - // the end of the block has been found - if doRender { - // trim newlines - end := i - for end > 0 && data[end-1] == '\n' { - end-- - } - finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) - } - - return i -} - -func finalizeHTMLBlock(block *Node) { - block.Literal = block.content - block.content = nil -} - -// HTML comment, lax form -func (p *Markdown) htmlComment(data []byte, doRender bool) int { - i := p.inlineHTMLComment(data) - // needs to end with a blank line - if j := p.isEmpty(data[i:]); j > 0 { - size := i + j - if doRender { - // trim trailing newlines - end := size - for end > 0 && data[end-1] == '\n' { - end-- - } - block := p.addBlock(HTMLBlock, data[:end]) - finalizeHTMLBlock(block) - } - return size - } - return 0 -} - -// HR, which is the only self-closing block tag considered -func (p *Markdown) htmlHr(data []byte, doRender bool) int { - if len(data) < 4 { - return 0 - } - if data[0] != '<' || (data[1] != 'h' && data[1] != 'H') || (data[2] != 'r' && data[2] != 'R') { - return 0 - } - if data[3] != ' ' && data[3] != '/' && data[3] != '>' { - // not an
    tag after all; at least not a valid one - return 0 - } - i := 3 - for i < len(data) && data[i] != '>' && data[i] != '\n' { - i++ - } - if i < len(data) && data[i] == '>' { - i++ - if j := p.isEmpty(data[i:]); j > 0 { - size := i + j - if doRender { - // trim newlines - end := size - for end > 0 && data[end-1] == '\n' { - end-- - } - finalizeHTMLBlock(p.addBlock(HTMLBlock, data[:end])) - } - return size - } - } - return 0 -} - -func (p *Markdown) htmlFindTag(data []byte) (string, bool) { - i := 0 - for i < len(data) && isalnum(data[i]) { - i++ - } - key := string(data[:i]) - if _, ok := blockTags[key]; ok { - return key, true - } - return "", false -} - -func (p *Markdown) htmlFindEnd(tag string, data []byte) int { - // assume data[0] == '<' && data[1] == '/' already tested - if tag == "hr" { - return 2 - } - // check if tag is a match - closetag := []byte("") - if !bytes.HasPrefix(data, closetag) { - return 0 - } - i := len(closetag) - - // check that the rest of the line is blank - skip := 0 - if skip = p.isEmpty(data[i:]); skip == 0 { - return 0 - } - i += skip - skip = 0 - - if i >= len(data) { - return i - } - - if p.extensions&LaxHTMLBlocks != 0 { - return i - } - if skip = p.isEmpty(data[i:]); skip == 0 { - // following line must be blank - return 0 - } - - return i + skip -} - -func (*Markdown) isEmpty(data []byte) int { - // it is okay to call isEmpty on an empty buffer - if len(data) == 0 { - return 0 - } - - var i int - for i = 0; i < len(data) && data[i] != '\n'; i++ { - if data[i] != ' ' && data[i] != '\t' { - return 0 - } - } - if i < len(data) && data[i] == '\n' { - i++ - } - return i -} - -func (*Markdown) isHRule(data []byte) bool { - i := 0 - - // skip up to three spaces - for i < 3 && data[i] == ' ' { - i++ - } - - // look at the hrule char - if data[i] != '*' && data[i] != '-' && data[i] != '_' { - return false - } - c := data[i] - - // the whole line must be the char or whitespace - n := 0 - for i < len(data) && data[i] != '\n' { - switch { - case data[i] == c: - n++ - case data[i] != ' ': - return false - } - i++ - } - - return n >= 3 -} - -// isFenceLine checks if there's a fence line (e.g., ``` or ``` go) at the beginning of data, -// and returns the end index if so, or 0 otherwise. It also returns the marker found. -// If info is not nil, it gets set to the syntax specified in the fence line. -func isFenceLine(data []byte, info *string, oldmarker string) (end int, marker string) { - i, size := 0, 0 - - // skip up to three spaces - for i < len(data) && i < 3 && data[i] == ' ' { - i++ - } - - // check for the marker characters: ~ or ` - if i >= len(data) { - return 0, "" - } - if data[i] != '~' && data[i] != '`' { - return 0, "" - } - - c := data[i] - - // the whole line must be the same char or whitespace - for i < len(data) && data[i] == c { - size++ - i++ - } - - // the marker char must occur at least 3 times - if size < 3 { - return 0, "" - } - marker = string(data[i-size : i]) - - // if this is the end marker, it must match the beginning marker - if oldmarker != "" && marker != oldmarker { - return 0, "" - } - - // TODO(shurcooL): It's probably a good idea to simplify the 2 code paths here - // into one, always get the info string, and discard it if the caller doesn't care. - if info != nil { - infoLength := 0 - i = skipChar(data, i, ' ') - - if i >= len(data) { - if i == len(data) { - return i, marker - } - return 0, "" - } - - infoStart := i - - if data[i] == '{' { - i++ - infoStart++ - - for i < len(data) && data[i] != '}' && data[i] != '\n' { - infoLength++ - i++ - } - - if i >= len(data) || data[i] != '}' { - return 0, "" - } - - // strip all whitespace at the beginning and the end - // of the {} block - for infoLength > 0 && isspace(data[infoStart]) { - infoStart++ - infoLength-- - } - - for infoLength > 0 && isspace(data[infoStart+infoLength-1]) { - infoLength-- - } - i++ - i = skipChar(data, i, ' ') - } else { - for i < len(data) && !isverticalspace(data[i]) { - infoLength++ - i++ - } - } - - *info = strings.TrimSpace(string(data[infoStart : infoStart+infoLength])) - } - - if i == len(data) { - return i, marker - } - if i > len(data) || data[i] != '\n' { - return 0, "" - } - return i + 1, marker // Take newline into account. -} - -// fencedCodeBlock returns the end index if data contains a fenced code block at the beginning, -// or 0 otherwise. It writes to out if doRender is true, otherwise it has no side effects. -// If doRender is true, a final newline is mandatory to recognize the fenced code block. -func (p *Markdown) fencedCodeBlock(data []byte, doRender bool) int { - var info string - beg, marker := isFenceLine(data, &info, "") - if beg == 0 || beg >= len(data) { - return 0 - } - - var work bytes.Buffer - work.Write([]byte(info)) - work.WriteByte('\n') - - for { - // safe to assume beg < len(data) - - // check for the end of the code block - fenceEnd, _ := isFenceLine(data[beg:], nil, marker) - if fenceEnd != 0 { - beg += fenceEnd - break - } - - // copy the current line - end := skipUntilChar(data, beg, '\n') + 1 - - // did we reach the end of the buffer without a closing marker? - if end >= len(data) { - return 0 - } - - // verbatim copy to the working buffer - if doRender { - work.Write(data[beg:end]) - } - beg = end - } - - if doRender { - block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer - block.IsFenced = true - finalizeCodeBlock(block) - } - - return beg -} - -func unescapeChar(str []byte) []byte { - if str[0] == '\\' { - return []byte{str[1]} - } - return []byte(html.UnescapeString(string(str))) -} - -func unescapeString(str []byte) []byte { - if reBackslashOrAmp.Match(str) { - return reEntityOrEscapedChar.ReplaceAllFunc(str, unescapeChar) - } - return str -} - -func finalizeCodeBlock(block *Node) { - if block.IsFenced { - newlinePos := bytes.IndexByte(block.content, '\n') - firstLine := block.content[:newlinePos] - rest := block.content[newlinePos+1:] - block.Info = unescapeString(bytes.Trim(firstLine, "\n")) - block.Literal = rest - } else { - block.Literal = block.content - } - block.content = nil -} - -func (p *Markdown) table(data []byte) int { - table := p.addBlock(Table, nil) - i, columns := p.tableHeader(data) - if i == 0 { - p.tip = table.Parent - table.Unlink() - return 0 - } - - p.addBlock(TableBody, nil) - - for i < len(data) { - pipes, rowStart := 0, i - for ; i < len(data) && data[i] != '\n'; i++ { - if data[i] == '|' { - pipes++ - } - } - - if pipes == 0 { - i = rowStart - break - } - - // include the newline in data sent to tableRow - if i < len(data) && data[i] == '\n' { - i++ - } - p.tableRow(data[rowStart:i], columns, false) - } - - return i -} - -// check if the specified position is preceded by an odd number of backslashes -func isBackslashEscaped(data []byte, i int) bool { - backslashes := 0 - for i-backslashes-1 >= 0 && data[i-backslashes-1] == '\\' { - backslashes++ - } - return backslashes&1 == 1 -} - -func (p *Markdown) tableHeader(data []byte) (size int, columns []CellAlignFlags) { - i := 0 - colCount := 1 - for i = 0; i < len(data) && data[i] != '\n'; i++ { - if data[i] == '|' && !isBackslashEscaped(data, i) { - colCount++ - } - } - - // doesn't look like a table header - if colCount == 1 { - return - } - - // include the newline in the data sent to tableRow - j := i - if j < len(data) && data[j] == '\n' { - j++ - } - header := data[:j] - - // column count ignores pipes at beginning or end of line - if data[0] == '|' { - colCount-- - } - if i > 2 && data[i-1] == '|' && !isBackslashEscaped(data, i-1) { - colCount-- - } - - columns = make([]CellAlignFlags, colCount) - - // move on to the header underline - i++ - if i >= len(data) { - return - } - - if data[i] == '|' && !isBackslashEscaped(data, i) { - i++ - } - i = skipChar(data, i, ' ') - - // each column header is of form: / *:?-+:? *|/ with # dashes + # colons >= 3 - // and trailing | optional on last column - col := 0 - for i < len(data) && data[i] != '\n' { - dashes := 0 - - if data[i] == ':' { - i++ - columns[col] |= TableAlignmentLeft - dashes++ - } - for i < len(data) && data[i] == '-' { - i++ - dashes++ - } - if i < len(data) && data[i] == ':' { - i++ - columns[col] |= TableAlignmentRight - dashes++ - } - for i < len(data) && data[i] == ' ' { - i++ - } - if i == len(data) { - return - } - // end of column test is messy - switch { - case dashes < 3: - // not a valid column - return - - case data[i] == '|' && !isBackslashEscaped(data, i): - // marker found, now skip past trailing whitespace - col++ - i++ - for i < len(data) && data[i] == ' ' { - i++ - } - - // trailing junk found after last column - if col >= colCount && i < len(data) && data[i] != '\n' { - return - } - - case (data[i] != '|' || isBackslashEscaped(data, i)) && col+1 < colCount: - // something else found where marker was required - return - - case data[i] == '\n': - // marker is optional for the last column - col++ - - default: - // trailing junk found after last column - return - } - } - if col != colCount { - return - } - - p.addBlock(TableHead, nil) - p.tableRow(header, columns, true) - size = i - if size < len(data) && data[size] == '\n' { - size++ - } - return -} - -func (p *Markdown) tableRow(data []byte, columns []CellAlignFlags, header bool) { - p.addBlock(TableRow, nil) - i, col := 0, 0 - - if data[i] == '|' && !isBackslashEscaped(data, i) { - i++ - } - - for col = 0; col < len(columns) && i < len(data); col++ { - for i < len(data) && data[i] == ' ' { - i++ - } - - cellStart := i - - for i < len(data) && (data[i] != '|' || isBackslashEscaped(data, i)) && data[i] != '\n' { - i++ - } - - cellEnd := i - - // skip the end-of-cell marker, possibly taking us past end of buffer - i++ - - for cellEnd > cellStart && cellEnd-1 < len(data) && data[cellEnd-1] == ' ' { - cellEnd-- - } - - cell := p.addBlock(TableCell, data[cellStart:cellEnd]) - cell.IsHeader = header - cell.Align = columns[col] - } - - // pad it out with empty columns to get the right number - for ; col < len(columns); col++ { - cell := p.addBlock(TableCell, nil) - cell.IsHeader = header - cell.Align = columns[col] - } - - // silently ignore rows with too many cells -} - -// returns blockquote prefix length -func (p *Markdown) quotePrefix(data []byte) int { - i := 0 - for i < 3 && i < len(data) && data[i] == ' ' { - i++ - } - if i < len(data) && data[i] == '>' { - if i+1 < len(data) && data[i+1] == ' ' { - return i + 2 - } - return i + 1 - } - return 0 -} - -// blockquote ends with at least one blank line -// followed by something without a blockquote prefix -func (p *Markdown) terminateBlockquote(data []byte, beg, end int) bool { - if p.isEmpty(data[beg:]) <= 0 { - return false - } - if end >= len(data) { - return true - } - return p.quotePrefix(data[end:]) == 0 && p.isEmpty(data[end:]) == 0 -} - -// parse a blockquote fragment -func (p *Markdown) quote(data []byte) int { - block := p.addBlock(BlockQuote, nil) - var raw bytes.Buffer - beg, end := 0, 0 - for beg < len(data) { - end = beg - // Step over whole lines, collecting them. While doing that, check for - // fenced code and if one's found, incorporate it altogether, - // irregardless of any contents inside it - for end < len(data) && data[end] != '\n' { - if p.extensions&FencedCode != 0 { - if i := p.fencedCodeBlock(data[end:], false); i > 0 { - // -1 to compensate for the extra end++ after the loop: - end += i - 1 - break - } - } - end++ - } - if end < len(data) && data[end] == '\n' { - end++ - } - if pre := p.quotePrefix(data[beg:]); pre > 0 { - // skip the prefix - beg += pre - } else if p.terminateBlockquote(data, beg, end) { - break - } - // this line is part of the blockquote - raw.Write(data[beg:end]) - beg = end - } - p.block(raw.Bytes()) - p.finalize(block) - return end -} - -// returns prefix length for block code -func (p *Markdown) codePrefix(data []byte) int { - if len(data) >= 1 && data[0] == '\t' { - return 1 - } - if len(data) >= 4 && data[0] == ' ' && data[1] == ' ' && data[2] == ' ' && data[3] == ' ' { - return 4 - } - return 0 -} - -func (p *Markdown) code(data []byte) int { - var work bytes.Buffer - - i := 0 - for i < len(data) { - beg := i - for i < len(data) && data[i] != '\n' { - i++ - } - if i < len(data) && data[i] == '\n' { - i++ - } - - blankline := p.isEmpty(data[beg:i]) > 0 - if pre := p.codePrefix(data[beg:i]); pre > 0 { - beg += pre - } else if !blankline { - // non-empty, non-prefixed line breaks the pre - i = beg - break - } - - // verbatim copy to the working buffer - if blankline { - work.WriteByte('\n') - } else { - work.Write(data[beg:i]) - } - } - - // trim all the \n off the end of work - workbytes := work.Bytes() - eol := len(workbytes) - for eol > 0 && workbytes[eol-1] == '\n' { - eol-- - } - if eol != len(workbytes) { - work.Truncate(eol) - } - - work.WriteByte('\n') - - block := p.addBlock(CodeBlock, work.Bytes()) // TODO: get rid of temp buffer - block.IsFenced = false - finalizeCodeBlock(block) - - return i -} - -// returns unordered list item prefix -func (p *Markdown) uliPrefix(data []byte) int { - i := 0 - // start with up to 3 spaces - for i < len(data) && i < 3 && data[i] == ' ' { - i++ - } - if i >= len(data)-1 { - return 0 - } - // need one of {'*', '+', '-'} followed by a space or a tab - if (data[i] != '*' && data[i] != '+' && data[i] != '-') || - (data[i+1] != ' ' && data[i+1] != '\t') { - return 0 - } - return i + 2 -} - -// returns ordered list item prefix -func (p *Markdown) oliPrefix(data []byte) int { - i := 0 - - // start with up to 3 spaces - for i < 3 && i < len(data) && data[i] == ' ' { - i++ - } - - // count the digits - start := i - for i < len(data) && data[i] >= '0' && data[i] <= '9' { - i++ - } - if start == i || i >= len(data)-1 { - return 0 - } - - // we need >= 1 digits followed by a dot and a space or a tab - if data[i] != '.' || !(data[i+1] == ' ' || data[i+1] == '\t') { - return 0 - } - return i + 2 -} - -// returns definition list item prefix -func (p *Markdown) dliPrefix(data []byte) int { - if len(data) < 2 { - return 0 - } - i := 0 - // need a ':' followed by a space or a tab - if data[i] != ':' || !(data[i+1] == ' ' || data[i+1] == '\t') { - return 0 - } - for i < len(data) && data[i] == ' ' { - i++ - } - return i + 2 -} - -// parse ordered or unordered list block -func (p *Markdown) list(data []byte, flags ListType) int { - i := 0 - flags |= ListItemBeginningOfList - block := p.addBlock(List, nil) - block.ListFlags = flags - block.Tight = true - - for i < len(data) { - skip := p.listItem(data[i:], &flags) - if flags&ListItemContainsBlock != 0 { - block.ListData.Tight = false - } - i += skip - if skip == 0 || flags&ListItemEndOfList != 0 { - break - } - flags &= ^ListItemBeginningOfList - } - - above := block.Parent - finalizeList(block) - p.tip = above - return i -} - -// Returns true if the list item is not the same type as its parent list -func (p *Markdown) listTypeChanged(data []byte, flags *ListType) bool { - if p.dliPrefix(data) > 0 && *flags&ListTypeDefinition == 0 { - return true - } else if p.oliPrefix(data) > 0 && *flags&ListTypeOrdered == 0 { - return true - } else if p.uliPrefix(data) > 0 && (*flags&ListTypeOrdered != 0 || *flags&ListTypeDefinition != 0) { - return true - } - return false -} - -// Returns true if block ends with a blank line, descending if needed -// into lists and sublists. -func endsWithBlankLine(block *Node) bool { - // TODO: figure this out. Always false now. - for block != nil { - //if block.lastLineBlank { - //return true - //} - t := block.Type - if t == List || t == Item { - block = block.LastChild - } else { - break - } - } - return false -} - -func finalizeList(block *Node) { - block.open = false - item := block.FirstChild - for item != nil { - // check for non-final list item ending with blank line: - if endsWithBlankLine(item) && item.Next != nil { - block.ListData.Tight = false - break - } - // recurse into children of list item, to see if there are spaces - // between any of them: - subItem := item.FirstChild - for subItem != nil { - if endsWithBlankLine(subItem) && (item.Next != nil || subItem.Next != nil) { - block.ListData.Tight = false - break - } - subItem = subItem.Next - } - item = item.Next - } -} - -// Parse a single list item. -// Assumes initial prefix is already removed if this is a sublist. -func (p *Markdown) listItem(data []byte, flags *ListType) int { - // keep track of the indentation of the first line - itemIndent := 0 - if data[0] == '\t' { - itemIndent += 4 - } else { - for itemIndent < 3 && data[itemIndent] == ' ' { - itemIndent++ - } - } - - var bulletChar byte = '*' - i := p.uliPrefix(data) - if i == 0 { - i = p.oliPrefix(data) - } else { - bulletChar = data[i-2] - } - if i == 0 { - i = p.dliPrefix(data) - // reset definition term flag - if i > 0 { - *flags &= ^ListTypeTerm - } - } - if i == 0 { - // if in definition list, set term flag and continue - if *flags&ListTypeDefinition != 0 { - *flags |= ListTypeTerm - } else { - return 0 - } - } - - // skip leading whitespace on first line - for i < len(data) && data[i] == ' ' { - i++ - } - - // find the end of the line - line := i - for i > 0 && i < len(data) && data[i-1] != '\n' { - i++ - } - - // get working buffer - var raw bytes.Buffer - - // put the first line into the working buffer - raw.Write(data[line:i]) - line = i - - // process the following lines - containsBlankLine := false - sublist := 0 - codeBlockMarker := "" - -gatherlines: - for line < len(data) { - i++ - - // find the end of this line - for i < len(data) && data[i-1] != '\n' { - i++ - } - - // if it is an empty line, guess that it is part of this item - // and move on to the next line - if p.isEmpty(data[line:i]) > 0 { - containsBlankLine = true - line = i - continue - } - - // calculate the indentation - indent := 0 - indentIndex := 0 - if data[line] == '\t' { - indentIndex++ - indent += 4 - } else { - for indent < 4 && line+indent < i && data[line+indent] == ' ' { - indent++ - indentIndex++ - } - } - - chunk := data[line+indentIndex : i] - - if p.extensions&FencedCode != 0 { - // determine if in or out of codeblock - // if in codeblock, ignore normal list processing - _, marker := isFenceLine(chunk, nil, codeBlockMarker) - if marker != "" { - if codeBlockMarker == "" { - // start of codeblock - codeBlockMarker = marker - } else { - // end of codeblock. - codeBlockMarker = "" - } - } - // we are in a codeblock, write line, and continue - if codeBlockMarker != "" || marker != "" { - raw.Write(data[line+indentIndex : i]) - line = i - continue gatherlines - } - } - - // evaluate how this line fits in - switch { - // is this a nested list item? - case (p.uliPrefix(chunk) > 0 && !p.isHRule(chunk)) || - p.oliPrefix(chunk) > 0 || - p.dliPrefix(chunk) > 0: - - // to be a nested list, it must be indented more - // if not, it is either a different kind of list - // or the next item in the same list - if indent <= itemIndent { - if p.listTypeChanged(chunk, flags) { - *flags |= ListItemEndOfList - } else if containsBlankLine { - *flags |= ListItemContainsBlock - } - - break gatherlines - } - - if containsBlankLine { - *flags |= ListItemContainsBlock - } - - // is this the first item in the nested list? - if sublist == 0 { - sublist = raw.Len() - } - - // is this a nested prefix heading? - case p.isPrefixHeading(chunk): - // if the heading is not indented, it is not nested in the list - // and thus ends the list - if containsBlankLine && indent < 4 { - *flags |= ListItemEndOfList - break gatherlines - } - *flags |= ListItemContainsBlock - - // anything following an empty line is only part - // of this item if it is indented 4 spaces - // (regardless of the indentation of the beginning of the item) - case containsBlankLine && indent < 4: - if *flags&ListTypeDefinition != 0 && i < len(data)-1 { - // is the next item still a part of this list? - next := i - for next < len(data) && data[next] != '\n' { - next++ - } - for next < len(data)-1 && data[next] == '\n' { - next++ - } - if i < len(data)-1 && data[i] != ':' && data[next] != ':' { - *flags |= ListItemEndOfList - } - } else { - *flags |= ListItemEndOfList - } - break gatherlines - - // a blank line means this should be parsed as a block - case containsBlankLine: - raw.WriteByte('\n') - *flags |= ListItemContainsBlock - } - - // if this line was preceded by one or more blanks, - // re-introduce the blank into the buffer - if containsBlankLine { - containsBlankLine = false - raw.WriteByte('\n') - } - - // add the line into the working buffer without prefix - raw.Write(data[line+indentIndex : i]) - - line = i - } - - rawBytes := raw.Bytes() - - block := p.addBlock(Item, nil) - block.ListFlags = *flags - block.Tight = false - block.BulletChar = bulletChar - block.Delimiter = '.' // Only '.' is possible in Markdown, but ')' will also be possible in CommonMark - - // render the contents of the list item - if *flags&ListItemContainsBlock != 0 && *flags&ListTypeTerm == 0 { - // intermediate render of block item, except for definition term - if sublist > 0 { - p.block(rawBytes[:sublist]) - p.block(rawBytes[sublist:]) - } else { - p.block(rawBytes) - } - } else { - // intermediate render of inline item - if sublist > 0 { - child := p.addChild(Paragraph, 0) - child.content = rawBytes[:sublist] - p.block(rawBytes[sublist:]) - } else { - child := p.addChild(Paragraph, 0) - child.content = rawBytes - } - } - return line -} - -// render a single paragraph that has already been parsed out -func (p *Markdown) renderParagraph(data []byte) { - if len(data) == 0 { - return - } - - // trim leading spaces - beg := 0 - for data[beg] == ' ' { - beg++ - } - - end := len(data) - // trim trailing newline - if data[len(data)-1] == '\n' { - end-- - } - - // trim trailing spaces - for end > beg && data[end-1] == ' ' { - end-- - } - - p.addBlock(Paragraph, data[beg:end]) -} - -func (p *Markdown) paragraph(data []byte) int { - // prev: index of 1st char of previous line - // line: index of 1st char of current line - // i: index of cursor/end of current line - var prev, line, i int - tabSize := TabSizeDefault - if p.extensions&TabSizeEight != 0 { - tabSize = TabSizeDouble - } - // keep going until we find something to mark the end of the paragraph - for i < len(data) { - // mark the beginning of the current line - prev = line - current := data[i:] - line = i - - // did we find a reference or a footnote? If so, end a paragraph - // preceding it and report that we have consumed up to the end of that - // reference: - if refEnd := isReference(p, current, tabSize); refEnd > 0 { - p.renderParagraph(data[:i]) - return i + refEnd - } - - // did we find a blank line marking the end of the paragraph? - if n := p.isEmpty(current); n > 0 { - // did this blank line followed by a definition list item? - if p.extensions&DefinitionLists != 0 { - if i < len(data)-1 && data[i+1] == ':' { - return p.list(data[prev:], ListTypeDefinition) - } - } - - p.renderParagraph(data[:i]) - return i + n - } - - // an underline under some text marks a heading, so our paragraph ended on prev line - if i > 0 { - if level := p.isUnderlinedHeading(current); level > 0 { - // render the paragraph - p.renderParagraph(data[:prev]) - - // ignore leading and trailing whitespace - eol := i - 1 - for prev < eol && data[prev] == ' ' { - prev++ - } - for eol > prev && data[eol-1] == ' ' { - eol-- - } - - id := "" - if p.extensions&AutoHeadingIDs != 0 { - id = sanitized_anchor_name.Create(string(data[prev:eol])) - } - - block := p.addBlock(Heading, data[prev:eol]) - block.Level = level - block.HeadingID = id - - // find the end of the underline - for i < len(data) && data[i] != '\n' { - i++ - } - return i - } - } - - // if the next line starts a block of HTML, then the paragraph ends here - if p.extensions&LaxHTMLBlocks != 0 { - if data[i] == '<' && p.html(current, false) > 0 { - // rewind to before the HTML block - p.renderParagraph(data[:i]) - return i - } - } - - // if there's a prefixed heading or a horizontal rule after this, paragraph is over - if p.isPrefixHeading(current) || p.isHRule(current) { - p.renderParagraph(data[:i]) - return i - } - - // if there's a fenced code block, paragraph is over - if p.extensions&FencedCode != 0 { - if p.fencedCodeBlock(current, false) > 0 { - p.renderParagraph(data[:i]) - return i - } - } - - // if there's a definition list item, prev line is a definition term - if p.extensions&DefinitionLists != 0 { - if p.dliPrefix(current) != 0 { - ret := p.list(data[prev:], ListTypeDefinition) - return ret - } - } - - // if there's a list after this, paragraph is over - if p.extensions&NoEmptyLineBeforeBlock != 0 { - if p.uliPrefix(current) != 0 || - p.oliPrefix(current) != 0 || - p.quotePrefix(current) != 0 || - p.codePrefix(current) != 0 { - p.renderParagraph(data[:i]) - return i - } - } - - // otherwise, scan to the beginning of the next line - nl := bytes.IndexByte(data[i:], '\n') - if nl >= 0 { - i += nl + 1 - } else { - i += len(data[i:]) - } - } - - p.renderParagraph(data[:i]) - return i -} - -func skipChar(data []byte, start int, char byte) int { - i := start - for i < len(data) && data[i] == char { - i++ - } - return i -} - -func skipUntilChar(text []byte, start int, char byte) int { - i := start - for i < len(text) && text[i] != char { - i++ - } - return i -} diff --git a/vendor/github.com/russross/blackfriday/v2/doc.go b/vendor/github.com/russross/blackfriday/v2/doc.go deleted file mode 100644 index 5b3fa987..00000000 --- a/vendor/github.com/russross/blackfriday/v2/doc.go +++ /dev/null @@ -1,18 +0,0 @@ -// Package blackfriday is a markdown processor. -// -// It translates plain text with simple formatting rules into an AST, which can -// then be further processed to HTML (provided by Blackfriday itself) or other -// formats (provided by the community). -// -// The simplest way to invoke Blackfriday is to call the Run function. It will -// take a text input and produce a text output in HTML (or other format). -// -// A slightly more sophisticated way to use Blackfriday is to create a Markdown -// processor and to call Parse, which returns a syntax tree for the input -// document. You can leverage Blackfriday's parsing for content extraction from -// markdown documents. You can assign a custom renderer and set various options -// to the Markdown processor. -// -// If you're interested in calling Blackfriday from command line, see -// https://github.com/russross/blackfriday-tool. -package blackfriday diff --git a/vendor/github.com/russross/blackfriday/v2/esc.go b/vendor/github.com/russross/blackfriday/v2/esc.go deleted file mode 100644 index 6385f27c..00000000 --- a/vendor/github.com/russross/blackfriday/v2/esc.go +++ /dev/null @@ -1,34 +0,0 @@ -package blackfriday - -import ( - "html" - "io" -) - -var htmlEscaper = [256][]byte{ - '&': []byte("&"), - '<': []byte("<"), - '>': []byte(">"), - '"': []byte("""), -} - -func escapeHTML(w io.Writer, s []byte) { - var start, end int - for end < len(s) { - escSeq := htmlEscaper[s[end]] - if escSeq != nil { - w.Write(s[start:end]) - w.Write(escSeq) - start = end + 1 - } - end++ - } - if start < len(s) && end <= len(s) { - w.Write(s[start:end]) - } -} - -func escLink(w io.Writer, text []byte) { - unesc := html.UnescapeString(string(text)) - escapeHTML(w, []byte(unesc)) -} diff --git a/vendor/github.com/russross/blackfriday/v2/go.mod b/vendor/github.com/russross/blackfriday/v2/go.mod deleted file mode 100644 index 620b74e0..00000000 --- a/vendor/github.com/russross/blackfriday/v2/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/russross/blackfriday/v2 diff --git a/vendor/github.com/russross/blackfriday/v2/html.go b/vendor/github.com/russross/blackfriday/v2/html.go deleted file mode 100644 index 284c8718..00000000 --- a/vendor/github.com/russross/blackfriday/v2/html.go +++ /dev/null @@ -1,949 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// -// HTML rendering backend -// -// - -package blackfriday - -import ( - "bytes" - "fmt" - "io" - "regexp" - "strings" -) - -// HTMLFlags control optional behavior of HTML renderer. -type HTMLFlags int - -// HTML renderer configuration options. -const ( - HTMLFlagsNone HTMLFlags = 0 - SkipHTML HTMLFlags = 1 << iota // Skip preformatted HTML blocks - SkipImages // Skip embedded images - SkipLinks // Skip all links - Safelink // Only link to trusted protocols - NofollowLinks // Only link with rel="nofollow" - NoreferrerLinks // Only link with rel="noreferrer" - NoopenerLinks // Only link with rel="noopener" - HrefTargetBlank // Add a blank target - CompletePage // Generate a complete HTML page - UseXHTML // Generate XHTML output instead of HTML - FootnoteReturnLinks // Generate a link at the end of a footnote to return to the source - Smartypants // Enable smart punctuation substitutions - SmartypantsFractions // Enable smart fractions (with Smartypants) - SmartypantsDashes // Enable smart dashes (with Smartypants) - SmartypantsLatexDashes // Enable LaTeX-style dashes (with Smartypants) - SmartypantsAngledQuotes // Enable angled double quotes (with Smartypants) for double quotes rendering - SmartypantsQuotesNBSP // Enable « French guillemets » (with Smartypants) - TOC // Generate a table of contents -) - -var ( - htmlTagRe = regexp.MustCompile("(?i)^" + htmlTag) -) - -const ( - htmlTag = "(?:" + openTag + "|" + closeTag + "|" + htmlComment + "|" + - processingInstruction + "|" + declaration + "|" + cdata + ")" - closeTag = "]" - openTag = "<" + tagName + attribute + "*" + "\\s*/?>" - attribute = "(?:" + "\\s+" + attributeName + attributeValueSpec + "?)" - attributeValue = "(?:" + unquotedValue + "|" + singleQuotedValue + "|" + doubleQuotedValue + ")" - attributeValueSpec = "(?:" + "\\s*=" + "\\s*" + attributeValue + ")" - attributeName = "[a-zA-Z_:][a-zA-Z0-9:._-]*" - cdata = "" - declaration = "]*>" - doubleQuotedValue = "\"[^\"]*\"" - htmlComment = "|" - processingInstruction = "[<][?].*?[?][>]" - singleQuotedValue = "'[^']*'" - tagName = "[A-Za-z][A-Za-z0-9-]*" - unquotedValue = "[^\"'=<>`\\x00-\\x20]+" -) - -// HTMLRendererParameters is a collection of supplementary parameters tweaking -// the behavior of various parts of HTML renderer. -type HTMLRendererParameters struct { - // Prepend this text to each relative URL. - AbsolutePrefix string - // Add this text to each footnote anchor, to ensure uniqueness. - FootnoteAnchorPrefix string - // Show this text inside the tag for a footnote return link, if the - // HTML_FOOTNOTE_RETURN_LINKS flag is enabled. If blank, the string - // [return] is used. - FootnoteReturnLinkContents string - // If set, add this text to the front of each Heading ID, to ensure - // uniqueness. - HeadingIDPrefix string - // If set, add this text to the back of each Heading ID, to ensure uniqueness. - HeadingIDSuffix string - // Increase heading levels: if the offset is 1,

    becomes

    etc. - // Negative offset is also valid. - // Resulting levels are clipped between 1 and 6. - HeadingLevelOffset int - - Title string // Document title (used if CompletePage is set) - CSS string // Optional CSS file URL (used if CompletePage is set) - Icon string // Optional icon file URL (used if CompletePage is set) - - Flags HTMLFlags // Flags allow customizing this renderer's behavior -} - -// HTMLRenderer is a type that implements the Renderer interface for HTML output. -// -// Do not create this directly, instead use the NewHTMLRenderer function. -type HTMLRenderer struct { - HTMLRendererParameters - - closeTag string // how to end singleton tags: either " />" or ">" - - // Track heading IDs to prevent ID collision in a single generation. - headingIDs map[string]int - - lastOutputLen int - disableTags int - - sr *SPRenderer -} - -const ( - xhtmlClose = " />" - htmlClose = ">" -) - -// NewHTMLRenderer creates and configures an HTMLRenderer object, which -// satisfies the Renderer interface. -func NewHTMLRenderer(params HTMLRendererParameters) *HTMLRenderer { - // configure the rendering engine - closeTag := htmlClose - if params.Flags&UseXHTML != 0 { - closeTag = xhtmlClose - } - - if params.FootnoteReturnLinkContents == "" { - params.FootnoteReturnLinkContents = `[return]` - } - - return &HTMLRenderer{ - HTMLRendererParameters: params, - - closeTag: closeTag, - headingIDs: make(map[string]int), - - sr: NewSmartypantsRenderer(params.Flags), - } -} - -func isHTMLTag(tag []byte, tagname string) bool { - found, _ := findHTMLTagPos(tag, tagname) - return found -} - -// Look for a character, but ignore it when it's in any kind of quotes, it -// might be JavaScript -func skipUntilCharIgnoreQuotes(html []byte, start int, char byte) int { - inSingleQuote := false - inDoubleQuote := false - inGraveQuote := false - i := start - for i < len(html) { - switch { - case html[i] == char && !inSingleQuote && !inDoubleQuote && !inGraveQuote: - return i - case html[i] == '\'': - inSingleQuote = !inSingleQuote - case html[i] == '"': - inDoubleQuote = !inDoubleQuote - case html[i] == '`': - inGraveQuote = !inGraveQuote - } - i++ - } - return start -} - -func findHTMLTagPos(tag []byte, tagname string) (bool, int) { - i := 0 - if i < len(tag) && tag[0] != '<' { - return false, -1 - } - i++ - i = skipSpace(tag, i) - - if i < len(tag) && tag[i] == '/' { - i++ - } - - i = skipSpace(tag, i) - j := 0 - for ; i < len(tag); i, j = i+1, j+1 { - if j >= len(tagname) { - break - } - - if strings.ToLower(string(tag[i]))[0] != tagname[j] { - return false, -1 - } - } - - if i == len(tag) { - return false, -1 - } - - rightAngle := skipUntilCharIgnoreQuotes(tag, i, '>') - if rightAngle >= i { - return true, rightAngle - } - - return false, -1 -} - -func skipSpace(tag []byte, i int) int { - for i < len(tag) && isspace(tag[i]) { - i++ - } - return i -} - -func isRelativeLink(link []byte) (yes bool) { - // a tag begin with '#' - if link[0] == '#' { - return true - } - - // link begin with '/' but not '//', the second maybe a protocol relative link - if len(link) >= 2 && link[0] == '/' && link[1] != '/' { - return true - } - - // only the root '/' - if len(link) == 1 && link[0] == '/' { - return true - } - - // current directory : begin with "./" - if bytes.HasPrefix(link, []byte("./")) { - return true - } - - // parent directory : begin with "../" - if bytes.HasPrefix(link, []byte("../")) { - return true - } - - return false -} - -func (r *HTMLRenderer) ensureUniqueHeadingID(id string) string { - for count, found := r.headingIDs[id]; found; count, found = r.headingIDs[id] { - tmp := fmt.Sprintf("%s-%d", id, count+1) - - if _, tmpFound := r.headingIDs[tmp]; !tmpFound { - r.headingIDs[id] = count + 1 - id = tmp - } else { - id = id + "-1" - } - } - - if _, found := r.headingIDs[id]; !found { - r.headingIDs[id] = 0 - } - - return id -} - -func (r *HTMLRenderer) addAbsPrefix(link []byte) []byte { - if r.AbsolutePrefix != "" && isRelativeLink(link) && link[0] != '.' { - newDest := r.AbsolutePrefix - if link[0] != '/' { - newDest += "/" - } - newDest += string(link) - return []byte(newDest) - } - return link -} - -func appendLinkAttrs(attrs []string, flags HTMLFlags, link []byte) []string { - if isRelativeLink(link) { - return attrs - } - val := []string{} - if flags&NofollowLinks != 0 { - val = append(val, "nofollow") - } - if flags&NoreferrerLinks != 0 { - val = append(val, "noreferrer") - } - if flags&NoopenerLinks != 0 { - val = append(val, "noopener") - } - if flags&HrefTargetBlank != 0 { - attrs = append(attrs, "target=\"_blank\"") - } - if len(val) == 0 { - return attrs - } - attr := fmt.Sprintf("rel=%q", strings.Join(val, " ")) - return append(attrs, attr) -} - -func isMailto(link []byte) bool { - return bytes.HasPrefix(link, []byte("mailto:")) -} - -func needSkipLink(flags HTMLFlags, dest []byte) bool { - if flags&SkipLinks != 0 { - return true - } - return flags&Safelink != 0 && !isSafeLink(dest) && !isMailto(dest) -} - -func isSmartypantable(node *Node) bool { - pt := node.Parent.Type - return pt != Link && pt != CodeBlock && pt != Code -} - -func appendLanguageAttr(attrs []string, info []byte) []string { - if len(info) == 0 { - return attrs - } - endOfLang := bytes.IndexAny(info, "\t ") - if endOfLang < 0 { - endOfLang = len(info) - } - return append(attrs, fmt.Sprintf("class=\"language-%s\"", info[:endOfLang])) -} - -func (r *HTMLRenderer) tag(w io.Writer, name []byte, attrs []string) { - w.Write(name) - if len(attrs) > 0 { - w.Write(spaceBytes) - w.Write([]byte(strings.Join(attrs, " "))) - } - w.Write(gtBytes) - r.lastOutputLen = 1 -} - -func footnoteRef(prefix string, node *Node) []byte { - urlFrag := prefix + string(slugify(node.Destination)) - anchor := fmt.Sprintf(`%d`, urlFrag, node.NoteID) - return []byte(fmt.Sprintf(`%s`, urlFrag, anchor)) -} - -func footnoteItem(prefix string, slug []byte) []byte { - return []byte(fmt.Sprintf(`
  • `, prefix, slug)) -} - -func footnoteReturnLink(prefix, returnLink string, slug []byte) []byte { - const format = ` %s` - return []byte(fmt.Sprintf(format, prefix, slug, returnLink)) -} - -func itemOpenCR(node *Node) bool { - if node.Prev == nil { - return false - } - ld := node.Parent.ListData - return !ld.Tight && ld.ListFlags&ListTypeDefinition == 0 -} - -func skipParagraphTags(node *Node) bool { - grandparent := node.Parent.Parent - if grandparent == nil || grandparent.Type != List { - return false - } - tightOrTerm := grandparent.Tight || node.Parent.ListFlags&ListTypeTerm != 0 - return grandparent.Type == List && tightOrTerm -} - -func cellAlignment(align CellAlignFlags) string { - switch align { - case TableAlignmentLeft: - return "left" - case TableAlignmentRight: - return "right" - case TableAlignmentCenter: - return "center" - default: - return "" - } -} - -func (r *HTMLRenderer) out(w io.Writer, text []byte) { - if r.disableTags > 0 { - w.Write(htmlTagRe.ReplaceAll(text, []byte{})) - } else { - w.Write(text) - } - r.lastOutputLen = len(text) -} - -func (r *HTMLRenderer) cr(w io.Writer) { - if r.lastOutputLen > 0 { - r.out(w, nlBytes) - } -} - -var ( - nlBytes = []byte{'\n'} - gtBytes = []byte{'>'} - spaceBytes = []byte{' '} -) - -var ( - brTag = []byte("
    ") - brXHTMLTag = []byte("
    ") - emTag = []byte("") - emCloseTag = []byte("") - strongTag = []byte("") - strongCloseTag = []byte("") - delTag = []byte("") - delCloseTag = []byte("") - ttTag = []byte("") - ttCloseTag = []byte("") - aTag = []byte("") - preTag = []byte("
    ")
    -	preCloseTag        = []byte("
    ") - codeTag = []byte("") - codeCloseTag = []byte("") - pTag = []byte("

    ") - pCloseTag = []byte("

    ") - blockquoteTag = []byte("
    ") - blockquoteCloseTag = []byte("
    ") - hrTag = []byte("
    ") - hrXHTMLTag = []byte("
    ") - ulTag = []byte("
      ") - ulCloseTag = []byte("
    ") - olTag = []byte("
      ") - olCloseTag = []byte("
    ") - dlTag = []byte("
    ") - dlCloseTag = []byte("
    ") - liTag = []byte("
  • ") - liCloseTag = []byte("
  • ") - ddTag = []byte("
    ") - ddCloseTag = []byte("
    ") - dtTag = []byte("
    ") - dtCloseTag = []byte("
    ") - tableTag = []byte("") - tableCloseTag = []byte("
    ") - tdTag = []byte("") - thTag = []byte("") - theadTag = []byte("") - theadCloseTag = []byte("") - tbodyTag = []byte("") - tbodyCloseTag = []byte("") - trTag = []byte("") - trCloseTag = []byte("") - h1Tag = []byte("") - h2Tag = []byte("") - h3Tag = []byte("") - h4Tag = []byte("") - h5Tag = []byte("") - h6Tag = []byte("") - - footnotesDivBytes = []byte("\n
    \n\n") - footnotesCloseDivBytes = []byte("\n
    \n") -) - -func headingTagsFromLevel(level int) ([]byte, []byte) { - if level <= 1 { - return h1Tag, h1CloseTag - } - switch level { - case 2: - return h2Tag, h2CloseTag - case 3: - return h3Tag, h3CloseTag - case 4: - return h4Tag, h4CloseTag - case 5: - return h5Tag, h5CloseTag - } - return h6Tag, h6CloseTag -} - -func (r *HTMLRenderer) outHRTag(w io.Writer) { - if r.Flags&UseXHTML == 0 { - r.out(w, hrTag) - } else { - r.out(w, hrXHTMLTag) - } -} - -// RenderNode is a default renderer of a single node of a syntax tree. For -// block nodes it will be called twice: first time with entering=true, second -// time with entering=false, so that it could know when it's working on an open -// tag and when on close. It writes the result to w. -// -// The return value is a way to tell the calling walker to adjust its walk -// pattern: e.g. it can terminate the traversal by returning Terminate. Or it -// can ask the walker to skip a subtree of this node by returning SkipChildren. -// The typical behavior is to return GoToNext, which asks for the usual -// traversal to the next node. -func (r *HTMLRenderer) RenderNode(w io.Writer, node *Node, entering bool) WalkStatus { - attrs := []string{} - switch node.Type { - case Text: - if r.Flags&Smartypants != 0 { - var tmp bytes.Buffer - escapeHTML(&tmp, node.Literal) - r.sr.Process(w, tmp.Bytes()) - } else { - if node.Parent.Type == Link { - escLink(w, node.Literal) - } else { - escapeHTML(w, node.Literal) - } - } - case Softbreak: - r.cr(w) - // TODO: make it configurable via out(renderer.softbreak) - case Hardbreak: - if r.Flags&UseXHTML == 0 { - r.out(w, brTag) - } else { - r.out(w, brXHTMLTag) - } - r.cr(w) - case Emph: - if entering { - r.out(w, emTag) - } else { - r.out(w, emCloseTag) - } - case Strong: - if entering { - r.out(w, strongTag) - } else { - r.out(w, strongCloseTag) - } - case Del: - if entering { - r.out(w, delTag) - } else { - r.out(w, delCloseTag) - } - case HTMLSpan: - if r.Flags&SkipHTML != 0 { - break - } - r.out(w, node.Literal) - case Link: - // mark it but don't link it if it is not a safe link: no smartypants - dest := node.LinkData.Destination - if needSkipLink(r.Flags, dest) { - if entering { - r.out(w, ttTag) - } else { - r.out(w, ttCloseTag) - } - } else { - if entering { - dest = r.addAbsPrefix(dest) - var hrefBuf bytes.Buffer - hrefBuf.WriteString("href=\"") - escLink(&hrefBuf, dest) - hrefBuf.WriteByte('"') - attrs = append(attrs, hrefBuf.String()) - if node.NoteID != 0 { - r.out(w, footnoteRef(r.FootnoteAnchorPrefix, node)) - break - } - attrs = appendLinkAttrs(attrs, r.Flags, dest) - if len(node.LinkData.Title) > 0 { - var titleBuff bytes.Buffer - titleBuff.WriteString("title=\"") - escapeHTML(&titleBuff, node.LinkData.Title) - titleBuff.WriteByte('"') - attrs = append(attrs, titleBuff.String()) - } - r.tag(w, aTag, attrs) - } else { - if node.NoteID != 0 { - break - } - r.out(w, aCloseTag) - } - } - case Image: - if r.Flags&SkipImages != 0 { - return SkipChildren - } - if entering { - dest := node.LinkData.Destination - dest = r.addAbsPrefix(dest) - if r.disableTags == 0 { - //if options.safe && potentiallyUnsafe(dest) { - //out(w, ``)
-				//} else {
-				r.out(w, []byte(`<img src=`)) - } - } - case Code: - r.out(w, codeTag) - escapeHTML(w, node.Literal) - r.out(w, codeCloseTag) - case Document: - break - case Paragraph: - if skipParagraphTags(node) { - break - } - if entering { - // TODO: untangle this clusterfuck about when the newlines need - // to be added and when not. - if node.Prev != nil { - switch node.Prev.Type { - case HTMLBlock, List, Paragraph, Heading, CodeBlock, BlockQuote, HorizontalRule: - r.cr(w) - } - } - if node.Parent.Type == BlockQuote && node.Prev == nil { - r.cr(w) - } - r.out(w, pTag) - } else { - r.out(w, pCloseTag) - if !(node.Parent.Type == Item && node.Next == nil) { - r.cr(w) - } - } - case BlockQuote: - if entering { - r.cr(w) - r.out(w, blockquoteTag) - } else { - r.out(w, blockquoteCloseTag) - r.cr(w) - } - case HTMLBlock: - if r.Flags&SkipHTML != 0 { - break - } - r.cr(w) - r.out(w, node.Literal) - r.cr(w) - case Heading: - headingLevel := r.HTMLRendererParameters.HeadingLevelOffset + node.Level - openTag, closeTag := headingTagsFromLevel(headingLevel) - if entering { - if node.IsTitleblock { - attrs = append(attrs, `class="title"`) - } - if node.HeadingID != "" { - id := r.ensureUniqueHeadingID(node.HeadingID) - if r.HeadingIDPrefix != "" { - id = r.HeadingIDPrefix + id - } - if r.HeadingIDSuffix != "" { - id = id + r.HeadingIDSuffix - } - attrs = append(attrs, fmt.Sprintf(`id="%s"`, id)) - } - r.cr(w) - r.tag(w, openTag, attrs) - } else { - r.out(w, closeTag) - if !(node.Parent.Type == Item && node.Next == nil) { - r.cr(w) - } - } - case HorizontalRule: - r.cr(w) - r.outHRTag(w) - r.cr(w) - case List: - openTag := ulTag - closeTag := ulCloseTag - if node.ListFlags&ListTypeOrdered != 0 { - openTag = olTag - closeTag = olCloseTag - } - if node.ListFlags&ListTypeDefinition != 0 { - openTag = dlTag - closeTag = dlCloseTag - } - if entering { - if node.IsFootnotesList { - r.out(w, footnotesDivBytes) - r.outHRTag(w) - r.cr(w) - } - r.cr(w) - if node.Parent.Type == Item && node.Parent.Parent.Tight { - r.cr(w) - } - r.tag(w, openTag[:len(openTag)-1], attrs) - r.cr(w) - } else { - r.out(w, closeTag) - //cr(w) - //if node.parent.Type != Item { - // cr(w) - //} - if node.Parent.Type == Item && node.Next != nil { - r.cr(w) - } - if node.Parent.Type == Document || node.Parent.Type == BlockQuote { - r.cr(w) - } - if node.IsFootnotesList { - r.out(w, footnotesCloseDivBytes) - } - } - case Item: - openTag := liTag - closeTag := liCloseTag - if node.ListFlags&ListTypeDefinition != 0 { - openTag = ddTag - closeTag = ddCloseTag - } - if node.ListFlags&ListTypeTerm != 0 { - openTag = dtTag - closeTag = dtCloseTag - } - if entering { - if itemOpenCR(node) { - r.cr(w) - } - if node.ListData.RefLink != nil { - slug := slugify(node.ListData.RefLink) - r.out(w, footnoteItem(r.FootnoteAnchorPrefix, slug)) - break - } - r.out(w, openTag) - } else { - if node.ListData.RefLink != nil { - slug := slugify(node.ListData.RefLink) - if r.Flags&FootnoteReturnLinks != 0 { - r.out(w, footnoteReturnLink(r.FootnoteAnchorPrefix, r.FootnoteReturnLinkContents, slug)) - } - } - r.out(w, closeTag) - r.cr(w) - } - case CodeBlock: - attrs = appendLanguageAttr(attrs, node.Info) - r.cr(w) - r.out(w, preTag) - r.tag(w, codeTag[:len(codeTag)-1], attrs) - escapeHTML(w, node.Literal) - r.out(w, codeCloseTag) - r.out(w, preCloseTag) - if node.Parent.Type != Item { - r.cr(w) - } - case Table: - if entering { - r.cr(w) - r.out(w, tableTag) - } else { - r.out(w, tableCloseTag) - r.cr(w) - } - case TableCell: - openTag := tdTag - closeTag := tdCloseTag - if node.IsHeader { - openTag = thTag - closeTag = thCloseTag - } - if entering { - align := cellAlignment(node.Align) - if align != "" { - attrs = append(attrs, fmt.Sprintf(`align="%s"`, align)) - } - if node.Prev == nil { - r.cr(w) - } - r.tag(w, openTag, attrs) - } else { - r.out(w, closeTag) - r.cr(w) - } - case TableHead: - if entering { - r.cr(w) - r.out(w, theadTag) - } else { - r.out(w, theadCloseTag) - r.cr(w) - } - case TableBody: - if entering { - r.cr(w) - r.out(w, tbodyTag) - // XXX: this is to adhere to a rather silly test. Should fix test. - if node.FirstChild == nil { - r.cr(w) - } - } else { - r.out(w, tbodyCloseTag) - r.cr(w) - } - case TableRow: - if entering { - r.cr(w) - r.out(w, trTag) - } else { - r.out(w, trCloseTag) - r.cr(w) - } - default: - panic("Unknown node type " + node.Type.String()) - } - return GoToNext -} - -// RenderHeader writes HTML document preamble and TOC if requested. -func (r *HTMLRenderer) RenderHeader(w io.Writer, ast *Node) { - r.writeDocumentHeader(w) - if r.Flags&TOC != 0 { - r.writeTOC(w, ast) - } -} - -// RenderFooter writes HTML document footer. -func (r *HTMLRenderer) RenderFooter(w io.Writer, ast *Node) { - if r.Flags&CompletePage == 0 { - return - } - io.WriteString(w, "\n\n\n") -} - -func (r *HTMLRenderer) writeDocumentHeader(w io.Writer) { - if r.Flags&CompletePage == 0 { - return - } - ending := "" - if r.Flags&UseXHTML != 0 { - io.WriteString(w, "\n") - io.WriteString(w, "\n") - ending = " /" - } else { - io.WriteString(w, "\n") - io.WriteString(w, "\n") - } - io.WriteString(w, "\n") - io.WriteString(w, " ") - if r.Flags&Smartypants != 0 { - r.sr.Process(w, []byte(r.Title)) - } else { - escapeHTML(w, []byte(r.Title)) - } - io.WriteString(w, "\n") - io.WriteString(w, " \n") - io.WriteString(w, " \n") - if r.CSS != "" { - io.WriteString(w, " \n") - } - if r.Icon != "" { - io.WriteString(w, " \n") - } - io.WriteString(w, "\n") - io.WriteString(w, "\n\n") -} - -func (r *HTMLRenderer) writeTOC(w io.Writer, ast *Node) { - buf := bytes.Buffer{} - - inHeading := false - tocLevel := 0 - headingCount := 0 - - ast.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Heading && !node.HeadingData.IsTitleblock { - inHeading = entering - if entering { - node.HeadingID = fmt.Sprintf("toc_%d", headingCount) - if node.Level == tocLevel { - buf.WriteString("

  • \n\n
  • ") - } else if node.Level < tocLevel { - for node.Level < tocLevel { - tocLevel-- - buf.WriteString("
  • \n") - } - buf.WriteString("\n\n
  • ") - } else { - for node.Level > tocLevel { - tocLevel++ - buf.WriteString("\n") - } - - if buf.Len() > 0 { - io.WriteString(w, "\n") - } - r.lastOutputLen = buf.Len() -} diff --git a/vendor/github.com/russross/blackfriday/v2/inline.go b/vendor/github.com/russross/blackfriday/v2/inline.go deleted file mode 100644 index 4ed29079..00000000 --- a/vendor/github.com/russross/blackfriday/v2/inline.go +++ /dev/null @@ -1,1228 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// Functions to parse inline elements. -// - -package blackfriday - -import ( - "bytes" - "regexp" - "strconv" -) - -var ( - urlRe = `((https?|ftp):\/\/|\/)[-A-Za-z0-9+&@#\/%?=~_|!:,.;\(\)]+` - anchorRe = regexp.MustCompile(`^(]+")?\s?>` + urlRe + `<\/a>)`) - - // https://www.w3.org/TR/html5/syntax.html#character-references - // highest unicode code point in 17 planes (2^20): 1,114,112d = - // 7 dec digits or 6 hex digits - // named entity references can be 2-31 characters with stuff like < - // at one end and ∳ at the other. There - // are also sometimes numbers at the end, although this isn't inherent - // in the specification; there are never numbers anywhere else in - // current character references, though; see ¾ and ▒, etc. - // https://www.w3.org/TR/html5/syntax.html#named-character-references - // - // entity := "&" (named group | number ref) ";" - // named group := [a-zA-Z]{2,31}[0-9]{0,2} - // number ref := "#" (dec ref | hex ref) - // dec ref := [0-9]{1,7} - // hex ref := ("x" | "X") [0-9a-fA-F]{1,6} - htmlEntityRe = regexp.MustCompile(`&([a-zA-Z]{2,31}[0-9]{0,2}|#([0-9]{1,7}|[xX][0-9a-fA-F]{1,6}));`) -) - -// Functions to parse text within a block -// Each function returns the number of chars taken care of -// data is the complete block being rendered -// offset is the number of valid chars before the current cursor - -func (p *Markdown) inline(currBlock *Node, data []byte) { - // handlers might call us recursively: enforce a maximum depth - if p.nesting >= p.maxNesting || len(data) == 0 { - return - } - p.nesting++ - beg, end := 0, 0 - for end < len(data) { - handler := p.inlineCallback[data[end]] - if handler != nil { - if consumed, node := handler(p, data, end); consumed == 0 { - // No action from the callback. - end++ - } else { - // Copy inactive chars into the output. - currBlock.AppendChild(text(data[beg:end])) - if node != nil { - currBlock.AppendChild(node) - } - // Skip past whatever the callback used. - beg = end + consumed - end = beg - } - } else { - end++ - } - } - if beg < len(data) { - if data[end-1] == '\n' { - end-- - } - currBlock.AppendChild(text(data[beg:end])) - } - p.nesting-- -} - -// single and double emphasis parsing -func emphasis(p *Markdown, data []byte, offset int) (int, *Node) { - data = data[offset:] - c := data[0] - - if len(data) > 2 && data[1] != c { - // whitespace cannot follow an opening emphasis; - // strikethrough only takes two characters '~~' - if c == '~' || isspace(data[1]) { - return 0, nil - } - ret, node := helperEmphasis(p, data[1:], c) - if ret == 0 { - return 0, nil - } - - return ret + 1, node - } - - if len(data) > 3 && data[1] == c && data[2] != c { - if isspace(data[2]) { - return 0, nil - } - ret, node := helperDoubleEmphasis(p, data[2:], c) - if ret == 0 { - return 0, nil - } - - return ret + 2, node - } - - if len(data) > 4 && data[1] == c && data[2] == c && data[3] != c { - if c == '~' || isspace(data[3]) { - return 0, nil - } - ret, node := helperTripleEmphasis(p, data, 3, c) - if ret == 0 { - return 0, nil - } - - return ret + 3, node - } - - return 0, nil -} - -func codeSpan(p *Markdown, data []byte, offset int) (int, *Node) { - data = data[offset:] - - nb := 0 - - // count the number of backticks in the delimiter - for nb < len(data) && data[nb] == '`' { - nb++ - } - - // find the next delimiter - i, end := 0, 0 - for end = nb; end < len(data) && i < nb; end++ { - if data[end] == '`' { - i++ - } else { - i = 0 - } - } - - // no matching delimiter? - if i < nb && end >= len(data) { - return 0, nil - } - - // trim outside whitespace - fBegin := nb - for fBegin < end && data[fBegin] == ' ' { - fBegin++ - } - - fEnd := end - nb - for fEnd > fBegin && data[fEnd-1] == ' ' { - fEnd-- - } - - // render the code span - if fBegin != fEnd { - code := NewNode(Code) - code.Literal = data[fBegin:fEnd] - return end, code - } - - return end, nil -} - -// newline preceded by two spaces becomes
    -func maybeLineBreak(p *Markdown, data []byte, offset int) (int, *Node) { - origOffset := offset - for offset < len(data) && data[offset] == ' ' { - offset++ - } - - if offset < len(data) && data[offset] == '\n' { - if offset-origOffset >= 2 { - return offset - origOffset + 1, NewNode(Hardbreak) - } - return offset - origOffset, nil - } - return 0, nil -} - -// newline without two spaces works when HardLineBreak is enabled -func lineBreak(p *Markdown, data []byte, offset int) (int, *Node) { - if p.extensions&HardLineBreak != 0 { - return 1, NewNode(Hardbreak) - } - return 0, nil -} - -type linkType int - -const ( - linkNormal linkType = iota - linkImg - linkDeferredFootnote - linkInlineFootnote -) - -func isReferenceStyleLink(data []byte, pos int, t linkType) bool { - if t == linkDeferredFootnote { - return false - } - return pos < len(data)-1 && data[pos] == '[' && data[pos+1] != '^' -} - -func maybeImage(p *Markdown, data []byte, offset int) (int, *Node) { - if offset < len(data)-1 && data[offset+1] == '[' { - return link(p, data, offset) - } - return 0, nil -} - -func maybeInlineFootnote(p *Markdown, data []byte, offset int) (int, *Node) { - if offset < len(data)-1 && data[offset+1] == '[' { - return link(p, data, offset) - } - return 0, nil -} - -// '[': parse a link or an image or a footnote -func link(p *Markdown, data []byte, offset int) (int, *Node) { - // no links allowed inside regular links, footnote, and deferred footnotes - if p.insideLink && (offset > 0 && data[offset-1] == '[' || len(data)-1 > offset && data[offset+1] == '^') { - return 0, nil - } - - var t linkType - switch { - // special case: ![^text] == deferred footnote (that follows something with - // an exclamation point) - case p.extensions&Footnotes != 0 && len(data)-1 > offset && data[offset+1] == '^': - t = linkDeferredFootnote - // ![alt] == image - case offset >= 0 && data[offset] == '!': - t = linkImg - offset++ - // ^[text] == inline footnote - // [^refId] == deferred footnote - case p.extensions&Footnotes != 0: - if offset >= 0 && data[offset] == '^' { - t = linkInlineFootnote - offset++ - } else if len(data)-1 > offset && data[offset+1] == '^' { - t = linkDeferredFootnote - } - // [text] == regular link - default: - t = linkNormal - } - - data = data[offset:] - - var ( - i = 1 - noteID int - title, link, altContent []byte - textHasNl = false - ) - - if t == linkDeferredFootnote { - i++ - } - - // look for the matching closing bracket - for level := 1; level > 0 && i < len(data); i++ { - switch { - case data[i] == '\n': - textHasNl = true - - case data[i-1] == '\\': - continue - - case data[i] == '[': - level++ - - case data[i] == ']': - level-- - if level <= 0 { - i-- // compensate for extra i++ in for loop - } - } - } - - if i >= len(data) { - return 0, nil - } - - txtE := i - i++ - var footnoteNode *Node - - // skip any amount of whitespace or newline - // (this is much more lax than original markdown syntax) - for i < len(data) && isspace(data[i]) { - i++ - } - - // inline style link - switch { - case i < len(data) && data[i] == '(': - // skip initial whitespace - i++ - - for i < len(data) && isspace(data[i]) { - i++ - } - - linkB := i - - // look for link end: ' " ) - findlinkend: - for i < len(data) { - switch { - case data[i] == '\\': - i += 2 - - case data[i] == ')' || data[i] == '\'' || data[i] == '"': - break findlinkend - - default: - i++ - } - } - - if i >= len(data) { - return 0, nil - } - linkE := i - - // look for title end if present - titleB, titleE := 0, 0 - if data[i] == '\'' || data[i] == '"' { - i++ - titleB = i - - findtitleend: - for i < len(data) { - switch { - case data[i] == '\\': - i += 2 - - case data[i] == ')': - break findtitleend - - default: - i++ - } - } - - if i >= len(data) { - return 0, nil - } - - // skip whitespace after title - titleE = i - 1 - for titleE > titleB && isspace(data[titleE]) { - titleE-- - } - - // check for closing quote presence - if data[titleE] != '\'' && data[titleE] != '"' { - titleB, titleE = 0, 0 - linkE = i - } - } - - // remove whitespace at the end of the link - for linkE > linkB && isspace(data[linkE-1]) { - linkE-- - } - - // remove optional angle brackets around the link - if data[linkB] == '<' { - linkB++ - } - if data[linkE-1] == '>' { - linkE-- - } - - // build escaped link and title - if linkE > linkB { - link = data[linkB:linkE] - } - - if titleE > titleB { - title = data[titleB:titleE] - } - - i++ - - // reference style link - case isReferenceStyleLink(data, i, t): - var id []byte - altContentConsidered := false - - // look for the id - i++ - linkB := i - for i < len(data) && data[i] != ']' { - i++ - } - if i >= len(data) { - return 0, nil - } - linkE := i - - // find the reference - if linkB == linkE { - if textHasNl { - var b bytes.Buffer - - for j := 1; j < txtE; j++ { - switch { - case data[j] != '\n': - b.WriteByte(data[j]) - case data[j-1] != ' ': - b.WriteByte(' ') - } - } - - id = b.Bytes() - } else { - id = data[1:txtE] - altContentConsidered = true - } - } else { - id = data[linkB:linkE] - } - - // find the reference with matching id - lr, ok := p.getRef(string(id)) - if !ok { - return 0, nil - } - - // keep link and title from reference - link = lr.link - title = lr.title - if altContentConsidered { - altContent = lr.text - } - i++ - - // shortcut reference style link or reference or inline footnote - default: - var id []byte - - // craft the id - if textHasNl { - var b bytes.Buffer - - for j := 1; j < txtE; j++ { - switch { - case data[j] != '\n': - b.WriteByte(data[j]) - case data[j-1] != ' ': - b.WriteByte(' ') - } - } - - id = b.Bytes() - } else { - if t == linkDeferredFootnote { - id = data[2:txtE] // get rid of the ^ - } else { - id = data[1:txtE] - } - } - - footnoteNode = NewNode(Item) - if t == linkInlineFootnote { - // create a new reference - noteID = len(p.notes) + 1 - - var fragment []byte - if len(id) > 0 { - if len(id) < 16 { - fragment = make([]byte, len(id)) - } else { - fragment = make([]byte, 16) - } - copy(fragment, slugify(id)) - } else { - fragment = append([]byte("footnote-"), []byte(strconv.Itoa(noteID))...) - } - - ref := &reference{ - noteID: noteID, - hasBlock: false, - link: fragment, - title: id, - footnote: footnoteNode, - } - - p.notes = append(p.notes, ref) - - link = ref.link - title = ref.title - } else { - // find the reference with matching id - lr, ok := p.getRef(string(id)) - if !ok { - return 0, nil - } - - if t == linkDeferredFootnote { - lr.noteID = len(p.notes) + 1 - lr.footnote = footnoteNode - p.notes = append(p.notes, lr) - } - - // keep link and title from reference - link = lr.link - // if inline footnote, title == footnote contents - title = lr.title - noteID = lr.noteID - } - - // rewind the whitespace - i = txtE + 1 - } - - var uLink []byte - if t == linkNormal || t == linkImg { - if len(link) > 0 { - var uLinkBuf bytes.Buffer - unescapeText(&uLinkBuf, link) - uLink = uLinkBuf.Bytes() - } - - // links need something to click on and somewhere to go - if len(uLink) == 0 || (t == linkNormal && txtE <= 1) { - return 0, nil - } - } - - // call the relevant rendering function - var linkNode *Node - switch t { - case linkNormal: - linkNode = NewNode(Link) - linkNode.Destination = normalizeURI(uLink) - linkNode.Title = title - if len(altContent) > 0 { - linkNode.AppendChild(text(altContent)) - } else { - // links cannot contain other links, so turn off link parsing - // temporarily and recurse - insideLink := p.insideLink - p.insideLink = true - p.inline(linkNode, data[1:txtE]) - p.insideLink = insideLink - } - - case linkImg: - linkNode = NewNode(Image) - linkNode.Destination = uLink - linkNode.Title = title - linkNode.AppendChild(text(data[1:txtE])) - i++ - - case linkInlineFootnote, linkDeferredFootnote: - linkNode = NewNode(Link) - linkNode.Destination = link - linkNode.Title = title - linkNode.NoteID = noteID - linkNode.Footnote = footnoteNode - if t == linkInlineFootnote { - i++ - } - - default: - return 0, nil - } - - return i, linkNode -} - -func (p *Markdown) inlineHTMLComment(data []byte) int { - if len(data) < 5 { - return 0 - } - if data[0] != '<' || data[1] != '!' || data[2] != '-' || data[3] != '-' { - return 0 - } - i := 5 - // scan for an end-of-comment marker, across lines if necessary - for i < len(data) && !(data[i-2] == '-' && data[i-1] == '-' && data[i] == '>') { - i++ - } - // no end-of-comment marker - if i >= len(data) { - return 0 - } - return i + 1 -} - -func stripMailto(link []byte) []byte { - if bytes.HasPrefix(link, []byte("mailto://")) { - return link[9:] - } else if bytes.HasPrefix(link, []byte("mailto:")) { - return link[7:] - } else { - return link - } -} - -// autolinkType specifies a kind of autolink that gets detected. -type autolinkType int - -// These are the possible flag values for the autolink renderer. -const ( - notAutolink autolinkType = iota - normalAutolink - emailAutolink -) - -// '<' when tags or autolinks are allowed -func leftAngle(p *Markdown, data []byte, offset int) (int, *Node) { - data = data[offset:] - altype, end := tagLength(data) - if size := p.inlineHTMLComment(data); size > 0 { - end = size - } - if end > 2 { - if altype != notAutolink { - var uLink bytes.Buffer - unescapeText(&uLink, data[1:end+1-2]) - if uLink.Len() > 0 { - link := uLink.Bytes() - node := NewNode(Link) - node.Destination = link - if altype == emailAutolink { - node.Destination = append([]byte("mailto:"), link...) - } - node.AppendChild(text(stripMailto(link))) - return end, node - } - } else { - htmlTag := NewNode(HTMLSpan) - htmlTag.Literal = data[:end] - return end, htmlTag - } - } - - return end, nil -} - -// '\\' backslash escape -var escapeChars = []byte("\\`*_{}[]()#+-.!:|&<>~") - -func escape(p *Markdown, data []byte, offset int) (int, *Node) { - data = data[offset:] - - if len(data) > 1 { - if p.extensions&BackslashLineBreak != 0 && data[1] == '\n' { - return 2, NewNode(Hardbreak) - } - if bytes.IndexByte(escapeChars, data[1]) < 0 { - return 0, nil - } - - return 2, text(data[1:2]) - } - - return 2, nil -} - -func unescapeText(ob *bytes.Buffer, src []byte) { - i := 0 - for i < len(src) { - org := i - for i < len(src) && src[i] != '\\' { - i++ - } - - if i > org { - ob.Write(src[org:i]) - } - - if i+1 >= len(src) { - break - } - - ob.WriteByte(src[i+1]) - i += 2 - } -} - -// '&' escaped when it doesn't belong to an entity -// valid entities are assumed to be anything matching &#?[A-Za-z0-9]+; -func entity(p *Markdown, data []byte, offset int) (int, *Node) { - data = data[offset:] - - end := 1 - - if end < len(data) && data[end] == '#' { - end++ - } - - for end < len(data) && isalnum(data[end]) { - end++ - } - - if end < len(data) && data[end] == ';' { - end++ // real entity - } else { - return 0, nil // lone '&' - } - - ent := data[:end] - // undo & escaping or it will be converted to &amp; by another - // escaper in the renderer - if bytes.Equal(ent, []byte("&")) { - ent = []byte{'&'} - } - - return end, text(ent) -} - -func linkEndsWithEntity(data []byte, linkEnd int) bool { - entityRanges := htmlEntityRe.FindAllIndex(data[:linkEnd], -1) - return entityRanges != nil && entityRanges[len(entityRanges)-1][1] == linkEnd -} - -// hasPrefixCaseInsensitive is a custom implementation of -// strings.HasPrefix(strings.ToLower(s), prefix) -// we rolled our own because ToLower pulls in a huge machinery of lowercasing -// anything from Unicode and that's very slow. Since this func will only be -// used on ASCII protocol prefixes, we can take shortcuts. -func hasPrefixCaseInsensitive(s, prefix []byte) bool { - if len(s) < len(prefix) { - return false - } - delta := byte('a' - 'A') - for i, b := range prefix { - if b != s[i] && b != s[i]+delta { - return false - } - } - return true -} - -var protocolPrefixes = [][]byte{ - []byte("http://"), - []byte("https://"), - []byte("ftp://"), - []byte("file://"), - []byte("mailto:"), -} - -const shortestPrefix = 6 // len("ftp://"), the shortest of the above - -func maybeAutoLink(p *Markdown, data []byte, offset int) (int, *Node) { - // quick check to rule out most false hits - if p.insideLink || len(data) < offset+shortestPrefix { - return 0, nil - } - for _, prefix := range protocolPrefixes { - endOfHead := offset + 8 // 8 is the len() of the longest prefix - if endOfHead > len(data) { - endOfHead = len(data) - } - if hasPrefixCaseInsensitive(data[offset:endOfHead], prefix) { - return autoLink(p, data, offset) - } - } - return 0, nil -} - -func autoLink(p *Markdown, data []byte, offset int) (int, *Node) { - // Now a more expensive check to see if we're not inside an anchor element - anchorStart := offset - offsetFromAnchor := 0 - for anchorStart > 0 && data[anchorStart] != '<' { - anchorStart-- - offsetFromAnchor++ - } - - anchorStr := anchorRe.Find(data[anchorStart:]) - if anchorStr != nil { - anchorClose := NewNode(HTMLSpan) - anchorClose.Literal = anchorStr[offsetFromAnchor:] - return len(anchorStr) - offsetFromAnchor, anchorClose - } - - // scan backward for a word boundary - rewind := 0 - for offset-rewind > 0 && rewind <= 7 && isletter(data[offset-rewind-1]) { - rewind++ - } - if rewind > 6 { // longest supported protocol is "mailto" which has 6 letters - return 0, nil - } - - origData := data - data = data[offset-rewind:] - - if !isSafeLink(data) { - return 0, nil - } - - linkEnd := 0 - for linkEnd < len(data) && !isEndOfLink(data[linkEnd]) { - linkEnd++ - } - - // Skip punctuation at the end of the link - if (data[linkEnd-1] == '.' || data[linkEnd-1] == ',') && data[linkEnd-2] != '\\' { - linkEnd-- - } - - // But don't skip semicolon if it's a part of escaped entity: - if data[linkEnd-1] == ';' && data[linkEnd-2] != '\\' && !linkEndsWithEntity(data, linkEnd) { - linkEnd-- - } - - // See if the link finishes with a punctuation sign that can be closed. - var copen byte - switch data[linkEnd-1] { - case '"': - copen = '"' - case '\'': - copen = '\'' - case ')': - copen = '(' - case ']': - copen = '[' - case '}': - copen = '{' - default: - copen = 0 - } - - if copen != 0 { - bufEnd := offset - rewind + linkEnd - 2 - - openDelim := 1 - - /* Try to close the final punctuation sign in this same line; - * if we managed to close it outside of the URL, that means that it's - * not part of the URL. If it closes inside the URL, that means it - * is part of the URL. - * - * Examples: - * - * foo http://www.pokemon.com/Pikachu_(Electric) bar - * => http://www.pokemon.com/Pikachu_(Electric) - * - * foo (http://www.pokemon.com/Pikachu_(Electric)) bar - * => http://www.pokemon.com/Pikachu_(Electric) - * - * foo http://www.pokemon.com/Pikachu_(Electric)) bar - * => http://www.pokemon.com/Pikachu_(Electric)) - * - * (foo http://www.pokemon.com/Pikachu_(Electric)) bar - * => foo http://www.pokemon.com/Pikachu_(Electric) - */ - - for bufEnd >= 0 && origData[bufEnd] != '\n' && openDelim != 0 { - if origData[bufEnd] == data[linkEnd-1] { - openDelim++ - } - - if origData[bufEnd] == copen { - openDelim-- - } - - bufEnd-- - } - - if openDelim == 0 { - linkEnd-- - } - } - - var uLink bytes.Buffer - unescapeText(&uLink, data[:linkEnd]) - - if uLink.Len() > 0 { - node := NewNode(Link) - node.Destination = uLink.Bytes() - node.AppendChild(text(uLink.Bytes())) - return linkEnd, node - } - - return linkEnd, nil -} - -func isEndOfLink(char byte) bool { - return isspace(char) || char == '<' -} - -var validUris = [][]byte{[]byte("http://"), []byte("https://"), []byte("ftp://"), []byte("mailto://")} -var validPaths = [][]byte{[]byte("/"), []byte("./"), []byte("../")} - -func isSafeLink(link []byte) bool { - for _, path := range validPaths { - if len(link) >= len(path) && bytes.Equal(link[:len(path)], path) { - if len(link) == len(path) { - return true - } else if isalnum(link[len(path)]) { - return true - } - } - } - - for _, prefix := range validUris { - // TODO: handle unicode here - // case-insensitive prefix test - if len(link) > len(prefix) && bytes.Equal(bytes.ToLower(link[:len(prefix)]), prefix) && isalnum(link[len(prefix)]) { - return true - } - } - - return false -} - -// return the length of the given tag, or 0 is it's not valid -func tagLength(data []byte) (autolink autolinkType, end int) { - var i, j int - - // a valid tag can't be shorter than 3 chars - if len(data) < 3 { - return notAutolink, 0 - } - - // begins with a '<' optionally followed by '/', followed by letter or number - if data[0] != '<' { - return notAutolink, 0 - } - if data[1] == '/' { - i = 2 - } else { - i = 1 - } - - if !isalnum(data[i]) { - return notAutolink, 0 - } - - // scheme test - autolink = notAutolink - - // try to find the beginning of an URI - for i < len(data) && (isalnum(data[i]) || data[i] == '.' || data[i] == '+' || data[i] == '-') { - i++ - } - - if i > 1 && i < len(data) && data[i] == '@' { - if j = isMailtoAutoLink(data[i:]); j != 0 { - return emailAutolink, i + j - } - } - - if i > 2 && i < len(data) && data[i] == ':' { - autolink = normalAutolink - i++ - } - - // complete autolink test: no whitespace or ' or " - switch { - case i >= len(data): - autolink = notAutolink - case autolink != notAutolink: - j = i - - for i < len(data) { - if data[i] == '\\' { - i += 2 - } else if data[i] == '>' || data[i] == '\'' || data[i] == '"' || isspace(data[i]) { - break - } else { - i++ - } - - } - - if i >= len(data) { - return autolink, 0 - } - if i > j && data[i] == '>' { - return autolink, i + 1 - } - - // one of the forbidden chars has been found - autolink = notAutolink - } - i += bytes.IndexByte(data[i:], '>') - if i < 0 { - return autolink, 0 - } - return autolink, i + 1 -} - -// look for the address part of a mail autolink and '>' -// this is less strict than the original markdown e-mail address matching -func isMailtoAutoLink(data []byte) int { - nb := 0 - - // address is assumed to be: [-@._a-zA-Z0-9]+ with exactly one '@' - for i := 0; i < len(data); i++ { - if isalnum(data[i]) { - continue - } - - switch data[i] { - case '@': - nb++ - - case '-', '.', '_': - break - - case '>': - if nb == 1 { - return i + 1 - } - return 0 - default: - return 0 - } - } - - return 0 -} - -// look for the next emph char, skipping other constructs -func helperFindEmphChar(data []byte, c byte) int { - i := 0 - - for i < len(data) { - for i < len(data) && data[i] != c && data[i] != '`' && data[i] != '[' { - i++ - } - if i >= len(data) { - return 0 - } - // do not count escaped chars - if i != 0 && data[i-1] == '\\' { - i++ - continue - } - if data[i] == c { - return i - } - - if data[i] == '`' { - // skip a code span - tmpI := 0 - i++ - for i < len(data) && data[i] != '`' { - if tmpI == 0 && data[i] == c { - tmpI = i - } - i++ - } - if i >= len(data) { - return tmpI - } - i++ - } else if data[i] == '[' { - // skip a link - tmpI := 0 - i++ - for i < len(data) && data[i] != ']' { - if tmpI == 0 && data[i] == c { - tmpI = i - } - i++ - } - i++ - for i < len(data) && (data[i] == ' ' || data[i] == '\n') { - i++ - } - if i >= len(data) { - return tmpI - } - if data[i] != '[' && data[i] != '(' { // not a link - if tmpI > 0 { - return tmpI - } - continue - } - cc := data[i] - i++ - for i < len(data) && data[i] != cc { - if tmpI == 0 && data[i] == c { - return i - } - i++ - } - if i >= len(data) { - return tmpI - } - i++ - } - } - return 0 -} - -func helperEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { - i := 0 - - // skip one symbol if coming from emph3 - if len(data) > 1 && data[0] == c && data[1] == c { - i = 1 - } - - for i < len(data) { - length := helperFindEmphChar(data[i:], c) - if length == 0 { - return 0, nil - } - i += length - if i >= len(data) { - return 0, nil - } - - if i+1 < len(data) && data[i+1] == c { - i++ - continue - } - - if data[i] == c && !isspace(data[i-1]) { - - if p.extensions&NoIntraEmphasis != 0 { - if !(i+1 == len(data) || isspace(data[i+1]) || ispunct(data[i+1])) { - continue - } - } - - emph := NewNode(Emph) - p.inline(emph, data[:i]) - return i + 1, emph - } - } - - return 0, nil -} - -func helperDoubleEmphasis(p *Markdown, data []byte, c byte) (int, *Node) { - i := 0 - - for i < len(data) { - length := helperFindEmphChar(data[i:], c) - if length == 0 { - return 0, nil - } - i += length - - if i+1 < len(data) && data[i] == c && data[i+1] == c && i > 0 && !isspace(data[i-1]) { - nodeType := Strong - if c == '~' { - nodeType = Del - } - node := NewNode(nodeType) - p.inline(node, data[:i]) - return i + 2, node - } - i++ - } - return 0, nil -} - -func helperTripleEmphasis(p *Markdown, data []byte, offset int, c byte) (int, *Node) { - i := 0 - origData := data - data = data[offset:] - - for i < len(data) { - length := helperFindEmphChar(data[i:], c) - if length == 0 { - return 0, nil - } - i += length - - // skip whitespace preceded symbols - if data[i] != c || isspace(data[i-1]) { - continue - } - - switch { - case i+2 < len(data) && data[i+1] == c && data[i+2] == c: - // triple symbol found - strong := NewNode(Strong) - em := NewNode(Emph) - strong.AppendChild(em) - p.inline(em, data[:i]) - return i + 3, strong - case (i+1 < len(data) && data[i+1] == c): - // double symbol found, hand over to emph1 - length, node := helperEmphasis(p, origData[offset-2:], c) - if length == 0 { - return 0, nil - } - return length - 2, node - default: - // single symbol found, hand over to emph2 - length, node := helperDoubleEmphasis(p, origData[offset-1:], c) - if length == 0 { - return 0, nil - } - return length - 1, node - } - } - return 0, nil -} - -func text(s []byte) *Node { - node := NewNode(Text) - node.Literal = s - return node -} - -func normalizeURI(s []byte) []byte { - return s // TODO: implement -} diff --git a/vendor/github.com/russross/blackfriday/v2/markdown.go b/vendor/github.com/russross/blackfriday/v2/markdown.go deleted file mode 100644 index 58d2e453..00000000 --- a/vendor/github.com/russross/blackfriday/v2/markdown.go +++ /dev/null @@ -1,950 +0,0 @@ -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. - -package blackfriday - -import ( - "bytes" - "fmt" - "io" - "strings" - "unicode/utf8" -) - -// -// Markdown parsing and processing -// - -// Version string of the package. Appears in the rendered document when -// CompletePage flag is on. -const Version = "2.0" - -// Extensions is a bitwise or'ed collection of enabled Blackfriday's -// extensions. -type Extensions int - -// These are the supported markdown parsing extensions. -// OR these values together to select multiple extensions. -const ( - NoExtensions Extensions = 0 - NoIntraEmphasis Extensions = 1 << iota // Ignore emphasis markers inside words - Tables // Render tables - FencedCode // Render fenced code blocks - Autolink // Detect embedded URLs that are not explicitly marked - Strikethrough // Strikethrough text using ~~test~~ - LaxHTMLBlocks // Loosen up HTML block parsing rules - SpaceHeadings // Be strict about prefix heading rules - HardLineBreak // Translate newlines into line breaks - TabSizeEight // Expand tabs to eight spaces instead of four - Footnotes // Pandoc-style footnotes - NoEmptyLineBeforeBlock // No need to insert an empty line to start a (code, quote, ordered list, unordered list) block - HeadingIDs // specify heading IDs with {#id} - Titleblock // Titleblock ala pandoc - AutoHeadingIDs // Create the heading ID from the text - BackslashLineBreak // Translate trailing backslashes into line breaks - DefinitionLists // Render definition lists - - CommonHTMLFlags HTMLFlags = UseXHTML | Smartypants | - SmartypantsFractions | SmartypantsDashes | SmartypantsLatexDashes - - CommonExtensions Extensions = NoIntraEmphasis | Tables | FencedCode | - Autolink | Strikethrough | SpaceHeadings | HeadingIDs | - BackslashLineBreak | DefinitionLists -) - -// ListType contains bitwise or'ed flags for list and list item objects. -type ListType int - -// These are the possible flag values for the ListItem renderer. -// Multiple flag values may be ORed together. -// These are mostly of interest if you are writing a new output format. -const ( - ListTypeOrdered ListType = 1 << iota - ListTypeDefinition - ListTypeTerm - - ListItemContainsBlock - ListItemBeginningOfList // TODO: figure out if this is of any use now - ListItemEndOfList -) - -// CellAlignFlags holds a type of alignment in a table cell. -type CellAlignFlags int - -// These are the possible flag values for the table cell renderer. -// Only a single one of these values will be used; they are not ORed together. -// These are mostly of interest if you are writing a new output format. -const ( - TableAlignmentLeft CellAlignFlags = 1 << iota - TableAlignmentRight - TableAlignmentCenter = (TableAlignmentLeft | TableAlignmentRight) -) - -// The size of a tab stop. -const ( - TabSizeDefault = 4 - TabSizeDouble = 8 -) - -// blockTags is a set of tags that are recognized as HTML block tags. -// Any of these can be included in markdown text without special escaping. -var blockTags = map[string]struct{}{ - "blockquote": {}, - "del": {}, - "div": {}, - "dl": {}, - "fieldset": {}, - "form": {}, - "h1": {}, - "h2": {}, - "h3": {}, - "h4": {}, - "h5": {}, - "h6": {}, - "iframe": {}, - "ins": {}, - "math": {}, - "noscript": {}, - "ol": {}, - "pre": {}, - "p": {}, - "script": {}, - "style": {}, - "table": {}, - "ul": {}, - - // HTML5 - "address": {}, - "article": {}, - "aside": {}, - "canvas": {}, - "figcaption": {}, - "figure": {}, - "footer": {}, - "header": {}, - "hgroup": {}, - "main": {}, - "nav": {}, - "output": {}, - "progress": {}, - "section": {}, - "video": {}, -} - -// Renderer is the rendering interface. This is mostly of interest if you are -// implementing a new rendering format. -// -// Only an HTML implementation is provided in this repository, see the README -// for external implementations. -type Renderer interface { - // RenderNode is the main rendering method. It will be called once for - // every leaf node and twice for every non-leaf node (first with - // entering=true, then with entering=false). The method should write its - // rendition of the node to the supplied writer w. - RenderNode(w io.Writer, node *Node, entering bool) WalkStatus - - // RenderHeader is a method that allows the renderer to produce some - // content preceding the main body of the output document. The header is - // understood in the broad sense here. For example, the default HTML - // renderer will write not only the HTML document preamble, but also the - // table of contents if it was requested. - // - // The method will be passed an entire document tree, in case a particular - // implementation needs to inspect it to produce output. - // - // The output should be written to the supplied writer w. If your - // implementation has no header to write, supply an empty implementation. - RenderHeader(w io.Writer, ast *Node) - - // RenderFooter is a symmetric counterpart of RenderHeader. - RenderFooter(w io.Writer, ast *Node) -} - -// Callback functions for inline parsing. One such function is defined -// for each character that triggers a response when parsing inline data. -type inlineParser func(p *Markdown, data []byte, offset int) (int, *Node) - -// Markdown is a type that holds extensions and the runtime state used by -// Parse, and the renderer. You can not use it directly, construct it with New. -type Markdown struct { - renderer Renderer - referenceOverride ReferenceOverrideFunc - refs map[string]*reference - inlineCallback [256]inlineParser - extensions Extensions - nesting int - maxNesting int - insideLink bool - - // Footnotes need to be ordered as well as available to quickly check for - // presence. If a ref is also a footnote, it's stored both in refs and here - // in notes. Slice is nil if footnotes not enabled. - notes []*reference - - doc *Node - tip *Node // = doc - oldTip *Node - lastMatchedContainer *Node // = doc - allClosed bool -} - -func (p *Markdown) getRef(refid string) (ref *reference, found bool) { - if p.referenceOverride != nil { - r, overridden := p.referenceOverride(refid) - if overridden { - if r == nil { - return nil, false - } - return &reference{ - link: []byte(r.Link), - title: []byte(r.Title), - noteID: 0, - hasBlock: false, - text: []byte(r.Text)}, true - } - } - // refs are case insensitive - ref, found = p.refs[strings.ToLower(refid)] - return ref, found -} - -func (p *Markdown) finalize(block *Node) { - above := block.Parent - block.open = false - p.tip = above -} - -func (p *Markdown) addChild(node NodeType, offset uint32) *Node { - return p.addExistingChild(NewNode(node), offset) -} - -func (p *Markdown) addExistingChild(node *Node, offset uint32) *Node { - for !p.tip.canContain(node.Type) { - p.finalize(p.tip) - } - p.tip.AppendChild(node) - p.tip = node - return node -} - -func (p *Markdown) closeUnmatchedBlocks() { - if !p.allClosed { - for p.oldTip != p.lastMatchedContainer { - parent := p.oldTip.Parent - p.finalize(p.oldTip) - p.oldTip = parent - } - p.allClosed = true - } -} - -// -// -// Public interface -// -// - -// Reference represents the details of a link. -// See the documentation in Options for more details on use-case. -type Reference struct { - // Link is usually the URL the reference points to. - Link string - // Title is the alternate text describing the link in more detail. - Title string - // Text is the optional text to override the ref with if the syntax used was - // [refid][] - Text string -} - -// ReferenceOverrideFunc is expected to be called with a reference string and -// return either a valid Reference type that the reference string maps to or -// nil. If overridden is false, the default reference logic will be executed. -// See the documentation in Options for more details on use-case. -type ReferenceOverrideFunc func(reference string) (ref *Reference, overridden bool) - -// New constructs a Markdown processor. You can use the same With* functions as -// for Run() to customize parser's behavior and the renderer. -func New(opts ...Option) *Markdown { - var p Markdown - for _, opt := range opts { - opt(&p) - } - p.refs = make(map[string]*reference) - p.maxNesting = 16 - p.insideLink = false - docNode := NewNode(Document) - p.doc = docNode - p.tip = docNode - p.oldTip = docNode - p.lastMatchedContainer = docNode - p.allClosed = true - // register inline parsers - p.inlineCallback[' '] = maybeLineBreak - p.inlineCallback['*'] = emphasis - p.inlineCallback['_'] = emphasis - if p.extensions&Strikethrough != 0 { - p.inlineCallback['~'] = emphasis - } - p.inlineCallback['`'] = codeSpan - p.inlineCallback['\n'] = lineBreak - p.inlineCallback['['] = link - p.inlineCallback['<'] = leftAngle - p.inlineCallback['\\'] = escape - p.inlineCallback['&'] = entity - p.inlineCallback['!'] = maybeImage - p.inlineCallback['^'] = maybeInlineFootnote - if p.extensions&Autolink != 0 { - p.inlineCallback['h'] = maybeAutoLink - p.inlineCallback['m'] = maybeAutoLink - p.inlineCallback['f'] = maybeAutoLink - p.inlineCallback['H'] = maybeAutoLink - p.inlineCallback['M'] = maybeAutoLink - p.inlineCallback['F'] = maybeAutoLink - } - if p.extensions&Footnotes != 0 { - p.notes = make([]*reference, 0) - } - return &p -} - -// Option customizes the Markdown processor's default behavior. -type Option func(*Markdown) - -// WithRenderer allows you to override the default renderer. -func WithRenderer(r Renderer) Option { - return func(p *Markdown) { - p.renderer = r - } -} - -// WithExtensions allows you to pick some of the many extensions provided by -// Blackfriday. You can bitwise OR them. -func WithExtensions(e Extensions) Option { - return func(p *Markdown) { - p.extensions = e - } -} - -// WithNoExtensions turns off all extensions and custom behavior. -func WithNoExtensions() Option { - return func(p *Markdown) { - p.extensions = NoExtensions - p.renderer = NewHTMLRenderer(HTMLRendererParameters{ - Flags: HTMLFlagsNone, - }) - } -} - -// WithRefOverride sets an optional function callback that is called every -// time a reference is resolved. -// -// In Markdown, the link reference syntax can be made to resolve a link to -// a reference instead of an inline URL, in one of the following ways: -// -// * [link text][refid] -// * [refid][] -// -// Usually, the refid is defined at the bottom of the Markdown document. If -// this override function is provided, the refid is passed to the override -// function first, before consulting the defined refids at the bottom. If -// the override function indicates an override did not occur, the refids at -// the bottom will be used to fill in the link details. -func WithRefOverride(o ReferenceOverrideFunc) Option { - return func(p *Markdown) { - p.referenceOverride = o - } -} - -// Run is the main entry point to Blackfriday. It parses and renders a -// block of markdown-encoded text. -// -// The simplest invocation of Run takes one argument, input: -// output := Run(input) -// This will parse the input with CommonExtensions enabled and render it with -// the default HTMLRenderer (with CommonHTMLFlags). -// -// Variadic arguments opts can customize the default behavior. Since Markdown -// type does not contain exported fields, you can not use it directly. Instead, -// use the With* functions. For example, this will call the most basic -// functionality, with no extensions: -// output := Run(input, WithNoExtensions()) -// -// You can use any number of With* arguments, even contradicting ones. They -// will be applied in order of appearance and the latter will override the -// former: -// output := Run(input, WithNoExtensions(), WithExtensions(exts), -// WithRenderer(yourRenderer)) -func Run(input []byte, opts ...Option) []byte { - r := NewHTMLRenderer(HTMLRendererParameters{ - Flags: CommonHTMLFlags, - }) - optList := []Option{WithRenderer(r), WithExtensions(CommonExtensions)} - optList = append(optList, opts...) - parser := New(optList...) - ast := parser.Parse(input) - var buf bytes.Buffer - parser.renderer.RenderHeader(&buf, ast) - ast.Walk(func(node *Node, entering bool) WalkStatus { - return parser.renderer.RenderNode(&buf, node, entering) - }) - parser.renderer.RenderFooter(&buf, ast) - return buf.Bytes() -} - -// Parse is an entry point to the parsing part of Blackfriday. It takes an -// input markdown document and produces a syntax tree for its contents. This -// tree can then be rendered with a default or custom renderer, or -// analyzed/transformed by the caller to whatever non-standard needs they have. -// The return value is the root node of the syntax tree. -func (p *Markdown) Parse(input []byte) *Node { - p.block(input) - // Walk the tree and finish up some of unfinished blocks - for p.tip != nil { - p.finalize(p.tip) - } - // Walk the tree again and process inline markdown in each block - p.doc.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Paragraph || node.Type == Heading || node.Type == TableCell { - p.inline(node, node.content) - node.content = nil - } - return GoToNext - }) - p.parseRefsToAST() - return p.doc -} - -func (p *Markdown) parseRefsToAST() { - if p.extensions&Footnotes == 0 || len(p.notes) == 0 { - return - } - p.tip = p.doc - block := p.addBlock(List, nil) - block.IsFootnotesList = true - block.ListFlags = ListTypeOrdered - flags := ListItemBeginningOfList - // Note: this loop is intentionally explicit, not range-form. This is - // because the body of the loop will append nested footnotes to p.notes and - // we need to process those late additions. Range form would only walk over - // the fixed initial set. - for i := 0; i < len(p.notes); i++ { - ref := p.notes[i] - p.addExistingChild(ref.footnote, 0) - block := ref.footnote - block.ListFlags = flags | ListTypeOrdered - block.RefLink = ref.link - if ref.hasBlock { - flags |= ListItemContainsBlock - p.block(ref.title) - } else { - p.inline(block, ref.title) - } - flags &^= ListItemBeginningOfList | ListItemContainsBlock - } - above := block.Parent - finalizeList(block) - p.tip = above - block.Walk(func(node *Node, entering bool) WalkStatus { - if node.Type == Paragraph || node.Type == Heading { - p.inline(node, node.content) - node.content = nil - } - return GoToNext - }) -} - -// -// Link references -// -// This section implements support for references that (usually) appear -// as footnotes in a document, and can be referenced anywhere in the document. -// The basic format is: -// -// [1]: http://www.google.com/ "Google" -// [2]: http://www.github.com/ "Github" -// -// Anywhere in the document, the reference can be linked by referring to its -// label, i.e., 1 and 2 in this example, as in: -// -// This library is hosted on [Github][2], a git hosting site. -// -// Actual footnotes as specified in Pandoc and supported by some other Markdown -// libraries such as php-markdown are also taken care of. They look like this: -// -// This sentence needs a bit of further explanation.[^note] -// -// [^note]: This is the explanation. -// -// Footnotes should be placed at the end of the document in an ordered list. -// Finally, there are inline footnotes such as: -// -// Inline footnotes^[Also supported.] provide a quick inline explanation, -// but are rendered at the bottom of the document. -// - -// reference holds all information necessary for a reference-style links or -// footnotes. -// -// Consider this markdown with reference-style links: -// -// [link][ref] -// -// [ref]: /url/ "tooltip title" -// -// It will be ultimately converted to this HTML: -// -//

    link

    -// -// And a reference structure will be populated as follows: -// -// p.refs["ref"] = &reference{ -// link: "/url/", -// title: "tooltip title", -// } -// -// Alternatively, reference can contain information about a footnote. Consider -// this markdown: -// -// Text needing a footnote.[^a] -// -// [^a]: This is the note -// -// A reference structure will be populated as follows: -// -// p.refs["a"] = &reference{ -// link: "a", -// title: "This is the note", -// noteID: , -// } -// -// TODO: As you can see, it begs for splitting into two dedicated structures -// for refs and for footnotes. -type reference struct { - link []byte - title []byte - noteID int // 0 if not a footnote ref - hasBlock bool - footnote *Node // a link to the Item node within a list of footnotes - - text []byte // only gets populated by refOverride feature with Reference.Text -} - -func (r *reference) String() string { - return fmt.Sprintf("{link: %q, title: %q, text: %q, noteID: %d, hasBlock: %v}", - r.link, r.title, r.text, r.noteID, r.hasBlock) -} - -// Check whether or not data starts with a reference link. -// If so, it is parsed and stored in the list of references -// (in the render struct). -// Returns the number of bytes to skip to move past it, -// or zero if the first line is not a reference. -func isReference(p *Markdown, data []byte, tabSize int) int { - // up to 3 optional leading spaces - if len(data) < 4 { - return 0 - } - i := 0 - for i < 3 && data[i] == ' ' { - i++ - } - - noteID := 0 - - // id part: anything but a newline between brackets - if data[i] != '[' { - return 0 - } - i++ - if p.extensions&Footnotes != 0 { - if i < len(data) && data[i] == '^' { - // we can set it to anything here because the proper noteIds will - // be assigned later during the second pass. It just has to be != 0 - noteID = 1 - i++ - } - } - idOffset := i - for i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != ']' { - i++ - } - if i >= len(data) || data[i] != ']' { - return 0 - } - idEnd := i - // footnotes can have empty ID, like this: [^], but a reference can not be - // empty like this: []. Break early if it's not a footnote and there's no ID - if noteID == 0 && idOffset == idEnd { - return 0 - } - // spacer: colon (space | tab)* newline? (space | tab)* - i++ - if i >= len(data) || data[i] != ':' { - return 0 - } - i++ - for i < len(data) && (data[i] == ' ' || data[i] == '\t') { - i++ - } - if i < len(data) && (data[i] == '\n' || data[i] == '\r') { - i++ - if i < len(data) && data[i] == '\n' && data[i-1] == '\r' { - i++ - } - } - for i < len(data) && (data[i] == ' ' || data[i] == '\t') { - i++ - } - if i >= len(data) { - return 0 - } - - var ( - linkOffset, linkEnd int - titleOffset, titleEnd int - lineEnd int - raw []byte - hasBlock bool - ) - - if p.extensions&Footnotes != 0 && noteID != 0 { - linkOffset, linkEnd, raw, hasBlock = scanFootnote(p, data, i, tabSize) - lineEnd = linkEnd - } else { - linkOffset, linkEnd, titleOffset, titleEnd, lineEnd = scanLinkRef(p, data, i) - } - if lineEnd == 0 { - return 0 - } - - // a valid ref has been found - - ref := &reference{ - noteID: noteID, - hasBlock: hasBlock, - } - - if noteID > 0 { - // reusing the link field for the id since footnotes don't have links - ref.link = data[idOffset:idEnd] - // if footnote, it's not really a title, it's the contained text - ref.title = raw - } else { - ref.link = data[linkOffset:linkEnd] - ref.title = data[titleOffset:titleEnd] - } - - // id matches are case-insensitive - id := string(bytes.ToLower(data[idOffset:idEnd])) - - p.refs[id] = ref - - return lineEnd -} - -func scanLinkRef(p *Markdown, data []byte, i int) (linkOffset, linkEnd, titleOffset, titleEnd, lineEnd int) { - // link: whitespace-free sequence, optionally between angle brackets - if data[i] == '<' { - i++ - } - linkOffset = i - for i < len(data) && data[i] != ' ' && data[i] != '\t' && data[i] != '\n' && data[i] != '\r' { - i++ - } - linkEnd = i - if data[linkOffset] == '<' && data[linkEnd-1] == '>' { - linkOffset++ - linkEnd-- - } - - // optional spacer: (space | tab)* (newline | '\'' | '"' | '(' ) - for i < len(data) && (data[i] == ' ' || data[i] == '\t') { - i++ - } - if i < len(data) && data[i] != '\n' && data[i] != '\r' && data[i] != '\'' && data[i] != '"' && data[i] != '(' { - return - } - - // compute end-of-line - if i >= len(data) || data[i] == '\r' || data[i] == '\n' { - lineEnd = i - } - if i+1 < len(data) && data[i] == '\r' && data[i+1] == '\n' { - lineEnd++ - } - - // optional (space|tab)* spacer after a newline - if lineEnd > 0 { - i = lineEnd + 1 - for i < len(data) && (data[i] == ' ' || data[i] == '\t') { - i++ - } - } - - // optional title: any non-newline sequence enclosed in '"() alone on its line - if i+1 < len(data) && (data[i] == '\'' || data[i] == '"' || data[i] == '(') { - i++ - titleOffset = i - - // look for EOL - for i < len(data) && data[i] != '\n' && data[i] != '\r' { - i++ - } - if i+1 < len(data) && data[i] == '\n' && data[i+1] == '\r' { - titleEnd = i + 1 - } else { - titleEnd = i - } - - // step back - i-- - for i > titleOffset && (data[i] == ' ' || data[i] == '\t') { - i-- - } - if i > titleOffset && (data[i] == '\'' || data[i] == '"' || data[i] == ')') { - lineEnd = titleEnd - titleEnd = i - } - } - - return -} - -// The first bit of this logic is the same as Parser.listItem, but the rest -// is much simpler. This function simply finds the entire block and shifts it -// over by one tab if it is indeed a block (just returns the line if it's not). -// blockEnd is the end of the section in the input buffer, and contents is the -// extracted text that was shifted over one tab. It will need to be rendered at -// the end of the document. -func scanFootnote(p *Markdown, data []byte, i, indentSize int) (blockStart, blockEnd int, contents []byte, hasBlock bool) { - if i == 0 || len(data) == 0 { - return - } - - // skip leading whitespace on first line - for i < len(data) && data[i] == ' ' { - i++ - } - - blockStart = i - - // find the end of the line - blockEnd = i - for i < len(data) && data[i-1] != '\n' { - i++ - } - - // get working buffer - var raw bytes.Buffer - - // put the first line into the working buffer - raw.Write(data[blockEnd:i]) - blockEnd = i - - // process the following lines - containsBlankLine := false - -gatherLines: - for blockEnd < len(data) { - i++ - - // find the end of this line - for i < len(data) && data[i-1] != '\n' { - i++ - } - - // if it is an empty line, guess that it is part of this item - // and move on to the next line - if p.isEmpty(data[blockEnd:i]) > 0 { - containsBlankLine = true - blockEnd = i - continue - } - - n := 0 - if n = isIndented(data[blockEnd:i], indentSize); n == 0 { - // this is the end of the block. - // we don't want to include this last line in the index. - break gatherLines - } - - // if there were blank lines before this one, insert a new one now - if containsBlankLine { - raw.WriteByte('\n') - containsBlankLine = false - } - - // get rid of that first tab, write to buffer - raw.Write(data[blockEnd+n : i]) - hasBlock = true - - blockEnd = i - } - - if data[blockEnd-1] != '\n' { - raw.WriteByte('\n') - } - - contents = raw.Bytes() - - return -} - -// -// -// Miscellaneous helper functions -// -// - -// Test if a character is a punctuation symbol. -// Taken from a private function in regexp in the stdlib. -func ispunct(c byte) bool { - for _, r := range []byte("!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~") { - if c == r { - return true - } - } - return false -} - -// Test if a character is a whitespace character. -func isspace(c byte) bool { - return ishorizontalspace(c) || isverticalspace(c) -} - -// Test if a character is a horizontal whitespace character. -func ishorizontalspace(c byte) bool { - return c == ' ' || c == '\t' -} - -// Test if a character is a vertical character. -func isverticalspace(c byte) bool { - return c == '\n' || c == '\r' || c == '\f' || c == '\v' -} - -// Test if a character is letter. -func isletter(c byte) bool { - return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') -} - -// Test if a character is a letter or a digit. -// TODO: check when this is looking for ASCII alnum and when it should use unicode -func isalnum(c byte) bool { - return (c >= '0' && c <= '9') || isletter(c) -} - -// Replace tab characters with spaces, aligning to the next TAB_SIZE column. -// always ends output with a newline -func expandTabs(out *bytes.Buffer, line []byte, tabSize int) { - // first, check for common cases: no tabs, or only tabs at beginning of line - i, prefix := 0, 0 - slowcase := false - for i = 0; i < len(line); i++ { - if line[i] == '\t' { - if prefix == i { - prefix++ - } else { - slowcase = true - break - } - } - } - - // no need to decode runes if all tabs are at the beginning of the line - if !slowcase { - for i = 0; i < prefix*tabSize; i++ { - out.WriteByte(' ') - } - out.Write(line[prefix:]) - return - } - - // the slow case: we need to count runes to figure out how - // many spaces to insert for each tab - column := 0 - i = 0 - for i < len(line) { - start := i - for i < len(line) && line[i] != '\t' { - _, size := utf8.DecodeRune(line[i:]) - i += size - column++ - } - - if i > start { - out.Write(line[start:i]) - } - - if i >= len(line) { - break - } - - for { - out.WriteByte(' ') - column++ - if column%tabSize == 0 { - break - } - } - - i++ - } -} - -// Find if a line counts as indented or not. -// Returns number of characters the indent is (0 = not indented). -func isIndented(data []byte, indentSize int) int { - if len(data) == 0 { - return 0 - } - if data[0] == '\t' { - return 1 - } - if len(data) < indentSize { - return 0 - } - for i := 0; i < indentSize; i++ { - if data[i] != ' ' { - return 0 - } - } - return indentSize -} - -// Create a url-safe slug for fragments -func slugify(in []byte) []byte { - if len(in) == 0 { - return in - } - out := make([]byte, 0, len(in)) - sym := false - - for _, ch := range in { - if isalnum(ch) { - sym = false - out = append(out, ch) - } else if sym { - continue - } else { - out = append(out, '-') - sym = true - } - } - var a, b int - var ch byte - for a, ch = range out { - if ch != '-' { - break - } - } - for b = len(out) - 1; b > 0; b-- { - if out[b] != '-' { - break - } - } - return out[a : b+1] -} diff --git a/vendor/github.com/russross/blackfriday/v2/node.go b/vendor/github.com/russross/blackfriday/v2/node.go deleted file mode 100644 index 51b9e8c1..00000000 --- a/vendor/github.com/russross/blackfriday/v2/node.go +++ /dev/null @@ -1,354 +0,0 @@ -package blackfriday - -import ( - "bytes" - "fmt" -) - -// NodeType specifies a type of a single node of a syntax tree. Usually one -// node (and its type) corresponds to a single markdown feature, e.g. emphasis -// or code block. -type NodeType int - -// Constants for identifying different types of nodes. See NodeType. -const ( - Document NodeType = iota - BlockQuote - List - Item - Paragraph - Heading - HorizontalRule - Emph - Strong - Del - Link - Image - Text - HTMLBlock - CodeBlock - Softbreak - Hardbreak - Code - HTMLSpan - Table - TableCell - TableHead - TableBody - TableRow -) - -var nodeTypeNames = []string{ - Document: "Document", - BlockQuote: "BlockQuote", - List: "List", - Item: "Item", - Paragraph: "Paragraph", - Heading: "Heading", - HorizontalRule: "HorizontalRule", - Emph: "Emph", - Strong: "Strong", - Del: "Del", - Link: "Link", - Image: "Image", - Text: "Text", - HTMLBlock: "HTMLBlock", - CodeBlock: "CodeBlock", - Softbreak: "Softbreak", - Hardbreak: "Hardbreak", - Code: "Code", - HTMLSpan: "HTMLSpan", - Table: "Table", - TableCell: "TableCell", - TableHead: "TableHead", - TableBody: "TableBody", - TableRow: "TableRow", -} - -func (t NodeType) String() string { - return nodeTypeNames[t] -} - -// ListData contains fields relevant to a List and Item node type. -type ListData struct { - ListFlags ListType - Tight bool // Skip

    s around list item data if true - BulletChar byte // '*', '+' or '-' in bullet lists - Delimiter byte // '.' or ')' after the number in ordered lists - RefLink []byte // If not nil, turns this list item into a footnote item and triggers different rendering - IsFootnotesList bool // This is a list of footnotes -} - -// LinkData contains fields relevant to a Link node type. -type LinkData struct { - Destination []byte // Destination is what goes into a href - Title []byte // Title is the tooltip thing that goes in a title attribute - NoteID int // NoteID contains a serial number of a footnote, zero if it's not a footnote - Footnote *Node // If it's a footnote, this is a direct link to the footnote Node. Otherwise nil. -} - -// CodeBlockData contains fields relevant to a CodeBlock node type. -type CodeBlockData struct { - IsFenced bool // Specifies whether it's a fenced code block or an indented one - Info []byte // This holds the info string - FenceChar byte - FenceLength int - FenceOffset int -} - -// TableCellData contains fields relevant to a TableCell node type. -type TableCellData struct { - IsHeader bool // This tells if it's under the header row - Align CellAlignFlags // This holds the value for align attribute -} - -// HeadingData contains fields relevant to a Heading node type. -type HeadingData struct { - Level int // This holds the heading level number - HeadingID string // This might hold heading ID, if present - IsTitleblock bool // Specifies whether it's a title block -} - -// Node is a single element in the abstract syntax tree of the parsed document. -// It holds connections to the structurally neighboring nodes and, for certain -// types of nodes, additional information that might be needed when rendering. -type Node struct { - Type NodeType // Determines the type of the node - Parent *Node // Points to the parent - FirstChild *Node // Points to the first child, if any - LastChild *Node // Points to the last child, if any - Prev *Node // Previous sibling; nil if it's the first child - Next *Node // Next sibling; nil if it's the last child - - Literal []byte // Text contents of the leaf nodes - - HeadingData // Populated if Type is Heading - ListData // Populated if Type is List - CodeBlockData // Populated if Type is CodeBlock - LinkData // Populated if Type is Link - TableCellData // Populated if Type is TableCell - - content []byte // Markdown content of the block nodes - open bool // Specifies an open block node that has not been finished to process yet -} - -// NewNode allocates a node of a specified type. -func NewNode(typ NodeType) *Node { - return &Node{ - Type: typ, - open: true, - } -} - -func (n *Node) String() string { - ellipsis := "" - snippet := n.Literal - if len(snippet) > 16 { - snippet = snippet[:16] - ellipsis = "..." - } - return fmt.Sprintf("%s: '%s%s'", n.Type, snippet, ellipsis) -} - -// Unlink removes node 'n' from the tree. -// It panics if the node is nil. -func (n *Node) Unlink() { - if n.Prev != nil { - n.Prev.Next = n.Next - } else if n.Parent != nil { - n.Parent.FirstChild = n.Next - } - if n.Next != nil { - n.Next.Prev = n.Prev - } else if n.Parent != nil { - n.Parent.LastChild = n.Prev - } - n.Parent = nil - n.Next = nil - n.Prev = nil -} - -// AppendChild adds a node 'child' as a child of 'n'. -// It panics if either node is nil. -func (n *Node) AppendChild(child *Node) { - child.Unlink() - child.Parent = n - if n.LastChild != nil { - n.LastChild.Next = child - child.Prev = n.LastChild - n.LastChild = child - } else { - n.FirstChild = child - n.LastChild = child - } -} - -// InsertBefore inserts 'sibling' immediately before 'n'. -// It panics if either node is nil. -func (n *Node) InsertBefore(sibling *Node) { - sibling.Unlink() - sibling.Prev = n.Prev - if sibling.Prev != nil { - sibling.Prev.Next = sibling - } - sibling.Next = n - n.Prev = sibling - sibling.Parent = n.Parent - if sibling.Prev == nil { - sibling.Parent.FirstChild = sibling - } -} - -func (n *Node) isContainer() bool { - switch n.Type { - case Document: - fallthrough - case BlockQuote: - fallthrough - case List: - fallthrough - case Item: - fallthrough - case Paragraph: - fallthrough - case Heading: - fallthrough - case Emph: - fallthrough - case Strong: - fallthrough - case Del: - fallthrough - case Link: - fallthrough - case Image: - fallthrough - case Table: - fallthrough - case TableHead: - fallthrough - case TableBody: - fallthrough - case TableRow: - fallthrough - case TableCell: - return true - default: - return false - } -} - -func (n *Node) canContain(t NodeType) bool { - if n.Type == List { - return t == Item - } - if n.Type == Document || n.Type == BlockQuote || n.Type == Item { - return t != Item - } - if n.Type == Table { - return t == TableHead || t == TableBody - } - if n.Type == TableHead || n.Type == TableBody { - return t == TableRow - } - if n.Type == TableRow { - return t == TableCell - } - return false -} - -// WalkStatus allows NodeVisitor to have some control over the tree traversal. -// It is returned from NodeVisitor and different values allow Node.Walk to -// decide which node to go to next. -type WalkStatus int - -const ( - // GoToNext is the default traversal of every node. - GoToNext WalkStatus = iota - // SkipChildren tells walker to skip all children of current node. - SkipChildren - // Terminate tells walker to terminate the traversal. - Terminate -) - -// NodeVisitor is a callback to be called when traversing the syntax tree. -// Called twice for every node: once with entering=true when the branch is -// first visited, then with entering=false after all the children are done. -type NodeVisitor func(node *Node, entering bool) WalkStatus - -// Walk is a convenience method that instantiates a walker and starts a -// traversal of subtree rooted at n. -func (n *Node) Walk(visitor NodeVisitor) { - w := newNodeWalker(n) - for w.current != nil { - status := visitor(w.current, w.entering) - switch status { - case GoToNext: - w.next() - case SkipChildren: - w.entering = false - w.next() - case Terminate: - return - } - } -} - -type nodeWalker struct { - current *Node - root *Node - entering bool -} - -func newNodeWalker(root *Node) *nodeWalker { - return &nodeWalker{ - current: root, - root: root, - entering: true, - } -} - -func (nw *nodeWalker) next() { - if (!nw.current.isContainer() || !nw.entering) && nw.current == nw.root { - nw.current = nil - return - } - if nw.entering && nw.current.isContainer() { - if nw.current.FirstChild != nil { - nw.current = nw.current.FirstChild - nw.entering = true - } else { - nw.entering = false - } - } else if nw.current.Next == nil { - nw.current = nw.current.Parent - nw.entering = false - } else { - nw.current = nw.current.Next - nw.entering = true - } -} - -func dump(ast *Node) { - fmt.Println(dumpString(ast)) -} - -func dumpR(ast *Node, depth int) string { - if ast == nil { - return "" - } - indent := bytes.Repeat([]byte("\t"), depth) - content := ast.Literal - if content == nil { - content = ast.content - } - result := fmt.Sprintf("%s%s(%q)\n", indent, ast.Type, content) - for n := ast.FirstChild; n != nil; n = n.Next { - result += dumpR(n, depth+1) - } - return result -} - -func dumpString(ast *Node) string { - return dumpR(ast, 0) -} diff --git a/vendor/github.com/russross/blackfriday/v2/smartypants.go b/vendor/github.com/russross/blackfriday/v2/smartypants.go deleted file mode 100644 index 3a220e94..00000000 --- a/vendor/github.com/russross/blackfriday/v2/smartypants.go +++ /dev/null @@ -1,457 +0,0 @@ -// -// Blackfriday Markdown Processor -// Available at http://github.com/russross/blackfriday -// -// Copyright © 2011 Russ Ross . -// Distributed under the Simplified BSD License. -// See README.md for details. -// - -// -// -// SmartyPants rendering -// -// - -package blackfriday - -import ( - "bytes" - "io" -) - -// SPRenderer is a struct containing state of a Smartypants renderer. -type SPRenderer struct { - inSingleQuote bool - inDoubleQuote bool - callbacks [256]smartCallback -} - -func wordBoundary(c byte) bool { - return c == 0 || isspace(c) || ispunct(c) -} - -func tolower(c byte) byte { - if c >= 'A' && c <= 'Z' { - return c - 'A' + 'a' - } - return c -} - -func isdigit(c byte) bool { - return c >= '0' && c <= '9' -} - -func smartQuoteHelper(out *bytes.Buffer, previousChar byte, nextChar byte, quote byte, isOpen *bool, addNBSP bool) bool { - // edge of the buffer is likely to be a tag that we don't get to see, - // so we treat it like text sometimes - - // enumerate all sixteen possibilities for (previousChar, nextChar) - // each can be one of {0, space, punct, other} - switch { - case previousChar == 0 && nextChar == 0: - // context is not any help here, so toggle - *isOpen = !*isOpen - case isspace(previousChar) && nextChar == 0: - // [ "] might be [ "foo...] - *isOpen = true - case ispunct(previousChar) && nextChar == 0: - // [!"] hmm... could be [Run!"] or [("...] - *isOpen = false - case /* isnormal(previousChar) && */ nextChar == 0: - // [a"] is probably a close - *isOpen = false - case previousChar == 0 && isspace(nextChar): - // [" ] might be [...foo" ] - *isOpen = false - case isspace(previousChar) && isspace(nextChar): - // [ " ] context is not any help here, so toggle - *isOpen = !*isOpen - case ispunct(previousChar) && isspace(nextChar): - // [!" ] is probably a close - *isOpen = false - case /* isnormal(previousChar) && */ isspace(nextChar): - // [a" ] this is one of the easy cases - *isOpen = false - case previousChar == 0 && ispunct(nextChar): - // ["!] hmm... could be ["$1.95] or ["!...] - *isOpen = false - case isspace(previousChar) && ispunct(nextChar): - // [ "!] looks more like [ "$1.95] - *isOpen = true - case ispunct(previousChar) && ispunct(nextChar): - // [!"!] context is not any help here, so toggle - *isOpen = !*isOpen - case /* isnormal(previousChar) && */ ispunct(nextChar): - // [a"!] is probably a close - *isOpen = false - case previousChar == 0 /* && isnormal(nextChar) */ : - // ["a] is probably an open - *isOpen = true - case isspace(previousChar) /* && isnormal(nextChar) */ : - // [ "a] this is one of the easy cases - *isOpen = true - case ispunct(previousChar) /* && isnormal(nextChar) */ : - // [!"a] is probably an open - *isOpen = true - default: - // [a'b] maybe a contraction? - *isOpen = false - } - - // Note that with the limited lookahead, this non-breaking - // space will also be appended to single double quotes. - if addNBSP && !*isOpen { - out.WriteString(" ") - } - - out.WriteByte('&') - if *isOpen { - out.WriteByte('l') - } else { - out.WriteByte('r') - } - out.WriteByte(quote) - out.WriteString("quo;") - - if addNBSP && *isOpen { - out.WriteString(" ") - } - - return true -} - -func (r *SPRenderer) smartSingleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 2 { - t1 := tolower(text[1]) - - if t1 == '\'' { - nextChar := byte(0) - if len(text) >= 3 { - nextChar = text[2] - } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { - return 1 - } - } - - if (t1 == 's' || t1 == 't' || t1 == 'm' || t1 == 'd') && (len(text) < 3 || wordBoundary(text[2])) { - out.WriteString("’") - return 0 - } - - if len(text) >= 3 { - t2 := tolower(text[2]) - - if ((t1 == 'r' && t2 == 'e') || (t1 == 'l' && t2 == 'l') || (t1 == 'v' && t2 == 'e')) && - (len(text) < 4 || wordBoundary(text[3])) { - out.WriteString("’") - return 0 - } - } - } - - nextChar := byte(0) - if len(text) > 1 { - nextChar = text[1] - } - if smartQuoteHelper(out, previousChar, nextChar, 's', &r.inSingleQuote, false) { - return 0 - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartParens(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 3 { - t1 := tolower(text[1]) - t2 := tolower(text[2]) - - if t1 == 'c' && t2 == ')' { - out.WriteString("©") - return 2 - } - - if t1 == 'r' && t2 == ')' { - out.WriteString("®") - return 2 - } - - if len(text) >= 4 && t1 == 't' && t2 == 'm' && text[3] == ')' { - out.WriteString("™") - return 3 - } - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartDash(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 2 { - if text[1] == '-' { - out.WriteString("—") - return 1 - } - - if wordBoundary(previousChar) && wordBoundary(text[1]) { - out.WriteString("–") - return 0 - } - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartDashLatex(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 3 && text[1] == '-' && text[2] == '-' { - out.WriteString("—") - return 2 - } - if len(text) >= 2 && text[1] == '-' { - out.WriteString("–") - return 1 - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartAmpVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte, addNBSP bool) int { - if bytes.HasPrefix(text, []byte(""")) { - nextChar := byte(0) - if len(text) >= 7 { - nextChar = text[6] - } - if smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, addNBSP) { - return 5 - } - } - - if bytes.HasPrefix(text, []byte("�")) { - return 3 - } - - out.WriteByte('&') - return 0 -} - -func (r *SPRenderer) smartAmp(angledQuotes, addNBSP bool) func(*bytes.Buffer, byte, []byte) int { - var quote byte = 'd' - if angledQuotes { - quote = 'a' - } - - return func(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartAmpVariant(out, previousChar, text, quote, addNBSP) - } -} - -func (r *SPRenderer) smartPeriod(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 3 && text[1] == '.' && text[2] == '.' { - out.WriteString("…") - return 2 - } - - if len(text) >= 5 && text[1] == ' ' && text[2] == '.' && text[3] == ' ' && text[4] == '.' { - out.WriteString("…") - return 4 - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartBacktick(out *bytes.Buffer, previousChar byte, text []byte) int { - if len(text) >= 2 && text[1] == '`' { - nextChar := byte(0) - if len(text) >= 3 { - nextChar = text[2] - } - if smartQuoteHelper(out, previousChar, nextChar, 'd', &r.inDoubleQuote, false) { - return 1 - } - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartNumberGeneric(out *bytes.Buffer, previousChar byte, text []byte) int { - if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { - // is it of the form digits/digits(word boundary)?, i.e., \d+/\d+\b - // note: check for regular slash (/) or fraction slash (⁄, 0x2044, or 0xe2 81 84 in utf-8) - // and avoid changing dates like 1/23/2005 into fractions. - numEnd := 0 - for len(text) > numEnd && isdigit(text[numEnd]) { - numEnd++ - } - if numEnd == 0 { - out.WriteByte(text[0]) - return 0 - } - denStart := numEnd + 1 - if len(text) > numEnd+3 && text[numEnd] == 0xe2 && text[numEnd+1] == 0x81 && text[numEnd+2] == 0x84 { - denStart = numEnd + 3 - } else if len(text) < numEnd+2 || text[numEnd] != '/' { - out.WriteByte(text[0]) - return 0 - } - denEnd := denStart - for len(text) > denEnd && isdigit(text[denEnd]) { - denEnd++ - } - if denEnd == denStart { - out.WriteByte(text[0]) - return 0 - } - if len(text) == denEnd || wordBoundary(text[denEnd]) && text[denEnd] != '/' { - out.WriteString("") - out.Write(text[:numEnd]) - out.WriteString("") - out.Write(text[denStart:denEnd]) - out.WriteString("") - return denEnd - 1 - } - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartNumber(out *bytes.Buffer, previousChar byte, text []byte) int { - if wordBoundary(previousChar) && previousChar != '/' && len(text) >= 3 { - if text[0] == '1' && text[1] == '/' && text[2] == '2' { - if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' { - out.WriteString("½") - return 2 - } - } - - if text[0] == '1' && text[1] == '/' && text[2] == '4' { - if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 5 && tolower(text[3]) == 't' && tolower(text[4]) == 'h') { - out.WriteString("¼") - return 2 - } - } - - if text[0] == '3' && text[1] == '/' && text[2] == '4' { - if len(text) < 4 || wordBoundary(text[3]) && text[3] != '/' || (len(text) >= 6 && tolower(text[3]) == 't' && tolower(text[4]) == 'h' && tolower(text[5]) == 's') { - out.WriteString("¾") - return 2 - } - } - } - - out.WriteByte(text[0]) - return 0 -} - -func (r *SPRenderer) smartDoubleQuoteVariant(out *bytes.Buffer, previousChar byte, text []byte, quote byte) int { - nextChar := byte(0) - if len(text) > 1 { - nextChar = text[1] - } - if !smartQuoteHelper(out, previousChar, nextChar, quote, &r.inDoubleQuote, false) { - out.WriteString(""") - } - - return 0 -} - -func (r *SPRenderer) smartDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartDoubleQuoteVariant(out, previousChar, text, 'd') -} - -func (r *SPRenderer) smartAngledDoubleQuote(out *bytes.Buffer, previousChar byte, text []byte) int { - return r.smartDoubleQuoteVariant(out, previousChar, text, 'a') -} - -func (r *SPRenderer) smartLeftAngle(out *bytes.Buffer, previousChar byte, text []byte) int { - i := 0 - - for i < len(text) && text[i] != '>' { - i++ - } - - out.Write(text[:i+1]) - return i -} - -type smartCallback func(out *bytes.Buffer, previousChar byte, text []byte) int - -// NewSmartypantsRenderer constructs a Smartypants renderer object. -func NewSmartypantsRenderer(flags HTMLFlags) *SPRenderer { - var ( - r SPRenderer - - smartAmpAngled = r.smartAmp(true, false) - smartAmpAngledNBSP = r.smartAmp(true, true) - smartAmpRegular = r.smartAmp(false, false) - smartAmpRegularNBSP = r.smartAmp(false, true) - - addNBSP = flags&SmartypantsQuotesNBSP != 0 - ) - - if flags&SmartypantsAngledQuotes == 0 { - r.callbacks['"'] = r.smartDoubleQuote - if !addNBSP { - r.callbacks['&'] = smartAmpRegular - } else { - r.callbacks['&'] = smartAmpRegularNBSP - } - } else { - r.callbacks['"'] = r.smartAngledDoubleQuote - if !addNBSP { - r.callbacks['&'] = smartAmpAngled - } else { - r.callbacks['&'] = smartAmpAngledNBSP - } - } - r.callbacks['\''] = r.smartSingleQuote - r.callbacks['('] = r.smartParens - if flags&SmartypantsDashes != 0 { - if flags&SmartypantsLatexDashes == 0 { - r.callbacks['-'] = r.smartDash - } else { - r.callbacks['-'] = r.smartDashLatex - } - } - r.callbacks['.'] = r.smartPeriod - if flags&SmartypantsFractions == 0 { - r.callbacks['1'] = r.smartNumber - r.callbacks['3'] = r.smartNumber - } else { - for ch := '1'; ch <= '9'; ch++ { - r.callbacks[ch] = r.smartNumberGeneric - } - } - r.callbacks['<'] = r.smartLeftAngle - r.callbacks['`'] = r.smartBacktick - return &r -} - -// Process is the entry point of the Smartypants renderer. -func (r *SPRenderer) Process(w io.Writer, text []byte) { - mark := 0 - for i := 0; i < len(text); i++ { - if action := r.callbacks[text[i]]; action != nil { - if i > mark { - w.Write(text[mark:i]) - } - previousChar := byte(0) - if i > 0 { - previousChar = text[i-1] - } - var tmp bytes.Buffer - i += action(&tmp, previousChar, text[i:]) - w.Write(tmp.Bytes()) - mark = i + 1 - } - } - if mark < len(text) { - w.Write(text[mark:]) - } -} diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml b/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml deleted file mode 100644 index 93b1fcdb..00000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/.travis.yml +++ /dev/null @@ -1,16 +0,0 @@ -sudo: false -language: go -go: - - 1.x - - master -matrix: - allow_failures: - - go: master - fast_finish: true -install: - - # Do nothing. This is needed to prevent default install action "go get -t -v ./..." from happening here (we want it to happen inside script step). -script: - - go get -t -v ./... - - diff -u <(echo -n) <(gofmt -d -s .) - - go tool vet . - - go test -v -race ./... diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE b/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE deleted file mode 100644 index c35c17af..00000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2015 Dmitri Shuralyov - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md b/vendor/github.com/shurcooL/sanitized_anchor_name/README.md deleted file mode 100644 index 670bf0fe..00000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/README.md +++ /dev/null @@ -1,36 +0,0 @@ -sanitized_anchor_name -===================== - -[![Build Status](https://travis-ci.org/shurcooL/sanitized_anchor_name.svg?branch=master)](https://travis-ci.org/shurcooL/sanitized_anchor_name) [![GoDoc](https://godoc.org/github.com/shurcooL/sanitized_anchor_name?status.svg)](https://godoc.org/github.com/shurcooL/sanitized_anchor_name) - -Package sanitized_anchor_name provides a func to create sanitized anchor names. - -Its logic can be reused by multiple packages to create interoperable anchor names -and links to those anchors. - -At this time, it does not try to ensure that generated anchor names -are unique, that responsibility falls on the caller. - -Installation ------------- - -```bash -go get -u github.com/shurcooL/sanitized_anchor_name -``` - -Example -------- - -```Go -anchorName := sanitized_anchor_name.Create("This is a header") - -fmt.Println(anchorName) - -// Output: -// this-is-a-header -``` - -License -------- - -- [MIT License](LICENSE) diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod b/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod deleted file mode 100644 index 1e255347..00000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/go.mod +++ /dev/null @@ -1 +0,0 @@ -module github.com/shurcooL/sanitized_anchor_name diff --git a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go b/vendor/github.com/shurcooL/sanitized_anchor_name/main.go deleted file mode 100644 index 6a77d124..00000000 --- a/vendor/github.com/shurcooL/sanitized_anchor_name/main.go +++ /dev/null @@ -1,29 +0,0 @@ -// Package sanitized_anchor_name provides a func to create sanitized anchor names. -// -// Its logic can be reused by multiple packages to create interoperable anchor names -// and links to those anchors. -// -// At this time, it does not try to ensure that generated anchor names -// are unique, that responsibility falls on the caller. -package sanitized_anchor_name // import "github.com/shurcooL/sanitized_anchor_name" - -import "unicode" - -// Create returns a sanitized anchor name for the given text. -func Create(text string) string { - var anchorName []rune - var futureDash = false - for _, r := range text { - switch { - case unicode.IsLetter(r) || unicode.IsNumber(r): - if futureDash && len(anchorName) > 0 { - anchorName = append(anchorName, '-') - } - futureDash = false - anchorName = append(anchorName, unicode.ToLower(r)) - default: - futureDash = true - } - } - return string(anchorName) -} diff --git a/vendor/github.com/urfave/cli/v2/.flake8 b/vendor/github.com/urfave/cli/v2/.flake8 deleted file mode 100644 index 6deafc26..00000000 --- a/vendor/github.com/urfave/cli/v2/.flake8 +++ /dev/null @@ -1,2 +0,0 @@ -[flake8] -max-line-length = 120 diff --git a/vendor/github.com/urfave/cli/v2/.gitignore b/vendor/github.com/urfave/cli/v2/.gitignore deleted file mode 100644 index 2d5e149b..00000000 --- a/vendor/github.com/urfave/cli/v2/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -*.coverprofile -*.orig -node_modules/ -vendor -.idea -internal/*/built-example -coverage.txt diff --git a/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md b/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md deleted file mode 100644 index 41ba294f..00000000 --- a/vendor/github.com/urfave/cli/v2/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,74 +0,0 @@ -# Contributor Covenant Code of Conduct - -## Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, gender identity and expression, level of experience, -education, socio-economic status, nationality, personal appearance, race, -religion, or sexual identity and orientation. - -## Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -## Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -## Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -## Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting Dan Buch at dan@meatballhat.com. All complaints will be -reviewed and investigated and will result in a response that is deemed necessary -and appropriate to the circumstances. The project team is obligated to maintain -confidentiality with regard to the reporter of an incident. Further details of -specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -## Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - diff --git a/vendor/github.com/urfave/cli/v2/LICENSE b/vendor/github.com/urfave/cli/v2/LICENSE deleted file mode 100644 index 42a597e2..00000000 --- a/vendor/github.com/urfave/cli/v2/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -MIT License - -Copyright (c) 2016 Jeremy Saenz & Contributors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/vendor/github.com/urfave/cli/v2/README.md b/vendor/github.com/urfave/cli/v2/README.md deleted file mode 100644 index c9237fbc..00000000 --- a/vendor/github.com/urfave/cli/v2/README.md +++ /dev/null @@ -1,66 +0,0 @@ -cli -=== - -[![GoDoc](https://godoc.org/github.com/urfave/cli?status.svg)](https://godoc.org/github.com/urfave/cli) -[![codebeat](https://codebeat.co/badges/0a8f30aa-f975-404b-b878-5fab3ae1cc5f)](https://codebeat.co/projects/github-com-urfave-cli) -[![Go Report Card](https://goreportcard.com/badge/urfave/cli)](https://goreportcard.com/report/urfave/cli) -[![codecov](https://codecov.io/gh/urfave/cli/branch/master/graph/badge.svg)](https://codecov.io/gh/urfave/cli) - -cli is a simple, fast, and fun package for building command line apps in Go. The -goal is to enable developers to write fast and distributable command line -applications in an expressive way. - -## Usage Documentation - -Usage documentation exists for each major version. Don't know what version you're on? You're probably using the version from the `master` branch, which is currently `v2`. - -- `v2` - [./docs/v2/manual.md](./docs/v2/manual.md) -- `v1` - [./docs/v1/manual.md](./docs/v1/manual.md) - -## Installation - -Make sure you have a working Go environment. Go version 1.11+ is supported. [See the install instructions for Go](http://golang.org/doc/install.html). - -Go Modules are strongly recommended when using this package. [See the go blog guide on using Go Modules](https://blog.golang.org/using-go-modules). - -### Using `v2` releases - -``` -$ GO111MODULE=on go get github.com/urfave/cli/v2 -``` - -```go -... -import ( - "github.com/urfave/cli/v2" // imports as package "cli" -) -... -``` - -### Using `v1` releases - -``` -$ GO111MODULE=on go get github.com/urfave/cli -``` - -```go -... -import ( - "github.com/urfave/cli" -) -... -``` - -### GOPATH - -Make sure your `PATH` includes the `$GOPATH/bin` directory so your commands can -be easily used: -``` -export PATH=$PATH:$GOPATH/bin -``` - -### Supported platforms - -cli is tested against multiple versions of Go on Linux, and against the latest -released version of Go on OS X and Windows. This project uses Github Actions for -builds. For more build info, please look at the [./.github/workflows/cli.yml](https://github.com/urfave/cli/blob/master/.github/workflows/cli.yml). diff --git a/vendor/github.com/urfave/cli/v2/app.go b/vendor/github.com/urfave/cli/v2/app.go deleted file mode 100644 index d0c8f84e..00000000 --- a/vendor/github.com/urfave/cli/v2/app.go +++ /dev/null @@ -1,542 +0,0 @@ -package cli - -import ( - "context" - "flag" - "fmt" - "io" - "os" - "path/filepath" - "sort" - "time" -) - -var ( - changeLogURL = "https://github.com/urfave/cli/blob/master/docs/CHANGELOG.md" - appActionDeprecationURL = fmt.Sprintf("%s#deprecated-cli-app-action-signature", changeLogURL) - contactSysadmin = "This is an error in the application. Please contact the distributor of this application if this is not you." - errInvalidActionType = NewExitError("ERROR invalid Action type. "+ - fmt.Sprintf("Must be `func(*Context`)` or `func(*Context) error). %s", contactSysadmin)+ - fmt.Sprintf("See %s", appActionDeprecationURL), 2) -) - -// App is the main structure of a cli application. It is recommended that -// an app be created with the cli.NewApp() function -type App struct { - // The name of the program. Defaults to path.Base(os.Args[0]) - Name string - // Full name of command for help, defaults to Name - HelpName string - // Description of the program. - Usage string - // Text to override the USAGE section of help - UsageText string - // Description of the program argument format. - ArgsUsage string - // Version of the program - Version string - // Description of the program - Description string - // List of commands to execute - Commands []*Command - // List of flags to parse - Flags []Flag - // Boolean to enable bash completion commands - EnableBashCompletion bool - // Boolean to hide built-in help command and help flag - HideHelp bool - // Boolean to hide built-in help command but keep help flag. - // Ignored if HideHelp is true. - HideHelpCommand bool - // Boolean to hide built-in version flag and the VERSION section of help - HideVersion bool - // categories contains the categorized commands and is populated on app startup - categories CommandCategories - // An action to execute when the shell completion flag is set - BashComplete BashCompleteFunc - // An action to execute before any subcommands are run, but after the context is ready - // If a non-nil error is returned, no subcommands are run - Before BeforeFunc - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After AfterFunc - // The action to execute when no subcommands are specified - Action ActionFunc - // Execute this function if the proper command cannot be found - CommandNotFound CommandNotFoundFunc - // Execute this function if an usage error occurs - OnUsageError OnUsageErrorFunc - // Compilation date - Compiled time.Time - // List of all authors who contributed - Authors []*Author - // Copyright of the binary if any - Copyright string - // Writer writer to write output to - Writer io.Writer - // ErrWriter writes error output - ErrWriter io.Writer - // Execute this function to handle ExitErrors. If not provided, HandleExitCoder is provided to - // function as a default, so this is optional. - ExitErrHandler ExitErrHandlerFunc - // Other custom info - Metadata map[string]interface{} - // Carries a function which returns app specific info. - ExtraInfo func() map[string]string - // CustomAppHelpTemplate the text template for app help topic. - // cli.go uses text/template to render templates. You can - // render custom help text by setting this variable. - CustomAppHelpTemplate string - // Boolean to enable short-option handling so user can combine several - // single-character bool arguments into one - // i.e. foobar -o -v -> foobar -ov - UseShortOptionHandling bool - - didSetup bool -} - -// Tries to find out when this binary was compiled. -// Returns the current time if it fails to find it. -func compileTime() time.Time { - info, err := os.Stat(os.Args[0]) - if err != nil { - return time.Now() - } - return info.ModTime() -} - -// NewApp creates a new cli Application with some reasonable defaults for Name, -// Usage, Version and Action. -func NewApp() *App { - return &App{ - Name: filepath.Base(os.Args[0]), - HelpName: filepath.Base(os.Args[0]), - Usage: "A new cli application", - UsageText: "", - BashComplete: DefaultAppComplete, - Action: helpCommand.Action, - Compiled: compileTime(), - Writer: os.Stdout, - } -} - -// Setup runs initialization code to ensure all data structures are ready for -// `Run` or inspection prior to `Run`. It is internally called by `Run`, but -// will return early if setup has already happened. -func (a *App) Setup() { - if a.didSetup { - return - } - - a.didSetup = true - - if a.Name == "" { - a.Name = filepath.Base(os.Args[0]) - } - - if a.HelpName == "" { - a.HelpName = filepath.Base(os.Args[0]) - } - - if a.Usage == "" { - a.Usage = "A new cli application" - } - - if a.Version == "" { - a.HideVersion = true - } - - if a.BashComplete == nil { - a.BashComplete = DefaultAppComplete - } - - if a.Action == nil { - a.Action = helpCommand.Action - } - - if a.Compiled == (time.Time{}) { - a.Compiled = compileTime() - } - - if a.Writer == nil { - a.Writer = os.Stdout - } - - var newCommands []*Command - - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCommands = append(newCommands, c) - } - a.Commands = newCommands - - if a.Command(helpCommand.Name) == nil && !a.HideHelp { - if !a.HideHelpCommand { - a.appendCommand(helpCommand) - } - - if HelpFlag != nil { - a.appendFlag(HelpFlag) - } - } - - if !a.HideVersion { - a.appendFlag(VersionFlag) - } - - a.categories = newCommandCategories() - for _, command := range a.Commands { - a.categories.AddCommand(command.Category, command) - } - sort.Sort(a.categories.(*commandCategories)) - - if a.Metadata == nil { - a.Metadata = make(map[string]interface{}) - } - - if a.Writer == nil { - a.Writer = os.Stdout - } -} - -func (a *App) newFlagSet() (*flag.FlagSet, error) { - return flagSet(a.Name, a.Flags) -} - -func (a *App) useShortOptionHandling() bool { - return a.UseShortOptionHandling -} - -// Run is the entry point to the cli app. Parses the arguments slice and routes -// to the proper flag/args combination -func (a *App) Run(arguments []string) (err error) { - return a.RunContext(context.Background(), arguments) -} - -// RunContext is like Run except it takes a Context that will be -// passed to its commands and sub-commands. Through this, you can -// propagate timeouts and cancellation requests -func (a *App) RunContext(ctx context.Context, arguments []string) (err error) { - a.Setup() - - // handle the completion flag separately from the flagset since - // completion could be attempted after a flag, but before its value was put - // on the command line. this causes the flagset to interpret the completion - // flag name as the value of the flag before it which is undesirable - // note that we can only do this because the shell autocomplete function - // always appends the completion flag at the end of the command - shellComplete, arguments := checkShellCompleteFlag(a, arguments) - - set, err := a.newFlagSet() - if err != nil { - return err - } - - err = parseIter(set, a, arguments[1:], shellComplete) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, &Context{Context: ctx}) - if nerr != nil { - _, _ = fmt.Fprintln(a.Writer, nerr) - _ = ShowAppHelp(context) - return nerr - } - context.shellComplete = shellComplete - - if checkCompletions(context) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err := a.OnUsageError(context, err, false) - a.handleExitCoder(context, err) - return err - } - _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - _ = ShowAppHelp(context) - return err - } - - if !a.HideHelp && checkHelp(context) { - _ = ShowAppHelp(context) - return nil - } - - if !a.HideVersion && checkVersion(context) { - ShowVersion(context) - return nil - } - - cerr := checkRequiredFlags(a.Flags, context) - if cerr != nil { - _ = ShowAppHelp(context) - return cerr - } - - if a.After != nil { - defer func() { - if afterErr := a.After(context); afterErr != nil { - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - beforeErr := a.Before(context) - if beforeErr != nil { - _, _ = fmt.Fprintf(a.Writer, "%v\n\n", beforeErr) - _ = ShowAppHelp(context) - a.handleExitCoder(context, beforeErr) - err = beforeErr - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - if a.Action == nil { - a.Action = helpCommand.Action - } - - // Run default Action - err = a.Action(context) - - a.handleExitCoder(context, err) - return err -} - -// RunAndExitOnError calls .Run() and exits non-zero if an error was returned -// -// Deprecated: instead you should return an error that fulfills cli.ExitCoder -// to cli.App.Run. This will cause the application to exit with the given eror -// code in the cli.ExitCoder -func (a *App) RunAndExitOnError() { - if err := a.Run(os.Args); err != nil { - _, _ = fmt.Fprintln(a.errWriter(), err) - OsExiter(1) - } -} - -// RunAsSubcommand invokes the subcommand given the context, parses ctx.Args() to -// generate command-specific flags -func (a *App) RunAsSubcommand(ctx *Context) (err error) { - // Setup also handles HideHelp and HideHelpCommand - a.Setup() - - var newCmds []*Command - for _, c := range a.Commands { - if c.HelpName == "" { - c.HelpName = fmt.Sprintf("%s %s", a.HelpName, c.Name) - } - newCmds = append(newCmds, c) - } - a.Commands = newCmds - - set, err := a.newFlagSet() - if err != nil { - return err - } - - err = parseIter(set, a, ctx.Args().Tail(), ctx.shellComplete) - nerr := normalizeFlags(a.Flags, set) - context := NewContext(a, set, ctx) - - if nerr != nil { - _, _ = fmt.Fprintln(a.Writer, nerr) - _, _ = fmt.Fprintln(a.Writer) - if len(a.Commands) > 0 { - _ = ShowSubcommandHelp(context) - } else { - _ = ShowCommandHelp(ctx, context.Args().First()) - } - return nerr - } - - if checkCompletions(context) { - return nil - } - - if err != nil { - if a.OnUsageError != nil { - err = a.OnUsageError(context, err, true) - a.handleExitCoder(context, err) - return err - } - _, _ = fmt.Fprintf(a.Writer, "%s %s\n\n", "Incorrect Usage.", err.Error()) - _ = ShowSubcommandHelp(context) - return err - } - - if len(a.Commands) > 0 { - if checkSubcommandHelp(context) { - return nil - } - } else { - if checkCommandHelp(ctx, context.Args().First()) { - return nil - } - } - - cerr := checkRequiredFlags(a.Flags, context) - if cerr != nil { - _ = ShowSubcommandHelp(context) - return cerr - } - - if a.After != nil { - defer func() { - afterErr := a.After(context) - if afterErr != nil { - a.handleExitCoder(context, err) - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if a.Before != nil { - beforeErr := a.Before(context) - if beforeErr != nil { - a.handleExitCoder(context, beforeErr) - err = beforeErr - return err - } - } - - args := context.Args() - if args.Present() { - name := args.First() - c := a.Command(name) - if c != nil { - return c.Run(context) - } - } - - // Run default Action - err = a.Action(context) - - a.handleExitCoder(context, err) - return err -} - -// Command returns the named command on App. Returns nil if the command does not exist -func (a *App) Command(name string) *Command { - for _, c := range a.Commands { - if c.HasName(name) { - return c - } - } - - return nil -} - -// VisibleCategories returns a slice of categories and commands that are -// Hidden=false -func (a *App) VisibleCategories() []CommandCategory { - ret := []CommandCategory{} - for _, category := range a.categories.Categories() { - if visible := func() CommandCategory { - if len(category.VisibleCommands()) > 0 { - return category - } - return nil - }(); visible != nil { - ret = append(ret, visible) - } - } - return ret -} - -// VisibleCommands returns a slice of the Commands with Hidden=false -func (a *App) VisibleCommands() []*Command { - var ret []*Command - for _, command := range a.Commands { - if !command.Hidden { - ret = append(ret, command) - } - } - return ret -} - -// VisibleFlags returns a slice of the Flags with Hidden=false -func (a *App) VisibleFlags() []Flag { - return visibleFlags(a.Flags) -} - -func (a *App) errWriter() io.Writer { - // When the app ErrWriter is nil use the package level one. - if a.ErrWriter == nil { - return ErrWriter - } - - return a.ErrWriter -} - -func (a *App) appendFlag(fl Flag) { - if !hasFlag(a.Flags, fl) { - a.Flags = append(a.Flags, fl) - } -} - -func (a *App) appendCommand(c *Command) { - if !hasCommand(a.Commands, c) { - a.Commands = append(a.Commands, c) - } -} - -func (a *App) handleExitCoder(context *Context, err error) { - if a.ExitErrHandler != nil { - a.ExitErrHandler(context, err) - } else { - HandleExitCoder(err) - } -} - -// Author represents someone who has contributed to a cli project. -type Author struct { - Name string // The Authors name - Email string // The Authors email -} - -// String makes Author comply to the Stringer interface, to allow an easy print in the templating process -func (a *Author) String() string { - e := "" - if a.Email != "" { - e = " <" + a.Email + ">" - } - - return fmt.Sprintf("%v%v", a.Name, e) -} - -// HandleAction attempts to figure out which Action signature was used. If -// it's an ActionFunc or a func with the legacy signature for Action, the func -// is run! -func HandleAction(action interface{}, context *Context) (err error) { - switch a := action.(type) { - case ActionFunc: - return a(context) - case func(*Context) error: - return a(context) - case func(*Context): // deprecated function signature - a(context) - return nil - } - - return errInvalidActionType -} diff --git a/vendor/github.com/urfave/cli/v2/args.go b/vendor/github.com/urfave/cli/v2/args.go deleted file mode 100644 index bd65c17b..00000000 --- a/vendor/github.com/urfave/cli/v2/args.go +++ /dev/null @@ -1,54 +0,0 @@ -package cli - -type Args interface { - // Get returns the nth argument, or else a blank string - Get(n int) string - // First returns the first argument, or else a blank string - First() string - // Tail returns the rest of the arguments (not the first one) - // or else an empty string slice - Tail() []string - // Len returns the length of the wrapped slice - Len() int - // Present checks if there are any arguments present - Present() bool - // Slice returns a copy of the internal slice - Slice() []string -} - -type args []string - -func (a *args) Get(n int) string { - if len(*a) > n { - return (*a)[n] - } - return "" -} - -func (a *args) First() string { - return a.Get(0) -} - -func (a *args) Tail() []string { - if a.Len() >= 2 { - tail := []string((*a)[1:]) - ret := make([]string, len(tail)) - copy(ret, tail) - return ret - } - return []string{} -} - -func (a *args) Len() int { - return len(*a) -} - -func (a *args) Present() bool { - return a.Len() != 0 -} - -func (a *args) Slice() []string { - ret := make([]string, len(*a)) - copy(ret, *a) - return ret -} diff --git a/vendor/github.com/urfave/cli/v2/category.go b/vendor/github.com/urfave/cli/v2/category.go deleted file mode 100644 index 867e3908..00000000 --- a/vendor/github.com/urfave/cli/v2/category.go +++ /dev/null @@ -1,79 +0,0 @@ -package cli - -// CommandCategories interface allows for category manipulation -type CommandCategories interface { - // AddCommand adds a command to a category, creating a new category if necessary. - AddCommand(category string, command *Command) - // categories returns a copy of the category slice - Categories() []CommandCategory -} - -type commandCategories []*commandCategory - -func newCommandCategories() CommandCategories { - ret := commandCategories([]*commandCategory{}) - return &ret -} - -func (c *commandCategories) Less(i, j int) bool { - return lexicographicLess((*c)[i].Name(), (*c)[j].Name()) -} - -func (c *commandCategories) Len() int { - return len(*c) -} - -func (c *commandCategories) Swap(i, j int) { - (*c)[i], (*c)[j] = (*c)[j], (*c)[i] -} - -func (c *commandCategories) AddCommand(category string, command *Command) { - for _, commandCategory := range []*commandCategory(*c) { - if commandCategory.name == category { - commandCategory.commands = append(commandCategory.commands, command) - return - } - } - newVal := append(*c, - &commandCategory{name: category, commands: []*Command{command}}) - *c = newVal -} - -func (c *commandCategories) Categories() []CommandCategory { - ret := make([]CommandCategory, len(*c)) - for i, cat := range *c { - ret[i] = cat - } - return ret -} - -// CommandCategory is a category containing commands. -type CommandCategory interface { - // Name returns the category name string - Name() string - // VisibleCommands returns a slice of the Commands with Hidden=false - VisibleCommands() []*Command -} - -type commandCategory struct { - name string - commands []*Command -} - -func (c *commandCategory) Name() string { - return c.name -} - -func (c *commandCategory) VisibleCommands() []*Command { - if c.commands == nil { - c.commands = []*Command{} - } - - var ret []*Command - for _, command := range c.commands { - if !command.Hidden { - ret = append(ret, command) - } - } - return ret -} diff --git a/vendor/github.com/urfave/cli/v2/cli.go b/vendor/github.com/urfave/cli/v2/cli.go deleted file mode 100644 index 62a5bc22..00000000 --- a/vendor/github.com/urfave/cli/v2/cli.go +++ /dev/null @@ -1,23 +0,0 @@ -// Package cli provides a minimal framework for creating and organizing command line -// Go applications. cli is designed to be easy to understand and write, the most simple -// cli application can be written as follows: -// func main() { -// (&cli.App{}).Run(os.Args) -// } -// -// Of course this application does not do much, so let's make this an actual application: -// func main() { -// app := &cli.App{ -// Name: "greet", -// Usage: "say a greeting", -// Action: func(c *cli.Context) error { -// fmt.Println("Greetings") -// return nil -// }, -// } -// -// app.Run(os.Args) -// } -package cli - -//go:generate go run flag-gen/main.go flag-gen/assets_vfsdata.go diff --git a/vendor/github.com/urfave/cli/v2/command.go b/vendor/github.com/urfave/cli/v2/command.go deleted file mode 100644 index 95840f32..00000000 --- a/vendor/github.com/urfave/cli/v2/command.go +++ /dev/null @@ -1,301 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "sort" - "strings" -) - -// Command is a subcommand for a cli.App. -type Command struct { - // The name of the command - Name string - // A list of aliases for the command - Aliases []string - // A short description of the usage of this command - Usage string - // Custom text to show on USAGE section of help - UsageText string - // A longer explanation of how the command works - Description string - // A short description of the arguments of this command - ArgsUsage string - // The category the command is part of - Category string - // The function to call when checking for bash command completions - BashComplete BashCompleteFunc - // An action to execute before any sub-subcommands are run, but after the context is ready - // If a non-nil error is returned, no sub-subcommands are run - Before BeforeFunc - // An action to execute after any subcommands are run, but after the subcommand has finished - // It is run even if Action() panics - After AfterFunc - // The function to call when this command is invoked - Action ActionFunc - // Execute this function if a usage error occurs. - OnUsageError OnUsageErrorFunc - // List of child commands - Subcommands []*Command - // List of flags to parse - Flags []Flag - // Treat all flags as normal arguments if true - SkipFlagParsing bool - // Boolean to hide built-in help command and help flag - HideHelp bool - // Boolean to hide built-in help command but keep help flag - // Ignored if HideHelp is true. - HideHelpCommand bool - // Boolean to hide this command from help or completion - Hidden bool - // Boolean to enable short-option handling so user can combine several - // single-character bool arguments into one - // i.e. foobar -o -v -> foobar -ov - UseShortOptionHandling bool - - // Full name of command for help, defaults to full command name, including parent commands. - HelpName string - commandNamePath []string - - // CustomHelpTemplate the text template for the command help topic. - // cli.go uses text/template to render templates. You can - // render custom help text by setting this variable. - CustomHelpTemplate string -} - -type Commands []*Command - -type CommandsByName []*Command - -func (c CommandsByName) Len() int { - return len(c) -} - -func (c CommandsByName) Less(i, j int) bool { - return lexicographicLess(c[i].Name, c[j].Name) -} - -func (c CommandsByName) Swap(i, j int) { - c[i], c[j] = c[j], c[i] -} - -// FullName returns the full name of the command. -// For subcommands this ensures that parent commands are part of the command path -func (c *Command) FullName() string { - if c.commandNamePath == nil { - return c.Name - } - return strings.Join(c.commandNamePath, " ") -} - -// Run invokes the command given the context, parses ctx.Args() to generate command-specific flags -func (c *Command) Run(ctx *Context) (err error) { - if len(c.Subcommands) > 0 { - return c.startApp(ctx) - } - - if !c.HideHelp && HelpFlag != nil { - // append help to flags - c.appendFlag(HelpFlag) - } - - if ctx.App.UseShortOptionHandling { - c.UseShortOptionHandling = true - } - - set, err := c.parseFlags(ctx.Args(), ctx.shellComplete) - - context := NewContext(ctx.App, set, ctx) - context.Command = c - if checkCommandCompletions(context, c.Name) { - return nil - } - - if err != nil { - if c.OnUsageError != nil { - err = c.OnUsageError(context, err, false) - context.App.handleExitCoder(context, err) - return err - } - _, _ = fmt.Fprintln(context.App.Writer, "Incorrect Usage:", err.Error()) - _, _ = fmt.Fprintln(context.App.Writer) - _ = ShowCommandHelp(context, c.Name) - return err - } - - if checkCommandHelp(context, c.Name) { - return nil - } - - cerr := checkRequiredFlags(c.Flags, context) - if cerr != nil { - _ = ShowCommandHelp(context, c.Name) - return cerr - } - - if c.After != nil { - defer func() { - afterErr := c.After(context) - if afterErr != nil { - context.App.handleExitCoder(context, err) - if err != nil { - err = newMultiError(err, afterErr) - } else { - err = afterErr - } - } - }() - } - - if c.Before != nil { - err = c.Before(context) - if err != nil { - _ = ShowCommandHelp(context, c.Name) - context.App.handleExitCoder(context, err) - return err - } - } - - if c.Action == nil { - c.Action = helpSubcommand.Action - } - - context.Command = c - err = c.Action(context) - - if err != nil { - context.App.handleExitCoder(context, err) - } - return err -} - -func (c *Command) newFlagSet() (*flag.FlagSet, error) { - return flagSet(c.Name, c.Flags) -} - -func (c *Command) useShortOptionHandling() bool { - return c.UseShortOptionHandling -} - -func (c *Command) parseFlags(args Args, shellComplete bool) (*flag.FlagSet, error) { - set, err := c.newFlagSet() - if err != nil { - return nil, err - } - - if c.SkipFlagParsing { - return set, set.Parse(append([]string{"--"}, args.Tail()...)) - } - - err = parseIter(set, c, args.Tail(), shellComplete) - if err != nil { - return nil, err - } - - err = normalizeFlags(c.Flags, set) - if err != nil { - return nil, err - } - - return set, nil -} - -// Names returns the names including short names and aliases. -func (c *Command) Names() []string { - return append([]string{c.Name}, c.Aliases...) -} - -// HasName returns true if Command.Name matches given name -func (c *Command) HasName(name string) bool { - for _, n := range c.Names() { - if n == name { - return true - } - } - return false -} - -func (c *Command) startApp(ctx *Context) error { - app := &App{ - Metadata: ctx.App.Metadata, - Name: fmt.Sprintf("%s %s", ctx.App.Name, c.Name), - } - - if c.HelpName == "" { - app.HelpName = c.HelpName - } else { - app.HelpName = app.Name - } - - app.Usage = c.Usage - app.Description = c.Description - app.ArgsUsage = c.ArgsUsage - - // set CommandNotFound - app.CommandNotFound = ctx.App.CommandNotFound - app.CustomAppHelpTemplate = c.CustomHelpTemplate - - // set the flags and commands - app.Commands = c.Subcommands - app.Flags = c.Flags - app.HideHelp = c.HideHelp - app.HideHelpCommand = c.HideHelpCommand - - app.Version = ctx.App.Version - app.HideVersion = ctx.App.HideVersion - app.Compiled = ctx.App.Compiled - app.Writer = ctx.App.Writer - app.ErrWriter = ctx.App.ErrWriter - app.ExitErrHandler = ctx.App.ExitErrHandler - app.UseShortOptionHandling = ctx.App.UseShortOptionHandling - - app.categories = newCommandCategories() - for _, command := range c.Subcommands { - app.categories.AddCommand(command.Category, command) - } - - sort.Sort(app.categories.(*commandCategories)) - - // bash completion - app.EnableBashCompletion = ctx.App.EnableBashCompletion - if c.BashComplete != nil { - app.BashComplete = c.BashComplete - } - - // set the actions - app.Before = c.Before - app.After = c.After - if c.Action != nil { - app.Action = c.Action - } else { - app.Action = helpSubcommand.Action - } - app.OnUsageError = c.OnUsageError - - for index, cc := range app.Commands { - app.Commands[index].commandNamePath = []string{c.Name, cc.Name} - } - - return app.RunAsSubcommand(ctx) -} - -// VisibleFlags returns a slice of the Flags with Hidden=false -func (c *Command) VisibleFlags() []Flag { - return visibleFlags(c.Flags) -} - -func (c *Command) appendFlag(fl Flag) { - if !hasFlag(c.Flags, fl) { - c.Flags = append(c.Flags, fl) - } -} - -func hasCommand(commands []*Command, command *Command) bool { - for _, existing := range commands { - if command == existing { - return true - } - } - - return false -} diff --git a/vendor/github.com/urfave/cli/v2/context.go b/vendor/github.com/urfave/cli/v2/context.go deleted file mode 100644 index 74ed5191..00000000 --- a/vendor/github.com/urfave/cli/v2/context.go +++ /dev/null @@ -1,273 +0,0 @@ -package cli - -import ( - "context" - "errors" - "flag" - "fmt" - "strings" -) - -// Context is a type that is passed through to -// each Handler action in a cli application. Context -// can be used to retrieve context-specific args and -// parsed command-line options. -type Context struct { - context.Context - App *App - Command *Command - shellComplete bool - flagSet *flag.FlagSet - parentContext *Context -} - -// NewContext creates a new context. For use in when invoking an App or Command action. -func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { - c := &Context{App: app, flagSet: set, parentContext: parentCtx} - if parentCtx != nil { - c.Context = parentCtx.Context - c.shellComplete = parentCtx.shellComplete - if parentCtx.flagSet == nil { - parentCtx.flagSet = &flag.FlagSet{} - } - } - - c.Command = &Command{} - - if c.Context == nil { - c.Context = context.Background() - } - - return c -} - -// NumFlags returns the number of flags set -func (c *Context) NumFlags() int { - return c.flagSet.NFlag() -} - -// Set sets a context flag to a value. -func (c *Context) Set(name, value string) error { - return c.flagSet.Set(name, value) -} - -// IsSet determines if the flag was actually set -func (c *Context) IsSet(name string) bool { - if fs := lookupFlagSet(name, c); fs != nil { - if fs := lookupFlagSet(name, c); fs != nil { - isSet := false - fs.Visit(func(f *flag.Flag) { - if f.Name == name { - isSet = true - } - }) - if isSet { - return true - } - } - - f := lookupFlag(name, c) - if f == nil { - return false - } - - return f.IsSet() - } - - return false -} - -// LocalFlagNames returns a slice of flag names used in this context. -func (c *Context) LocalFlagNames() []string { - var names []string - c.flagSet.Visit(makeFlagNameVisitor(&names)) - return names -} - -// FlagNames returns a slice of flag names used by the this context and all of -// its parent contexts. -func (c *Context) FlagNames() []string { - var names []string - for _, ctx := range c.Lineage() { - ctx.flagSet.Visit(makeFlagNameVisitor(&names)) - } - return names -} - -// Lineage returns *this* context and all of its ancestor contexts in order from -// child to parent -func (c *Context) Lineage() []*Context { - var lineage []*Context - - for cur := c; cur != nil; cur = cur.parentContext { - lineage = append(lineage, cur) - } - - return lineage -} - -// Value returns the value of the flag corresponding to `name` -func (c *Context) Value(name string) interface{} { - return c.flagSet.Lookup(name).Value.(flag.Getter).Get() -} - -// Args returns the command line arguments associated with the context. -func (c *Context) Args() Args { - ret := args(c.flagSet.Args()) - return &ret -} - -// NArg returns the number of the command line arguments. -func (c *Context) NArg() int { - return c.Args().Len() -} - -func lookupFlag(name string, ctx *Context) Flag { - for _, c := range ctx.Lineage() { - if c.Command == nil { - continue - } - - for _, f := range c.Command.Flags { - for _, n := range f.Names() { - if n == name { - return f - } - } - } - } - - if ctx.App != nil { - for _, f := range ctx.App.Flags { - for _, n := range f.Names() { - if n == name { - return f - } - } - } - } - - return nil -} - -func lookupFlagSet(name string, ctx *Context) *flag.FlagSet { - for _, c := range ctx.Lineage() { - if f := c.flagSet.Lookup(name); f != nil { - return c.flagSet - } - } - - return nil -} - -func copyFlag(name string, ff *flag.Flag, set *flag.FlagSet) { - switch ff.Value.(type) { - case Serializer: - _ = set.Set(name, ff.Value.(Serializer).Serialize()) - default: - _ = set.Set(name, ff.Value.String()) - } -} - -func normalizeFlags(flags []Flag, set *flag.FlagSet) error { - visited := make(map[string]bool) - set.Visit(func(f *flag.Flag) { - visited[f.Name] = true - }) - for _, f := range flags { - parts := f.Names() - if len(parts) == 1 { - continue - } - var ff *flag.Flag - for _, name := range parts { - name = strings.Trim(name, " ") - if visited[name] { - if ff != nil { - return errors.New("Cannot use two forms of the same flag: " + name + " " + ff.Name) - } - ff = set.Lookup(name) - } - } - if ff == nil { - continue - } - for _, name := range parts { - name = strings.Trim(name, " ") - if !visited[name] { - copyFlag(name, ff, set) - } - } - } - return nil -} - -func makeFlagNameVisitor(names *[]string) func(*flag.Flag) { - return func(f *flag.Flag) { - nameParts := strings.Split(f.Name, ",") - name := strings.TrimSpace(nameParts[0]) - - for _, part := range nameParts { - part = strings.TrimSpace(part) - if len(part) > len(name) { - name = part - } - } - - if name != "" { - *names = append(*names, name) - } - } -} - -type requiredFlagsErr interface { - error - getMissingFlags() []string -} - -type errRequiredFlags struct { - missingFlags []string -} - -func (e *errRequiredFlags) Error() string { - numberOfMissingFlags := len(e.missingFlags) - if numberOfMissingFlags == 1 { - return fmt.Sprintf("Required flag %q not set", e.missingFlags[0]) - } - joinedMissingFlags := strings.Join(e.missingFlags, ", ") - return fmt.Sprintf("Required flags %q not set", joinedMissingFlags) -} - -func (e *errRequiredFlags) getMissingFlags() []string { - return e.missingFlags -} - -func checkRequiredFlags(flags []Flag, context *Context) requiredFlagsErr { - var missingFlags []string - for _, f := range flags { - if rf, ok := f.(RequiredFlag); ok && rf.IsRequired() { - var flagPresent bool - var flagName string - - for _, key := range f.Names() { - if len(key) > 1 { - flagName = key - } - - if context.IsSet(strings.TrimSpace(key)) { - flagPresent = true - } - } - - if !flagPresent && flagName != "" { - missingFlags = append(missingFlags, flagName) - } - } - } - - if len(missingFlags) != 0 { - return &errRequiredFlags{missingFlags: missingFlags} - } - - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/docs.go b/vendor/github.com/urfave/cli/v2/docs.go deleted file mode 100644 index dc16fc82..00000000 --- a/vendor/github.com/urfave/cli/v2/docs.go +++ /dev/null @@ -1,148 +0,0 @@ -package cli - -import ( - "bytes" - "fmt" - "io" - "sort" - "strings" - "text/template" - - "github.com/cpuguy83/go-md2man/v2/md2man" -) - -// ToMarkdown creates a markdown string for the `*App` -// The function errors if either parsing or writing of the string fails. -func (a *App) ToMarkdown() (string, error) { - var w bytes.Buffer - if err := a.writeDocTemplate(&w); err != nil { - return "", err - } - return w.String(), nil -} - -// ToMan creates a man page string for the `*App` -// The function errors if either parsing or writing of the string fails. -func (a *App) ToMan() (string, error) { - var w bytes.Buffer - if err := a.writeDocTemplate(&w); err != nil { - return "", err - } - man := md2man.Render(w.Bytes()) - return string(man), nil -} - -type cliTemplate struct { - App *App - Commands []string - GlobalArgs []string - SynopsisArgs []string -} - -func (a *App) writeDocTemplate(w io.Writer) error { - const name = "cli" - t, err := template.New(name).Parse(MarkdownDocTemplate) - if err != nil { - return err - } - return t.ExecuteTemplate(w, name, &cliTemplate{ - App: a, - Commands: prepareCommands(a.Commands, 0), - GlobalArgs: prepareArgsWithValues(a.VisibleFlags()), - SynopsisArgs: prepareArgsSynopsis(a.VisibleFlags()), - }) -} - -func prepareCommands(commands []*Command, level int) []string { - var coms []string - for _, command := range commands { - if command.Hidden { - continue - } - usage := "" - if command.Usage != "" { - usage = command.Usage - } - - prepared := fmt.Sprintf("%s %s\n\n%s\n", - strings.Repeat("#", level+2), - strings.Join(command.Names(), ", "), - usage, - ) - - flags := prepareArgsWithValues(command.Flags) - if len(flags) > 0 { - prepared += fmt.Sprintf("\n%s", strings.Join(flags, "\n")) - } - - coms = append(coms, prepared) - - // recursevly iterate subcommands - if len(command.Subcommands) > 0 { - coms = append( - coms, - prepareCommands(command.Subcommands, level+1)..., - ) - } - } - - return coms -} - -func prepareArgsWithValues(flags []Flag) []string { - return prepareFlags(flags, ", ", "**", "**", `""`, true) -} - -func prepareArgsSynopsis(flags []Flag) []string { - return prepareFlags(flags, "|", "[", "]", "[value]", false) -} - -func prepareFlags( - flags []Flag, - sep, opener, closer, value string, - addDetails bool, -) []string { - args := []string{} - for _, f := range flags { - flag, ok := f.(DocGenerationFlag) - if !ok { - continue - } - modifiedArg := opener - - for _, s := range flag.Names() { - trimmed := strings.TrimSpace(s) - if len(modifiedArg) > len(opener) { - modifiedArg += sep - } - if len(trimmed) > 1 { - modifiedArg += fmt.Sprintf("--%s", trimmed) - } else { - modifiedArg += fmt.Sprintf("-%s", trimmed) - } - } - modifiedArg += closer - if flag.TakesValue() { - modifiedArg += fmt.Sprintf("=%s", value) - } - - if addDetails { - modifiedArg += flagDetails(flag) - } - - args = append(args, modifiedArg+"\n") - - } - sort.Strings(args) - return args -} - -// flagDetails returns a string containing the flags metadata -func flagDetails(flag DocGenerationFlag) string { - description := flag.GetUsage() - value := flag.GetValue() - if value != "" { - description += " (default: " + value + ")" - } - return ": " + description -} diff --git a/vendor/github.com/urfave/cli/v2/errors.go b/vendor/github.com/urfave/cli/v2/errors.go deleted file mode 100644 index 344b4361..00000000 --- a/vendor/github.com/urfave/cli/v2/errors.go +++ /dev/null @@ -1,131 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "os" - "strings" -) - -// OsExiter is the function used when the app exits. If not set defaults to os.Exit. -var OsExiter = os.Exit - -// ErrWriter is used to write errors to the user. This can be anything -// implementing the io.Writer interface and defaults to os.Stderr. -var ErrWriter io.Writer = os.Stderr - -// MultiError is an error that wraps multiple errors. -type MultiError interface { - error - // Errors returns a copy of the errors slice - Errors() []error -} - -// NewMultiError creates a new MultiError. Pass in one or more errors. -func newMultiError(err ...error) MultiError { - ret := multiError(err) - return &ret -} - -type multiError []error - -// Error implements the error interface. -func (m *multiError) Error() string { - errs := make([]string, len(*m)) - for i, err := range *m { - errs[i] = err.Error() - } - - return strings.Join(errs, "\n") -} - -// Errors returns a copy of the errors slice -func (m *multiError) Errors() []error { - errs := make([]error, len(*m)) - for _, err := range *m { - errs = append(errs, err) - } - return errs -} - -// ErrorFormatter is the interface that will suitably format the error output -type ErrorFormatter interface { - Format(s fmt.State, verb rune) -} - -// ExitCoder is the interface checked by `App` and `Command` for a custom exit -// code -type ExitCoder interface { - error - ExitCode() int -} - -type exitError struct { - exitCode int - message interface{} -} - -// NewExitError makes a new *exitError -func NewExitError(message interface{}, exitCode int) ExitCoder { - return Exit(message, exitCode) -} - -// Exit wraps a message and exit code into an ExitCoder suitable for handling by -// HandleExitCoder -func Exit(message interface{}, exitCode int) ExitCoder { - return &exitError{ - message: message, - exitCode: exitCode, - } -} - -func (ee *exitError) Error() string { - return fmt.Sprintf("%v", ee.message) -} - -func (ee *exitError) ExitCode() int { - return ee.exitCode -} - -// HandleExitCoder checks if the error fulfills the ExitCoder interface, and if -// so prints the error to stderr (if it is non-empty) and calls OsExiter with the -// given exit code. If the given error is a MultiError, then this func is -// called on all members of the Errors slice and calls OsExiter with the last exit code. -func HandleExitCoder(err error) { - if err == nil { - return - } - - if exitErr, ok := err.(ExitCoder); ok { - if err.Error() != "" { - if _, ok := exitErr.(ErrorFormatter); ok { - _, _ = fmt.Fprintf(ErrWriter, "%+v\n", err) - } else { - _, _ = fmt.Fprintln(ErrWriter, err) - } - } - OsExiter(exitErr.ExitCode()) - return - } - - if multiErr, ok := err.(MultiError); ok { - code := handleMultiError(multiErr) - OsExiter(code) - return - } -} - -func handleMultiError(multiErr MultiError) int { - code := 1 - for _, merr := range multiErr.Errors() { - if multiErr2, ok := merr.(MultiError); ok { - code = handleMultiError(multiErr2) - } else if merr != nil { - fmt.Fprintln(ErrWriter, merr) - if exitErr, ok := merr.(ExitCoder); ok { - code = exitErr.ExitCode() - } - } - } - return code -} diff --git a/vendor/github.com/urfave/cli/v2/fish.go b/vendor/github.com/urfave/cli/v2/fish.go deleted file mode 100644 index 67122c9f..00000000 --- a/vendor/github.com/urfave/cli/v2/fish.go +++ /dev/null @@ -1,192 +0,0 @@ -package cli - -import ( - "bytes" - "fmt" - "io" - "strings" - "text/template" -) - -// ToFishCompletion creates a fish completion string for the `*App` -// The function errors if either parsing or writing of the string fails. -func (a *App) ToFishCompletion() (string, error) { - var w bytes.Buffer - if err := a.writeFishCompletionTemplate(&w); err != nil { - return "", err - } - return w.String(), nil -} - -type fishCompletionTemplate struct { - App *App - Completions []string - AllCommands []string -} - -func (a *App) writeFishCompletionTemplate(w io.Writer) error { - const name = "cli" - t, err := template.New(name).Parse(FishCompletionTemplate) - if err != nil { - return err - } - allCommands := []string{} - - // Add global flags - completions := a.prepareFishFlags(a.VisibleFlags(), allCommands) - - // Add help flag - if !a.HideHelp { - completions = append( - completions, - a.prepareFishFlags([]Flag{HelpFlag}, allCommands)..., - ) - } - - // Add version flag - if !a.HideVersion { - completions = append( - completions, - a.prepareFishFlags([]Flag{VersionFlag}, allCommands)..., - ) - } - - // Add commands and their flags - completions = append( - completions, - a.prepareFishCommands(a.VisibleCommands(), &allCommands, []string{})..., - ) - - return t.ExecuteTemplate(w, name, &fishCompletionTemplate{ - App: a, - Completions: completions, - AllCommands: allCommands, - }) -} - -func (a *App) prepareFishCommands(commands []*Command, allCommands *[]string, previousCommands []string) []string { - completions := []string{} - for _, command := range commands { - if command.Hidden { - continue - } - - var completion strings.Builder - completion.WriteString(fmt.Sprintf( - "complete -r -c %s -n '%s' -a '%s'", - a.Name, - a.fishSubcommandHelper(previousCommands), - strings.Join(command.Names(), " "), - )) - - if command.Usage != "" { - completion.WriteString(fmt.Sprintf(" -d '%s'", - escapeSingleQuotes(command.Usage))) - } - - if !command.HideHelp { - completions = append( - completions, - a.prepareFishFlags([]Flag{HelpFlag}, command.Names())..., - ) - } - - *allCommands = append(*allCommands, command.Names()...) - completions = append(completions, completion.String()) - completions = append( - completions, - a.prepareFishFlags(command.Flags, command.Names())..., - ) - - // recursevly iterate subcommands - if len(command.Subcommands) > 0 { - completions = append( - completions, - a.prepareFishCommands( - command.Subcommands, allCommands, command.Names(), - )..., - ) - } - } - - return completions -} - -func (a *App) prepareFishFlags(flags []Flag, previousCommands []string) []string { - completions := []string{} - for _, f := range flags { - flag, ok := f.(DocGenerationFlag) - if !ok { - continue - } - - completion := &strings.Builder{} - completion.WriteString(fmt.Sprintf( - "complete -c %s -n '%s'", - a.Name, - a.fishSubcommandHelper(previousCommands), - )) - - fishAddFileFlag(f, completion) - - for idx, opt := range flag.Names() { - if idx == 0 { - completion.WriteString(fmt.Sprintf( - " -l %s", strings.TrimSpace(opt), - )) - } else { - completion.WriteString(fmt.Sprintf( - " -s %s", strings.TrimSpace(opt), - )) - - } - } - - if flag.TakesValue() { - completion.WriteString(" -r") - } - - if flag.GetUsage() != "" { - completion.WriteString(fmt.Sprintf(" -d '%s'", - escapeSingleQuotes(flag.GetUsage()))) - } - - completions = append(completions, completion.String()) - } - - return completions -} - -func fishAddFileFlag(flag Flag, completion *strings.Builder) { - switch f := flag.(type) { - case *GenericFlag: - if f.TakesFile { - return - } - case *StringFlag: - if f.TakesFile { - return - } - case *StringSliceFlag: - if f.TakesFile { - return - } - } - completion.WriteString(" -f") -} - -func (a *App) fishSubcommandHelper(allCommands []string) string { - fishHelper := fmt.Sprintf("__fish_%s_no_subcommand", a.Name) - if len(allCommands) > 0 { - fishHelper = fmt.Sprintf( - "__fish_seen_subcommand_from %s", - strings.Join(allCommands, " "), - ) - } - return fishHelper - -} - -func escapeSingleQuotes(input string) string { - return strings.Replace(input, `'`, `\'`, -1) -} diff --git a/vendor/github.com/urfave/cli/v2/flag.go b/vendor/github.com/urfave/cli/v2/flag.go deleted file mode 100644 index ad97c2d0..00000000 --- a/vendor/github.com/urfave/cli/v2/flag.go +++ /dev/null @@ -1,388 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "io/ioutil" - "reflect" - "regexp" - "runtime" - "strconv" - "strings" - "syscall" - "time" -) - -const defaultPlaceholder = "value" - -var ( - slPfx = fmt.Sprintf("sl:::%d:::", time.Now().UTC().UnixNano()) - - commaWhitespace = regexp.MustCompile("[, ]+.*") -) - -// BashCompletionFlag enables bash-completion for all commands and subcommands -var BashCompletionFlag Flag = &BoolFlag{ - Name: "generate-bash-completion", - Hidden: true, -} - -// VersionFlag prints the version for the application -var VersionFlag Flag = &BoolFlag{ - Name: "version", - Aliases: []string{"v"}, - Usage: "print the version", -} - -// HelpFlag prints the help for all commands and subcommands. -// Set to nil to disable the flag. The subcommand -// will still be added unless HideHelp or HideHelpCommand is set to true. -var HelpFlag Flag = &BoolFlag{ - Name: "help", - Aliases: []string{"h"}, - Usage: "show help", -} - -// FlagStringer converts a flag definition to a string. This is used by help -// to display a flag. -var FlagStringer FlagStringFunc = stringifyFlag - -// Serializer is used to circumvent the limitations of flag.FlagSet.Set -type Serializer interface { - Serialize() string -} - -// FlagNamePrefixer converts a full flag name and its placeholder into the help -// message flag prefix. This is used by the default FlagStringer. -var FlagNamePrefixer FlagNamePrefixFunc = prefixedNames - -// FlagEnvHinter annotates flag help message with the environment variable -// details. This is used by the default FlagStringer. -var FlagEnvHinter FlagEnvHintFunc = withEnvHint - -// FlagFileHinter annotates flag help message with the environment variable -// details. This is used by the default FlagStringer. -var FlagFileHinter FlagFileHintFunc = withFileHint - -// FlagsByName is a slice of Flag. -type FlagsByName []Flag - -func (f FlagsByName) Len() int { - return len(f) -} - -func (f FlagsByName) Less(i, j int) bool { - if len(f[j].Names()) == 0 { - return false - } else if len(f[i].Names()) == 0 { - return true - } - return lexicographicLess(f[i].Names()[0], f[j].Names()[0]) -} - -func (f FlagsByName) Swap(i, j int) { - f[i], f[j] = f[j], f[i] -} - -// Flag is a common interface related to parsing flags in cli. -// For more advanced flag parsing techniques, it is recommended that -// this interface be implemented. -type Flag interface { - fmt.Stringer - // Apply Flag settings to the given flag set - Apply(*flag.FlagSet) error - Names() []string - IsSet() bool -} - -// RequiredFlag is an interface that allows us to mark flags as required -// it allows flags required flags to be backwards compatible with the Flag interface -type RequiredFlag interface { - Flag - - IsRequired() bool -} - -// DocGenerationFlag is an interface that allows documentation generation for the flag -type DocGenerationFlag interface { - Flag - - // TakesValue returns true if the flag takes a value, otherwise false - TakesValue() bool - - // GetUsage returns the usage string for the flag - GetUsage() string - - // GetValue returns the flags value as string representation and an empty - // string if the flag takes no value at all. - GetValue() string -} - -func flagSet(name string, flags []Flag) (*flag.FlagSet, error) { - set := flag.NewFlagSet(name, flag.ContinueOnError) - - for _, f := range flags { - if err := f.Apply(set); err != nil { - return nil, err - } - } - set.SetOutput(ioutil.Discard) - return set, nil -} - -func visibleFlags(fl []Flag) []Flag { - var visible []Flag - for _, f := range fl { - field := flagValue(f).FieldByName("Hidden") - if !field.IsValid() || !field.Bool() { - visible = append(visible, f) - } - } - return visible -} - -func prefixFor(name string) (prefix string) { - if len(name) == 1 { - prefix = "-" - } else { - prefix = "--" - } - - return -} - -// Returns the placeholder, if any, and the unquoted usage string. -func unquoteUsage(usage string) (string, string) { - for i := 0; i < len(usage); i++ { - if usage[i] == '`' { - for j := i + 1; j < len(usage); j++ { - if usage[j] == '`' { - name := usage[i+1 : j] - usage = usage[:i] + name + usage[j+1:] - return name, usage - } - } - break - } - } - return "", usage -} - -func prefixedNames(names []string, placeholder string) string { - var prefixed string - for i, name := range names { - if name == "" { - continue - } - - prefixed += prefixFor(name) + name - if placeholder != "" { - prefixed += " " + placeholder - } - if i < len(names)-1 { - prefixed += ", " - } - } - return prefixed -} - -func withEnvHint(envVars []string, str string) string { - envText := "" - if envVars != nil && len(envVars) > 0 { - prefix := "$" - suffix := "" - sep := ", $" - if runtime.GOOS == "windows" { - prefix = "%" - suffix = "%" - sep = "%, %" - } - - envText = fmt.Sprintf(" [%s%s%s]", prefix, strings.Join(envVars, sep), suffix) - } - return str + envText -} - -func flagNames(name string, aliases []string) []string { - var ret []string - - for _, part := range append([]string{name}, aliases...) { - // v1 -> v2 migration warning zone: - // Strip off anything after the first found comma or space, which - // *hopefully* makes it a tiny bit more obvious that unexpected behavior is - // caused by using the v1 form of stringly typed "Name". - ret = append(ret, commaWhitespace.ReplaceAllString(part, "")) - } - - return ret -} - -func flagStringSliceField(f Flag, name string) []string { - fv := flagValue(f) - field := fv.FieldByName(name) - - if field.IsValid() { - return field.Interface().([]string) - } - - return []string{} -} - -func withFileHint(filePath, str string) string { - fileText := "" - if filePath != "" { - fileText = fmt.Sprintf(" [%s]", filePath) - } - return str + fileText -} - -func flagValue(f Flag) reflect.Value { - fv := reflect.ValueOf(f) - for fv.Kind() == reflect.Ptr { - fv = reflect.Indirect(fv) - } - return fv -} - -func formatDefault(format string) string { - return " (default: " + format + ")" -} - -func stringifyFlag(f Flag) string { - fv := flagValue(f) - - switch f := f.(type) { - case *IntSliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyIntSliceFlag(f)) - case *Int64SliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyInt64SliceFlag(f)) - case *Float64SliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyFloat64SliceFlag(f)) - case *StringSliceFlag: - return withEnvHint(flagStringSliceField(f, "EnvVars"), - stringifyStringSliceFlag(f)) - } - - placeholder, usage := unquoteUsage(fv.FieldByName("Usage").String()) - - needsPlaceholder := false - defaultValueString := "" - val := fv.FieldByName("Value") - if val.IsValid() { - needsPlaceholder = val.Kind() != reflect.Bool - defaultValueString = fmt.Sprintf(formatDefault("%v"), val.Interface()) - - if val.Kind() == reflect.String && val.String() != "" { - defaultValueString = fmt.Sprintf(formatDefault("%q"), val.String()) - } - } - - helpText := fv.FieldByName("DefaultText") - if helpText.IsValid() && helpText.String() != "" { - needsPlaceholder = val.Kind() != reflect.Bool - defaultValueString = fmt.Sprintf(formatDefault("%s"), helpText.String()) - } - - if defaultValueString == formatDefault("") { - defaultValueString = "" - } - - if needsPlaceholder && placeholder == "" { - placeholder = defaultPlaceholder - } - - usageWithDefault := strings.TrimSpace(usage + defaultValueString) - - return withEnvHint(flagStringSliceField(f, "EnvVars"), - fmt.Sprintf("%s\t%s", prefixedNames(f.Names(), placeholder), usageWithDefault)) -} - -func stringifyIntSliceFlag(f *IntSliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.Itoa(i)) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyInt64SliceFlag(f *Int64SliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strconv.FormatInt(i, 10)) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyFloat64SliceFlag(f *Float64SliceFlag) string { - var defaultVals []string - - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, i := range f.Value.Value() { - defaultVals = append(defaultVals, strings.TrimRight(strings.TrimRight(fmt.Sprintf("%f", i), "0"), ".")) - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifyStringSliceFlag(f *StringSliceFlag) string { - var defaultVals []string - if f.Value != nil && len(f.Value.Value()) > 0 { - for _, s := range f.Value.Value() { - if len(s) > 0 { - defaultVals = append(defaultVals, strconv.Quote(s)) - } - } - } - - return stringifySliceFlag(f.Usage, f.Names(), defaultVals) -} - -func stringifySliceFlag(usage string, names, defaultVals []string) string { - placeholder, usage := unquoteUsage(usage) - if placeholder == "" { - placeholder = defaultPlaceholder - } - - defaultVal := "" - if len(defaultVals) > 0 { - defaultVal = fmt.Sprintf(formatDefault("%s"), strings.Join(defaultVals, ", ")) - } - - usageWithDefault := strings.TrimSpace(fmt.Sprintf("%s%s", usage, defaultVal)) - return fmt.Sprintf("%s\t%s", prefixedNames(names, placeholder), usageWithDefault) -} - -func hasFlag(flags []Flag, fl Flag) bool { - for _, existing := range flags { - if fl == existing { - return true - } - } - - return false -} - -func flagFromEnvOrFile(envVars []string, filePath string) (val string, ok bool) { - for _, envVar := range envVars { - envVar = strings.TrimSpace(envVar) - if val, ok := syscall.Getenv(envVar); ok { - return val, true - } - } - for _, fileVar := range strings.Split(filePath, ",") { - if data, err := ioutil.ReadFile(fileVar); err == nil { - return string(data), true - } - } - return "", false -} diff --git a/vendor/github.com/urfave/cli/v2/flag_bool.go b/vendor/github.com/urfave/cli/v2/flag_bool.go deleted file mode 100644 index bc9ea35d..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_bool.go +++ /dev/null @@ -1,106 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// BoolFlag is a flag with type bool -type BoolFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value bool - DefaultText string - Destination *bool - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *BoolFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *BoolFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *BoolFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *BoolFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *BoolFlag) TakesValue() bool { - return false -} - -// GetUsage returns the usage string for the flag -func (f *BoolFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *BoolFlag) GetValue() string { - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *BoolFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valBool, err := strconv.ParseBool(val) - - if err != nil { - return fmt.Errorf("could not parse %q as bool value for flag %s: %s", val, f.Name, err) - } - - f.Value = valBool - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.BoolVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Bool(name, f.Value, f.Usage) - } - - return nil -} - -// Bool looks up the value of a local BoolFlag, returns -// false if not found -func (c *Context) Bool(name string) bool { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupBool(name, fs) - } - return false -} - -func lookupBool(name string, set *flag.FlagSet) bool { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseBool(f.Value.String()) - if err != nil { - return false - } - return parsed - } - return false -} diff --git a/vendor/github.com/urfave/cli/v2/flag_duration.go b/vendor/github.com/urfave/cli/v2/flag_duration.go deleted file mode 100644 index 22a2e672..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_duration.go +++ /dev/null @@ -1,105 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "time" -) - -// DurationFlag is a flag with type time.Duration (see https://golang.org/pkg/time/#ParseDuration) -type DurationFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value time.Duration - DefaultText string - Destination *time.Duration - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *DurationFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *DurationFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *DurationFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *DurationFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *DurationFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *DurationFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *DurationFlag) GetValue() string { - return f.Value.String() -} - -// Apply populates the flag given the flag set and environment -func (f *DurationFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valDuration, err := time.ParseDuration(val) - - if err != nil { - return fmt.Errorf("could not parse %q as duration value for flag %s: %s", val, f.Name, err) - } - - f.Value = valDuration - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.DurationVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Duration(name, f.Value, f.Usage) - } - return nil -} - -// Duration looks up the value of a local DurationFlag, returns -// 0 if not found -func (c *Context) Duration(name string) time.Duration { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupDuration(name, fs) - } - return 0 -} - -func lookupDuration(name string, set *flag.FlagSet) time.Duration { - f := set.Lookup(name) - if f != nil { - parsed, err := time.ParseDuration(f.Value.String()) - if err != nil { - return 0 - } - return parsed - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/flag_float64.go b/vendor/github.com/urfave/cli/v2/flag_float64.go deleted file mode 100644 index 91c778c8..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_float64.go +++ /dev/null @@ -1,106 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// Float64Flag is a flag with type float64 -type Float64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value float64 - DefaultText string - Destination *float64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Float64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Float64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Float64Flag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *Float64Flag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *Float64Flag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *Float64Flag) GetValue() string { - return fmt.Sprintf("%f", f.Value) -} - -// Apply populates the flag given the flag set and environment -func (f *Float64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valFloat, err := strconv.ParseFloat(val, 10) - - if err != nil { - return fmt.Errorf("could not parse %q as float64 value for flag %s: %s", val, f.Name, err) - } - - f.Value = valFloat - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.Float64Var(f.Destination, name, f.Value, f.Usage) - continue - } - set.Float64(name, f.Value, f.Usage) - } - - return nil -} - -// Float64 looks up the value of a local Float64Flag, returns -// 0 if not found -func (c *Context) Float64(name string) float64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupFloat64(name, fs) - } - return 0 -} - -func lookupFloat64(name string, set *flag.FlagSet) float64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseFloat(f.Value.String(), 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/flag_float64_slice.go b/vendor/github.com/urfave/cli/v2/flag_float64_slice.go deleted file mode 100644 index 706ee6cd..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_float64_slice.go +++ /dev/null @@ -1,163 +0,0 @@ -package cli - -import ( - "encoding/json" - "flag" - "fmt" - "strconv" - "strings" -) - -// Float64Slice wraps []float64 to satisfy flag.Value -type Float64Slice struct { - slice []float64 - hasBeenSet bool -} - -// NewFloat64Slice makes a *Float64Slice with default values -func NewFloat64Slice(defaults ...float64) *Float64Slice { - return &Float64Slice{slice: append([]float64{}, defaults...)} -} - -// Set parses the value into a float64 and appends it to the list of values -func (f *Float64Slice) Set(value string) error { - if !f.hasBeenSet { - f.slice = []float64{} - f.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &f.slice) - f.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseFloat(value, 64) - if err != nil { - return err - } - - f.slice = append(f.slice, tmp) - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (f *Float64Slice) String() string { - return fmt.Sprintf("%#v", f.slice) -} - -// Serialize allows Float64Slice to fulfill Serializer -func (f *Float64Slice) Serialize() string { - jsonBytes, _ := json.Marshal(f.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of float64s set by this flag -func (f *Float64Slice) Value() []float64 { - return f.slice -} - -// Get returns the slice of float64s set by this flag -func (f *Float64Slice) Get() interface{} { - return *f -} - -// Float64SliceFlag is a flag with type *Float64Slice -type Float64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *Float64Slice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Float64SliceFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Float64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Float64SliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Float64SliceFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true if the flag takes a value, otherwise false -func (f *Float64SliceFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *Float64SliceFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *Float64SliceFlag) GetValue() string { - if f.Value != nil { - return f.Value.String() - } - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *Float64SliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - f.Value = &Float64Slice{} - - for _, s := range strings.Split(val, ",") { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as float64 slice value for flag %s: %s", f.Value, f.Name, err) - } - } - - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Value == nil { - f.Value = &Float64Slice{} - } - set.Var(f.Value, name, f.Usage) - } - - return nil -} - -// Float64Slice looks up the value of a local Float64SliceFlag, returns -// nil if not found -func (c *Context) Float64Slice(name string) []float64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupFloat64Slice(name, fs) - } - return nil -} - -func lookupFloat64Slice(name string, set *flag.FlagSet) []float64 { - f := set.Lookup(name) - if f != nil { - if slice, ok := f.Value.(*Float64Slice); ok { - return slice.Value() - } - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_generic.go b/vendor/github.com/urfave/cli/v2/flag_generic.go deleted file mode 100644 index b0c8ff44..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_generic.go +++ /dev/null @@ -1,108 +0,0 @@ -package cli - -import ( - "flag" - "fmt" -) - -// Generic is a generic parseable type identified by a specific flag -type Generic interface { - Set(value string) error - String() string -} - -// GenericFlag is a flag with type Generic -type GenericFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value Generic - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *GenericFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *GenericFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *GenericFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *GenericFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *GenericFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *GenericFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *GenericFlag) GetValue() string { - if f.Value != nil { - return f.Value.String() - } - return "" -} - -// Apply takes the flagset and calls Set on the generic flag with the value -// provided by the user for parsing by the flag -func (f GenericFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - if err := f.Value.Set(val); err != nil { - return fmt.Errorf("could not parse %q as value for flag %s: %s", val, f.Name, err) - } - - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - - return nil -} - -// Generic looks up the value of a local GenericFlag, returns -// nil if not found -func (c *Context) Generic(name string) interface{} { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupGeneric(name, fs) - } - return nil -} - -func lookupGeneric(name string, set *flag.FlagSet) interface{} { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value, error(nil) - if err != nil { - return nil - } - return parsed - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_int.go b/vendor/github.com/urfave/cli/v2/flag_int.go deleted file mode 100644 index ac39d4a9..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_int.go +++ /dev/null @@ -1,106 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// IntFlag is a flag with type int -type IntFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value int - DefaultText string - Destination *int - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *IntFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *IntFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *IntFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *IntFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *IntFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *IntFlag) GetValue() string { - return fmt.Sprintf("%d", f.Value) -} - -// Apply populates the flag given the flag set and environment -func (f *IntFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valInt, err := strconv.ParseInt(val, 0, 64) - - if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) - } - - f.Value = int(valInt) - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.IntVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Int(name, f.Value, f.Usage) - } - - return nil -} - -// Int looks up the value of a local IntFlag, returns -// 0 if not found -func (c *Context) Int(name string) int { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupInt(name, fs) - } - return 0 -} - -func lookupInt(name string, set *flag.FlagSet) int { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return int(parsed) - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/flag_int64.go b/vendor/github.com/urfave/cli/v2/flag_int64.go deleted file mode 100644 index e0999126..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_int64.go +++ /dev/null @@ -1,105 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// Int64Flag is a flag with type int64 -type Int64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value int64 - DefaultText string - Destination *int64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Int64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Int64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Int64Flag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *Int64Flag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *Int64Flag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *Int64Flag) GetValue() string { - return fmt.Sprintf("%d", f.Value) -} - -// Apply populates the flag given the flag set and environment -func (f *Int64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valInt, err := strconv.ParseInt(val, 0, 64) - - if err != nil { - return fmt.Errorf("could not parse %q as int value for flag %s: %s", val, f.Name, err) - } - - f.Value = valInt - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.Int64Var(f.Destination, name, f.Value, f.Usage) - continue - } - set.Int64(name, f.Value, f.Usage) - } - return nil -} - -// Int64 looks up the value of a local Int64Flag, returns -// 0 if not found -func (c *Context) Int64(name string) int64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupInt64(name, fs) - } - return 0 -} - -func lookupInt64(name string, set *flag.FlagSet) int64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseInt(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/flag_int64_slice.go b/vendor/github.com/urfave/cli/v2/flag_int64_slice.go deleted file mode 100644 index 6c7fd937..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_int64_slice.go +++ /dev/null @@ -1,159 +0,0 @@ -package cli - -import ( - "encoding/json" - "flag" - "fmt" - "strconv" - "strings" -) - -// Int64Slice wraps []int64 to satisfy flag.Value -type Int64Slice struct { - slice []int64 - hasBeenSet bool -} - -// NewInt64Slice makes an *Int64Slice with default values -func NewInt64Slice(defaults ...int64) *Int64Slice { - return &Int64Slice{slice: append([]int64{}, defaults...)} -} - -// Set parses the value into an integer and appends it to the list of values -func (i *Int64Slice) Set(value string) error { - if !i.hasBeenSet { - i.slice = []int64{} - i.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) - i.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } - - i.slice = append(i.slice, tmp) - - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (i *Int64Slice) String() string { - return fmt.Sprintf("%#v", i.slice) -} - -// Serialize allows Int64Slice to fulfill Serializer -func (i *Int64Slice) Serialize() string { - jsonBytes, _ := json.Marshal(i.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of ints set by this flag -func (i *Int64Slice) Value() []int64 { - return i.slice -} - -// Get returns the slice of ints set by this flag -func (i *Int64Slice) Get() interface{} { - return *i -} - -// Int64SliceFlag is a flag with type *Int64Slice -type Int64SliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *Int64Slice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Int64SliceFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Int64SliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Int64SliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Int64SliceFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *Int64SliceFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f Int64SliceFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *Int64SliceFlag) GetValue() string { - if f.Value != nil { - return f.Value.String() - } - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *Int64SliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = &Int64Slice{} - - for _, s := range strings.Split(val, ",") { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as int64 slice value for flag %s: %s", val, f.Name, err) - } - } - - f.HasBeenSet = true - } - - for _, name := range f.Names() { - if f.Value == nil { - f.Value = &Int64Slice{} - } - set.Var(f.Value, name, f.Usage) - } - - return nil -} - -// Int64Slice looks up the value of a local Int64SliceFlag, returns -// nil if not found -func (c *Context) Int64Slice(name string) []int64 { - return lookupInt64Slice(name, c.flagSet) -} - -func lookupInt64Slice(name string, set *flag.FlagSet) []int64 { - f := set.Lookup(name) - if f != nil { - if slice, ok := f.Value.(*Int64Slice); ok { - return slice.Value() - } - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_int_slice.go b/vendor/github.com/urfave/cli/v2/flag_int_slice.go deleted file mode 100644 index 4e0afc02..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_int_slice.go +++ /dev/null @@ -1,173 +0,0 @@ -package cli - -import ( - "encoding/json" - "flag" - "fmt" - "strconv" - "strings" -) - -// IntSlice wraps []int to satisfy flag.Value -type IntSlice struct { - slice []int - hasBeenSet bool -} - -// NewIntSlice makes an *IntSlice with default values -func NewIntSlice(defaults ...int) *IntSlice { - return &IntSlice{slice: append([]int{}, defaults...)} -} - -// TODO: Consistently have specific Set function for Int64 and Float64 ? -// SetInt directly adds an integer to the list of values -func (i *IntSlice) SetInt(value int) { - if !i.hasBeenSet { - i.slice = []int{} - i.hasBeenSet = true - } - - i.slice = append(i.slice, value) -} - -// Set parses the value into an integer and appends it to the list of values -func (i *IntSlice) Set(value string) error { - if !i.hasBeenSet { - i.slice = []int{} - i.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &i.slice) - i.hasBeenSet = true - return nil - } - - tmp, err := strconv.ParseInt(value, 0, 64) - if err != nil { - return err - } - - i.slice = append(i.slice, int(tmp)) - - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (i *IntSlice) String() string { - return fmt.Sprintf("%#v", i.slice) -} - -// Serialize allows IntSlice to fulfill Serializer -func (i *IntSlice) Serialize() string { - jsonBytes, _ := json.Marshal(i.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of ints set by this flag -func (i *IntSlice) Value() []int { - return i.slice -} - -// Get returns the slice of ints set by this flag -func (i *IntSlice) Get() interface{} { - return *i -} - -// IntSliceFlag is a flag with type *IntSlice -type IntSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value *IntSlice - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *IntSliceFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *IntSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *IntSliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *IntSliceFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *IntSliceFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f IntSliceFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *IntSliceFlag) GetValue() string { - if f.Value != nil { - return f.Value.String() - } - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *IntSliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = &IntSlice{} - - for _, s := range strings.Split(val, ",") { - if err := f.Value.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as int slice value for flag %s: %s", val, f.Name, err) - } - } - - f.HasBeenSet = true - } - - for _, name := range f.Names() { - if f.Value == nil { - f.Value = &IntSlice{} - } - set.Var(f.Value, name, f.Usage) - } - - return nil -} - -// IntSlice looks up the value of a local IntSliceFlag, returns -// nil if not found -func (c *Context) IntSlice(name string) []int { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupIntSlice(name, c.flagSet) - } - return nil -} - -func lookupIntSlice(name string, set *flag.FlagSet) []int { - f := set.Lookup(name) - if f != nil { - if slice, ok := f.Value.(*IntSlice); ok { - return slice.Value() - } - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_path.go b/vendor/github.com/urfave/cli/v2/flag_path.go deleted file mode 100644 index 8070dc4b..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_path.go +++ /dev/null @@ -1,95 +0,0 @@ -package cli - -import "flag" - -type PathFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value string - DefaultText string - Destination *string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *PathFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *PathFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *PathFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *PathFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *PathFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *PathFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *PathFlag) GetValue() string { - return f.Value -} - -// Apply populates the flag given the flag set and environment -func (f *PathFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = val - f.HasBeenSet = true - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.String(name, f.Value, f.Usage) - } - - return nil -} - -// Path looks up the value of a local PathFlag, returns -// "" if not found -func (c *Context) Path(name string) string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupPath(name, fs) - } - - return "" -} - -func lookupPath(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value.String(), error(nil) - if err != nil { - return "" - } - return parsed - } - return "" -} diff --git a/vendor/github.com/urfave/cli/v2/flag_string.go b/vendor/github.com/urfave/cli/v2/flag_string.go deleted file mode 100644 index 400bb532..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_string.go +++ /dev/null @@ -1,95 +0,0 @@ -package cli - -import "flag" - -// StringFlag is a flag with type string -type StringFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value string - DefaultText string - Destination *string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *StringFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *StringFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *StringFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *StringFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *StringFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *StringFlag) GetValue() string { - return f.Value -} - -// Apply populates the flag given the flag set and environment -func (f *StringFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = val - f.HasBeenSet = true - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.StringVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.String(name, f.Value, f.Usage) - } - - return nil -} - -// String looks up the value of a local StringFlag, returns -// "" if not found -func (c *Context) String(name string) string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupString(name, fs) - } - return "" -} - -func lookupString(name string, set *flag.FlagSet) string { - f := set.Lookup(name) - if f != nil { - parsed, err := f.Value.String(), error(nil) - if err != nil { - return "" - } - return parsed - } - return "" -} diff --git a/vendor/github.com/urfave/cli/v2/flag_string_slice.go b/vendor/github.com/urfave/cli/v2/flag_string_slice.go deleted file mode 100644 index ac363bf6..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_string_slice.go +++ /dev/null @@ -1,171 +0,0 @@ -package cli - -import ( - "encoding/json" - "flag" - "fmt" - "strings" -) - -// StringSlice wraps a []string to satisfy flag.Value -type StringSlice struct { - slice []string - hasBeenSet bool -} - -// NewStringSlice creates a *StringSlice with default values -func NewStringSlice(defaults ...string) *StringSlice { - return &StringSlice{slice: append([]string{}, defaults...)} -} - -// Set appends the string value to the list of values -func (s *StringSlice) Set(value string) error { - if !s.hasBeenSet { - s.slice = []string{} - s.hasBeenSet = true - } - - if strings.HasPrefix(value, slPfx) { - // Deserializing assumes overwrite - _ = json.Unmarshal([]byte(strings.Replace(value, slPfx, "", 1)), &s.slice) - s.hasBeenSet = true - return nil - } - - s.slice = append(s.slice, value) - - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (s *StringSlice) String() string { - return fmt.Sprintf("%s", s.slice) -} - -// Serialize allows StringSlice to fulfill Serializer -func (s *StringSlice) Serialize() string { - jsonBytes, _ := json.Marshal(s.slice) - return fmt.Sprintf("%s%s", slPfx, string(jsonBytes)) -} - -// Value returns the slice of strings set by this flag -func (s *StringSlice) Value() []string { - return s.slice -} - -// Get returns the slice of strings set by this flag -func (s *StringSlice) Get() interface{} { - return *s -} - -// StringSliceFlag is a flag with type *StringSlice -type StringSliceFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - TakesFile bool - Value *StringSlice - DefaultText string - HasBeenSet bool - Destination *StringSlice -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *StringSliceFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *StringSliceFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *StringSliceFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *StringSliceFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *StringSliceFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *StringSliceFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *StringSliceFlag) GetValue() string { - if f.Value != nil { - return f.Value.String() - } - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *StringSliceFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - f.Value = &StringSlice{} - destination := f.Value - if f.Destination != nil { - destination = f.Destination - } - - for _, s := range strings.Split(val, ",") { - if err := destination.Set(strings.TrimSpace(s)); err != nil { - return fmt.Errorf("could not parse %q as string value for flag %s: %s", val, f.Name, err) - } - } - - // Set this to false so that we reset the slice if we then set values from - // flags that have already been set by the environment. - destination.hasBeenSet = false - f.HasBeenSet = true - } - - for _, name := range f.Names() { - if f.Value == nil { - f.Value = &StringSlice{} - } - - if f.Destination != nil { - set.Var(f.Destination, name, f.Usage) - continue - } - - set.Var(f.Value, name, f.Usage) - } - - return nil -} - -// StringSlice looks up the value of a local StringSliceFlag, returns -// nil if not found -func (c *Context) StringSlice(name string) []string { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupStringSlice(name, fs) - } - return nil -} - -func lookupStringSlice(name string, set *flag.FlagSet) []string { - f := set.Lookup(name) - if f != nil { - if slice, ok := f.Value.(*StringSlice); ok { - return slice.Value() - } - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_timestamp.go b/vendor/github.com/urfave/cli/v2/flag_timestamp.go deleted file mode 100644 index 9fac1d1e..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_timestamp.go +++ /dev/null @@ -1,152 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "time" -) - -// Timestamp wrap to satisfy golang's flag interface. -type Timestamp struct { - timestamp *time.Time - hasBeenSet bool - layout string -} - -// Timestamp constructor -func NewTimestamp(timestamp time.Time) *Timestamp { - return &Timestamp{timestamp: ×tamp} -} - -// Set the timestamp value directly -func (t *Timestamp) SetTimestamp(value time.Time) { - if !t.hasBeenSet { - t.timestamp = &value - t.hasBeenSet = true - } -} - -// Set the timestamp string layout for future parsing -func (t *Timestamp) SetLayout(layout string) { - t.layout = layout -} - -// Parses the string value to timestamp -func (t *Timestamp) Set(value string) error { - timestamp, err := time.Parse(t.layout, value) - if err != nil { - return err - } - - t.timestamp = ×tamp - t.hasBeenSet = true - return nil -} - -// String returns a readable representation of this value (for usage defaults) -func (t *Timestamp) String() string { - return fmt.Sprintf("%#v", t.timestamp) -} - -// Value returns the timestamp value stored in the flag -func (t *Timestamp) Value() *time.Time { - return t.timestamp -} - -// Get returns the flag structure -func (t *Timestamp) Get() interface{} { - return *t -} - -// TimestampFlag is a flag with type time -type TimestampFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Layout string - Value *Timestamp - DefaultText string - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *TimestampFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *TimestampFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *TimestampFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *TimestampFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *TimestampFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *TimestampFlag) GetUsage() string { - return f.Usage -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *TimestampFlag) GetValue() string { - if f.Value != nil { - return f.Value.timestamp.String() - } - return "" -} - -// Apply populates the flag given the flag set and environment -func (f *TimestampFlag) Apply(set *flag.FlagSet) error { - if f.Layout == "" { - return fmt.Errorf("timestamp Layout is required") - } - f.Value = &Timestamp{} - f.Value.SetLayout(f.Layout) - - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if err := f.Value.Set(val); err != nil { - return fmt.Errorf("could not parse %q as timestamp value for flag %s: %s", val, f.Name, err) - } - f.HasBeenSet = true - } - - for _, name := range f.Names() { - set.Var(f.Value, name, f.Usage) - } - return nil -} - -// Timestamp gets the timestamp from a flag name -func (c *Context) Timestamp(name string) *time.Time { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupTimestamp(name, fs) - } - return nil -} - -// Fetches the timestamp value from the local timestampWrap -func lookupTimestamp(name string, set *flag.FlagSet) *time.Time { - f := set.Lookup(name) - if f != nil { - return (f.Value.(*Timestamp)).Value() - } - return nil -} diff --git a/vendor/github.com/urfave/cli/v2/flag_uint.go b/vendor/github.com/urfave/cli/v2/flag_uint.go deleted file mode 100644 index 2e5e76b0..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_uint.go +++ /dev/null @@ -1,105 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// UintFlag is a flag with type uint -type UintFlag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value uint - DefaultText string - Destination *uint - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *UintFlag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *UintFlag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *UintFlag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *UintFlag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *UintFlag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *UintFlag) GetUsage() string { - return f.Usage -} - -// Apply populates the flag given the flag set and environment -func (f *UintFlag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valInt, err := strconv.ParseUint(val, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as uint value for flag %s: %s", val, f.Name, err) - } - - f.Value = uint(valInt) - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.UintVar(f.Destination, name, f.Value, f.Usage) - continue - } - set.Uint(name, f.Value, f.Usage) - } - - return nil -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *UintFlag) GetValue() string { - return fmt.Sprintf("%d", f.Value) -} - -// Uint looks up the value of a local UintFlag, returns -// 0 if not found -func (c *Context) Uint(name string) uint { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupUint(name, fs) - } - return 0 -} - -func lookupUint(name string, set *flag.FlagSet) uint { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return uint(parsed) - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/flag_uint64.go b/vendor/github.com/urfave/cli/v2/flag_uint64.go deleted file mode 100644 index 8fc3289d..00000000 --- a/vendor/github.com/urfave/cli/v2/flag_uint64.go +++ /dev/null @@ -1,105 +0,0 @@ -package cli - -import ( - "flag" - "fmt" - "strconv" -) - -// Uint64Flag is a flag with type uint64 -type Uint64Flag struct { - Name string - Aliases []string - Usage string - EnvVars []string - FilePath string - Required bool - Hidden bool - Value uint64 - DefaultText string - Destination *uint64 - HasBeenSet bool -} - -// IsSet returns whether or not the flag has been set through env or file -func (f *Uint64Flag) IsSet() bool { - return f.HasBeenSet -} - -// String returns a readable representation of this value -// (for usage defaults) -func (f *Uint64Flag) String() string { - return FlagStringer(f) -} - -// Names returns the names of the flag -func (f *Uint64Flag) Names() []string { - return flagNames(f.Name, f.Aliases) -} - -// IsRequired returns whether or not the flag is required -func (f *Uint64Flag) IsRequired() bool { - return f.Required -} - -// TakesValue returns true of the flag takes a value, otherwise false -func (f *Uint64Flag) TakesValue() bool { - return true -} - -// GetUsage returns the usage string for the flag -func (f *Uint64Flag) GetUsage() string { - return f.Usage -} - -// Apply populates the flag given the flag set and environment -func (f *Uint64Flag) Apply(set *flag.FlagSet) error { - if val, ok := flagFromEnvOrFile(f.EnvVars, f.FilePath); ok { - if val != "" { - valInt, err := strconv.ParseUint(val, 0, 64) - if err != nil { - return fmt.Errorf("could not parse %q as uint64 value for flag %s: %s", val, f.Name, err) - } - - f.Value = valInt - f.HasBeenSet = true - } - } - - for _, name := range f.Names() { - if f.Destination != nil { - set.Uint64Var(f.Destination, name, f.Value, f.Usage) - continue - } - set.Uint64(name, f.Value, f.Usage) - } - - return nil -} - -// GetValue returns the flags value as string representation and an empty -// string if the flag takes no value at all. -func (f *Uint64Flag) GetValue() string { - return fmt.Sprintf("%d", f.Value) -} - -// Uint64 looks up the value of a local Uint64Flag, returns -// 0 if not found -func (c *Context) Uint64(name string) uint64 { - if fs := lookupFlagSet(name, c); fs != nil { - return lookupUint64(name, fs) - } - return 0 -} - -func lookupUint64(name string, set *flag.FlagSet) uint64 { - f := set.Lookup(name) - if f != nil { - parsed, err := strconv.ParseUint(f.Value.String(), 0, 64) - if err != nil { - return 0 - } - return parsed - } - return 0 -} diff --git a/vendor/github.com/urfave/cli/v2/funcs.go b/vendor/github.com/urfave/cli/v2/funcs.go deleted file mode 100644 index 474c48fa..00000000 --- a/vendor/github.com/urfave/cli/v2/funcs.go +++ /dev/null @@ -1,44 +0,0 @@ -package cli - -// BashCompleteFunc is an action to execute when the shell completion flag is set -type BashCompleteFunc func(*Context) - -// BeforeFunc is an action to execute before any subcommands are run, but after -// the context is ready if a non-nil error is returned, no subcommands are run -type BeforeFunc func(*Context) error - -// AfterFunc is an action to execute after any subcommands are run, but after the -// subcommand has finished it is run even if Action() panics -type AfterFunc func(*Context) error - -// ActionFunc is the action to execute when no subcommands are specified -type ActionFunc func(*Context) error - -// CommandNotFoundFunc is executed if the proper command cannot be found -type CommandNotFoundFunc func(*Context, string) - -// OnUsageErrorFunc is executed if an usage error occurs. This is useful for displaying -// customized usage error messages. This function is able to replace the -// original error messages. If this function is not set, the "Incorrect usage" -// is displayed and the execution is interrupted. -type OnUsageErrorFunc func(context *Context, err error, isSubcommand bool) error - -// ExitErrHandlerFunc is executed if provided in order to handle exitError values -// returned by Actions and Before/After functions. -type ExitErrHandlerFunc func(context *Context, err error) - -// FlagStringFunc is used by the help generation to display a flag, which is -// expected to be a single line. -type FlagStringFunc func(Flag) string - -// FlagNamePrefixFunc is used by the default FlagStringFunc to create prefix -// text for a flag's full name. -type FlagNamePrefixFunc func(fullName []string, placeholder string) string - -// FlagEnvHintFunc is used by the default FlagStringFunc to annotate flag help -// with the environment variable details. -type FlagEnvHintFunc func(envVars []string, str string) string - -// FlagFileHintFunc is used by the default FlagStringFunc to annotate flag help -// with the file path details. -type FlagFileHintFunc func(filePath, str string) string diff --git a/vendor/github.com/urfave/cli/v2/go.mod b/vendor/github.com/urfave/cli/v2/go.mod deleted file mode 100644 index c38d41c1..00000000 --- a/vendor/github.com/urfave/cli/v2/go.mod +++ /dev/null @@ -1,9 +0,0 @@ -module github.com/urfave/cli/v2 - -go 1.11 - -require ( - github.com/BurntSushi/toml v0.3.1 - github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d - gopkg.in/yaml.v2 v2.2.2 -) diff --git a/vendor/github.com/urfave/cli/v2/go.sum b/vendor/github.com/urfave/cli/v2/go.sum deleted file mode 100644 index ef121ff5..00000000 --- a/vendor/github.com/urfave/cli/v2/go.sum +++ /dev/null @@ -1,14 +0,0 @@ -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= -github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= -github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= -github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/vendor/github.com/urfave/cli/v2/help.go b/vendor/github.com/urfave/cli/v2/help.go deleted file mode 100644 index c1e974a4..00000000 --- a/vendor/github.com/urfave/cli/v2/help.go +++ /dev/null @@ -1,368 +0,0 @@ -package cli - -import ( - "fmt" - "io" - "os" - "strings" - "text/tabwriter" - "text/template" - "unicode/utf8" -) - -var helpCommand = &Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() - if args.Present() { - return ShowCommandHelp(c, args.First()) - } - - _ = ShowAppHelp(c) - return nil - }, -} - -var helpSubcommand = &Command{ - Name: "help", - Aliases: []string{"h"}, - Usage: "Shows a list of commands or help for one command", - ArgsUsage: "[command]", - Action: func(c *Context) error { - args := c.Args() - if args.Present() { - return ShowCommandHelp(c, args.First()) - } - - return ShowSubcommandHelp(c) - }, -} - -// Prints help for the App or Command -type helpPrinter func(w io.Writer, templ string, data interface{}) - -// Prints help for the App or Command with custom template function. -type helpPrinterCustom func(w io.Writer, templ string, data interface{}, customFunc map[string]interface{}) - -// HelpPrinter is a function that writes the help output. If not set explicitly, -// this calls HelpPrinterCustom using only the default template functions. -// -// If custom logic for printing help is required, this function can be -// overridden. If the ExtraInfo field is defined on an App, this function -// should not be modified, as HelpPrinterCustom will be used directly in order -// to capture the extra information. -var HelpPrinter helpPrinter = printHelp - -// HelpPrinterCustom is a function that writes the help output. It is used as -// the default implementation of HelpPrinter, and may be called directly if -// the ExtraInfo field is set on an App. -var HelpPrinterCustom helpPrinterCustom = printHelpCustom - -// VersionPrinter prints the version for the App -var VersionPrinter = printVersion - -// ShowAppHelpAndExit - Prints the list of subcommands for the app and exits with exit code. -func ShowAppHelpAndExit(c *Context, exitCode int) { - _ = ShowAppHelp(c) - os.Exit(exitCode) -} - -// ShowAppHelp is an action that displays the help. -func ShowAppHelp(c *Context) error { - template := c.App.CustomAppHelpTemplate - if template == "" { - template = AppHelpTemplate - } - - if c.App.ExtraInfo == nil { - HelpPrinter(c.App.Writer, template, c.App) - return nil - } - - customAppData := func() map[string]interface{} { - return map[string]interface{}{ - "ExtraInfo": c.App.ExtraInfo, - } - } - HelpPrinterCustom(c.App.Writer, template, c.App, customAppData()) - - return nil -} - -// DefaultAppComplete prints the list of subcommands as the default app completion method -func DefaultAppComplete(c *Context) { - DefaultCompleteWithFlags(nil)(c) -} - -func printCommandSuggestions(commands []*Command, writer io.Writer) { - for _, command := range commands { - if command.Hidden { - continue - } - if os.Getenv("_CLI_ZSH_AUTOCOMPLETE_HACK") == "1" { - for _, name := range command.Names() { - _, _ = fmt.Fprintf(writer, "%s:%s\n", name, command.Usage) - } - } else { - for _, name := range command.Names() { - _, _ = fmt.Fprintf(writer, "%s\n", name) - } - } - } -} - -func cliArgContains(flagName string) bool { - for _, name := range strings.Split(flagName, ",") { - name = strings.TrimSpace(name) - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 - } - flag := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - for _, a := range os.Args { - if a == flag { - return true - } - } - } - return false -} - -func printFlagSuggestions(lastArg string, flags []Flag, writer io.Writer) { - cur := strings.TrimPrefix(lastArg, "-") - cur = strings.TrimPrefix(cur, "-") - for _, flag := range flags { - if bflag, ok := flag.(*BoolFlag); ok && bflag.Hidden { - continue - } - for _, name := range flag.Names() { - name = strings.TrimSpace(name) - // this will get total count utf8 letters in flag name - count := utf8.RuneCountInString(name) - if count > 2 { - count = 2 // resuse this count to generate single - or -- in flag completion - } - // if flag name has more than one utf8 letter and last argument in cli has -- prefix then - // skip flag completion for short flags example -v or -x - if strings.HasPrefix(lastArg, "--") && count == 1 { - continue - } - // match if last argument matches this flag and it is not repeated - if strings.HasPrefix(name, cur) && cur != name && !cliArgContains(name) { - flagCompletion := fmt.Sprintf("%s%s", strings.Repeat("-", count), name) - _, _ = fmt.Fprintln(writer, flagCompletion) - } - } - } -} - -func DefaultCompleteWithFlags(cmd *Command) func(c *Context) { - return func(c *Context) { - if len(os.Args) > 2 { - lastArg := os.Args[len(os.Args)-2] - if strings.HasPrefix(lastArg, "-") { - printFlagSuggestions(lastArg, c.App.Flags, c.App.Writer) - if cmd != nil { - printFlagSuggestions(lastArg, cmd.Flags, c.App.Writer) - } - return - } - } - if cmd != nil { - printCommandSuggestions(cmd.Subcommands, c.App.Writer) - } else { - printCommandSuggestions(c.App.Commands, c.App.Writer) - } - } -} - -// ShowCommandHelpAndExit - exits with code after showing help -func ShowCommandHelpAndExit(c *Context, command string, code int) { - _ = ShowCommandHelp(c, command) - os.Exit(code) -} - -// ShowCommandHelp prints help for the given command -func ShowCommandHelp(ctx *Context, command string) error { - // show the subcommand help for a command with subcommands - if command == "" { - HelpPrinter(ctx.App.Writer, SubcommandHelpTemplate, ctx.App) - return nil - } - - for _, c := range ctx.App.Commands { - if c.HasName(command) { - templ := c.CustomHelpTemplate - if templ == "" { - templ = CommandHelpTemplate - } - - HelpPrinter(ctx.App.Writer, templ, c) - - return nil - } - } - - if ctx.App.CommandNotFound == nil { - return Exit(fmt.Sprintf("No help topic for '%v'", command), 3) - } - - ctx.App.CommandNotFound(ctx, command) - return nil -} - -// ShowSubcommandHelp prints help for the given subcommand -func ShowSubcommandHelp(c *Context) error { - if c == nil { - return nil - } - - if c.Command != nil { - return ShowCommandHelp(c, c.Command.Name) - } - - return ShowCommandHelp(c, "") -} - -// ShowVersion prints the version number of the App -func ShowVersion(c *Context) { - VersionPrinter(c) -} - -func printVersion(c *Context) { - _, _ = fmt.Fprintf(c.App.Writer, "%v version %v\n", c.App.Name, c.App.Version) -} - -// ShowCompletions prints the lists of commands within a given context -func ShowCompletions(c *Context) { - a := c.App - if a != nil && a.BashComplete != nil { - a.BashComplete(c) - } -} - -// ShowCommandCompletions prints the custom completions for a given command -func ShowCommandCompletions(ctx *Context, command string) { - c := ctx.App.Command(command) - if c != nil { - if c.BashComplete != nil { - c.BashComplete(ctx) - } else { - DefaultCompleteWithFlags(c)(ctx) - } - } - -} - -// printHelpCustom is the default implementation of HelpPrinterCustom. -// -// The customFuncs map will be combined with a default template.FuncMap to -// allow using arbitrary functions in template rendering. -func printHelpCustom(out io.Writer, templ string, data interface{}, customFuncs map[string]interface{}) { - funcMap := template.FuncMap{ - "join": strings.Join, - } - for key, value := range customFuncs { - funcMap[key] = value - } - - w := tabwriter.NewWriter(out, 1, 8, 2, ' ', 0) - t := template.Must(template.New("help").Funcs(funcMap).Parse(templ)) - - err := t.Execute(w, data) - if err != nil { - // If the writer is closed, t.Execute will fail, and there's nothing - // we can do to recover. - if os.Getenv("CLI_TEMPLATE_ERROR_DEBUG") != "" { - _, _ = fmt.Fprintf(ErrWriter, "CLI TEMPLATE ERROR: %#v\n", err) - } - return - } - _ = w.Flush() -} - -func printHelp(out io.Writer, templ string, data interface{}) { - HelpPrinterCustom(out, templ, data, nil) -} - -func checkVersion(c *Context) bool { - found := false - for _, name := range VersionFlag.Names() { - if c.Bool(name) { - found = true - } - } - return found -} - -func checkHelp(c *Context) bool { - found := false - for _, name := range HelpFlag.Names() { - if c.Bool(name) { - found = true - } - } - return found -} - -func checkCommandHelp(c *Context, name string) bool { - if c.Bool("h") || c.Bool("help") { - _ = ShowCommandHelp(c, name) - return true - } - - return false -} - -func checkSubcommandHelp(c *Context) bool { - if c.Bool("h") || c.Bool("help") { - _ = ShowSubcommandHelp(c) - return true - } - - return false -} - -func checkShellCompleteFlag(a *App, arguments []string) (bool, []string) { - if !a.EnableBashCompletion { - return false, arguments - } - - pos := len(arguments) - 1 - lastArg := arguments[pos] - - if lastArg != "--generate-bash-completion" { - return false, arguments - } - - return true, arguments[:pos] -} - -func checkCompletions(c *Context) bool { - if !c.shellComplete { - return false - } - - if args := c.Args(); args.Present() { - name := args.First() - if cmd := c.App.Command(name); cmd != nil { - // let the command handle the completion - return false - } - } - - ShowCompletions(c) - return true -} - -func checkCommandCompletions(c *Context, name string) bool { - if !c.shellComplete { - return false - } - - ShowCommandCompletions(c, name) - return true -} diff --git a/vendor/github.com/urfave/cli/v2/parse.go b/vendor/github.com/urfave/cli/v2/parse.go deleted file mode 100644 index 7df17296..00000000 --- a/vendor/github.com/urfave/cli/v2/parse.go +++ /dev/null @@ -1,94 +0,0 @@ -package cli - -import ( - "flag" - "strings" -) - -type iterativeParser interface { - newFlagSet() (*flag.FlagSet, error) - useShortOptionHandling() bool -} - -// To enable short-option handling (e.g., "-it" vs "-i -t") we have to -// iteratively catch parsing errors. This way we achieve LR parsing without -// transforming any arguments. Otherwise, there is no way we can discriminate -// combined short options from common arguments that should be left untouched. -// Pass `shellComplete` to continue parsing options on failure during shell -// completion when, the user-supplied options may be incomplete. -func parseIter(set *flag.FlagSet, ip iterativeParser, args []string, shellComplete bool) error { - for { - err := set.Parse(args) - if !ip.useShortOptionHandling() || err == nil { - if shellComplete { - return nil - } - return err - } - - errStr := err.Error() - trimmed := strings.TrimPrefix(errStr, "flag provided but not defined: -") - if errStr == trimmed { - return err - } - - // regenerate the initial args with the split short opts - argsWereSplit := false - for i, arg := range args { - // skip args that are not part of the error message - if name := strings.TrimLeft(arg, "-"); name != trimmed { - continue - } - - // if we can't split, the error was accurate - shortOpts := splitShortOptions(set, arg) - if len(shortOpts) == 1 { - return err - } - - // swap current argument with the split version - args = append(args[:i], append(shortOpts, args[i+1:]...)...) - argsWereSplit = true - break - } - - // This should be an impossible to reach code path, but in case the arg - // splitting failed to happen, this will prevent infinite loops - if !argsWereSplit { - return err - } - - // Since custom parsing failed, replace the flag set before retrying - newSet, err := ip.newFlagSet() - if err != nil { - return err - } - *set = *newSet - } -} - -func splitShortOptions(set *flag.FlagSet, arg string) []string { - shortFlagsExist := func(s string) bool { - for _, c := range s[1:] { - if f := set.Lookup(string(c)); f == nil { - return false - } - } - return true - } - - if !isSplittable(arg) || !shortFlagsExist(arg) { - return []string{arg} - } - - separated := make([]string, 0, len(arg)-1) - for _, flagChar := range arg[1:] { - separated = append(separated, "-"+string(flagChar)) - } - - return separated -} - -func isSplittable(flagArg string) bool { - return strings.HasPrefix(flagArg, "-") && !strings.HasPrefix(flagArg, "--") && len(flagArg) > 2 -} diff --git a/vendor/github.com/urfave/cli/v2/sort.go b/vendor/github.com/urfave/cli/v2/sort.go deleted file mode 100644 index 23d1c2f7..00000000 --- a/vendor/github.com/urfave/cli/v2/sort.go +++ /dev/null @@ -1,29 +0,0 @@ -package cli - -import "unicode" - -// lexicographicLess compares strings alphabetically considering case. -func lexicographicLess(i, j string) bool { - iRunes := []rune(i) - jRunes := []rune(j) - - lenShared := len(iRunes) - if lenShared > len(jRunes) { - lenShared = len(jRunes) - } - - for index := 0; index < lenShared; index++ { - ir := iRunes[index] - jr := jRunes[index] - - if lir, ljr := unicode.ToLower(ir), unicode.ToLower(jr); lir != ljr { - return lir < ljr - } - - if ir != jr { - return ir < jr - } - } - - return i < j -} diff --git a/vendor/github.com/urfave/cli/v2/template.go b/vendor/github.com/urfave/cli/v2/template.go deleted file mode 100644 index aee3e049..00000000 --- a/vendor/github.com/urfave/cli/v2/template.go +++ /dev/null @@ -1,120 +0,0 @@ -package cli - -// AppHelpTemplate is the text template for the Default help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var AppHelpTemplate = `NAME: - {{.Name}}{{if .Usage}} - {{.Usage}}{{end}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} {{if .VisibleFlags}}[global options]{{end}}{{if .Commands}} command [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Version}}{{if not .HideVersion}} - -VERSION: - {{.Version}}{{end}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if len .Authors}} - -AUTHOR{{with $length := len .Authors}}{{if ne 1 $length}}S{{end}}{{end}}: - {{range $index, $author := .Authors}}{{if $index}} - {{end}}{{$author}}{{end}}{{end}}{{if .VisibleCommands}} - -COMMANDS:{{range .VisibleCategories}}{{if .Name}} - {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} - -GLOBAL OPTIONS: - {{range $index, $option := .VisibleFlags}}{{if $index}} - {{end}}{{$option}}{{end}}{{end}}{{if .Copyright}} - -COPYRIGHT: - {{.Copyright}}{{end}} -` - -// CommandHelpTemplate is the text template for the command help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var CommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}}{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Category}} - -CATEGORY: - {{.Category}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}}{{if .VisibleFlags}} - -OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} -` - -// SubcommandHelpTemplate is the text template for the subcommand help topic. -// cli.go uses text/template to render templates. You can -// render custom help text by setting this variable. -var SubcommandHelpTemplate = `NAME: - {{.HelpName}} - {{.Usage}} - -USAGE: - {{if .UsageText}}{{.UsageText}}{{else}}{{.HelpName}} command{{if .VisibleFlags}} [command options]{{end}} {{if .ArgsUsage}}{{.ArgsUsage}}{{else}}[arguments...]{{end}}{{end}}{{if .Description}} - -DESCRIPTION: - {{.Description}}{{end}} - -COMMANDS:{{range .VisibleCategories}}{{if .Name}} - {{.Name}}:{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{else}}{{range .VisibleCommands}} - {{join .Names ", "}}{{"\t"}}{{.Usage}}{{end}}{{end}}{{end}}{{if .VisibleFlags}} - -OPTIONS: - {{range .VisibleFlags}}{{.}} - {{end}}{{end}} -` - -var MarkdownDocTemplate = `% {{ .App.Name }} 8 - -# NAME - -{{ .App.Name }}{{ if .App.Usage }} - {{ .App.Usage }}{{ end }} - -# SYNOPSIS - -{{ .App.Name }} -{{ if .SynopsisArgs }} -` + "```" + ` -{{ range $v := .SynopsisArgs }}{{ $v }}{{ end }}` + "```" + ` -{{ end }}{{ if .App.UsageText }} -# DESCRIPTION - -{{ .App.UsageText }} -{{ end }} -**Usage**: - -` + "```" + ` -{{ .App.Name }} [GLOBAL OPTIONS] command [COMMAND OPTIONS] [ARGUMENTS...] -` + "```" + ` -{{ if .GlobalArgs }} -# GLOBAL OPTIONS -{{ range $v := .GlobalArgs }} -{{ $v }}{{ end }} -{{ end }}{{ if .Commands }} -# COMMANDS -{{ range $v := .Commands }} -{{ $v }}{{ end }}{{ end }}` - -var FishCompletionTemplate = `# {{ .App.Name }} fish shell completion - -function __fish_{{ .App.Name }}_no_subcommand --description 'Test if there has been any subcommand yet' - for i in (commandline -opc) - if contains -- $i{{ range $v := .AllCommands }} {{ $v }}{{ end }} - return 1 - end - end - return 0 -end - -{{ range $v := .Completions }}{{ $v }} -{{ end }}` diff --git a/vendor/modules.txt b/vendor/modules.txt deleted file mode 100644 index 6651fd33..00000000 --- a/vendor/modules.txt +++ /dev/null @@ -1,15 +0,0 @@ -# github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d -github.com/cpuguy83/go-md2man/v2/md2man -# github.com/mattn/go-runewidth v0.0.9 -## explicit -github.com/mattn/go-runewidth -# github.com/olekukonko/tablewriter v0.0.4 -## explicit -github.com/olekukonko/tablewriter -# github.com/russross/blackfriday/v2 v2.0.1 -github.com/russross/blackfriday/v2 -# github.com/shurcooL/sanitized_anchor_name v1.0.0 -github.com/shurcooL/sanitized_anchor_name -# github.com/urfave/cli/v2 v2.2.0 -## explicit -github.com/urfave/cli/v2 From 6c260d1b939e34bc71d3ba855f28ef80c3750db9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 15:11:26 -0400 Subject: [PATCH 22/69] Cleanup --- pkg/box/utils.go | 90 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 pkg/box/utils.go diff --git a/pkg/box/utils.go b/pkg/box/utils.go new file mode 100644 index 00000000..61cd05df --- /dev/null +++ b/pkg/box/utils.go @@ -0,0 +1,90 @@ +module box + + +// FileStatus returns the status of a file. +func FileStatus(name string) (string, error) { + /* + DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). + ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. + SHREDDED: Plaintext is missing. + GPGMISSING: The .gpg file is missing. Oops? + PLAINERROR: Can't access the plaintext file to determine status. + GPGERROR: Can't access .gpg file to determine status. + */ + + p := name + e := p + ".gpg" + ps, perr := os.Stat(p) + es, eerr := os.Stat(e) + if perr == nil && eerr == nil { + if ps.ModTime().Before(es.ModTime()) { + return "ENCRYPTED", nil + } + return "DECRYPTED", nil + } + + if eerr != nil { + if os.IsNotExist(eerr) { + return "GPGMISSING", nil + } + return "GPGERROR", eerr + } + + if perr != nil { + if os.IsNotExist(perr) { + return "SHREDDED", nil + } + } + return "PLAINERROR", perr +} + + + +// func isChanged(pname string) (bool, error) { +// // if .gpg exists but not plainfile: unchanged +// // if plaintext exists but not .gpg: changed +// // if plainfile < .gpg: unchanged +// // if plainfile > .gpg: don't know, need to try diff + +// // Gather info about the files: + +// pstat, perr := os.Stat(pname) +// if perr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr) +// } +// gname := pname + ".gpg" +// gstat, gerr := os.Stat(gname) +// if gerr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr) +// } + +// pexists := perr == nil +// gexists := gerr == nil + +// // Use the above rules: + +// // if .gpg exists but not plainfile: unchanged +// if gexists && !pexists { +// return false, nil +// } + +// // if plaintext exists but not .gpg: changed +// if pexists && !gexists { +// return true, nil +// } + +// // At this point we can conclude that both p and g exist. +// // Can't hurt to test that assertion. +// if (!pexists) && (!gexists) { +// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname) +// } + +// pmodtime := pstat.ModTime() +// gmodtime := gstat.ModTime() +// // if plainfile < .gpg: unchanged +// if pmodtime.Before(gmodtime) { +// return false, nil +// } +// // if plainfile > .gpg: don't know, need to try diff +// return false, fmt.Errorf("Can not know for sure. Try git diff?") +// } From 126f60fcc6aba2b0fe831906f619bf9e768e10e0 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 15:11:49 -0400 Subject: [PATCH 23/69] cleanup --- pkg/box/utils.go | 5 ++- pkg/box/verbs.go | 86 ------------------------------------------------ 2 files changed, 2 insertions(+), 89 deletions(-) diff --git a/pkg/box/utils.go b/pkg/box/utils.go index 61cd05df..bec9cf02 100644 --- a/pkg/box/utils.go +++ b/pkg/box/utils.go @@ -1,5 +1,6 @@ -module box +package box +import "os" // FileStatus returns the status of a file. func FileStatus(name string) (string, error) { @@ -38,8 +39,6 @@ func FileStatus(name string) (string, error) { return "PLAINERROR", perr } - - // func isChanged(pname string) (bool, error) { // // if .gpg exists but not plainfile: unchanged // // if plaintext exists but not .gpg: changed diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index c0488611..dfa24c11 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -121,92 +121,6 @@ func (bx *Box) Shred(names []string) error { return fmt.Errorf("NOT IMPLEMENTED: Shred") } -// func isChanged(pname string) (bool, error) { -// // if .gpg exists but not plainfile: unchanged -// // if plaintext exists but not .gpg: changed -// // if plainfile < .gpg: unchanged -// // if plainfile > .gpg: don't know, need to try diff - -// // Gather info about the files: - -// pstat, perr := os.Stat(pname) -// if perr != nil && (!os.IsNotExist(perr)) { -// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr) -// } -// gname := pname + ".gpg" -// gstat, gerr := os.Stat(gname) -// if gerr != nil && (!os.IsNotExist(perr)) { -// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr) -// } - -// pexists := perr == nil -// gexists := gerr == nil - -// // Use the above rules: - -// // if .gpg exists but not plainfile: unchanged -// if gexists && !pexists { -// return false, nil -// } - -// // if plaintext exists but not .gpg: changed -// if pexists && !gexists { -// return true, nil -// } - -// // At this point we can conclude that both p and g exist. -// // Can't hurt to test that assertion. -// if (!pexists) && (!gexists) { -// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname) -// } - -// pmodtime := pstat.ModTime() -// gmodtime := gstat.ModTime() -// // if plainfile < .gpg: unchanged -// if pmodtime.Before(gmodtime) { -// return false, nil -// } -// // if plainfile > .gpg: don't know, need to try diff -// return false, fmt.Errorf("Can not know for sure. Try git diff?") -// } - -// FileStatus returns the status of a file. -func FileStatus(name string) (string, error) { - /* - DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). - ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. - SHREDDED: Plaintext is missing. - GPGMISSING: The .gpg file is missing. Oops? - PLAINERROR: Can't access the plaintext file to determine status. - GPGERROR: Can't access .gpg file to determine status. - */ - - p := name - e := p + ".gpg" - ps, perr := os.Stat(p) - es, eerr := os.Stat(e) - if perr == nil && eerr == nil { - if ps.ModTime().Before(es.ModTime()) { - return "ENCRYPTED", nil - } - return "DECRYPTED", nil - } - - if eerr != nil { - if os.IsNotExist(eerr) { - return "GPGMISSING", nil - } - return "GPGERROR", eerr - } - - if perr != nil { - if os.IsNotExist(perr) { - return "SHREDDED", nil - } - } - return "PLAINERROR", perr -} - // Status prints the status of files. func (bx *Box) Status(names []string, nameOnly bool, match string) error { From edad8a879ea6065584dfdd1259f8933b17a9b792 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 17:47:36 -0400 Subject: [PATCH 24/69] snapshot --- cmd/blackbox/blackbox.go | 2 + cmd/blackbox/flags.go | 21 ++++++---- pkg/bbutil/filestats.go | 22 +++++----- pkg/box/box.go | 61 ++++++++++++++++++++------- pkg/box/utils.go | 89 ---------------------------------------- pkg/box/verbs.go | 51 ++++++++++++++++++++--- vcs/vcs.go | 1 + 7 files changed, 121 insertions(+), 126 deletions(-) delete mode 100644 pkg/box/utils.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 2e8eb967..6016a76f 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -4,6 +4,8 @@ import ( "fmt" "os" + _ "github.com/StackExchange/blackbox/crypters" + _ "github.com/StackExchange/blackbox/crypters/_all" _ "github.com/StackExchange/blackbox/vcs" _ "github.com/StackExchange/blackbox/vcs/_all" ) diff --git a/cmd/blackbox/flags.go b/cmd/blackbox/flags.go index d2a5f6c7..9ef10f8f 100644 --- a/cmd/blackbox/flags.go +++ b/cmd/blackbox/flags.go @@ -9,14 +9,19 @@ func flags() *cli.App { app.Version = "2.0.0" app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" - // app.Flags = []cli.Flag{ - // &cli.BoolFlag{ - // Name: "dry-run", - // Aliases: []string{"n"}, - // Usage: "show what would have been done", - // Destination: &dryRun, - // }, - // } + app.Flags = []cli.Flag{ + // &cli.BoolFlag{ + // Name: "dry-run", + // Aliases: []string{"n"}, + // Usage: "show what would have been done", + // Destination: &dryRun, + // }, + &cli.StringFlag{ + Name: "crypto", + Usage: "Crypto back-end plugin", + Value: "GnuPG", + }, + } app.Commands = []*cli.Command{ diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index f36f908f..da81ba9a 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -2,6 +2,7 @@ package bbutil import "os" +// DirExists returns true if directory exists. func DirExists(path string) (bool, error) { stat, err := os.Stat(path) if err == nil { @@ -13,13 +14,14 @@ func DirExists(path string) (bool, error) { return true, err } -//func FileExists(path string) (bool, error) { -// _, err := os.Stat(path) -// if err == nil { -// return true, nil -// } -// if os.IsNotExist(err) { -// return false, nil -// } -// return false, err -//} +// FileExistsOrProblem returns true if the file exists or if we can't determine its existance. +func FileExistsOrProblem(path string) bool { + _, err := os.Stat(path) + if err == nil { + return true + } + if os.IsNotExist(err) { + return false + } + return true +} diff --git a/pkg/box/box.go b/pkg/box/box.go index 5fd8f60b..65ccc56f 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/StackExchange/blackbox/crypters" "github.com/StackExchange/blackbox/pkg/bbutil" "github.com/StackExchange/blackbox/vcs" "github.com/urfave/cli/v2" @@ -21,10 +22,15 @@ type Box struct { RepoBaseDir string // Base directory of the repo. ConfigDir string // Path to the .blackbox config directory. // - Admins []string // If non-empty, the list of admins. - Files []string // If non-empty, the list of files. - Vcs vcs.Vcs // Handle for VCS access. - VcsName string // name of the VCS + Admins []string // If non-empty, the list of admins. + Files []string // If non-empty, the list of files. + FilesSet map[string]bool // If non-nil, a set of Files. + IsRegistered map[string]bool // + // + Vcs vcs.Vcs // Interface access to the VCS. + VcsName string // name of the VCS + Crypter crypters.Crypter // Inteface access to GPG. + CrypterName string // Name of the crypter in use. } // StatusMode is a type of query. @@ -64,7 +70,7 @@ func NewFromFlags(c *cli.Context) *Box { for _, v := range vcs.Catalog { h, err = v.New() if err != nil { - return nil + return nil // No idea how that would happen. } if h.Discover(bx.RepoBaseDir) { bx.Vcs = h @@ -72,6 +78,29 @@ func NewFromFlags(c *cli.Context) *Box { break } } + // We can assume something was found because "none" always says yes. + + // Pick a crypto backend (GnuPG, go-openpgp, etc.) + //var cbe crypters.Crypter + var chandle crypters.Crypter + for _, v := range crypters.Catalog { + //fmt.Printf("Trying %v %v\n", v.Name) + if strings.ToLower(v.Name) == strings.ToLower(c.String("crypto")) { + //fmt.Printf("CRYPTER = %v\n", v.Name) + chandle, err = v.New() + if err != nil { + return nil // No idea how that would happen. + } + bx.Crypter = chandle + bx.CrypterName = v.Name + //fmt.Printf("USING! %v\n", v.Name) + break + } + } + if bx.Crypter == nil { + fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the default\n") + os.Exit(1) + } return bx } @@ -155,9 +184,10 @@ func (bx *Box) getAdmins() ([]string, error) { return nil, fmt.Errorf("getAdmins can't load admin list") } -func (bx *Box) getFiles() ([]string, error) { +// getFiles populates Files and FileMap. +func (bx *Box) getFiles() error { if len(bx.Files) != 0 { - return bx.Files, nil + return nil } // TODO(tlim): Try the json file. @@ -165,14 +195,17 @@ func (bx *Box) getFiles() ([]string, error) { // Try the legacy file: fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") b, err := ioutil.ReadFile(fn) - c := strings.TrimSpace(string(b)) - if err == nil { - bx.Files = strings.Split(c, "\n") - return bx.Files, nil + if err != nil { + return fmt.Errorf("getFiles can't read %q: %v", fn, err) } - if !os.IsNotExist(err) { - return nil, fmt.Errorf("getFiles can't open %q: %v", fn, err) + + c := strings.TrimSpace(string(b)) + + bx.Files = strings.Split(c, "\n") + bx.FilesSet = make(map[string]bool) + for _, s := range bx.Files { + bx.FilesSet[s] = true } - return nil, fmt.Errorf("getFiles can't load file list") + return nil } diff --git a/pkg/box/utils.go b/pkg/box/utils.go deleted file mode 100644 index bec9cf02..00000000 --- a/pkg/box/utils.go +++ /dev/null @@ -1,89 +0,0 @@ -package box - -import "os" - -// FileStatus returns the status of a file. -func FileStatus(name string) (string, error) { - /* - DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). - ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. - SHREDDED: Plaintext is missing. - GPGMISSING: The .gpg file is missing. Oops? - PLAINERROR: Can't access the plaintext file to determine status. - GPGERROR: Can't access .gpg file to determine status. - */ - - p := name - e := p + ".gpg" - ps, perr := os.Stat(p) - es, eerr := os.Stat(e) - if perr == nil && eerr == nil { - if ps.ModTime().Before(es.ModTime()) { - return "ENCRYPTED", nil - } - return "DECRYPTED", nil - } - - if eerr != nil { - if os.IsNotExist(eerr) { - return "GPGMISSING", nil - } - return "GPGERROR", eerr - } - - if perr != nil { - if os.IsNotExist(perr) { - return "SHREDDED", nil - } - } - return "PLAINERROR", perr -} - -// func isChanged(pname string) (bool, error) { -// // if .gpg exists but not plainfile: unchanged -// // if plaintext exists but not .gpg: changed -// // if plainfile < .gpg: unchanged -// // if plainfile > .gpg: don't know, need to try diff - -// // Gather info about the files: - -// pstat, perr := os.Stat(pname) -// if perr != nil && (!os.IsNotExist(perr)) { -// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr) -// } -// gname := pname + ".gpg" -// gstat, gerr := os.Stat(gname) -// if gerr != nil && (!os.IsNotExist(perr)) { -// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr) -// } - -// pexists := perr == nil -// gexists := gerr == nil - -// // Use the above rules: - -// // if .gpg exists but not plainfile: unchanged -// if gexists && !pexists { -// return false, nil -// } - -// // if plaintext exists but not .gpg: changed -// if pexists && !gexists { -// return true, nil -// } - -// // At this point we can conclude that both p and g exist. -// // Can't hurt to test that assertion. -// if (!pexists) && (!gexists) { -// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname) -// } - -// pmodtime := pstat.ModTime() -// gmodtime := gstat.ModTime() -// // if plainfile < .gpg: unchanged -// if pmodtime.Before(gmodtime) { -// return false, nil -// } -// // if plainfile > .gpg: don't know, need to try diff -// return false, fmt.Errorf("Can not know for sure. Try git diff?") -// } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index dfa24c11..25b407e5 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -7,6 +7,7 @@ import ( "os" "strings" + "github.com/StackExchange/blackbox/pkg/bbutil" "github.com/olekukonko/tablewriter" ) @@ -40,7 +41,47 @@ func (bx *Box) Cat([]string) error { } // Decrypt decrypts a file. -func (bx *Box) Decrypt(names []string, overwrite bool, bulk bool, setgroup string) error { +func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup string) error { + var err error + + err = bx.getFiles() + if err != nil { + return err + } + + groupchange := false + gid := -1 + if setgroup != "" { + gid, err = parseGroup(setgroup) + if err != nil { + return fmt.Errorf("Invalid group name or gid: %w", err) + } + } + + if len(names) == 0 { + names = bx.Files + } + for _, name := range names { + if !bx.FilesSet[name] { + logErr.Printf("Skipping %q: File not registered with Blackbox", name) + } + if (!overwrite) && bbutil.FileExistsOrProblem(name) { + logErr.Printf("Skipping %q: Will not overwrite existing file", name) + continue + } + if bx.Crypter == nil { + fmt.Printf("NO CRYPTER!!!\n") + } + err := bx.Crypter.Decrypt(name, overwrite) + if err != nil { + logErr.Printf("%q: %v", name, err) + continue + } + if groupchange { + os.Chown(name, -1, gid) + } + } + return fmt.Errorf("NOT IMPLEMENTED: Decrypt") } @@ -66,11 +107,11 @@ func (bx *Box) FileAdd(names []string, overwrite bool) error { // FileList lists the files. func (bx *Box) FileList() error { - files, err := bx.getFiles() + err := bx.getFiles() if err != nil { return err } - for _, v := range files { + for _, v := range bx.Files { fmt.Println(v) } return nil @@ -89,7 +130,7 @@ func (bx *Box) Info() error { logErr.Printf("getAdmins error: %v", err) } - _, err = bx.getFiles() + err = bx.getFiles() if err != nil { logErr.Printf("getFiles error: %v", err) } @@ -124,7 +165,7 @@ func (bx *Box) Shred(names []string) error { // Status prints the status of files. func (bx *Box) Status(names []string, nameOnly bool, match string) error { - _, err := bx.getFiles() + err := bx.getFiles() if err != nil { return err } diff --git a/vcs/vcs.go b/vcs/vcs.go index 960ce722..39268206 100644 --- a/vcs/vcs.go +++ b/vcs/vcs.go @@ -26,6 +26,7 @@ var Catalog []*Item // Register a new VCS. func Register(name string, priority int, newfn NewFnSig) { + //fmt.Printf("VCS registered: %v\n", name) item := &Item{ Name: name, New: newfn, From 66eeea284096dc4235edba1f6a13b1e22cde3605 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 17:48:06 -0400 Subject: [PATCH 25/69] snapshot! --- crypters/_all/all.go | 5 ++ crypters/crypters.go | 39 ++++++++++++++ crypters/gnupg/gnupg.go | 25 +++++++++ models/crypters.go | 7 +++ pkg/box/boxutils.go | 116 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 192 insertions(+) create mode 100644 crypters/_all/all.go create mode 100644 crypters/crypters.go create mode 100644 crypters/gnupg/gnupg.go create mode 100644 models/crypters.go create mode 100644 pkg/box/boxutils.go diff --git a/crypters/_all/all.go b/crypters/_all/all.go new file mode 100644 index 00000000..468ce579 --- /dev/null +++ b/crypters/_all/all.go @@ -0,0 +1,5 @@ +package all + +import ( + _ "github.com/StackExchange/blackbox/crypters/gnupg" +) diff --git a/crypters/crypters.go b/crypters/crypters.go new file mode 100644 index 00000000..75957510 --- /dev/null +++ b/crypters/crypters.go @@ -0,0 +1,39 @@ +package crypters + +import ( + "sort" + + "github.com/StackExchange/blackbox/models" +) + +// Crypter is the handle +type Crypter interface { + models.Crypter +} + +// NewFnSig function signature needed by reg. +type NewFnSig func() (Crypter, error) + +// Item stores one item +type Item struct { + Name string + New NewFnSig + Priority int +} + +// Catalog is the list of registered vcs's. +var Catalog []*Item + +// Register a new VCS. +func Register(name string, priority int, newfn NewFnSig) { + //fmt.Printf("CRYPTER registered: %v\n", name) + item := &Item{ + Name: name, + New: newfn, + Priority: priority, + } + Catalog = append(Catalog, item) + + // Keep the list sorted. + sort.Slice(Catalog, func(i, j int) bool { return Catalog[j].Priority < Catalog[i].Priority }) +} diff --git a/crypters/gnupg/gnupg.go b/crypters/gnupg/gnupg.go new file mode 100644 index 00000000..a0b82eb3 --- /dev/null +++ b/crypters/gnupg/gnupg.go @@ -0,0 +1,25 @@ +package gnupg + +import ( + "fmt" + + "github.com/StackExchange/blackbox/crypters" +) + +func init() { + crypters.Register("GnuPG", 100, registerNew) +} + +// CrypterHandle is the handle +type CrypterHandle struct { +} + +func registerNew() (crypters.Crypter, error) { + return &CrypterHandle{}, nil +} + +// Decrypt decrypts a file, possibly overwriting the plaintext. +func (crypt CrypterHandle) Decrypt(name string, overwrite bool) error { + fmt.Printf("WOULD decrypt %v (overwrite=%v)\n", name, overwrite) + return nil +} diff --git a/models/crypters.go b/models/crypters.go new file mode 100644 index 00000000..acb93343 --- /dev/null +++ b/models/crypters.go @@ -0,0 +1,7 @@ +package models + +// CryptoSystem is gpg binaries, go-opengpg, etc. +type Crypter interface { + // Decrypt name+".gpg", possibly overwriting name. + Decrypt(filename string, overwrite bool) error +} diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go new file mode 100644 index 00000000..b9cefff6 --- /dev/null +++ b/pkg/box/boxutils.go @@ -0,0 +1,116 @@ +package box + +import ( + "fmt" + "os" + "os/user" + "strconv" +) + +// FileStatus returns the status of a file. +func FileStatus(name string) (string, error) { + /* + DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). + ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. + SHREDDED: Plaintext is missing. + GPGMISSING: The .gpg file is missing. Oops? + PLAINERROR: Can't access the plaintext file to determine status. + GPGERROR: Can't access .gpg file to determine status. + */ + + p := name + e := p + ".gpg" + ps, perr := os.Stat(p) + es, eerr := os.Stat(e) + if perr == nil && eerr == nil { + if ps.ModTime().Before(es.ModTime()) { + return "ENCRYPTED", nil + } + return "DECRYPTED", nil + } + + if eerr != nil { + if os.IsNotExist(eerr) { + return "GPGMISSING", nil + } + return "GPGERROR", eerr + } + + if perr != nil { + if os.IsNotExist(perr) { + return "SHREDDED", nil + } + } + return "PLAINERROR", perr +} + +// func isChanged(pname string) (bool, error) { +// // if .gpg exists but not plainfile: unchanged +// // if plaintext exists but not .gpg: changed +// // if plainfile < .gpg: unchanged +// // if plainfile > .gpg: don't know, need to try diff + +// // Gather info about the files: + +// pstat, perr := os.Stat(pname) +// if perr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", pname, perr) +// } +// gname := pname + ".gpg" +// gstat, gerr := os.Stat(gname) +// if gerr != nil && (!os.IsNotExist(perr)) { +// return false, fmt.Errorf("isChanged(%q) returned error: %w", gname, gerr) +// } + +// pexists := perr == nil +// gexists := gerr == nil + +// // Use the above rules: + +// // if .gpg exists but not plainfile: unchanged +// if gexists && !pexists { +// return false, nil +// } + +// // if plaintext exists but not .gpg: changed +// if pexists && !gexists { +// return true, nil +// } + +// // At this point we can conclude that both p and g exist. +// // Can't hurt to test that assertion. +// if (!pexists) && (!gexists) { +// return false, fmt.Errorf("Assertion failed. p and g should exist: pn=%q", pname) +// } + +// pmodtime := pstat.ModTime() +// gmodtime := gstat.ModTime() +// // if plainfile < .gpg: unchanged +// if pmodtime.Before(gmodtime) { +// return false, nil +// } +// // if plainfile > .gpg: don't know, need to try diff +// return false, fmt.Errorf("Can not know for sure. Try git diff?") +// } + +func parseGroup(userinput string) (int, error) { + if userinput == "" { + return -1, fmt.Errorf("group spec is empty string") + } + + // If it is a valid number, use it. + i, err := strconv.Atoi(userinput) + if err == nil { + return i, nil + } + + // If not a number, look it up by name. + g, err := user.LookupGroup(userinput) + if err != nil { + i, err = strconv.Atoi(g.Gid) + return i, nil + } + + // Give up. + return -1, err +} From 9998758dee38d682ded1da76cb6a34acc1278f68 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 17:56:20 -0400 Subject: [PATCH 26/69] Rename pkgs --- cmd/blackbox/blackbox.go | 8 ++++---- cmd/blackbox/parse.go | 2 +- crypters/_all/all.go | 5 ----- go.mod | 2 +- pkg/bbutil/repoinfo.go | 4 ++-- pkg/box/box.go | 6 +++--- pkg/box/verbs.go | 2 +- pkg/crypters/_all/all.go | 5 +++++ {crypters => pkg/crypters}/crypters.go | 2 +- {crypters => pkg/crypters}/gnupg/gnupg.go | 2 +- pkg/vcs/_all/all.go | 6 ++++++ {vcs => pkg/vcs}/git/git.go | 4 ++-- {vcs => pkg/vcs}/none/none.go | 2 +- {vcs => pkg/vcs}/vcs.go | 2 +- vcs/_all/all.go | 6 ------ 15 files changed, 29 insertions(+), 29 deletions(-) delete mode 100644 crypters/_all/all.go create mode 100644 pkg/crypters/_all/all.go rename {crypters => pkg/crypters}/crypters.go (93%) rename {crypters => pkg/crypters}/gnupg/gnupg.go (89%) create mode 100644 pkg/vcs/_all/all.go rename {vcs => pkg/vcs}/git/git.go (80%) rename {vcs => pkg/vcs}/none/none.go (85%) rename {vcs => pkg/vcs}/vcs.go (93%) delete mode 100644 vcs/_all/all.go diff --git a/cmd/blackbox/blackbox.go b/cmd/blackbox/blackbox.go index 6016a76f..28ce017d 100644 --- a/cmd/blackbox/blackbox.go +++ b/cmd/blackbox/blackbox.go @@ -4,10 +4,10 @@ import ( "fmt" "os" - _ "github.com/StackExchange/blackbox/crypters" - _ "github.com/StackExchange/blackbox/crypters/_all" - _ "github.com/StackExchange/blackbox/vcs" - _ "github.com/StackExchange/blackbox/vcs/_all" + _ "github.com/StackExchange/blackbox/v2/pkg/crypters" + _ "github.com/StackExchange/blackbox/v2/pkg/crypters/_all" + _ "github.com/StackExchange/blackbox/v2/pkg/vcs" + _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" ) var dryRun bool diff --git a/cmd/blackbox/parse.go b/cmd/blackbox/parse.go index fbb6eaea..6414f76e 100644 --- a/cmd/blackbox/parse.go +++ b/cmd/blackbox/parse.go @@ -8,7 +8,7 @@ import ( "log" "os" - "github.com/StackExchange/blackbox/pkg/box" + "github.com/StackExchange/blackbox/v2/pkg/box" "github.com/urfave/cli/v2" ) diff --git a/crypters/_all/all.go b/crypters/_all/all.go deleted file mode 100644 index 468ce579..00000000 --- a/crypters/_all/all.go +++ /dev/null @@ -1,5 +0,0 @@ -package all - -import ( - _ "github.com/StackExchange/blackbox/crypters/gnupg" -) diff --git a/go.mod b/go.mod index 06587bbf..639047b5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/StackExchange/blackbox +module github.com/StackExchange/blackbox/v2 go 1.14 diff --git a/pkg/bbutil/repoinfo.go b/pkg/bbutil/repoinfo.go index 3d333642..a8eb1665 100644 --- a/pkg/bbutil/repoinfo.go +++ b/pkg/bbutil/repoinfo.go @@ -4,8 +4,8 @@ package bbutil // "os" // "path/filepath" // -// "github.com/StackExchange/blackbox/pkg/bbgit" -// "github.com/StackExchange/blackbox/pkg/bbnone" +// "github.com/StackExchange/blackbox/v2/pkg/bbgit" +// "github.com/StackExchange/blackbox/v2/pkg/bbnone" // ) // // // Vcser is the interface that defines a plug-in VCS system. diff --git a/pkg/box/box.go b/pkg/box/box.go index 65ccc56f..95315ced 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -10,9 +10,9 @@ import ( "path/filepath" "strings" - "github.com/StackExchange/blackbox/crypters" - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/StackExchange/blackbox/vcs" + "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/crypters" + "github.com/StackExchange/blackbox/v2/pkg/vcs" "github.com/urfave/cli/v2" ) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 25b407e5..b5ea114c 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -7,7 +7,7 @@ import ( "os" "strings" - "github.com/StackExchange/blackbox/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/olekukonko/tablewriter" ) diff --git a/pkg/crypters/_all/all.go b/pkg/crypters/_all/all.go new file mode 100644 index 00000000..bcde761b --- /dev/null +++ b/pkg/crypters/_all/all.go @@ -0,0 +1,5 @@ +package all + +import ( + _ "github.com/StackExchange/blackbox/v2/pkg/crypters/gnupg" +) diff --git a/crypters/crypters.go b/pkg/crypters/crypters.go similarity index 93% rename from crypters/crypters.go rename to pkg/crypters/crypters.go index 75957510..73e95f70 100644 --- a/crypters/crypters.go +++ b/pkg/crypters/crypters.go @@ -3,7 +3,7 @@ package crypters import ( "sort" - "github.com/StackExchange/blackbox/models" + "github.com/StackExchange/blackbox/v2/models" ) // Crypter is the handle diff --git a/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go similarity index 89% rename from crypters/gnupg/gnupg.go rename to pkg/crypters/gnupg/gnupg.go index a0b82eb3..0deb5c58 100644 --- a/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -3,7 +3,7 @@ package gnupg import ( "fmt" - "github.com/StackExchange/blackbox/crypters" + "github.com/StackExchange/blackbox/v2/pkg/crypters" ) func init() { diff --git a/pkg/vcs/_all/all.go b/pkg/vcs/_all/all.go new file mode 100644 index 00000000..2ad31ed0 --- /dev/null +++ b/pkg/vcs/_all/all.go @@ -0,0 +1,6 @@ +package all + +import ( + _ "github.com/StackExchange/blackbox/v2/pkg/vcs/git" + _ "github.com/StackExchange/blackbox/v2/pkg/vcs/none" +) diff --git a/vcs/git/git.go b/pkg/vcs/git/git.go similarity index 80% rename from vcs/git/git.go rename to pkg/vcs/git/git.go index 60389988..b4d2a4dd 100644 --- a/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -3,8 +3,8 @@ package git import ( "path/filepath" - "github.com/StackExchange/blackbox/pkg/bbutil" - "github.com/StackExchange/blackbox/vcs" + "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/vcs" ) func init() { diff --git a/vcs/none/none.go b/pkg/vcs/none/none.go similarity index 85% rename from vcs/none/none.go rename to pkg/vcs/none/none.go index faa115f4..5e35ef9f 100644 --- a/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -1,7 +1,7 @@ package none import ( - "github.com/StackExchange/blackbox/vcs" + "github.com/StackExchange/blackbox/v2/pkg/vcs" ) func init() { diff --git a/vcs/vcs.go b/pkg/vcs/vcs.go similarity index 93% rename from vcs/vcs.go rename to pkg/vcs/vcs.go index 39268206..8cfa9ee3 100644 --- a/vcs/vcs.go +++ b/pkg/vcs/vcs.go @@ -3,7 +3,7 @@ package vcs import ( "sort" - "github.com/StackExchange/blackbox/models" + "github.com/StackExchange/blackbox/v2/models" ) // Vcs is the handle diff --git a/vcs/_all/all.go b/vcs/_all/all.go deleted file mode 100644 index f6512648..00000000 --- a/vcs/_all/all.go +++ /dev/null @@ -1,6 +0,0 @@ -package all - -import ( - _ "github.com/StackExchange/blackbox/vcs/git" - _ "github.com/StackExchange/blackbox/vcs/none" -) From 8c329b1305912565b904d1255f3f3ecf87611671 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 18:00:05 -0400 Subject: [PATCH 27/69] linting --- models/crypters.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/crypters.go b/models/crypters.go index acb93343..b618eaae 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -1,6 +1,6 @@ package models -// CryptoSystem is gpg binaries, go-opengpg, etc. +// Crypter is gpg binaries, go-opengpg, etc. type Crypter interface { // Decrypt name+".gpg", possibly overwriting name. Decrypt(filename string, overwrite bool) error From 56665df1ff59c0396acf1d2621f1738e50e35a5f Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 7 Jun 2020 22:36:02 -0400 Subject: [PATCH 28/69] Implement decrypt --askagent and --umask --- DESIGN.md | 33 ++++++++++++++++++ cmd/blackbox/{flags.go => cli.go} | 19 ++++++++--- cmd/blackbox/{parse.go => drive.go} | 52 +++++++++-------------------- models/crypters.go | 2 +- pkg/bbutil/runbash.go | 5 ++- pkg/box/box.go | 10 +++--- pkg/box/boxutils.go | 18 ++++++++++ pkg/box/verbs.go | 18 +++++++--- pkg/crypters/gnupg/gnupg.go | 41 ++++++++++++++++++++--- 9 files changed, 140 insertions(+), 58 deletions(-) rename cmd/blackbox/{flags.go => cli.go} (88%) rename cmd/blackbox/{parse.go => drive.go} (83%) diff --git a/DESIGN.md b/DESIGN.md index c4ae6623..28ca459f 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -39,3 +39,36 @@ They layers look like this: | Interfaces | `pkg/*` | Interfaces and their implementations | At least that's the goal. We'll see how well we can achieve this. + + +Version 2.0 +=========== + +Software architecture. + +We try to keep the command-line parsing separate from the business +logic and all plug-ins. This keeps things clean and easy to refactor. +In fact layer 2 could be used as a stand-alone module for projects +that want to embed blackbox actions. + +Layer 1: The command itself + + * cmd/blackbox/blackbox.go -- main() not much more + * cmd/blackbox/cli.go -- Set up and call the ufave/cli flag parser + * cmd/blackbox/drive.go -- Check # of arguments, conflicting flags, and then call the businss logic layer + +Layer 2: The business logic + + * pkg/box/box.go -- The interface to accessing .blackbox (admins, files, etc.) + * pkg/box/verbs.go -- Verbs called by Layer 1. Just the verbs + * pkg/box/boxutils.go -- Functions needed by the verbs + +Layer 3: The plug-ins + + * pkg/vcs/... -- Plug-ins for Git, (Mercurial, Subversion, Perforce,) and None + * pkg/crypters/... -- Plug-ins for PGP access: GnuPG, (go-openpgp, others in the future) + +Layer 4: Support functions for use by Layer 3 + + * pkg/bbutil/filestats.go -- File manipulations + * pkg/bbutil/runbash.go -- Safely run external Linux commands diff --git a/cmd/blackbox/flags.go b/cmd/blackbox/cli.go similarity index 88% rename from cmd/blackbox/flags.go rename to cmd/blackbox/cli.go index 9ef10f8f..5f14edf0 100644 --- a/cmd/blackbox/flags.go +++ b/cmd/blackbox/cli.go @@ -1,5 +1,7 @@ package main +// cli.go -- Create urfave/cli datastructures and apply them. + import ( "github.com/urfave/cli/v2" ) @@ -17,9 +19,16 @@ func flags() *cli.App { // Destination: &dryRun, // }, &cli.StringFlag{ - Name: "crypto", - Usage: "Crypto back-end plugin", - Value: "GnuPG", + Name: "crypto", + Usage: "Crypto back-end plugin", + Value: "GnuPG", + EnvVars: []string{"BLACKBOX_FLAG_CRYPTO"}, + }, + &cli.IntFlag{ + Name: "umask", + Usage: "umask to set when decrypting", + Value: 0o027, + EnvVars: []string{"BLACKBOX_FLAG_UMASK", "DECRYPT_UMASK"}, }, } @@ -33,9 +42,9 @@ func flags() *cli.App { Usage: "Decrypt file(s)", Flags: []cli.Flag{ &cli.BoolFlag{Name: "all", Usage: "All registered files"}, - &cli.BoolFlag{Name: "bulk", Usage: "Do not prompt to start gpg-agent"}, + &cli.BoolFlag{Name: "agentcheck", Usage: "Do not check for gpg-agent when using --all"}, &cli.StringFlag{Name: "group", Usage: "Set group ownership"}, - &cli.StringFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"}, + &cli.BoolFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"}, }, Action: func(c *cli.Context) error { return cmdDecrypt(c) }, }, diff --git a/cmd/blackbox/parse.go b/cmd/blackbox/drive.go similarity index 83% rename from cmd/blackbox/parse.go rename to cmd/blackbox/drive.go index 6414f76e..447baa7e 100644 --- a/cmd/blackbox/parse.go +++ b/cmd/blackbox/drive.go @@ -1,7 +1,7 @@ package main -// Processes the flags and arguments and calls the appropriate -// business logic. +// Now that cli.go has processed the flags, validate there are no +// conflicts and drive to the business logic. import ( "fmt" @@ -21,14 +21,11 @@ func init() { } func allOrSomeFiles(c *cli.Context) error { - if c.Bool("all") { - if c.Args().Present() { - return fmt.Errorf("Can not specify filenames and --all") - } - } else { - if !c.Args().Present() { - return fmt.Errorf("Must specify at least one file name or --all") - } + if c.Bool("all") && c.Args().Present() { + return fmt.Errorf("Can not specify filenames and --all") + } + if (!c.Args().Present()) && (!c.Bool("all")) { + return fmt.Errorf("Must specify at least one file name or --all") } return nil } @@ -72,14 +69,18 @@ func cmdDecrypt(c *cli.Context) error { if err := allOrSomeFiles(c); err != nil { return err } - bulk := false - if c.Bool("all") { - bulk = c.Bool("bulk") // Only applies to --all + + // The default for --agentcheck is off normally, and on when using --all. + pauseNeeded := c.Bool("all") + // If the user used the flag, abide by it. + if c.IsSet("agentcheck") { + pauseNeeded = c.Bool("agentcheck") } + bx := box.NewFromFlags(c) return bx.Decrypt(c.Args().Slice(), c.Bool("overwrite"), - bulk, + pauseNeeded, c.String("group"), ) } @@ -177,29 +178,6 @@ func cmdStatus(c *cli.Context) error { return bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type")) } -// func cmdDecrypt(allFiles bool, filenames []string, group string) error { -// bbu, err := bbutil.New() -// if err != nil { -// return err -// } -// -// // prepare_keychain -// -// fnames, valid, err := bbu.FileIterator(allFiles, filenames) -// if err != nil { -// return errors.Wrap(err, "decrypt") -// } -// for i, filename := range fnames { -// if valid[i] { -// bbu.DecryptFile(filename, group, true) -// } else { -// fmt.Fprintf(os.Stderr, "SKIPPING: %q\n", filename) -// } -// } -// -// return nil -// } - // func cmdRegList(c *cli.Context) error { // if len(c.Args()) != 0 { // fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") diff --git a/models/crypters.go b/models/crypters.go index b618eaae..8528b8ed 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -3,5 +3,5 @@ package models // Crypter is gpg binaries, go-opengpg, etc. type Crypter interface { // Decrypt name+".gpg", possibly overwriting name. - Decrypt(filename string, overwrite bool) error + Decrypt(filename string, overwrite bool, umask int) error } diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index fcacd69c..5635110d 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -22,5 +22,8 @@ func RunBash(command string, args ...string) error { log.Fatal(err) } err = cmd.Wait() - return fmt.Errorf("run_bash: %w", err) + if err != nil { + return fmt.Errorf("run_bash: %w", err) + } + return nil } diff --git a/pkg/box/box.go b/pkg/box/box.go index 95315ced..2b63db5c 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -22,15 +22,16 @@ type Box struct { RepoBaseDir string // Base directory of the repo. ConfigDir string // Path to the .blackbox config directory. // - Admins []string // If non-empty, the list of admins. - Files []string // If non-empty, the list of files. - FilesSet map[string]bool // If non-nil, a set of Files. - IsRegistered map[string]bool // + Admins []string // If non-empty, the list of admins. + Files []string // If non-empty, the list of files. + FilesSet map[string]bool // If non-nil, a set of Files. // Vcs vcs.Vcs // Interface access to the VCS. VcsName string // name of the VCS Crypter crypters.Crypter // Inteface access to GPG. CrypterName string // Name of the crypter in use. + // + Umask int // umask to set when decrypting } // StatusMode is a type of query. @@ -64,6 +65,7 @@ func NewFromFlags(c *cli.Context) *Box { } bx.RepoBaseDir = repoBaseDir bx.ConfigDir = configDir + bx.Umask = c.Int("umask") // Discover which kind of VCS is in use. var h vcs.Vcs diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index b9cefff6..99a98196 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -1,6 +1,7 @@ package box import ( + "bufio" "fmt" "os" "os/user" @@ -114,3 +115,20 @@ func parseGroup(userinput string) (int, error) { // Give up. return -1, err } + +func gpgAgentNotice() { + // Is gpg-agent configured? + if os.Getenv("GPG_AGENT_INFO") != "" { + return + } + + // TODO(tlim): v1 verifies that "gpg-agent --version" outputs a version + // string that is 2.1.0 or higher. It seems that 1.x is incompatible. + + fmt.Println("WARNING: You probably want to run gpg-agent as") + fmt.Println("you will be asked for your passphrase many times.") + fmt.Println("Example: $ eval $(gpg-agent --daemon)") + fmt.Print("Press CTRL-C now to stop. ENTER to continue: ") + input := bufio.NewScanner(os.Stdin) + input.Scan() +} diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index b5ea114c..2a7ba475 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -58,10 +58,15 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup } } + if bulkpause { + gpgAgentNotice() + } + if len(names) == 0 { names = bx.Files } for _, name := range names { + fmt.Printf("========== DECRYPTING %q\n", name) if !bx.FilesSet[name] { logErr.Printf("Skipping %q: File not registered with Blackbox", name) } @@ -69,20 +74,23 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup logErr.Printf("Skipping %q: Will not overwrite existing file", name) continue } - if bx.Crypter == nil { - fmt.Printf("NO CRYPTER!!!\n") - } - err := bx.Crypter.Decrypt(name, overwrite) + + // TODO(tlim) v1 detects zero-length files and removes them, even + // if overwrite is disabled. I don't think anyone has ever used that + // feature. That said, we could immplement that here. + + err := bx.Crypter.Decrypt(name, overwrite, bx.Umask) if err != nil { logErr.Printf("%q: %v", name, err) continue } + if groupchange { os.Chown(name, -1, gid) } } - return fmt.Errorf("NOT IMPLEMENTED: Decrypt") + return nil } // Diff ... diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 0deb5c58..d5acc451 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -1,8 +1,10 @@ package gnupg import ( - "fmt" + "os" + "os/exec" + "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/crypters" ) @@ -12,14 +14,43 @@ func init() { // CrypterHandle is the handle type CrypterHandle struct { + GPGCmd string // "gpg2" or "gpg" } func registerNew() (crypters.Crypter, error) { - return &CrypterHandle{}, nil + + crypt := &CrypterHandle{} + + // Which binary to use? + path, err := exec.LookPath("gpg2") + if err != nil { + path, err = exec.LookPath("gpg") + if err != nil { + path = "gpg2" + } + } + crypt.GPGCmd = path + + return crypt, nil } // Decrypt decrypts a file, possibly overwriting the plaintext. -func (crypt CrypterHandle) Decrypt(name string, overwrite bool) error { - fmt.Printf("WOULD decrypt %v (overwrite=%v)\n", name, overwrite) - return nil +func (crypt CrypterHandle) Decrypt(name string, overwrite bool, umask int) error { + + if overwrite { + _ = os.Remove(name) + } + + crypt.prepareKeyChain() + + //oldumask := syscall.Umask(umask) + err := bbutil.RunBash(crypt.GPGCmd, + "--use-agent", + "-q", + "--decrypt", + "-o", name, + name+".gpg", + ) + //syscall.Umask(oldumask) + return err } From f860c73a12155c7e98204460e18fb8820f1e1e65 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 8 Jun 2020 01:20:07 -0400 Subject: [PATCH 29/69] Random fixes --- Version2-Ideas.md | 35 ++--------- integrationTest/NOTES.txt | 83 +++++++++++++++++++++++++ pkg/box/verbs.go | 16 +++-- pkg/crypters/gnupg/gnupg.go | 7 +-- pkg/crypters/gnupg/keychain.go | 107 +++++++++++++++++++++++++++++++++ 5 files changed, 207 insertions(+), 41 deletions(-) create mode 100644 integrationTest/NOTES.txt create mode 100644 pkg/crypters/gnupg/keychain.go diff --git a/Version2-Ideas.md b/Version2-Ideas.md index ec688ca9..1cbe8df9 100644 --- a/Version2-Ideas.md +++ b/Version2-Ideas.md @@ -18,24 +18,6 @@ These are the things I'd like to change someday. There should be one program, with subcommands that have names that make more sense: -* `blackbox init` -* `blackbox info` -* `blackbox admin add ` -* `blackbox admin remove ` -* `blackbox admin list` -* `blackbox files add` -* `blackbox files list` -* `blackbox files remove` -* `blackbox encrypt ...` -* `blackbox decrypt ...` -* `blackbox cat ...` -* `blackbox edit ...` -* `blackbox reencrypt` -* `blackbox shred` -* `blackbox diff ...` -* `blackbox files list-unchanged` -* `blackbox files list-changed` - * `blackbox admin add ` * `blackbox admin list` * `blackbox admin remove ` @@ -44,23 +26,14 @@ There should be one program, with subcommands that have names that make more sen * `blackbox diff ...` * `blackbox edit ...` * `blackbox encrypt ...` -* `blackbox file add` +* `blackbox file add ...` * `blackbox file list` -* `blackbox file remove` +* `blackbox file remove ...` * `blackbox info` * `blackbox init` * `blackbox reencrypt` -* `blackbox shred` -* `blackbox status all` -* `blackbox status changed` -* `blackbox status unchanged` - - -Flags where appropriate - -* -verbose -v -* -noshred -* -debug +* `blackbox shred --all| ...` +* `blackbox status --all| ...` Backwards compatibility: The old scripts will be rewritten to use the new commands. diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt new file mode 100644 index 00000000..57cbe97e --- /dev/null +++ b/integrationTest/NOTES.txt @@ -0,0 +1,83 @@ + + + +Something like... + +# Startup +* Create a repo (git, none) + +# Test basic operations: +* As Alice: + * initialize blackbox, add her keys to it, see that the usual files + exist. See her name in bb-admins.txt + * encrypt a file, see that the plaintext is deleted, see the file in bb-files.txt + * decrypt the file, see the original plaintext is recovered. + * Encrypt a file --noshred. + * Decrypt the file, it should fail as the plaintext exists. + * Remove the plaintext. + * Decrypt the file, it should fail as the plaintext exists. + +# Test hand-off from Alice to Bob. +* As Bob + * add himself to the admins. +* As Alice + * Update-all-files + * Create a new file. Encrypt it. +* As Bob + * Decrypt both files + * Verify contents of the new file, and the file from previous. + * Create a new file. Encrypt it. +* As Alice: + * Decrypt all files. + * Verify contents of the 3 plaintext files. + +# Test a git-less directory +* Copy the old repo somewhere. Remove the .git directory. +* As Alice: + * Decrypt all + * Verify plaintext contents + +# Test post-deploy with/without GID +* Back at the original repo: +* Shred all +* Run post-deploy. Verify. +* Shred all +* Run post-deploy with a custom GID. Verify. + +# Test removing an admin +* As Bob: + * removes Alice. (Verify) + * Re-encrypt + * Decrypt all & verify. +* As alice + * Decrypting should fail. + +# Test funny names and paths + * my/path/to/relsecrets.txt + * cwd=other/place ../../my/path/to/relsecrets.txt + * !important!.txt + * #andpounds.txt + * stars*bars?.txt + * space space.txt +* Do add/encrypt/decrypt +* Do blackbox_update_all_files +* Do remove them all + +# When people start asking for commands to work with relative paths +# Test from outside the repo +* mkdir ../other/place +* cd ../other/place +* decrypt ../../secret1.txt +* encrypt ../../secret1.txt + +# Test specific commands: +# blackbox admins list +# blackbox file list +# blackbox status --name-only (create 1 of each "type") +# blackbox status --type=FOO + +# These should all fail: +# blackbox file list --all +# blackbox file list blah +# blackbox shred list --all +# blackbox shred list blah diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 2a7ba475..e891207e 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -143,14 +143,18 @@ func (bx *Box) Info() error { logErr.Printf("getFiles error: %v", err) } - fmt.Println("BLACKBOX:") - fmt.Printf("bx.ConfigDir=%q\n", bx.ConfigDir) //fmt.Printf("bx.Admins=%q\n", bx.Admins) - fmt.Printf("len(bx.Admins)=%v\n", len(bx.Admins)) //fmt.Printf("bx.Files=%q\n", bx.Files) - fmt.Printf("len(bx.Files)=%v\n", len(bx.Files)) - fmt.Printf("bx.Vcs=%v\n", bx.Vcs) - fmt.Printf("bx.VcsName=%q\n", bx.VcsName) + + fmt.Println("BLACKBOX:") + fmt.Printf(" ConfigDir: %q\n", bx.ConfigDir) + fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) + fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) + fmt.Printf(" Files: count=%v\n", len(bx.Files)) + fmt.Printf(" Vcs: %v\n", bx.Vcs) + fmt.Printf(" VcsName: %q\n", bx.VcsName) + fmt.Printf(" Crypter: %v\n", bx.Crypter) + fmt.Printf(" CrypterName: %q\n", bx.CrypterName) return nil } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index d5acc451..5fbb7976 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -3,6 +3,7 @@ package gnupg import ( "os" "os/exec" + "syscall" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/crypters" @@ -41,9 +42,7 @@ func (crypt CrypterHandle) Decrypt(name string, overwrite bool, umask int) error _ = os.Remove(name) } - crypt.prepareKeyChain() - - //oldumask := syscall.Umask(umask) + oldumask := syscall.Umask(umask) err := bbutil.RunBash(crypt.GPGCmd, "--use-agent", "-q", @@ -51,6 +50,6 @@ func (crypt CrypterHandle) Decrypt(name string, overwrite bool, umask int) error "-o", name, name+".gpg", ) - //syscall.Umask(oldumask) + syscall.Umask(oldumask) return err } diff --git a/pkg/crypters/gnupg/keychain.go b/pkg/crypters/gnupg/keychain.go new file mode 100644 index 00000000..93b0f626 --- /dev/null +++ b/pkg/crypters/gnupg/keychain.go @@ -0,0 +1,107 @@ +package gnupg + +/* + +# How does Blackbox manage key rings? + +Blackbox uses the user's .gnupg directory for most actions, such as decrypting data. +Decrypting requires the user's private key, which is stored by the user in their +home directory (and up to them to store safely). +Black box does not store the user's private key in the repo. + +When encrypting data, black needs the public key of all the admins, not just the users. +To assure that the user's `.gnupg` has all these public keys, prior to +encrypting data the public keys are imported from .blackbox, which stores +a keychain that stores the public (not private!) keys of all the admins. + +FYI: v1 does this import before decrypting, because I didn't know any better. + +# Binary compatibility: + +When writing v1, we didn't realize that the pubkey.gpg file is a binary format +that is not intended to be portable. In fact, it is intentionally not portable. +This means that all admins must use the exact same version of GnuPG +or the files (pubring.gpg or pubring.kbx) may get corrupted. + +In v2, we store the public keys in the portable ascii format +in a file called `.blackbox/public-keys-db.asc`. +It will also update the binary files if they exist. +If `.blackbox/public-keys-db.asc` doesn't exist, it will be created. + +Eventually we will stop updating the binary files. + +# Importing public keys to the user + +How to import the public keys to the user's GPG system: + +If pubkeyring-ascii.txt exists: + gpg --import pubkeyring-ascii.asc +Else if pubring.kbx + gpg --import pubring.kbx +Else if pubring.gpg + gpg --import pubring.gpg + +This is what v1 does: + #if gpg2 is installed next to gpg like on ubuntu 16 + if [[ "$GPG" != "gpg2" ]]; then + $GPG --export --no-default-keyring --keyring "$(get_pubring_path)" >"$keyringasc" + $GPG --import "$keyringasc" 2>&1 | egrep -v 'not changed$' >&2 + else + $GPG --keyring "$(get_pubring_path)" --export | $GPG --import + fi + +# How to add a key to the keyring? + +Old, binary format: + # Get the key they want to add: + FOO is a user-specified directory, otherwise $HOME/.gnupg: + $GPG --homedir="FOO" --export -a "$KEYNAME" >TEMPFILE + # Import into the binary files: + KEYRINGDIR is .blackbox + $GPG --no-permission-warning --homedir="$KEYRINGDIR" --import TEMPFILE + # Git add any of these files if they exist: + pubring.gpg pubring.kbx trustdb.gpg blackbox-admins.txt + # Tell the user to git commit them. + +New, ascii format: + # Get the key to be added. Write to a TEMPFILE + FOO is a user-specified directory, otherwise $HOME/.gnupg: + $GPG --homedir="FOO" --export -a "$KEYNAME" >TEMPFILE + # Make a tempdir called TEMPDIR + # Import the pubkeyring-ascii.txt to TEMPDIR's keyring. (Skip if file not found) + # Import the temp1 data to TEMPDIR + # Export the TEMPDIR to create a new .blackbox/pubkeyring-ascii.txt + PATH_TO_BINARY is the path to .blackbox/pubring.gpg; if that's not found then pubring.kbx + $GPG --keyring PATH_TO_BINARY --export -a --output .blackbox/pubkeyring-ascii.txt + # Git add .blackbox/pubkeyring-ascii.txt and .blackbox/blackbox-admins.txt + # Tell the user to git commit them. + # Delete TEMPDIR + +# How to remove a key from the keyring? + +Old, binary format: + # Remove key from the binary file + $GPG --no-permission-warning --homedir="$KEYRINGDIR" --batch --yes --delete-key "$KEYNAME" || true + # Git add any of these files if they exist: + pubring.gpg pubring.kbx trustdb.gpg blackbox-admins.txt + # Tell the user to git commit them. + +New, ascii format: + # Make a tempdir called TEMPDIR + # Import the pubkeyring-ascii.txt to TEMPDIR's keyring. (Skip if file not found) + # Remove key from the ring file + $GPG --no-permission-warning --homedir="$KEYRINGDIR" --batch --yes --delete-key "$KEYNAME" || true + # Export the TEMPDIR to create a new .blackbox/pubkeyring-ascii.txt + PATH_TO_BINARY is the path to .blackbox/pubring.gpg; if that's not found then pubring.kbx + $GPG --keyring PATH_TO_BINARY --export -a --output .blackbox/pubkeyring-ascii.txt + # Git add .blackbox/pubkeyring-ascii.txt and .blackbox/blackbox-admins.txt + # Update the .blackbox copy of pubring.gpg, pubring.kbx, or trustdb.gpg (if they exist) + # with copies from TEMPDIR (if they exist). Git add any files that are updated. + # Tell the user to git commit them. + # Delete TEMPDIR + +*/ + +func prepareUserKeychain() error { + return nil +} From b3aa0450e131655d48065e1e4ad84fbbec2d595e Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 8 Jun 2020 01:24:27 -0400 Subject: [PATCH 30/69] snapshot --- integrationTest/NOTES.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index 57cbe97e..ff74278f 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -3,6 +3,10 @@ Something like... + +This should accept VCS-type and --crypto flags. +Then a shell script should run various combinations of VCS and crypters. + # Startup * Create a repo (git, none) From df2bb78b3580459d5c8ac870bb634a1e58472888 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 9 Jun 2020 12:01:59 -0400 Subject: [PATCH 31/69] integration test framework works --- .gitignore | 4 + cmd/blackbox/cli.go | 12 ++ cmd/blackbox/drive.go | 64 +++-------- go.mod | 2 + go.sum | 19 ++++ integrationTest/integration_test.go | 100 +++++++++++++++++ integrationTest/ithelpers.go | 167 ++++++++++++++++++++++++++++ models/crypters.go | 2 + models/vcs.go | 5 + pkg/box/box.go | 65 ++++++----- pkg/box/verbs.go | 64 ++++++++++- pkg/crypters/crypters.go | 19 ++++ pkg/crypters/gnupg/gnupg.go | 9 +- pkg/vcs/git/git.go | 18 ++- pkg/vcs/none/none.go | 16 ++- pkg/vcs/vcs.go | 16 +++ 16 files changed, 495 insertions(+), 87 deletions(-) create mode 100644 integrationTest/integration_test.go create mode 100644 integrationTest/ithelpers.go diff --git a/.gitignore b/.gitignore index 03b21364..2df62f6b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,3 +51,7 @@ coverage.xml # Sphinx documentation docs/_build/ + +# Blackbox +bbintegration +.*.swp diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index 5f14edf0..ad3eebd9 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -18,6 +18,11 @@ func flags() *cli.App { // Usage: "show what would have been done", // Destination: &dryRun, // }, + &cli.StringFlag{ + Name: "vcs", + Usage: "Use this VCS (GIT, NONE) rather than autodetect", + EnvVars: []string{"BLACKBOX_FLAG_VCS"}, + }, &cli.StringFlag{ Name: "crypto", Usage: "Crypto back-end plugin", @@ -175,6 +180,13 @@ func flags() *cli.App { Action: func(c *cli.Context) error { return cmdReencrypt(c) }, }, + { + Name: "testing_init", + Usage: "For use with integration test", + Category: "INTEGRATION TEST", + Action: func(c *cli.Context) error { return testingInit(c) }, + }, + // } diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index 447baa7e..62bb3f41 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -146,11 +146,11 @@ func cmdInfo(c *cli.Context) error { } func cmdInit(c *cli.Context) error { - if c.Args().Present() { - return fmt.Errorf("This command takes zero arguments") + if c.Args().Len() > 1 { + return fmt.Errorf("This command takes one or two arguments") } - bx := box.NewFromFlags(c) - return bx.Init() + bx := box.NewUninitialized() + return bx.Init(c.Args().First(), c.String("vcs")) } func cmdReencrypt(c *cli.Context) error { @@ -170,7 +170,6 @@ func cmdShred(c *cli.Context) error { } func cmdStatus(c *cli.Context) error { - if c.Bool("all") && c.Args().Present() { return fmt.Errorf("Can not specify filenames and --all") } @@ -178,45 +177,16 @@ func cmdStatus(c *cli.Context) error { return bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type")) } -// func cmdRegList(c *cli.Context) error { -// if len(c.Args()) != 0 { -// fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") -// return nil -// } -// -// bbu, err := bbutil.New() -// if err != nil { -// return err -// } -// names, err := bbu.RegisteredFiles() -// if err != nil { -// return err -// } -// for _, item := range names { -// fmt.Println(item.Name) -// } -// return nil -// } - -// func cmdRegStatus(c *cli.Context) error { -// -// if len(c.Args()) != 0 { -// fmt.Fprintf(c.App.Writer, "ERROR: Command does not take any arguments\n") -// return nil -// } -// -// bbu, err := bbutil.New() -// if err != nil { -// return err -// } -// names, err := bbu.RegisteredFiles() -// if err != nil { -// return err -// } -// -// for _, item := range names { -// s := bbutil.FileStatus(bbu.RepoBaseDir, item.Name) -// fmt.Printf("%s\t%s\n", s, item.Name) -// } -// return nil -// } +// These are "secret" commands used by the integration tests. + +func testingInit(c *cli.Context) error { + if c.Args().Present() { + return fmt.Errorf("No args required") + } + fmt.Printf( + "c.String(vcs) reports %q\n", + c.String("vcs"), + ) + bx := box.NewBare(c.String("vcs")) + return bx.TestingInitRepo() +} diff --git a/go.mod b/go.mod index 639047b5..7f43a864 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,9 @@ module github.com/StackExchange/blackbox/v2 go 1.14 require ( + github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/mattn/go-runewidth v0.0.9 // indirect github.com/olekukonko/tablewriter v0.0.4 + github.com/sergi/go-diff v1.1.0 // indirect github.com/urfave/cli/v2 v2.2.0 ) diff --git a/go.sum b/go.sum index 460d92fb..76c865f8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,16 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+twI54= github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= @@ -11,9 +21,18 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0= +github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/urfave/cli/v2 v2.2.0 h1:JTTnM6wKzdA0Jqodd966MVj4vWbbquZykeX1sKbe2C4= github.com/urfave/cli/v2 v2.2.0/go.mod h1:SE9GqnLQmjVa0iPEY0f1w3ygNIYcIJ0OKPMoW2caLfQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go new file mode 100644 index 00000000..7c0bbe11 --- /dev/null +++ b/integrationTest/integration_test.go @@ -0,0 +1,100 @@ +package main + +import ( + "flag" + "fmt" + "os" + "os/exec" + "path/filepath" + "testing" +) + +var verbose = flag.Bool("verbose", false, "reveal stderr") +var vcsToTest = flag.String("testvcs", "GIT", "VCS to test") + +//var crypterToTest = flag.String("crypter", "GnuPG", "crypter to test") + +func init() { + testing.Init() + flag.Parse() +} + +func compile(t *testing.T) { + if PathToBlackBox() != "" { + // It's been compiled already. + return + } + // Make sure we have the latest binary + fmt.Println("Compiling") + cmd := exec.Command("go", "build", "-o", "../bbintegration", "../cmd/blackbox") + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + t.Fatalf("setup_compile: %v", err) + } + cwd, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + SetPathToBlackBox(filepath.Join(cwd, "../bbintegration")) +} + +func setup(t *testing.T) { + compile(t) + fmt.Printf("flag.testvcs is %v\n", *vcsToTest) + vh := getVcs(t, *vcsToTest) + fmt.Printf("Using BLACKBOX_FLAG_VCS=%v\n", vh.Name()) + os.Setenv("BLACKBOX_FLAG_VCS", vh.Name()) + +} + +func TestInitInvalidArgs(t *testing.T) { + compile(t) + // Only zero or one args are permitted. + invalidArgs(t, "init", "one", "two") + invalidArgs(t, "init", "one", "two", "three") +} + +func TestBasicCommands(t *testing.T) { + setup(t) + createDummyRepo(t, *vcsToTest) + + // admin + checkOutput(t, "file", "list", + "000-admin-list.txt", + ) + invalidArgs(t, "admin", "list", "--all") + invalidArgs(t, "admin", "one") + + // file + checkOutput(t, "000-file-list.txt", "file", "list") + invalidArgs(t, "file", "list", "one") + invalidArgs(t, "file", "list", "--all") + + // status + + // reencrypt + + // decrypt + + // encrypt + + // edit + invalidArgs(t, "edit") + invalidArgs(t, "edit", "--all") + + // cat + + // diff + + // shred + +} + +// func TestAliceAndBob(t *testing.T) { +// setupUser(t, "alice", "a") +// setupUser(t, "bob", "b") +// runBB(t, "init") +// runBB(t, "admin", "add", "alice@") +// } diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go new file mode 100644 index 00000000..1a08c612 --- /dev/null +++ b/integrationTest/ithelpers.go @@ -0,0 +1,167 @@ +package main + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "sort" + "strings" + "testing" + + "github.com/StackExchange/blackbox/v2/pkg/vcs" + _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" + "github.com/andreyvit/diff" +) + +func getVcs(t *testing.T, name string) vcs.Vcs { + t.Helper() + // Set up the vcs + for _, v := range vcs.Catalog { + fmt.Printf("Testing vcs: %v == %v\n", name, v.Name) + if strings.ToLower(v.Name) == strings.ToLower(name) { + h, err := v.New() + if err != nil { + return nil // No idea how that would happen. + } + return h + } + fmt.Print("Nope.\n") + + } + return nil +} + +// TestBasicCommands's helpers + +func createDummyRepo(t *testing.T, vcsname string) { + t.Helper() + fmt.Printf("createDummyRepo()\n") + + dir, err := ioutil.TempDir("", "repo") + if err != nil { + t.Fatalf("createDummyRepo: Could not make tempdir: %v", err) + } + defer os.RemoveAll(dir) // clean up + + os.Chdir(dir) + + runBB(t, "testing_init") // Runs "git init" and then vcs.Discover() + runBB(t, "init", "yes") + addLineSorted(t, ".blackbox/blackbox-admins.txt", "user1@example.com") + addLineSorted(t, ".blackbox/blackbox-admins.txt", "user2@example.com") + addLineSorted(t, ".blackbox/blackbox-files.txt", "foo.txt") + addLineSorted(t, ".blackbox/blackbox-files.txt", "bar.txt") + makeFile(t, "foo.txt", "I am the foo.txt file!") + makeFile(t, "bar.txt", "I am the foo.txt file!") + makeFile(t, "foo.txt.gpg", "V nz gur sbb.gkg svyr!") + makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!") +} + +func addLineSorted(t *testing.T, name string, newlines ...string) { + t.Helper() + + contents, err := ioutil.ReadFile(name) + if err != nil { + t.Fatalf("addLinesSorted can't read %q: %v", name, err) + } + lines := strings.Split(string(contents), "\n") + lines = append(lines, newlines...) + sort.Strings(lines) + err = ioutil.WriteFile(name, []byte(strings.Join(lines, "\n")), 0o666) + if err != nil { + t.Fatalf("addLinesSorted can't write %q: %v", name, err) + } +} + +func makeFile(t *testing.T, name string, lines ...string) { + t.Helper() + + err := ioutil.WriteFile(name, []byte(strings.Join(lines, "\n")), 0o666) + if err != nil { + t.Fatalf("makeFile can't create %q: %v", name, err) + } +} + +// checkOutput runs blackbox with args, the last arg is the filename +// of the expected output. Error if output is not expected. +func checkOutput(t *testing.T, args ...string) { + t.Helper() + + // Pop off the last arg. Use it as the filename for expected output. + n := len(args) - 1 + name := args[n] + args = args[:n] + + want, err := ioutil.ReadFile(filepath.Join("test_data", name)) + if err != nil { + t.Fatalf("checkOutput can't read %v: %v", name, err) + } + + cmd := exec.Command("blackbox", args...) + cmd.Stdin = nil + cmd.Stdout = nil + cmd.Stderr = os.Stderr + got, err := cmd.Output() + if err != nil { + t.Fatal(fmt.Errorf("checkOutput(%q): %w", args, err)) + } + + if w, g := string(want), string(got); w != g { + t.Errorf("checkOutput(%q) mismatch (-got +want):\n%s", + args, diff.LineDiff(g, w)) + } + +} + +func invalidArgs(t *testing.T, args ...string) { + t.Helper() + + fmt.Printf("invalidArgs(%q): \n", args) + cmd := exec.Command(PathToBlackBox(), args...) + cmd.Stdin = nil + if *verbose { + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + } + err := cmd.Run() + if err == nil { + fmt.Println("BAD") + t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args)) + } + if *verbose { + fmt.Printf("GOOD (expected): err=%q\n", err) + } else { + fmt.Println("GOOD (expected)") + } +} + +// TestAliceAndBob's helpers. + +func setupUser(t *testing.T, user, passphrase string) { + t.Helper() + fmt.Printf("DEBUG: setupUser %q %q\n", user, passphrase) +} + +var pathToBlackBox string + +// PathToBlackBox returns the path to the executable we compile for integration testing. +func PathToBlackBox() string { return pathToBlackBox } + +// SetPathToBlackBox sets the path. +func SetPathToBlackBox(n string) { pathToBlackBox = n } + +func runBB(t *testing.T, args ...string) { + t.Helper() + + fmt.Printf("runBB(%q)\n", args) + cmd := exec.Command(PathToBlackBox(), args...) + cmd.Stdin = nil + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + t.Fatal(fmt.Errorf("runBB(%q): %w", args, err)) + } +} diff --git a/models/crypters.go b/models/crypters.go index 8528b8ed..e8e93e71 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -2,6 +2,8 @@ package models // Crypter is gpg binaries, go-opengpg, etc. type Crypter interface { + // Name returns the plug-in's canonical name. + Name() string // Decrypt name+".gpg", possibly overwriting name. Decrypt(filename string, overwrite bool, umask int) error } diff --git a/models/vcs.go b/models/vcs.go index c8018d40..28711004 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -2,5 +2,10 @@ package models // Vcs is git/hg/etc. type Vcs interface { + // Name returns the plug-in's canonical name. + Name() string + // Discover returns true if the cwd is a VCS of this type. Discover(repobasedir string) bool + // Initialize a repo of this type (for use by integration tests) + TestingInitRepo() error } diff --git a/pkg/box/box.go b/pkg/box/box.go index 2b63db5c..c89757a2 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -26,10 +26,8 @@ type Box struct { Files []string // If non-empty, the list of files. FilesSet map[string]bool // If non-nil, a set of Files. // - Vcs vcs.Vcs // Interface access to the VCS. - VcsName string // name of the VCS - Crypter crypters.Crypter // Inteface access to GPG. - CrypterName string // Name of the crypter in use. + Vcs vcs.Vcs // Interface access to the VCS. + Crypter crypters.Crypter // Inteface access to GPG. // Umask int // umask to set when decrypting } @@ -54,6 +52,34 @@ func init() { logErr = log.New(os.Stderr, "", 0) } +// NewUninitialized creates a box when nothing exists. +// Useful for the "init" subcommand. +func NewUninitialized() *Box { + return &Box{} +} + +// NewBare creates a box in a bare environment, with no +// autodiscovery of VCS. +// Useful only in integration tests. +func NewBare(vcsname string) *Box { + bx := &Box{} + + // Set up the vcs + var vh vcs.Vcs + var err error + for _, v := range vcs.Catalog { + if strings.ToLower(v.Name) == strings.ToLower(vcsname) { + vh, err = v.New() + if err != nil { + return nil // No idea how that would happen. + } + } + } + bx.Vcs = vh + + return bx +} + // NewFromFlags creates a box using items from flags. func NewFromFlags(c *cli.Context) *Box { bx := &Box{} @@ -68,37 +94,10 @@ func NewFromFlags(c *cli.Context) *Box { bx.Umask = c.Int("umask") // Discover which kind of VCS is in use. - var h vcs.Vcs - for _, v := range vcs.Catalog { - h, err = v.New() - if err != nil { - return nil // No idea how that would happen. - } - if h.Discover(bx.RepoBaseDir) { - bx.Vcs = h - bx.VcsName = v.Name - break - } - } - // We can assume something was found because "none" always says yes. + bx.Vcs = vcs.DetermineVcs(bx.RepoBaseDir) // Pick a crypto backend (GnuPG, go-openpgp, etc.) - //var cbe crypters.Crypter - var chandle crypters.Crypter - for _, v := range crypters.Catalog { - //fmt.Printf("Trying %v %v\n", v.Name) - if strings.ToLower(v.Name) == strings.ToLower(c.String("crypto")) { - //fmt.Printf("CRYPTER = %v\n", v.Name) - chandle, err = v.New() - if err != nil { - return nil // No idea how that would happen. - } - bx.Crypter = chandle - bx.CrypterName = v.Name - //fmt.Printf("USING! %v\n", v.Name) - break - } - } + bx.Crypter = crypters.SearchByName(c.String("crypto")) if bx.Crypter == nil { fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the default\n") os.Exit(1) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index e891207e..2397450e 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -3,8 +3,11 @@ package box // This file implements the business logic related to a black box. import ( + "bufio" "fmt" "os" + "path/filepath" + "strconv" "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" @@ -152,16 +155,52 @@ func (bx *Box) Info() error { fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) fmt.Printf(" Vcs: %v\n", bx.Vcs) - fmt.Printf(" VcsName: %q\n", bx.VcsName) + fmt.Printf(" VcsName: %q\n", bx.Vcs.Name()) fmt.Printf(" Crypter: %v\n", bx.Crypter) - fmt.Printf(" CrypterName: %q\n", bx.CrypterName) + fmt.Printf(" CrypterName: %q\n", bx.Crypter.Name()) return nil } // Init initializes a repo. -func (bx *Box) Init() error { - return fmt.Errorf("NOT IMPLEMENTED: Init") +func (bx *Box) Init(yes, vcsname string) error { + fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) + if yes != "yes" { + fmt.Printf("Enable blackbox for this %v repo? (yes/no)", bx.Vcs.Name()) + input := bufio.NewScanner(os.Stdin) + input.Scan() + b, _ := strconv.ParseBool(input.Text()) + if !b { + fmt.Printf("As you wish. Exiting.") + return nil + } + } + + // TODO(tom): Handle per-user repo names + bbdir := filepath.Join(bx.RepoBaseDir, ".blackbox") + err := os.Mkdir(bbdir, 0o750) + if err != nil { + return err + } + + // touch blackbox-admins.txt + // touch blackbox-files.txt + + // Tell vcs to make a "don't mess with cr/lf" file in. + // bbdir + blackbox-admins.txt (return file to commit.. .gitignore) + // bbdir + blackbox-files.txt (return file to commit) + + // ignores := []string{ + // "pubring.gpg~", + // "pubring.kbx~", + // "secring.gpg", + // } + + // Tell vcs to suggest tracking: + // message: INITIALIZE BLACKBOX + // files: blackbox-admins.txt blackbox-files.txt + + return nil } // Reencrypt decrypts and reencrypts files. @@ -226,3 +265,20 @@ func (bx *Box) Status(names []string, nameOnly bool, match string) error { return nil } + +// TestingInitRepo initializes a repo. +func (bx *Box) TestingInitRepo() error { + if bx.Vcs == nil { + fmt.Println("bx.Vcs is nil") + fmt.Printf("BLACKBOX_FLAG_VCS=%q\n", os.Getenv("BLACKBOX_FLAG_VCS")) + os.Exit(1) + } + err := bx.Vcs.TestingInitRepo() + if err != nil { + return err + } + if !bx.Vcs.Discover("") { + return fmt.Errorf("TestingInitRepo failed Discovery") + } + return nil +} diff --git a/pkg/crypters/crypters.go b/pkg/crypters/crypters.go index 73e95f70..f9414f35 100644 --- a/pkg/crypters/crypters.go +++ b/pkg/crypters/crypters.go @@ -2,6 +2,7 @@ package crypters import ( "sort" + "strings" "github.com/StackExchange/blackbox/v2/models" ) @@ -24,6 +25,24 @@ type Item struct { // Catalog is the list of registered vcs's. var Catalog []*Item +// SearchByName returns a Crypter handle for name. +// The search is case insensitive. +func SearchByName(name string) Crypter { + name = strings.ToLower(name) + for _, v := range Catalog { + //fmt.Printf("Trying %v %v\n", v.Name) + if strings.ToLower(v.Name) == name { + chandle, err := v.New() + if err != nil { + return nil // No idea how that would happen. + } + //fmt.Printf("USING! %v\n", v.Name) + return chandle + } + } + return nil +} + // Register a new VCS. func Register(name string, priority int, newfn NewFnSig) { //fmt.Printf("CRYPTER registered: %v\n", name) diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 5fbb7976..78065677 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -9,8 +9,10 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/crypters" ) +var pluginName = "GnuPG" + func init() { - crypters.Register("GnuPG", 100, registerNew) + crypters.Register(pluginName, 100, registerNew) } // CrypterHandle is the handle @@ -35,6 +37,11 @@ func registerNew() (crypters.Crypter, error) { return crypt, nil } +// Name returns my name. +func (crypt CrypterHandle) Name() string { + return pluginName +} + // Decrypt decrypts a file, possibly overwriting the plaintext. func (crypt CrypterHandle) Decrypt(name string, overwrite bool, umask int) error { diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index b4d2a4dd..978a188d 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -7,8 +7,10 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/vcs" ) +var pluginName = "GIT" + func init() { - vcs.Register("GIT", 100, newGit) + vcs.Register(pluginName, 100, newGit) } // VcsHandle is the handle @@ -19,6 +21,11 @@ func newGit() (vcs.Vcs, error) { return &VcsHandle{}, nil } +// Name returns my name. +func (v VcsHandle) Name() string { + return pluginName +} + // Discover returns false. func (v VcsHandle) Discover(repobasedir string) bool { n := filepath.Join(repobasedir, ".git") @@ -28,3 +35,12 @@ func (v VcsHandle) Discover(repobasedir string) bool { } return found } + +// The following are "secret" functions only used by the integration testing system. + +// TestingInitRepo initializes a repo. + +func (v VcsHandle) TestingInitRepo() error { + bbutil.RunBash("git", "init") + return nil +} diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 5e35ef9f..12d0d17a 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -4,8 +4,10 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/vcs" ) +var pluginName = "GIT" + func init() { - vcs.Register("NONE", 0, newNone) + vcs.Register(pluginName, 0, newNone) } // VcsHandle is @@ -17,7 +19,19 @@ func newNone() (vcs.Vcs, error) { return &VcsHandle{}, nil } +// Name returns my name. +func (v VcsHandle) Name() string { + return pluginName +} + // Discover returns true func (v VcsHandle) Discover(repobasedir string) bool { return true } + +// The following are "secret" functions only used by the integration testing system. + +// TestingInitRepo initializes a repo. +func (v VcsHandle) TestingInitRepo() error { + return nil +} diff --git a/pkg/vcs/vcs.go b/pkg/vcs/vcs.go index 8cfa9ee3..0f884172 100644 --- a/pkg/vcs/vcs.go +++ b/pkg/vcs/vcs.go @@ -24,6 +24,22 @@ type Item struct { // Catalog is the list of registered vcs's. var Catalog []*Item +// DetermineVcs polls the VCS plug-ins to determine the VCS of directory. +// The first to succeed is returned. +// It never returns nil, since "NONE" is always valid. +func DetermineVcs(dir string) Vcs { + for _, v := range Catalog { + h, err := v.New() + if err != nil { + return nil // No idea how that would happen. + } + if h.Discover(dir) { + return h + } + } + return nil +} + // Register a new VCS. func Register(name string, priority int, newfn NewFnSig) { //fmt.Printf("VCS registered: %v\n", name) From 164376f8093218f70b9da4dcb52b70719d4a9c64 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 10 Jun 2020 09:31:00 -0400 Subject: [PATCH 32/69] Integration tests work all the way through "init". --- .gitignore | 2 + cmd/blackbox/cli.go | 17 +- cmd/blackbox/drive.go | 5 +- integrationTest/integration_test.go | 21 ++- integrationTest/ithelpers.go | 52 +++--- integrationTest/test_data/000-admin-list.txt | 2 + integrationTest/test_data/000-file-list.txt | 2 + pkg/bbutil/adminplain.go | 62 ------- pkg/bbutil/filestats.go | 61 ++++++- pkg/bbutil/iterator.go | 37 ---- pkg/bbutil/reg.go | 41 ----- pkg/bbutil/repoinfo.go | 112 ------------ pkg/bbutil/sortedfile_test.go | 63 +++++++ pkg/box/box.go | 172 ++++++++++--------- pkg/box/boxutils.go | 73 ++++++++ pkg/box/verbs.go | 20 ++- 16 files changed, 368 insertions(+), 374 deletions(-) create mode 100644 integrationTest/test_data/000-admin-list.txt create mode 100644 integrationTest/test_data/000-file-list.txt delete mode 100644 pkg/bbutil/adminplain.go delete mode 100644 pkg/bbutil/iterator.go delete mode 100644 pkg/bbutil/reg.go delete mode 100644 pkg/bbutil/repoinfo.go create mode 100644 pkg/bbutil/sortedfile_test.go diff --git a/.gitignore b/.gitignore index 2df62f6b..8e7c989b 100644 --- a/.gitignore +++ b/.gitignore @@ -51,6 +51,8 @@ coverage.xml # Sphinx documentation docs/_build/ +# macOS +.DS_Store # Blackbox bbintegration diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index ad3eebd9..f8bf17a5 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -21,19 +21,30 @@ func flags() *cli.App { &cli.StringFlag{ Name: "vcs", Usage: "Use this VCS (GIT, NONE) rather than autodetect", - EnvVars: []string{"BLACKBOX_FLAG_VCS"}, + EnvVars: []string{"BLACKBOX_VCS"}, }, &cli.StringFlag{ Name: "crypto", Usage: "Crypto back-end plugin", Value: "GnuPG", - EnvVars: []string{"BLACKBOX_FLAG_CRYPTO"}, + EnvVars: []string{"BLACKBOX_CRYPTO"}, + }, + &cli.StringFlag{ + Name: "config", + Usage: "Path to config", + Value: ".blackbox", + EnvVars: []string{"BLACKBOX_CONFIGDIR", "BLACKBOXDATA"}, + }, + &cli.StringFlag{ + Name: "team", + Usage: "Use .blackbox-$TEAM as the configdir", + EnvVars: []string{"BLACKBOX_TEAM"}, }, &cli.IntFlag{ Name: "umask", Usage: "umask to set when decrypting", Value: 0o027, - EnvVars: []string{"BLACKBOX_FLAG_UMASK", "DECRYPT_UMASK"}, + EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"}, }, } diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index 62bb3f41..dd2fed8f 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -149,7 +149,8 @@ func cmdInit(c *cli.Context) error { if c.Args().Len() > 1 { return fmt.Errorf("This command takes one or two arguments") } - bx := box.NewUninitialized() + bx := box.NewUninitialized(c.String("configdir"), c.String("team")) + fmt.Printf("cmdInit configdir=%q\n", bx.ConfigDir) return bx.Init(c.Args().First(), c.String("vcs")) } @@ -187,6 +188,6 @@ func testingInit(c *cli.Context) error { "c.String(vcs) reports %q\n", c.String("vcs"), ) - bx := box.NewBare(c.String("vcs")) + bx := box.NewForTestingInit(c.String("vcs")) return bx.TestingInitRepo() } diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7c0bbe11..b77f61fe 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -41,34 +41,41 @@ func compile(t *testing.T) { } func setup(t *testing.T) { - compile(t) fmt.Printf("flag.testvcs is %v\n", *vcsToTest) vh := getVcs(t, *vcsToTest) - fmt.Printf("Using BLACKBOX_FLAG_VCS=%v\n", vh.Name()) - os.Setenv("BLACKBOX_FLAG_VCS", vh.Name()) + fmt.Printf("Using BLACKBOX_VCS=%v\n", vh.Name()) + os.Setenv("BLACKBOX_VCS", vh.Name()) + op, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + originPath = op } func TestInitInvalidArgs(t *testing.T) { compile(t) + // Only zero or one args are permitted. invalidArgs(t, "init", "one", "two") invalidArgs(t, "init", "one", "two", "three") } func TestBasicCommands(t *testing.T) { + // These are basic tests that work on a fake repo. + // The repo has mostly real data, except any .gpg file + // is just garbage. + compile(t) setup(t) createDummyRepo(t, *vcsToTest) // admin - checkOutput(t, "file", "list", - "000-admin-list.txt", - ) + checkOutput(t, "admin", "list", "000-admin-list.txt") invalidArgs(t, "admin", "list", "--all") invalidArgs(t, "admin", "one") // file - checkOutput(t, "000-file-list.txt", "file", "list") + checkOutput(t, "file", "list", "000-file-list.txt") invalidArgs(t, "file", "list", "one") invalidArgs(t, "file", "list", "--all") diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 1a08c612..ac7e6581 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -6,15 +6,17 @@ import ( "os" "os/exec" "path/filepath" - "sort" "strings" "testing" + "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/vcs" _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" "github.com/andreyvit/diff" ) +var originPath string + func getVcs(t *testing.T, name string) vcs.Vcs { t.Helper() // Set up the vcs @@ -36,14 +38,26 @@ func getVcs(t *testing.T, name string) vcs.Vcs { // TestBasicCommands's helpers func createDummyRepo(t *testing.T, vcsname string) { + // This creates a repo with real data, except any .gpg file + // is just garbage. + t.Helper() fmt.Printf("createDummyRepo()\n") - dir, err := ioutil.TempDir("", "repo") + var dir string + var err error + if false { + dir, err = ioutil.TempDir("", "repo") + defer os.RemoveAll(dir) // clean up + } else { + dir = "/tmp/repo" + os.RemoveAll(filepath.Join(dir, ".")) + err = os.Mkdir(dir, 0o770) + } if err != nil { t.Fatalf("createDummyRepo: Could not make tempdir: %v", err) } - defer os.RemoveAll(dir) // clean up + fmt.Printf("TESTING DIRECTORY: cd %v\n", dir) os.Chdir(dir) @@ -59,19 +73,10 @@ func createDummyRepo(t *testing.T, vcsname string) { makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!") } -func addLineSorted(t *testing.T, name string, newlines ...string) { - t.Helper() - - contents, err := ioutil.ReadFile(name) - if err != nil { - t.Fatalf("addLinesSorted can't read %q: %v", name, err) - } - lines := strings.Split(string(contents), "\n") - lines = append(lines, newlines...) - sort.Strings(lines) - err = ioutil.WriteFile(name, []byte(strings.Join(lines, "\n")), 0o666) +func addLineSorted(t *testing.T, filename, line string) { + err := bbutil.AddLinesToSortedFile(filename, line) if err != nil { - t.Fatalf("addLinesSorted can't write %q: %v", name, err) + t.Fatalf("addLineSorted failed: %v", err) } } @@ -94,12 +99,7 @@ func checkOutput(t *testing.T, args ...string) { name := args[n] args = args[:n] - want, err := ioutil.ReadFile(filepath.Join("test_data", name)) - if err != nil { - t.Fatalf("checkOutput can't read %v: %v", name, err) - } - - cmd := exec.Command("blackbox", args...) + cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil cmd.Stdout = nil cmd.Stderr = os.Stderr @@ -108,6 +108,11 @@ func checkOutput(t *testing.T, args ...string) { t.Fatal(fmt.Errorf("checkOutput(%q): %w", args, err)) } + want, err := ioutil.ReadFile(filepath.Join(originPath, "test_data", name)) + if err != nil { + t.Fatalf("checkOutput can't read %v: %v", name, err) + } + if w, g := string(want), string(got); w != g { t.Errorf("checkOutput(%q) mismatch (-got +want):\n%s", args, diff.LineDiff(g, w)) @@ -150,7 +155,10 @@ var pathToBlackBox string func PathToBlackBox() string { return pathToBlackBox } // SetPathToBlackBox sets the path. -func SetPathToBlackBox(n string) { pathToBlackBox = n } +func SetPathToBlackBox(n string) { + fmt.Printf("PathToBlackBox=%q\n", n) + pathToBlackBox = n +} func runBB(t *testing.T, args ...string) { t.Helper() diff --git a/integrationTest/test_data/000-admin-list.txt b/integrationTest/test_data/000-admin-list.txt new file mode 100644 index 00000000..5dc1ca0e --- /dev/null +++ b/integrationTest/test_data/000-admin-list.txt @@ -0,0 +1,2 @@ +user1@example.com +user2@example.com diff --git a/integrationTest/test_data/000-file-list.txt b/integrationTest/test_data/000-file-list.txt new file mode 100644 index 00000000..ee1e7122 --- /dev/null +++ b/integrationTest/test_data/000-file-list.txt @@ -0,0 +1,2 @@ +bar.txt +foo.txt diff --git a/pkg/bbutil/adminplain.go b/pkg/bbutil/adminplain.go deleted file mode 100644 index e0895e49..00000000 --- a/pkg/bbutil/adminplain.go +++ /dev/null @@ -1,62 +0,0 @@ -package bbutil - -// -// import ( -// "io/ioutil" -// "log" -// "path/filepath" -// "sort" -// "strings" -// -// ) -// -// func plainAdminsFile(dir string) string { -// return filepath.Join(dir, "blackbox-admins.txt") -// } -// -// // Administrators returns the list of administrators. -// func plainListAdmins(dir string) ([]Administrator, error) { -// adminFilename := plainAdminsFile(dir) -// d, err := ioutil.ReadFile(adminFilename) -// if err != nil { -// return nil, errors.Wrap(err, "Could not read the list of administrators") -// } -// -// // remove a trailing \n. -// s := strings.TrimSuffix(string(d), "\n") // remove a single newline. -// names := strings.Split(s, "\n") -// if !sort.StringsAreSorted(names) { -// log.Fatalf("Admin list is corrupted. It is not sorted; %q", adminFilename) -// } -// r := make([]Administrator, len(names)) -// for i, name := range names { -// r[i].Name = name -// } -// -// return r, nil -// } -// -// // plainWriteAdmins rewrites the admins file. -// func plainWriteAdmins(dir string, admins []Administrator) error { -// return errors.New("UNIMPLEMENTED") -// } -// -// // plainAddAdmins adds one administrator by email address. -// func plainAddAdmin(dir string, admin Administrator) error { -// admins, err := plainListAdmins(dir) -// if err != nil { -// return err -// } -// -// // Add it to the list, sort it into position. -// admins = append(admins, admin) -// sort.Slice(admins, func(i, j int) bool { -// return admins[i].Name < admins[j].Name -// }) -// -// return plainWriteAdmins(dir, admins) -// } -// -// func plainRemoveAdmin(dir string, admin Administrator) error { -// return errors.New("UNIMPLEMENTED") -// } diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index da81ba9a..499b05f9 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -1,6 +1,13 @@ package bbutil -import "os" +import ( + "fmt" + "io/ioutil" + "os" + "sort" + "strings" + "time" +) // DirExists returns true if directory exists. func DirExists(path string) (bool, error) { @@ -25,3 +32,55 @@ func FileExistsOrProblem(path string) bool { } return true } + +// TouchFile updates the timestamp of a file. +func TouchFile(name string) error { + var err error + _, err = os.Stat(name) + if os.IsNotExist(err) { + file, err := os.Create(name) + if err != nil { + return fmt.Errorf("TouchFile failed: %w", err) + } + file.Close() + } + + currentTime := time.Now().Local() + return os.Chtimes(name, currentTime, currentTime) +} + +// ReadFileLines is like ioutil.ReadFile() but returns an []string. +func ReadFileLines(filename string) ([]string, error) { + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + s := string(b) + s = strings.TrimSuffix(s, "\n") + if s == "" { + return []string{}, nil + } + l := strings.Split(s, "\n") + return l, nil +} + +// AddLinesToSortedFile adds a line to a sorted file. +func AddLinesToSortedFile(filename string, newlines ...string) error { + lines, err := ReadFileLines(filename) + //fmt.Printf("DEBUG: read=%q\n", lines) + if err != nil { + return fmt.Errorf("AddLinesToSortedFile can't read %q: %w", filename, err) + } + if !sort.StringsAreSorted(lines) { + return fmt.Errorf("AddLinesToSortedFile: file wasn't sorted: %v", filename) + } + lines = append(lines, newlines...) + sort.Strings(lines) + contents := strings.Join(lines, "\n") + "\n" + //fmt.Printf("DEBUG: write=%q\n", contents) + err = ioutil.WriteFile(filename, []byte(contents), 0o666) + if err != nil { + return fmt.Errorf("AddLinesToSortedFile can't write %q: %w", filename, err) + } + return nil +} diff --git a/pkg/bbutil/iterator.go b/pkg/bbutil/iterator.go deleted file mode 100644 index 8d993b80..00000000 --- a/pkg/bbutil/iterator.go +++ /dev/null @@ -1,37 +0,0 @@ -package bbutil - -// import ( -// "sort" -// ) -// -// // FileIterator return a list of files to process. -// -// func (bbu *RepoInfo) FileIterator(allFiles bool, fnames []string) ([]string, []bool, error) { -// regfiles, err := bbu.RegisteredFiles() -// if err != nil { -// return nil, nil, err -// } -// -// allnames := make([]string, len(regfiles)) -// for i, r := range regfiles { -// allnames[i] = r.Name -// } -// -// if allFiles { -// isvalid := make([]bool, len(allnames)) -// for n := range allnames { -// isvalid[n] = true -// } -// return allnames, isvalid, nil -// } -// -// retnames := make([]string, len(fnames)) -// isvalid := make([]bool, len(fnames)) -// for n, fn := range fnames { -// retnames[n] = fn -// i := sort.SearchStrings(allnames, fn) -// isvalid[n] = i < len(allnames) && allnames[i] == fn -// } -// -// return retnames, isvalid, nil -// } diff --git a/pkg/bbutil/reg.go b/pkg/bbutil/reg.go deleted file mode 100644 index d5b88793..00000000 --- a/pkg/bbutil/reg.go +++ /dev/null @@ -1,41 +0,0 @@ -package bbutil - -// -// import ( -// "io/ioutil" -// "log" -// "os" -// "path/filepath" -// "sort" -// "strings" -// -// ) -// -// // RegFile is a description of a registered file. -// type RegFile struct { -// Name string -// } -// -// // RegisteredFiles returns a list of the registered files. -// func (bbu *RepoInfo) RegisteredFiles() ([]RegFile, error) { -// blackboxFiles := filepath.Join(bbu.BlackboxConfigDir, "blackbox-files.txt") -// d, err := ioutil.ReadFile(blackboxFiles) -// if err != nil { -// return nil, errors.Wrap(err, "Could not read the list of registered files") -// } -// -// // remove a trailing \n. -// // NB(tlim): We can't remove all trailing whitespace because filenames may contain whitespace. -// s := strings.TrimSuffix(string(d), "\n") // remove a single newline. -// -// names := strings.Split(s, "\n") -// if !sort.StringsAreSorted(names) { -// log.Fatalf("Files list is corrupted. It is not sorted; %q", blackboxFiles) -// } -// r := make([]RegFile, len(names)) -// for i, name := range names { -// r[i].Name = name -// } -// -// return r, nil -// } diff --git a/pkg/bbutil/repoinfo.go b/pkg/bbutil/repoinfo.go deleted file mode 100644 index a8eb1665..00000000 --- a/pkg/bbutil/repoinfo.go +++ /dev/null @@ -1,112 +0,0 @@ -package bbutil - -// import ( -// "os" -// "path/filepath" -// -// "github.com/StackExchange/blackbox/v2/pkg/bbgit" -// "github.com/StackExchange/blackbox/v2/pkg/bbnone" -// ) -// -// // Vcser is the interface that defines a plug-in VCS system. -// type Vcser interface { -// Name() string // Returns the name of this VCS type. -// RepoBaseDir() string // Returns the full path leading to this repo. -// } -// -// // RepoInfo stores info about the current repository. -// type RepoInfo struct { -// Vcs Vcser -// // BaseDir specifies the path (from "/") to the base of the VCS repo. -// RepoBaseDir string // REPOBASE -// BlackboxConfigDir string // BLACKBOXDATA -// // KEYRINGDIR="$REPOBASE/$BLACKBOXDATA" -// // BB_ADMINS_FILE="blackbox-admins.txt" -// // BB_ADMINS="${KEYRINGDIR}/${BB_ADMINS_FILE}" -// // SECRING="${KEYRINGDIR}/secring.gpg" -// } -// -// // New is a factory. -// func New() (*RepoInfo, error) { -// repo := &RepoInfo{} -// -// vcs, err := vcsType() -// if err != nil { -// return nil, err -// } -// repo.Vcs = vcs -// -// // What is the base directory of the repo? -// base := os.Getenv("BLACKBOX_REPOBASE") -// if base == "" { -// base = repo.Vcs.RepoBaseDir() -// } -// repo.RepoBaseDir = base -// -// // Where are the blackbox config files? -// repo.BlackboxConfigDir, err = findConfigDir(repo.RepoBaseDir) -// if err != nil { -// return nil, err -// } -// -// return repo, nil -// } -// -// // vcsType discovers the VCS type based on the BB_VCSTYPE env variable or by probing. -// func vcsType() (Vcser, error) { -// switch vcsName := os.Getenv("BB_VCSTYPE"); vcsName { -// case "git": -// return bbgit.New() -// // case "hg": -// // case "svn": -// default: -// break -// } -// answer, err := bbgit.New() -// if err == nil { -// return answer, nil -// } -// // TODO(tlim): Should we print err? -// // answer = bbhg.New() -// // if answer != nil { -// // return answer -// // } -// // answer = bbsvn.New() -// // if answer != nil { -// // return answer -// // } -// return bbnone.New() -// } -// -// // If BLACKBOXDATA is not set, search list this of directory paths. -// var configDirCandidates = []string{ -// "keyrings/live", -// ".blackbox", // Last item is the default. -// } -// -// // findConfigDir returns the configuration directory. It first checks the -// // BLACKBOXDATA env variable, then a list of candidates, lastly returning the -// // last candidate as the default. -// func findConfigDir(repoBase string) (string, error) { -// if dir := os.Getenv("BLACKBOXDATA"); dir != "" { -// //fmt.Fprintln(os.Stderr, "USING BBENV", dir) -// return filepath.Join(repoBase, dir), nil -// } -// var p string -// for _, c := range configDirCandidates { -// p = filepath.Join(repoBase, c) -// //fmt.Fprintf(os.Stderr, "Trying %q\n", p) -// if st, err := os.Stat(p); err == nil { -// mode := st.Mode() -// if mode.IsDir() { -// // FIXME(tlim): We are assuming that "not found" -// // and "i/o error" are both reasons to skip the candidates. -// // Maybe we should see what kind of error it is an output -// // some diagnostics if the problem is more than just "no found"? -// //fmt.Fprintf(os.Stderr, "RETURNING %q\n", p) -// return p, nil -// } -// } -// } -// return p, nil -// } diff --git a/pkg/bbutil/sortedfile_test.go b/pkg/bbutil/sortedfile_test.go new file mode 100644 index 00000000..53a6e83b --- /dev/null +++ b/pkg/bbutil/sortedfile_test.go @@ -0,0 +1,63 @@ +package bbutil + +import ( + "io/ioutil" + "os" + "testing" +) + +func TestAddLinesToSortedFile(t *testing.T) { + + var tests = []struct { + start string + add []string + expected string + }{ + { + "", + []string{"one"}, + "one\n", + }, + { + "begin\ntwo\n", + []string{"at top"}, + "at top\nbegin\ntwo\n", + }, + { + "begin\ntwo\n", + []string{"zbottom"}, + "begin\ntwo\nzbottom\n", + }, + { + "begin\ntwo\n", + []string{"middle"}, + "begin\nmiddle\ntwo\n", + }, + } + + for i, test := range tests { + content := []byte(test.start) + tmpfile, err := ioutil.TempFile("", "example") + if err != nil { + t.Fatal(err) + } + tmpfilename := tmpfile.Name() + defer os.Remove(tmpfilename) + + if _, err := tmpfile.Write(content); err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + t.Fatal(err) + } + AddLinesToSortedFile(tmpfilename, test.add...) + expected := test.expected + + got, err := ioutil.ReadFile(tmpfilename) + if expected != string(got) { + t.Errorf("test %v: contents wrong:\nexpected: %q\n got: %q", i, expected, got) + } + os.Remove(tmpfilename) + } + +} diff --git a/pkg/box/box.go b/pkg/box/box.go index c89757a2..a4fc4486 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -19,8 +19,9 @@ import ( // Box describes what we know about a box. type Box struct { // + Team string // Name of the team (i.e. .blackbox-$TEAM) RepoBaseDir string // Base directory of the repo. - ConfigDir string // Path to the .blackbox config directory. + ConfigDir string // Path of the .blackbox config directory (Rel to RepoBseDir) // Admins []string // If non-empty, the list of admins. Files []string // If non-empty, the list of files. @@ -53,15 +54,18 @@ func init() { } // NewUninitialized creates a box when nothing exists. -// Useful for the "init" subcommand. -func NewUninitialized() *Box { - return &Box{} +// Only for use with the "init" subcommand. +func NewUninitialized(configdir, team string) *Box { + bx := &Box{} + bx.Team = team + bx.ConfigDir = GenerateConfigDir(configdir, team) + return bx } -// NewBare creates a box in a bare environment, with no +// NewForTestingInit creates a box in a bare environment, with no // autodiscovery of VCS. // Useful only in integration tests. -func NewBare(vcsname string) *Box { +func NewForTestingInit(vcsname string) *Box { bx := &Box{} // Set up the vcs @@ -80,18 +84,44 @@ func NewBare(vcsname string) *Box { return bx } +/* + +test_init: + +* Generates .git in the current directory +* We don't know or use configdir, team, etc. + +init: +* Assumes the git repo was created already. +* Finds VCS (search up the tree for .git, .hg, none) +* If not found assume VCS=None, configdir=$pwd +* Set repobase based on where .git was found. +* If .blackbox/blackbox-team found, error. +* Create .blackbox or .blackbox-$team in configdir + +post-init commands: +* Assumes the git repo was created already. +* Assumes .blackbox or .blackbox-$team exists. +* Finds VCS & rebobase (search up the tree for .git, .hg, none) +* If VCS=None, search for repobase by looking for .blackbox. +* If .blackbox not found, error (needs init) + +*/ + // NewFromFlags creates a box using items from flags. func NewFromFlags(c *cli.Context) *Box { bx := &Box{} - repoBaseDir, configDir, err := findBaseAndConfigDir() - if err != nil { - logErr.Println(err) - return bx - } - bx.RepoBaseDir = repoBaseDir - bx.ConfigDir = configDir bx.Umask = c.Int("umask") + bx.Team = c.String("team") + + // repoBaseDir, configDir, err := findBaseAndConfigDir() + // if err != nil { + // logErr.Println(err) + // return bx + // } + // bx.RepoBaseDir = repoBaseDir + // bx.ConfigDir = configDir // Discover which kind of VCS is in use. bx.Vcs = vcs.DetermineVcs(bx.RepoBaseDir) @@ -103,67 +133,56 @@ func NewFromFlags(c *cli.Context) *Box { os.Exit(1) } - return bx -} - -func findBaseAndConfigDir() (repodir, configdir string, err error) { - - // If BLACKBOXDATA/BLACKBOX_CONFIGDIR is set, that is the config dir. - d := os.Getenv("BLACKBOXDATA") - c := os.Getenv("BLACKBOX_CONFIGDIR") - r := os.Getenv("BLACKBOX_REPOBASEDIR") - // If any of those are used, r must be set and one or both of d & c - // must be set. d is used before c. - if d != "" { - logErr.Printf("BLACKBOXDATA deprecated. Please use BLACKBOX_CONFIGDIR") - } - if (d != "") || (c != "") || (r != "") { - if (d != "") && (r != "") { - return r, d, nil - } - if (c != "") && (r != "") { - return r, c, nil - } - return c, r, fmt.Errorf("if BLACKBOX_REPOBASEDIR or BLACKBOX_REPOBASEDIR is used, BLACKBOX_REPOBASEDIR must be set") - } - - // Otherwise, search up the tree for the config dir. - - candidates := []string{} - if team := os.Getenv("BLACKBOX_TEAM"); team != "" { - candidates = append([]string{".blackbox-" + team}, candidates...) - } - candidates = append(candidates, ".blackbox") - candidates = append(candidates, "keyrings/live") - - // Prevent an infinite loop by only doing "cd .." this many times - maxDirLevels := 100 - - relpath := "" - for i := 0; i < maxDirLevels; i++ { - // Does relpath contain any of our directory names? - for _, c := range candidates { - t := filepath.Join(relpath, c) - d, err := bbutil.DirExists(t) - if err != nil { - return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) - } - if d { - return relpath, t, nil - } - } - // If we are at the root, stop. - if abs, _ := filepath.Abs(relpath); abs == "/" { - break - } - // Try one directory up - relpath = filepath.Join("..", relpath) + // Are we using .blackbox or what? + var err error + bx.ConfigDir, err = FindConfigDir(c.String("config"), c.String("team")) + if err != nil { + return nil } - return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") + return bx } +// func findBaseAndConfigDir() (repodir, configdir string, err error) { + +// // Otherwise, search up the tree for the config dir. + +// candidates := []string{} +// if team := os.Getenv("BLACKBOX_TEAM"); team != "" { +// candidates = append([]string{".blackbox-" + team}, candidates...) +// } +// candidates = append(candidates, ".blackbox") +// candidates = append(candidates, "keyrings/live") + +// // Prevent an infinite loop by only doing "cd .." this many times +// maxDirLevels := 100 + +// relpath := "" +// for i := 0; i < maxDirLevels; i++ { +// // Does relpath contain any of our directory names? +// for _, c := range candidates { +// t := filepath.Join(relpath, c) +// d, err := bbutil.DirExists(t) +// if err != nil { +// return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) +// } +// if d { +// return relpath, t, nil +// } +// } +// // If we are at the root, stop. +// if abs, _ := filepath.Abs(relpath); abs == "/" { +// break +// } +// // Try one directory up +// relpath = filepath.Join("..", relpath) +// } + +// return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") +// } + func (bx *Box) getAdmins() ([]string, error) { + // Memoized if len(bx.Admins) != 0 { return bx.Admins, nil } @@ -172,17 +191,12 @@ func (bx *Box) getAdmins() ([]string, error) { // Try the legacy file: fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") - b, err := ioutil.ReadFile(fn) - c := strings.TrimSpace(string(b)) - if err == nil { - bx.Admins = strings.Split(c, "\n") - return bx.Admins, nil - } - if !os.IsNotExist(err) { - return nil, fmt.Errorf("getAdmins can't open %q: %v", fn, err) + logErr.Printf("Admins file: %q", fn) + a, err := bbutil.ReadFileLines(fn) + if err != nil { + return nil, fmt.Errorf("getAdmins can't load admins (%q): %v", fn, err) } - - return nil, fmt.Errorf("getAdmins can't load admin list") + return a, nil } // getFiles populates Files and FileMap. diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 99a98196..e053d164 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -5,7 +5,10 @@ import ( "fmt" "os" "os/user" + "path/filepath" "strconv" + + "github.com/StackExchange/blackbox/v2/pkg/bbutil" ) // FileStatus returns the status of a file. @@ -116,6 +119,76 @@ func parseGroup(userinput string) (int, error) { return -1, err } +// GenerateConfigDir calculates configdir for uninitialized +// repos where discovery won't work. +// The result is an absolute path, unless Abs fails. +// If --config is set, use that value no matter what. +// Otherwise use .blackbox-$team or .blackbox (if no team). +func GenerateConfigDir(configdir, team string) string { + // if configdir is set, use it. + if configdir != "" { + if p, err := filepath.Abs(configdir); err == nil { + return p + } + return configdir + } + + var c string + if team == "" { + c = ".blackbox" + } else { + c = ".blackbox-" + team + } + if p, err := filepath.Abs(c); err == nil { + return p + } + return c +} + +// FindConfigDir tests various places until it finds the config dir. +func FindConfigDir(configdir, team string) (string, error) { + // if configdir is set, use it. + if configdir != "" { + if p, err := filepath.Abs(configdir); err == nil { + return p, nil + } + return configdir, nil + } + + // Otherwise, search up the tree for the config dir. + candidates := []string{} + if team != "" { + candidates = append([]string{".blackbox-" + team}, candidates...) + } else { + candidates = append(candidates, ".blackbox") + } + candidates = append(candidates, "keyrings/live") + // Prevent an infinite loop by only doing "cd .." this many times + maxDirLevels := 100 + relpath := "" + for i := 0; i < maxDirLevels; i++ { + // Does relpath contain any of our directory names? + for _, c := range candidates { + t := filepath.Join(relpath, c) + d, err := bbutil.DirExists(t) + if err != nil { + return "", fmt.Errorf("dirExists(%q) failed: %v", t, err) + } + if d { + return filepath.Abs(relpath) + } + } + // If we are at the root, stop. + if abs, _ := filepath.Abs(relpath); abs == "/" { + break + } + // Try one directory up + relpath = filepath.Join("..", relpath) + } + + return "", fmt.Errorf("No .blackbox directory found in cwd or above") +} + func gpgAgentNotice() { // Is gpg-agent configured? if os.Getenv("GPG_AGENT_INFO") != "" { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 2397450e..d08d0010 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -164,7 +164,11 @@ func (bx *Box) Info() error { // Init initializes a repo. func (bx *Box) Init(yes, vcsname string) error { - fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) + //fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) + + fmt.Printf("team is: %q\n", bx.Team) + fmt.Printf("configdir will be: %q\n", bx.ConfigDir) + if yes != "yes" { fmt.Printf("Enable blackbox for this %v repo? (yes/no)", bx.Vcs.Name()) input := bufio.NewScanner(os.Stdin) @@ -176,17 +180,17 @@ func (bx *Box) Init(yes, vcsname string) error { } } - // TODO(tom): Handle per-user repo names - bbdir := filepath.Join(bx.RepoBaseDir, ".blackbox") - err := os.Mkdir(bbdir, 0o750) + err := os.Mkdir(bx.ConfigDir, 0o750) if err != nil { return err } - // touch blackbox-admins.txt - // touch blackbox-files.txt + bbadmins := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") + bbutil.TouchFile(bbadmins) + bbfiles := filepath.Join(bx.ConfigDir, "blackbox-files.txt") + bbutil.TouchFile(bbfiles) - // Tell vcs to make a "don't mess with cr/lf" file in. + // FIXME(tlim) Tell vcs to make a "don't mess with cr/lf" file in. // bbdir + blackbox-admins.txt (return file to commit.. .gitignore) // bbdir + blackbox-files.txt (return file to commit) @@ -270,7 +274,7 @@ func (bx *Box) Status(names []string, nameOnly bool, match string) error { func (bx *Box) TestingInitRepo() error { if bx.Vcs == nil { fmt.Println("bx.Vcs is nil") - fmt.Printf("BLACKBOX_FLAG_VCS=%q\n", os.Getenv("BLACKBOX_FLAG_VCS")) + fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS")) os.Exit(1) } err := bx.Vcs.TestingInitRepo() From 2c8400cd18b081d7d066538b7985c6d13d71b36c Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Wed, 10 Jun 2020 22:04:13 -0400 Subject: [PATCH 33/69] init completed --- integrationTest/integration_test.go | 6 +- models/vcs.go | 10 +- pkg/bbgit/discover.go | 23 ----- pkg/bbutil/filestats.go | 17 +++- pkg/box/box.go | 146 +++++++++++++--------------- pkg/box/boxutils.go | 13 +-- pkg/box/verbs.go | 30 +++--- pkg/vcs/git/git.go | 42 ++++++++ pkg/vcs/none/none.go | 15 +++ pkg/vcs/vcs.go | 2 +- 10 files changed, 180 insertions(+), 124 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index b77f61fe..66bd20cf 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -53,12 +53,16 @@ func setup(t *testing.T) { originPath = op } -func TestInitInvalidArgs(t *testing.T) { +func TestInit(t *testing.T) { compile(t) // Only zero or one args are permitted. invalidArgs(t, "init", "one", "two") invalidArgs(t, "init", "one", "two", "three") + + runBB(t, "init", "yes") + // Verify blackbox-files.txt is empty, permissions (0o640) + // Verify blackbox-admins.txt is empty, permissions (0o640) } func TestBasicCommands(t *testing.T) { diff --git a/models/vcs.go b/models/vcs.go index 28711004..9d572906 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -6,6 +6,14 @@ type Vcs interface { Name() string // Discover returns true if the cwd is a VCS of this type. Discover(repobasedir string) bool - // Initialize a repo of this type (for use by integration tests) + + // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. + SetFileTypeUnix(repobasedir string, files ...string) error + // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. + IgnoreAnywhere(repobasedir string, files ...string) error + // SuggestTracking tells the VCS to suggest the user commit these files. + SuggestTracking(repobasedir string, message string, files ...string) error + + // TestingInitRepo initializes a repo of this type (for use by integration tests) TestingInitRepo() error } diff --git a/pkg/bbgit/discover.go b/pkg/bbgit/discover.go index 07880f39..fd3786f7 100644 --- a/pkg/bbgit/discover.go +++ b/pkg/bbgit/discover.go @@ -1,18 +1,5 @@ package bbgit -// -// import ( -// "os/exec" -// "strings" -// -// ) -// -// // GitInfo contains Git-specific info about this repository. -// type GitInfo struct { -// baseDir string -// } -// -// // New is a factory; returns error if this is not a Git repo. // func New() (*GitInfo, error) { // ri := new(GitInfo) // path, err := exec.LookPath("git") @@ -26,13 +13,3 @@ package bbgit // ri.baseDir = strings.TrimSuffix(string(baseDir), "\n") // remove a single newline. // return ri, nil // } -// -// // Name returns the name of this type of repo. -// func (repo *GitInfo) Name() string { -// return "git" -// } -// -// // RepoBaseDir returns -// func (repo *GitInfo) RepoBaseDir() string { -// return repo.baseDir -// } diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 499b05f9..4e8055b2 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -78,9 +78,24 @@ func AddLinesToSortedFile(filename string, newlines ...string) error { sort.Strings(lines) contents := strings.Join(lines, "\n") + "\n" //fmt.Printf("DEBUG: write=%q\n", contents) - err = ioutil.WriteFile(filename, []byte(contents), 0o666) + err = ioutil.WriteFile(filename, []byte(contents), 0o660) if err != nil { return fmt.Errorf("AddLinesToSortedFile can't write %q: %w", filename, err) } return nil } + +// AddLinesToFile adds lines to the end of a file. +func AddLinesToFile(filename string, newlines ...string) error { + lines, err := ReadFileLines(filename) + if err != nil { + return fmt.Errorf("AddLinesToFile can't read %q: %w", filename, err) + } + lines = append(lines, newlines...) + contents := strings.Join(lines, "\n") + "\n" + err = ioutil.WriteFile(filename, []byte(contents), 0o660) + if err != nil { + return fmt.Errorf("AddLinesToFile can't write %q: %w", filename, err) + } + return nil +} diff --git a/pkg/box/box.go b/pkg/box/box.go index a4fc4486..cb3253b1 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -18,19 +18,20 @@ import ( // Box describes what we know about a box. type Box struct { - // - Team string // Name of the team (i.e. .blackbox-$TEAM) - RepoBaseDir string // Base directory of the repo. - ConfigDir string // Path of the .blackbox config directory (Rel to RepoBseDir) - // + // Paths: + Team string // Name of the team (i.e. .blackbox-$TEAM) TODO(tlim): Can this be deleted? + RepoBaseDir string // Abs path to the VCS repo. + ConfigDir string // Abs path to the .blackbox (or whatever) directory. + ConfigDirRel string // Path to the .blackbox (or whatever) directory relative to RepoBaseDir + // Settings: + Umask int // umask to set when decrypting + // Cache of data gathered from .blackbox: Admins []string // If non-empty, the list of admins. Files []string // If non-empty, the list of files. FilesSet map[string]bool // If non-nil, a set of Files. - // + // Handles to interfaces: Vcs vcs.Vcs // Interface access to the VCS. Crypter crypters.Crypter // Inteface access to GPG. - // - Umask int // umask to set when decrypting } // StatusMode is a type of query. @@ -53,26 +54,78 @@ func init() { logErr = log.New(os.Stderr, "", 0) } -// NewUninitialized creates a box when nothing exists. -// Only for use with the "init" subcommand. +// NewFromFlags creates a box using items from flags. Nearly all subcommands use this. +func NewFromFlags(c *cli.Context) *Box { + /* + Nearly all subcommands use this. It is used with a VCS repo + that has blackbox already initialized. + + Commands need: How we populate it: + bx.Vcs: Discovered by calling each plug-in until succeeds. + bx.ConfigDir: Is discovered. + bx.RepoBaseDir: Is discovered. + */ + bx := &Box{ + Umask: c.Int("umask"), + Team: c.String("team"), + } + + // Discover which kind of VCS is in use. + bx.Vcs = vcs.Discover(bx.RepoBaseDir) + + // Pick a crypto backend (GnuPG, go-openpgp, etc.) + bx.Crypter = crypters.SearchByName(c.String("crypto")) + if bx.Crypter == nil { + fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the damn default\n") + os.Exit(1) + } + + // Are we using .blackbox or what? + var err error + bx.ConfigDir, err = FindConfigDir(c.String("config"), c.String("team")) + if err != nil { + return nil + } + + return bx +} + +// NewUninitialized creates a box in a pre-init situation. func NewUninitialized(configdir, team string) *Box { - bx := &Box{} - bx.Team = team + /* + This is for "blackbox init" (used before ".blackbox*" exists) + + Init needs: How we populate it: + bx.Vcs: Discovered by calling each plug-in until succeeds. + bx.ConfigDir: Generated algorithmically (it doesn't exist yet). + bx.RepoBaseDir: Generated algorithmically (it doesn't exist yet). + */ + bx := &Box{ + Team: team, + } + bx.Vcs = vcs.Discover(bx.RepoBaseDir) bx.ConfigDir = GenerateConfigDir(configdir, team) return bx } -// NewForTestingInit creates a box in a bare environment, with no -// autodiscovery of VCS. -// Useful only in integration tests. +// NewForTestingInit creates a box in a bare environment. func NewForTestingInit(vcsname string) *Box { + /* + + This is for "blackbox test_init" (secret command used in integration tests; when nothing exists) + + TestingInitRepo only uses bx.Vcs, so that's all we set. + + Populates bx.Vcs by finding the provider named vcsname. + */ bx := &Box{} - // Set up the vcs + // Find the var vh vcs.Vcs var err error + vcsname = strings.ToLower(vcsname) for _, v := range vcs.Catalog { - if strings.ToLower(v.Name) == strings.ToLower(vcsname) { + if strings.ToLower(v.Name) == vcsname { vh, err = v.New() if err != nil { return nil // No idea how that would happen. @@ -84,65 +137,6 @@ func NewForTestingInit(vcsname string) *Box { return bx } -/* - -test_init: - -* Generates .git in the current directory -* We don't know or use configdir, team, etc. - -init: -* Assumes the git repo was created already. -* Finds VCS (search up the tree for .git, .hg, none) -* If not found assume VCS=None, configdir=$pwd -* Set repobase based on where .git was found. -* If .blackbox/blackbox-team found, error. -* Create .blackbox or .blackbox-$team in configdir - -post-init commands: -* Assumes the git repo was created already. -* Assumes .blackbox or .blackbox-$team exists. -* Finds VCS & rebobase (search up the tree for .git, .hg, none) -* If VCS=None, search for repobase by looking for .blackbox. -* If .blackbox not found, error (needs init) - -*/ - -// NewFromFlags creates a box using items from flags. -func NewFromFlags(c *cli.Context) *Box { - bx := &Box{} - - bx.Umask = c.Int("umask") - bx.Team = c.String("team") - - // repoBaseDir, configDir, err := findBaseAndConfigDir() - // if err != nil { - // logErr.Println(err) - // return bx - // } - // bx.RepoBaseDir = repoBaseDir - // bx.ConfigDir = configDir - - // Discover which kind of VCS is in use. - bx.Vcs = vcs.DetermineVcs(bx.RepoBaseDir) - - // Pick a crypto backend (GnuPG, go-openpgp, etc.) - bx.Crypter = crypters.SearchByName(c.String("crypto")) - if bx.Crypter == nil { - fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the default\n") - os.Exit(1) - } - - // Are we using .blackbox or what? - var err error - bx.ConfigDir, err = FindConfigDir(c.String("config"), c.String("team")) - if err != nil { - return nil - } - - return bx -} - // func findBaseAndConfigDir() (repodir, configdir string, err error) { // // Otherwise, search up the tree for the config dir. diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index e053d164..99982d2a 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -120,12 +120,11 @@ func parseGroup(userinput string) (int, error) { } // GenerateConfigDir calculates configdir for uninitialized -// repos where discovery won't work. -// The result is an absolute path, unless Abs fails. -// If --config is set, use that value no matter what. +// repos (where discovery won't work). +// The result is an absolute path. +// If --config is set, use that value (no matter what). // Otherwise use .blackbox-$team or .blackbox (if no team). func GenerateConfigDir(configdir, team string) string { - // if configdir is set, use it. if configdir != "" { if p, err := filepath.Abs(configdir); err == nil { return p @@ -133,10 +132,8 @@ func GenerateConfigDir(configdir, team string) string { return configdir } - var c string - if team == "" { - c = ".blackbox" - } else { + c := ".blackbox" + if team != "" { c = ".blackbox-" + team } if p, err := filepath.Abs(c); err == nil { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index d08d0010..35f52daa 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -175,7 +175,7 @@ func (bx *Box) Init(yes, vcsname string) error { input.Scan() b, _ := strconv.ParseBool(input.Text()) if !b { - fmt.Printf("As you wish. Exiting.") + fmt.Println("Ok. Maybe some other time.") return nil } } @@ -187,22 +187,23 @@ func (bx *Box) Init(yes, vcsname string) error { bbadmins := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") bbutil.TouchFile(bbadmins) + bbadminsRel := filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt") + bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbadminsRel) + bbfiles := filepath.Join(bx.ConfigDir, "blackbox-files.txt") bbutil.TouchFile(bbfiles) + bbfilesRel := filepath.Join(bx.ConfigDirRel, "blackbox-files.txt") + bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbfilesRel) - // FIXME(tlim) Tell vcs to make a "don't mess with cr/lf" file in. - // bbdir + blackbox-admins.txt (return file to commit.. .gitignore) - // bbdir + blackbox-files.txt (return file to commit) - - // ignores := []string{ - // "pubring.gpg~", - // "pubring.kbx~", - // "secring.gpg", - // } + bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, + "pubring.gpg~", + "pubring.kbx~", + "secring.gpg", + ) - // Tell vcs to suggest tracking: - // message: INITIALIZE BLACKBOX - // files: blackbox-admins.txt blackbox-files.txt + bx.Vcs.SuggestTracking(bx.RepoBaseDir, "INITIALIZE BLACKBOX", + bbadminsRel, bbfilesRel, + ) return nil } @@ -271,7 +272,10 @@ func (bx *Box) Status(names []string, nameOnly bool, match string) error { } // TestingInitRepo initializes a repo. +// Uses bx.Vcs to create ".git" or whatever. +// Uses bx.Vcs to discover what was created, testing its work. func (bx *Box) TestingInitRepo() error { + if bx.Vcs == nil { fmt.Println("bx.Vcs is nil") fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS")) diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 978a188d..e07fa6f0 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -1,6 +1,7 @@ package git import ( + "fmt" "path/filepath" "github.com/StackExchange/blackbox/v2/pkg/bbutil" @@ -36,6 +37,47 @@ func (v VcsHandle) Discover(repobasedir string) bool { return found } +// SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. +func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { + // Add to the .gitattributes in the same directory as the file. + for _, file := range files { + d, n := filepath.Split(file) + err := bbutil.TouchFile(filepath.Join(repobasedir, d, ".gitattributes")) + if err != nil { + return err + } + err = bbutil.AddLinesToFile(filepath.Join(repobasedir, d, ".gitattributes"), + fmt.Sprintf("%q text eol=lf", n)) + if err != nil { + return err + } + } + return nil +} + +// IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. +func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { + // Add to the .gitignore file in the repobasedir. + ignore := filepath.Join(repobasedir, ".gitignore") + err := bbutil.TouchFile(ignore) + if err != nil { + return err + } + return bbutil.AddLinesToFile(ignore, files...) +} + +// SuggestTracking tells the VCS to suggest the user commit these files. +func (v VcsHandle) SuggestTracking(repobasedir string, message string, files ...string) error { + fmt.Print(` +NEXT STEP: You need to manually check these in: + git commit -m'INITIALIZE BLACKBOX'`) + for _, file := range files { + fmt.Print(fmt.Sprintf(" %q", file)) + } + fmt.Println() + return nil +} + // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 12d0d17a..26ea0d52 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -29,6 +29,21 @@ func (v VcsHandle) Discover(repobasedir string) bool { return true } +// SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. +func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { + return nil +} + +// IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. +func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { + return nil +} + +// SuggestTracking tells the VCS to suggest the user commit these files. +func (v VcsHandle) SuggestTracking(repobasedir string, message string, files ...string) error { + return nil +} + // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. diff --git a/pkg/vcs/vcs.go b/pkg/vcs/vcs.go index 0f884172..3b679902 100644 --- a/pkg/vcs/vcs.go +++ b/pkg/vcs/vcs.go @@ -27,7 +27,7 @@ var Catalog []*Item // DetermineVcs polls the VCS plug-ins to determine the VCS of directory. // The first to succeed is returned. // It never returns nil, since "NONE" is always valid. -func DetermineVcs(dir string) Vcs { +func Discover(dir string) Vcs { for _, v := range Catalog { h, err := v.New() if err != nil { From b544a02dd2e7a94d390c0ee9b96426571d702a18 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 12 Jun 2020 00:11:54 -0400 Subject: [PATCH 34/69] Mostly working --- cmd/blackbox/cli.go | 6 +- cmd/blackbox/drive.go | 8 +- integrationTest/asserts.go | 53 +++++ integrationTest/integration_test.go | 48 ++++- integrationTest/ithelpers.go | 258 +++++++++++++++++++++-- integrationTest/test_data/000-status.txt | 11 + models/crypters.go | 4 +- pkg/bbutil/filestats.go | 4 +- pkg/box/box.go | 9 +- pkg/box/boxutils.go | 4 + pkg/box/verbs.go | 74 +++++-- pkg/crypters/gnupg/gnupg.go | 41 +++- pkg/vcs/git/git.go | 8 +- 13 files changed, 463 insertions(+), 65 deletions(-) create mode 100644 integrationTest/asserts.go create mode 100644 integrationTest/test_data/000-status.txt diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index f8bf17a5..04791de4 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -3,6 +3,8 @@ package main // cli.go -- Create urfave/cli datastructures and apply them. import ( + "syscall" + "github.com/urfave/cli/v2" ) @@ -43,7 +45,7 @@ func flags() *cli.App { &cli.IntFlag{ Name: "umask", Usage: "umask to set when decrypting", - Value: 0o027, + Value: syscall.Umask(syscall.Umask(0)), EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"}, }, } @@ -70,7 +72,7 @@ func flags() *cli.App { Aliases: []string{"en", "end"}, Usage: "Encrypts file(s)", Flags: []cli.Flag{ - &cli.BoolFlag{Name: "leave", Usage: "Do not remove plaintext version"}, + &cli.BoolFlag{Name: "shred", Usage: "Remove plaintext afterwords"}, }, Action: func(c *cli.Context) error { return cmdEncrypt(c) }, }, diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index dd2fed8f..77af6cb6 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -105,12 +105,8 @@ func cmdEncrypt(c *cli.Context) error { if err := allOrSomeFiles(c); err != nil { return err } - bulk := false - if c.Bool("all") { - bulk = c.Bool("bulk") // Only applies to --all - } bx := box.NewFromFlags(c) - return bx.Encrypt(c.Args().Slice(), bulk, c.String("group"), c.Bool("overwrite")) + return bx.Encrypt(c.Args().Slice(), c.Int("umask"), c.Bool("shred")) } func cmdFileAdd(c *cli.Context) error { @@ -167,7 +163,7 @@ func cmdShred(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Shred(c.Args().Slice()) + return bx.Shred(c.Args().Slice()...) } func cmdStatus(c *cli.Context) error { diff --git a/integrationTest/asserts.go b/integrationTest/asserts.go new file mode 100644 index 00000000..5e58f7f5 --- /dev/null +++ b/integrationTest/asserts.go @@ -0,0 +1,53 @@ +package main + +import ( + "io/ioutil" + "os" + "testing" +) + +func assertFileMissing(t *testing.T, name string) { + t.Helper() + _, err := os.Stat(name) + if err != nil && os.IsNotExist(err) { + return + } + if err == nil { + t.Fatalf("assertFileMissing failed: %v exists", name) + } + t.Fatalf("assertFileMissing: %q: %v", name, err) +} + +func assertFileExists(t *testing.T, name string) { + t.Helper() + _, err := os.Stat(name) + if err == nil { + return + } + if os.IsNotExist(err) { + t.Fatalf("assertFileExists failed: %v not exist", name) + } + t.Fatalf("assertFileExists: file can't be accessed: %v: %v", name, err) +} + +func assertFileEmpty(t *testing.T, name string) { + t.Helper() + c, err := ioutil.ReadFile(name) + if err != nil { + t.Fatal(err) + } + if len(c) != 0 { + t.Fatalf("got=%v want=%v: %v", len(c), 0, name) + } +} + +func assertFilePerms(t *testing.T, name string, perms os.FileMode) { + t.Helper() + s, err := os.Stat(name) + if err != nil { + t.Fatal(err) + } + if s.Mode() != perms { + t.Fatalf("got=%#o want=%#o: %v", s.Mode(), perms, name) + } +} diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 66bd20cf..eb3b66c7 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -3,10 +3,20 @@ package main import ( "flag" "fmt" + "log" "os" "os/exec" "path/filepath" "testing" + + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "testing" + + _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" ) var verbose = flag.Bool("verbose", false, "reveal stderr") @@ -14,9 +24,23 @@ var vcsToTest = flag.String("testvcs", "GIT", "VCS to test") //var crypterToTest = flag.String("crypter", "GnuPG", "crypter to test") +var logErr *log.Logger +var logVerbose *log.Logger + func init() { testing.Init() flag.Parse() + + if logErr == nil { + logErr = log.New(os.Stderr, "", 0) + } + if logVerbose == nil { + if *verbose { + logVerbose = log.New(os.Stderr, "", 0) + } else { + logVerbose = log.New(nil, "", 0) + } + } } func compile(t *testing.T) { @@ -25,7 +49,7 @@ func compile(t *testing.T) { return } // Make sure we have the latest binary - fmt.Println("Compiling") + fmt.Println("========== Compiling") cmd := exec.Command("go", "build", "-o", "../bbintegration", "../cmd/blackbox") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr @@ -41,9 +65,9 @@ func compile(t *testing.T) { } func setup(t *testing.T) { - fmt.Printf("flag.testvcs is %v\n", *vcsToTest) + logVerbose.Printf("flag.testvcs is %v", *vcsToTest) vh := getVcs(t, *vcsToTest) - fmt.Printf("Using BLACKBOX_VCS=%v\n", vh.Name()) + logVerbose.Printf("Using BLACKBOX_VCS=%v", vh.Name()) os.Setenv("BLACKBOX_VCS", vh.Name()) op, err := os.Getwd() @@ -61,8 +85,10 @@ func TestInit(t *testing.T) { invalidArgs(t, "init", "one", "two", "three") runBB(t, "init", "yes") - // Verify blackbox-files.txt is empty, permissions (0o640) - // Verify blackbox-admins.txt is empty, permissions (0o640) + assertFileEmpty(t, ".blackbox/blackbox-admins.txt") + assertFileEmpty(t, ".blackbox/blackbox-files.txt") + assertFilePerms(t, ".blackbox/blackbox-admins.txt", 0o640) + assertFilePerms(t, ".blackbox/blackbox-files.txt", 0o640) } func TestBasicCommands(t *testing.T) { @@ -84,12 +110,20 @@ func TestBasicCommands(t *testing.T) { invalidArgs(t, "file", "list", "--all") // status + createDummyFiles(t) + checkOutput(t, "status", "000-status.txt") - // reencrypt + // encrypt + runBB(t, "encrypt", "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") // decrypt + runBB(t, "decrypt", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") - // encrypt + // reencrypt // edit invalidArgs(t, "edit") diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index ac7e6581..7f8fd91e 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -3,11 +3,13 @@ package main import ( "fmt" "io/ioutil" + "log" "os" "os/exec" "path/filepath" "strings" "testing" + "time" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/vcs" @@ -17,11 +19,27 @@ import ( var originPath string +var logErr *log.Logger +var logVerbose *log.Logger + +func init() { + if logErr == nil { + logErr = log.New(os.Stderr, "", 0) + } + if logVerbose == nil { + if *verbose { + logVerbose = log.New(os.Stderr, "", 0) + } else { + logVerbose = log.New(nil, "", 0) + } + } +} + func getVcs(t *testing.T, name string) vcs.Vcs { t.Helper() // Set up the vcs for _, v := range vcs.Catalog { - fmt.Printf("Testing vcs: %v == %v\n", name, v.Name) + logVerbose.Printf("Testing vcs: %v == %v", name, v.Name) if strings.ToLower(v.Name) == strings.ToLower(name) { h, err := v.New() if err != nil { @@ -29,7 +47,7 @@ func getVcs(t *testing.T, name string) vcs.Vcs { } return h } - fmt.Print("Nope.\n") + logVerbose.Println("...Nope.") } return nil @@ -42,7 +60,7 @@ func createDummyRepo(t *testing.T, vcsname string) { // is just garbage. t.Helper() - fmt.Printf("createDummyRepo()\n") + logVerbose.Printf("createDummyRepo()\n") var dir string var err error @@ -57,7 +75,7 @@ func createDummyRepo(t *testing.T, vcsname string) { if err != nil { t.Fatalf("createDummyRepo: Could not make tempdir: %v", err) } - fmt.Printf("TESTING DIRECTORY: cd %v\n", dir) + logVerbose.Printf("TESTING DIRECTORY: cd %v\n", dir) os.Chdir(dir) @@ -73,6 +91,60 @@ func createDummyRepo(t *testing.T, vcsname string) { makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!") } +func createDummyFiles(t *testing.T) { + // This creates a few files with real plaintext but fake cyphertext. + // There are a variety of timestamps to enable many statuses. + t.Helper() + + // DECRYPTED: File is decrypted and ready to edit (unknown if it has been edited). + // ENCRYPTED: GPG file is newer than plaintext. Indicates recented edited then encrypted. + // SHREDDED: Plaintext is missing. + // GPGMISSING: The .gpg file is missing. Oops? + // PLAINERROR: Can't access the plaintext file to determine status. + // GPGERROR: Can't access .gpg file to determine status. + + addLineSorted(t, ".blackbox/blackbox-files.txt", "status-DECRYPTED.txt") + addLineSorted(t, ".blackbox/blackbox-files.txt", "status-ENCRYPTED.txt") + addLineSorted(t, ".blackbox/blackbox-files.txt", "status-SHREDDED.txt") + addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGMISSING.txt") + // addLineSorted(t, ".blackbox/blackbox-files.txt", "status-PLAINERROR.txt") + // addLineSorted(t, ".blackbox/blackbox-files.txt", "status-GPGERROR.txt") + addLineSorted(t, ".blackbox/blackbox-files.txt", "status-BOTHMISSING.txt") + + // Combination of age difference either missing, file error, both missing. + makeFile(t, "status-DECRYPTED.txt", "File with DECRYPTED in it.") + makeFile(t, "status-DECRYPTED.txt.gpg", "Svyr jvgu QRPELCGRQ va vg.") + + makeFile(t, "status-ENCRYPTED.txt", "File with ENCRYPTED in it.") + makeFile(t, "status-ENCRYPTED.txt.gpg", "Svyr jvgu RAPELCGRQ va vg.") + + // Plaintext intentionally missing. + makeFile(t, "status-SHREDDED.txt.gpg", "Svyr jvgu FUERQQRQ va vg.") + + makeFile(t, "status-GPGMISSING.txt", "File with GPGMISSING in it.") + // gpg file intentionally missing. + + // makeFile(t, "status-PLAINERROR.txt", "File with PLAINERROR in it.") + // makeFile(t, "status-PLAINERROR.txt.gpg", "Svyr jvgu CYNVAREEBE va vg.") + // setFilePerms(t, "status-PLAINERROR.txt", 0o000) + + // makeFile(t, "status-GPGERROR.txt", "File with GPGERROR in it.") + // makeFile(t, "status-GPGERROR.txt.gpg", "Svyr jvgu TCTREEBE va vg.") + // setFilePerms(t, "status-GPGERROR.txt.gpg", 0o000) + + // Plaintext intentionally missing. ("status-BOTHMISSING.txt") + // gpg file intentionally missing. ("status-BOTHMISSING.txt.gpg") + + time.Sleep(200 * time.Millisecond) + + if err := bbutil.Touch("status-DECRYPTED.txt"); err != nil { + t.Fatal(err) + } + if err := bbutil.Touch("status-ENCRYPTED.txt.gpg"); err != nil { + t.Fatal(err) + } +} + func addLineSorted(t *testing.T, filename, line string) { err := bbutil.AddLinesToSortedFile(filename, line) if err != nil { @@ -89,6 +161,15 @@ func makeFile(t *testing.T, name string, lines ...string) { } } +func setFilePerms(t *testing.T, name string, perms int) { + t.Helper() + + err := os.Chmod(name, os.FileMode(perms)) + if err != nil { + t.Fatalf("setFilePerms can't chmod %q: %v", name, err) + } +} + // checkOutput runs blackbox with args, the last arg is the filename // of the expected output. Error if output is not expected. func checkOutput(t *testing.T, args ...string) { @@ -123,7 +204,7 @@ func checkOutput(t *testing.T, args ...string) { func invalidArgs(t *testing.T, args ...string) { t.Helper() - fmt.Printf("invalidArgs(%q): \n", args) + logVerbose.Printf("invalidArgs(%q): \n", args) cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil if *verbose { @@ -132,21 +213,17 @@ func invalidArgs(t *testing.T, args ...string) { } err := cmd.Run() if err == nil { - fmt.Println("BAD") + logVerbose.Println("BAD") t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args)) } - if *verbose { - fmt.Printf("GOOD (expected): err=%q\n", err) - } else { - fmt.Println("GOOD (expected)") - } + logVerbose.Printf("GOOD (that's expected): err=%q\n", err) } // TestAliceAndBob's helpers. func setupUser(t *testing.T, user, passphrase string) { t.Helper() - fmt.Printf("DEBUG: setupUser %q %q\n", user, passphrase) + logVerbose.Printf("DEBUG: setupUser %q %q\n", user, passphrase) } var pathToBlackBox string @@ -156,14 +233,14 @@ func PathToBlackBox() string { return pathToBlackBox } // SetPathToBlackBox sets the path. func SetPathToBlackBox(n string) { - fmt.Printf("PathToBlackBox=%q\n", n) + logVerbose.Printf("PathToBlackBox=%q\n", n) pathToBlackBox = n } func runBB(t *testing.T, args ...string) { t.Helper() - fmt.Printf("runBB(%q)\n", args) + logVerbose.Printf("runBB(%q)\n", args) cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil cmd.Stdout = os.Stdout @@ -173,3 +250,156 @@ func runBB(t *testing.T, args ...string) { t.Fatal(fmt.Errorf("runBB(%q): %w", args, err)) } } + +// # NB: This is copied from _blackbox_common.sh +// function get_pubring_path() { +// : "${KEYRINGDIR:=keyrings/live}" ; +// if [[ -f "${KEYRINGDIR}/pubring.gpg" ]]; then +// echo "${KEYRINGDIR}/pubring.gpg" +// else +// echo "${KEYRINGDIR}/pubring.kbx" +// fi +// } + +func phase(msg string) { + logVerbose.Println("********************") + logVerbose.Println("********************") + logVerbose.Printf("********* %v\n", msg) + logVerbose.Println("********************") + logVerbose.Println("********************") +} + +// function md5sum_file() { +// # Portably generate the MD5 hash of file $1. +// case $(uname -s) in +// Darwin | FreeBSD ) +// md5 -r "$1" | awk '{ print $1 }' +// ;; +// NetBSD ) +// md5 -q "$1" +// ;; +// SunOS ) +// digest -a md5 "$1" +// ;; +// Linux ) +// md5sum "$1" | awk '{ print $1 }' +// ;; +// CYGWIN* ) +// md5sum "$1" | awk '{ print $1 }' +// ;; +// * ) +// echo 'ERROR: Unknown OS. Exiting.' +// exit 1 +// ;; +// esac +// } +// +// function assert_file_missing() { +// if [[ -e "$1" ]]; then +// echo "ASSERT FAILED: ${1} should not exist." +// exit 1 +// fi +// } +// +// function assert_file_exists() { +// if [[ ! -e "$1" ]]; then +// echo "ASSERT FAILED: ${1} should exist." +// echo "PWD=$(/usr/bin/env pwd -P)" +// #echo "LS START" +// #ls -la +// #echo "LS END" +// exit 1 +// fi +// } +// function assert_file_md5hash() { +// local file="$1" +// local wanted="$2" +// assert_file_exists "$file" +// local found +// found=$(md5sum_file "$file") +// if [[ "$wanted" != "$found" ]]; then +// echo "ASSERT FAILED: $file hash wanted=$wanted found=$found" +// exit 1 +// fi +// } +// function assert_file_group() { +// local file="$1" +// local wanted="$2" +// local found +// assert_file_exists "$file" +// +// case $(uname -s) in +// Darwin | FreeBSD | NetBSD ) +// found=$(stat -f '%Dg' "$file") +// ;; +// Linux | SunOS ) +// found=$(stat -c '%g' "$file") +// ;; +// CYGWIN* ) +// echo "ASSERT_FILE_GROUP: Running on Cygwin. Not being tested." +// return 0 +// ;; +// * ) +// echo 'ERROR: Unknown OS. Exiting.' +// exit 1 +// ;; +// esac +// +// echo "DEBUG: assert_file_group X${wanted}X vs. X${found}X" +// echo "DEBUG:" $(which stat) +// if [[ "$wanted" != "$found" ]]; then +// echo "ASSERT FAILED: $file chgrp group wanted=$wanted found=$found" +// exit 1 +// fi +// } +// function assert_file_perm() { +// local wanted="$1" +// local file="$2" +// local found +// assert_file_exists "$file" +// +// case $(uname -s) in +// Darwin | FreeBSD | NetBSD ) +// found=$(stat -f '%Sp' "$file") +// ;; +// # NB(tlim): CYGWIN hasn't been tested. It might be more like Darwin. +// Linux | CYGWIN* | SunOS ) +// found=$(stat -c '%A' "$file") +// ;; +// * ) +// echo 'ERROR: Unknown OS. Exiting.' +// exit 1 +// ;; +// esac +// +// echo "DEBUG: assert_file_perm X${wanted}X vs. X${found}X" +// echo "DEBUG:" $(which stat) +// if [[ "$wanted" != "$found" ]]; then +// echo "ASSERT FAILED: $file chgrp perm wanted=$wanted found=$found" +// exit 1 +// fi +// } +// function assert_line_not_exists() { +// local target="$1" +// local file="$2" +// assert_file_exists "$file" +// if grep -F -x -s -q >/dev/null "$target" "$file" ; then +// echo "ASSERT FAILED: line '$target' should not exist in file $file" +// echo "==== file contents: START $file" +// cat "$file" +// echo "==== file contents: END $file" +// exit 1 +// fi +// } +// function assert_line_exists() { +// local target="$1" +// local file="$2" +// assert_file_exists "$file" +// if ! grep -F -x -s -q >/dev/null "$target" "$file" ; then +// echo "ASSERT FAILED: line '$target' should exist in file $file" +// echo "==== file contents: START $file" +// cat "$file" +// echo "==== file contents: END $file" +// exit 1 +// fi +// } diff --git a/integrationTest/test_data/000-status.txt b/integrationTest/test_data/000-status.txt new file mode 100644 index 00000000..2e967b64 --- /dev/null +++ b/integrationTest/test_data/000-status.txt @@ -0,0 +1,11 @@ ++-------------+------------------------+ +| STATUS | NAME | ++-------------+------------------------+ +| ENCRYPTED | bar.txt | +| ENCRYPTED | foo.txt | +| BOTHMISSING | status-BOTHMISSING.txt | +| DECRYPTED | status-DECRYPTED.txt | +| ENCRYPTED | status-ENCRYPTED.txt | +| GPGMISSING | status-GPGMISSING.txt | +| SHREDDED | status-SHREDDED.txt | ++-------------+------------------------+ diff --git a/models/crypters.go b/models/crypters.go index e8e93e71..0831eae0 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -5,5 +5,7 @@ type Crypter interface { // Name returns the plug-in's canonical name. Name() string // Decrypt name+".gpg", possibly overwriting name. - Decrypt(filename string, overwrite bool, umask int) error + Decrypt(filename string, umask int, overwrite bool) error + // Encrypt name, overwriting name+".gpg" + Encrypt(filename string, umask int, receivers []string) error } diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 4e8055b2..0247dd44 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -33,8 +33,8 @@ func FileExistsOrProblem(path string) bool { return true } -// TouchFile updates the timestamp of a file. -func TouchFile(name string) error { +// Touch updates the timestamp of a file. +func Touch(name string) error { var err error _, err = os.Stat(name) if os.IsNotExist(err) { diff --git a/pkg/box/box.go b/pkg/box/box.go index cb3253b1..f0106e2b 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -175,10 +175,10 @@ func NewForTestingInit(vcsname string) *Box { // return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") // } -func (bx *Box) getAdmins() ([]string, error) { +func (bx *Box) getAdmins() error { // Memoized if len(bx.Admins) != 0 { - return bx.Admins, nil + return nil } // TODO(tlim): Try the json file. @@ -188,9 +188,10 @@ func (bx *Box) getAdmins() ([]string, error) { logErr.Printf("Admins file: %q", fn) a, err := bbutil.ReadFileLines(fn) if err != nil { - return nil, fmt.Errorf("getAdmins can't load admins (%q): %v", fn, err) + return fmt.Errorf("getAdmins can't load admins (%q): %v", fn, err) } - return a, nil + bx.Admins = a + return nil } // getFiles populates Files and FileMap. diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 99982d2a..60abd90b 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -33,6 +33,10 @@ func FileStatus(name string) (string, error) { return "DECRYPTED", nil } + if os.IsNotExist(perr) && os.IsNotExist(eerr) { + return "BOTHMISSING", nil + } + if eerr != nil { if os.IsNotExist(eerr) { return "GPGMISSING", nil diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 35f52daa..d5b59941 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -21,13 +21,12 @@ func (bx *Box) AdminAdd([]string) error { // AdminList lists the admin id's. func (bx *Box) AdminList() error { - - admins, err := bx.getAdmins() + err := bx.getAdmins() if err != nil { return err } - for _, v := range admins { + for _, v := range bx.Admins { fmt.Println(v) } return nil @@ -52,6 +51,10 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup return err } + if bulkpause { + gpgAgentNotice() + } + groupchange := false gid := -1 if setgroup != "" { @@ -61,10 +64,6 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup } } - if bulkpause { - gpgAgentNotice() - } - if len(names) == 0 { names = bx.Files } @@ -82,7 +81,11 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup // if overwrite is disabled. I don't think anyone has ever used that // feature. That said, we could immplement that here. - err := bx.Crypter.Decrypt(name, overwrite, bx.Umask) + // TODO(tlim) v1 takes the md5 has of the plaintext before it decrypts, + // then compares the new plaintext's md5. It prints "EXTRACTED" if + // there is a change. + + err := bx.Crypter.Decrypt(name, bx.Umask, overwrite) if err != nil { logErr.Printf("%q: %v", name, err) continue @@ -107,8 +110,45 @@ func (bx *Box) Edit([]string) error { } // Encrypt encrypts a file. -func (bx *Box) Encrypt(names []string, bulk bool, setgroup string, overwrite bool) error { - return fmt.Errorf("NOT IMPLEMENTED: Encrypt") +func (bx *Box) Encrypt(names []string, umask int, shred bool) error { + var err error + + err = bx.getAdmins() + if err != nil { + return err + } + + err = bx.getFiles() + if err != nil { + return err + } + if len(names) == 0 { + names = bx.Files + } + + var suggest []string + for _, name := range names { + fmt.Printf("========== ENCRYPTING %q\n", name) + if !bx.FilesSet[name] { + logErr.Printf("Skipping %q: File not registered with Blackbox", name) + } + err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) + if err != nil { + logErr.Printf("%q: %v", name, err) + continue + } + suggest = append(suggest, fmt.Sprintf("Updated: %q", name)) + if shred { + bx.Shred(name) + } + } + + bx.Vcs.SuggestTracking(bx.RepoBaseDir, + strings.Join(names, "\n")+"\n", + names..., + ) + + return nil } // FileAdd enrolls files. @@ -136,14 +176,14 @@ func (bx *Box) FileRemove(names []string) error { // Info prints debugging info. func (bx *Box) Info() error { - _, err := bx.getAdmins() + err := bx.getFiles() if err != nil { - logErr.Printf("getAdmins error: %v", err) + logErr.Printf("Info: %v", err) } - err = bx.getFiles() + err = bx.getAdmins() if err != nil { - logErr.Printf("getFiles error: %v", err) + logErr.Printf("Info: %v", err) } //fmt.Printf("bx.Admins=%q\n", bx.Admins) @@ -186,12 +226,12 @@ func (bx *Box) Init(yes, vcsname string) error { } bbadmins := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") - bbutil.TouchFile(bbadmins) + bbutil.Touch(bbadmins) bbadminsRel := filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt") bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbadminsRel) bbfiles := filepath.Join(bx.ConfigDir, "blackbox-files.txt") - bbutil.TouchFile(bbfiles) + bbutil.Touch(bbfiles) bbfilesRel := filepath.Join(bx.ConfigDirRel, "blackbox-files.txt") bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbfilesRel) @@ -214,7 +254,7 @@ func (bx *Box) Reencrypt(names []string) error { } // Shred shreds files. -func (bx *Box) Shred(names []string) error { +func (bx *Box) Shred(names ...string) error { return fmt.Errorf("NOT IMPLEMENTED: Shred") } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 78065677..c06d4fb4 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -1,7 +1,6 @@ package gnupg import ( - "os" "os/exec" "syscall" @@ -42,21 +41,43 @@ func (crypt CrypterHandle) Name() string { return pluginName } -// Decrypt decrypts a file, possibly overwriting the plaintext. -func (crypt CrypterHandle) Decrypt(name string, overwrite bool, umask int) error { +// Decrypt name+".gpg", possibly overwriting name. +func (crypt CrypterHandle) Decrypt(filename string, umask int, overwrite bool) error { + a := []string{ + "--use-agent", + "-q", + "--decrypt", + "-o", filename, + } if overwrite { - _ = os.Remove(name) + a = append(a, "--yes") } + a = append(a, filename+".gpg") oldumask := syscall.Umask(umask) - err := bbutil.RunBash(crypt.GPGCmd, + err := bbutil.RunBash(crypt.GPGCmd, a...) + syscall.Umask(oldumask) + return err +} + +// Encrypt name, overwriting name+".gpg" +func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []string) error { + a := []string{ "--use-agent", - "-q", - "--decrypt", - "-o", name, - name+".gpg", - ) + "--yes", + "--trust-model=always", + "--encrypt", + "-o", filename + ".gpg", + } + for _, f := range receivers { + a = append(a, "-r", f) + } + a = append(a, filename) + + oldumask := syscall.Umask(umask) + err := bbutil.RunBash(crypt.GPGCmd, a...) syscall.Umask(oldumask) + return err } diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index e07fa6f0..c66337af 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -42,7 +42,7 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { // Add to the .gitattributes in the same directory as the file. for _, file := range files { d, n := filepath.Split(file) - err := bbutil.TouchFile(filepath.Join(repobasedir, d, ".gitattributes")) + err := bbutil.Touch(filepath.Join(repobasedir, d, ".gitattributes")) if err != nil { return err } @@ -59,7 +59,7 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { // Add to the .gitignore file in the repobasedir. ignore := filepath.Join(repobasedir, ".gitignore") - err := bbutil.TouchFile(ignore) + err := bbutil.Touch(ignore) if err != nil { return err } @@ -78,6 +78,10 @@ NEXT STEP: You need to manually check these in: return nil } +//echo "========== Encrypting: $unencrypted" >&2 +//$GPG --use-agent --yes --trust-model=always --encrypt -o "$encrypted" $(awk '{ print "-r" $1 }' < "$BB_ADMINS") "$unencrypted" >&2 +//echo '========== Encrypting: DONE' >&2 + // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. From e831a5a4c6f3f75c5621f645e7b4480a6ac38337 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 12 Jun 2020 06:49:53 -0400 Subject: [PATCH 35/69] logging works --- cmd/blackbox/cli.go | 4 +++ cmd/blackbox/drive.go | 6 ++-- integrationTest/integration_test.go | 31 ++++--------------- integrationTest/ithelpers.go | 24 ++++++++------- pkg/bblog/bblog.go | 46 +++++++++++++++++++++++++++++ pkg/box/box.go | 21 +++++++------ pkg/box/verbs.go | 18 +++++------ 7 files changed, 92 insertions(+), 58 deletions(-) create mode 100644 pkg/bblog/bblog.go diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index 04791de4..2c21d42a 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -48,6 +48,10 @@ func flags() *cli.App { Value: syscall.Umask(syscall.Umask(0)), EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"}, }, + &cli.BoolFlag{ + Name: "debug", + Usage: "Show debug output", + }, } app.Commands = []*cli.Command{ diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index 77af6cb6..aedd3372 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -8,6 +8,7 @@ import ( "log" "os" + "github.com/StackExchange/blackbox/v2/pkg/bblog" "github.com/StackExchange/blackbox/v2/pkg/box" "github.com/urfave/cli/v2" ) @@ -146,7 +147,6 @@ func cmdInit(c *cli.Context) error { return fmt.Errorf("This command takes one or two arguments") } bx := box.NewUninitialized(c.String("configdir"), c.String("team")) - fmt.Printf("cmdInit configdir=%q\n", bx.ConfigDir) return bx.Init(c.Args().First(), c.String("vcs")) } @@ -180,7 +180,9 @@ func testingInit(c *cli.Context) error { if c.Args().Present() { return fmt.Errorf("No args required") } - fmt.Printf( + + logDebug := bblog.GetDebug(c.Bool("debug")) + logDebug.Printf( "c.String(vcs) reports %q\n", c.String("vcs"), ) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index eb3b66c7..8f696b27 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -3,44 +3,23 @@ package main import ( "flag" "fmt" - "log" - "os" - "os/exec" - "path/filepath" - "testing" - - "fmt" - "log" "os" "os/exec" "path/filepath" "testing" + "github.com/StackExchange/blackbox/v2/pkg/bblog" + _ "github.com/StackExchange/blackbox/v2/pkg/bblog" _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" ) -var verbose = flag.Bool("verbose", false, "reveal stderr") var vcsToTest = flag.String("testvcs", "GIT", "VCS to test") //var crypterToTest = flag.String("crypter", "GnuPG", "crypter to test") -var logErr *log.Logger -var logVerbose *log.Logger - func init() { testing.Init() flag.Parse() - - if logErr == nil { - logErr = log.New(os.Stderr, "", 0) - } - if logVerbose == nil { - if *verbose { - logVerbose = log.New(os.Stderr, "", 0) - } else { - logVerbose = log.New(nil, "", 0) - } - } } func compile(t *testing.T) { @@ -65,9 +44,11 @@ func compile(t *testing.T) { } func setup(t *testing.T) { - logVerbose.Printf("flag.testvcs is %v", *vcsToTest) + logDebug := bblog.GetDebug(*verbose) + + logDebug.Printf("flag.testvcs is %v", *vcsToTest) vh := getVcs(t, *vcsToTest) - logVerbose.Printf("Using BLACKBOX_VCS=%v", vh.Name()) + logDebug.Printf("Using BLACKBOX_VCS=%v", vh.Name()) os.Setenv("BLACKBOX_VCS", vh.Name()) op, err := os.Getwd() diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 7f8fd91e..d1a274d4 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -1,6 +1,7 @@ package main import ( + "flag" "fmt" "io/ioutil" "log" @@ -11,28 +12,29 @@ import ( "testing" "time" + "github.com/StackExchange/blackbox/v2/pkg/bblog" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/vcs" _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" + "github.com/andreyvit/diff" ) +var verbose = flag.Bool("verbose", false, "reveal stderr") + var originPath string +func init() { + testing.Init() + flag.Parse() +} + var logErr *log.Logger var logVerbose *log.Logger func init() { - if logErr == nil { - logErr = log.New(os.Stderr, "", 0) - } - if logVerbose == nil { - if *verbose { - logVerbose = log.New(os.Stderr, "", 0) - } else { - logVerbose = log.New(nil, "", 0) - } - } + logErr = bblog.GetErr() + logVerbose = bblog.GetDebug(*verbose) } func getVcs(t *testing.T, name string) vcs.Vcs { @@ -216,7 +218,7 @@ func invalidArgs(t *testing.T, args ...string) { logVerbose.Println("BAD") t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args)) } - logVerbose.Printf("GOOD (that's expected): err=%q\n", err) + logVerbose.Printf("^^^^ (correct error received): err=%q\n", err) } // TestAliceAndBob's helpers. diff --git a/pkg/bblog/bblog.go b/pkg/bblog/bblog.go new file mode 100644 index 00000000..b2dcb950 --- /dev/null +++ b/pkg/bblog/bblog.go @@ -0,0 +1,46 @@ +package bblog + +import ( + "io/ioutil" + "log" + "os" +) + +/* + +To use this, include the following lines in your .go file. + +var logErr *log.Logger +var logDebug *log.Logger +func init() { + logErr = bblog.GetLogErr() + logDebug = bblog.GetLogDebug(verbose) +} + +Or in a function: + + logErr := bblog.GetLogErr() + logDebug := bblog.GetLogDebug(verbose) + logDebug.Printf("whatever") + +*/ + +var logErr *log.Logger +var logDebug *log.Logger + +func GetErr() *log.Logger { + if logErr == nil { + logErr = log.New(os.Stderr, "", 0) + } + return logErr +} + +func GetDebug(visible bool) *log.Logger { + if visible { + logDebug = log.New(os.Stderr, "", 0) + } else { + // Invisible mode (i.e. display nothing) + logDebug = log.New(ioutil.Discard, "", 0) + } + return logDebug +} diff --git a/pkg/box/box.go b/pkg/box/box.go index f0106e2b..edd3771d 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + "github.com/StackExchange/blackbox/v2/pkg/bblog" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/crypters" "github.com/StackExchange/blackbox/v2/pkg/vcs" @@ -30,8 +31,10 @@ type Box struct { Files []string // If non-empty, the list of files. FilesSet map[string]bool // If non-nil, a set of Files. // Handles to interfaces: - Vcs vcs.Vcs // Interface access to the VCS. - Crypter crypters.Crypter // Inteface access to GPG. + Vcs vcs.Vcs // Interface access to the VCS. + Crypter crypters.Crypter // Inteface access to GPG. + logErr *log.Logger + logDebug *log.Logger } // StatusMode is a type of query. @@ -48,12 +51,6 @@ const ( Changed ) -var logErr *log.Logger - -func init() { - logErr = log.New(os.Stderr, "", 0) -} - // NewFromFlags creates a box using items from flags. Nearly all subcommands use this. func NewFromFlags(c *cli.Context) *Box { /* @@ -66,8 +63,10 @@ func NewFromFlags(c *cli.Context) *Box { bx.RepoBaseDir: Is discovered. */ bx := &Box{ - Umask: c.Int("umask"), - Team: c.String("team"), + Umask: c.Int("umask"), + Team: c.String("team"), + logErr: bblog.GetErr(), + logDebug: bblog.GetDebug(c.Bool("verbose")), } // Discover which kind of VCS is in use. @@ -185,7 +184,7 @@ func (bx *Box) getAdmins() error { // Try the legacy file: fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") - logErr.Printf("Admins file: %q", fn) + bx.logDebug.Printf("Admins file: %q", fn) a, err := bbutil.ReadFileLines(fn) if err != nil { return fmt.Errorf("getAdmins can't load admins (%q): %v", fn, err) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index d5b59941..da7ffd9b 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -70,10 +70,10 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup for _, name := range names { fmt.Printf("========== DECRYPTING %q\n", name) if !bx.FilesSet[name] { - logErr.Printf("Skipping %q: File not registered with Blackbox", name) + bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) } if (!overwrite) && bbutil.FileExistsOrProblem(name) { - logErr.Printf("Skipping %q: Will not overwrite existing file", name) + bx.logErr.Printf("Skipping %q: Will not overwrite existing file", name) continue } @@ -87,7 +87,7 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup err := bx.Crypter.Decrypt(name, bx.Umask, overwrite) if err != nil { - logErr.Printf("%q: %v", name, err) + bx.logErr.Printf("%q: %v", name, err) continue } @@ -130,11 +130,11 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { for _, name := range names { fmt.Printf("========== ENCRYPTING %q\n", name) if !bx.FilesSet[name] { - logErr.Printf("Skipping %q: File not registered with Blackbox", name) + bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) } err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { - logErr.Printf("%q: %v", name, err) + bx.logErr.Printf("%q: %v", name, err) continue } suggest = append(suggest, fmt.Sprintf("Updated: %q", name)) @@ -178,12 +178,12 @@ func (bx *Box) Info() error { err := bx.getFiles() if err != nil { - logErr.Printf("Info: %v", err) + bx.logErr.Printf("Info: %v", err) } err = bx.getAdmins() if err != nil { - logErr.Printf("Info: %v", err) + bx.logErr.Printf("Info: %v", err) } //fmt.Printf("bx.Admins=%q\n", bx.Admins) @@ -206,8 +206,8 @@ func (bx *Box) Info() error { func (bx *Box) Init(yes, vcsname string) error { //fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) - fmt.Printf("team is: %q\n", bx.Team) - fmt.Printf("configdir will be: %q\n", bx.ConfigDir) + //fmt.Printf("team is: %q\n", bx.Team) + //fmt.Printf("configdir will be: %q\n", bx.ConfigDir) if yes != "yes" { fmt.Printf("Enable blackbox for this %v repo? (yes/no)", bx.Vcs.Name()) From 0a0068d7f7bdf0c0e9e8877ce122ae8a458dfec9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 12 Jun 2020 07:08:22 -0400 Subject: [PATCH 36/69] fix logging --- .gitignore | 1 + pkg/box/box.go | 2 +- pkg/box/verbs.go | 2 +- pkg/crypters/crypters.go | 6 +++--- pkg/crypters/gnupg/gnupg.go | 14 +++++++++++--- 5 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.gitignore b/.gitignore index 8e7c989b..21f230d0 100644 --- a/.gitignore +++ b/.gitignore @@ -57,3 +57,4 @@ docs/_build/ # Blackbox bbintegration .*.swp +/integrationTest/.blackbox diff --git a/pkg/box/box.go b/pkg/box/box.go index edd3771d..3b2f38a6 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -73,7 +73,7 @@ func NewFromFlags(c *cli.Context) *Box { bx.Vcs = vcs.Discover(bx.RepoBaseDir) // Pick a crypto backend (GnuPG, go-openpgp, etc.) - bx.Crypter = crypters.SearchByName(c.String("crypto")) + bx.Crypter = crypters.SearchByName(c.String("crypto"), c.Bool("debug")) if bx.Crypter == nil { fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the damn default\n") os.Exit(1) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index da7ffd9b..e5650bcf 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -134,7 +134,7 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { } err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { - bx.logErr.Printf("%q: %v", name, err) + bx.logErr.Printf("Failed to encrypt %q: %v", name, err) continue } suggest = append(suggest, fmt.Sprintf("Updated: %q", name)) diff --git a/pkg/crypters/crypters.go b/pkg/crypters/crypters.go index f9414f35..8e83e9b8 100644 --- a/pkg/crypters/crypters.go +++ b/pkg/crypters/crypters.go @@ -13,7 +13,7 @@ type Crypter interface { } // NewFnSig function signature needed by reg. -type NewFnSig func() (Crypter, error) +type NewFnSig func(debug bool) (Crypter, error) // Item stores one item type Item struct { @@ -27,12 +27,12 @@ var Catalog []*Item // SearchByName returns a Crypter handle for name. // The search is case insensitive. -func SearchByName(name string) Crypter { +func SearchByName(name string, debug bool) Crypter { name = strings.ToLower(name) for _, v := range Catalog { //fmt.Printf("Trying %v %v\n", v.Name) if strings.ToLower(v.Name) == name { - chandle, err := v.New() + chandle, err := v.New(debug) if err != nil { return nil // No idea how that would happen. } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index c06d4fb4..2eef213e 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -1,9 +1,11 @@ package gnupg import ( + "log" "os/exec" "syscall" + "github.com/StackExchange/blackbox/v2/pkg/bblog" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/crypters" ) @@ -16,12 +18,17 @@ func init() { // CrypterHandle is the handle type CrypterHandle struct { - GPGCmd string // "gpg2" or "gpg" + GPGCmd string // "gpg2" or "gpg" + logErr *log.Logger + logDebug *log.Logger } -func registerNew() (crypters.Crypter, error) { +func registerNew(debug bool) (crypters.Crypter, error) { - crypt := &CrypterHandle{} + crypt := &CrypterHandle{ + logErr: bblog.GetErr(), + logDebug: bblog.GetDebug(debug), + } // Which binary to use? path, err := exec.LookPath("gpg2") @@ -76,6 +83,7 @@ func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []strin a = append(a, filename) oldumask := syscall.Umask(umask) + crypt.logDebug.Printf("Args = %q", a) err := bbutil.RunBash(crypt.GPGCmd, a...) syscall.Umask(oldumask) From a189a4a0eea5a3f40d9ab6c9a3f261b9995db4f1 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 14 Jun 2020 17:07:04 -0400 Subject: [PATCH 37/69] wip! --- integrationTest/integration_test.go | 26 ++++--- integrationTest/ithelpers.go | 107 +++++++++++++++++++++++++--- pkg/bblog/bblog.go | 2 + pkg/bbutil/runbash.go | 18 +++-- pkg/vcs/git/git.go | 5 -- 5 files changed, 128 insertions(+), 30 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 8f696b27..3de729bd 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -80,20 +80,24 @@ func TestBasicCommands(t *testing.T) { setup(t) createDummyRepo(t, *vcsToTest) - // admin - checkOutput(t, "admin", "list", "000-admin-list.txt") - invalidArgs(t, "admin", "list", "--all") - invalidArgs(t, "admin", "one") + phase("Alice creates a repo. Creates secret.txt.") + makeFile(t, "secret.txt", "this is my secret") - // file - checkOutput(t, "file", "list", "000-file-list.txt") - invalidArgs(t, "file", "list", "one") - invalidArgs(t, "file", "list", "--all") + phase("Alice creates a GPG key...") + makeAdmin(t, "alice", "Alice Example", "alice@example.com") + become(t, "alice") - // status - createDummyFiles(t) - checkOutput(t, "status", "000-status.txt") + runBB(t, "admin", "add", "alice@example.com") +} + +func TestAlice(t *testing.T) { + // Create an empty repo with a user named Alice who + // performs many operations. All files are valid. + compile(t) + setup(t) + createDummyRepo(t, *vcsToTest) + createDummyFilesAdmin(t) // encrypt runBB(t, "encrypt", "foo.txt") assertFileMissing(t, "foo.txt") diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index d1a274d4..96d3d662 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -24,6 +24,16 @@ var verbose = flag.Bool("verbose", false, "reveal stderr") var originPath string +type userinfo struct { + name string + dir string // .gnupg-$name + agentInfo string // GPG_AGENT_INFO + email string + fullname string +} + +var users = map[string]*userinfo{} + func init() { testing.Init() flag.Parse() @@ -72,7 +82,7 @@ func createDummyRepo(t *testing.T, vcsname string) { } else { dir = "/tmp/repo" os.RemoveAll(filepath.Join(dir, ".")) - err = os.Mkdir(dir, 0o770) + err = os.Mkdir(dir, 0o000) } if err != nil { t.Fatalf("createDummyRepo: Could not make tempdir: %v", err) @@ -83,6 +93,9 @@ func createDummyRepo(t *testing.T, vcsname string) { runBB(t, "testing_init") // Runs "git init" and then vcs.Discover() runBB(t, "init", "yes") +} + +func createDummyFilesAdmin(t *testing.T) { addLineSorted(t, ".blackbox/blackbox-admins.txt", "user1@example.com") addLineSorted(t, ".blackbox/blackbox-admins.txt", "user2@example.com") addLineSorted(t, ".blackbox/blackbox-files.txt", "foo.txt") @@ -93,7 +106,7 @@ func createDummyRepo(t *testing.T, vcsname string) { makeFile(t, "bar.txt.gpg", "V nz gur one.gkg svyr!") } -func createDummyFiles(t *testing.T) { +func createFilesStatus(t *testing.T) { // This creates a few files with real plaintext but fake cyphertext. // There are a variety of timestamps to enable many statuses. t.Helper() @@ -126,17 +139,19 @@ func createDummyFiles(t *testing.T) { makeFile(t, "status-GPGMISSING.txt", "File with GPGMISSING in it.") // gpg file intentionally missing. + // Plaintext intentionally missing. ("status-BOTHMISSING.txt") + // gpg file intentionally missing. ("status-BOTHMISSING.txt.gpg") + + // NB(tlim): commented out. I can't think of an error I can reproduce. // makeFile(t, "status-PLAINERROR.txt", "File with PLAINERROR in it.") // makeFile(t, "status-PLAINERROR.txt.gpg", "Svyr jvgu CYNVAREEBE va vg.") // setFilePerms(t, "status-PLAINERROR.txt", 0o000) + // NB(tlim): commented out. I can't think of an error I can reproduce. // makeFile(t, "status-GPGERROR.txt", "File with GPGERROR in it.") // makeFile(t, "status-GPGERROR.txt.gpg", "Svyr jvgu TCTREEBE va vg.") // setFilePerms(t, "status-GPGERROR.txt.gpg", 0o000) - // Plaintext intentionally missing. ("status-BOTHMISSING.txt") - // gpg file intentionally missing. ("status-BOTHMISSING.txt.gpg") - time.Sleep(200 * time.Millisecond) if err := bbutil.Touch("status-DECRYPTED.txt"); err != nil { @@ -177,10 +192,8 @@ func setFilePerms(t *testing.T, name string, perms int) { func checkOutput(t *testing.T, args ...string) { t.Helper() - // Pop off the last arg. Use it as the filename for expected output. - n := len(args) - 1 - name := args[n] - args = args[:n] + // Pop off the last arg. Use it as the filename. + name, args := args[len(args)-1], args[:len(args)-1] cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil @@ -271,6 +284,82 @@ func phase(msg string) { logVerbose.Println("********************") } +func makeAdmin(t *testing.T, name, fullname, email string) { + testing.Init() + + dir, err := filepath.Abs(filepath.Join(os.Getenv("HOME"), ".gnupg-"+name)) + if err != nil { + t.Fatal(err) + } + os.Mkdir(dir, 0o077) + + u := &userinfo{ + name: name, + dir: dir, + fullname: fullname, + email: email, + } + users[name] = u + + // GNUPGHOME=u.dir + // echo 'pinentry-program' "$(which pinentry-tty)" >> "$GNUPGHOME/gpg-agent.conf" + os.Setenv("GNUPGHOME", u.dir) + out, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") + if err != nil { + t.Fatal(err) + } + u.agentInfo = strings.TrimSpace(out) + + os.Setenv("GNUPGHOME", u.dir) + // Generate key: + bbutil.RunBash("gpg", + "--homedir", u.dir, + "--batch", + "--passphrase", "", + "--quick-generate-key", "u.email", + ) +} + +func become(t *testing.T, name string) { + testing.Init() + u := users[name] + + os.Setenv("GNUPGHOME", u.dir) + os.Setenv("GPG_AGENT_INFO", u.agentInfo) + bbutil.RunBash("git", "config", "user.name", u.name) + bbutil.RunBash("git", "config", "user.email", u.fullname) +} + +// // Get fingerprint: +// // Retrieve fingerprint of generated key. +// // Use it to extract the secret/public keys. +// // (stolen from https://raymii.org/s/articles/GPG_noninteractive_batch_sign_trust_and_send_gnupg_keys.html) +// +// // fpr=`gpg --homedir /tmp/blackbox_createrole --fingerprint --with-colons "$ROLE_NAME" | awk -F: '/fpr:/ {print $10}' | head -n 1` +// var fpr string +// bbutil.RunBashOutput("gpg", +// "--homedir", "/tmp/blackbox_createrole", +// "--fingerprint", +// "--with-colons", +// u.email, +// ) +// for i, l := range string.Split(out, "\n") { +// if string.HasPrefix(l, "fpr:") { +// fpr = strings.Split(l, ":")[9] +// } +// break +// } +// +// // Create key key: +// // gpg --homedir "$gpghomedir" --batch --passphrase '' --quick-add-key "$fpr" rsa encr +// bbutil.RunBash("gpg", +// "--homedir", u.dir, +// "--batch", +// "--passphrase", "", +// "--quick-add-key", fpr, +// "rsa", "encr", +// ) + // function md5sum_file() { // # Portably generate the MD5 hash of file $1. // case $(uname -s) in diff --git a/pkg/bblog/bblog.go b/pkg/bblog/bblog.go index b2dcb950..e51587d5 100644 --- a/pkg/bblog/bblog.go +++ b/pkg/bblog/bblog.go @@ -28,6 +28,7 @@ Or in a function: var logErr *log.Logger var logDebug *log.Logger +// GetErr returns a logger handle used for errors func GetErr() *log.Logger { if logErr == nil { logErr = log.New(os.Stderr, "", 0) @@ -35,6 +36,7 @@ func GetErr() *log.Logger { return logErr } +// GetDebug returns a Logger handle used for debug info (output is discarded if viable=false) func GetDebug(visible bool) *log.Logger { if visible { logDebug = log.New(os.Stderr, "", 0) diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index 5635110d..6f3cb76e 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -9,10 +9,6 @@ import ( // RunBash runs a Bash command. func RunBash(command string, args ...string) error { - //if dryRun { - // fmt.Printf("DRY_RUN: Would run exec.Command(%v, %v)\n", command, args) - // return nil - //} cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout @@ -23,7 +19,19 @@ func RunBash(command string, args ...string) error { } err = cmd.Wait() if err != nil { - return fmt.Errorf("run_bash: %w", err) + return fmt.Errorf("RunBash err=%w", err) } return nil } + +// RunBash runs a Bash command. +func RunBashOutput(command string, args ...string) (string, error) { + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("RunBashOutput err=%w", err) + } + return string(out), err +} diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index c66337af..8a62be89 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -78,14 +78,9 @@ NEXT STEP: You need to manually check these in: return nil } -//echo "========== Encrypting: $unencrypted" >&2 -//$GPG --use-agent --yes --trust-model=always --encrypt -o "$encrypted" $(awk '{ print "-r" $1 }' < "$BB_ADMINS") "$unencrypted" >&2 -//echo '========== Encrypting: DONE' >&2 - // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. - func (v VcsHandle) TestingInitRepo() error { bbutil.RunBash("git", "init") return nil From e0e61f57bab73ffc709dc33d21c6ccb1847ca19d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 14 Jun 2020 20:41:42 -0400 Subject: [PATCH 38/69] wip! --- cmd/blackbox/cli.go | 6 +- go.mod | 1 + go.sum | 2 + integrationTest/integration_test.go | 58 +++++++++++--- integrationTest/ithelpers.go | 93 +++++++++++++--------- integrationTest/test_data/000-status.txt | 2 - integrationTest/test_data/status-noreg.txt | 6 ++ pkg/bbgit/discover.go | 15 ---- pkg/bblog/bblog.go | 10 +-- pkg/box/box.go | 7 ++ pkg/box/boxutils.go | 16 +++- pkg/box/verbs.go | 14 +++- pkg/vcs/git/git.go | 5 +- pkg/vcs/none/none.go | 5 +- 14 files changed, 162 insertions(+), 78 deletions(-) create mode 100644 integrationTest/test_data/status-noreg.txt delete mode 100644 pkg/bbgit/discover.go diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index 2c21d42a..898a946b 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -32,9 +32,9 @@ func flags() *cli.App { EnvVars: []string{"BLACKBOX_CRYPTO"}, }, &cli.StringFlag{ - Name: "config", - Usage: "Path to config", - Value: ".blackbox", + Name: "config", + Usage: "Path to config", + //Value: ".blackbox", EnvVars: []string{"BLACKBOX_CONFIGDIR", "BLACKBOXDATA"}, }, &cli.StringFlag{ diff --git a/go.mod b/go.mod index 7f43a864..9e66cb0f 100644 --- a/go.mod +++ b/go.mod @@ -3,6 +3,7 @@ module github.com/StackExchange/blackbox/v2 go 1.14 require ( + github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/mattn/go-runewidth v0.0.9 // indirect github.com/olekukonko/tablewriter v0.0.4 diff --git a/go.sum b/go.sum index 76c865f8..23c9d9b4 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965 h1:XlKWFR4yjGVmKa6AoUntvu0G2in7DGcbd+dQFGuoA6s= +github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965/go.mod h1:k+NmgLkBQBiBOGbFJkqGcCCb2rD5OnsWQHIkza8PAWQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 3de729bd..b5b8531e 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -20,6 +20,12 @@ var vcsToTest = flag.String("testvcs", "GIT", "VCS to test") func init() { testing.Init() flag.Parse() + + op, err := os.Getwd() + if err != nil { + panic(err) + } + originPath = op } func compile(t *testing.T) { @@ -51,15 +57,11 @@ func setup(t *testing.T) { logDebug.Printf("Using BLACKBOX_VCS=%v", vh.Name()) os.Setenv("BLACKBOX_VCS", vh.Name()) - op, err := os.Getwd() - if err != nil { - t.Fatal(err) - } - originPath = op } func TestInit(t *testing.T) { compile(t) + makeHomeDir(t, "init") // Only zero or one args are permitted. invalidArgs(t, "init", "one", "two") @@ -72,13 +74,51 @@ func TestInit(t *testing.T) { assertFilePerms(t, ".blackbox/blackbox-files.txt", 0o640) } -func TestBasicCommands(t *testing.T) { +func TestList(t *testing.T) { + compile(t) + makeHomeDir(t, "init") + + runBB(t, "init", "yes") + createDummyFilesAdmin(t) + checkOutput(t, "admin", "list", "000-admin-list.txt") + checkOutput(t, "file", "list", "000-file-list.txt") + + invalidArgs(t, "file", "list", "extra") + invalidArgs(t, "admin", "list", "extra") +} + +func TestStatus(t *testing.T) { + compile(t) + makeHomeDir(t, "init") + + runBB(t, "init", "yes") + createFilesStatus(t) + checkOutput(t, "status", "000-status.txt") +} + +func TestStatus_notreg(t *testing.T) { + compile(t) + makeHomeDir(t, "init") + + runBB(t, "init", "yes") + createFilesStatus(t) + checkOutput(t, "status", "status-ENCRYPTED.txt", "blah.txt", + "status-noreg.txt") +} + +// TestBasicCommands tests of the basic functions, using a fake homedir and repo. +// The files are full of garbage, not real encrypted data. +func TestBasic(t *testing.T) { // These are basic tests that work on a fake repo. // The repo has mostly real data, except any .gpg file // is just garbage. compile(t) setup(t) - createDummyRepo(t, *vcsToTest) + makeHomeDir(t, "Basic") + + runBB(t, "testing_init") // Runs "git init" or equiv + assertFileExists(t, ".git") + runBB(t, "init", "yes") // Creates .blackbox or equiv phase("Alice creates a repo. Creates secret.txt.") makeFile(t, "secret.txt", "this is my secret") @@ -96,8 +136,8 @@ func TestAlice(t *testing.T) { // performs many operations. All files are valid. compile(t) setup(t) - createDummyRepo(t, *vcsToTest) - createDummyFilesAdmin(t) + populateDummyRepo(t, *vcsToTest) + // encrypt runBB(t, "encrypt", "foo.txt") assertFileMissing(t, "foo.txt") diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 96d3d662..6eecf070 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -16,14 +16,13 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/vcs" _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" + "github.com/TomOnTime/hind" "github.com/andreyvit/diff" ) var verbose = flag.Bool("verbose", false, "reveal stderr") -var originPath string - type userinfo struct { name string dir string // .gnupg-$name @@ -40,18 +39,18 @@ func init() { } var logErr *log.Logger -var logVerbose *log.Logger +var logDebug *log.Logger func init() { logErr = bblog.GetErr() - logVerbose = bblog.GetDebug(*verbose) + logDebug = bblog.GetDebug(*verbose) } func getVcs(t *testing.T, name string) vcs.Vcs { t.Helper() // Set up the vcs for _, v := range vcs.Catalog { - logVerbose.Printf("Testing vcs: %v == %v", name, v.Name) + logDebug.Printf("Testing vcs: %v == %v", name, v.Name) if strings.ToLower(v.Name) == strings.ToLower(name) { h, err := v.New() if err != nil { @@ -59,7 +58,7 @@ func getVcs(t *testing.T, name string) vcs.Vcs { } return h } - logVerbose.Println("...Nope.") + logDebug.Println("...Nope.") } return nil @@ -67,32 +66,52 @@ func getVcs(t *testing.T, name string) vcs.Vcs { // TestBasicCommands's helpers -func createDummyRepo(t *testing.T, vcsname string) { - // This creates a repo with real data, except any .gpg file - // is just garbage. - +func makeHomeDir(t *testing.T, testname string) { t.Helper() - logVerbose.Printf("createDummyRepo()\n") - - var dir string + var homedir string var err error + if false { - dir, err = ioutil.TempDir("", "repo") - defer os.RemoveAll(dir) // clean up + // Make a random location that is deleted later + homedir, err = ioutil.TempDir("", filepath.Join("bbhome-"+testname)) + defer os.RemoveAll(homedir) // clean up + if err != nil { + t.Fatal(err) + } } else { - dir = "/tmp/repo" - os.RemoveAll(filepath.Join(dir, ".")) - err = os.Mkdir(dir, 0o000) + // Make a predictable location. wipe and re-use + homedir = "/tmp/bbhome-" + testname + os.RemoveAll(homedir) + err = os.Mkdir(homedir, 0o770) + if err != nil { + t.Fatal(fmt.Errorf("mk-home %q: %v", homedir, err)) + } + } + + err = os.Setenv("HOME", homedir) + if err != nil { + t.Fatal(err) + } + logDebug.Printf("TESTING DIR HOME: cd %v\n", homedir) + + repodir := filepath.Join(homedir, "repo") + err = os.Mkdir(repodir, 0o770) + if err != nil { + t.Fatal(fmt.Errorf("mk-repo %q: %v", repodir, err)) } + err = os.Chdir(repodir) if err != nil { - t.Fatalf("createDummyRepo: Could not make tempdir: %v", err) + t.Fatal(err) } - logVerbose.Printf("TESTING DIRECTORY: cd %v\n", dir) +} - os.Chdir(dir) +func populateDummyRepo(t *testing.T, vcsname string) { + // This creates a repo with real data, except any .gpg file + // is just garbage. + + t.Helper() + logDebug.Printf("populateDummyRepo()\n") - runBB(t, "testing_init") // Runs "git init" and then vcs.Discover() - runBB(t, "init", "yes") } func createDummyFilesAdmin(t *testing.T) { @@ -187,13 +206,15 @@ func setFilePerms(t *testing.T, name string, perms int) { } } +var originPath string // CWD when program started. + // checkOutput runs blackbox with args, the last arg is the filename // of the expected output. Error if output is not expected. func checkOutput(t *testing.T, args ...string) { t.Helper() // Pop off the last arg. Use it as the filename. - name, args := args[len(args)-1], args[:len(args)-1] + name, args := args[hind.G(args)], args[:hind.G(args)] cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil @@ -219,7 +240,7 @@ func checkOutput(t *testing.T, args ...string) { func invalidArgs(t *testing.T, args ...string) { t.Helper() - logVerbose.Printf("invalidArgs(%q): \n", args) + logDebug.Printf("invalidArgs(%q): \n", args) cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil if *verbose { @@ -228,17 +249,17 @@ func invalidArgs(t *testing.T, args ...string) { } err := cmd.Run() if err == nil { - logVerbose.Println("BAD") + logDebug.Println("BAD") t.Fatal(fmt.Errorf("invalidArgs(%q): wanted failure but got success", args)) } - logVerbose.Printf("^^^^ (correct error received): err=%q\n", err) + logDebug.Printf("^^^^ (correct error received): err=%q\n", err) } // TestAliceAndBob's helpers. func setupUser(t *testing.T, user, passphrase string) { t.Helper() - logVerbose.Printf("DEBUG: setupUser %q %q\n", user, passphrase) + logDebug.Printf("DEBUG: setupUser %q %q\n", user, passphrase) } var pathToBlackBox string @@ -248,14 +269,14 @@ func PathToBlackBox() string { return pathToBlackBox } // SetPathToBlackBox sets the path. func SetPathToBlackBox(n string) { - logVerbose.Printf("PathToBlackBox=%q\n", n) + logDebug.Printf("PathToBlackBox=%q\n", n) pathToBlackBox = n } func runBB(t *testing.T, args ...string) { t.Helper() - logVerbose.Printf("runBB(%q)\n", args) + logDebug.Printf("runBB(%q)\n", args) cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil cmd.Stdout = os.Stdout @@ -277,11 +298,11 @@ func runBB(t *testing.T, args ...string) { // } func phase(msg string) { - logVerbose.Println("********************") - logVerbose.Println("********************") - logVerbose.Printf("********* %v\n", msg) - logVerbose.Println("********************") - logVerbose.Println("********************") + logDebug.Println("********************") + logDebug.Println("********************") + logDebug.Printf("********* %v\n", msg) + logDebug.Println("********************") + logDebug.Println("********************") } func makeAdmin(t *testing.T, name, fullname, email string) { @@ -291,7 +312,7 @@ func makeAdmin(t *testing.T, name, fullname, email string) { if err != nil { t.Fatal(err) } - os.Mkdir(dir, 0o077) + os.Mkdir(dir, 0o700) u := &userinfo{ name: name, diff --git a/integrationTest/test_data/000-status.txt b/integrationTest/test_data/000-status.txt index 2e967b64..7ecf5510 100644 --- a/integrationTest/test_data/000-status.txt +++ b/integrationTest/test_data/000-status.txt @@ -1,8 +1,6 @@ +-------------+------------------------+ | STATUS | NAME | +-------------+------------------------+ -| ENCRYPTED | bar.txt | -| ENCRYPTED | foo.txt | | BOTHMISSING | status-BOTHMISSING.txt | | DECRYPTED | status-DECRYPTED.txt | | ENCRYPTED | status-ENCRYPTED.txt | diff --git a/integrationTest/test_data/status-noreg.txt b/integrationTest/test_data/status-noreg.txt new file mode 100644 index 00000000..9b12e46e --- /dev/null +++ b/integrationTest/test_data/status-noreg.txt @@ -0,0 +1,6 @@ ++-----------+----------------------+ +| STATUS | NAME | ++-----------+----------------------+ +| ENCRYPTED | status-ENCRYPTED.txt | +| NOTREG | blah.txt | ++-----------+----------------------+ diff --git a/pkg/bbgit/discover.go b/pkg/bbgit/discover.go deleted file mode 100644 index fd3786f7..00000000 --- a/pkg/bbgit/discover.go +++ /dev/null @@ -1,15 +0,0 @@ -package bbgit - -// func New() (*GitInfo, error) { -// ri := new(GitInfo) -// path, err := exec.LookPath("git") -// if err != nil { -// return nil, nil -// } -// baseDir, err := exec.Command(path, "rev-parse", "--show-toplevel").Output() -// if err != nil { -// return nil, errors.Wrap(err, "bbgit:") -// } -// ri.baseDir = strings.TrimSuffix(string(baseDir), "\n") // remove a single newline. -// return ri, nil -// } diff --git a/pkg/bblog/bblog.go b/pkg/bblog/bblog.go index e51587d5..1b25b0af 100644 --- a/pkg/bblog/bblog.go +++ b/pkg/bblog/bblog.go @@ -13,15 +13,15 @@ To use this, include the following lines in your .go file. var logErr *log.Logger var logDebug *log.Logger func init() { - logErr = bblog.GetLogErr() - logDebug = bblog.GetLogDebug(verbose) + logErr = bblog.GetErr() + logDebug = bblog.GetDebug(verbose) } Or in a function: - logErr := bblog.GetLogErr() - logDebug := bblog.GetLogDebug(verbose) - logDebug.Printf("whatever") + logErr := bblog.GetErr() + logDebug := bblog.GetDebug(verbose) + logDebug.Printf("whatever: %v", err) */ diff --git a/pkg/box/box.go b/pkg/box/box.go index 3b2f38a6..ab451ef0 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -17,6 +17,9 @@ import ( "github.com/urfave/cli/v2" ) +var logErr *log.Logger +var logDebug *log.Logger + // Box describes what we know about a box. type Box struct { // Paths: @@ -62,6 +65,10 @@ func NewFromFlags(c *cli.Context) *Box { bx.ConfigDir: Is discovered. bx.RepoBaseDir: Is discovered. */ + + logErr = bblog.GetErr() + logDebug = bblog.GetDebug(c.Bool("debug")) + bx := &Box{ Umask: c.Int("umask"), Team: c.String("team"), diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 60abd90b..36f9fe33 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -148,10 +148,16 @@ func GenerateConfigDir(configdir, team string) string { // FindConfigDir tests various places until it finds the config dir. func FindConfigDir(configdir, team string) (string, error) { + // if configdir is set, use it. if configdir != "" { - if p, err := filepath.Abs(configdir); err == nil { - return p, nil + logDebug.Printf("CONFIG IS SET. NOT DISCOVERING: %q\n", configdir) + _, err := os.Stat(configdir) + if err != nil { + return "", fmt.Errorf("config dir %q error: %v", configdir, err) + } + if _, err := filepath.Abs(configdir); err != nil { + return "", fmt.Errorf("config dir abs %q error: %v", configdir, err) } return configdir, nil } @@ -164,6 +170,9 @@ func FindConfigDir(configdir, team string) (string, error) { candidates = append(candidates, ".blackbox") } candidates = append(candidates, "keyrings/live") + + logDebug.Printf("DEBUG: candidates = %q\n", candidates) + // Prevent an infinite loop by only doing "cd .." this many times maxDirLevels := 100 relpath := "" @@ -171,12 +180,13 @@ func FindConfigDir(configdir, team string) (string, error) { // Does relpath contain any of our directory names? for _, c := range candidates { t := filepath.Join(relpath, c) + logDebug.Printf("Trying %q\n", t) d, err := bbutil.DirExists(t) if err != nil { return "", fmt.Errorf("dirExists(%q) failed: %v", t, err) } if d { - return filepath.Abs(relpath) + return filepath.Abs(t) } } // If we are at the root, stop. diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index e5650bcf..194a7076 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -279,7 +279,13 @@ func (bx *Box) Status(names []string, nameOnly bool, match string) error { var tcData bool for _, name := range flist { - stat, err := FileStatus(name) + var stat string + var err error + if _, ok := bx.FilesSet[name]; ok { + stat, err = FileStatus(name) + } else { + stat, err = "NOTREG", nil + } if (match == "") || (stat == match) { if err == nil { data = append(data, []string{stat, name}) @@ -321,9 +327,13 @@ func (bx *Box) TestingInitRepo() error { fmt.Printf("BLACKBOX_VCS=%q\n", os.Getenv("BLACKBOX_VCS")) os.Exit(1) } + fmt.Printf("ABOUT TO CALL TestingInitRepo\n") + fmt.Printf("vcs = %v\n", bx.Vcs.Name()) err := bx.Vcs.TestingInitRepo() + fmt.Printf("RETURNED from TestingInitRepo: %v\n", err) + fmt.Println(os.Getwd()) if err != nil { - return err + return fmt.Errorf("TestingInitRepo returned: %w", err) } if !bx.Vcs.Discover("") { return fmt.Errorf("TestingInitRepo failed Discovery") diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 8a62be89..129b31a4 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -82,6 +82,7 @@ NEXT STEP: You need to manually check these in: // TestingInitRepo initializes a repo. func (v VcsHandle) TestingInitRepo() error { - bbutil.RunBash("git", "init") - return nil + fmt.Println("RUNNING GIT INIT") + return bbutil.RunBash("git", "init") + } diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 26ea0d52..38d40b32 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -1,10 +1,12 @@ package none import ( + "fmt" + "github.com/StackExchange/blackbox/v2/pkg/vcs" ) -var pluginName = "GIT" +var pluginName = "NONE" func init() { vcs.Register(pluginName, 0, newNone) @@ -48,5 +50,6 @@ func (v VcsHandle) SuggestTracking(repobasedir string, message string, files ... // TestingInitRepo initializes a repo. func (v VcsHandle) TestingInitRepo() error { + fmt.Println("VCS=none, TestingInitRepo") return nil } From 7276fae1b51407f60107c653b35b535d4d03916b Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 15 Jun 2020 10:15:05 -0400 Subject: [PATCH 39/69] Now working: init, admin add, file add, encrypt, decrypt, shred --- cmd/blackbox/cli.go | 9 +- cmd/blackbox/drive.go | 10 +-- integrationTest/asserts.go | 15 ++++ integrationTest/integration_test.go | 55 ++++++++---- integrationTest/ithelpers.go | 15 ++-- integrationTest/run-tests-verbose.sh | 3 + integrationTest/run-tests.sh | 3 + models/crypters.go | 4 +- models/vcs.go | 2 +- pkg/bbutil/filestats.go | 31 +++++++ pkg/bbutil/runbash.go | 17 +++- pkg/box/box.go | 22 +++-- pkg/box/verbs.go | 127 +++++++++++++++++++++++---- pkg/crypters/gnupg/gnupg.go | 45 +++++++++- pkg/vcs/git/git.go | 6 +- pkg/vcs/none/none.go | 2 +- 16 files changed, 297 insertions(+), 69 deletions(-) create mode 100644 integrationTest/run-tests-verbose.sh create mode 100644 integrationTest/run-tests.sh diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index 898a946b..d33d2a22 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -49,8 +49,9 @@ func flags() *cli.App { EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"}, }, &cli.BoolFlag{ - Name: "debug", - Usage: "Show debug output", + Name: "debug", + Usage: "Show debug output", + EnvVars: []string{"BLACKBOX_DEBUG"}, }, } @@ -142,7 +143,7 @@ func flags() *cli.App { Name: "add", Usage: "Registers file with the system", Flags: []cli.Flag{ - &cli.BoolFlag{Name: "leave", Usage: "Do not remove plaintext version"}, + &cli.BoolFlag{Name: "shred", Usage: "Remove plaintext afterwords"}, }, Action: func(c *cli.Context) error { return cmdFileAdd(c) }, }, @@ -168,7 +169,7 @@ func flags() *cli.App { { Name: "shred", - Usage: "Shred the plaintext", + Usage: "Shred files, or --all for all registered files", Flags: []cli.Flag{ &cli.BoolFlag{Name: "all", Usage: "All registered files"}, }, diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index aedd3372..f0ba4f93 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -34,12 +34,12 @@ func allOrSomeFiles(c *cli.Context) error { // Keep these functions in alphabetical order. func cmdAdminAdd(c *cli.Context) error { - if !c.Args().Present() { + if c.NArg() == 0 || c.NArg() > 2 { return fmt.Errorf( - "Must specify at least one admin's GnuPG user-id (i.e. email address)") + "Must specify one admin's GnuPG user-id (i.e. email address) and optionally the directory of the pubkey data (default ~/.GnuPG)") } bx := box.NewFromFlags(c) - return bx.AdminAdd(c.Args().Slice()) + return bx.AdminAdd(c.Args().Get(0), c.Args().Get(1)) } func cmdAdminList(c *cli.Context) error { @@ -115,7 +115,7 @@ func cmdFileAdd(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) - return bx.FileAdd(c.Args().Slice(), c.Bool("overwrite")) + return bx.FileAdd(c.Args().Slice(), c.Bool("shred")) } func cmdFileList(c *cli.Context) error { @@ -163,7 +163,7 @@ func cmdShred(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Shred(c.Args().Slice()...) + return bx.Shred(c.Args().Slice()) } func cmdStatus(c *cli.Context) error { diff --git a/integrationTest/asserts.go b/integrationTest/asserts.go index 5e58f7f5..91f5871c 100644 --- a/integrationTest/asserts.go +++ b/integrationTest/asserts.go @@ -4,6 +4,8 @@ import ( "io/ioutil" "os" "testing" + + "github.com/andreyvit/diff" ) func assertFileMissing(t *testing.T, name string) { @@ -41,6 +43,19 @@ func assertFileEmpty(t *testing.T, name string) { } } +func assertFileContents(t *testing.T, name string, contents string) { + t.Helper() + c, err := ioutil.ReadFile(name) + if err != nil { + t.Fatal(err) + } + + if w, g := contents, string(c); w != g { + t.Errorf("assertFileContents(%q) mismatch (-got +want):\n%s", + name, diff.LineDiff(g, w)) + } +} + func assertFilePerms(t *testing.T, name string, perms os.FileMode) { t.Helper() s, err := os.Stat(name) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index b5b8531e..632c74ff 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -96,6 +96,17 @@ func TestStatus(t *testing.T) { checkOutput(t, "status", "000-status.txt") } +func TestShred(t *testing.T) { + compile(t) + makeHomeDir(t, "shred") + runBB(t, "init", "yes") + + makeFile(t, "shredme.txt", "File with SHREDME in it.") + assertFileExists(t, "shredme.txt") + runBB(t, "shred", "shredme.txt") + assertFileMissing(t, "shredme.txt") +} + func TestStatus_notreg(t *testing.T) { compile(t) makeHomeDir(t, "init") @@ -123,36 +134,39 @@ func TestBasic(t *testing.T) { phase("Alice creates a repo. Creates secret.txt.") makeFile(t, "secret.txt", "this is my secret") - phase("Alice creates a GPG key...") - makeAdmin(t, "alice", "Alice Example", "alice@example.com") + phase("Alice creates a GPG key") + gpgdir := makeAdmin(t, "alice", "Alice Example", "alice@example.com") become(t, "alice") - runBB(t, "admin", "add", "alice@example.com") - -} - -func TestAlice(t *testing.T) { - // Create an empty repo with a user named Alice who - // performs many operations. All files are valid. - compile(t) - setup(t) - populateDummyRepo(t, *vcsToTest) + phase("Alice enrolls as an admin") + runBB(t, "admin", "add", "alice@example.com", gpgdir) // encrypt - runBB(t, "encrypt", "foo.txt") + phase("Alice registers foo.txt") + plaintextFoo := "I am the foo.txt file!\n" + makeFile(t, "foo.txt", plaintextFoo) + runBB(t, "file", "add", "--shred", "foo.txt") + //runBB(t, "encrypt", "--shred", "foo.txt") + // We shred the plaintext so that we are sure that when Decrypt runs, + // we can verify the contents wasn't just sitting there all the time. assertFileMissing(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") + phase("Alice decrypts foo.txt") // decrypt runBB(t, "decrypt", "foo.txt") assertFileExists(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") + assertFileContents(t, "foo.txt", plaintextFoo) - // reencrypt + // encrypts (without shredding) + phase("Alice encrypts foo.txt (again)") + runBB(t, "encrypt", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + assertFileContents(t, "foo.txt", plaintextFoo) - // edit - invalidArgs(t, "edit") - invalidArgs(t, "edit", "--all") + // reencrypt // cat @@ -167,4 +181,11 @@ func TestAlice(t *testing.T) { // setupUser(t, "bob", "b") // runBB(t, "init") // runBB(t, "admin", "add", "alice@") + +// FYI: test "admins add" with multiple people. + +// Edit requires a name, and doesn't work with --all. +// invalidArgs(t, "edit") +// invalidArgs(t, "edit", "--all") + // } diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 6eecf070..98f1e009 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -105,16 +105,9 @@ func makeHomeDir(t *testing.T, testname string) { } } -func populateDummyRepo(t *testing.T, vcsname string) { +func createDummyFilesAdmin(t *testing.T) { // This creates a repo with real data, except any .gpg file // is just garbage. - - t.Helper() - logDebug.Printf("populateDummyRepo()\n") - -} - -func createDummyFilesAdmin(t *testing.T) { addLineSorted(t, ".blackbox/blackbox-admins.txt", "user1@example.com") addLineSorted(t, ".blackbox/blackbox-admins.txt", "user2@example.com") addLineSorted(t, ".blackbox/blackbox-files.txt", "foo.txt") @@ -305,7 +298,7 @@ func phase(msg string) { logDebug.Println("********************") } -func makeAdmin(t *testing.T, name, fullname, email string) { +func makeAdmin(t *testing.T, name, fullname, email string) string { testing.Init() dir, err := filepath.Abs(filepath.Join(os.Getenv("HOME"), ".gnupg-"+name)) @@ -337,8 +330,10 @@ func makeAdmin(t *testing.T, name, fullname, email string) { "--homedir", u.dir, "--batch", "--passphrase", "", - "--quick-generate-key", "u.email", + "--quick-generate-key", u.email, ) + + return u.dir } func become(t *testing.T, name string) { diff --git a/integrationTest/run-tests-verbose.sh b/integrationTest/run-tests-verbose.sh new file mode 100644 index 00000000..f0e35831 --- /dev/null +++ b/integrationTest/run-tests-verbose.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose "$@" diff --git a/integrationTest/run-tests.sh b/integrationTest/run-tests.sh new file mode 100644 index 00000000..56496958 --- /dev/null +++ b/integrationTest/run-tests.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +rm -rf /tmp/bbhome-* && go test "$@" diff --git a/models/crypters.go b/models/crypters.go index 0831eae0..cab4ebbd 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -7,5 +7,7 @@ type Crypter interface { // Decrypt name+".gpg", possibly overwriting name. Decrypt(filename string, umask int, overwrite bool) error // Encrypt name, overwriting name+".gpg" - Encrypt(filename string, umask int, receivers []string) error + Encrypt(filename string, umask int, receivers []string) (string, error) + // AddNewKey extracts keyname from sourcedir's GnuPG chain to destdir keychain. + AddNewKey(keyname, sourcedir, destdir string) ([]string, error) } diff --git a/models/vcs.go b/models/vcs.go index 9d572906..65bdc7f0 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -12,7 +12,7 @@ type Vcs interface { // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. IgnoreAnywhere(repobasedir string, files ...string) error // SuggestTracking tells the VCS to suggest the user commit these files. - SuggestTracking(repobasedir string, message string, files ...string) error + SuggestTracking(repobasedir string, message string, files []string) error // TestingInitRepo initializes a repo of this type (for use by integration tests) TestingInitRepo() error diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 0247dd44..57a4ec28 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -4,6 +4,7 @@ import ( "fmt" "io/ioutil" "os" + "os/exec" "sort" "strings" "time" @@ -49,6 +50,36 @@ func Touch(name string) error { return os.Chtimes(name, currentTime, currentTime) } +// ShredFiles securely erases a list of files. +func ShredFiles(names []string) error { + var path, flag string + var err error + if path, err = exec.LookPath("shred"); err == nil { + flag = "-u" + } else if path, err = exec.LookPath("srm"); err == nil { + flag = "-f" + } else if path, err = exec.LookPath("rm"); err == nil { + flag = "-f" + // FIXME(tlim): Test if "rm -P $tempfile" returns a error. + // If it doesn't, flag = "-Pf" + } + + // TODO(tlim) DO the shredding in parallel like in v1. + + for _, n := range names { + fmt.Printf("SHREDDING (%q, %q): %q\n", path, flag, n) + e := RunBash(path, flag, n) + if e != nil { + err = e + fmt.Printf("ERROR: %v", e) + } else { + fmt.Println() + } + } + fmt.Println("DONE.") + return err +} + // ReadFileLines is like ioutil.ReadFile() but returns an []string. func ReadFileLines(filename string) ([]string, error) { b, err := ioutil.ReadFile(filename) diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index 6f3cb76e..70e61055 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -1,6 +1,7 @@ package bbutil import ( + "bytes" "fmt" "log" "os" @@ -24,7 +25,7 @@ func RunBash(command string, args ...string) error { return nil } -// RunBash runs a Bash command. +// RunBashOutput runs a Bash command, captures output. func RunBashOutput(command string, args ...string) (string, error) { cmd := exec.Command(command, args...) cmd.Stdin = os.Stdin @@ -35,3 +36,17 @@ func RunBashOutput(command string, args ...string) (string, error) { } return string(out), err } + +// RunBashInput runs a Bash command, sends input on stdin. +func RunBashInput(input string, command string, args ...string) error { + + cmd := exec.Command(command, args...) + cmd.Stdin = bytes.NewBuffer([]byte(input)) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + err := cmd.Run() + if err != nil { + return fmt.Errorf("RunBashInput err=%w", err) + } + return err +} diff --git a/pkg/box/box.go b/pkg/box/box.go index ab451ef0..2fc11606 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -4,10 +4,10 @@ package box import ( "fmt" - "io/ioutil" "log" "os" "path/filepath" + "sort" "strings" "github.com/StackExchange/blackbox/v2/pkg/bblog" @@ -194,9 +194,13 @@ func (bx *Box) getAdmins() error { bx.logDebug.Printf("Admins file: %q", fn) a, err := bbutil.ReadFileLines(fn) if err != nil { - return fmt.Errorf("getAdmins can't load admins (%q): %v", fn, err) + return fmt.Errorf("getAdmins can't load %q: %v", fn, err) + } + if !sort.StringsAreSorted(a) { + return fmt.Errorf("file corrupt. Lines not sorted: %v", fn) } bx.Admins = a + return nil } @@ -210,15 +214,17 @@ func (bx *Box) getFiles() error { // Try the legacy file: fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") - b, err := ioutil.ReadFile(fn) + bx.logDebug.Printf("Files file: %q", fn) + a, err := bbutil.ReadFileLines(fn) if err != nil { - return fmt.Errorf("getFiles can't read %q: %v", fn, err) + return fmt.Errorf("getFiles can't load %q: %v", fn, err) } + if !sort.StringsAreSorted(a) { + return fmt.Errorf("file corrupt. Lines not sorted: %v", fn) + } + bx.Files = a - c := strings.TrimSpace(string(b)) - - bx.Files = strings.Split(c, "\n") - bx.FilesSet = make(map[string]bool) + bx.FilesSet = make(map[string]bool, len(bx.Files)) for _, s := range bx.Files { bx.FilesSet[s] = true } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 194a7076..e3870bb8 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -7,6 +7,7 @@ import ( "fmt" "os" "path/filepath" + "sort" "strconv" "strings" @@ -15,8 +16,36 @@ import ( ) // AdminAdd adds admins. -func (bx *Box) AdminAdd([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: AdminAdd") +func (bx *Box) AdminAdd(nom string, sdir string) error { + err := bx.getAdmins() + if err != nil { + return err + } + + fmt.Printf("ADMINS=%q\n", bx.Admins) + + // Check for duplicates. + if i := sort.SearchStrings(bx.Admins, nom); i < len(bx.Admins) && bx.Admins[i] == nom { + return fmt.Errorf("Admin %v already an admin", nom) + } + + sugg, err := bx.Crypter.AddNewKey(nom, sdir, bx.ConfigDir) + if err != nil { + return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) + } + + // TODO(tlim): Try the json file. + + // Try the legacy file: + fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") + bx.logDebug.Printf("Admins file: %q", fn) + err = bbutil.AddLinesToSortedFile(fn, nom) + if err != nil { + return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err) + } + + bx.Vcs.SuggestTracking(bx.RepoBaseDir, "NEW ADMIN: "+nom, sugg) + return nil } // AdminList lists the admin id's. @@ -126,34 +155,91 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { names = bx.Files } - var suggest []string + var suggestMsg []string + var suggestFiles []string for _, name := range names { fmt.Printf("========== ENCRYPTING %q\n", name) if !bx.FilesSet[name] { bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) + continue } - err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) + s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { bx.logErr.Printf("Failed to encrypt %q: %v", name, err) continue } - suggest = append(suggest, fmt.Sprintf("Updated: %q", name)) + suggestMsg = append(suggestMsg, name) + suggestFiles = append(suggestFiles, s) if shred { - bx.Shred(name) + bx.Shred([]string{name}) } } - bx.Vcs.SuggestTracking(bx.RepoBaseDir, - strings.Join(names, "\n")+"\n", - names..., - ) + if len(suggestFiles) != 0 { + bx.Vcs.SuggestTracking(bx.RepoBaseDir, + "ENCRYPTED "+strings.Join(suggestMsg, " "), + suggestFiles, + ) + } return nil } // FileAdd enrolls files. -func (bx *Box) FileAdd(names []string, overwrite bool) error { - return fmt.Errorf("NOT IMPLEMENTED: FileAdd") +func (bx *Box) FileAdd(names []string, shred bool) error { + bx.logDebug.Printf("FileAdd(shred=%v, %v)", shred, names) + + // Check for dups. + // Encrypt them all. + // If that succeeds, add to the blackbox-files.txt file. + // (optionally) shred the plaintext. + + err := bx.getAdmins() + if err != nil { + return err + } + err = bx.getFiles() + if err != nil { + return err + } + + // Check for duplicates. + for _, n := range names { + if i := sort.SearchStrings(bx.Files, n); i < len(bx.Files) && bx.Files[i] == n { + return fmt.Errorf("file %q already registered", n) + } + } + + // Encrypt + var encryptedNames []string + for _, name := range names { + s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) + if err != nil { + return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) + } + encryptedNames = append(encryptedNames, s) + } + + // TODO(tlim): Try the json file. + + // Try the legacy file: + fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") + bx.logDebug.Printf("Files file: %q", fn) + err = bbutil.AddLinesToSortedFile(fn, names...) + if err != nil { + return fmt.Errorf("could not update file (%q,%q): %v", fn, names, err) + } + + err = bx.Shred(names) + if err != nil { + bx.logErr.Printf("Error while shredding: %v", err) + } + + bx.Vcs.SuggestTracking( + bx.RepoBaseDir, + "NEW FILES: "+strings.Join(names, " "), + encryptedNames) + return nil } // FileList lists the files. @@ -242,7 +328,7 @@ func (bx *Box) Init(yes, vcsname string) error { ) bx.Vcs.SuggestTracking(bx.RepoBaseDir, "INITIALIZE BLACKBOX", - bbadminsRel, bbfilesRel, + []string{bbadminsRel, bbfilesRel}, ) return nil @@ -254,8 +340,19 @@ func (bx *Box) Reencrypt(names []string) error { } // Shred shreds files. -func (bx *Box) Shred(names ...string) error { - return fmt.Errorf("NOT IMPLEMENTED: Shred") +func (bx *Box) Shred(names []string) error { + + err := bx.getFiles() + // Calling getFiles() has the benefit of making sure we are in a repo. + if err != nil { + return err + } + + if len(names) == 0 { + names = bx.Files + } + + return bbutil.ShredFiles(names) } // Status prints the status of files. diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 2eef213e..ce2fc24c 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -1,6 +1,7 @@ package gnupg import ( + "fmt" "log" "os/exec" "syscall" @@ -69,13 +70,15 @@ func (crypt CrypterHandle) Decrypt(filename string, umask int, overwrite bool) e } // Encrypt name, overwriting name+".gpg" -func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []string) error { +func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []string) (string, error) { + crypt.logDebug.Printf("Encrypt(%q, %d, %q)", filename, umask, receivers) + encrypted := filename + ".gpg" a := []string{ "--use-agent", "--yes", "--trust-model=always", "--encrypt", - "-o", filename + ".gpg", + "-o", encrypted, } for _, f := range receivers { a = append(a, "-r", f) @@ -87,5 +90,41 @@ func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []strin err := bbutil.RunBash(crypt.GPGCmd, a...) syscall.Umask(oldumask) - return err + return encrypted, err +} + +// AddNewKey extracts keyname from sourcedir's GnuPG chain to destdir keychain. +// It returns a list of files that may have changed. +func (crypt CrypterHandle) AddNewKey(keyname, sourcedir, destdir string) ([]string, error) { + + // $GPG --homedir="$2" --export -a "$KEYNAME" >"$pubkeyfile" + args := []string{ + "--export", + "-a", + } + if sourcedir != "" { + args = append(args, "--homedir", sourcedir) + } + args = append(args, keyname) + crypt.logDebug.Printf("ADDNEWKEY: Extracting key=%v: gpg, %v\n", keyname, args) + pubkey, err := bbutil.RunBashOutput("gpg", args...) + if len(pubkey) == 0 { + return nil, fmt.Errorf("Nothing found when %q exported from %q", keyname, sourcedir) + } + + // $GPG --no-permission-warning --homedir="$KEYRINGDIR" --import "$pubkeyfile" + args = []string{ + "--no-permission-warning", + "--homedir", destdir, + "--import", + } + crypt.logDebug.Printf("ADDNEWKEY: Importing: gpg %v\n", args) + err = bbutil.RunBashInput(pubkey, "gpg", args...) + if err != nil { + return nil, fmt.Errorf("AddNewKey failed: %w", err) + } + + // Suggest: ${pubring_path} trustdb.gpg blackbox-admins.txt + + return nil, nil } diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 129b31a4..3b9d4b76 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -67,10 +67,10 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { } // SuggestTracking tells the VCS to suggest the user commit these files. -func (v VcsHandle) SuggestTracking(repobasedir string, message string, files ...string) error { - fmt.Print(` +func (v VcsHandle) SuggestTracking(repobasedir string, message string, files []string) error { + fmt.Printf(` NEXT STEP: You need to manually check these in: - git commit -m'INITIALIZE BLACKBOX'`) + git commit -m%q`, message) for _, file := range files { fmt.Print(fmt.Sprintf(" %q", file)) } diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 38d40b32..86efc1b2 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -42,7 +42,7 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { } // SuggestTracking tells the VCS to suggest the user commit these files. -func (v VcsHandle) SuggestTracking(repobasedir string, message string, files ...string) error { +func (v VcsHandle) SuggestTracking(repobasedir string, message string, files []string) error { return nil } From b17a11abd47cb7b29c1377b30891a149f68be6d1 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 15 Jun 2020 21:29:46 -0400 Subject: [PATCH 40/69] Cat works! --- cmd/blackbox/cli.go | 4 +- cmd/blackbox/drive.go | 13 +- go.mod | 1 - go.sum | 2 - integrationTest/NOTES.txt | 7 ++ integrationTest/integration_test.go | 40 +++++-- integrationTest/ithelpers.go | 22 ++-- integrationTest/run-tests-verbose.sh | 3 - integrationTest/run-tests.sh | 3 - integrationTest/test_data/basic-status.txt | 5 + integrationTest/test_data/reencrypt-plain.txt | 1 + models/crypters.go | 2 + pkg/bbutil/filestats.go | 7 +- pkg/bbutil/rbio_test.go | 21 ++++ pkg/bbutil/runbash.go | 15 ++- pkg/box/boxutils.go | 8 ++ pkg/box/verbs.go | 113 +++++++++++++++--- pkg/crypters/gnupg/gnupg.go | 26 ++++ pkg/vcs/git/git.go | 12 ++ 19 files changed, 252 insertions(+), 53 deletions(-) delete mode 100644 integrationTest/run-tests-verbose.sh delete mode 100644 integrationTest/run-tests.sh create mode 100644 integrationTest/test_data/basic-status.txt create mode 100644 integrationTest/test_data/reencrypt-plain.txt create mode 100644 pkg/bbutil/rbio_test.go diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index d33d2a22..073b49ed 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -190,10 +190,12 @@ func flags() *cli.App { { Name: "reencrypt", - Usage: "Decrypt then re-encrypt files", + Usage: "Decrypt then re-encrypt files (erases any plaintext)", Category: "ADMINISTRATIVE", Flags: []cli.Flag{ &cli.BoolFlag{Name: "all", Usage: "All registered files"}, + &cli.BoolFlag{Name: "overwrite", Usage: "Overwrite plaintext if it exists"}, + &cli.BoolFlag{Name: "agentcheck", Usage: "Do not check for gpg-agent when using --all"}, }, Action: func(c *cli.Context) error { return cmdReencrypt(c) }, }, diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index f0ba4f93..76085d7f 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -154,8 +154,19 @@ func cmdReencrypt(c *cli.Context) error { if err := allOrSomeFiles(c); err != nil { return err } + + // The default for --agentcheck is off normally, and on when using --all. + pauseNeeded := c.Bool("all") + // If the user used the flag, abide by it. + if c.IsSet("agentcheck") { + pauseNeeded = c.Bool("agentcheck") + } + bx := box.NewFromFlags(c) - return bx.Reencrypt(c.Args().Slice()) + return bx.Reencrypt(c.Args().Slice(), + c.Bool("overwrite"), + pauseNeeded, + ) } func cmdShred(c *cli.Context) error { diff --git a/go.mod b/go.mod index 9e66cb0f..7f43a864 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,6 @@ module github.com/StackExchange/blackbox/v2 go 1.14 require ( - github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965 github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/mattn/go-runewidth v0.0.9 // indirect github.com/olekukonko/tablewriter v0.0.4 diff --git a/go.sum b/go.sum index 23c9d9b4..76c865f8 100644 --- a/go.sum +++ b/go.sum @@ -1,6 +1,4 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965 h1:XlKWFR4yjGVmKa6AoUntvu0G2in7DGcbd+dQFGuoA6s= -github.com/TomOnTime/hind v0.0.0-20200614150227-473623602965/go.mod h1:k+NmgLkBQBiBOGbFJkqGcCCb2rD5OnsWQHIkza8PAWQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index ff74278f..592e70f1 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -85,3 +85,10 @@ Then a shell script should run various combinations of VCS and crypters. # blackbox file list blah # blackbox shred list --all # blackbox shred list blah + + + +rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -run +rm -rf /tmp/bbhome-* && go test -run + +( gbb && cd cmd/blackbox && go install ) && blackbox diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 632c74ff..e9157063 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -80,8 +80,8 @@ func TestList(t *testing.T) { runBB(t, "init", "yes") createDummyFilesAdmin(t) - checkOutput(t, "admin", "list", "000-admin-list.txt") - checkOutput(t, "file", "list", "000-file-list.txt") + checkOutput("000-admin-list.txt", t, "admin", "list") + checkOutput("000-file-list.txt", t, "file", "list") invalidArgs(t, "file", "list", "extra") invalidArgs(t, "admin", "list", "extra") @@ -93,7 +93,7 @@ func TestStatus(t *testing.T) { runBB(t, "init", "yes") createFilesStatus(t) - checkOutput(t, "status", "000-status.txt") + checkOutput("000-status.txt", t, "status") } func TestShred(t *testing.T) { @@ -101,7 +101,7 @@ func TestShred(t *testing.T) { makeHomeDir(t, "shred") runBB(t, "init", "yes") - makeFile(t, "shredme.txt", "File with SHREDME in it.") + makeFile(t, "shredme.txt", "File with SHREDME in it.\n") assertFileExists(t, "shredme.txt") runBB(t, "shred", "shredme.txt") assertFileMissing(t, "shredme.txt") @@ -113,8 +113,7 @@ func TestStatus_notreg(t *testing.T) { runBB(t, "init", "yes") createFilesStatus(t) - checkOutput(t, "status", "status-ENCRYPTED.txt", "blah.txt", - "status-noreg.txt") + checkOutput("status-noreg.txt", t, "status", "status-ENCRYPTED.txt", "blah.txt") } // TestBasicCommands tests of the basic functions, using a fake homedir and repo. @@ -167,13 +166,30 @@ func TestBasic(t *testing.T) { assertFileContents(t, "foo.txt", plaintextFoo) // reencrypt + phase("Alice reencrypts") + checkOutput("basic-status.txt", t, "status") + runBB(t, "reencrypt", "--overwrite", "foo.txt") - // cat - - // diff - - // shred - + // cat & shred + phase("Alice cats") + makeFile(t, "foo.txt", plaintextFoo) + // foo.txt=plain result=plain + runBB(t, "encrypt", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") + // foo.txt=missing result=plain + runBB(t, "shred", "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") + // foo.txt=altered result=altered + phase("Alice edits then cats") + plainAltered := "I am the altered file!\n" + makeFile(t, "foo.txt", plainAltered) + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") } // func TestAliceAndBob(t *testing.T) { diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 98f1e009..7c1e7795 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -16,7 +16,6 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/vcs" _ "github.com/StackExchange/blackbox/v2/pkg/vcs/_all" - "github.com/TomOnTime/hind" "github.com/andreyvit/diff" ) @@ -181,10 +180,10 @@ func addLineSorted(t *testing.T, filename, line string) { } } -func makeFile(t *testing.T, name string, lines ...string) { +func makeFile(t *testing.T, name string, content string) { t.Helper() - err := ioutil.WriteFile(name, []byte(strings.Join(lines, "\n")), 0o666) + err := ioutil.WriteFile(name, []byte(content), 0o666) if err != nil { t.Fatalf("makeFile can't create %q: %v", name, err) } @@ -203,27 +202,30 @@ var originPath string // CWD when program started. // checkOutput runs blackbox with args, the last arg is the filename // of the expected output. Error if output is not expected. -func checkOutput(t *testing.T, args ...string) { +func checkOutput(name string, t *testing.T, args ...string) { t.Helper() - // Pop off the last arg. Use it as the filename. - name, args := args[hind.G(args)], args[:hind.G(args)] - cmd := exec.Command(PathToBlackBox(), args...) cmd.Stdin = nil cmd.Stdout = nil cmd.Stderr = os.Stderr - got, err := cmd.Output() + var gb []byte + gb, err := cmd.Output() if err != nil { t.Fatal(fmt.Errorf("checkOutput(%q): %w", args, err)) } + got := string(gb) - want, err := ioutil.ReadFile(filepath.Join(originPath, "test_data", name)) + wb, err := ioutil.ReadFile(filepath.Join(originPath, "test_data", name)) if err != nil { t.Fatalf("checkOutput can't read %v: %v", name, err) } + want := string(wb) + + //fmt.Printf("CHECKOUTPUT g: %v\n", got) + //fmt.Printf("CHECKOUTPUT w: %v\n", want) - if w, g := string(want), string(got); w != g { + if g, w := got, want; g != w { t.Errorf("checkOutput(%q) mismatch (-got +want):\n%s", args, diff.LineDiff(g, w)) } diff --git a/integrationTest/run-tests-verbose.sh b/integrationTest/run-tests-verbose.sh deleted file mode 100644 index f0e35831..00000000 --- a/integrationTest/run-tests-verbose.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose "$@" diff --git a/integrationTest/run-tests.sh b/integrationTest/run-tests.sh deleted file mode 100644 index 56496958..00000000 --- a/integrationTest/run-tests.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -rm -rf /tmp/bbhome-* && go test "$@" diff --git a/integrationTest/test_data/basic-status.txt b/integrationTest/test_data/basic-status.txt new file mode 100644 index 00000000..134241a3 --- /dev/null +++ b/integrationTest/test_data/basic-status.txt @@ -0,0 +1,5 @@ ++-----------+---------+ +| STATUS | NAME | ++-----------+---------+ +| ENCRYPTED | foo.txt | ++-----------+---------+ diff --git a/integrationTest/test_data/reencrypt-plain.txt b/integrationTest/test_data/reencrypt-plain.txt new file mode 100644 index 00000000..dcb5c6c4 --- /dev/null +++ b/integrationTest/test_data/reencrypt-plain.txt @@ -0,0 +1 @@ +I am the foo.txt file! diff --git a/models/crypters.go b/models/crypters.go index cab4ebbd..f3c24f94 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -8,6 +8,8 @@ type Crypter interface { Decrypt(filename string, umask int, overwrite bool) error // Encrypt name, overwriting name+".gpg" Encrypt(filename string, umask int, receivers []string) (string, error) + // Cat outputs a file, unencrypting if needed. + Cat(filename string) ([]byte, error) // AddNewKey extracts keyname from sourcedir's GnuPG chain to destdir keychain. AddNewKey(keyname, sourcedir, destdir string) ([]string, error) } diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 57a4ec28..fb0790c8 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -67,16 +67,13 @@ func ShredFiles(names []string) error { // TODO(tlim) DO the shredding in parallel like in v1. for _, n := range names { - fmt.Printf("SHREDDING (%q, %q): %q\n", path, flag, n) + fmt.Printf("========== SHREDDING (%q, %q): %q\n", path, flag, n) e := RunBash(path, flag, n) if e != nil { err = e - fmt.Printf("ERROR: %v", e) - } else { - fmt.Println() + fmt.Printf("ERROR: %v\n", e) } } - fmt.Println("DONE.") return err } diff --git a/pkg/bbutil/rbio_test.go b/pkg/bbutil/rbio_test.go new file mode 100644 index 00000000..6757c391 --- /dev/null +++ b/pkg/bbutil/rbio_test.go @@ -0,0 +1,21 @@ +package bbutil + +import ( + "testing" +) + +func TestRunBashInputOutput(t *testing.T) { + + in := "This is a test of the RBIO system.\n" + bin := []byte(in) + + out, err := RunBashInputOutput(bin, "cat") + sout := string(out) + if err != nil { + t.Error(err) + } + + if in != sout { + t.Errorf("not equal %q %q", in, out) + } +} diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index 70e61055..d1e6cc54 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -48,5 +48,18 @@ func RunBashInput(input string, command string, args ...string) error { if err != nil { return fmt.Errorf("RunBashInput err=%w", err) } - return err + return nil +} + +// RunBashInputOutput runs a Bash command, sends input on stdin. +func RunBashInputOutput(input []byte, command string, args ...string) ([]byte, error) { + + cmd := exec.Command(command, args...) + cmd.Stdin = bytes.NewBuffer(input) + cmd.Stderr = os.Stderr + out, err := cmd.Output() + if err != nil { + return nil, fmt.Errorf("RunBashInputOutput err=%w", err) + } + return out, nil } diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 36f9fe33..40c285ba 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -216,3 +216,11 @@ func gpgAgentNotice() { input := bufio.NewScanner(os.Stdin) input.Scan() } + +func shouldWeOverwrite() { + fmt.Println() + fmt.Println("WARNING: This will overwrite any unencrypted files laying about.") + fmt.Print("Press CTRL-C now to stop. ENTER to continue: ") + input := bufio.NewScanner(os.Stdin) + input.Scan() +} diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index e3870bb8..ff7a333b 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -67,8 +67,15 @@ func (bx *Box) AdminRemove([]string) error { } // Cat outputs a file, unencrypting if needed. -func (bx *Box) Cat([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: Cat") +func (bx *Box) Cat(names []string) error { + for _, name := range names { + out, err := bx.Crypter.Cat(name) + if err != nil { + return err + } + fmt.Print(string(out)) + } + return nil } // Decrypt decrypts a file. @@ -96,6 +103,10 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup if len(names) == 0 { names = bx.Files } + return decryptMany(bx, names, overwrite, groupchange, gid) +} + +func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid int) error { for _, name := range names { fmt.Printf("========== DECRYPTING %q\n", name) if !bx.FilesSet[name] { @@ -124,7 +135,6 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup os.Chown(name, -1, gid) } } - return nil } @@ -155,6 +165,22 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { names = bx.Files } + suggestMsg, suggestFiles, err := encryptMany(bx, names, shred) + if err != nil { + return err + } + + if len(suggestFiles) != 0 { + bx.Vcs.SuggestTracking(bx.RepoBaseDir, + "ENCRYPTED "+strings.Join(suggestMsg, " "), + suggestFiles, + ) + } + + return nil +} + +func encryptMany(bx *Box, names []string, shred bool) ([]string, []string, error) { var suggestMsg []string var suggestFiles []string for _, name := range names { @@ -163,6 +189,10 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) continue } + if !bbutil.FileExistsOrProblem(name) { + bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name) + continue + } s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { bx.logErr.Printf("Failed to encrypt %q: %v", name, err) @@ -174,15 +204,7 @@ func (bx *Box) Encrypt(names []string, umask int, shred bool) error { bx.Shred([]string{name}) } } - - if len(suggestFiles) != 0 { - bx.Vcs.SuggestTracking(bx.RepoBaseDir, - "ENCRYPTED "+strings.Join(suggestMsg, " "), - suggestFiles, - ) - } - - return nil + return suggestMsg, suggestFiles, nil } // FileAdd enrolls files. @@ -335,8 +357,71 @@ func (bx *Box) Init(yes, vcsname string) error { } // Reencrypt decrypts and reencrypts files. -func (bx *Box) Reencrypt(names []string) error { - return fmt.Errorf("NOT IMPLEMENTED: Reencrypt") +func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { + + err := bx.getAdmins() + if err != nil { + return err + } + err = bx.getFiles() + if err != nil { + return err + } + if len(names) == 0 { + names = bx.Files + } + + if bulkpause { + gpgAgentNotice() + } + + fmt.Println("========== blackbox administrators are:") + bx.AdminList() + + if overwrite { + for _, n := range names { + if bbutil.FileExistsOrProblem(n) { + bbutil.ShredFiles([]string{n}) + } + } + } else { + warned := false + for _, n := range names { + if bbutil.FileExistsOrProblem(n) { + if !warned { + fmt.Printf("========== Shred these files?\n") + warned = true + } + fmt.Println("SHRED?", n) + } + } + if warned { + shouldWeOverwrite() + } + } + + // Decrypt + err = decryptMany(bx, names, overwrite, false, 0) + if err != nil { + return fmt.Errorf("reencrypt failed decrypt: %w", err) + } + suggestMsg, suggestFiles, err := encryptMany(bx, names, false) + if err != nil { + return fmt.Errorf("reencrypt failed encrypt: %w", err) + } + err = bbutil.ShredFiles(names) + if err != nil { + return fmt.Errorf("reencrypt failed shred: %w", err) + } + + if len(suggestFiles) != 0 { + bx.Vcs.SuggestTracking(bx.RepoBaseDir, + "ENCRYPTED "+strings.Join(suggestMsg, " "), + suggestFiles, + ) + } + + return nil } // Shred shreds files. diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index ce2fc24c..165ead9b 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -2,7 +2,9 @@ package gnupg import ( "fmt" + "io/ioutil" "log" + "os" "os/exec" "syscall" @@ -69,6 +71,30 @@ func (crypt CrypterHandle) Decrypt(filename string, umask int, overwrite bool) e return err } +// Cat returns the plaintext or, if it is missing, the decrypted cyphertext. +func (crypt CrypterHandle) Cat(filename string) ([]byte, error) { + + a := []string{ + "--use-agent", + "-q", + "--decrypt", + } + + // TODO(tlim): This assumes the entire gpg file fits in memory. If + // this becomes a problem, re-implement this using exec Cmd.StdinPipe() + // and feed the input in chunks. + in, err := ioutil.ReadFile(filename + ".gpg") + if err != nil { + if os.IsNotExist(err) { + // Encrypted file doesn't exit? Return the plaintext. + return ioutil.ReadFile(filename) + } + return nil, err + } + + return bbutil.RunBashInputOutput(in, crypt.GPGCmd, a...) +} + // Encrypt name, overwriting name+".gpg" func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []string) (string, error) { crypt.logDebug.Printf("Encrypt(%q, %d, %q)", filename, umask, receivers) diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 3b9d4b76..e5277ff2 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -78,6 +78,18 @@ NEXT STEP: You need to manually check these in: return nil } +// Add makes a file visible to the VCS (like "git add"). +func (v VcsHandle) Add(repobasedir string, files []string) error { + + // TODO(tlim): Make sure that files are within repobasedir. + + var gpgnames []string + for _, n := range files { + gpgnames = append(gpgnames, n+".gpg") + } + return bbutil.RunBash("git", append([]string{"add"}, gpgnames...)...) +} + // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. From a47bc5d2c39b2411b2915aba36ddc8039c8a5b75 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 15 Jun 2020 22:41:51 -0400 Subject: [PATCH 41/69] cat and other commands now work. shell commands replaced --- bin/blackbox_addadmin.v2 | 7 +++ bin/blackbox_cat.v2 | 9 ++++ bin/blackbox_decrypt_all_files.v2 | 7 +++ bin/blackbox_decrypt_file.v2 | 7 +++ bin/blackbox_edit.v2 | 9 ++++ bin/blackbox_edit_end.v2 | 6 +++ bin/blackbox_edit_start.v2 | 6 +++ bin/blackbox_initialize.v2 | 11 +++++ bin/blackbox_list_admins.v2 | 6 +++ bin/blackbox_list_files.v2 | 6 +++ bin/blackbox_listadmins.v2 | 6 +++ bin/blackbox_postdeploy.v2 | 13 ++++++ bin/blackbox_register_new_file.v2 | 6 +++ bin/blackbox_removeadmin.v2 | 7 +++ bin/blackbox_shred_all_files.v2 | 6 +++ bin/blackbox_update_all_files.v2 | 6 +++ bin/blackbox_view.v2 | 6 +++ bin/blackbox_whatsnew.v2 | 49 +++++++++++++++++++++ cmd/blackbox/cli.go | 6 +++ cmd/blackbox/drive.go | 2 +- pkg/box/box.go | 4 +- pkg/box/boxutils.go | 12 +++++ pkg/box/verbs.go | 73 +++++++++++++++++++++++++++++-- 23 files changed, 264 insertions(+), 6 deletions(-) create mode 100755 bin/blackbox_addadmin.v2 create mode 100755 bin/blackbox_cat.v2 create mode 100755 bin/blackbox_decrypt_all_files.v2 create mode 100755 bin/blackbox_decrypt_file.v2 create mode 100755 bin/blackbox_edit.v2 create mode 100755 bin/blackbox_edit_end.v2 create mode 100755 bin/blackbox_edit_start.v2 create mode 100755 bin/blackbox_initialize.v2 create mode 100755 bin/blackbox_list_admins.v2 create mode 100755 bin/blackbox_list_files.v2 create mode 100755 bin/blackbox_listadmins.v2 create mode 100755 bin/blackbox_postdeploy.v2 create mode 100755 bin/blackbox_register_new_file.v2 create mode 100755 bin/blackbox_removeadmin.v2 create mode 100755 bin/blackbox_shred_all_files.v2 create mode 100755 bin/blackbox_update_all_files.v2 create mode 100755 bin/blackbox_view.v2 create mode 100755 bin/blackbox_whatsnew.v2 diff --git a/bin/blackbox_addadmin.v2 b/bin/blackbox_addadmin.v2 new file mode 100755 index 00000000..cb9a0a2b --- /dev/null +++ b/bin/blackbox_addadmin.v2 @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# +# blackbox_addadmin -- Add an admin to the system +# + +exec blackbox admin add "$@" diff --git a/bin/blackbox_cat.v2 b/bin/blackbox_cat.v2 new file mode 100755 index 00000000..363f6f3c --- /dev/null +++ b/bin/blackbox_cat.v2 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# +# blackbox_cat -- cat a file from the .gpg, not the plaintext. +# +# NOTE: This is different than in v1 where the plaintext was always +# used if available. +# +exec blackbox cat "$@" diff --git a/bin/blackbox_decrypt_all_files.v2 b/bin/blackbox_decrypt_all_files.v2 new file mode 100755 index 00000000..ae7f4b50 --- /dev/null +++ b/bin/blackbox_decrypt_all_files.v2 @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# +# blackbox_decrypt_all_files -- Decrypt all blackbox files (INTERACTIVE). +# + +exec blackbox decrypt --all --agentcheck=true --overwrite "@" diff --git a/bin/blackbox_decrypt_file.v2 b/bin/blackbox_decrypt_file.v2 new file mode 100755 index 00000000..bd1dd874 --- /dev/null +++ b/bin/blackbox_decrypt_file.v2 @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# +# blackbox_decrypt_file -- Decrypt one or more blackbox files. +# + +exec blackbox decrypt --overwrite "$@" diff --git a/bin/blackbox_edit.v2 b/bin/blackbox_edit.v2 new file mode 100755 index 00000000..101d7f0d --- /dev/null +++ b/bin/blackbox_edit.v2 @@ -0,0 +1,9 @@ +#!/usr/bin/env bash + +# +# blackbox_edit -- Decrypt a file temporarily for edition, then re-encrypts it again +# + +# NB(tlim): In v1 this offers to register any non-registered files. + +exec blackbox edit "$@" diff --git a/bin/blackbox_edit_end.v2 b/bin/blackbox_edit_end.v2 new file mode 100755 index 00000000..7ca8294b --- /dev/null +++ b/bin/blackbox_edit_end.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_edit_end -- Re-encrypt file after edits. +# +exec blackbox encrypt --shred "$@" diff --git a/bin/blackbox_edit_start.v2 b/bin/blackbox_edit_start.v2 new file mode 100755 index 00000000..8710aeb9 --- /dev/null +++ b/bin/blackbox_edit_start.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_edit_start -- Decrypt a file for editing. +# +exec blackbox decrypt "$@" diff --git a/bin/blackbox_initialize.v2 b/bin/blackbox_initialize.v2 new file mode 100755 index 00000000..9f17c866 --- /dev/null +++ b/bin/blackbox_initialize.v2 @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +# +# blackbox_initialize -- Enable blackbox for a GIT or HG repo. +# +# +# Example: +# blackbox_initialize +# + +exec blackbox init "$@" diff --git a/bin/blackbox_list_admins.v2 b/bin/blackbox_list_admins.v2 new file mode 100755 index 00000000..d89f9115 --- /dev/null +++ b/bin/blackbox_list_admins.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_list_admins -- List authorized admins +# +exec blackbox admin list diff --git a/bin/blackbox_list_files.v2 b/bin/blackbox_list_files.v2 new file mode 100755 index 00000000..10bea264 --- /dev/null +++ b/bin/blackbox_list_files.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_list_files -- List files that black box is tracking +# +exec blackbox file list diff --git a/bin/blackbox_listadmins.v2 b/bin/blackbox_listadmins.v2 new file mode 100755 index 00000000..cff41187 --- /dev/null +++ b/bin/blackbox_listadmins.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_listadmins -- List active admins for keyring +# +exec blackbox admin list diff --git a/bin/blackbox_postdeploy.v2 b/bin/blackbox_postdeploy.v2 new file mode 100755 index 00000000..92800032 --- /dev/null +++ b/bin/blackbox_postdeploy.v2 @@ -0,0 +1,13 @@ +#!/usr/bin/env bash + +# +# blackbox_postdeploy -- Decrypt all blackbox files. +# +exec blackbox decrypt --all --overwrite --group "$ + + +if [[ "$1" == "" ]]; then + blackbox decrypt --all --overwrite +else + blackbox decrypt --all --overwrite --group "$1" +fi diff --git a/bin/blackbox_register_new_file.v2 b/bin/blackbox_register_new_file.v2 new file mode 100755 index 00000000..6c52a6f4 --- /dev/null +++ b/bin/blackbox_register_new_file.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_register_new_file -- Enroll new file(s) in the blackbox system. +# +exec blackbox file add --shred "$@" diff --git a/bin/blackbox_removeadmin.v2 b/bin/blackbox_removeadmin.v2 new file mode 100755 index 00000000..6877c8a5 --- /dev/null +++ b/bin/blackbox_removeadmin.v2 @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# +# blackbox_removeadmin -- Remove an admin to the system +# NOTE: Does not remove admin from the keyring. +# +exec blackbox admin remove "$@" diff --git a/bin/blackbox_shred_all_files.v2 b/bin/blackbox_shred_all_files.v2 new file mode 100755 index 00000000..5659e5ec --- /dev/null +++ b/bin/blackbox_shred_all_files.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_shred_all_files -- shred all decrypted versions of encrypted files +# +exec blackbox shred --all diff --git a/bin/blackbox_update_all_files.v2 b/bin/blackbox_update_all_files.v2 new file mode 100755 index 00000000..9b00879b --- /dev/null +++ b/bin/blackbox_update_all_files.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_update_all_files -- Decrypt then re-encrypt all files. Useful after keys are changed. +# +exec blackbox reencrypt --all --agentcheck diff --git a/bin/blackbox_view.v2 b/bin/blackbox_view.v2 new file mode 100755 index 00000000..580fc322 --- /dev/null +++ b/bin/blackbox_view.v2 @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_view -- Decrypt a file, view it, shred it +# +blackbox cat "$@" | ${PAGER:-less} diff --git a/bin/blackbox_whatsnew.v2 b/bin/blackbox_whatsnew.v2 new file mode 100755 index 00000000..9f85390b --- /dev/null +++ b/bin/blackbox_whatsnew.v2 @@ -0,0 +1,49 @@ +#!/usr/bin/env bash + +# +# blackbox_whatsnew - show what has changed in the last commit for a given file +# + +set -e +source "${0%/*}/_blackbox_common.sh" + +if [[ $# -ne 1 ]] +then + echo "Pass only 1 file at a time" + exit 1 +fi + +fail_if_not_in_repo +gpg_agent_notice + +COLUMNS=`tput cols` +FILE=$1 +GIT="git log --abbrev-commit --pretty=oneline" +CURR_COMMIT=`$GIT $FILE | head -1 | awk '{print $1}'` +PREV_COMMIT=`$GIT ${CURR_COMMIT}~1 $FILE | head -1 | awk '{print $1}'` +# Use colordiff if available +if which colordiff > /dev/null 2>&1 + then DIFF="colordiff" + else DIFF="diff" +fi + +cat_commit() +{ + COMMIT=$1 + git checkout $COMMIT $FILE + echo "[$COMMIT] $FILE" + echo "---------------------" + "${BLACKBOX_HOME}/blackbox_cat" $FILE | sed '/========== PLAINFILE/,/========== EXTRACTING/d' +} + +CURR_CONTENT=`cat_commit $CURR_COMMIT` +PREV_CONTENT=`cat_commit $PREV_COMMIT` +clear + +# For some unknown reason this command executes fine but return exit code 1 +$DIFF -y --width $COLUMNS \ + <(echo "CURRENT" "$CURR_CONTENT" | fold -w $(( $COLUMNS / 2 - 4 )) ) \ + <(echo "PREVIOUS" "$PREV_CONTENT" | fold -w $(( $COLUMNS / 2 - 4 )) ) + +git checkout $CURR_COMMIT $FILE +echo diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index 073b49ed..f3bf430e 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -42,6 +42,12 @@ func flags() *cli.App { Usage: "Use .blackbox-$TEAM as the configdir", EnvVars: []string{"BLACKBOX_TEAM"}, }, + &cli.StringFlag{ + Name: "editor", + Usage: "editor to use", + Value: "vi", + EnvVars: []string{"EDITOR", "BLACKBOX_EDITOR"}, + }, &cli.IntFlag{ Name: "umask", Usage: "umask to set when decrypting", diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index 76085d7f..ff413a50 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -107,7 +107,7 @@ func cmdEncrypt(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Encrypt(c.Args().Slice(), c.Int("umask"), c.Bool("shred")) + return bx.Encrypt(c.Args().Slice(), c.Bool("shred")) } func cmdFileAdd(c *cli.Context) error { diff --git a/pkg/box/box.go b/pkg/box/box.go index 2fc11606..0bd2109e 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -28,7 +28,8 @@ type Box struct { ConfigDir string // Abs path to the .blackbox (or whatever) directory. ConfigDirRel string // Path to the .blackbox (or whatever) directory relative to RepoBaseDir // Settings: - Umask int // umask to set when decrypting + Umask int // umask to set when decrypting + Editor string // Editor to call // Cache of data gathered from .blackbox: Admins []string // If non-empty, the list of admins. Files []string // If non-empty, the list of files. @@ -71,6 +72,7 @@ func NewFromFlags(c *cli.Context) *Box { bx := &Box{ Umask: c.Int("umask"), + Editor: c.String("editor"), Team: c.String("team"), logErr: bblog.GetErr(), logDebug: bblog.GetDebug(c.Bool("verbose")), diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 40c285ba..d2ebc003 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -7,6 +7,7 @@ import ( "os/user" "path/filepath" "strconv" + "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" ) @@ -52,6 +53,17 @@ func FileStatus(name string) (string, error) { return "PLAINERROR", perr } +func anyGpg(names []string) error { + for _, name := range names { + if strings.HasSuffix(name, ".gpg") { + return fmt.Errorf( + "no not specify .gpg files. Specify %q not %q", + strings.TrimSuffix(name, ".gpg"), name) + } + } + return nil +} + // func isChanged(pname string) (bool, error) { // // if .gpg exists but not plainfile: unchanged // // if plaintext exists but not .gpg: changed diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index ff7a333b..0bc8ebed 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -5,6 +5,7 @@ package box import ( "bufio" "fmt" + "io/ioutil" "os" "path/filepath" "sort" @@ -68,8 +69,18 @@ func (bx *Box) AdminRemove([]string) error { // Cat outputs a file, unencrypting if needed. func (bx *Box) Cat(names []string) error { + if err := anyGpg(names); err != nil { + return err + } + for _, name := range names { - out, err := bx.Crypter.Cat(name) + var out []byte + var err error + if _, ok := bx.FilesSet[name]; ok { + out, err = bx.Crypter.Cat(name) + } else { + out, err = ioutil.ReadFile(name) + } if err != nil { return err } @@ -82,6 +93,10 @@ func (bx *Box) Cat(names []string) error { func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup string) error { var err error + if err := anyGpg(names); err != nil { + return err + } + err = bx.getFiles() if err != nil { return err @@ -131,7 +146,10 @@ func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid continue } + // FIXME(tlim): Clone the file perms from the .gpg file to the plaintext file. + if groupchange { + // FIXME(tlim): Also "chmod g+r" the file. os.Chown(name, -1, gid) } } @@ -144,14 +162,42 @@ func (bx *Box) Diff([]string) error { } // Edit unencrypts, calls editor, calls encrypt. -func (bx *Box) Edit([]string) error { - return fmt.Errorf("NOT IMPLEMENTED: Edit") +func (bx *Box) Edit(names []string) error { + + if err := anyGpg(names); err != nil { + return err + } + + err := bx.getFiles() + if err != nil { + return err + } + + for _, name := range names { + if _, ok := bx.FilesSet[name]; ok { + if !bbutil.FileExistsOrProblem(name) { + err := bx.Crypter.Decrypt(name, bx.Umask, false) + if err != nil { + return fmt.Errorf("edit failed %q: %w", name, err) + } + } + } + err := bbutil.RunBash(bx.Editor, name) + if err != nil { + return err + } + } + return nil } // Encrypt encrypts a file. -func (bx *Box) Encrypt(names []string, umask int, shred bool) error { +func (bx *Box) Encrypt(names []string, shred bool) error { var err error + if err = anyGpg(names); err != nil { + return err + } + err = bx.getAdmins() if err != nil { return err @@ -216,6 +262,14 @@ func (bx *Box) FileAdd(names []string, shred bool) error { // If that succeeds, add to the blackbox-files.txt file. // (optionally) shred the plaintext. + // FIXME(tlim): Check if the plaintext is in GIT. If it is, + // remove it from Git and print a warning that they should + // eliminate the history or rotate any secrets. + + if err := anyGpg(names); err != nil { + return err + } + err := bx.getAdmins() if err != nil { return err @@ -224,6 +278,9 @@ func (bx *Box) FileAdd(names []string, shred bool) error { if err != nil { return err } + if err := anyGpg(names); err != nil { + return err + } // Check for duplicates. for _, n := range names { @@ -359,6 +416,10 @@ func (bx *Box) Init(yes, vcsname string) error { // Reencrypt decrypts and reencrypts files. func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { + if err := anyGpg(names); err != nil { + return err + } + err := bx.getAdmins() if err != nil { return err @@ -427,6 +488,10 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { // Shred shreds files. func (bx *Box) Shred(names []string) error { + if err := anyGpg(names); err != nil { + return err + } + err := bx.getFiles() // Calling getFiles() has the benefit of making sure we are in a repo. if err != nil { From fa2a643b8674a247ba412bfc4fe26db583945e78 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 19 Jun 2020 05:03:41 -0400 Subject: [PATCH 42/69] create binv2 --- bin/blackbox_addadmin.v2 => binv2/blackbox_addadmin | 0 bin/blackbox_cat.v2 => binv2/blackbox_cat | 0 .../blackbox_decrypt_all_files | 0 .../blackbox_decrypt_file | 0 binv2/blackbox_deregister_file | 7 +++++++ binv2/blackbox_diff | 6 ++++++ bin/blackbox_edit.v2 => binv2/blackbox_edit | 3 +-- bin/blackbox_edit_end.v2 => binv2/blackbox_edit_end | 0 bin/blackbox_edit_start.v2 => binv2/blackbox_edit_start | 0 bin/blackbox_initialize.v2 => binv2/blackbox_initialize | 4 ++-- bin/blackbox_list_admins.v2 => binv2/blackbox_list_admins | 0 bin/blackbox_list_files.v2 => binv2/blackbox_list_files | 0 bin/blackbox_listadmins.v2 => binv2/blackbox_listadmins | 0 bin/blackbox_postdeploy.v2 => binv2/blackbox_postdeploy | 5 +---- binv2/blackbox_recurse | 6 ++++++ .../blackbox_register_new_file | 0 bin/blackbox_removeadmin.v2 => binv2/blackbox_removeadmin | 0 .../blackbox_shred_all_files | 0 .../blackbox_update_all_files | 0 bin/blackbox_view.v2 => binv2/blackbox_view | 0 bin/blackbox_whatsnew.v2 => binv2/blackbox_whatsnew | 2 ++ 21 files changed, 25 insertions(+), 8 deletions(-) rename bin/blackbox_addadmin.v2 => binv2/blackbox_addadmin (100%) rename bin/blackbox_cat.v2 => binv2/blackbox_cat (100%) rename bin/blackbox_decrypt_all_files.v2 => binv2/blackbox_decrypt_all_files (100%) rename bin/blackbox_decrypt_file.v2 => binv2/blackbox_decrypt_file (100%) create mode 100755 binv2/blackbox_deregister_file create mode 100755 binv2/blackbox_diff rename bin/blackbox_edit.v2 => binv2/blackbox_edit (63%) rename bin/blackbox_edit_end.v2 => binv2/blackbox_edit_end (100%) rename bin/blackbox_edit_start.v2 => binv2/blackbox_edit_start (100%) rename bin/blackbox_initialize.v2 => binv2/blackbox_initialize (64%) rename bin/blackbox_list_admins.v2 => binv2/blackbox_list_admins (100%) rename bin/blackbox_list_files.v2 => binv2/blackbox_list_files (100%) rename bin/blackbox_listadmins.v2 => binv2/blackbox_listadmins (100%) rename bin/blackbox_postdeploy.v2 => binv2/blackbox_postdeploy (59%) create mode 100755 binv2/blackbox_recurse rename bin/blackbox_register_new_file.v2 => binv2/blackbox_register_new_file (100%) rename bin/blackbox_removeadmin.v2 => binv2/blackbox_removeadmin (100%) rename bin/blackbox_shred_all_files.v2 => binv2/blackbox_shred_all_files (100%) rename bin/blackbox_update_all_files.v2 => binv2/blackbox_update_all_files (100%) rename bin/blackbox_view.v2 => binv2/blackbox_view (100%) rename bin/blackbox_whatsnew.v2 => binv2/blackbox_whatsnew (97%) diff --git a/bin/blackbox_addadmin.v2 b/binv2/blackbox_addadmin similarity index 100% rename from bin/blackbox_addadmin.v2 rename to binv2/blackbox_addadmin diff --git a/bin/blackbox_cat.v2 b/binv2/blackbox_cat similarity index 100% rename from bin/blackbox_cat.v2 rename to binv2/blackbox_cat diff --git a/bin/blackbox_decrypt_all_files.v2 b/binv2/blackbox_decrypt_all_files similarity index 100% rename from bin/blackbox_decrypt_all_files.v2 rename to binv2/blackbox_decrypt_all_files diff --git a/bin/blackbox_decrypt_file.v2 b/binv2/blackbox_decrypt_file similarity index 100% rename from bin/blackbox_decrypt_file.v2 rename to binv2/blackbox_decrypt_file diff --git a/binv2/blackbox_deregister_file b/binv2/blackbox_deregister_file new file mode 100755 index 00000000..c0e0fafa --- /dev/null +++ b/binv2/blackbox_deregister_file @@ -0,0 +1,7 @@ +#!/usr/bin/env bash + +# +# blackbox_deregister_file -- Remove a file from the blackbox system. +# + +exec blackbox file remove --safe "$@" diff --git a/binv2/blackbox_diff b/binv2/blackbox_diff new file mode 100755 index 00000000..b0983369 --- /dev/null +++ b/binv2/blackbox_diff @@ -0,0 +1,6 @@ +#!/usr/bin/env bash + +# +# blackbox_diff -- Show all differences between encrypted and plaintext. +# +exec blackbox diff --diff "$@" diff --git a/bin/blackbox_edit.v2 b/binv2/blackbox_edit similarity index 63% rename from bin/blackbox_edit.v2 rename to binv2/blackbox_edit index 101d7f0d..01cdc496 100755 --- a/bin/blackbox_edit.v2 +++ b/binv2/blackbox_edit @@ -3,7 +3,6 @@ # # blackbox_edit -- Decrypt a file temporarily for edition, then re-encrypts it again # - -# NB(tlim): In v1 this offers to register any non-registered files. +# NOTE: In v1 this offers to register any non-registered files. v2 does not. exec blackbox edit "$@" diff --git a/bin/blackbox_edit_end.v2 b/binv2/blackbox_edit_end similarity index 100% rename from bin/blackbox_edit_end.v2 rename to binv2/blackbox_edit_end diff --git a/bin/blackbox_edit_start.v2 b/binv2/blackbox_edit_start similarity index 100% rename from bin/blackbox_edit_start.v2 rename to binv2/blackbox_edit_start diff --git a/bin/blackbox_initialize.v2 b/binv2/blackbox_initialize similarity index 64% rename from bin/blackbox_initialize.v2 rename to binv2/blackbox_initialize index 9f17c866..2ce8e47c 100755 --- a/bin/blackbox_initialize.v2 +++ b/binv2/blackbox_initialize @@ -3,9 +3,9 @@ # # blackbox_initialize -- Enable blackbox for a GIT or HG repo. # -# # Example: # blackbox_initialize -# +# blackbox_initialize yes +# blackbox_initialize yes /path/to/my/gnupg/config exec blackbox init "$@" diff --git a/bin/blackbox_list_admins.v2 b/binv2/blackbox_list_admins similarity index 100% rename from bin/blackbox_list_admins.v2 rename to binv2/blackbox_list_admins diff --git a/bin/blackbox_list_files.v2 b/binv2/blackbox_list_files similarity index 100% rename from bin/blackbox_list_files.v2 rename to binv2/blackbox_list_files diff --git a/bin/blackbox_listadmins.v2 b/binv2/blackbox_listadmins similarity index 100% rename from bin/blackbox_listadmins.v2 rename to binv2/blackbox_listadmins diff --git a/bin/blackbox_postdeploy.v2 b/binv2/blackbox_postdeploy similarity index 59% rename from bin/blackbox_postdeploy.v2 rename to binv2/blackbox_postdeploy index 92800032..f9377a5e 100755 --- a/bin/blackbox_postdeploy.v2 +++ b/binv2/blackbox_postdeploy @@ -3,11 +3,8 @@ # # blackbox_postdeploy -- Decrypt all blackbox files. # -exec blackbox decrypt --all --overwrite --group "$ - - if [[ "$1" == "" ]]; then blackbox decrypt --all --overwrite else - blackbox decrypt --all --overwrite --group "$1" + blackbox decrypt --all --overwrite --group "$1" " fi diff --git a/binv2/blackbox_recurse b/binv2/blackbox_recurse new file mode 100755 index 00000000..9ab4b1fb --- /dev/null +++ b/binv2/blackbox_recurse @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +# proposed space for blackbox recurion...coming soon +set -e +source "${0%/*}/_blackbox_common.sh" + +echo "$REBOBASE" diff --git a/bin/blackbox_register_new_file.v2 b/binv2/blackbox_register_new_file similarity index 100% rename from bin/blackbox_register_new_file.v2 rename to binv2/blackbox_register_new_file diff --git a/bin/blackbox_removeadmin.v2 b/binv2/blackbox_removeadmin similarity index 100% rename from bin/blackbox_removeadmin.v2 rename to binv2/blackbox_removeadmin diff --git a/bin/blackbox_shred_all_files.v2 b/binv2/blackbox_shred_all_files similarity index 100% rename from bin/blackbox_shred_all_files.v2 rename to binv2/blackbox_shred_all_files diff --git a/bin/blackbox_update_all_files.v2 b/binv2/blackbox_update_all_files similarity index 100% rename from bin/blackbox_update_all_files.v2 rename to binv2/blackbox_update_all_files diff --git a/bin/blackbox_view.v2 b/binv2/blackbox_view similarity index 100% rename from bin/blackbox_view.v2 rename to binv2/blackbox_view diff --git a/bin/blackbox_whatsnew.v2 b/binv2/blackbox_whatsnew similarity index 97% rename from bin/blackbox_whatsnew.v2 rename to binv2/blackbox_whatsnew index 9f85390b..996b0b91 100755 --- a/bin/blackbox_whatsnew.v2 +++ b/binv2/blackbox_whatsnew @@ -3,6 +3,8 @@ # # blackbox_whatsnew - show what has changed in the last commit for a given file # +exec blackbox whatsnew "$@" +exit 0 set -e source "${0%/*}/_blackbox_common.sh" From 051800649663935b464be8679d0d151a9c0d36f2 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 19 Jun 2020 13:18:24 -0400 Subject: [PATCH 43/69] integration tests working --- binv2/blackbox_addadmin | 5 - binv2/blackbox_cat | 7 - binv2/blackbox_decrypt_all_files | 5 - binv2/blackbox_decrypt_file | 5 - binv2/blackbox_deregister_file | 5 - binv2/blackbox_diff | 4 - binv2/blackbox_edit | 6 - binv2/blackbox_edit_end | 4 - binv2/blackbox_edit_start | 4 - binv2/blackbox_initialize | 9 - binv2/blackbox_list_admins | 4 - binv2/blackbox_list_files | 4 - binv2/blackbox_listadmins | 4 - binv2/blackbox_postdeploy | 4 - binv2/blackbox_recurse | 6 - binv2/blackbox_register_new_file | 4 - binv2/blackbox_removeadmin | 5 - binv2/blackbox_shred_all_files | 4 - binv2/blackbox_update_all_files | 4 - binv2/blackbox_view | 4 - integrationTest/NOTES.txt | 5 - integrationTest/integration_test.go | 52 ++--- integrationTest/ithelpers.go | 4 + integrationTest/test_data/alice-cat-plain.txt | 1 + pkg/bbutil/filestats.go | 36 +++- pkg/box/verbs.go | 8 +- pkg/crypters/gnupg/gnupg.go | 2 + pkg/tainedname/tainedname.go | 194 ++++++++++++++++++ pkg/tainedname/tainedname_test.go | 52 +++++ pkg/vcs/git/git.go | 3 +- 30 files changed, 315 insertions(+), 139 deletions(-) delete mode 100755 binv2/blackbox_recurse create mode 100644 integrationTest/test_data/alice-cat-plain.txt create mode 100644 pkg/tainedname/tainedname.go create mode 100644 pkg/tainedname/tainedname_test.go diff --git a/binv2/blackbox_addadmin b/binv2/blackbox_addadmin index cb9a0a2b..78242c3f 100755 --- a/binv2/blackbox_addadmin +++ b/binv2/blackbox_addadmin @@ -1,7 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_addadmin -- Add an admin to the system -# - exec blackbox admin add "$@" diff --git a/binv2/blackbox_cat b/binv2/blackbox_cat index 363f6f3c..bde064f3 100755 --- a/binv2/blackbox_cat +++ b/binv2/blackbox_cat @@ -1,9 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_cat -- cat a file from the .gpg, not the plaintext. -# -# NOTE: This is different than in v1 where the plaintext was always -# used if available. -# exec blackbox cat "$@" diff --git a/binv2/blackbox_decrypt_all_files b/binv2/blackbox_decrypt_all_files index ae7f4b50..19f1f9dd 100755 --- a/binv2/blackbox_decrypt_all_files +++ b/binv2/blackbox_decrypt_all_files @@ -1,7 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_decrypt_all_files -- Decrypt all blackbox files (INTERACTIVE). -# - exec blackbox decrypt --all --agentcheck=true --overwrite "@" diff --git a/binv2/blackbox_decrypt_file b/binv2/blackbox_decrypt_file index bd1dd874..ca1f46aa 100755 --- a/binv2/blackbox_decrypt_file +++ b/binv2/blackbox_decrypt_file @@ -1,7 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_decrypt_file -- Decrypt one or more blackbox files. -# - exec blackbox decrypt --overwrite "$@" diff --git a/binv2/blackbox_deregister_file b/binv2/blackbox_deregister_file index c0e0fafa..18ade4a4 100755 --- a/binv2/blackbox_deregister_file +++ b/binv2/blackbox_deregister_file @@ -1,7 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_deregister_file -- Remove a file from the blackbox system. -# - exec blackbox file remove --safe "$@" diff --git a/binv2/blackbox_diff b/binv2/blackbox_diff index b0983369..82be48aa 100755 --- a/binv2/blackbox_diff +++ b/binv2/blackbox_diff @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_diff -- Show all differences between encrypted and plaintext. -# exec blackbox diff --diff "$@" diff --git a/binv2/blackbox_edit b/binv2/blackbox_edit index 01cdc496..f06cfa55 100755 --- a/binv2/blackbox_edit +++ b/binv2/blackbox_edit @@ -1,8 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_edit -- Decrypt a file temporarily for edition, then re-encrypts it again -# -# NOTE: In v1 this offers to register any non-registered files. v2 does not. - exec blackbox edit "$@" diff --git a/binv2/blackbox_edit_end b/binv2/blackbox_edit_end index 7ca8294b..3f44f72e 100755 --- a/binv2/blackbox_edit_end +++ b/binv2/blackbox_edit_end @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_edit_end -- Re-encrypt file after edits. -# exec blackbox encrypt --shred "$@" diff --git a/binv2/blackbox_edit_start b/binv2/blackbox_edit_start index 8710aeb9..449ac3a6 100755 --- a/binv2/blackbox_edit_start +++ b/binv2/blackbox_edit_start @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_edit_start -- Decrypt a file for editing. -# exec blackbox decrypt "$@" diff --git a/binv2/blackbox_initialize b/binv2/blackbox_initialize index 2ce8e47c..0845a202 100755 --- a/binv2/blackbox_initialize +++ b/binv2/blackbox_initialize @@ -1,11 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_initialize -- Enable blackbox for a GIT or HG repo. -# -# Example: -# blackbox_initialize -# blackbox_initialize yes -# blackbox_initialize yes /path/to/my/gnupg/config - exec blackbox init "$@" diff --git a/binv2/blackbox_list_admins b/binv2/blackbox_list_admins index d89f9115..d971bb12 100755 --- a/binv2/blackbox_list_admins +++ b/binv2/blackbox_list_admins @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_list_admins -- List authorized admins -# exec blackbox admin list diff --git a/binv2/blackbox_list_files b/binv2/blackbox_list_files index 10bea264..bd4fae3d 100755 --- a/binv2/blackbox_list_files +++ b/binv2/blackbox_list_files @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_list_files -- List files that black box is tracking -# exec blackbox file list diff --git a/binv2/blackbox_listadmins b/binv2/blackbox_listadmins index cff41187..d971bb12 100755 --- a/binv2/blackbox_listadmins +++ b/binv2/blackbox_listadmins @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_listadmins -- List active admins for keyring -# exec blackbox admin list diff --git a/binv2/blackbox_postdeploy b/binv2/blackbox_postdeploy index f9377a5e..3abdb80a 100755 --- a/binv2/blackbox_postdeploy +++ b/binv2/blackbox_postdeploy @@ -1,8 +1,4 @@ #!/usr/bin/env bash - -# -# blackbox_postdeploy -- Decrypt all blackbox files. -# if [[ "$1" == "" ]]; then blackbox decrypt --all --overwrite else diff --git a/binv2/blackbox_recurse b/binv2/blackbox_recurse deleted file mode 100755 index 9ab4b1fb..00000000 --- a/binv2/blackbox_recurse +++ /dev/null @@ -1,6 +0,0 @@ -#!/usr/bin/env bash -# proposed space for blackbox recurion...coming soon -set -e -source "${0%/*}/_blackbox_common.sh" - -echo "$REBOBASE" diff --git a/binv2/blackbox_register_new_file b/binv2/blackbox_register_new_file index 6c52a6f4..76797931 100755 --- a/binv2/blackbox_register_new_file +++ b/binv2/blackbox_register_new_file @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_register_new_file -- Enroll new file(s) in the blackbox system. -# exec blackbox file add --shred "$@" diff --git a/binv2/blackbox_removeadmin b/binv2/blackbox_removeadmin index 6877c8a5..ede86d19 100755 --- a/binv2/blackbox_removeadmin +++ b/binv2/blackbox_removeadmin @@ -1,7 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_removeadmin -- Remove an admin to the system -# NOTE: Does not remove admin from the keyring. -# exec blackbox admin remove "$@" diff --git a/binv2/blackbox_shred_all_files b/binv2/blackbox_shred_all_files index 5659e5ec..4237cc8b 100755 --- a/binv2/blackbox_shred_all_files +++ b/binv2/blackbox_shred_all_files @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_shred_all_files -- shred all decrypted versions of encrypted files -# exec blackbox shred --all diff --git a/binv2/blackbox_update_all_files b/binv2/blackbox_update_all_files index 9b00879b..00caf9b6 100755 --- a/binv2/blackbox_update_all_files +++ b/binv2/blackbox_update_all_files @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_update_all_files -- Decrypt then re-encrypt all files. Useful after keys are changed. -# exec blackbox reencrypt --all --agentcheck diff --git a/binv2/blackbox_view b/binv2/blackbox_view index 580fc322..04b0060f 100755 --- a/binv2/blackbox_view +++ b/binv2/blackbox_view @@ -1,6 +1,2 @@ #!/usr/bin/env bash - -# -# blackbox_view -- Decrypt a file, view it, shred it -# blackbox cat "$@" | ${PAGER:-less} diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index 592e70f1..e23bcf42 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -1,9 +1,4 @@ - - -Something like... - - This should accept VCS-type and --crypto flags. Then a shell script should run various combinations of VCS and crypters. diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index e9157063..9a663452 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -126,6 +126,9 @@ func TestBasic(t *testing.T) { setup(t) makeHomeDir(t, "Basic") + plaintextFoo := "I am the foo.txt file!\n" + plainAltered := "I am the altered file!\n" + runBB(t, "testing_init") // Runs "git init" or equiv assertFileExists(t, ".git") runBB(t, "init", "yes") // Creates .blackbox or equiv @@ -142,7 +145,6 @@ func TestBasic(t *testing.T) { // encrypt phase("Alice registers foo.txt") - plaintextFoo := "I am the foo.txt file!\n" makeFile(t, "foo.txt", plaintextFoo) runBB(t, "file", "add", "--shred", "foo.txt") //runBB(t, "encrypt", "--shred", "foo.txt") @@ -170,38 +172,36 @@ func TestBasic(t *testing.T) { checkOutput("basic-status.txt", t, "status") runBB(t, "reencrypt", "--overwrite", "foo.txt") - // cat & shred - phase("Alice cats") - makeFile(t, "foo.txt", plaintextFoo) + // Test variations of cat + // foo.txt=plain result=plain + phase("Alice cats plain:plain") + makeFile(t, "foo.txt", plaintextFoo) + assertFileExists(t, "foo.txt") runBB(t, "encrypt", "foo.txt") assertFileExists(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") - checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") - // foo.txt=missing result=plain - runBB(t, "shred", "foo.txt") - assertFileMissing(t, "foo.txt") + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileExists(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") - checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") - // foo.txt=altered result=altered - phase("Alice edits then cats") - plainAltered := "I am the altered file!\n" + + // foo.txt=altered result=plain + phase("Alice cats altered:plain") makeFile(t, "foo.txt", plainAltered) assertFileExists(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") - checkOutput("reencrypt-plain.txt", t, "cat", "foo.txt") -} - -// func TestAliceAndBob(t *testing.T) { -// setupUser(t, "alice", "a") -// setupUser(t, "bob", "b") -// runBB(t, "init") -// runBB(t, "admin", "add", "alice@") - -// FYI: test "admins add" with multiple people. + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") -// Edit requires a name, and doesn't work with --all. -// invalidArgs(t, "edit") -// invalidArgs(t, "edit", "--all") + // foo.txt=missing result=plain + phase("Alice cats missing:plain") + removeFile(t, "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") -// } +} diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 7c1e7795..dd833625 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -180,6 +180,10 @@ func addLineSorted(t *testing.T, filename, line string) { } } +func removeFile(t *testing.T, name string) { + os.RemoveAll(name) +} + func makeFile(t *testing.T, name string, content string) { t.Helper() diff --git a/integrationTest/test_data/alice-cat-plain.txt b/integrationTest/test_data/alice-cat-plain.txt new file mode 100644 index 00000000..dcb5c6c4 --- /dev/null +++ b/integrationTest/test_data/alice-cat-plain.txt @@ -0,0 +1 @@ +I am the foo.txt file! diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index fb0790c8..6b673221 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -50,24 +50,42 @@ func Touch(name string) error { return os.Chtimes(name, currentTime, currentTime) } -// ShredFiles securely erases a list of files. -func ShredFiles(names []string) error { - var path, flag string +var shredPath, shredFlag string + +func shredCmd() (string, string) { + if shredPath != "" { + return shredPath, shredFlag + } + + var path string var err error if path, err = exec.LookPath("shred"); err == nil { - flag = "-u" + shredPath, shredFlag = path, "-u" } else if path, err = exec.LookPath("srm"); err == nil { - flag = "-f" + shredPath, shredFlag = path, "-f" } else if path, err = exec.LookPath("rm"); err == nil { - flag = "-f" - // FIXME(tlim): Test if "rm -P $tempfile" returns a error. - // If it doesn't, flag = "-Pf" + shredPath, shredFlag = path, "-f" + // Does this command support the "-P" flag? + tmpfile, err := ioutil.TempFile("", "rmtest") + defer os.Remove(tmpfile.Name()) // clean up + err = RunBash("rm", "-P", tmpfile.Name()) + if err != nil { + shredFlag = "-Pf" + } } + return shredPath, shredFlag +} + +// ShredFiles securely erases a list of files. +func ShredFiles(names []string) error { + // TODO(tlim) DO the shredding in parallel like in v1. + path, flag := shredCmd() + var err error for _, n := range names { - fmt.Printf("========== SHREDDING (%q, %q): %q\n", path, flag, n) + fmt.Printf("========== SHREDDING: %q\n", n) e := RunBash(path, flag, n) if e != nil { err = e diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 0bc8ebed..ed1b92ea 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -70,6 +70,11 @@ func (bx *Box) AdminRemove([]string) error { // Cat outputs a file, unencrypting if needed. func (bx *Box) Cat(names []string) error { if err := anyGpg(names); err != nil { + return fmt.Errorf("cat: %w", err) + } + + err := bx.getFiles() + if err != nil { return err } @@ -82,7 +87,8 @@ func (bx *Box) Cat(names []string) error { out, err = ioutil.ReadFile(name) } if err != nil { - return err + bx.logErr.Printf("BX_CRY3\n") + return fmt.Errorf("cat: %w", err) } fmt.Print(string(out)) } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 165ead9b..5d770ba4 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -85,10 +85,12 @@ func (crypt CrypterHandle) Cat(filename string) ([]byte, error) { // and feed the input in chunks. in, err := ioutil.ReadFile(filename + ".gpg") if err != nil { + if os.IsNotExist(err) { // Encrypted file doesn't exit? Return the plaintext. return ioutil.ReadFile(filename) } + return nil, err } diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go new file mode 100644 index 00000000..52c142e9 --- /dev/null +++ b/pkg/tainedname/tainedname.go @@ -0,0 +1,194 @@ +package tainedname + +// tainedname -- A string with a Stringer that is shell safe. + +// This goes to great lengths to make sure the String() is pastable. +// Whitespace and shell "special chars" are handled as expected. + +// However to be extra paranoid, unicode is turned into backtick +// printf statements. I don't know anyone that puts unicode in their +// filenames, but I hope they appreciate this. + +// Most people would just use strconv.QuoteToGraphic() but I'm a +// control freak. + +import ( + "fmt" + "strings" +) + +type dubious string + +func New(s string) dubious { + return dubious(s) +} + +type protection int + +const ( + Unknown protection = iota + None // Nothing special + SingleQuote // Requires at least a single quote + DoubleQuote // Can only be in a double-quoted string +) + +var IsAQuote = None // Handled as a special case +var IsSpace = SingleQuote // " " (ascii 32) +var ShellUnsafe = SingleQuote // bash special +var GlobUnsafe = SingleQuote // Could be a glob +var InterpolationUnsafe = SingleQuote // Used in bash string interpolation +var HasBackslash = DoubleQuote // things like \n \t \r \000 \xFF + +func max(i, j protection) protection { + if i > j { + return i + } else { + return j + } +} + +type tabEntry struct { + level protection + fn func(s rune) string +} + +var tab [128]tabEntry + +func init() { + + for i := 0; i <= 31; i++ { // Control chars + tab[i] = tabEntry{HasBackslash, oct()} + } + tab['\t'] = tabEntry{HasBackslash, literal(`\t`)} // Override + tab['\n'] = tabEntry{HasBackslash, literal(`\n`)} // Override + tab['\r'] = tabEntry{HasBackslash, literal(`\r`)} // Override + tab[' '] = tabEntry{IsSpace, same()} + tab['!'] = tabEntry{ShellUnsafe, same()} + tab['"'] = tabEntry{IsAQuote, same()} + tab['#'] = tabEntry{ShellUnsafe, same()} + tab['@'] = tabEntry{InterpolationUnsafe, same()} + tab['$'] = tabEntry{InterpolationUnsafe, same()} + tab['%'] = tabEntry{InterpolationUnsafe, same()} + tab['&'] = tabEntry{ShellUnsafe, same()} + tab['\''] = tabEntry{IsAQuote, same()} + tab['('] = tabEntry{ShellUnsafe, same()} + tab[')'] = tabEntry{ShellUnsafe, same()} + tab['*'] = tabEntry{GlobUnsafe, same()} + tab['+'] = tabEntry{GlobUnsafe, same()} + tab[','] = tabEntry{None, same()} + tab['-'] = tabEntry{None, same()} + tab['.'] = tabEntry{None, same()} + tab['/'] = tabEntry{None, same()} + for i := '0'; i <= '9'; i++ { + tab[i] = tabEntry{None, same()} + } + tab[':'] = tabEntry{InterpolationUnsafe, same()} // ${foo:=default} + tab[';'] = tabEntry{ShellUnsafe, same()} + tab['<'] = tabEntry{ShellUnsafe, same()} + tab['='] = tabEntry{InterpolationUnsafe, same()} // ${foo:=default} + tab['>'] = tabEntry{ShellUnsafe, same()} + tab['?'] = tabEntry{GlobUnsafe, same()} + tab['@'] = tabEntry{InterpolationUnsafe, same()} // ${myarray[@]}; + for i := 'A'; i <= 'Z'; i++ { + tab[i] = tabEntry{None, same()} + } + tab['['] = tabEntry{ShellUnsafe, same()} + tab['\\'] = tabEntry{ShellUnsafe, same()} + tab[']'] = tabEntry{GlobUnsafe, same()} + tab['^'] = tabEntry{GlobUnsafe, same()} + tab['_'] = tabEntry{None, same()} + tab['`'] = tabEntry{ShellUnsafe, same()} + for i := 'a'; i <= 'z'; i++ { + tab[i] = tabEntry{None, same()} + } + tab['{'] = tabEntry{ShellUnsafe, same()} + tab['|'] = tabEntry{ShellUnsafe, same()} + tab['}'] = tabEntry{ShellUnsafe, same()} + tab['~'] = tabEntry{ShellUnsafe, same()} + tab[127] = tabEntry{HasBackslash, oct()} + + // Check our work. All indexes should have been set. + for i, e := range tab { + if e.level == 0 || e.fn == nil { + panic(fmt.Sprintf("tabEntry %d not set!", i)) + } + } + +} + +// literal return this exact string. +func literal(s string) func(s rune) string { + return func(rune) string { return s } +} + +// same converts the rune to a string. +func same() func(r rune) string { + return func(r rune) string { return string(r) } +} + +// oct returns the octal representing the value. +func oct() func(r rune) string { + return func(r rune) string { return fmt.Sprintf(`\%03o`, r) } +} + +// String returns a version of the dirty string that is absolutely +// safe to paste into a command line. +func (dirty dubious) String() string { + if dirty == "" { + return `""` + } + + var b strings.Builder + b.Grow(len(dirty) + 2) + + level := Unknown + unicode := false + for _, r := range dirty { + if r < 128 { + level = max(level, tab[r].level) + b.WriteString(tab[r].fn(r)) + } else { + level = max(level, DoubleQuote) + b.WriteString(escapeRune(r)) + unicode = true + } + } + s := b.String() + + switch level { + case None: + return string(dirty) + case SingleQuote: + // A single quoted string accepts all chars except the single + // quote itself, which must be replaced with: '"'"' + return "'" + strings.Join(strings.Split(s, "'"), `'"'"'`) + "'" + case DoubleQuote: + if unicode { + return "$(printf '" + s + "')" + } else { + return `"` + s + `"` + } + default: + } + // should not happen + return fmt.Sprintf("%q", s) + +} + +// escapeRune returns a string of octal escapes that represent the rune. +func escapeRune(r rune) string { + b := []byte(string(rune(r))) // Convert to the indivdual bytes, utf8-encoded. + // fmt.Printf("rune: len=%d %s %v\n", len(s), s, []byte(s)) + switch len(b) { + case 1: + return fmt.Sprintf(`\%03o`, b[0]) + case 2: + return fmt.Sprintf(`\%03o\%03o`, b[0], b[1]) + case 3: + return fmt.Sprintf(`\%03o\%03o\%03o`, b[0], b[1], b[2]) + case 4: + return fmt.Sprintf(`\%03o\%03o\%03o\%03o`, b[0], b[1], b[2], b[3]) + default: + return string(rune(r)) + } +} diff --git a/pkg/tainedname/tainedname_test.go b/pkg/tainedname/tainedname_test.go new file mode 100644 index 00000000..09dc9ccc --- /dev/null +++ b/pkg/tainedname/tainedname_test.go @@ -0,0 +1,52 @@ +package tainedname + +import ( + "testing" +) + +func TestString(t *testing.T) { + for i, test := range []struct{ data, expected string }{ + {"", `""`}, + {"one", "one"}, + {"two\n", `"two\n"`}, + {"tab tab", `"tab\ttab"`}, + {"tab\ttab", `"tab\ttab"`}, + {"new\nline", `"new\nline"`}, + {"¡que!", `$(printf '\302\241que!')`}, + {"thé", `$(printf 'th\303\251')`}, + {"pound£", `$(printf 'pound\302\243')`}, + {"*.go", `'*.go'`}, + {"rm -rf / ; echo done", `'rm -rf / ; echo done'`}, + {"smile\u263a", `$(printf 'smile\342\230\272')`}, + {"dub\U0001D4E6", `$(printf 'dub\360\235\223\246')`}, + {"four\U0010FFFF", `$(printf 'four\364\217\277\277')`}, + } { + g := New(test.data).String() + if g == test.expected { + t.Logf("%03d: PASSED go(%q) bash: %s\n", i, test.data, test.expected) + } else { + t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) + } + } +} + +func TestEscapeRune(t *testing.T) { + for i, test := range []struct { + data rune + expected string + }{ + {'a', `\141`}, + {'é', `\303\251`}, + {'☺', `\342\230\272`}, + {'글', `\352\270\200`}, + {'𩸽', `\360\251\270\275`}, + //{"\U0010FEDC", `"'\U0010fedc'"`}, + } { + g := escapeRune(test.data) + if g == test.expected { + t.Logf("%03d: PASSED go=(%q) bash=(%s)\n", i, test.data, test.expected) + } else { + t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) + } + } +} diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index e5277ff2..98c85f29 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/tainedname" "github.com/StackExchange/blackbox/v2/pkg/vcs" ) @@ -72,7 +73,7 @@ func (v VcsHandle) SuggestTracking(repobasedir string, message string, files []s NEXT STEP: You need to manually check these in: git commit -m%q`, message) for _, file := range files { - fmt.Print(fmt.Sprintf(" %q", file)) + fmt.Print(" " + tainedname.New(file).String()) } fmt.Println() return nil From b963da03a615c2be32aa5d9af97528168a169724 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 19 Jun 2020 20:59:12 +0000 Subject: [PATCH 44/69] Now works on CentOS 7 --- integrationTest/ithelpers.go | 135 +++++++++++++++++++++++++++++++---- 1 file changed, 120 insertions(+), 15 deletions(-) diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index dd833625..69e5aca9 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -81,7 +81,7 @@ func makeHomeDir(t *testing.T, testname string) { // Make a predictable location. wipe and re-use homedir = "/tmp/bbhome-" + testname os.RemoveAll(homedir) - err = os.Mkdir(homedir, 0o770) + err = os.Mkdir(homedir, 0770) if err != nil { t.Fatal(fmt.Errorf("mk-home %q: %v", homedir, err)) } @@ -94,7 +94,7 @@ func makeHomeDir(t *testing.T, testname string) { logDebug.Printf("TESTING DIR HOME: cd %v\n", homedir) repodir := filepath.Join(homedir, "repo") - err = os.Mkdir(repodir, 0o770) + err = os.Mkdir(repodir, 0770) if err != nil { t.Fatal(fmt.Errorf("mk-repo %q: %v", repodir, err)) } @@ -156,12 +156,12 @@ func createFilesStatus(t *testing.T) { // NB(tlim): commented out. I can't think of an error I can reproduce. // makeFile(t, "status-PLAINERROR.txt", "File with PLAINERROR in it.") // makeFile(t, "status-PLAINERROR.txt.gpg", "Svyr jvgu CYNVAREEBE va vg.") - // setFilePerms(t, "status-PLAINERROR.txt", 0o000) + // setFilePerms(t, "status-PLAINERROR.txt", 0000) // NB(tlim): commented out. I can't think of an error I can reproduce. // makeFile(t, "status-GPGERROR.txt", "File with GPGERROR in it.") // makeFile(t, "status-GPGERROR.txt.gpg", "Svyr jvgu TCTREEBE va vg.") - // setFilePerms(t, "status-GPGERROR.txt.gpg", 0o000) + // setFilePerms(t, "status-GPGERROR.txt.gpg", 0000) time.Sleep(200 * time.Millisecond) @@ -187,7 +187,7 @@ func removeFile(t *testing.T, name string) { func makeFile(t *testing.T, name string, content string) { t.Helper() - err := ioutil.WriteFile(name, []byte(content), 0o666) + err := ioutil.WriteFile(name, []byte(content), 0666) if err != nil { t.Fatalf("makeFile can't create %q: %v", name, err) } @@ -311,7 +311,7 @@ func makeAdmin(t *testing.T, name, fullname, email string) string { if err != nil { t.Fatal(err) } - os.Mkdir(dir, 0o700) + os.Mkdir(dir, 0700) u := &userinfo{ name: name, @@ -324,24 +324,129 @@ func makeAdmin(t *testing.T, name, fullname, email string) string { // GNUPGHOME=u.dir // echo 'pinentry-program' "$(which pinentry-tty)" >> "$GNUPGHOME/gpg-agent.conf" os.Setenv("GNUPGHOME", u.dir) - out, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") + ai, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") + // NB(tlim): It should return something like: + // `GPG_AGENT_INFO=/home/tlimoncelli/.gnupg/S.gpg-agent:18548:1; export GPG_AGENT_INFO;` if err != nil { - t.Fatal(err) + //t.Fatal(err) + } + if strings.HasPrefix(ai, "GPG_AGENT_INFO=") { + u.agentInfo = ai[15:strings.Index(ai, ";")] + os.Setenv("GPG_AGENT_INFO", u.agentInfo) + fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo) + } else { + fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") } - u.agentInfo = strings.TrimSpace(out) os.Setenv("GNUPGHOME", u.dir) // Generate key: - bbutil.RunBash("gpg", - "--homedir", u.dir, - "--batch", - "--passphrase", "", - "--quick-generate-key", u.email, - ) + if hasQuick(t) { + fmt.Println("DISCOVERED: NEW GPG") + fmt.Printf("Generating %q using --qgk\n", u.email) + bbutil.RunBash("gpg", + "--homedir", u.dir, + "--batch", + "--passphrase", "", + "--quick-generate-key", u.email, + ) + if err != nil { + t.Fatal(err) + } + + } else { + + fmt.Println("DISCOVERED: OLD GPG") + fmt.Println("MAKING KEY") + + tmpfile, err := ioutil.TempFile("", "example") + if err != nil { + log.Fatal(err) + } + defer os.Remove(tmpfile.Name()) // clean up + + batch := `%echo Generating a basic OpenPGP key +Key-Type: RSA +Key-Length: 2048 +Subkey-Type: RSA +Subkey-Length: 2048 +Name-Real: ` + u.fullname + ` +Name-Comment: Not for actual use +Name-Email: ` + u.email + ` +Expire-Date: 0 +%pubring ` + filepath.Join(u.dir, `pubring.gpg`) + ` +%secring ` + filepath.Join(u.dir, `secring.gpg`) + ` +# Do a commit here, so that we can later print "done" +%commit +%echo done` + //fmt.Printf("BATCH START\n%s\nBATCH END\n", batch) + fmt.Fprintln(tmpfile, batch) + + // FIXME(tlim): The batch file should include a password, but then + // we need to figure out how to get "blackbox encrypt" and other + // commands to input a password in an automated way. + // To experiment with this, add after "Expire-Date:" a line like: + // Passphrase: kljfhslfjkhsaljkhsdflgjkhsd + // Current status: without that line GPG keys have no passphrase + // and none is requested. + + bbutil.RunBash("gpg", + "--homedir", u.dir, + "--verbose", + "--batch", + "--gen-key", + tmpfile.Name(), + ) + if err != nil { + t.Fatal(err) + } + if err := tmpfile.Close(); err != nil { + log.Fatal(err) + } + + // We do this just to for gpg to create trustdb.gpg + bbutil.RunBash("gpg", + "--homedir", u.dir, + "--list-keys", + ) + if err != nil { + t.Fatal(err) + } + + bbutil.RunBash("gpg", + "--homedir", u.dir, + "--list-secret-keys", + ) + if err != nil { + t.Fatal(err) + } + + } return u.dir } +func hasQuick(t *testing.T) bool { + testing.Init() + fmt.Println("========== Do we have --quick-generate-key?") + err := bbutil.RunBash("gpg2", + "--dry-run", + "--quick-generate-key", + "--batch", + "--passphrase", "", + "foo", "rsa", "encr") + fmt.Println("========== Done") + if err == nil { + return true + } + //fmt.Printf("DISCOVER GPG: %d", err.ExitCode()) + if exitError, ok := err.(*exec.ExitError); ok { + if exitError.ExitCode() == 0 { + return true + } + } + return false +} + func become(t *testing.T, name string) { testing.Init() u := users[name] From b3e9e5278afcdd665f7b8a8a4cf1fbd29c48cf49 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 23 Jun 2020 16:56:17 -0400 Subject: [PATCH 45/69] aRough out Docs --- docs/CODE-LAYOUT.md | 55 ----------- docs/README.md | 61 ++++++++++++ docs/admin-ops.md | 148 ++++++++++++++++++++++++++++++ docs/advanced.md | 46 ++++++++++ docs/alternatives.md | 14 +++ docs/backwards-compatibility.md | 55 +++++++++++ docs/compatibility.md | 70 ++++++++++++++ docs/dev-code-overview.md | 36 ++++++++ docs/dev.md | 36 ++++++++ docs/enable-repo.md | 58 ++++++++++++ docs/encryption.md | 54 +++++++++++ docs/expired-keys.md | 62 +++++++++++++ docs/file-ops.md | 55 +++++++++++ docs/full-command-list.md | 34 +++++++ docs/git-tips.md | 23 +++++ docs/gnupg-tips.md | 31 +++++++ docs/installation.md | 17 ++++ docs/role-accounts.md | 158 ++++++++++++++++++++++++++++++++ docs/subversion-tips.md | 21 +++++ docs/support.md | 16 ++++ docs/user-overview.md | 124 +++++++++++++++++++++++++ docs/why-is-this-important.md | 17 ++++ docs/with-ansible.md | 18 ++++ docs/with-puppet.md | 68 ++++++++++++++ pkg/bbnone/discover.go | 29 ------ 25 files changed, 1222 insertions(+), 84 deletions(-) delete mode 100644 docs/CODE-LAYOUT.md create mode 100644 docs/README.md create mode 100644 docs/admin-ops.md create mode 100644 docs/advanced.md create mode 100644 docs/alternatives.md create mode 100644 docs/backwards-compatibility.md create mode 100644 docs/compatibility.md create mode 100644 docs/dev-code-overview.md create mode 100644 docs/dev.md create mode 100644 docs/enable-repo.md create mode 100644 docs/encryption.md create mode 100644 docs/expired-keys.md create mode 100644 docs/file-ops.md create mode 100644 docs/full-command-list.md create mode 100644 docs/git-tips.md create mode 100644 docs/gnupg-tips.md create mode 100644 docs/installation.md create mode 100644 docs/role-accounts.md create mode 100644 docs/subversion-tips.md create mode 100644 docs/support.md create mode 100644 docs/user-overview.md create mode 100644 docs/why-is-this-important.md create mode 100644 docs/with-ansible.md create mode 100644 docs/with-puppet.md delete mode 100644 pkg/bbnone/discover.go diff --git a/docs/CODE-LAYOUT.md b/docs/CODE-LAYOUT.md deleted file mode 100644 index 0d88ff74..00000000 --- a/docs/CODE-LAYOUT.md +++ /dev/null @@ -1,55 +0,0 @@ - - -# External access - -blackbox.go - - cmd\_\*() -- Validate command line and call op\_\*() functions to do the work. - - This code deals with all command line flags and ENV variables. - - Prints status messages, errors, warns, etc. - - Calls bbutil.go op\_() operations exclusively. - - Determines VCS in use, GPG version in use. - -pkg/bbutil.go: - - op\_\*() -- Perform operations (can be used from other Go code) - - This code is ignorant of flags and ENV variables. - - This code is silent. Never prints to stdio/stderr. Returns errors for parent to print. - - Decrypt(filename) - - CopyPermissions(src, dst) - - EncryptedFilename(plainfilename string) (encryptedfilename string) - - UnencryptedFilename(plainfilename string) (encryptedfilename string) - - - admin.go -- generic admin manager - - ListAdmins() - - AddAdmins() - - RemoveAdmins() - - ListFiles() - - FileStatus() - - FileStatusAll() - - IsOnFilelist() - - IsNotOnFilelist() - - adminplain.go -- read/write blackbox-admins.txt - - listAdminsPlain() - - addAdminsPlain() - - removeAdminsPlain() - - listFilesPlain() - - fileStatusPlain() - - fileStatusAllPlain() - - FUTURE: a json equivalent of each plain function. Functions in main.go decide which to call. - -crypto functions - -models.go: - interface for gpg. - - Decrypt(encrypted, unencrypted) error - - Encrypt(unencrypted, encrypted) error - - GetPubKey(dirname, keyname) (pubkey string) - - ImportPubKey(dirname, pubkey) touchedFiles []string -cryptcmd/bb\_gpg\_v1.go: - - struct Gpgv1 -cryptcmd/bb\_gpg\_v2.go: - - struct Gpgv2 - -vcs/models.go - interface for talk with VCS systems. -vcs/gpgv1/ -vcs/gpgv2/ diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..9032cea8 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,61 @@ +BlackBox +======== + +Blackbox is an open source tool that enables you to safe store sensitive information in +Git (or other) repos by encrypting them with GPG. Only the encrypted +version of the file is available. You can be free to provide access +to the repo, as but only people with the right GPG keys can access the +encrypted data. + +Things you should **never** store in a repo without encryption: + +* TLS (SSL) certificates +* Passwords +* API keys +* And more! + +Project Info: + +* [Overview](user-overview) +* [Why is this important?](why-is-this-important) +* [Support/Community](support) +* [How BB encrypts](encryption) +* [OS Compatibility](compatibility) +* [Installation Instructions](installation) +* [Alternatives](alternatives) + +User Info: + +* [Enabling Blackbox on a Repo](enable-repo) +* [Enroll a file](enable-repo) +* [Full Command List](full-command-list) +* [Add/Remove users](admin-ops) +* [Add/Remove files](file-ops) +* [Advanced techiques](advanced) +* [Use with Role Accounts](role-accounts) +* [Backwards Compatibility](backwards-compatibility) +* [Replacing expired keys](expired-keys) +* [Git Tips](git-tips) +* [SubVersion Tips](subversion-tips) +* [GnuPG tips](gnupg-tips) +* [Use with Ansible](with-ansible) +* [Use with Puppet](with-puppet) + +For contributors: + +* [Developer Info](dev) +* [Code overview](dev-code-overview) +* [Add new OS support]() +* [Add new VCS support]() + + +A slide presentation about an older release [is on SlideShare](http://www.slideshare.net/TomLimoncelli/the-blackbox-project-sfae). + +Join our mailing list: [https://groups.google.com/d/forum/blackbox-project](https://groups.google.com/d/forum/blackbox-project) + + +License +======= + +This content is released under the MIT License. +See the [LICENSE.txt](LICENSE.txt) file. diff --git a/docs/admin-ops.md b/docs/admin-ops.md new file mode 100644 index 00000000..7767154b --- /dev/null +++ b/docs/admin-ops.md @@ -0,0 +1,148 @@ +User Management +=============== + + +# Who are the current admins? + +``` +blackbox admin list +``` + + +# Add a new user (admin) + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +`.blackbox/blackbox-admins.txt` is a file that lists which users are able to decrypt files. (More pedantically, it is a list of the GnuPG key names that the file is encrypted for.) + +To join the list of people that can edit the file requires three steps; You create a GPG key and add it to the key ring. Then, someone that already has access adds you to the system. Lastly, you should test your access. + +## Step 1: NEWPERSON creates a GPG key pair on a secure machine and add to public keychain. + +If you don't already have a GPG key, here's how to generate one: + +``` +gpg --gen-key +``` + +WARNING: New versions of GPG generate keys which are not understood by +old versions of GPG. If you generate a key with a new version of GPG, +this will cause problems for users of older versions of GPG. +Therefore it is recommended that you either assure that everyone using +Blackbox have the exact same version of GPG, or generate GPG keys +using a version of GPG as old as the oldest version of GPG used by +everyone using Blackbox. + +Pick defaults for encryption settings, 0 expiration. Pick a VERY GOOD +passphrase. Store a backup of the private key someplace secure. For +example, keep the backup copy on a USB drive that is locked in safe. +Or, at least put it on a secure machine with little or no internet +access, full-disk-encryption, etc. Your employer probably has rules +about how to store such things. + +FYI: If generating the key is slow, this is usually because the system +isn't generating enough entropy. Tip: Open another window on that +machine and run this command: `ls -R /` + +Now that you have a GPG key, add yourself as an admin: + +``` +blackbox admin add KEYNAME +``` + +...where "KEYNAME" is the email address listed in the gpg key you created previously. For example: + +``` +blackbox admin add tal@example.com +``` + +When the command completes successfully, instructions on how to commit these changes will be output. Run the command as given to commit the changes. It will look like this: + +``` +git commit -m'NEW ADMIN: tal@example.com' .blackbox/pubring.gpg .blackbox/trustdb.gpg .blackbox/blackbox-admins.txt +``` + + +Then push it to the repo: + +``` +git push + +or + +ht push + +(or whatever is appropriate) +``` + +NOTE: Creating a Role Account? If you are adding the pubring.gpg of a role account, you can specify the directory where the pubring.gpg file can be found as a 2nd parameter: `blackbox admin add puppetmaster@puppet-master-1.example.com /path/to/the/dir` + +## Step 2: AN EXISTING ADMIN accepts you into the system. + +Ask someone that already has access to re-encrypt the data files. This +gives you access. They simply decrypt and re-encrypt the data without +making any changes. + +Pre-check: Verify the new keys look good. + +``` +git pull # Or whatever is required for your system +gpg --homedir=.blackbox --list-keys +``` + +For example, examine the key name (email address) to make sure it conforms to corporate standards. + +Import the keychain into your personal keychain and reencrypt: + +``` +gpg --import .blackbox/pubring.gpg +blackbox reencrypt --all shred +``` + +Push the re-encrypted files: + +``` +git commit -a +git push + +or + +hg commit +hg push +``` + +### Step 3: NEWPERSON tests. + +Make sure you can decrypt a file. (Suggestion: Keep a dummy file in +VCS just for new people to practice on.) + + +# Remove a user + +Simply run `blackbox admin remove` with their keyname then re-encrypt: + +Example: + +``` +blackbox admin remove olduser@example.com +blackbox reencrypt --all shred +``` + +When the command completes, you will be given a reminder to check in the change and push it. + +Note that their keys will still be in the key ring, but they will go unused. If you'd like to clean up the keyring, use the normal GPG commands and check in the file. + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +``` +gpg --homedir=.blackbox --list-keys +gpg --homedir=.blackbox --delete-key olduser@example.com +git commit -m'Cleaned olduser@example.com from keyring' .blackbox/* +``` + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +The key ring only has public keys. There are no secret keys to delete. + +Remember that this person did have access to all the secrets at one time. They could have made a copy. Therefore, to be completely secure, you should change all passwords, generate new SSL keys, and so on just like when anyone that had privileged access leaves an organization. + diff --git a/docs/advanced.md b/docs/advanced.md new file mode 100644 index 00000000..77062826 --- /dev/null +++ b/docs/advanced.md @@ -0,0 +1,46 @@ +Advanced Techniques +=================== + + +# Using Blackbox without a repo + +If the files are copied out of a repo they can still be decrypted and +edited. Obviously edits, changes to keys, and such will be lost if +they are made outside the repo. Also note that commands are most +likely to only work if run from the base directory (i.e. the parent to +the .blackbox directory). + +Without a repo, all commands must be run from the same directory +as the ".blackbox" directory. It might work otherwise but no +promises. + + +# Mixing gpg 1.x/2.0 and 2.2 + +WARNING: Each version of GnuPG uses a different, and incompatible, +binary format to store the keychain. When Blackbox was originally +created, I didn't know this. Things are mostly upwards compatible. +That said, if you have some admins with GnuPG 1.x and others with GnuPG 2.2, +you may corrupt the keychain. + +A future version will store the keychain in an GnuPG-approved +version-neutral format. + + +# Having gpg and gpg2 on the same machine + +NOTE: This is not implemented at this time. TODO(tlim) Use GPG to find +the binary. + +In some situations, team members or automated roles need to install gpg +2.x alongside the system gpg version 1.x to catch up with the team's gpg +version. On Ubuntu 16, you can ```apt-get install gnupg2``` which +installs the binary gpg2. If you want to use this gpg2 binary, run every +blackbox command with GPG=gpg2. + +For example: + +``` +GPG=gpg2 blackbox_postdeploy +``` + diff --git a/docs/alternatives.md b/docs/alternatives.md new file mode 100644 index 00000000..01d52ea0 --- /dev/null +++ b/docs/alternatives.md @@ -0,0 +1,14 @@ +Alternatives +============ + +Here are other open source packages that do something similar to +BlackBox. If you like them better than BlackBox, please use them. + +- [git-crypt](https://www.agwa.name/projects/git-crypt/) +- [Pass](http://www.zx2c4.com/projects/password-store/) +- [Transcrypt](https://github.com/elasticdog/transcrypt) +- [Keyringer](https://keyringer.pw/) +- [git-secret](https://github.com/sobolevn/git-secret) + +git-crypt has the best git integration. Once set up it is nearly +transparent to the users. However it only works with git. diff --git a/docs/backwards-compatibility.md b/docs/backwards-compatibility.md new file mode 100644 index 00000000..a2790ae0 --- /dev/null +++ b/docs/backwards-compatibility.md @@ -0,0 +1,55 @@ +Backwards Compatibility +======================= + +# Where is the configuration stored? .blackbox vs. keyrings/live + +Blackbox stores its configuration data in the `.blackbox` subdirectory. Older +repos use `keyrings/live`. For backwards compatibility either will work. + +All documentation refers to `.blackbox`. + +You can convert an old repo by simply renaming the directory: + +``` +mv keyrings/live .blackbox +rmdir keyrings +``` + +There is no technical reason to convert old repos except that it is less +confusing to users. + +This change was made in commit 60e782a0, release v1.20180615. + + +# How blackbox fines the config directory: + +## Creating the repo: + +`blackbox init` creates the config directory in the root +of the repo. Here's how it picks the name: + +- If `$BLACKBOX_TEAM` is set, `.blackbox-$BLACKBOX_TEAM` is used. +- If the flag `--team ` is set, it uses `.blackbox-` +- Otherwise, it uses `.blackbox` + +When searching for the configuration directory, the following +locations are checked. First match wins. + +- `.blackbox-$BLACKBOX_TEAM` (only if `$BLACKBOX_TEAM` is set) +- The value of `--config value` (if the flag is set) +- `$BLACKBOX_CONFIGDIR` (the preferred env. variable to use) +- `$BLACKBOXDATA` (for backwards compatibility with v1) +- `.blackbox` +- `keyrings/live` (for backwards compatibility) + +NOTE: The env variables and `--config` should be set to the full path +to the config directory (i.e.: `/Users/tom/gitstuff/myrepo/.blackbox`). +If it is set to a relative directory (i.e. `.blackbox` or +`../myrepo/.blackbox`) most commands will break. + +NOTE: Why the change from `$BLACKBOXDATA` to `$BLACKBOX_CONFIGDIR`? We want +all the env. variables to begin with the prefix `BLACKBOX_`. If v1 +supported another name, that is still supported. If you are starting +with v2 and have no other users using v1, please use the `BLACKBOX_` +prefix. + diff --git a/docs/compatibility.md b/docs/compatibility.md new file mode 100644 index 00000000..93b75fbf --- /dev/null +++ b/docs/compatibility.md @@ -0,0 +1,70 @@ +Compatibility +============= + +Blackbox supports a plug-in archtecture to easily support multiple VCS +system. Current support is for: + +VCS/DVCS support: + +* git +* "none" (repo-less use is supported) +* WOULD LOVE VOLUNTEERS TO HELP ADD SUPPORT FOR: hg, svn, p4 + +GPG versions + +* Git 1.x and 2.0 +* Git 2.2 and higher +* WOULD LOVE VOLUNTEERS TO HELP ADD SUPPORT FOR: + golang.org/x/crypto/openpgp (this would make the code have no + external dependencies) + +Operating systems: + +Blackbox should work on any Linux system with GnuPG installed. +Blackbox simply looks for `gpg` in `$PATH`. + +Windows: It should work (but has not been extensively tested) on +Windows WSL2. + +Automated testing is done on these combinations: + +* macOS: GnuPG 2.2 executables from https://gpgtools.org/ +* CentOS: GnuPG 2.0.x executables from the "base" or "updates" repo. + +Windows native: VOLUNTEER NEEDED to make a native Windows version +(should be rather simple as Go does most of the work) + +NOTE: Version 1 worked on CentOS/RedHat, macOS, Gygwin, WinGW, NetBSD, +and SmartOS. Hopefully we can achieve that broad level of support in +the future. Any system that is supported by the Go language and +has GuPG 2.0.x or higher binaries available should be easy to achieve. +We'd also like to have automated testing for the same. + +# Windows + +BlackBox assumes that `blackbox-admins.txt` and `blackbox-files.txt` will have +LF line endings. Windows users should be careful to configure Git or other systems +to not convert or "fix" those files. + +If you use Git, add the following lines to your `.gitattributes` file: + + **/blackbox-admins.txt text eol=lf + **/blackbox-files.txt text eol=lf + +The latest version of `blackbox_initialize` will create a `.gitattributes` file in the `$BLACKBOXDATA` +directory (usually `.blackbox`) for you. + +TODO: Needs testing. + +# Cygwin + +TODO: List what packages are required for building the software. + +TODO: List what packages are required for running the software. + + +# MinGW + +MinGW (comes with Git for Windows) support requires the following: + +TODO: FILL IN any requirements diff --git a/docs/dev-code-overview.md b/docs/dev-code-overview.md new file mode 100644 index 00000000..319a0e5f --- /dev/null +++ b/docs/dev-code-overview.md @@ -0,0 +1,36 @@ +Code Overview +============= + +Here is how the code is laid out. + +TODO(tlim): Add a diagram of the layers + +``` +cmd/blackbox/ The command line tool. + blackbox.go main() + cli.go Definition of all subcommands and flags + drive.go Processes flags and calls functions in verbs.go + NOTE: These are the only files that are aware of the + flags. Everything else gets the flag data passed to it + as a parameter. This way the remaining system can be + used as a module. + +pkg/box/ High-level functions related to "the black box". + verbs.go One function per subcommand. + box.go Functions for manipulating the files in .blackbox + boxutils.go Helper functions for the above. + +pkg/bblog/ Module that provides logging facilities. +pkg/bbutil/ Functions that are useful to box, plug-ins, etc. +pkg/tainedname/ Module for printing filenames escaped for Bash. + +models/vcs.go The interface that defines a VCS plug-in. +models/crypters.go The interface that defines a GPG plug-in. + +pkg/crypters/ Plug-ins for GPG functionality. +pkg/crypters/gnupg Plug-in that runs an external gpg binary (found via $PATH) + +pkg/vcs/ Plug-ins for VCS functionality. +pkg/vcs/none Repo-less mode. +pkg/vcs/git Git mode. +``` diff --git a/docs/dev.md b/docs/dev.md new file mode 100644 index 00000000..cea0e328 --- /dev/null +++ b/docs/dev.md @@ -0,0 +1,36 @@ +Developer Info +============== + +Code submissions are gladly welcomed! The code is fairly easy to read. + +Get the code: + +``` +git clone git@github.com:StackExchange/blackbox.git +``` + +Test your changes: + +``` +go test ./... +``` + +This runs through a number of system tests. It creates a repo, +encrypts files, decrypts files, and so on. You can run these tests to +verify that the changes you made didn't break anything. You can also +use these tests to verify that the system works with a new operating +system. + +Please submit tests with code changes: + +The best way to change BlackBox is via Test Driven Development. First +add a test to `tools/confidence.sh`. This test should fail, and +demonstrate the need for the change you are about to make. Then fix +the bug or add the feature you want. When you are done, `make +confidence` should pass all tests. The PR you submit should include +your code as well as the new test. This way the confidence tests +accumulate as the system grows as we know future changes don't break +old features. + +Note: More info about compatibility are on the [Compatibility Page](compatibility) + diff --git a/docs/enable-repo.md b/docs/enable-repo.md new file mode 100644 index 00000000..24b55ddb --- /dev/null +++ b/docs/enable-repo.md @@ -0,0 +1,58 @@ +Enabling Blackbox on a Repo +=========================== + +Overview: +1. Run the initialization command +2. Add at least one admin. +3. Add files. (don't add files before the admins) + +The long version: + +1. If you don't have a GPG key, set it up using instructions such as: +[Set up GPG key](https://help.github.com/articles/generating-a-new-gpg-key/). \ +Now you are ready to go. + +1. `cd` into a Git, Mercurial, Subversion or Perforce repository and run `blackbox init`. + +1. Add yourself with `blackbox admin add YOUR@EMAIL` + +1. Commit the files as directed. + +That's it! + +At this point you should encrypt a file and make sure you can decrypt +it. This verifies that everything is working as expected. + + +1. Pick a file to be encrypted. Since this is a test, you might want + to create a test file. Call it `secret.txt` and edit the file + so that it includes your mother's maiden name. Just kidding! + Store this sentence: `This is my test file.` + +2. Run `blackbox file add secret.txt` + +3. Decode the encrypted version: `blackbox cat secret.txt` + +The "cat" subcommand only accesses the encrypted (`.gpg`) file and is +a good way to see that the file was encrypted properly. You should +see `This is my test file.` + +4 Verify that editing the file works. + +To view and/or edit a file, run `blackbox edit --shred secret.txt` + +Now encrypt it and shred the original: + +``` +blackbox encrypt --shred secret.txt +``` + +Now make sure you can decrypt the new file: + +``` +blackbox cat secret.txt +``` + +You should see the changed text. + +Now commit and push `secret.txt.gpg` and you are done! diff --git a/docs/encryption.md b/docs/encryption.md new file mode 100644 index 00000000..6a0ad577 --- /dev/null +++ b/docs/encryption.md @@ -0,0 +1,54 @@ +How is the encryption done? +=========================== + +GPG has many different ways to encrypt a file. BlackBox uses the mode +that lets you specify a list of keys that can decrypt the message. + +If you have 5 people ("admins") that should be able to access the +secrets, each creates a GPG key and adds their public key to the +keychain. The GPG command used to encrypt the file lists all 5 key +names, and therefore any 1 key can decrypt the file. + +Blackbox stores a copy of the public keys of all admins. It never +stores the private keys. + +To remove someone's access, remove that admin's key name (i.e. email +address) from the list of admins and re-encrypt all the files. They +can still read the .gpg file (assuming they have access to the +repository) but they can't decrypt it any more. + +*What if they kept a copy of the old repo before you removed access?* +Yes, they can decrypt old versions of the file. This is why when an +admin leaves the team, you should change all your passwords, SSL +certs, and so on. You should have been doing that before BlackBox, +right? + +*Why don't you use symmetric keys?* In other words, why mess with all +this GPG key stuff and instead why don't we just encrypt all the files +with a single passphrase. Yes, GPG supports that, but then we are +managing a shared password, which is fraught with problems. If someone +"leaves the team" we would have to communicate to everyone a new +password. Now we just have to remove their key. This scales better. + +*How do automated processes decrypt without asking for a password?* +GPG requires a passphrase on a private key. However, it permits the +creation of subkeys that have no passphrase. For automated processes, +create a subkey that is only stored on the machine that needs to +decrypt the files. For example, at Stack Exchange, when our Continuous +Integration (CI) system pushes a code change to our Puppet masters, +they run `blackbox decrypt --all --overwrite` to decrypt all the files. +The user that +runs this code has a subkey that doesn't require a passphrase. Since +we have many masters, each has its own key. And, yes, this means our +Puppet Masters have to be very secure. However, they were already +secure because, like, dude... if you can break into someone's puppet +master you own their network. + +*If you use Puppet, why didn't you just use hiera-eyaml?* There are 4 +reasons: + +1. This works with any Git or Mercurial repo, even if you aren't using Puppet. +2. hiera-eyaml decrypts "on demand" which means your Puppet Master now uses a lot of CPU to decrypt keys every time it is contacted. It slows down your master, which, in my case, is already slow enough. +3. This works with binary files, without having to ASCIIify them and paste them into a YAML file. Have you tried to do this with a cert that is 10K long and changes every few weeks? Ick. +4. hiera-eyaml didn't exist when I wrote this. (That's the real reason.) + diff --git a/docs/expired-keys.md b/docs/expired-keys.md new file mode 100644 index 00000000..7f7756a6 --- /dev/null +++ b/docs/expired-keys.md @@ -0,0 +1,62 @@ +Replacing expired keys +====================== + +If someone's key has already expired, blackbox will stop +encrypting. You see this error: + +``` +$ blackbox_edit_end modified_file.txt +--> Error: can't re-encrypt because a key has expired. +``` + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +You can also detect keys that are about to expire by issuing this command and manually reviewing the "expired:" dates: + + gpg --homedir=.blackbox --list-keys + +or... list UIDs that will expire within 1 month from today: (Warning: this also lists keys without an expiration date) + + gpg --homedir=.blackbox --list-keys --with-colons --fixed-list-mode | grep ^uid | awk -F: '$6 < '$(( $(date +%s) + 2592000)) + +Here's how to replace the key: + +- Step 1. Administrator removes expired user: + +Warning: This process will erase any unencrypted files that you were in the process of editing. Copy them elsewhere and restore the changes when done. + +``` +blackbox_removeadmin expired_user@example.com +# This next command overwrites any changed unencrypted files. See warning above. +blackbox_update_all_files +git commit -m "Re-encrypt all files" +gpg --homedir=.blackbox --delete-key expired_user@example.com +git commit -m 'Cleaned expired_user@example.com from keyring' .blackbox/* +git push +``` + +- Step 2. Expired user adds an updated key: + +``` +git pull +blackbox_addadmin updated_user@example.com +git commit -m'NEW ADMIN: updated_user@example.com .blackbox/pubring.gpg .blackbox/trustdb.gpg .blackbox/blackbox-admins.txt +git push +``` + +- Step 3. Administrator re-encrypts all files with the updated key of the expired user: + +``` +git pull +gpg --import .blackbox/pubring.gpg +blackbox_update_all_files +git commit -m "Re-encrypt all files" +git push +``` + +- Step 4: Clean up: + +Any files that were temporarily copied in the first step so as to not be overwritten can now be copied back and re-encrypted with the `blackbox_edit_end` command. + +(Thanks to @chishaku for finding a solution to this problem!) + diff --git a/docs/file-ops.md b/docs/file-ops.md new file mode 100644 index 00000000..3c37b6b9 --- /dev/null +++ b/docs/file-ops.md @@ -0,0 +1,55 @@ +How to add/remove a file into the system? +========================================= + +# Adding files: + +- If you need to, start the GPG Agent: `eval $(gpg-agent --daemon)` +- Add the file to the system: + +``` +blackbox file add path/to/file.name.key + +# If you want to delete the old plaintext: +blackbox file add --shred path/to/file.name.key +``` + +Multiple file names can be specified on the command line: + +Example 1: Register 2 files: + +``` +blackbox file add --shred file1.txt file2.txt +``` + +Example 2: Register all the files in `$DIR`: + +``` +find $DIR -type f -not -name '*.gpg' -print0 | xargs -0 blackbox file add +``` + + +# Removing files + +This command + +``` +blackbox file remove path/to/file.name.key +``` + +TODO(tlim): Add examples. + +# List files + +To see what files are currently enrolled in the system: + +``` +blackbox file list +``` + +You can also see their status: + +``` +blackbox status +blackbox status just_one_file.txt +blackbox status --type ENCRYPTED +``` diff --git a/docs/full-command-list.md b/docs/full-command-list.md new file mode 100644 index 00000000..ae04878f --- /dev/null +++ b/docs/full-command-list.md @@ -0,0 +1,34 @@ +Blackbox Command List +===================== + +## Global Flags +### `--vcs` +### `--crypto` +### `--config` +### `--team` +### `--editor` +### `--umask` +### `--debug` +### `--help` +### `--help` +### `--version` +## User Commands +### `blackbox decrypt` +### `blackbox encrypt` +### `blackbox edit` +### `blackbox cat` +### `blackbox diff` +### `blackbox shred` +### `blackbox help` +## User Commands +### `blackbox init` +### `blackbox admin` +### `blackbox file` +### `blackbox status` +### `blackbox reencrypt` +## Debug +### `blackbox info` +## Integration Test (secret menu) +### `blackbox testing_init` + +TODO(tlim): Can we automatically generate this? The data is all in cli.go diff --git a/docs/git-tips.md b/docs/git-tips.md new file mode 100644 index 00000000..23895786 --- /dev/null +++ b/docs/git-tips.md @@ -0,0 +1,23 @@ +GIT tips +======== + + +# Configure git to show diffs in encrypted files + +It's possible to tell Git to decrypt versions of the file before running them through `git diff` or `git log`. To achieve this do: + +- Add the following to `.gitattributes` at the top of the git repository: + +``` +*.gpg diff=blackbox +``` + +- Add the following to `.git/config`: + +``` +[diff "blackbox"] + textconv = gpg --use-agent -q --batch --decrypt +```` + +And now commands like `git log -p file.gpg` will show a nice log of the changes in the encrypted file. + diff --git a/docs/gnupg-tips.md b/docs/gnupg-tips.md new file mode 100644 index 00000000..541289a9 --- /dev/null +++ b/docs/gnupg-tips.md @@ -0,0 +1,31 @@ +GnuPG tips +========== + +# Common error messages + +* Message: `gpg: filename: skipped: No public key` +* Solution: Usually this means there is an item in + `.blackbox/blackbox-admins.txt` that is not the name of the key. + Either something invalid was inserted (like a filename instead of a + username) or a user has left the organization and their key was + removed from the keychain, but their name wasn't removed from the + blackbox-admins.txt file. + +* Message: `gpg: decryption failed: No secret key` +* Solution: Usually means you forgot to re-encrypt the file with the new key. + +* Message: `Error: can't re-encrypt because a key has expired.` +* Solution: A user's key has expired and can't be used to encrypt any more. Follow the [Replace expired keys](expired-keys) page. + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +# GnuPG problems + +Blackbox is just a front-end to GPG. If you get into a problem with a +key or file, you'll usually have better luck asking for advice on +the gnupg users mailing list TODO: Get link to this list + + +The author of Blackbox is not a GnuPG expert. He wrote Blackbox +because it was better than trying to remember GPG's horrible flag +names. diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 00000000..72da1dfa --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,17 @@ +Installation Instructions +========================= + +Currently blackbox v2 is installed by compiling the code and +copying the binary someplace: + +TODO: + +``` +git clone FILL IN +``` + +Future: We will have RPM, DEB, Chocolately packages. + + +Next step: [Enable on a repo](enable-repo) + diff --git a/docs/role-accounts.md b/docs/role-accounts.md new file mode 100644 index 00000000..25196714 --- /dev/null +++ b/docs/role-accounts.md @@ -0,0 +1,158 @@ +Set up automated users or "role accounts" +========================================= + +TODO(tlim): I think this is overly complex. With GnuPG 2.2 and later, +you can use `--password '' --quick-generate-key userid` and you are +done. No need for subkeys. Maybe rework this? + +With role accounts, you have an automated system that needs to be able +to decrypt secrets without a password. This means the security of your +repo is based on how locked down the automation system is. This +is risky, so be careful. + + +i.e. This is how a Puppet Master can have access to the unencrypted data. + +FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" + +An automated user (a "role account") is one that that must be able to decrypt without a passphrase. In general you'll want to do this for the user that pulls the files from the repo to the master. This may be automated with Jenkins CI or other CI system. + +GPG keys have to have a passphrase. However, passphrases are optional on subkeys. Therefore, we will create a key with a passphrase then create a subkey without a passphrase. Since the subkey is very powerful, it should be created on a very secure machine. + +There's another catch. The role account probably can't check files into Git/Mercurial. It probably only has read-only access to the repo. That's a good security policy. This means that the role account can't be used to upload the subkey public bits into the repo. + +Therefore, we will create the key/subkey on a secure machine as yourself. From there we can commit the public portions into the repo. Also from this account we will export the parts that the role account needs, copy them to where the role account can access them, and import them as the role account. + +ProTip: If asked to generate entropy, consider running this on the same machine in another window: `sudo dd if=/dev/sda of=/dev/null` + +For the rest of this doc, you'll need to make the following substitutions: + +- ROLEUSER: svc_deployacct or whatever your role account's name is. +- NEWMASTER: the machine this role account exists on. +- SECUREHOST: The machine you use to create the keys. + +NOTE: This should be more automated/scripted. Patches welcome. + +On SECUREHOST, create the puppet master's keys: + +``` +$ mkdir /tmp/NEWMASTER +$ cd /tmp/NEWMASTER +$ gpg --homedir . --gen-key +Your selection? + (1) RSA and RSA (default) +What keysize do you want? (2048) DEFAULT +Key is valid for? (0) DEFAULT + +# Real name: Puppet CI Deploy Account +# Email address: svc_deployacct@hostname.domain.name +``` + +NOTE: Rather than a real email address, use the username@FQDN of the host the key will be used on. If you use this role account on many machines, each should have its own key. By using the FQDN of the host, you will be able to know which key is which. In this doc, we'll refer to username@FQDN as $KEYNAME + +Save the passphrase somewhere safe! + +Create a sub-key that has no password: + +``` +$ gpg --homedir . --edit-key svc_deployacct +gpg> addkey +(enter passphrase) + Please select what kind of key you want: + (3) DSA (sign only) + (4) RSA (sign only) + (5) Elgamal (encrypt only) + (6) RSA (encrypt only) +Your selection? 6 +What keysize do you want? (2048) +Key is valid for? (0) +Command> key 2 +(the new subkey has a "*" next to it) +Command> passwd +(enter the main key's passphrase) +(enter an empty passphrase for the subkey... confirm you want to do this) +Command> save +``` + +Now securely export this directory to NEWMASTER: + +``` +gpg --homedir . --export -a svc_sadeploy >/tmp/NEWMASTER/pubkey.txt +tar cvf /tmp/keys.tar . +rsync -avP /tmp/keys.tar NEWMASTER:/tmp/. +``` + +On NEWMASTER, receive the new GnuPG config: + +``` +sudo -u svc_deployacct bash +mkdir -m 0700 -p ~/.gnupg +cd ~/.gnupg && tar xpvf /tmp/keys.tar +``` + + + +Back on SECUREHOST, add the new email address to .blackbox/blackbox-admins.txt: + +``` +cd /path/to/the/repo +blackbox_addadmin $KEYNAME /tmp/NEWMASTER +``` + +Verify that secring.gpg is a zero-length file. If it isn't, you have somehow added a private key to the keyring. Start over. + +``` +cd .blackbox +ls -l secring.gpg +``` + +Commit the recent changes: + +``` +cd .blackbox +git commit -m"Adding key for KEYNAME" pubring.gpg trustdb.gpg blackbox-admins.txt +``` + +Regenerate all encrypted files with the new key: + +``` +blackbox_update_all_files +git status +git commit -m"updated encryption" -a +git push +``` + +On NEWMASTER, import the keys and decrypt the files: + +``` +sudo -u svc_sadeploy bash # Become the role account. +gpg --import /etc/puppet/.blackbox/pubring.gpg +export PATH=$PATH:/path/to/blackbox/bin +blackbox_postdeploy +sudo -u puppet cat /etc/puppet/hieradata/blackbox.yaml # or any encrypted file. +``` + +ProTip: If you get "gpg: decryption failed: No secret key" then you forgot to re-encrypt blackbox.yaml with the new key. + +On SECUREHOST, securely delete your files: + +``` +cd /tmp/NEWMASTER +# On machines with the "shred" command: +shred -u /tmp/keys.tar +find . -type f -print0 | xargs -0 shred -u +# All else: +rm -rf /tmp/NEWMASTER +``` + +Also shred any other temporary files you may have made. + + + diff --git a/docs/subversion-tips.md b/docs/subversion-tips.md new file mode 100644 index 00000000..6b6991c4 --- /dev/null +++ b/docs/subversion-tips.md @@ -0,0 +1,21 @@ +Subversion Tips +=============== + +NOTE: This is from v1. Can someone that uses Subversion check +this and update it? + + +The current implementation will store the blackbox in `/keyrings` at +the root of the entire repo. This will create an issue between +environments that have different roots (i.e. checking out `/` on +development vs `/releases/foo` in production). To get around this, you +can `export BLACKBOX_REPOBASE=/path/to/repo` and set a specific base +for your repo. + +This was originally written for git and supports a two-phase commit, +in which `commit` is a local commit and "push" sends the change +upstream to the version control server when something is registered or +deregistered with the system. The current implementation will +immediately `commit` a file (to the upstream subversion server) when +you execute a `blackbox_*` command. + diff --git a/docs/support.md b/docs/support.md new file mode 100644 index 00000000..72e04662 --- /dev/null +++ b/docs/support.md @@ -0,0 +1,16 @@ +Support +======= + +# Join our community! + +Join the [blackbox-project mailing list](https://groups.google.com/d/forum/blackbox-project)! + + + +# How to submit bugs or ask questions? + +We welcome questions, bug reports and feedback! + +The best place to start is to join the [blackbox-project mailing list](https://groups.google.com/d/forum/blackbox-project) and ask there. + +Bugs are tracked here in Github. Please feel free to [report bugs](https://github.com/StackExchange/blackbox/issues) yourself. diff --git a/docs/user-overview.md b/docs/user-overview.md new file mode 100644 index 00000000..66bd594a --- /dev/null +++ b/docs/user-overview.md @@ -0,0 +1,124 @@ +User Guide +========== + +# Overview + +Suppose you have a VCS repository (i.e. a Git or Mercurial repo) and +certain files contain secrets such as passwords or SSL private keys. +Often people just store such files "and hope that nobody finds them in +the repo". That's not safe. Hope is not a strategy. + +With BlackBox, those files are stored encrypted using GPG. Access to +the repo without also having the right GPG keys makes those files as worthless +as random bits. As long as you keep your GPG keys safe, you don't +have to worry about storing your VCS repo on an untrusted server or +letting anyone clone the repo. + +Heck, even if you trust your server, now you don't have to trust the +people that do backups of that server! + +Each person ("admin") of the system can decrypt all the files using +their GPG key, which has its own passphrase. The authorized GPG keys +can decrypt any file. This is better than systems that use one +GPG key (and passphrase) that must be shared among a group of people. +It is much better than having one passphrase for each file (I don't +think anyone actually does that). + +Since any admin's GPG key can decrypt the files, if one person leaves +the company, you don't have to communicate a new passphrase to everyone. +Simply disable the one key that should no longer have access. +The process for doing this is as easy as running 2 commands (1 to +disable their key, 1 to re-encrypt all files.) Obviously if they kept +a copy of the repo (and their own passphrase) before leaving the +company, they have access to the secrets. However, you should rotate +those secrets anyway. ("rotate secrets" means changing the passwords, +regenerating TLS certs, and so on). + +# Sample session: + +First we are going to list the files currently in the blackbox. In +this case, it is an SSH private key. + +``` +$ blackbox file list +modules/log_management/files/id_rsa +``` + +Excellent! Our coworkers have already registered a file with the +system. Let's decrypt it, edit it, and re-encrypt it. + +``` +$ blackbox decrypt modules/log_management/files/id_rsa +========== DECRYPTING "modules/log_management/files/id_rsa" +$ vi modules/log_management/files/id_rsa +``` + +That was easy so far! + +When we encrypt it, Blackbox will not commit the changes, but it +will give a hint that you should. It spells out the exact command you +need to type and even proposes a commit message. + +``` +$ blackbox encrypt modules/log_management/files/id_rsa +========== ENCRYPTING "modules/log_management/files/id_rsa" + +NEXT STEP: You need to manually check these in: + git commit -m"ENCRYPTED modules/log_management/files/id_rsa" modules/log_management/files/id_rsa.gpg +``` + +You can also use `blackbox edit ` to decrypt a file, edit it +(it will call `$EDITOR`) and re-encrypt it. + + +Now let's register a new file with the blackbox system. +`data/pass.yaml` is a small file that stores a very important +password. In this example, we had just stored the unecrypted +password in our repo. That's bad. Let's encrypt it. + +``` +$ blackbox file add data/pass.yaml +========== SHREDDING ("/bin/rm", "-f"): "data/pass.yaml" + +NEXT STEP: You need to manually check these in: + git commit -m"NEW FILES: data/pass.yaml" .gitignore keyrings/live/blackbox-files.txt modules/stacklb/pass.yaml modules/stacklb/pass.yaml.gpg +``` + +Before we commit the change, let's do a `git status` to see what else +has changed. + +``` +$ git status +On branch master +Changes to be committed: + (use "git restore --staged ..." to unstage) + modified: .gitignore + modified: keyrings/live/blackbox-files.txt + deleted: modules/stacklb/pass.yaml + new file: modules/stacklb/pass.yaml.gpg + +``` + +Notice that a number of files were modified: + +* `.gitignore`: This file is updated to include the plaintext + filename, so that you don't accidentally add it to the repo in the + future. +* `.blackbox/blackbox-files.txt`: The list of files that are registered with the system. +* `data/pass.yaml`: The file we encrypted is deleted from the repo. +* `data/pass.yaml.gpg`: The encrypted file is added to the repo. + +Even though pass.yaml was deleted from the repo, it is still in the +repo's history. Anyone with an old copy of the repo, or a new copy +that knows how to view the repo's history, can see the secret +password. For that reason, you should change the password and +re-encrypt the file. This is an important point. Blackbox is not +magic and it doesn't have a "Men In Black"-style neuralizer that +can make people forget the past. If someone leaves a project, you +have to change the old passwords, etc. + +Those are the basics. Your next step might be: + +* TODO: How to enable Blackbox for a repo. +* TODO: How to add yourself as an admin to a repo. +* TODO: Complete list of [all blackbox commands](all-commands) diff --git a/docs/why-is-this-important.md b/docs/why-is-this-important.md new file mode 100644 index 00000000..eba9cf26 --- /dev/null +++ b/docs/why-is-this-important.md @@ -0,0 +1,17 @@ +Why encrypt your secrets? +========================= + +OBVIOUSLY we don't want secret things like SSL private keys and +passwords to be leaked. + +NOT SO OBVIOUSLY when we store "secrets" in a VCS repo like Git or +Mercurial, suddenly we are less able to share our code with other +people. Communication between subteams of an organization is hurt. You +can't collaborate as well. Either you find yourself emailing +individual files around (yuck!), making a special repo with just the +files needed by your collaborators (yuck!!), or just deciding that +collaboration isn't worth all that effort (yuck!!!). + +The ability to be open and transparent about our code, with the +exception of a few specific files, is key to the kind of collaboration +that DevOps and modern IT practitioners need to do. diff --git a/docs/with-ansible.md b/docs/with-ansible.md new file mode 100644 index 00000000..cc88ab0c --- /dev/null +++ b/docs/with-ansible.md @@ -0,0 +1,18 @@ +How to use the secrets with Ansible? +=================================== + +Ansible Vault provides functionality for encrypting both entire files +and strings stored within files; however, keeping track of the +password(s) required for decryption is not handled by this module. + +Instead one must specify a password file when running the playbook. + +Ansible example for password file: `my_secret_password.txt.gpg` + +``` +ansible-playbook --vault-password-file my_secret_password.txt site.yml +``` + +Alternatively, one can specify this in the +`ANSIBLE_VAULT_PASSWORD_FILE` environment variable. + diff --git a/docs/with-puppet.md b/docs/with-puppet.md new file mode 100644 index 00000000..255a077b --- /dev/null +++ b/docs/with-puppet.md @@ -0,0 +1,68 @@ +How to use the secrets with Puppet? +=================================== + +# Entire files: + +Entire files, such as SSL certs and private keys, are treated just +like regular files. You decrypt them any time you push a new release +to the puppet master. + +Example of an encrypted file named `secret_file.key.gpg` + +* Plaintext file is: `modules/${module_name}/files/secret_file.key` +* Encrypted file is: `modules/${module_name}/files/secret_file.key.gpg` +* Puppet sees it as: `puppet:///modules/${module_name}/secret_file.key` + +Puppet code that stores `secret_file.key` in `/etc/my_little_secret.key`: + +``` +file { '/etc/my_little_secret.key': + ensure => 'file', + owner => 'root', + group => 'puppet', + mode => '0760', + source => "puppet:///modules/${module_name}/secret_file.key", # No ".gpg" +} +``` + +# Small strings: + +For small strings such as passwords and API keys, it makes sense +to store them in an (encrypted) YAML file which is then made +available via hiera. + +For example, we use a file called `blackbox.yaml`. You can access the +data in it using the hiera() function. + +*Setup:* + +Edit `hiera.yaml` to include "blackbox" to the search hierarchy: + +``` +:hierarchy: + - ... + - blackbox + - ... +``` + +In blackbox.yaml specify: + +``` +--- +module::test_password: "my secret password" +``` + +In your Puppet Code, access the password as you would any hiera data: + +``` +$the_password = hiera('module::test_password', 'fail') + +file {'/tmp/debug-blackbox.txt': + content => $the_password, + owner => 'root', + group => 'root', + mode => '0600', +} +``` + +The variable `$the_password` will contain "my secret password" and can be used anywhere strings are used. diff --git a/pkg/bbnone/discover.go b/pkg/bbnone/discover.go deleted file mode 100644 index e468500f..00000000 --- a/pkg/bbnone/discover.go +++ /dev/null @@ -1,29 +0,0 @@ -package bbnone - -import ( - "log" - "os" -) - -// NoneInfo contains Git-specific info about this repository. -type NoneInfo struct { -} - -// New is a factory; returns nil if this is not a Git repo. -func New() (*NoneInfo, error) { - return new(NoneInfo), nil -} - -// Name returns the name of this type of repo. -func (repo *NoneInfo) Name() string { - return "unknown" -} - -// RepoBaseDir returns the current working directory. -func (repo *NoneInfo) RepoBaseDir() string { - d, err := os.Getwd() - if err != nil { - log.Fatal(err) - } - return d -} From eccea8dadd73a7c219b876a045d7e990e4fa86c6 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 2 Jul 2020 06:50:53 -0400 Subject: [PATCH 46/69] fix comment --- pkg/vcs/vcs.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/vcs/vcs.go b/pkg/vcs/vcs.go index 3b679902..ef435255 100644 --- a/pkg/vcs/vcs.go +++ b/pkg/vcs/vcs.go @@ -24,7 +24,7 @@ type Item struct { // Catalog is the list of registered vcs's. var Catalog []*Item -// DetermineVcs polls the VCS plug-ins to determine the VCS of directory. +// Discover polls the VCS plug-ins to determine the VCS of directory. // The first to succeed is returned. // It never returns nil, since "NONE" is always valid. func Discover(dir string) Vcs { From ac66afd9777ecd7ef509acfdf9696cd6bbdab688 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 2 Jul 2020 07:01:16 -0400 Subject: [PATCH 47/69] linting --- pkg/tainedname/tainedname.go | 50 ++++++++++++++++++++++-------------- 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go index 52c142e9..10236215 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/tainedname/tainedname.go @@ -17,34 +17,48 @@ import ( "strings" ) -type dubious string +// Dubious is a string that can't be trusted to work on a shell line without escaping. +type Dubious string -func New(s string) dubious { - return dubious(s) +// New creates a dubious string. +func New(s string) Dubious { + return Dubious(s) } type protection int const ( - Unknown protection = iota - None // Nothing special - SingleQuote // Requires at least a single quote - DoubleQuote // Can only be in a double-quoted string + // Unknown indicates we don't know if it is safe. + Unknown protection = iota + // None requires no special escaping. + None // Nothing special + // SingleQuote is unsafe in bash and requires a single quote. + SingleQuote // Requires at least a single quote + // DoubleQuote is unsafe in bash and requires escaping or other double-quote features. + DoubleQuote // Can only be in a double-quoted string ) -var IsAQuote = None // Handled as a special case -var IsSpace = SingleQuote // " " (ascii 32) -var ShellUnsafe = SingleQuote // bash special -var GlobUnsafe = SingleQuote // Could be a glob -var InterpolationUnsafe = SingleQuote // Used in bash string interpolation -var HasBackslash = DoubleQuote // things like \n \t \r \000 \xFF +const ( + // IsAQuote is either a `'` or `"` + IsAQuote = None + // IsSpace is ascii 32 + IsSpace = SingleQuote + // ShellUnsafe is ()!$ or other bash special char + ShellUnsafe = SingleQuote + // GlobUnsafe means could be a glob char (* or ?) + GlobUnsafe = SingleQuote + // InterpolationUnsafe used in bash string interpolation ($) + InterpolationUnsafe = SingleQuote + // HasBackslash things like \n \t \r \000 \xFF + HasBackslash = DoubleQuote +) func max(i, j protection) protection { if i > j { return i - } else { - return j } + return j + } type tabEntry struct { @@ -133,7 +147,7 @@ func oct() func(r rune) string { // String returns a version of the dirty string that is absolutely // safe to paste into a command line. -func (dirty dubious) String() string { +func (dirty Dubious) String() string { if dirty == "" { return `""` } @@ -165,14 +179,12 @@ func (dirty dubious) String() string { case DoubleQuote: if unicode { return "$(printf '" + s + "')" - } else { - return `"` + s + `"` } + return `"` + s + `"` default: } // should not happen return fmt.Sprintf("%q", s) - } // escapeRune returns a string of octal escapes that represent the rune. From bea3af3d57bfe7650c9d8d0f4df0c98c6783399f Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Thu, 2 Jul 2020 16:53:58 -0400 Subject: [PATCH 48/69] Refactor how NeedsCommit is done --- cmd/blackbox/drive.go | 102 +++++++++++++++++++++++----- integrationTest/NOTES.txt | 4 +- integrationTest/integration_test.go | 19 ++++++ models/vcs.go | 6 +- pkg/box/boxutils.go | 24 +++++++ pkg/box/verbs.go | 73 ++++++++++---------- pkg/tainedname/tainedname.go | 42 ++++++++++++ pkg/vcs/git/git.go | 40 +++++++---- pkg/vcs/none/none.go | 12 ++-- 9 files changed, 246 insertions(+), 76 deletions(-) diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index ff413a50..4d3983a5 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -39,7 +39,11 @@ func cmdAdminAdd(c *cli.Context) error { "Must specify one admin's GnuPG user-id (i.e. email address) and optionally the directory of the pubkey data (default ~/.GnuPG)") } bx := box.NewFromFlags(c) - return bx.AdminAdd(c.Args().Get(0), c.Args().Get(1)) + err := bx.AdminAdd(c.Args().Get(0), c.Args().Get(1)) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdAdminList(c *cli.Context) error { @@ -47,7 +51,11 @@ func cmdAdminList(c *cli.Context) error { return fmt.Errorf("This command takes zero arguments") } bx := box.NewFromFlags(c) - return bx.AdminList() + err := bx.AdminList() + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdAdminRemove(c *cli.Context) error { @@ -55,7 +63,11 @@ func cmdAdminRemove(c *cli.Context) error { return fmt.Errorf("Must specify at least one admin's GnuPG user-id (i.e. email address)") } bx := box.NewFromFlags(c) - return bx.AdminRemove(c.Args().Slice()) + err := bx.AdminRemove(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdCat(c *cli.Context) error { @@ -63,7 +75,11 @@ func cmdCat(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) - return bx.Cat(c.Args().Slice()) + err := bx.Cat(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdDecrypt(c *cli.Context) error { @@ -79,11 +95,15 @@ func cmdDecrypt(c *cli.Context) error { } bx := box.NewFromFlags(c) - return bx.Decrypt(c.Args().Slice(), + err := bx.Decrypt(c.Args().Slice(), c.Bool("overwrite"), pauseNeeded, c.String("group"), ) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdDiff(c *cli.Context) error { @@ -91,7 +111,11 @@ func cmdDiff(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Diff(c.Args().Slice()) + err := bx.Diff(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdEdit(c *cli.Context) error { @@ -99,7 +123,11 @@ func cmdEdit(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) - return bx.Edit(c.Args().Slice()) + err := bx.Edit(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdEncrypt(c *cli.Context) error { @@ -107,7 +135,11 @@ func cmdEncrypt(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Encrypt(c.Args().Slice(), c.Bool("shred")) + err := bx.Encrypt(c.Args().Slice(), c.Bool("shred")) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdFileAdd(c *cli.Context) error { @@ -115,7 +147,11 @@ func cmdFileAdd(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) - return bx.FileAdd(c.Args().Slice(), c.Bool("shred")) + err := bx.FileAdd(c.Args().Slice(), c.Bool("shred")) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdFileList(c *cli.Context) error { @@ -123,7 +159,11 @@ func cmdFileList(c *cli.Context) error { return fmt.Errorf("This command takes zero arguments") } bx := box.NewFromFlags(c) - return bx.FileList() + err := bx.FileList() + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdFileRemove(c *cli.Context) error { @@ -131,7 +171,11 @@ func cmdFileRemove(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) - return bx.FileRemove(c.Args().Slice()) + err := bx.FileRemove(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdInfo(c *cli.Context) error { @@ -139,7 +183,11 @@ func cmdInfo(c *cli.Context) error { return fmt.Errorf("This command takes zero arguments") } bx := box.NewFromFlags(c) - return bx.Info() + err := bx.Info() + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdInit(c *cli.Context) error { @@ -147,7 +195,11 @@ func cmdInit(c *cli.Context) error { return fmt.Errorf("This command takes one or two arguments") } bx := box.NewUninitialized(c.String("configdir"), c.String("team")) - return bx.Init(c.Args().First(), c.String("vcs")) + err := bx.Init(c.Args().First(), c.String("vcs")) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdReencrypt(c *cli.Context) error { @@ -163,10 +215,14 @@ func cmdReencrypt(c *cli.Context) error { } bx := box.NewFromFlags(c) - return bx.Reencrypt(c.Args().Slice(), + err := bx.Reencrypt(c.Args().Slice(), c.Bool("overwrite"), pauseNeeded, ) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdShred(c *cli.Context) error { @@ -174,7 +230,11 @@ func cmdShred(c *cli.Context) error { return err } bx := box.NewFromFlags(c) - return bx.Shred(c.Args().Slice()) + err := bx.Shred(c.Args().Slice()) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } func cmdStatus(c *cli.Context) error { @@ -182,7 +242,11 @@ func cmdStatus(c *cli.Context) error { return fmt.Errorf("Can not specify filenames and --all") } bx := box.NewFromFlags(c) - return bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type")) + err := bx.Status(c.Args().Slice(), c.Bool("name-only"), c.String("type")) + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } // These are "secret" commands used by the integration tests. @@ -198,5 +262,9 @@ func testingInit(c *cli.Context) error { c.String("vcs"), ) bx := box.NewForTestingInit(c.String("vcs")) - return bx.TestingInitRepo() + err := bx.TestingInitRepo() + if err != nil { + return err + } + return bx.Vcs.FlushCommits() } diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index e23bcf42..f90de570 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -83,7 +83,7 @@ Then a shell script should run various combinations of VCS and crypters. -rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -run -rm -rf /tmp/bbhome-* && go test -run +rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -long -run +rm -rf /tmp/bbhome-* && go test -long -run ( gbb && cd cmd/blackbox && go install ) && blackbox diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 9a663452..7bc118f3 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -14,6 +14,7 @@ import ( ) var vcsToTest = flag.String("testvcs", "GIT", "VCS to test") +var longTests = flag.Bool("long", false, "Run long version of tests") //var crypterToTest = flag.String("crypter", "GnuPG", "crypter to test") @@ -60,6 +61,9 @@ func setup(t *testing.T) { } func TestInit(t *testing.T) { + if !*longTests { + return + } compile(t) makeHomeDir(t, "init") @@ -75,6 +79,9 @@ func TestInit(t *testing.T) { } func TestList(t *testing.T) { + if !*longTests { + return + } compile(t) makeHomeDir(t, "init") @@ -88,6 +95,9 @@ func TestList(t *testing.T) { } func TestStatus(t *testing.T) { + if !*longTests { + return + } compile(t) makeHomeDir(t, "init") @@ -97,6 +107,9 @@ func TestStatus(t *testing.T) { } func TestShred(t *testing.T) { + if !*longTests { + return + } compile(t) makeHomeDir(t, "shred") runBB(t, "init", "yes") @@ -108,6 +121,9 @@ func TestShred(t *testing.T) { } func TestStatus_notreg(t *testing.T) { + if !*longTests { + return + } compile(t) makeHomeDir(t, "init") @@ -119,6 +135,9 @@ func TestStatus_notreg(t *testing.T) { // TestBasicCommands tests of the basic functions, using a fake homedir and repo. // The files are full of garbage, not real encrypted data. func TestBasic(t *testing.T) { + if !*longTests { + return + } // These are basic tests that work on a fake repo. // The repo has mostly real data, except any .gpg file // is just garbage. diff --git a/models/vcs.go b/models/vcs.go index 65bdc7f0..36521aa1 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -11,8 +11,10 @@ type Vcs interface { SetFileTypeUnix(repobasedir string, files ...string) error // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. IgnoreAnywhere(repobasedir string, files ...string) error - // SuggestTracking tells the VCS to suggest the user commit these files. - SuggestTracking(repobasedir string, message string, files []string) error + // NeedsCommit queues up commits for later execution. + NeedsCommit(message string, repobasedir string, names []string) + // FlushCommits informs the VCS to do queued up commits. + FlushCommits() error // TestingInitRepo initializes a repo of this type (for use by integration tests) TestingInitRepo() error diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index d2ebc003..e6413768 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/tainedname" ) // FileStatus returns the status of a file. @@ -236,3 +237,26 @@ func shouldWeOverwrite() { input := bufio.NewScanner(os.Stdin) input.Scan() } + +// PrettyCommitMessage generates a pretty commit message. +func PrettyCommitMessage(verb string, files []string) string { + + if len(files) == 0 { + return verb + } + + for i := range files { + safe, redacted := tainedname.New(files[i]).RedactUnsafe() + if redacted { + files[i] = "\"" + safe + "\" (redacted)" + } else { + files[i] = safe + } + } + + if len(files) <= 2 || len(strings.Join(files, " ")) < 100 { + return verb + ": " + strings.Join(files, " ") + } + + return verb + ": " + strings.Join(files[:3], " ") + " ..." + " " + strings.Join(files, "\n ") + "\n" +} diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index ed1b92ea..86d351b8 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -1,7 +1,8 @@ package box // This file implements the business logic related to a black box. - +// These functions are usually called from cmd/blackbox/drive.go or +// external sytems that use box as a module. import ( "bufio" "fmt" @@ -23,14 +24,14 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { return err } - fmt.Printf("ADMINS=%q\n", bx.Admins) + //fmt.Printf("ADMINS=%q\n", bx.Admins) // Check for duplicates. if i := sort.SearchStrings(bx.Admins, nom); i < len(bx.Admins) && bx.Admins[i] == nom { return fmt.Errorf("Admin %v already an admin", nom) } - sugg, err := bx.Crypter.AddNewKey(nom, sdir, bx.ConfigDir) + changedFiles, err := bx.Crypter.AddNewKey(nom, sdir, bx.ConfigDir) if err != nil { return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) } @@ -45,7 +46,7 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err) } - bx.Vcs.SuggestTracking(bx.RepoBaseDir, "NEW ADMIN: "+nom, sugg) + bx.Vcs.NeedsCommit("NEW ADMIN: "+nom, bx.RepoBaseDir, changedFiles) return nil } @@ -128,6 +129,13 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup } func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid int) error { + + // TODO(tlim): If we want to decrypt them in parallel, go has a helper function + // called "sync.WaitGroup()"" which would be useful here. We would probably + // want to add a flag on the command line (stored in a field such as bx.ParallelMax) + // that limits the amount of parallelism. The default for the flag should + // probably be runtime.NumCPU(). + for _, name := range names { fmt.Printf("========== DECRYPTING %q\n", name) if !bx.FilesSet[name] { @@ -140,9 +148,9 @@ func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid // TODO(tlim) v1 detects zero-length files and removes them, even // if overwrite is disabled. I don't think anyone has ever used that - // feature. That said, we could immplement that here. + // feature. That said, if we want to do that, we would implement it here. - // TODO(tlim) v1 takes the md5 has of the plaintext before it decrypts, + // TODO(tlim) v1 takes the md5 hash of the plaintext before it decrypts, // then compares the new plaintext's md5. It prints "EXTRACTED" if // there is a change. @@ -217,24 +225,11 @@ func (bx *Box) Encrypt(names []string, shred bool) error { names = bx.Files } - suggestMsg, suggestFiles, err := encryptMany(bx, names, shred) - if err != nil { - return err - } - - if len(suggestFiles) != 0 { - bx.Vcs.SuggestTracking(bx.RepoBaseDir, - "ENCRYPTED "+strings.Join(suggestMsg, " "), - suggestFiles, - ) - } - - return nil + return encryptMany(bx, names, shred) } -func encryptMany(bx *Box, names []string, shred bool) ([]string, []string, error) { - var suggestMsg []string - var suggestFiles []string +func encryptMany(bx *Box, names []string, shred bool) error { + var enames []string for _, name := range names { fmt.Printf("========== ENCRYPTING %q\n", name) if !bx.FilesSet[name] { @@ -245,18 +240,23 @@ func encryptMany(bx *Box, names []string, shred bool) ([]string, []string, error bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name) continue } - s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) + ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { bx.logErr.Printf("Failed to encrypt %q: %v", name, err) continue } - suggestMsg = append(suggestMsg, name) - suggestFiles = append(suggestFiles, s) + enames = append(enames, ename) if shred { bx.Shred([]string{name}) } } - return suggestMsg, suggestFiles, nil + + bx.Vcs.NeedsCommit( + PrettyCommitMessage("REENCRYPTED", enames), + bx.RepoBaseDir, + enames, + ) + return nil } // FileAdd enrolls files. @@ -320,10 +320,11 @@ func (bx *Box) FileAdd(names []string, shred bool) error { bx.logErr.Printf("Error while shredding: %v", err) } - bx.Vcs.SuggestTracking( + bx.Vcs.NeedsCommit( + PrettyCommitMessage("NEW BLACKBOX FILES:", names), bx.RepoBaseDir, - "NEW FILES: "+strings.Join(names, " "), - encryptedNames) + encryptedNames, + ) return nil } @@ -412,10 +413,11 @@ func (bx *Box) Init(yes, vcsname string) error { "secring.gpg", ) - bx.Vcs.SuggestTracking(bx.RepoBaseDir, "INITIALIZE BLACKBOX", + bx.Vcs.NeedsCommit( + "INITIALIZE BLACKBOX", + bx.RepoBaseDir, []string{bbadminsRel, bbfilesRel}, ) - return nil } @@ -472,7 +474,7 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { if err != nil { return fmt.Errorf("reencrypt failed decrypt: %w", err) } - suggestMsg, suggestFiles, err := encryptMany(bx, names, false) + err = encryptMany(bx, names, false) if err != nil { return fmt.Errorf("reencrypt failed encrypt: %w", err) } @@ -481,13 +483,6 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { return fmt.Errorf("reencrypt failed shred: %w", err) } - if len(suggestFiles) != 0 { - bx.Vcs.SuggestTracking(bx.RepoBaseDir, - "ENCRYPTED "+strings.Join(suggestMsg, " "), - suggestFiles, - ) - } - return nil } diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go index 10236215..c0091e91 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/tainedname/tainedname.go @@ -15,6 +15,7 @@ package tainedname import ( "fmt" "strings" + "unicode" ) // Dubious is a string that can't be trusted to work on a shell line without escaping. @@ -145,6 +146,47 @@ func oct() func(r rune) string { return func(r rune) string { return fmt.Sprintf(`\%03o`, r) } } +// RedactUnsafe redacts any "bad" chars, returns true if anything +// redacted. +func (dirty Dubious) RedactUnsafe() (string, bool) { + if dirty == "" { + return `""`, false + } + + var b strings.Builder + b.Grow(len(dirty) + 2) + + needsQuote := false + redacted := false + + for _, r := range dirty { + if r == ' ' { + b.WriteRune(r) + needsQuote = true + } else if r == '\'' { + b.WriteRune(r) + needsQuote = true + } else if r == '"' { + b.WriteRune(r) + needsQuote = true + } else if r == '\'' { + b.WriteRune('X') + needsQuote = true + } else if unicode.IsPrint(r) { + b.WriteRune(r) + } else { + b.WriteRune('X') + redacted = true + } + } + + if needsQuote { + return "'" + b.String() + "'", redacted + } + + return b.String(), redacted +} + // String returns a version of the dirty string that is absolutely // safe to paste into a command line. func (dirty Dubious) String() string { diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 98c85f29..ed917bb9 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -5,6 +5,7 @@ import ( "path/filepath" "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/commitlater" "github.com/StackExchange/blackbox/v2/pkg/tainedname" "github.com/StackExchange/blackbox/v2/pkg/vcs" ) @@ -17,6 +18,7 @@ func init() { // VcsHandle is the handle type VcsHandle struct { + toCommit commitlater.List } func newGit() (vcs.Vcs, error) { @@ -67,18 +69,6 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { return bbutil.AddLinesToFile(ignore, files...) } -// SuggestTracking tells the VCS to suggest the user commit these files. -func (v VcsHandle) SuggestTracking(repobasedir string, message string, files []string) error { - fmt.Printf(` -NEXT STEP: You need to manually check these in: - git commit -m%q`, message) - for _, file := range files { - fmt.Print(" " + tainedname.New(file).String()) - } - fmt.Println() - return nil -} - // Add makes a file visible to the VCS (like "git add"). func (v VcsHandle) Add(repobasedir string, files []string) error { @@ -91,6 +81,32 @@ func (v VcsHandle) Add(repobasedir string, files []string) error { return bbutil.RunBash("git", append([]string{"add"}, gpgnames...)...) } +// NeedsCommit queues up commits for later execution. +func (v VcsHandle) NeedsCommit(message string, repobasedir string, names []string) { + v.toCommit.Add(message, repobasedir, names) +} + +// FlushCommits informs the VCS to do queued up commits. +func (v VcsHandle) FlushCommits() error { + return v.toCommit.Flush(v.suggestCommit) + // TODO(tlim): Some day we can add a command line flag that indicates that commits are + // to be done for real, not just suggested to the user. At that point, this function + // can call v.toCommit.Flush() with a function that actually does the commits insteada + // of suggesting them. Flag could be called --commit=auto vs --commit=suggest. +} + +// suggestCommit tells the user what commits are needed. +func (v VcsHandle) suggestCommit(repobasedir string, message string, files []string) error { + fmt.Printf(` +NEXT STEP: You need to manually check these in: + git commit -m%q`, message) + for _, file := range files { + fmt.Print(" " + tainedname.New(file).String()) + } + fmt.Println() + return nil +} + // The following are "secret" functions only used by the integration testing system. // TestingInitRepo initializes a repo. diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 86efc1b2..90c1e99f 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -14,7 +14,6 @@ func init() { // VcsHandle is type VcsHandle struct { - Age int } func newNone() (vcs.Vcs, error) { @@ -36,13 +35,18 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { return nil } -// IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. +// IgnoreAnywhere tells the VCS to ignore these files anywhere in the repo. func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { return nil } -// SuggestTracking tells the VCS to suggest the user commit these files. -func (v VcsHandle) SuggestTracking(repobasedir string, message string, files []string) error { +// NeedsCommit queues up commits for later execution. +func (v VcsHandle) NeedsCommit(message string, repobasedir string, names []string) { + return +} + +// FlushCommits informs the VCS to do queued up commits. +func (v VcsHandle) FlushCommits() error { return nil } From 4f79084548bbbe15fbb70a4ef071fc06708dcad1 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 3 Jul 2020 05:58:22 -0400 Subject: [PATCH 49/69] add missing files --- pkg/box/pretty_test.go | 31 +++++++++++++++++++++++++++++ pkg/commitlater/commitlater.go | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) create mode 100644 pkg/box/pretty_test.go create mode 100644 pkg/commitlater/commitlater.go diff --git a/pkg/box/pretty_test.go b/pkg/box/pretty_test.go new file mode 100644 index 00000000..29408140 --- /dev/null +++ b/pkg/box/pretty_test.go @@ -0,0 +1,31 @@ +package box + +import "testing" + +func TestPrettyCommitMessage(t *testing.T) { + long := "aVeryVeryLongLongLongStringStringString" + for i, test := range []struct { + data []string + expected string + }{ + {[]string{}, `HEADING`}, + {[]string{"one"}, `HEADING: one`}, + {[]string{"one", "two"}, `HEADING: one two`}, + {[]string{"one", "two", "three"}, `HEADING: one two three`}, + {[]string{"one", "two", "three", "four"}, `HEADING: one two three four`}, + {[]string{"one", "two", "three", "four", "five"}, `HEADING: one two three four five`}, + {[]string{"has spaces.txt"}, `HEADING: 'has spaces.txt'`}, + {[]string{"two\n"}, `HEADING: "twoX" (redacted)`}, + {[]string{"smile😁eyes"}, `HEADING: smile😁eyes`}, + {[]string{"tab\ttab", "two very long strings.txt"}, `HEADING: "tabXtab" (redacted) 'two very long strings.txt'`}, + {[]string{long, long, long, long}, "HEADING: aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString ... aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n"}, + } { + g := PrettyCommitMessage("HEADING", test.data) + if g == test.expected { + //t.Logf("%03d: PASSED files=%q\n", i, test.data) + t.Logf("%03d: PASSED", i) + } else { + t.Errorf("%03d: FAILED files==%q got=(%q) wanted=(%q)\n", i, test.data, g, test.expected) + } + } +} diff --git a/pkg/commitlater/commitlater.go b/pkg/commitlater/commitlater.go new file mode 100644 index 00000000..0e807bfe --- /dev/null +++ b/pkg/commitlater/commitlater.go @@ -0,0 +1,36 @@ +package commitlater + +import "fmt" + +type future struct { + message string // Message that describes this transaction. + dir string // Basedir of the files + files []string // Names of the files + display []string // Names as to be displayed to the user +} + +// List of futures to be done in the future. +type List struct { + items []*future +} + +// Add queues up a future commit. +func (list *List) Add(message string, repobasedir string, files []string) { + item := &future{ + message: message, + dir: repobasedir, + files: files, + } + list.items = append(list.items, item) +} + +// Flush executes queued commits. +func (list *List) Flush(f func(string, string, []string) error) error { + for _, fut := range list.items { + err := f(fut.message, fut.dir, fut.files) + if err != nil { + return fmt.Errorf("commit files (%q) failed: %w", fut.files, err) + } + } + return nil +} From 56ecdc8e50a043e37f604859f5501929068a90b5 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 3 Jul 2020 10:11:45 -0400 Subject: [PATCH 50/69] git commit messages are more pretty --- models/vcs.go | 7 ++++ pkg/box/box.go | 2 +- pkg/box/boxutils.go | 66 ++++++++++++++++++++++--------- pkg/box/pretty_test.go | 4 +- pkg/box/verbs.go | 41 ++++++++++++------- pkg/commitlater/commitlater.go | 59 +++++++++++++++++++++++++-- pkg/tainedname/tainedname.go | 26 +++++++++++- pkg/tainedname/tainedname_test.go | 30 ++++++++++++++ pkg/vcs/git/git.go | 65 ++++++++++++++++++++++++------ pkg/vcs/none/none.go | 9 +++++ 10 files changed, 256 insertions(+), 53 deletions(-) diff --git a/models/vcs.go b/models/vcs.go index 36521aa1..0356e795 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -1,5 +1,7 @@ package models +import "github.com/StackExchange/blackbox/v2/pkg/commitlater" + // Vcs is git/hg/etc. type Vcs interface { // Name returns the plug-in's canonical name. @@ -11,8 +13,13 @@ type Vcs interface { SetFileTypeUnix(repobasedir string, files ...string) error // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. IgnoreAnywhere(repobasedir string, files ...string) error + + // CommitTitle sets the title of the next commit. + CommitTitle(title string) // NeedsCommit queues up commits for later execution. NeedsCommit(message string, repobasedir string, names []string) + // DebugCommits dumps a list of future commits. + DebugCommits() commitlater.List // FlushCommits informs the VCS to do queued up commits. FlushCommits() error diff --git a/pkg/box/box.go b/pkg/box/box.go index 0bd2109e..ff83efd5 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -112,7 +112,7 @@ func NewUninitialized(configdir, team string) *Box { Team: team, } bx.Vcs = vcs.Discover(bx.RepoBaseDir) - bx.ConfigDir = GenerateConfigDir(configdir, team) + bx.ConfigDir, bx.ConfigDirRel = GenerateConfigDir(configdir, team) return bx } diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index e6413768..221ec3b7 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -6,6 +6,7 @@ import ( "os" "os/user" "path/filepath" + "runtime" "strconv" "strings" @@ -141,22 +142,46 @@ func parseGroup(userinput string) (int, error) { // The result is an absolute path. // If --config is set, use that value (no matter what). // Otherwise use .blackbox-$team or .blackbox (if no team). -func GenerateConfigDir(configdir, team string) string { - if configdir != "" { - if p, err := filepath.Abs(configdir); err == nil { - return p +func GenerateConfigDir(configdir, team string) (string, string) { + + if configdir == "" { + // Normal case. + rel := ".blackbox" + if team != "" { + rel = ".blackbox-" + team + } + configdir, err := filepath.Abs(rel) + if err != nil { + fmt.Printf("ERROR: init says: ABS should not happen: %v\n", err) + os.Exit(1) } - return configdir + return configdir, rel } - c := ".blackbox" - if team != "" { - c = ".blackbox-" + team + // User specified a configdir. Now we can to guess what they mean. + + // What's our CWD? + wd, err := os.Getwd() + if err != nil { + fmt.Printf("ERROR: init says: GETWD should not happen: %v\n", err) + os.Exit(1) } - if p, err := filepath.Abs(c); err == nil { - return p + + // They specified a relative path. Assume it is relative to wd. + if !filepath.IsAbs(configdir) { + // Assume they meant it is relative to PWD. + return wd, configdir + } + + // This is the difficult case. The user specified an absolute path to the + // config directory. We probably should just error out. Let's at least make + // one attempt at guessing what the Rel part should be. + rel, err := filepath.Rel(wd, configdir) + if err != nil { + fmt.Printf("ERROR: Run init from the repo base: %v\n", err) + os.Exit(1) } - return c + return configdir, rel } // FindConfigDir tests various places until it finds the config dir. @@ -218,6 +243,12 @@ func gpgAgentNotice() { if os.Getenv("GPG_AGENT_INFO") != "" { return } + // Are we on macOS? + if runtime.GOOS == "darwin" { + // We assume the use of https://gpgtools.org, which + // uses the keychain. + return + } // TODO(tlim): v1 verifies that "gpg-agent --version" outputs a version // string that is 2.1.0 or higher. It seems that 1.x is incompatible. @@ -242,19 +273,16 @@ func shouldWeOverwrite() { func PrettyCommitMessage(verb string, files []string) string { if len(files) == 0 { - return verb + // This use-case should probably be an error. + return verb + " (no files)" } + // Redact the names. for i := range files { - safe, redacted := tainedname.New(files[i]).RedactUnsafe() - if redacted { - files[i] = "\"" + safe + "\" (redacted)" - } else { - files[i] = safe - } + files[i] = tainedname.New(files[i]).RedactUnsafeWithComment() } - if len(files) <= 2 || len(strings.Join(files, " ")) < 100 { + if len(files) <= 2 || len(strings.Join(files, " ")) < 50 { return verb + ": " + strings.Join(files, " ") } diff --git a/pkg/box/pretty_test.go b/pkg/box/pretty_test.go index 29408140..5875eb3a 100644 --- a/pkg/box/pretty_test.go +++ b/pkg/box/pretty_test.go @@ -15,9 +15,9 @@ func TestPrettyCommitMessage(t *testing.T) { {[]string{"one", "two", "three", "four"}, `HEADING: one two three four`}, {[]string{"one", "two", "three", "four", "five"}, `HEADING: one two three four five`}, {[]string{"has spaces.txt"}, `HEADING: 'has spaces.txt'`}, - {[]string{"two\n"}, `HEADING: "twoX" (redacted)`}, + {[]string{"two\n"}, `HEADING: "twoX"(redacted)`}, {[]string{"smile😁eyes"}, `HEADING: smile😁eyes`}, - {[]string{"tab\ttab", "two very long strings.txt"}, `HEADING: "tabXtab" (redacted) 'two very long strings.txt'`}, + {[]string{"tab\ttab", "two very long strings.txt"}, `HEADING: "tabXtab"(redacted) 'two very long strings.txt'`}, {[]string{long, long, long, long}, "HEADING: aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString ... aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n"}, } { g := PrettyCommitMessage("HEADING", test.data) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 86d351b8..a74f22b7 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" + "github.com/StackExchange/blackbox/v2/pkg/tainedname" "github.com/olekukonko/tablewriter" ) @@ -382,10 +383,19 @@ func (bx *Box) Init(yes, vcsname string) error { //fmt.Printf("configdir will be: %q\n", bx.ConfigDir) if yes != "yes" { - fmt.Printf("Enable blackbox for this %v repo? (yes/no)", bx.Vcs.Name()) + fmt.Printf("Enable blackbox for this %v repo? (yes/no)? ", bx.Vcs.Name()) input := bufio.NewScanner(os.Stdin) input.Scan() - b, _ := strconv.ParseBool(input.Text()) + ans := input.Text() + b, err := strconv.ParseBool(ans) + if err != nil { + b = false + if len(ans) > 0 { + if ans[0] == 'y' || ans[0] == 'Y' { + b = true + } + } + } if !b { fmt.Println("Ok. Maybe some other time.") return nil @@ -397,15 +407,13 @@ func (bx *Box) Init(yes, vcsname string) error { return err } - bbadmins := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") - bbutil.Touch(bbadmins) - bbadminsRel := filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt") - bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbadminsRel) - - bbfiles := filepath.Join(bx.ConfigDir, "blackbox-files.txt") - bbutil.Touch(bbfiles) - bbfilesRel := filepath.Join(bx.ConfigDirRel, "blackbox-files.txt") - bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, bbfilesRel) + bbutil.Touch(filepath.Join(bx.ConfigDir, "blackbox-admins.txt")) + bbutil.Touch(filepath.Join(bx.ConfigDir, "blackbox-files.txt")) + bx.Vcs.SetFileTypeUnix( + bx.RepoBaseDir, + filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt"), + filepath.Join(bx.ConfigDirRel, "blackbox-files.txt"), + ) bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, "pubring.gpg~", @@ -413,11 +421,16 @@ func (bx *Box) Init(yes, vcsname string) error { "secring.gpg", ) + fs := []string{ + filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt"), + filepath.Join(bx.ConfigDirRel, "blackbox-files.txt"), + } bx.Vcs.NeedsCommit( - "INITIALIZE BLACKBOX", - bx.RepoBaseDir, - []string{bbadminsRel, bbfilesRel}, + "NEW: "+tainedname.RedactList(fs), + bx.RepoBaseDir, fs, ) + + bx.Vcs.CommitTitle("INITIALIZE BLACKBOX") return nil } diff --git a/pkg/commitlater/commitlater.go b/pkg/commitlater/commitlater.go index 0e807bfe..4c2e8b5f 100644 --- a/pkg/commitlater/commitlater.go +++ b/pkg/commitlater/commitlater.go @@ -1,6 +1,8 @@ package commitlater -import "fmt" +import ( + "fmt" +) type future struct { message string // Message that describes this transaction. @@ -21,16 +23,65 @@ func (list *List) Add(message string, repobasedir string, files []string) { dir: repobasedir, files: files, } + //o := len(list.items) list.items = append(list.items, item) + //n := len(list.items) + //fmt.Printf("LIST=%v (%v) ADD o=%v n=%v\n", list, list.items, o, n) +} + +func sameDirs(l *List) bool { + if len(l.items) <= 1 { + return true + } + for _, k := range l.items[1:] { + if k.dir != l.items[0].dir { + return false + } + } + return true } // Flush executes queued commits. -func (list *List) Flush(f func(string, string, []string) error) error { +func (list *List) Flush( + title string, + fadd func([]string) error, + fcommit func([]string, string, []string) error, +) error { + + // Just list the individual commit commands. + if title == "" || len(list.items) <= 1 || !sameDirs(list) { + for _, fut := range list.items { + err := fadd(fut.files) + if err != nil { + return fmt.Errorf("add files (%q) failed: %w", fut.files, err) + } + err = fcommit([]string{fut.message}, fut.dir, fut.files) + if err != nil { + return fmt.Errorf("commit files (%q) failed: %w", fut.files, err) + } + } + return nil + } + + // Create a long commit message. + var m []string + var f []string for _, fut := range list.items { - err := f(fut.message, fut.dir, fut.files) + err := fadd(fut.files) if err != nil { - return fmt.Errorf("commit files (%q) failed: %w", fut.files, err) + return fmt.Errorf("add files (%q) failed: %w", fut.files, err) } + m = append(m, fut.message) + f = append(f, fut.files...) } + msg := []string{title} + for _, mm := range m { + msg = append(msg, " * "+mm) + } + err := fcommit(msg, list.items[0].dir, f) + if err != nil { + return fmt.Errorf("commit files (%q) failed: %w", f, err) + } + return nil } diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go index c0091e91..c8528759 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/tainedname/tainedname.go @@ -146,8 +146,29 @@ func oct() func(r rune) string { return func(r rune) string { return fmt.Sprintf(`\%03o`, r) } } +// RedactList returns a string of redacted names. +func RedactList(names []string) string { + var msgs []string + for _, f := range names { + msgs = append(msgs, New(f).RedactUnsafeWithComment()) + } + return strings.Join(msgs, " ") +} + +// RedactUnsafeWithComment is like RedactUnsafe but appends +// "(redacted)" when appropriate. +func (dirty Dubious) RedactUnsafeWithComment() string { + s, b := dirty.RedactUnsafe() + if b { + return "\"" + s + "\"(redacted)" + } + return s +} + // RedactUnsafe redacts any "bad" chars, returns true if anything -// redacted. +// redacted. The resulting string should be human readable and +// pastable (for example, something to include in a git commit +// message) but not usable in os.Open(). func (dirty Dubious) RedactUnsafe() (string, bool) { if dirty == "" { return `""`, false @@ -188,7 +209,8 @@ func (dirty Dubious) RedactUnsafe() (string, bool) { } // String returns a version of the dirty string that is absolutely -// safe to paste into a command line. +// safe to paste into a bash command line, and will result in the +// correct filename being interpreted by bash. func (dirty Dubious) String() string { if dirty == "" { return `""` diff --git a/pkg/tainedname/tainedname_test.go b/pkg/tainedname/tainedname_test.go index 09dc9ccc..8300fb8c 100644 --- a/pkg/tainedname/tainedname_test.go +++ b/pkg/tainedname/tainedname_test.go @@ -4,6 +4,36 @@ import ( "testing" ) +func TestRedactUnsafe(t *testing.T) { + for i, test := range []struct{ data, expected string }{ + {"", `""`}, + {"one", "one"}, + {"has space.txt", "'has space.txt'"}, + {"has\ttab.txt", `hasXtab.txtR`}, + {"has\nnl.txt", `hasXnl.txtR`}, + {"has\rret.txt", `hasXret.txtR`}, + {"¡que!", `¡que!`}, + {"thé", `thé`}, + {"pound£", `pound£`}, + {"*.go", `*.go`}, + {"rm -rf / ; echo done", `'rm -rf / ; echo done'`}, + {"smile\u263a", `smile☺`}, + {"dub\U0001D4E6", `dub𝓦`}, + {"four\U0010FFFF", `fourXR`}, + } { + g, b := New(test.data).RedactUnsafe() + if b { + g = g + "R" + } + if g == test.expected { + //jt.Logf("%03d: PASSED go(%q) bash: %s\n", i, test.data, test.expected) + t.Logf("%03d: PASSED\n", i) + } else { + t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) + } + } +} + func TestString(t *testing.T) { for i, test := range []struct{ data, expected string }{ {"", `""`}, diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index ed917bb9..f934d1df 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -3,6 +3,7 @@ package git import ( "fmt" "path/filepath" + "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/commitlater" @@ -18,11 +19,14 @@ func init() { // VcsHandle is the handle type VcsHandle struct { - toCommit commitlater.List + commitTitle string + toCommit *commitlater.List // List of future commits + commitHeader bool // Has the "NEXT STEPS" header been printed? } func newGit() (vcs.Vcs, error) { - return &VcsHandle{}, nil + l := &commitlater.List{} + return &VcsHandle{toCommit: l}, nil } // Name returns my name. @@ -42,19 +46,34 @@ func (v VcsHandle) Discover(repobasedir string) bool { // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { + seen := make(map[string]bool) + // Add to the .gitattributes in the same directory as the file. for _, file := range files { d, n := filepath.Split(file) - err := bbutil.Touch(filepath.Join(repobasedir, d, ".gitattributes")) + af := filepath.Join(repobasedir, d, ".gitattributes") + err := bbutil.Touch(af) if err != nil { return err } - err = bbutil.AddLinesToFile(filepath.Join(repobasedir, d, ".gitattributes"), - fmt.Sprintf("%q text eol=lf", n)) + err = bbutil.AddLinesToFile(af, fmt.Sprintf("%q text eol=lf", n)) if err != nil { return err } + seen[af] = true + } + + var keys []string + for k := range seen { + keys = append(keys, k) } + + v.NeedsCommit( + "set gitattr=UNIX "+tainedname.RedactList(files), + repobasedir, + keys, + ) + return nil } @@ -66,6 +85,12 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { if err != nil { return err } + + v.NeedsCommit( + "gitignore "+tainedname.RedactList(files), + repobasedir, + []string{".gitignore"}, + ) return bbutil.AddLinesToFile(ignore, files...) } @@ -81,14 +106,29 @@ func (v VcsHandle) Add(repobasedir string, files []string) error { return bbutil.RunBash("git", append([]string{"add"}, gpgnames...)...) } +// CommitTitle indicates what the next commit title will be. +// This is used if a group of commits are merged into one. +func (v *VcsHandle) CommitTitle(title string) { + v.commitTitle = title +} + // NeedsCommit queues up commits for later execution. -func (v VcsHandle) NeedsCommit(message string, repobasedir string, names []string) { +func (v *VcsHandle) NeedsCommit(message string, repobasedir string, names []string) { v.toCommit.Add(message, repobasedir, names) } +// DebugCommits dumps the list of future commits. +func (v VcsHandle) DebugCommits() commitlater.List { + return *v.toCommit +} + // FlushCommits informs the VCS to do queued up commits. func (v VcsHandle) FlushCommits() error { - return v.toCommit.Flush(v.suggestCommit) + return v.toCommit.Flush( + v.commitTitle, + func(files []string) error { return bbutil.RunBash("git", append([]string{"add"}, files...)...) }, + v.suggestCommit, + ) // TODO(tlim): Some day we can add a command line flag that indicates that commits are // to be done for real, not just suggested to the user. At that point, this function // can call v.toCommit.Flush() with a function that actually does the commits insteada @@ -96,10 +136,13 @@ func (v VcsHandle) FlushCommits() error { } // suggestCommit tells the user what commits are needed. -func (v VcsHandle) suggestCommit(repobasedir string, message string, files []string) error { - fmt.Printf(` -NEXT STEP: You need to manually check these in: - git commit -m%q`, message) +func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files []string) error { + if !v.commitHeader { + fmt.Printf("NEXT STEP: You need to manually check these in:\n") + } + v.commitHeader = true + + fmt.Print(` git commit -m'`, strings.Join(messages, `' -m'`)+`'`) for _, file := range files { fmt.Print(" " + tainedname.New(file).String()) } diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 90c1e99f..20147896 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -3,6 +3,7 @@ package none import ( "fmt" + "github.com/StackExchange/blackbox/v2/pkg/commitlater" "github.com/StackExchange/blackbox/v2/pkg/vcs" ) @@ -40,11 +41,19 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { return nil } +// CommitTitle sets the title of the next commit. +func (v VcsHandle) CommitTitle(title string) {} + // NeedsCommit queues up commits for later execution. func (v VcsHandle) NeedsCommit(message string, repobasedir string, names []string) { return } +// DebugCommits dumps a list of future commits. +func (v VcsHandle) DebugCommits() commitlater.List { + return commitlater.List{} +} + // FlushCommits informs the VCS to do queued up commits. func (v VcsHandle) FlushCommits() error { return nil From 433b35728658987126915e7fc6b4b1fef9e054ae Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 3 Jul 2020 10:48:12 -0400 Subject: [PATCH 51/69] cleanup --- models/crypters.go | 2 +- pkg/box/box.go | 12 ++++++------ pkg/box/pretty_test.go | 2 +- pkg/box/verbs.go | 3 ++- pkg/crypters/gnupg/gnupg.go | 18 ++++++++++++++++-- pkg/vcs/git/git.go | 4 ++++ 6 files changed, 30 insertions(+), 11 deletions(-) diff --git a/models/crypters.go b/models/crypters.go index f3c24f94..e861fd00 100644 --- a/models/crypters.go +++ b/models/crypters.go @@ -11,5 +11,5 @@ type Crypter interface { // Cat outputs a file, unencrypting if needed. Cat(filename string) ([]byte, error) // AddNewKey extracts keyname from sourcedir's GnuPG chain to destdir keychain. - AddNewKey(keyname, sourcedir, destdir string) ([]string, error) + AddNewKey(keyname, repobasename, sourcedir, destdir string) ([]string, error) } diff --git a/pkg/box/box.go b/pkg/box/box.go index ff83efd5..98df2774 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -58,13 +58,13 @@ const ( // NewFromFlags creates a box using items from flags. Nearly all subcommands use this. func NewFromFlags(c *cli.Context) *Box { /* - Nearly all subcommands use this. It is used with a VCS repo - that has blackbox already initialized. + Nearly all subcommands use this. It is used with a VCS repo + that has blackbox already initialized. - Commands need: How we populate it: - bx.Vcs: Discovered by calling each plug-in until succeeds. - bx.ConfigDir: Is discovered. - bx.RepoBaseDir: Is discovered. + Commands need: How we populate it: + bx.Vcs: Discovered by calling each plug-in until succeeds. + bx.ConfigDir: Is discovered. + bx.RepoBaseDir: Is discovered. */ logErr = bblog.GetErr() diff --git a/pkg/box/pretty_test.go b/pkg/box/pretty_test.go index 5875eb3a..d6621f73 100644 --- a/pkg/box/pretty_test.go +++ b/pkg/box/pretty_test.go @@ -8,7 +8,7 @@ func TestPrettyCommitMessage(t *testing.T) { data []string expected string }{ - {[]string{}, `HEADING`}, + {[]string{}, `HEADING (no files)`}, {[]string{"one"}, `HEADING: one`}, {[]string{"one", "two"}, `HEADING: one two`}, {[]string{"one", "two", "three"}, `HEADING: one two three`}, diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index a74f22b7..5accc92b 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -32,7 +32,8 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { return fmt.Errorf("Admin %v already an admin", nom) } - changedFiles, err := bx.Crypter.AddNewKey(nom, sdir, bx.ConfigDir) + fmt.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir) + changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigDir) if err != nil { return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 5d770ba4..432981b7 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -6,6 +6,7 @@ import ( "log" "os" "os/exec" + "path/filepath" "syscall" "github.com/StackExchange/blackbox/v2/pkg/bblog" @@ -123,7 +124,7 @@ func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []strin // AddNewKey extracts keyname from sourcedir's GnuPG chain to destdir keychain. // It returns a list of files that may have changed. -func (crypt CrypterHandle) AddNewKey(keyname, sourcedir, destdir string) ([]string, error) { +func (crypt CrypterHandle) AddNewKey(keyname, repobasedir, sourcedir, destdir string) ([]string, error) { // $GPG --homedir="$2" --export -a "$KEYNAME" >"$pubkeyfile" args := []string{ @@ -153,6 +154,19 @@ func (crypt CrypterHandle) AddNewKey(keyname, sourcedir, destdir string) ([]stri } // Suggest: ${pubring_path} trustdb.gpg blackbox-admins.txt + var changed []string - return nil, nil + // Prefix each file with the relative path to it. + prefix, err := filepath.Rel(repobasedir, destdir) + if err != nil { + fmt.Printf("FAIL (%v) (%v) (%v)\n", repobasedir, destdir, err) + prefix = destdir + } + for _, file := range []string{"pubring.gpg", "pubring.kbx", "trustdb.gpg"} { + path := filepath.Join(destdir, file) + if bbutil.FileExistsOrProblem(path) { + changed = append(changed, filepath.Join(prefix, file)) + } + } + return changed, nil } diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index f934d1df..4c85d315 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -97,6 +97,10 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { // Add makes a file visible to the VCS (like "git add"). func (v VcsHandle) Add(repobasedir string, files []string) error { + if len(files) == 0 { + return nil + } + // TODO(tlim): Make sure that files are within repobasedir. var gpgnames []string From c0d18838c001b75d3303787f917cacfe68c21ca9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 3 Jul 2020 11:48:13 -0400 Subject: [PATCH 52/69] Fixing commits --- integrationTest/NOTES.txt | 2 ++ models/vcs.go | 6 ++-- pkg/box/box.go | 13 +++++++-- pkg/box/boxutils.go | 18 ++++++------ pkg/box/verbs.go | 21 +++++++++----- pkg/vcs/git/git.go | 58 +++++++++++++++++++++++++++++++++++++-- pkg/vcs/none/none.go | 7 ++++- 7 files changed, 103 insertions(+), 22 deletions(-) diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index f90de570..29f23b65 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -87,3 +87,5 @@ rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -long -run rm -rf /tmp/bbhome-* && go test -long -run ( gbb && cd cmd/blackbox && go install ) && blackbox + +cd /tmp && rm -rf /tmp/bbhome-* ; mkdir /tmp/bbhome-1 ; cd /tmp/bbhome-1 && git init ; ( gbb && cd cmd/blackbox && go install ) && blackbox init yes && gitmeWork ; git commit -mm -a ; blackbox admin add tlimoncelli ; git commit -mnewadmin -a ; echo secrt > secret.txt ; blackbox file add secret.txt diff --git a/models/vcs.go b/models/vcs.go index 0356e795..29aa96e0 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -11,8 +11,10 @@ type Vcs interface { // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. SetFileTypeUnix(repobasedir string, files ...string) error - // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. - IgnoreAnywhere(repobasedir string, files ...string) error + // IgnoreAnywhere tells the VCS to ignore these files anywhere in the repo. + IgnoreAnywhere(repobasedir string, files []string) error + // IgnoreAnywhere tells the VCS to ignore these files, rooted in the base of the repo. + IgnoreFiles(repobasedir string, files []string) error // CommitTitle sets the title of the next commit. CommitTitle(title string) diff --git a/pkg/box/box.go b/pkg/box/box.go index 98df2774..653b6256 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -78,6 +78,16 @@ func NewFromFlags(c *cli.Context) *Box { logDebug: bblog.GetDebug(c.Bool("verbose")), } + var err error + + // Assume we are chdir'ed to the base of the repo. + // TODO(tlim): In the future, we'll want the utilities to work from anywhere + // in the repo, but this is fine for now. + bx.RepoBaseDir, err = os.Getwd() + if err != nil { + bx.RepoBaseDir = "." + } + // Discover which kind of VCS is in use. bx.Vcs = vcs.Discover(bx.RepoBaseDir) @@ -89,8 +99,7 @@ func NewFromFlags(c *cli.Context) *Box { } // Are we using .blackbox or what? - var err error - bx.ConfigDir, err = FindConfigDir(c.String("config"), c.String("team")) + bx.ConfigDir, bx.ConfigDirRel, err = FindConfigDir(c.String("config"), c.String("team")) if err != nil { return nil } diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 221ec3b7..233a3399 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -185,19 +185,20 @@ func GenerateConfigDir(configdir, team string) (string, string) { } // FindConfigDir tests various places until it finds the config dir. -func FindConfigDir(configdir, team string) (string, error) { +// If we can't determine the relative path, "" is returned. +func FindConfigDir(configdir, team string) (string, string, error) { // if configdir is set, use it. if configdir != "" { logDebug.Printf("CONFIG IS SET. NOT DISCOVERING: %q\n", configdir) _, err := os.Stat(configdir) if err != nil { - return "", fmt.Errorf("config dir %q error: %v", configdir, err) + return "", "", fmt.Errorf("config dir %q error: %v", configdir, err) } if _, err := filepath.Abs(configdir); err != nil { - return "", fmt.Errorf("config dir abs %q error: %v", configdir, err) + return "", "", fmt.Errorf("config dir abs %q error: %v", configdir, err) } - return configdir, nil + return configdir, "", nil } // Otherwise, search up the tree for the config dir. @@ -213,7 +214,7 @@ func FindConfigDir(configdir, team string) (string, error) { // Prevent an infinite loop by only doing "cd .." this many times maxDirLevels := 100 - relpath := "" + relpath := "." for i := 0; i < maxDirLevels; i++ { // Does relpath contain any of our directory names? for _, c := range candidates { @@ -221,10 +222,11 @@ func FindConfigDir(configdir, team string) (string, error) { logDebug.Printf("Trying %q\n", t) d, err := bbutil.DirExists(t) if err != nil { - return "", fmt.Errorf("dirExists(%q) failed: %v", t, err) + return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) } if d { - return filepath.Abs(t) + a, e := filepath.Abs(t) + return a, c, e } } // If we are at the root, stop. @@ -235,7 +237,7 @@ func FindConfigDir(configdir, team string) (string, error) { relpath = filepath.Join("..", relpath) } - return "", fmt.Errorf("No .blackbox directory found in cwd or above") + return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") } func gpgAgentNotice() { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 5accc92b..6d39e1ce 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -298,13 +298,13 @@ func (bx *Box) FileAdd(names []string, shred bool) error { } // Encrypt - var encryptedNames []string + var needsCommit []string for _, name := range names { s, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) } - encryptedNames = append(encryptedNames, s) + needsCommit = append(needsCommit, s) } // TODO(tlim): Try the json file. @@ -322,10 +322,12 @@ func (bx *Box) FileAdd(names []string, shred bool) error { bx.logErr.Printf("Error while shredding: %v", err) } + bx.Vcs.IgnoreFiles(bx.RepoBaseDir, names) + bx.Vcs.NeedsCommit( - PrettyCommitMessage("NEW BLACKBOX FILES:", names), + PrettyCommitMessage("ADDING TO BLACKBOX", names), bx.RepoBaseDir, - encryptedNames, + append([]string{filepath.Join(bx.ConfigDirRel, "blackbox-files.txt")}, needsCommit...), ) return nil } @@ -364,10 +366,15 @@ func (bx *Box) Info() error { //fmt.Printf("bx.Files=%q\n", bx.Files) fmt.Println("BLACKBOX:") - fmt.Printf(" ConfigDir: %q\n", bx.ConfigDir) + fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) + fmt.Printf(" ConfigDir: %q\n", bx.ConfigDir) + fmt.Printf(" ConfigDirRel: %q\n", bx.ConfigDirRel) + fmt.Printf(" Umask: %O\n", bx.Umask) + fmt.Printf(" Edditor: %v\n", bx.Editor) fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) + fmt.Printf(" FilesSet: count=%v\n", len(bx.FilesSet)) fmt.Printf(" Vcs: %v\n", bx.Vcs) fmt.Printf(" VcsName: %q\n", bx.Vcs.Name()) fmt.Printf(" Crypter: %v\n", bx.Crypter) @@ -416,11 +423,11 @@ func (bx *Box) Init(yes, vcsname string) error { filepath.Join(bx.ConfigDirRel, "blackbox-files.txt"), ) - bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, + bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, []string{ "pubring.gpg~", "pubring.kbx~", "secring.gpg", - ) + }) fs := []string{ filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt"), diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 4c85d315..6b3f0c09 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -78,7 +78,7 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { } // IgnoreAnywhere tells the VCS to ignore these files anywhere rin the repo. -func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { +func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error { // Add to the .gitignore file in the repobasedir. ignore := filepath.Join(repobasedir, ".gitignore") err := bbutil.Touch(ignore) @@ -86,12 +86,66 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { return err } + err = bbutil.AddLinesToFile(ignore, files...) + if err != nil { + return err + } + + v.NeedsCommit( + "gitignore "+tainedname.RedactList(files), + repobasedir, + []string{".gitignore"}, + ) + return nil +} + +func gitSafeFilename(name string) string { + // TODO(tlim): Add unit tests. + // TODO(tlim): Confirm that *?[] escaping works. + if name == "" { + return "ERROR" + } + var b strings.Builder + b.Grow(len(name) + 2) + for _, r := range name { + if r == ' ' || r == '*' || r == '?' || r == '[' || r == ']' { + b.WriteRune('\\') + b.WriteRune(r) + } else { + b.WriteRune(r) + } + } + if name[0] == '!' || name[0] == '#' { + return `\` + b.String() + } + return b.String() +} + +// IgnoreFiles tells the VCS to ignore these files, specified relative to RepoBaseDir. +func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error { + + var lines []string + for _, f := range files { + lines = append(lines, "/"+gitSafeFilename(f)) + } + + // Add to the .gitignore file in the repobasedir. + ignore := filepath.Join(repobasedir, ".gitignore") + err := bbutil.Touch(ignore) + if err != nil { + return err + } + err = bbutil.AddLinesToFile(ignore, lines...) + if err != nil { + return err + } + v.NeedsCommit( "gitignore "+tainedname.RedactList(files), repobasedir, []string{".gitignore"}, ) - return bbutil.AddLinesToFile(ignore, files...) + return nil } // Add makes a file visible to the VCS (like "git add"). diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index 20147896..f41a2da3 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -37,7 +37,12 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { } // IgnoreAnywhere tells the VCS to ignore these files anywhere in the repo. -func (v VcsHandle) IgnoreAnywhere(repobasedir string, files ...string) error { +func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error { + return nil +} + +// IgnoreFiles tells the VCS to ignore these files anywhere in the repo. +func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error { return nil } From 0eb9fd1b711102b2ddc6d37a14ebc5ac3510896d Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 4 Jul 2020 14:02:48 -0400 Subject: [PATCH 53/69] bx.* dirs are now relative --- cmd/blackbox/drive.go | 26 ++++++++ models/vcs.go | 4 +- pkg/bbutil/filestats.go | 29 ++++++++ pkg/bbutil/runbash.go | 12 ++++ pkg/box/box.go | 130 +++++++++++++++--------------------- pkg/box/boxutils.go | 90 ++++--------------------- pkg/box/verbs.go | 43 ++++++------ pkg/crypters/gnupg/gnupg.go | 1 + pkg/vcs/git/git.go | 26 ++++++-- pkg/vcs/none/none.go | 12 +++- pkg/vcs/vcs.go | 37 ++++++++-- 11 files changed, 219 insertions(+), 191 deletions(-) diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index 4d3983a5..e8cc4fbc 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -31,6 +31,14 @@ func allOrSomeFiles(c *cli.Context) error { return nil } +const roError = `This command is disabled due to --config flag being used. +We can not determine if the flag's value is in or out of the repo, and +Blackbox can only work on one repo at a time. If the value is inside the +repo, and you'd like to suggest an algorithm that would let us determine that +automatically, please file a bug. We'd love to have this work better. In the +meanwhile, run this command without the --config flag, perhaps after cd'ing +to the base of the repo.` + // Keep these functions in alphabetical order. func cmdAdminAdd(c *cli.Context) error { @@ -39,6 +47,9 @@ func cmdAdminAdd(c *cli.Context) error { "Must specify one admin's GnuPG user-id (i.e. email address) and optionally the directory of the pubkey data (default ~/.GnuPG)") } bx := box.NewFromFlags(c) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.AdminAdd(c.Args().Get(0), c.Args().Get(1)) if err != nil { return err @@ -63,6 +74,9 @@ func cmdAdminRemove(c *cli.Context) error { return fmt.Errorf("Must specify at least one admin's GnuPG user-id (i.e. email address)") } bx := box.NewFromFlags(c) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.AdminRemove(c.Args().Slice()) if err != nil { return err @@ -147,6 +161,9 @@ func cmdFileAdd(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.FileAdd(c.Args().Slice(), c.Bool("shred")) if err != nil { return err @@ -171,6 +188,9 @@ func cmdFileRemove(c *cli.Context) error { return fmt.Errorf("Must specify at least one file name") } bx := box.NewFromFlags(c) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.FileRemove(c.Args().Slice()) if err != nil { return err @@ -195,6 +215,9 @@ func cmdInit(c *cli.Context) error { return fmt.Errorf("This command takes one or two arguments") } bx := box.NewUninitialized(c.String("configdir"), c.String("team")) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.Init(c.Args().First(), c.String("vcs")) if err != nil { return err @@ -262,6 +285,9 @@ func testingInit(c *cli.Context) error { c.String("vcs"), ) bx := box.NewForTestingInit(c.String("vcs")) + if bx.ConfigRO { + return fmt.Errorf(roError) + } err := bx.TestingInitRepo() if err != nil { return err diff --git a/models/vcs.go b/models/vcs.go index 29aa96e0..fb1ffb4c 100644 --- a/models/vcs.go +++ b/models/vcs.go @@ -6,8 +6,8 @@ import "github.com/StackExchange/blackbox/v2/pkg/commitlater" type Vcs interface { // Name returns the plug-in's canonical name. Name() string - // Discover returns true if the cwd is a VCS of this type. - Discover(repobasedir string) bool + // Discover returns true if we are a repo of this type; along with the Abs path to the repo root (or "" if we don't know). + Discover() (bool, string) // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. SetFileTypeUnix(repobasedir string, files ...string) error diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 6b673221..ea15f2f3 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -5,6 +5,7 @@ import ( "io/ioutil" "os" "os/exec" + "path/filepath" "sort" "strings" "time" @@ -145,3 +146,31 @@ func AddLinesToFile(filename string, newlines ...string) error { } return nil } + +// FindDirInParent looks for target in CWD, or .., or ../.., etc. +func FindDirInParent(target string) (string, error) { + // Prevent an infinite loop by only doing "cd .." this many times + maxDirLevels := 30 + relpath := "." + for i := 0; i < maxDirLevels; i++ { + // Does relpath contain our target? + t := filepath.Join(relpath, target) + //logDebug.Printf("Trying %q\n", t) + _, err := os.Stat(t) + if err == nil { + return t, nil + } + if !os.IsNotExist(err) { + return "", fmt.Errorf("stat failed FindDirInParent (%q): %w", t, err) + } + // Ok, it really wasn't found. + + // If we are at the root, stop. + if abs, err := filepath.Abs(relpath); err == nil && abs == "/" { + break + } + // Try one directory up + relpath = filepath.Join("..", relpath) + } + return "", fmt.Errorf("Not found") +} diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index d1e6cc54..0fdabcb9 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -37,6 +37,18 @@ func RunBashOutput(command string, args ...string) (string, error) { return string(out), err } +// RunBashOutputSilent runs a Bash command, captures output, discards stderr. +func RunBashOutputSilent(command string, args ...string) (string, error) { + cmd := exec.Command(command, args...) + cmd.Stdin = os.Stdin + // Leave cmd.Stderr unmodified and stderr is discarded. + out, err := cmd.Output() + if err != nil { + return "", fmt.Errorf("RunBashOutputSilent err=%w", err) + } + return string(out), err +} + // RunBashInput runs a Bash command, sends input on stdin. func RunBashInput(input string, command string, args ...string) error { diff --git a/pkg/box/box.go b/pkg/box/box.go index 653b6256..939fc269 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -23,10 +23,10 @@ var logDebug *log.Logger // Box describes what we know about a box. type Box struct { // Paths: - Team string // Name of the team (i.e. .blackbox-$TEAM) TODO(tlim): Can this be deleted? - RepoBaseDir string // Abs path to the VCS repo. - ConfigDir string // Abs path to the .blackbox (or whatever) directory. - ConfigDirRel string // Path to the .blackbox (or whatever) directory relative to RepoBaseDir + Team string // Name of the team (i.e. .blackbox-$TEAM) + RepoBaseDir string // Rel path to the VCS repo. + ConfigPath string // Abs or Rel path to the .blackbox (or whatever) directory. + ConfigRO bool // True if we should not try to change files in ConfigPath. // Settings: Umask int // umask to set when decrypting Editor string // Editor to call @@ -57,15 +57,10 @@ const ( // NewFromFlags creates a box using items from flags. Nearly all subcommands use this. func NewFromFlags(c *cli.Context) *Box { - /* - Nearly all subcommands use this. It is used with a VCS repo - that has blackbox already initialized. - Commands need: How we populate it: - bx.Vcs: Discovered by calling each plug-in until succeeds. - bx.ConfigDir: Is discovered. - bx.RepoBaseDir: Is discovered. - */ + // The goal of this is to create a fully-populated box (and box.Vcs) + // so that all subcommands have all the fields and interfaces they need + // to do their job. logErr = bblog.GetErr() logDebug = bblog.GetDebug(c.Bool("debug")) @@ -78,50 +73,75 @@ func NewFromFlags(c *cli.Context) *Box { logDebug: bblog.GetDebug(c.Bool("verbose")), } - var err error + // Discover which kind of VCS is in use, and the repo root. + bx.Vcs, bx.RepoBaseDir = vcs.Discover() + // TODO: Tell the Vcs about bx.RepoBaseDir? - // Assume we are chdir'ed to the base of the repo. - // TODO(tlim): In the future, we'll want the utilities to work from anywhere - // in the repo, but this is fine for now. - bx.RepoBaseDir, err = os.Getwd() - if err != nil { - bx.RepoBaseDir = "." - } - - // Discover which kind of VCS is in use. - bx.Vcs = vcs.Discover(bx.RepoBaseDir) - - // Pick a crypto backend (GnuPG, go-openpgp, etc.) + // Discover the crypto backend (GnuPG, go-openpgp, etc.) bx.Crypter = crypters.SearchByName(c.String("crypto"), c.Bool("debug")) if bx.Crypter == nil { fmt.Printf("ERROR! No CRYPTER found! Please set --crypto correctly or use the damn default\n") os.Exit(1) } - // Are we using .blackbox or what? - bx.ConfigDir, bx.ConfigDirRel, err = FindConfigDir(c.String("config"), c.String("team")) - if err != nil { - return nil + // Find the .blackbox (or equiv.) directory. + var err error + configFlag := c.String("config") + if configFlag == "" { + // Normal path. Flag not set, so we discover the path. + bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team")) + if err != nil { + fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n") + os.Exit(1) + } + } else { + // Flag is set. Better make sure it is valid. + if !filepath.IsAbs(configFlag) { + fmt.Printf("config flag value is a relative path. Too risky. Exiting.\n") + os.Exit(1) + // TODO(tlim): We could take the filepath.Abs(config) but until someone + // shows a use-case, just fail. + } + bx.ConfigPath = configFlag + bx.ConfigRO = true // External configs treated as read-only. + // TODO(tlim): We could get fancy here and set ConfigReadOnly=true only + // if we are sure configFlag is not within bx.RepoBaseDir. } return bx } // NewUninitialized creates a box in a pre-init situation. -func NewUninitialized(configdir, team string) *Box { +func NewUninitialized(configflag, team string) *Box { /* This is for "blackbox init" (used before ".blackbox*" exists) Init needs: How we populate it: bx.Vcs: Discovered by calling each plug-in until succeeds. bx.ConfigDir: Generated algorithmically (it doesn't exist yet). - bx.RepoBaseDir: Generated algorithmically (it doesn't exist yet). + bx.RepoBaseDir: Generated algorithmically (it doesn't exist yet). */ bx := &Box{ Team: team, } - bx.Vcs = vcs.Discover(bx.RepoBaseDir) - bx.ConfigDir, bx.ConfigDirRel = GenerateConfigDir(configdir, team) + bx.Vcs, bx.RepoBaseDir = vcs.Discover() + if configflag == "" { + rel := ".blackbox" + if team != "" { + rel = ".blackbox-" + team + } + bx.ConfigPath = filepath.Join(bx.RepoBaseDir, rel) + } else { + // Wait. The user is using the --config flag on a repo that + // hasn't been created yet? I hope this works! + fmt.Printf("ERROR: You can not set --config when initializing a new repo. Please run this command from within a repo, with no --config flag. Or, file a bug explaining your use caseyour use-case. Exiting!\n") + os.Exit(1) + // TODO(tlim): We could get fancy here and query the Vcs to see if the + // path would fall within the repo, figure out the relative path, and + // use that value. (and error if configflag is not within the repo). + // That would be error prone and would only help the zero users that + // ever see the above error message. + } return bx } @@ -130,9 +150,7 @@ func NewForTestingInit(vcsname string) *Box { /* This is for "blackbox test_init" (secret command used in integration tests; when nothing exists) - TestingInitRepo only uses bx.Vcs, so that's all we set. - Populates bx.Vcs by finding the provider named vcsname. */ bx := &Box{} @@ -154,44 +172,6 @@ func NewForTestingInit(vcsname string) *Box { return bx } -// func findBaseAndConfigDir() (repodir, configdir string, err error) { - -// // Otherwise, search up the tree for the config dir. - -// candidates := []string{} -// if team := os.Getenv("BLACKBOX_TEAM"); team != "" { -// candidates = append([]string{".blackbox-" + team}, candidates...) -// } -// candidates = append(candidates, ".blackbox") -// candidates = append(candidates, "keyrings/live") - -// // Prevent an infinite loop by only doing "cd .." this many times -// maxDirLevels := 100 - -// relpath := "" -// for i := 0; i < maxDirLevels; i++ { -// // Does relpath contain any of our directory names? -// for _, c := range candidates { -// t := filepath.Join(relpath, c) -// d, err := bbutil.DirExists(t) -// if err != nil { -// return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) -// } -// if d { -// return relpath, t, nil -// } -// } -// // If we are at the root, stop. -// if abs, _ := filepath.Abs(relpath); abs == "/" { -// break -// } -// // Try one directory up -// relpath = filepath.Join("..", relpath) -// } - -// return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") -// } - func (bx *Box) getAdmins() error { // Memoized if len(bx.Admins) != 0 { @@ -201,7 +181,7 @@ func (bx *Box) getAdmins() error { // TODO(tlim): Try the json file. // Try the legacy file: - fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") + fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") bx.logDebug.Printf("Admins file: %q", fn) a, err := bbutil.ReadFileLines(fn) if err != nil { @@ -224,7 +204,7 @@ func (bx *Box) getFiles() error { // TODO(tlim): Try the json file. // Try the legacy file: - fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") + fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt") bx.logDebug.Printf("Files file: %q", fn) a, err := bbutil.ReadFileLines(fn) if err != nil { diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 233a3399..84cd9395 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -10,7 +10,6 @@ import ( "strconv" "strings" - "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/tainedname" ) @@ -137,98 +136,37 @@ func parseGroup(userinput string) (int, error) { return -1, err } -// GenerateConfigDir calculates configdir for uninitialized -// repos (where discovery won't work). -// The result is an absolute path. -// If --config is set, use that value (no matter what). -// Otherwise use .blackbox-$team or .blackbox (if no team). -func GenerateConfigDir(configdir, team string) (string, string) { - - if configdir == "" { - // Normal case. - rel := ".blackbox" - if team != "" { - rel = ".blackbox-" + team - } - configdir, err := filepath.Abs(rel) - if err != nil { - fmt.Printf("ERROR: init says: ABS should not happen: %v\n", err) - os.Exit(1) - } - return configdir, rel - } - - // User specified a configdir. Now we can to guess what they mean. - - // What's our CWD? - wd, err := os.Getwd() - if err != nil { - fmt.Printf("ERROR: init says: GETWD should not happen: %v\n", err) - os.Exit(1) - } - - // They specified a relative path. Assume it is relative to wd. - if !filepath.IsAbs(configdir) { - // Assume they meant it is relative to PWD. - return wd, configdir - } - - // This is the difficult case. The user specified an absolute path to the - // config directory. We probably should just error out. Let's at least make - // one attempt at guessing what the Rel part should be. - rel, err := filepath.Rel(wd, configdir) - if err != nil { - fmt.Printf("ERROR: Run init from the repo base: %v\n", err) - os.Exit(1) - } - return configdir, rel -} - // FindConfigDir tests various places until it finds the config dir. // If we can't determine the relative path, "" is returned. -func FindConfigDir(configdir, team string) (string, string, error) { - - // if configdir is set, use it. - if configdir != "" { - logDebug.Printf("CONFIG IS SET. NOT DISCOVERING: %q\n", configdir) - _, err := os.Stat(configdir) - if err != nil { - return "", "", fmt.Errorf("config dir %q error: %v", configdir, err) - } - if _, err := filepath.Abs(configdir); err != nil { - return "", "", fmt.Errorf("config dir abs %q error: %v", configdir, err) - } - return configdir, "", nil - } +func FindConfigDir(reporoot, team string) (string, error) { - // Otherwise, search up the tree for the config dir. candidates := []string{} if team != "" { - candidates = append([]string{".blackbox-" + team}, candidates...) - } else { - candidates = append(candidates, ".blackbox") + candidates = append(candidates, ".blackbox-"+team) } + candidates = append(candidates, ".blackbox") candidates = append(candidates, "keyrings/live") - logDebug.Printf("DEBUG: candidates = %q\n", candidates) - // Prevent an infinite loop by only doing "cd .." this many times - maxDirLevels := 100 + maxDirLevels := 30 // Prevent an infinite loop relpath := "." for i := 0; i < maxDirLevels; i++ { // Does relpath contain any of our directory names? for _, c := range candidates { t := filepath.Join(relpath, c) logDebug.Printf("Trying %q\n", t) - d, err := bbutil.DirExists(t) - if err != nil { - return "", "", fmt.Errorf("dirExists(%q) failed: %v", t, err) + fi, err := os.Stat(t) + if err == nil && fi.IsDir() { + return t, nil } - if d { - a, e := filepath.Abs(t) - return a, c, e + if err == nil { + return "", fmt.Errorf("path %q is not a directory: %w", t, err) + } + if !os.IsNotExist(err) { + return "", fmt.Errorf("dirExists access error: %w", err) } } + // If we are at the root, stop. if abs, _ := filepath.Abs(relpath); abs == "/" { break @@ -237,7 +175,7 @@ func FindConfigDir(configdir, team string) (string, string, error) { relpath = filepath.Join("..", relpath) } - return "", "", fmt.Errorf("No .blackbox directory found in cwd or above") + return "", fmt.Errorf("No .blackbox (or equiv) directory found") } func gpgAgentNotice() { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 6d39e1ce..8b2db495 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -33,7 +33,7 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { } fmt.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir) - changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigDir) + changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigPath) if err != nil { return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) } @@ -41,7 +41,7 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { // TODO(tlim): Try the json file. // Try the legacy file: - fn := filepath.Join(bx.ConfigDir, "blackbox-admins.txt") + fn := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") bx.logDebug.Printf("Admins file: %q", fn) err = bbutil.AddLinesToSortedFile(fn, nom) if err != nil { @@ -310,7 +310,7 @@ func (bx *Box) FileAdd(names []string, shred bool) error { // TODO(tlim): Try the json file. // Try the legacy file: - fn := filepath.Join(bx.ConfigDir, "blackbox-files.txt") + fn := filepath.Join(bx.ConfigPath, "blackbox-files.txt") bx.logDebug.Printf("Files file: %q", fn) err = bbutil.AddLinesToSortedFile(fn, names...) if err != nil { @@ -327,7 +327,7 @@ func (bx *Box) FileAdd(names []string, shred bool) error { bx.Vcs.NeedsCommit( PrettyCommitMessage("ADDING TO BLACKBOX", names), bx.RepoBaseDir, - append([]string{filepath.Join(bx.ConfigDirRel, "blackbox-files.txt")}, needsCommit...), + append([]string{filepath.Join(bx.ConfigPath, "blackbox-files.txt")}, needsCommit...), ) return nil } @@ -368,9 +368,8 @@ func (bx *Box) Info() error { fmt.Println("BLACKBOX:") fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) - fmt.Printf(" ConfigDir: %q\n", bx.ConfigDir) - fmt.Printf(" ConfigDirRel: %q\n", bx.ConfigDirRel) - fmt.Printf(" Umask: %O\n", bx.Umask) + fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath) + fmt.Printf(" Umask: %o\n", bx.Umask) fmt.Printf(" Edditor: %v\n", bx.Editor) fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) @@ -385,10 +384,10 @@ func (bx *Box) Info() error { // Init initializes a repo. func (bx *Box) Init(yes, vcsname string) error { - //fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) + fmt.Printf("VCS root is: %q\n", bx.RepoBaseDir) - //fmt.Printf("team is: %q\n", bx.Team) - //fmt.Printf("configdir will be: %q\n", bx.ConfigDir) + fmt.Printf("team is: %q\n", bx.Team) + fmt.Printf("configdir will be: %q\n", bx.ConfigPath) if yes != "yes" { fmt.Printf("Enable blackbox for this %v repo? (yes/no)? ", bx.Vcs.Name()) @@ -410,18 +409,16 @@ func (bx *Box) Init(yes, vcsname string) error { } } - err := os.Mkdir(bx.ConfigDir, 0o750) + err := os.Mkdir(bx.ConfigPath, 0o750) if err != nil { return err } - bbutil.Touch(filepath.Join(bx.ConfigDir, "blackbox-admins.txt")) - bbutil.Touch(filepath.Join(bx.ConfigDir, "blackbox-files.txt")) - bx.Vcs.SetFileTypeUnix( - bx.RepoBaseDir, - filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt"), - filepath.Join(bx.ConfigDirRel, "blackbox-files.txt"), - ) + ba := filepath.Join(bx.ConfigPath, "blackbox-admins.txt") + bf := filepath.Join(bx.ConfigPath, "blackbox-files.txt") + bbutil.Touch(ba) + bbutil.Touch(bf) + bx.Vcs.SetFileTypeUnix(bx.RepoBaseDir, ba, bf) bx.Vcs.IgnoreAnywhere(bx.RepoBaseDir, []string{ "pubring.gpg~", @@ -429,13 +426,11 @@ func (bx *Box) Init(yes, vcsname string) error { "secring.gpg", }) - fs := []string{ - filepath.Join(bx.ConfigDirRel, "blackbox-admins.txt"), - filepath.Join(bx.ConfigDirRel, "blackbox-files.txt"), - } + fs := []string{ba, bf} bx.Vcs.NeedsCommit( "NEW: "+tainedname.RedactList(fs), - bx.RepoBaseDir, fs, + bx.RepoBaseDir, + fs, ) bx.Vcs.CommitTitle("INITIALIZE BLACKBOX") @@ -604,7 +599,7 @@ func (bx *Box) TestingInitRepo() error { if err != nil { return fmt.Errorf("TestingInitRepo returned: %w", err) } - if !bx.Vcs.Discover("") { + if b, _ := bx.Vcs.Discover(); !b { return fmt.Errorf("TestingInitRepo failed Discovery") } return nil diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index 432981b7..ef485443 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -148,6 +148,7 @@ func (crypt CrypterHandle) AddNewKey(keyname, repobasedir, sourcedir, destdir st "--import", } crypt.logDebug.Printf("ADDNEWKEY: Importing: gpg %v\n", args) + // fmt.Printf("DEBUG: crypter ADD %q", args) err = bbutil.RunBashInput(pubkey, "gpg", args...) if err != nil { return nil, fmt.Errorf("AddNewKey failed: %w", err) diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 6b3f0c09..62977a7e 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -19,6 +19,7 @@ func init() { // VcsHandle is the handle type VcsHandle struct { + repoRoot string // Abs or Rel path to the repo root commitTitle string toCommit *commitlater.List // List of future commits commitHeader bool // Has the "NEXT STEPS" header been printed? @@ -34,16 +35,29 @@ func (v VcsHandle) Name() string { return pluginName } -// Discover returns false. -func (v VcsHandle) Discover(repobasedir string) bool { - n := filepath.Join(repobasedir, ".git") - found, err := bbutil.DirExists(n) +func ultimate(s string) int { return len(s) - 1 } + +// Discover returns true if we are a repo of this type; along with the Abs path to the repo root (or "" if we don't know). +func (v VcsHandle) Discover() (bool, string) { + out, err := bbutil.RunBashOutputSilent("git", "rev-parse", "--show-toplevel") if err != nil { - return false + return false, "" + } + if out == "" { + fmt.Printf("WARNING: git rev-parse --show-toplevel has NO output??. Seems broken.") + return false, "" } - return found + if out[ultimate(out)] == '\n' { + out = out[0:ultimate(out)] + } + return err == nil, out } +//// SetRepoRoot informs the Vcs of the VCS root. +//func (v *VcsHandle) SetRepoRoot(dir string) { +// v.repoRoot = dir +//} + // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { seen := make(map[string]bool) diff --git a/pkg/vcs/none/none.go b/pkg/vcs/none/none.go index f41a2da3..da4f1078 100644 --- a/pkg/vcs/none/none.go +++ b/pkg/vcs/none/none.go @@ -15,6 +15,7 @@ func init() { // VcsHandle is type VcsHandle struct { + repoRoot string } func newNone() (vcs.Vcs, error) { @@ -26,11 +27,16 @@ func (v VcsHandle) Name() string { return pluginName } -// Discover returns true -func (v VcsHandle) Discover(repobasedir string) bool { - return true +// Discover returns true if we are a repo of this type; along with the Abs path to the repo root (or "" if we don't know). +func (v VcsHandle) Discover() (bool, string) { + return true, "" // We don't know the root. } +//// SetRepoRoot informs the Vcs of the VCS root. +//func (v *VcsHandle) SetRepoRoot(dir string) { +// v.repoRoot = dir +//} + // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { return nil diff --git a/pkg/vcs/vcs.go b/pkg/vcs/vcs.go index ef435255..022699f3 100644 --- a/pkg/vcs/vcs.go +++ b/pkg/vcs/vcs.go @@ -1,7 +1,11 @@ package vcs import ( + "fmt" + "os" + "path/filepath" "sort" + "strings" "github.com/StackExchange/blackbox/v2/models" ) @@ -27,17 +31,40 @@ var Catalog []*Item // Discover polls the VCS plug-ins to determine the VCS of directory. // The first to succeed is returned. // It never returns nil, since "NONE" is always valid. -func Discover(dir string) Vcs { +func Discover() (Vcs, string) { for _, v := range Catalog { h, err := v.New() if err != nil { - return nil // No idea how that would happen. + return nil, "" // No idea how that would happen. } - if h.Discover(dir) { - return h + if b, repodir := h.Discover(); b { + + // Try to find the rel path from CWD to RepoBase + wd, err := os.Getwd() + if err != nil { + fmt.Printf("ERROR: Can not determine cwd! Failing!\n") + os.Exit(1) + } + //fmt.Printf("DISCCOVER: WD=%q REPO=%q\n", wd, repodir) + if repodir != wd && strings.HasSuffix(repodir, wd) { + // This is a terrible hack. We're basically guessing + // at the filesystem layout. That said, it works on macOS. + // TODO(tlim): Abstract this out into a separate function + // so we can do integration tests on it (to know if it fails on + // a particular operating system.) + repodir = wd + } + r, err := filepath.Rel(wd, repodir) + if err != nil { + // Wait, we're not relative to each other? Give up and + // just return the abs repodir. + return h, repodir + } + return h, r } } - return nil + // This can't happen. If it does, we'll panic and that's ok. + return nil, "" } // Register a new VCS. From afcec7dbf465d158acbc7760b6f2b365cf67751a Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 4 Jul 2020 14:11:47 -0400 Subject: [PATCH 54/69] linting --- pkg/vcs/git/git.go | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 62977a7e..12d7784c 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -19,10 +19,9 @@ func init() { // VcsHandle is the handle type VcsHandle struct { - repoRoot string // Abs or Rel path to the repo root - commitTitle string - toCommit *commitlater.List // List of future commits - commitHeader bool // Has the "NEXT STEPS" header been printed? + commitTitle string + commitHeaderPrinted bool // Has the "NEXT STEPS" header been printed? + toCommit *commitlater.List // List of future commits } func newGit() (vcs.Vcs, error) { @@ -53,11 +52,6 @@ func (v VcsHandle) Discover() (bool, string) { return err == nil, out } -//// SetRepoRoot informs the Vcs of the VCS root. -//func (v *VcsHandle) SetRepoRoot(dir string) { -// v.repoRoot = dir -//} - // SetFileTypeUnix informs the VCS that files should maintain unix-style line endings. func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { seen := make(map[string]bool) @@ -77,15 +71,15 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { seen[af] = true } - var keys []string + var changedfiles []string for k := range seen { - keys = append(keys, k) + changedfiles = append(changedfiles, k) } v.NeedsCommit( "set gitattr=UNIX "+tainedname.RedactList(files), repobasedir, - keys, + changedfiles, ) return nil @@ -209,10 +203,10 @@ func (v VcsHandle) FlushCommits() error { // suggestCommit tells the user what commits are needed. func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files []string) error { - if !v.commitHeader { + if !v.commitHeaderPrinted { fmt.Printf("NEXT STEP: You need to manually check these in:\n") } - v.commitHeader = true + v.commitHeaderPrinted = true fmt.Print(` git commit -m'`, strings.Join(messages, `' -m'`)+`'`) for _, file := range files { From 6b8deb42fb0ec577440d86a37d24fbdb89fa2737 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 4 Jul 2020 14:17:11 -0400 Subject: [PATCH 55/69] Clean up redact --- pkg/box/boxutils.go | 2 +- pkg/tainedname/tainedname.go | 12 ++++++------ pkg/tainedname/tainedname_test.go | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 84cd9395..31739d38 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -219,7 +219,7 @@ func PrettyCommitMessage(verb string, files []string) string { // Redact the names. for i := range files { - files[i] = tainedname.New(files[i]).RedactUnsafeWithComment() + files[i] = tainedname.New(files[i]).Redact() } if len(files) <= 2 || len(strings.Join(files, " ")) < 50 { diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go index c8528759..ef804f37 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/tainedname/tainedname.go @@ -150,26 +150,26 @@ func oct() func(r rune) string { func RedactList(names []string) string { var msgs []string for _, f := range names { - msgs = append(msgs, New(f).RedactUnsafeWithComment()) + msgs = append(msgs, New(f).Redact()) } return strings.Join(msgs, " ") } -// RedactUnsafeWithComment is like RedactUnsafe but appends +// Redact is like redactHelper but appends // "(redacted)" when appropriate. -func (dirty Dubious) RedactUnsafeWithComment() string { - s, b := dirty.RedactUnsafe() +func (dirty Dubious) Redact() string { + s, b := dirty.redactHelper() if b { return "\"" + s + "\"(redacted)" } return s } -// RedactUnsafe redacts any "bad" chars, returns true if anything +// redactHelper redacts any "bad" chars, returns true if anything // redacted. The resulting string should be human readable and // pastable (for example, something to include in a git commit // message) but not usable in os.Open(). -func (dirty Dubious) RedactUnsafe() (string, bool) { +func (dirty Dubious) redactHelper() (string, bool) { if dirty == "" { return `""`, false } diff --git a/pkg/tainedname/tainedname_test.go b/pkg/tainedname/tainedname_test.go index 8300fb8c..d63b36f7 100644 --- a/pkg/tainedname/tainedname_test.go +++ b/pkg/tainedname/tainedname_test.go @@ -21,7 +21,7 @@ func TestRedactUnsafe(t *testing.T) { {"dub\U0001D4E6", `dub𝓦`}, {"four\U0010FFFF", `fourXR`}, } { - g, b := New(test.data).RedactUnsafe() + g, b := New(test.data).redactHelper() if b { g = g + "R" } From 7ab79e0011cbae7b5007e0c6373724f47d3ad180 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 4 Jul 2020 16:10:55 -0400 Subject: [PATCH 56/69] Fix bug: filenames with spaces dont work --- integrationTest/integration_test.go | 95 +++++++++++++++++++++++++++-- pkg/bbutil/runbash.go | 2 +- pkg/box/boxutils.go | 14 +++-- pkg/box/verbs.go | 18 ++++-- pkg/commitlater/commitlater.go | 7 +-- pkg/crypters/gnupg/gnupg.go | 8 ++- pkg/tainedname/tainedname.go | 11 ++-- pkg/vcs/git/git.go | 7 ++- 8 files changed, 129 insertions(+), 33 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index 7bc118f3..a2a33c28 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -132,9 +132,8 @@ func TestStatus_notreg(t *testing.T) { checkOutput("status-noreg.txt", t, "status", "status-ENCRYPTED.txt", "blah.txt") } -// TestBasicCommands tests of the basic functions, using a fake homedir and repo. -// The files are full of garbage, not real encrypted data. -func TestBasic(t *testing.T) { +// TestHard tests the functions using a fake homedir and repo. +func TestHard(t *testing.T) { if !*longTests { return } @@ -143,7 +142,7 @@ func TestBasic(t *testing.T) { // is just garbage. compile(t) setup(t) - makeHomeDir(t, "Basic") + makeHomeDir(t, "BasicAlice") plaintextFoo := "I am the foo.txt file!\n" plainAltered := "I am the altered file!\n" @@ -166,7 +165,7 @@ func TestBasic(t *testing.T) { phase("Alice registers foo.txt") makeFile(t, "foo.txt", plaintextFoo) runBB(t, "file", "add", "--shred", "foo.txt") - //runBB(t, "encrypt", "--shred", "foo.txt") + // "file add" encrypts the file. // We shred the plaintext so that we are sure that when Decrypt runs, // we can verify the contents wasn't just sitting there all the time. assertFileMissing(t, "foo.txt") @@ -223,4 +222,90 @@ func TestBasic(t *testing.T) { assertFileMissing(t, "foo.txt") assertFileExists(t, "foo.txt.gpg") + // Chapter 2: Bob + // Alice adds Bob. + // Bob encrypts a file. + // Bob makes sure he can decrypt alice's file. + // Bob removes Alice. + // Alice verifies she CAN'T decrypt files. + // Bob adds Alice back. + // Alice verifies she CAN decrypt files. + // Bob adds an encrypted file by mistake, "bb add" and fixes it. + // Bob corrupts the blackbox-admins.txt file, verifies that commands fail. + } + +// TestEvilFilenames verifies commands work with "difficult" file names +func TestEvilFilenames(t *testing.T) { + if !*longTests { + return + } + compile(t) + setup(t) + makeHomeDir(t, "Mallory") + + runBB(t, "testing_init") // Runs "git init" or equiv + assertFileExists(t, ".git") + runBB(t, "init", "yes") // Creates .blackbox or equiv + + phase("Malory creates a GPG key") + gpgdir := makeAdmin(t, "mallory", "Mallory Evil", "mallory@example.com") + become(t, "mallory") + + phase("Mallory enrolls as an admin") + runBB(t, "admin", "add", "mallory@example.com", gpgdir) + + _ = os.MkdirAll("my/path/to", 0o770) + _ = os.Mkdir("other", 0o770) + + for i, name := range []string{ + "!important!.txt", + "#andpounds.txt", + "stars*bars?.txt", + "space space.txt", + "tab\ttab.txt", + "ret\rret.txt", + "smile😁eyes", + "¡que!", + "thé", + "pound£", + "*.go", + "rm -f erase ; echo done", + `smile☺`, + `dub𝓦`, + "my/path/to/relsecrets.txt", + "my/../my/path/../path/to/myother.txt", + "other/../my//path/../path/to/otherother.txt", + //"new\nnew.txt", // \n not permitted + //"two\n", // \n not permitted (yet) + //"four\U0010FFFF", // Illegal byte sequence. git won't accept. + } { + phase(fmt.Sprintf("Mallory tries %02d: %q", i, name)) + contents := "the name of this file is the talking heads... i mean, " + name + makeFile(t, name, contents) + assertFileExists(t, name) + assertFileMissing(t, name+".gpg") + assertFileContents(t, name, contents) + + runBB(t, "file", "add", name) + assertFileMissing(t, name) + assertFileExists(t, name+".gpg") + + runBB(t, "decrypt", name) + assertFileExists(t, name) + assertFileExists(t, name+".gpg") + assertFileContents(t, name, contents) + + runBB(t, "encrypt", name) + assertFileExists(t, name) + assertFileExists(t, name+".gpg") + assertFileContents(t, name, contents) + + runBB(t, "shred", name) + assertFileMissing(t, name) + assertFileExists(t, name+".gpg") + } +} + +// More tests to implement. +// 1. Verify that the --gid works (blackbox decrypt --gid) diff --git a/pkg/bbutil/runbash.go b/pkg/bbutil/runbash.go index 0fdabcb9..bff442ce 100644 --- a/pkg/bbutil/runbash.go +++ b/pkg/bbutil/runbash.go @@ -20,7 +20,7 @@ func RunBash(command string, args ...string) error { } err = cmd.Wait() if err != nil { - return fmt.Errorf("RunBash err=%w", err) + return fmt.Errorf("RunBash cmd=%q err=%w", command, err) } return nil } diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 31739d38..6632171a 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -218,13 +218,19 @@ func PrettyCommitMessage(verb string, files []string) string { } // Redact the names. + var rfiles []string for i := range files { - files[i] = tainedname.New(files[i]).Redact() + rfiles = append(rfiles, tainedname.New(files[i]).Redact()) } - if len(files) <= 2 || len(strings.Join(files, " ")) < 50 { - return verb + ": " + strings.Join(files, " ") + if len(rfiles) <= 2 || len(strings.Join(rfiles, " ")) < 50 { + return verb + ": " + strings.Join(rfiles, " ") } - return verb + ": " + strings.Join(files[:3], " ") + " ..." + " " + strings.Join(files, "\n ") + "\n" + return (verb + ": " + + strings.Join(rfiles[:3], " ") + + " ..." + + " " + + strings.Join(rfiles, "\n ") + + "\n") } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 8b2db495..93a0888e 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -32,7 +32,7 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { return fmt.Errorf("Admin %v already an admin", nom) } - fmt.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir) + bx.logDebug.Printf("ADMIN ADD rbd=%q\n", bx.RepoBaseDir) changedFiles, err := bx.Crypter.AddNewKey(nom, bx.RepoBaseDir, sdir, bx.ConfigPath) if err != nil { return fmt.Errorf("AdminAdd failed AddNewKey: %v", err) @@ -232,6 +232,7 @@ func (bx *Box) Encrypt(names []string, shred bool) error { func encryptMany(bx *Box, names []string, shred bool) error { var enames []string + bx.logErr.Printf("EncryptMany = %q\n", names) for _, name := range names { fmt.Printf("========== ENCRYPTING %q\n", name) if !bx.FilesSet[name] { @@ -242,6 +243,7 @@ func encryptMany(bx *Box, names []string, shred bool) error { bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name) continue } + bx.logErr.Printf("EncryptMany Crypt = %q\n", name) ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { bx.logErr.Printf("Failed to encrypt %q: %v", name, err) @@ -290,6 +292,13 @@ func (bx *Box) FileAdd(names []string, shred bool) error { return err } + // Check for newlines + for _, n := range names { + if strings.ContainsAny(n, "\n") { + return fmt.Errorf("file %q contains a newlineregistered", n) + } + } + // Check for duplicates. for _, n := range names { if i := sort.SearchStrings(bx.Files, n); i < len(bx.Files) && bx.Files[i] == n { @@ -354,17 +363,14 @@ func (bx *Box) Info() error { err := bx.getFiles() if err != nil { - bx.logErr.Printf("Info: %v", err) + bx.logErr.Printf("Info getFiles: %v", err) } err = bx.getAdmins() if err != nil { - bx.logErr.Printf("Info: %v", err) + bx.logErr.Printf("Info getAdmins: %v", err) } - //fmt.Printf("bx.Admins=%q\n", bx.Admins) - //fmt.Printf("bx.Files=%q\n", bx.Files) - fmt.Println("BLACKBOX:") fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) diff --git a/pkg/commitlater/commitlater.go b/pkg/commitlater/commitlater.go index 4c2e8b5f..a3aba834 100644 --- a/pkg/commitlater/commitlater.go +++ b/pkg/commitlater/commitlater.go @@ -23,10 +23,7 @@ func (list *List) Add(message string, repobasedir string, files []string) { dir: repobasedir, files: files, } - //o := len(list.items) list.items = append(list.items, item) - //n := len(list.items) - //fmt.Printf("LIST=%v (%v) ADD o=%v n=%v\n", list, list.items, o, n) } func sameDirs(l *List) bool { @@ -53,7 +50,7 @@ func (list *List) Flush( for _, fut := range list.items { err := fadd(fut.files) if err != nil { - return fmt.Errorf("add files (%q) failed: %w", fut.files, err) + return fmt.Errorf("add files1 (%q) failed: %w", fut.files, err) } err = fcommit([]string{fut.message}, fut.dir, fut.files) if err != nil { @@ -69,7 +66,7 @@ func (list *List) Flush( for _, fut := range list.items { err := fadd(fut.files) if err != nil { - return fmt.Errorf("add files (%q) failed: %w", fut.files, err) + return fmt.Errorf("add files2 (%q) failed: %w", fut.files, err) } m = append(m, fut.message) f = append(f, fut.files...) diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index ef485443..b5c06979 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -100,6 +100,8 @@ func (crypt CrypterHandle) Cat(filename string) ([]byte, error) { // Encrypt name, overwriting name+".gpg" func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []string) (string, error) { + var err error + crypt.logDebug.Printf("Encrypt(%q, %d, %q)", filename, umask, receivers) encrypted := filename + ".gpg" a := []string{ @@ -112,11 +114,13 @@ func (crypt CrypterHandle) Encrypt(filename string, umask int, receivers []strin for _, f := range receivers { a = append(a, "-r", f) } + a = append(a, "--encrypt") a = append(a, filename) + //err = bbutil.RunBash("ls", "-la") oldumask := syscall.Umask(umask) crypt.logDebug.Printf("Args = %q", a) - err := bbutil.RunBash(crypt.GPGCmd, a...) + err = bbutil.RunBash(crypt.GPGCmd, a...) syscall.Umask(oldumask) return encrypted, err @@ -160,7 +164,7 @@ func (crypt CrypterHandle) AddNewKey(keyname, repobasedir, sourcedir, destdir st // Prefix each file with the relative path to it. prefix, err := filepath.Rel(repobasedir, destdir) if err != nil { - fmt.Printf("FAIL (%v) (%v) (%v)\n", repobasedir, destdir, err) + //fmt.Printf("FAIL (%v) (%v) (%v)\n", repobasedir, destdir, err) prefix = destdir } for _, file := range []string{"pubring.gpg", "pubring.kbx", "trustdb.gpg"} { diff --git a/pkg/tainedname/tainedname.go b/pkg/tainedname/tainedname.go index ef804f37..f6f3e72f 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/tainedname/tainedname.go @@ -202,7 +202,7 @@ func (dirty Dubious) redactHelper() (string, bool) { } if needsQuote { - return "'" + b.String() + "'", redacted + return `"` + b.String() + `"`, redacted } return b.String(), redacted @@ -220,7 +220,7 @@ func (dirty Dubious) String() string { b.Grow(len(dirty) + 2) level := Unknown - unicode := false + //unicode := false for _, r := range dirty { if r < 128 { level = max(level, tab[r].level) @@ -228,7 +228,7 @@ func (dirty Dubious) String() string { } else { level = max(level, DoubleQuote) b.WriteString(escapeRune(r)) - unicode = true + //unicode = true } } s := b.String() @@ -241,10 +241,7 @@ func (dirty Dubious) String() string { // quote itself, which must be replaced with: '"'"' return "'" + strings.Join(strings.Split(s, "'"), `'"'"'`) + "'" case DoubleQuote: - if unicode { - return "$(printf '" + s + "')" - } - return `"` + s + `"` + return `$(printf '%q' '` + s + `')` default: } // should not happen diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 12d7784c..9c2bb4b8 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -192,12 +192,14 @@ func (v VcsHandle) DebugCommits() commitlater.List { func (v VcsHandle) FlushCommits() error { return v.toCommit.Flush( v.commitTitle, - func(files []string) error { return bbutil.RunBash("git", append([]string{"add"}, files...)...) }, + func(files []string) error { + return bbutil.RunBash("git", append([]string{"add"}, files...)...) + }, v.suggestCommit, ) // TODO(tlim): Some day we can add a command line flag that indicates that commits are // to be done for real, not just suggested to the user. At that point, this function - // can call v.toCommit.Flush() with a function that actually does the commits insteada + // can call v.toCommit.Flush() with a function that actually does the commits instead // of suggesting them. Flag could be called --commit=auto vs --commit=suggest. } @@ -220,7 +222,6 @@ func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files [ // TestingInitRepo initializes a repo. func (v VcsHandle) TestingInitRepo() error { - fmt.Println("RUNNING GIT INIT") return bbutil.RunBash("git", "init") } From 6ff8d0d168c803ea5b811ffa7a9930290c4b0060 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 4 Jul 2020 16:21:04 -0400 Subject: [PATCH 57/69] macos doesn't need gpg-agent --- integrationTest/ithelpers.go | 27 +++++++++++++++------------ 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index 69e5aca9..efc8ea3f 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -8,6 +8,7 @@ import ( "os" "os/exec" "path/filepath" + "runtime" "strings" "testing" "time" @@ -324,18 +325,20 @@ func makeAdmin(t *testing.T, name, fullname, email string) string { // GNUPGHOME=u.dir // echo 'pinentry-program' "$(which pinentry-tty)" >> "$GNUPGHOME/gpg-agent.conf" os.Setenv("GNUPGHOME", u.dir) - ai, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") - // NB(tlim): It should return something like: - // `GPG_AGENT_INFO=/home/tlimoncelli/.gnupg/S.gpg-agent:18548:1; export GPG_AGENT_INFO;` - if err != nil { - //t.Fatal(err) - } - if strings.HasPrefix(ai, "GPG_AGENT_INFO=") { - u.agentInfo = ai[15:strings.Index(ai, ";")] - os.Setenv("GPG_AGENT_INFO", u.agentInfo) - fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo) - } else { - fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") + if runtime.GOOS != "darwin" { + ai, err := bbutil.RunBashOutput("gpg-agent", "--homedir", u.dir, "--daemon") + // NB(tlim): It should return something like: + // `GPG_AGENT_INFO=/home/tlimoncelli/.gnupg/S.gpg-agent:18548:1; export GPG_AGENT_INFO;` + if err != nil { + //t.Fatal(err) + } + if strings.HasPrefix(ai, "GPG_AGENT_INFO=") { + u.agentInfo = ai[15:strings.Index(ai, ";")] + os.Setenv("GPG_AGENT_INFO", u.agentInfo) + fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo) + } else { + fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") + } } os.Setenv("GNUPGHOME", u.dir) From 68a17b547431792c012120130b1a4c379f9d50cf Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 5 Jul 2020 06:43:50 -0400 Subject: [PATCH 58/69] Fix umask display. All --all commands now work from any directory --- cmd/blackbox/cli.go | 9 +++++++-- pkg/bbutil/filestats.go | 7 +++++++ pkg/box/box.go | 5 +++-- pkg/box/verbs.go | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/cmd/blackbox/cli.go b/cmd/blackbox/cli.go index f3bf430e..74a252f4 100644 --- a/cmd/blackbox/cli.go +++ b/cmd/blackbox/cli.go @@ -3,6 +3,7 @@ package main // cli.go -- Create urfave/cli datastructures and apply them. import ( + "fmt" "syscall" "github.com/urfave/cli/v2" @@ -13,6 +14,10 @@ func flags() *cli.App { app.Version = "2.0.0" app.Usage = "Maintain encrypted files in a VCS (Git, Hg, Svn)" + defUmask := syscall.Umask(0) + syscall.Umask(defUmask) + defUmaskS := fmt.Sprintf("%04o", defUmask) + app.Flags = []cli.Flag{ // &cli.BoolFlag{ // Name: "dry-run", @@ -48,10 +53,10 @@ func flags() *cli.App { Value: "vi", EnvVars: []string{"EDITOR", "BLACKBOX_EDITOR"}, }, - &cli.IntFlag{ + &cli.StringFlag{ Name: "umask", Usage: "umask to set when decrypting", - Value: syscall.Umask(syscall.Umask(0)), + Value: defUmaskS, EnvVars: []string{"BLACKBOX_UMASK", "DECRYPT_UMASK"}, }, &cli.BoolFlag{ diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index ea15f2f3..ba6c3a88 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -86,6 +86,13 @@ func ShredFiles(names []string) error { path, flag := shredCmd() var err error for _, n := range names { + _, err := os.Stat(n) + if err != nil { + if os.IsNotExist(err) { + fmt.Printf("======= already gone: %q\n", n) + continue + } + } fmt.Printf("========== SHREDDING: %q\n", n) e := RunBash(path, flag, n) if e != nil { diff --git a/pkg/box/box.go b/pkg/box/box.go index 939fc269..5420605e 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -75,7 +75,6 @@ func NewFromFlags(c *cli.Context) *Box { // Discover which kind of VCS is in use, and the repo root. bx.Vcs, bx.RepoBaseDir = vcs.Discover() - // TODO: Tell the Vcs about bx.RepoBaseDir? // Discover the crypto backend (GnuPG, go-openpgp, etc.) bx.Crypter = crypters.SearchByName(c.String("crypto"), c.Bool("debug")) @@ -213,7 +212,9 @@ func (bx *Box) getFiles() error { if !sort.StringsAreSorted(a) { return fmt.Errorf("file corrupt. Lines not sorted: %v", fn) } - bx.Files = a + for _, n := range a { + bx.Files = append(bx.Files, filepath.Join(bx.RepoBaseDir, n)) + } bx.FilesSet = make(map[string]bool, len(bx.Files)) for _, s := range bx.Files { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 93a0888e..fb57be45 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -375,7 +375,7 @@ func (bx *Box) Info() error { fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath) - fmt.Printf(" Umask: %o\n", bx.Umask) + fmt.Printf(" Umask: %04O\n", bx.Umask) fmt.Printf(" Edditor: %v\n", bx.Editor) fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) From fdb2559aebfded516b14b4f368ff37c00565b0ed Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 5 Jul 2020 07:30:49 -0400 Subject: [PATCH 59/69] Fix --debug. Fix --group --- cmd/blackbox/drive.go | 2 +- pkg/bblog/bblog.go | 4 ++-- pkg/box/box.go | 30 ++++++++++++++++++------------ pkg/box/boxutils.go | 2 +- pkg/box/verbs.go | 3 +++ 5 files changed, 25 insertions(+), 16 deletions(-) diff --git a/cmd/blackbox/drive.go b/cmd/blackbox/drive.go index e8cc4fbc..6e827ccc 100644 --- a/cmd/blackbox/drive.go +++ b/cmd/blackbox/drive.go @@ -214,7 +214,7 @@ func cmdInit(c *cli.Context) error { if c.Args().Len() > 1 { return fmt.Errorf("This command takes one or two arguments") } - bx := box.NewUninitialized(c.String("configdir"), c.String("team")) + bx := box.NewUninitialized(c) if bx.ConfigRO { return fmt.Errorf(roError) } diff --git a/pkg/bblog/bblog.go b/pkg/bblog/bblog.go index 1b25b0af..d55ccf6d 100644 --- a/pkg/bblog/bblog.go +++ b/pkg/bblog/bblog.go @@ -14,13 +14,13 @@ var logErr *log.Logger var logDebug *log.Logger func init() { logErr = bblog.GetErr() - logDebug = bblog.GetDebug(verbose) + logDebug = bblog.GetDebug(debug) } Or in a function: logErr := bblog.GetErr() - logDebug := bblog.GetDebug(verbose) + logDebug := bblog.GetDebug(debug) logDebug.Printf("whatever: %v", err) */ diff --git a/pkg/box/box.go b/pkg/box/box.go index 5420605e..21d2387f 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -30,6 +30,7 @@ type Box struct { // Settings: Umask int // umask to set when decrypting Editor string // Editor to call + Debug bool // Are we in debug logging mode? // Cache of data gathered from .blackbox: Admins []string // If non-empty, the list of admins. Files []string // If non-empty, the list of files. @@ -70,7 +71,8 @@ func NewFromFlags(c *cli.Context) *Box { Editor: c.String("editor"), Team: c.String("team"), logErr: bblog.GetErr(), - logDebug: bblog.GetDebug(c.Bool("verbose")), + logDebug: bblog.GetDebug(c.Bool("debug")), + Debug: c.Bool("debug"), } // Discover which kind of VCS is in use, and the repo root. @@ -89,7 +91,7 @@ func NewFromFlags(c *cli.Context) *Box { if configFlag == "" { // Normal path. Flag not set, so we discover the path. bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team")) - if err != nil { + if err != nil && c.Command.Name != "info" { fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n") os.Exit(1) } @@ -111,23 +113,27 @@ func NewFromFlags(c *cli.Context) *Box { } // NewUninitialized creates a box in a pre-init situation. -func NewUninitialized(configflag, team string) *Box { +func NewUninitialized(c *cli.Context) *Box { /* - This is for "blackbox init" (used before ".blackbox*" exists) + This is for "blackbox init" (used before ".blackbox*" exists) - Init needs: How we populate it: - bx.Vcs: Discovered by calling each plug-in until succeeds. - bx.ConfigDir: Generated algorithmically (it doesn't exist yet). - bx.RepoBaseDir: Generated algorithmically (it doesn't exist yet). + Init needs: How we populate it: + bx.Vcs: Discovered by calling each plug-in until succeeds. + bx.ConfigDir: Generated algorithmically (it doesn't exist yet). */ bx := &Box{ - Team: team, + Umask: c.Int("umask"), + Editor: c.String("editor"), + Team: c.String("team"), + logErr: bblog.GetErr(), + logDebug: bblog.GetDebug(c.Bool("debug")), + Debug: c.Bool("debug"), } bx.Vcs, bx.RepoBaseDir = vcs.Discover() - if configflag == "" { + if c.String("configdir") == "" { rel := ".blackbox" - if team != "" { - rel = ".blackbox-" + team + if bx.Team != "" { + rel = ".blackbox-" + bx.Team } bx.ConfigPath = filepath.Join(bx.RepoBaseDir, rel) } else { diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 6632171a..35e6f6ac 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -127,7 +127,7 @@ func parseGroup(userinput string) (int, error) { // If not a number, look it up by name. g, err := user.LookupGroup(userinput) - if err != nil { + if err == nil { i, err = strconv.Atoi(g.Gid) return i, nil } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index fb57be45..b3370a8b 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -122,7 +122,9 @@ func (bx *Box) Decrypt(names []string, overwrite bool, bulkpause bool, setgroup if err != nil { return fmt.Errorf("Invalid group name or gid: %w", err) } + groupchange = true } + bx.logDebug.Printf("DECRYPT GROUP %q %v,%v\n", setgroup, groupchange, gid) if len(names) == 0 { names = bx.Files @@ -384,6 +386,7 @@ func (bx *Box) Info() error { fmt.Printf(" VcsName: %q\n", bx.Vcs.Name()) fmt.Printf(" Crypter: %v\n", bx.Crypter) fmt.Printf(" CrypterName: %q\n", bx.Crypter.Name()) + fmt.Printf(" Debug: %v\n", bx.Debug) return nil } From fa53022b2867f9eb8b28dfca387cac91e6ddb9a9 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 5 Jul 2020 07:59:27 -0400 Subject: [PATCH 60/69] Fix links --- docs/README.md | 50 +++++++++++++++++++++---------------------- docs/compatibility.md | 26 ++++++++++++++-------- docs/dev.md | 2 +- docs/gnupg-tips.md | 2 +- docs/installation.md | 2 +- 5 files changed, 45 insertions(+), 37 deletions(-) diff --git a/docs/README.md b/docs/README.md index 9032cea8..b3b76032 100644 --- a/docs/README.md +++ b/docs/README.md @@ -16,37 +16,37 @@ Things you should **never** store in a repo without encryption: Project Info: -* [Overview](user-overview) -* [Why is this important?](why-is-this-important) -* [Support/Community](support) -* [How BB encrypts](encryption) -* [OS Compatibility](compatibility) -* [Installation Instructions](installation) -* [Alternatives](alternatives) +* [Overview](user-overview.md) +* [Why is this important?](why-is-this-important.md) +* [Support/Community](support.md) +* [How BB encrypts](encryption.md) +* [OS Compatibility](compatibility.md) +* [Installation Instructions](installation.md) +* [Alternatives](alternatives.md) User Info: -* [Enabling Blackbox on a Repo](enable-repo) -* [Enroll a file](enable-repo) -* [Full Command List](full-command-list) -* [Add/Remove users](admin-ops) -* [Add/Remove files](file-ops) -* [Advanced techiques](advanced) -* [Use with Role Accounts](role-accounts) -* [Backwards Compatibility](backwards-compatibility) -* [Replacing expired keys](expired-keys) -* [Git Tips](git-tips) -* [SubVersion Tips](subversion-tips) -* [GnuPG tips](gnupg-tips) -* [Use with Ansible](with-ansible) -* [Use with Puppet](with-puppet) +* [Enabling Blackbox on a Repo](enable-repo.md) +* [Enroll a file](enable-repo.md) +* [Full Command List](full-command-list.md) +* [Add/Remove users](admin-ops.md) +* [Add/Remove files](file-ops.md) +* [Advanced techiques](advanced.md) +* [Use with Role Accounts](role-accounts.md) +* [Backwards Compatibility](backwards-compatibility.md) +* [Replacing expired keys](expired-keys.md) +* [Git Tips](git-tips.md) +* [SubVersion Tips](subversion-tips.md) +* [GnuPG tips](gnupg-tips.md) +* [Use with Ansible](with-ansible.md) +* [Use with Puppet](with-puppet.md) For contributors: -* [Developer Info](dev) -* [Code overview](dev-code-overview) -* [Add new OS support]() -* [Add new VCS support]() +* [Developer Info](dev.md) +* [Code overview](dev-code-overview.md) +* [HOWTO: Add new OS support](dev-add-os-support.md) +* [HOWTO: Add new VCS support](dev-add-vcs-support.md) A slide presentation about an older release [is on SlideShare](http://www.slideshare.net/TomLimoncelli/the-blackbox-project-sfae). diff --git a/docs/compatibility.md b/docs/compatibility.md index 93b75fbf..0ae48658 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -1,16 +1,23 @@ Compatibility ============= +# Compatibility with Blackbox v1 + +The command names all changed from v1 to v2. The `binv2` directory +includes shell scripts that provide full backwards compatibility. + +# Supported Architectures + Blackbox supports a plug-in archtecture to easily support multiple VCS system. Current support is for: -VCS/DVCS support: +## Supported VCS/DVCS systems * git * "none" (repo-less use is supported) * WOULD LOVE VOLUNTEERS TO HELP ADD SUPPORT FOR: hg, svn, p4 -GPG versions +## Supported GPG versions * Git 1.x and 2.0 * Git 2.2 and higher @@ -18,7 +25,7 @@ GPG versions golang.org/x/crypto/openpgp (this would make the code have no external dependencies) -Operating systems: +## Supported Operating systems Blackbox should work on any Linux system with GnuPG installed. Blackbox simply looks for `gpg` in `$PATH`. @@ -26,7 +33,10 @@ Blackbox simply looks for `gpg` in `$PATH`. Windows: It should work (but has not been extensively tested) on Windows WSL2. -Automated testing is done on these combinations: +# Automated testing + +While many combinations work, we do automated tests +on these combinations. If any of these fail it blocks the release: * macOS: GnuPG 2.2 executables from https://gpgtools.org/ * CentOS: GnuPG 2.0.x executables from the "base" or "updates" repo. @@ -40,7 +50,7 @@ the future. Any system that is supported by the Go language and has GuPG 2.0.x or higher binaries available should be easy to achieve. We'd also like to have automated testing for the same. -# Windows +# Windows Support BlackBox assumes that `blackbox-admins.txt` and `blackbox-files.txt` will have LF line endings. Windows users should be careful to configure Git or other systems @@ -51,10 +61,8 @@ If you use Git, add the following lines to your `.gitattributes` file: **/blackbox-admins.txt text eol=lf **/blackbox-files.txt text eol=lf -The latest version of `blackbox_initialize` will create a `.gitattributes` file in the `$BLACKBOXDATA` -directory (usually `.blackbox`) for you. - -TODO: Needs testing. +The `blackbox init` (and newer versions of `blackbox_initialize`) +will create an appropriate `.gitattributes` file for you. # Cygwin diff --git a/docs/dev.md b/docs/dev.md index cea0e328..931407d8 100644 --- a/docs/dev.md +++ b/docs/dev.md @@ -32,5 +32,5 @@ your code as well as the new test. This way the confidence tests accumulate as the system grows as we know future changes don't break old features. -Note: More info about compatibility are on the [Compatibility Page](compatibility) +Note: More info about compatibility are on the [Compatibility Page](compatibility.md) diff --git a/docs/gnupg-tips.md b/docs/gnupg-tips.md index 541289a9..4c5a42be 100644 --- a/docs/gnupg-tips.md +++ b/docs/gnupg-tips.md @@ -15,7 +15,7 @@ GnuPG tips * Solution: Usually means you forgot to re-encrypt the file with the new key. * Message: `Error: can't re-encrypt because a key has expired.` -* Solution: A user's key has expired and can't be used to encrypt any more. Follow the [Replace expired keys](expired-keys) page. +* Solution: A user's key has expired and can't be used to encrypt any more. Follow the [Replace expired keys](expired-keys.md) page. FYI: Your repo may use `keyrings/live` instead of `.blackbox`. See "Where is the configuration stored?" diff --git a/docs/installation.md b/docs/installation.md index 72da1dfa..3777def8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -13,5 +13,5 @@ git clone FILL IN Future: We will have RPM, DEB, Chocolately packages. -Next step: [Enable on a repo](enable-repo) +Next step: [Enable on a repo](enable-repo.md) From b3b54fa16db1230d952fc706091cc6b938b76d21 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sun, 5 Jul 2020 12:18:11 -0400 Subject: [PATCH 61/69] Refactor makesafe --- binv2/blackbox_postdeploy | 6 +- go.sum | 1 + integrationTest/NOTES.txt | 2 +- pkg/box/boxutils.go | 24 +-- pkg/box/pretty_test.go | 14 +- pkg/box/verbs.go | 11 +- pkg/commitlater/commitlater.go | 2 +- .../tainedname.go => makesafe/makesafe.go} | 140 ++++++++++-------- pkg/makesafe/makesafe_test.go | 136 +++++++++++++++++ pkg/tainedname/tainedname_test.go | 82 ---------- pkg/vcs/git/git.go | 13 +- 11 files changed, 247 insertions(+), 184 deletions(-) rename pkg/{tainedname/tainedname.go => makesafe/makesafe.go} (69%) create mode 100644 pkg/makesafe/makesafe_test.go delete mode 100644 pkg/tainedname/tainedname_test.go diff --git a/binv2/blackbox_postdeploy b/binv2/blackbox_postdeploy index 3abdb80a..ef5a142e 100755 --- a/binv2/blackbox_postdeploy +++ b/binv2/blackbox_postdeploy @@ -1,6 +1,2 @@ #!/usr/bin/env bash -if [[ "$1" == "" ]]; then - blackbox decrypt --all --overwrite -else - blackbox decrypt --all --overwrite --group "$1" " -fi +blackbox decrypt --all --overwrite --group "$1" diff --git a/go.sum b/go.sum index 76c865f8..99dfded7 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,5 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/StackExchange/blackbox v0.0.0-20200527125317-e049c02655d2 h1:iihTTtoGPPfol75KUvwzSe+dlrVlAA78Ky2r+PA/tI0= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index 29f23b65..e3cd0b74 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -88,4 +88,4 @@ rm -rf /tmp/bbhome-* && go test -long -run ( gbb && cd cmd/blackbox && go install ) && blackbox -cd /tmp && rm -rf /tmp/bbhome-* ; mkdir /tmp/bbhome-1 ; cd /tmp/bbhome-1 && git init ; ( gbb && cd cmd/blackbox && go install ) && blackbox init yes && gitmeWork ; git commit -mm -a ; blackbox admin add tlimoncelli ; git commit -mnewadmin -a ; echo secrt > secret.txt ; blackbox file add secret.txt +cd /tmp && rm -rf /tmp/bbhome-* ; mkdir /tmp/bbhome-1 ; cd /tmp/bbhome-1 && git init ; gitmeWork ; ( gbb && cd cmd/blackbox && go install ) && blackbox init yes && gitmeWork ; git commit -mm -a ; blackbox admin add tlimoncelli ; git commit -mnewadmin -a ; echo secrt > secret.txt ; blackbox file add secret.txt diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 35e6f6ac..4540d9b1 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -10,7 +10,7 @@ import ( "strconv" "strings" - "github.com/StackExchange/blackbox/v2/pkg/tainedname" + "github.com/StackExchange/blackbox/v2/pkg/makesafe" ) // FileStatus returns the status of a file. @@ -211,26 +211,14 @@ func shouldWeOverwrite() { // PrettyCommitMessage generates a pretty commit message. func PrettyCommitMessage(verb string, files []string) string { - if len(files) == 0 { // This use-case should probably be an error. return verb + " (no files)" } - - // Redact the names. - var rfiles []string - for i := range files { - rfiles = append(rfiles, tainedname.New(files[i]).Redact()) + rfiles := makesafe.RedactMany(files) + m, truncated := makesafe.FirstFewFlag(rfiles) + if truncated { + return verb + ": " + m + " " + strings.Join(rfiles, "\n ") + "\n" } - - if len(rfiles) <= 2 || len(strings.Join(rfiles, " ")) < 50 { - return verb + ": " + strings.Join(rfiles, " ") - } - - return (verb + ": " + - strings.Join(rfiles[:3], " ") + - " ..." + - " " + - strings.Join(rfiles, "\n ") + - "\n") + return verb + ": " + m } diff --git a/pkg/box/pretty_test.go b/pkg/box/pretty_test.go index d6621f73..a7cb42f9 100644 --- a/pkg/box/pretty_test.go +++ b/pkg/box/pretty_test.go @@ -12,13 +12,17 @@ func TestPrettyCommitMessage(t *testing.T) { {[]string{"one"}, `HEADING: one`}, {[]string{"one", "two"}, `HEADING: one two`}, {[]string{"one", "two", "three"}, `HEADING: one two three`}, - {[]string{"one", "two", "three", "four"}, `HEADING: one two three four`}, - {[]string{"one", "two", "three", "four", "five"}, `HEADING: one two three four five`}, - {[]string{"has spaces.txt"}, `HEADING: 'has spaces.txt'`}, + {[]string{"one", "two", "three", "four"}, + `HEADING: one two three four`}, + {[]string{"one", "two", "three", "four", "five"}, + `HEADING: one two three four five`}, + {[]string{"has spaces.txt"}, `HEADING: "has spaces.txt"`}, {[]string{"two\n"}, `HEADING: "twoX"(redacted)`}, {[]string{"smile😁eyes"}, `HEADING: smile😁eyes`}, - {[]string{"tab\ttab", "two very long strings.txt"}, `HEADING: "tabXtab"(redacted) 'two very long strings.txt'`}, - {[]string{long, long, long, long}, "HEADING: aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString aVeryVeryLongLongLongStringStringString ... aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n aVeryVeryLongLongLongStringStringString\n"}, + {[]string{"tab\ttab", "two very long strings.txt"}, + `HEADING: "tabXtab"(redacted) "two very long strings.txt"`}, + {[]string{long, long, long, long}, + "HEADING: " + long + " " + long + " " + long + " " + long + " ... " + long + "\n " + long + "\n " + long + "\n " + long + "\n"}, } { g := PrettyCommitMessage("HEADING", test.data) if g == test.expected { diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index b3370a8b..59d1af59 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -14,7 +14,7 @@ import ( "strings" "github.com/StackExchange/blackbox/v2/pkg/bbutil" - "github.com/StackExchange/blackbox/v2/pkg/tainedname" + "github.com/StackExchange/blackbox/v2/pkg/makesafe" "github.com/olekukonko/tablewriter" ) @@ -47,6 +47,7 @@ func (bx *Box) AdminAdd(nom string, sdir string) error { if err != nil { return fmt.Errorf("could not update file (%q,%q): %v", fn, nom, err) } + changedFiles = append([]string{fn}, changedFiles...) bx.Vcs.NeedsCommit("NEW ADMIN: "+nom, bx.RepoBaseDir, changedFiles) return nil @@ -333,10 +334,12 @@ func (bx *Box) FileAdd(names []string, shred bool) error { bx.logErr.Printf("Error while shredding: %v", err) } + bx.Vcs.CommitTitle("BLACKBOX ADD FILE: " + makesafe.FirstFew(makesafe.ShellMany(names))) + bx.Vcs.IgnoreFiles(bx.RepoBaseDir, names) bx.Vcs.NeedsCommit( - PrettyCommitMessage("ADDING TO BLACKBOX", names), + PrettyCommitMessage("blackbox-files.txt add", names), bx.RepoBaseDir, append([]string{filepath.Join(bx.ConfigPath, "blackbox-files.txt")}, needsCommit...), ) @@ -377,7 +380,7 @@ func (bx *Box) Info() error { fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath) - fmt.Printf(" Umask: %04O\n", bx.Umask) + fmt.Printf(" Umask: %04o\n", bx.Umask) fmt.Printf(" Edditor: %v\n", bx.Editor) fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) @@ -437,7 +440,7 @@ func (bx *Box) Init(yes, vcsname string) error { fs := []string{ba, bf} bx.Vcs.NeedsCommit( - "NEW: "+tainedname.RedactList(fs), + "NEW: "+strings.Join(makesafe.RedactMany(fs), " "), bx.RepoBaseDir, fs, ) diff --git a/pkg/commitlater/commitlater.go b/pkg/commitlater/commitlater.go index a3aba834..80efadf0 100644 --- a/pkg/commitlater/commitlater.go +++ b/pkg/commitlater/commitlater.go @@ -46,7 +46,7 @@ func (list *List) Flush( ) error { // Just list the individual commit commands. - if title == "" || len(list.items) <= 1 || !sameDirs(list) { + if title == "" || len(list.items) < 2 || !sameDirs(list) { for _, fut := range list.items { err := fadd(fut.files) if err != nil { diff --git a/pkg/tainedname/tainedname.go b/pkg/makesafe/makesafe.go similarity index 69% rename from pkg/tainedname/tainedname.go rename to pkg/makesafe/makesafe.go index f6f3e72f..729b2af2 100644 --- a/pkg/tainedname/tainedname.go +++ b/pkg/makesafe/makesafe.go @@ -1,6 +1,6 @@ -package tainedname +package makesafe -// tainedname -- A string with a Stringer that is shell safe. +// untaint -- A string with a Stringer that is shell safe. // This goes to great lengths to make sure the String() is pastable. // Whitespace and shell "special chars" are handled as expected. @@ -18,14 +18,6 @@ import ( "unicode" ) -// Dubious is a string that can't be trusted to work on a shell line without escaping. -type Dubious string - -// New creates a dubious string. -func New(s string) Dubious { - return Dubious(s) -} - type protection int const ( @@ -146,53 +138,43 @@ func oct() func(r rune) string { return func(r rune) string { return fmt.Sprintf(`\%03o`, r) } } -// RedactList returns a string of redacted names. -func RedactList(names []string) string { - var msgs []string - for _, f := range names { - msgs = append(msgs, New(f).Redact()) - } - return strings.Join(msgs, " ") -} - -// Redact is like redactHelper but appends -// "(redacted)" when appropriate. -func (dirty Dubious) Redact() string { - s, b := dirty.redactHelper() - if b { - return "\"" + s + "\"(redacted)" - } - return s -} - -// redactHelper redacts any "bad" chars, returns true if anything -// redacted. The resulting string should be human readable and -// pastable (for example, something to include in a git commit -// message) but not usable in os.Open(). -func (dirty Dubious) redactHelper() (string, bool) { - if dirty == "" { - return `""`, false +// Redact returns a string that can be used in a shell single-quoted +// string. It may not be an exact representation, but it is safe +// to include on a command line. +// +// Redacted chars are changed to "X". +// If anything is redacted, the string is surounded by double quotes +// ("air quotes") and the string "(redacted)" is added to the end. +// If nothing is redacted, but it contains spaces, it is surrounded +// by double quotes. +// +// Example: `s` -> `s` +// Example: `space cadet.txt` -> `"space cadet.txt"` +// Example: `drink a \t soda` -> `"drink a X soda"(redacted)` +// Example: `smile☺` -> `"smile☺` +func Redact(tainted string) string { + + if tainted == "" { + return `""` } var b strings.Builder - b.Grow(len(dirty) + 2) + b.Grow(len(tainted) + 10) - needsQuote := false redacted := false + needsQuote := false - for _, r := range dirty { + for _, r := range tainted { if r == ' ' { b.WriteRune(r) needsQuote = true } else if r == '\'' { - b.WriteRune(r) - needsQuote = true + b.WriteRune('X') + redacted = true } else if r == '"' { + b.WriteRune('\\') b.WriteRune(r) needsQuote = true - } else if r == '\'' { - b.WriteRune('X') - needsQuote = true } else if unicode.IsPrint(r) { b.WriteRune(r) } else { @@ -201,48 +183,57 @@ func (dirty Dubious) redactHelper() (string, bool) { } } + if redacted { + return `"` + b.String() + `"(redacted)` + } if needsQuote { - return `"` + b.String() + `"`, redacted + return `"` + b.String() + `"` } + return tainted +} - return b.String(), redacted +// RedactMany returns the list after processing each element with Redact(). +func RedactMany(items []string) []string { + var r []string + for _, n := range items { + r = append(r, Redact(n)) + } + return r } -// String returns a version of the dirty string that is absolutely -// safe to paste into a bash command line, and will result in the -// correct filename being interpreted by bash. -func (dirty Dubious) String() string { - if dirty == "" { +// Shell returns the string formatted so that it is safe to be pasted +// into a command line to produce the desired filename as an argument +// to the command. +func Shell(tainted string) string { + if tainted == "" { return `""` } var b strings.Builder - b.Grow(len(dirty) + 2) + b.Grow(len(tainted) + 10) level := Unknown - //unicode := false - for _, r := range dirty { + for _, r := range tainted { if r < 128 { level = max(level, tab[r].level) b.WriteString(tab[r].fn(r)) } else { level = max(level, DoubleQuote) b.WriteString(escapeRune(r)) - //unicode = true } } s := b.String() - switch level { - case None: - return string(dirty) - case SingleQuote: + if level == None { + return tainted + } else if level == SingleQuote { // A single quoted string accepts all chars except the single // quote itself, which must be replaced with: '"'"' return "'" + strings.Join(strings.Split(s, "'"), `'"'"'`) + "'" - case DoubleQuote: + } else if level == DoubleQuote { + // A double-quoted string may include \xxx escapes and other + // things. Sadly bash doesn't interpret those, but printf will! return `$(printf '%q' '` + s + `')` - default: } // should not happen return fmt.Sprintf("%q", s) @@ -265,3 +256,30 @@ func escapeRune(r rune) string { return string(rune(r)) } } + +// ShellMany returns the list after processing each element with Shell(). +func ShellMany(items []string) []string { + var r []string + for _, n := range items { + r = append(r, Redact(n)) + } + return r +} + +// FirstFew returns the first few names. If any are truncated, it is +// noted by appending "...". The exact definition of "few" may change +// over time, and may be based on the number of chars not the list +func FirstFew(sl []string) string { + s, _ := FirstFewFlag(sl) + return s +} + +// FirstFewFlag is like FirstFew but returns true if truncation done. +func FirstFewFlag(sl []string) (string, bool) { + const maxitems = 4 + const maxlen = 70 + if len(sl) < maxitems || len(strings.Join(sl, " ")) < maxlen { + return strings.Join(sl, " "), false + } + return strings.Join(sl[:maxitems], " ") + " ...", true +} diff --git a/pkg/makesafe/makesafe_test.go b/pkg/makesafe/makesafe_test.go new file mode 100644 index 00000000..f037d213 --- /dev/null +++ b/pkg/makesafe/makesafe_test.go @@ -0,0 +1,136 @@ +package makesafe + +import ( + "testing" +) + +func TestRedact(t *testing.T) { + for i, test := range []struct{ data, expected string }{ + {"", `""`}, + {"one", "one"}, + {"has space.txt", `"has space.txt"`}, + {"has\ttab.txt", `"hasXtab.txt"(redacted)`}, + {"has\nnl.txt", `"hasXnl.txt"(redacted)`}, + {"has\rret.txt", `"hasXret.txt"(redacted)`}, + {"¡que!", `¡que!`}, + {"thé", `thé`}, + {"pound£", `pound£`}, + {"*.go", `*.go`}, + {"rm -rf / ; echo done", `"rm -rf / ; echo done"`}, + {"smile\u263a", `smile☺`}, + {"dub\U0001D4E6", `dub𝓦`}, + {"four\U0010FFFF", `"fourX"(redacted)`}, + } { + g := Redact(test.data) + if g == test.expected { + t.Logf("%03d: PASSED", i) + } else { + t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)", i, test.data, g, test.expected) + } + } +} + +func TestRedactMany(t *testing.T) { + data := []string{ + "", + "one", + "has space.txt", + "has\ttab.txt", + } + g := RedactMany(data) + if len(g) != 4 || g[0] != `""` || g[1] != `"has space.txt"` || g[2] != `"hasXtab.txt"(redacted)` { + t.Logf("PASSED") + } else { + t.Errorf("FAILED got=(%q)", g) + } +} + +func TestShell(t *testing.T) { + for i, test := range []struct{ data, expected string }{ + {"", `""`}, + {"one", "one"}, + {"two\n", `$(printf '%q' 'two\n')`}, + {"ta tab", `$(printf '%q' 'ta\ttab')`}, + {"tab\ttab", `$(printf '%q' 'tab\ttab')`}, + {"new\nline", `$(printf '%q' 'new\nline')`}, + {"¡que!", `$(printf '%q' '\302\241que!')`}, + {"thé", `$(printf '%q' 'th\303\251')`}, + {"pound£", `$(printf '%q' 'pound\302\243')`}, + {"*.go", `'*.go'`}, + {"rm -rf / ; echo done", `'rm -rf / ; echo done'`}, + {"smile\u263a", `$(printf '%q' 'smile\342\230\272')`}, + {"dub\U0001D4E6", `$(printf '%q' 'dub\360\235\223\246')`}, + {"four\U0010FFFF", `$(printf '%q' 'four\364\217\277\277')`}, + } { + g := Shell(test.data) + if g == test.expected { + t.Logf("%03d: PASSED", i) + //t.Logf("%03d: PASSED go(%q) bash: %s", i, test.data, test.expected) + } else { + t.Errorf("%03d: FAILED data=%q got=`%s` wanted=`%s`", i, test.data, g, test.expected) + } + } +} + +func TestEscapeRune(t *testing.T) { + for i, test := range []struct { + data rune + expected string + }{ + {'a', `\141`}, + {'é', `\303\251`}, + {'☺', `\342\230\272`}, + {'글', `\352\270\200`}, + {'𩸽', `\360\251\270\275`}, + //{"\U0010FEDC", `"'\U0010fedc'"`}, + } { + g := escapeRune(test.data) + if g == test.expected { + t.Logf("%03d: PASSED go=(%q) bash=(%s)", i, test.data, test.expected) + } else { + t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)", i, test.data, g, test.expected) + } + } +} + +func TestShellMany(t *testing.T) { + data := []string{ + "", + "one", + "has space.txt", + "¡que!", + } + g := ShellMany(data) + if len(g) != 4 || g[0] != `""` || g[1] != "one" || g[2] != `"has space.txt"` || g[3] != `$(printf '%q' '\302\241que!')` { + t.Logf("PASSED") + } else { + t.Errorf("FAILED got=(%q)", g) + } +} + +func TestFirstFewFlag(t *testing.T) { + for i, test := range []struct { + data []string + expectedFlag bool + expectedString string + }{ + {[]string{"", "one"}, false, ` one`}, + {[]string{"one"}, false, `one`}, + {[]string{"one", "two", "three", "longlonglong", "longlonglonglong", "manylonglonglog", "morelongonglonglong"}, true, ``}, + } { + gs, gf := FirstFewFlag(test.data) + if test.expectedFlag { + if gf == test.expectedFlag { + t.Logf("%03d: PASSED", i) + } else { + t.Errorf("%03d: FAILED data=%q got=(%q) wanted=(%q)", i, test.data, gs, test.expectedString) + } + } else { + if gf == test.expectedFlag && gs == test.expectedString { + t.Logf("%03d: PASSED", i) + } else { + t.Errorf("%03d: FAILED data=%q got=(%q) wanted=(%q)", i, test.data, gs, test.expectedString) + } + } + } +} diff --git a/pkg/tainedname/tainedname_test.go b/pkg/tainedname/tainedname_test.go deleted file mode 100644 index d63b36f7..00000000 --- a/pkg/tainedname/tainedname_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package tainedname - -import ( - "testing" -) - -func TestRedactUnsafe(t *testing.T) { - for i, test := range []struct{ data, expected string }{ - {"", `""`}, - {"one", "one"}, - {"has space.txt", "'has space.txt'"}, - {"has\ttab.txt", `hasXtab.txtR`}, - {"has\nnl.txt", `hasXnl.txtR`}, - {"has\rret.txt", `hasXret.txtR`}, - {"¡que!", `¡que!`}, - {"thé", `thé`}, - {"pound£", `pound£`}, - {"*.go", `*.go`}, - {"rm -rf / ; echo done", `'rm -rf / ; echo done'`}, - {"smile\u263a", `smile☺`}, - {"dub\U0001D4E6", `dub𝓦`}, - {"four\U0010FFFF", `fourXR`}, - } { - g, b := New(test.data).redactHelper() - if b { - g = g + "R" - } - if g == test.expected { - //jt.Logf("%03d: PASSED go(%q) bash: %s\n", i, test.data, test.expected) - t.Logf("%03d: PASSED\n", i) - } else { - t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) - } - } -} - -func TestString(t *testing.T) { - for i, test := range []struct{ data, expected string }{ - {"", `""`}, - {"one", "one"}, - {"two\n", `"two\n"`}, - {"tab tab", `"tab\ttab"`}, - {"tab\ttab", `"tab\ttab"`}, - {"new\nline", `"new\nline"`}, - {"¡que!", `$(printf '\302\241que!')`}, - {"thé", `$(printf 'th\303\251')`}, - {"pound£", `$(printf 'pound\302\243')`}, - {"*.go", `'*.go'`}, - {"rm -rf / ; echo done", `'rm -rf / ; echo done'`}, - {"smile\u263a", `$(printf 'smile\342\230\272')`}, - {"dub\U0001D4E6", `$(printf 'dub\360\235\223\246')`}, - {"four\U0010FFFF", `$(printf 'four\364\217\277\277')`}, - } { - g := New(test.data).String() - if g == test.expected { - t.Logf("%03d: PASSED go(%q) bash: %s\n", i, test.data, test.expected) - } else { - t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) - } - } -} - -func TestEscapeRune(t *testing.T) { - for i, test := range []struct { - data rune - expected string - }{ - {'a', `\141`}, - {'é', `\303\251`}, - {'☺', `\342\230\272`}, - {'글', `\352\270\200`}, - {'𩸽', `\360\251\270\275`}, - //{"\U0010FEDC", `"'\U0010fedc'"`}, - } { - g := escapeRune(test.data) - if g == test.expected { - t.Logf("%03d: PASSED go=(%q) bash=(%s)\n", i, test.data, test.expected) - } else { - t.Errorf("%03d: FAILED data=%q got=(%s) wanted=(%s)\n", i, test.data, g, test.expected) - } - } -} diff --git a/pkg/vcs/git/git.go b/pkg/vcs/git/git.go index 9c2bb4b8..9e73be2a 100644 --- a/pkg/vcs/git/git.go +++ b/pkg/vcs/git/git.go @@ -7,7 +7,7 @@ import ( "github.com/StackExchange/blackbox/v2/pkg/bbutil" "github.com/StackExchange/blackbox/v2/pkg/commitlater" - "github.com/StackExchange/blackbox/v2/pkg/tainedname" + "github.com/StackExchange/blackbox/v2/pkg/makesafe" "github.com/StackExchange/blackbox/v2/pkg/vcs" ) @@ -77,7 +77,7 @@ func (v VcsHandle) SetFileTypeUnix(repobasedir string, files ...string) error { } v.NeedsCommit( - "set gitattr=UNIX "+tainedname.RedactList(files), + "set gitattr=UNIX "+strings.Join(makesafe.RedactMany(files), " "), repobasedir, changedfiles, ) @@ -100,7 +100,7 @@ func (v VcsHandle) IgnoreAnywhere(repobasedir string, files []string) error { } v.NeedsCommit( - "gitignore "+tainedname.RedactList(files), + "gitignore "+strings.Join(makesafe.RedactMany(files), " "), repobasedir, []string{".gitignore"}, ) @@ -149,7 +149,7 @@ func (v VcsHandle) IgnoreFiles(repobasedir string, files []string) error { } v.NeedsCommit( - "gitignore "+tainedname.RedactList(files), + "gitignore "+strings.Join(makesafe.RedactMany(files), " "), repobasedir, []string{".gitignore"}, ) @@ -211,9 +211,8 @@ func (v *VcsHandle) suggestCommit(messages []string, repobasedir string, files [ v.commitHeaderPrinted = true fmt.Print(` git commit -m'`, strings.Join(messages, `' -m'`)+`'`) - for _, file := range files { - fmt.Print(" " + tainedname.New(file).String()) - } + fmt.Print(" ") + fmt.Print(strings.Join(makesafe.ShellMany(files), " ")) fmt.Println() return nil } From 259ba65aea2f6a7b9484af46c64cb2da7e67de2f Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 6 Jul 2020 08:05:03 -0400 Subject: [PATCH 62/69] cleanig up code --- integrationTest/NOTES.txt | 4 ++-- integrationTest/ithelpers.go | 35 +++++++++++++--------------------- pkg/bbutil/filestats.go | 7 ++++++- pkg/box/box.go | 26 +++++++++++++------------ pkg/box/verbs.go | 23 +++++++++++----------- pkg/crypters/gnupg/keychain.go | 8 ++++---- 6 files changed, 51 insertions(+), 52 deletions(-) diff --git a/integrationTest/NOTES.txt b/integrationTest/NOTES.txt index e3cd0b74..d482c7d2 100644 --- a/integrationTest/NOTES.txt +++ b/integrationTest/NOTES.txt @@ -83,8 +83,8 @@ Then a shell script should run various combinations of VCS and crypters. -rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -long -run -rm -rf /tmp/bbhome-* && go test -long -run +rm -rf /tmp/bbhome-* && BLACKBOX_DEBUG=true go test -verbose -long -nocleanup +rm -rf /tmp/bbhome-* && go test -long -nocleanup ( gbb && cd cmd/blackbox && go install ) && blackbox diff --git a/integrationTest/ithelpers.go b/integrationTest/ithelpers.go index efc8ea3f..5a49dcca 100644 --- a/integrationTest/ithelpers.go +++ b/integrationTest/ithelpers.go @@ -22,6 +22,7 @@ import ( ) var verbose = flag.Bool("verbose", false, "reveal stderr") +var nocleanup = flag.Bool("nocleanup", false, "do not delete the tmp directory") type userinfo struct { name string @@ -71,21 +72,21 @@ func makeHomeDir(t *testing.T, testname string) { var homedir string var err error - if false { - // Make a random location that is deleted later - homedir, err = ioutil.TempDir("", filepath.Join("bbhome-"+testname)) - defer os.RemoveAll(homedir) // clean up - if err != nil { - t.Fatal(err) - } - } else { - // Make a predictable location. wipe and re-use + if *nocleanup { + // Make a predictable location; don't deleted. homedir = "/tmp/bbhome-" + testname os.RemoveAll(homedir) err = os.Mkdir(homedir, 0770) if err != nil { t.Fatal(fmt.Errorf("mk-home %q: %v", homedir, err)) } + } else { + // Make a random location that is deleted automatically + homedir, err = ioutil.TempDir("", filepath.Join("bbhome-"+testname)) + defer os.RemoveAll(homedir) // clean up + if err != nil { + t.Fatal(err) + } } err = os.Setenv("HOME", homedir) @@ -287,16 +288,6 @@ func runBB(t *testing.T, args ...string) { } } -// # NB: This is copied from _blackbox_common.sh -// function get_pubring_path() { -// : "${KEYRINGDIR:=keyrings/live}" ; -// if [[ -f "${KEYRINGDIR}/pubring.gpg" ]]; then -// echo "${KEYRINGDIR}/pubring.gpg" -// else -// echo "${KEYRINGDIR}/pubring.kbx" -// fi -// } - func phase(msg string) { logDebug.Println("********************") logDebug.Println("********************") @@ -332,12 +323,12 @@ func makeAdmin(t *testing.T, name, fullname, email string) string { if err != nil { //t.Fatal(err) } - if strings.HasPrefix(ai, "GPG_AGENT_INFO=") { + if !strings.HasPrefix(ai, "GPG_AGENT_INFO=") { + fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") + } else { u.agentInfo = ai[15:strings.Index(ai, ";")] os.Setenv("GPG_AGENT_INFO", u.agentInfo) fmt.Printf("GPG_AGENT_INFO=%q (was %q)\n", ai, u.agentInfo) - } else { - fmt.Println("WARNING: gpg-agent didn't output what we expected. Assumed dead.") } } diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index ba6c3a88..7e89ec37 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -51,9 +51,13 @@ func Touch(name string) error { return os.Chtimes(name, currentTime, currentTime) } -var shredPath, shredFlag string +var shredPath, shredFlag string // Memoization cache +// shredCmd determines which command to use to securely erase a file. It returns +// the command to run and what flags to use with it. Determining the answer +// can be slow, therefore the answer is memoized and returned to future callers. func shredCmd() (string, string) { + // Use the memoized result. if shredPath != "" { return shredPath, shredFlag } @@ -75,6 +79,7 @@ func shredCmd() (string, string) { } } + // Single exit, so we don't have to repeat the memoization code. return shredPath, shredFlag } diff --git a/pkg/box/box.go b/pkg/box/box.go index 21d2387f..44882d16 100644 --- a/pkg/box/box.go +++ b/pkg/box/box.go @@ -88,27 +88,29 @@ func NewFromFlags(c *cli.Context) *Box { // Find the .blackbox (or equiv.) directory. var err error configFlag := c.String("config") - if configFlag == "" { - // Normal path. Flag not set, so we discover the path. - bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team")) - if err != nil && c.Command.Name != "info" { - fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n") - os.Exit(1) - } - } else { + if configFlag != "" { // Flag is set. Better make sure it is valid. if !filepath.IsAbs(configFlag) { fmt.Printf("config flag value is a relative path. Too risky. Exiting.\n") os.Exit(1) - // TODO(tlim): We could take the filepath.Abs(config) but until someone - // shows a use-case, just fail. + // NB(tlim): We could return filepath.Abs(config) or maybe it just + // works as is. I don't know, and until we have a use case to prove + // it out, it's best to just not implement this. } bx.ConfigPath = configFlag bx.ConfigRO = true // External configs treated as read-only. // TODO(tlim): We could get fancy here and set ConfigReadOnly=true only - // if we are sure configFlag is not within bx.RepoBaseDir. - } + // if we are sure configFlag is not within bx.RepoBaseDir. Again, I'd + // like to see a use-case before we implement this. + return bx + } + // Normal path. Flag not set, so we discover the path. + bx.ConfigPath, err = FindConfigDir(bx.RepoBaseDir, c.String("team")) + if err != nil && c.Command.Name != "info" { + fmt.Printf("Can't find .blackbox or equiv. Have you run init?\n") + os.Exit(1) + } return bx } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 59d1af59..1fba2f6e 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -576,19 +576,20 @@ func (bx *Box) Status(names []string, nameOnly bool, match string) error { if nameOnly { fmt.Println(strings.Join(onlylist, "\n")) + return nil + } + + table := tablewriter.NewWriter(os.Stdout) + table.SetAutoWrapText(false) + if thirdColumn { + table.SetHeader([]string{"Status", "Name", "Error"}) } else { - table := tablewriter.NewWriter(os.Stdout) - table.SetAutoWrapText(false) - if thirdColumn { - table.SetHeader([]string{"Status", "Name", "Error"}) - } else { - table.SetHeader([]string{"Status", "Name"}) - } - for _, v := range data { - table.Append(v) - } - table.Render() // Send output + table.SetHeader([]string{"Status", "Name"}) + } + for _, v := range data { + table.Append(v) } + table.Render() // Send output return nil } diff --git a/pkg/crypters/gnupg/keychain.go b/pkg/crypters/gnupg/keychain.go index 93b0f626..e666e2a7 100644 --- a/pkg/crypters/gnupg/keychain.go +++ b/pkg/crypters/gnupg/keychain.go @@ -46,7 +46,7 @@ This is what v1 does: if [[ "$GPG" != "gpg2" ]]; then $GPG --export --no-default-keyring --keyring "$(get_pubring_path)" >"$keyringasc" $GPG --import "$keyringasc" 2>&1 | egrep -v 'not changed$' >&2 - else + Else $GPG --keyring "$(get_pubring_path)" --export | $GPG --import fi @@ -102,6 +102,6 @@ New, ascii format: */ -func prepareUserKeychain() error { - return nil -} +//func prepareUserKeychain() error { +// return nil +//} From b6d4feea8f2ec290a559906fb3087bd4f3815681 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Mon, 6 Jul 2020 19:42:24 -0400 Subject: [PATCH 63/69] linting --- pkg/bbutil/filestats.go | 18 +++++++++++------- pkg/bbutil/sortedfile_test.go | 3 +++ pkg/box/verbs.go | 23 +++++++---------------- pkg/crypters/gnupg/gnupg.go | 3 +++ pkg/makesafe/makesafe.go | 2 +- 5 files changed, 25 insertions(+), 24 deletions(-) diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 7e89ec37..524f87e2 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -23,7 +23,7 @@ func DirExists(path string) (bool, error) { return true, err } -// FileExistsOrProblem returns true if the file exists or if we can't determine its existance. +// FileExistsOrProblem returns true if the file exists or if we can't determine its existence. func FileExistsOrProblem(path string) bool { _, err := os.Stat(path) if err == nil { @@ -72,10 +72,14 @@ func shredCmd() (string, string) { shredPath, shredFlag = path, "-f" // Does this command support the "-P" flag? tmpfile, err := ioutil.TempFile("", "rmtest") - defer os.Remove(tmpfile.Name()) // clean up - err = RunBash("rm", "-P", tmpfile.Name()) if err != nil { - shredFlag = "-Pf" + shredFlag = "-f" + } else { + defer os.Remove(tmpfile.Name()) // clean up + err = RunBash("rm", "-P", tmpfile.Name()) + if err != nil { + shredFlag = "-Pf" + } } } @@ -89,7 +93,7 @@ func ShredFiles(names []string) error { // TODO(tlim) DO the shredding in parallel like in v1. path, flag := shredCmd() - var err error + var eerr error for _, n := range names { _, err := os.Stat(n) if err != nil { @@ -101,11 +105,11 @@ func ShredFiles(names []string) error { fmt.Printf("========== SHREDDING: %q\n", n) e := RunBash(path, flag, n) if e != nil { - err = e + eerr = e fmt.Printf("ERROR: %v\n", e) } } - return err + return eerr } // ReadFileLines is like ioutil.ReadFile() but returns an []string. diff --git a/pkg/bbutil/sortedfile_test.go b/pkg/bbutil/sortedfile_test.go index 53a6e83b..6a5e2a7b 100644 --- a/pkg/bbutil/sortedfile_test.go +++ b/pkg/bbutil/sortedfile_test.go @@ -54,6 +54,9 @@ func TestAddLinesToSortedFile(t *testing.T) { expected := test.expected got, err := ioutil.ReadFile(tmpfilename) + if err != nil { + t.Fatal(err) + } if expected != string(got) { t.Errorf("test %v: contents wrong:\nexpected: %q\n got: %q", i, expected, got) } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 1fba2f6e..7362364d 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -455,13 +455,10 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { if err := anyGpg(names); err != nil { return err } - - err := bx.getAdmins() - if err != nil { + if err := bx.getAdmins(); err != nil { return err } - err = bx.getFiles() - if err != nil { + if err := bx.getFiles(); err != nil { return err } if len(names) == 0 { @@ -474,13 +471,10 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { fmt.Println("========== blackbox administrators are:") bx.AdminList() + fmt.Println("========== (the above people will be able to access the file)") if overwrite { - for _, n := range names { - if bbutil.FileExistsOrProblem(n) { - bbutil.ShredFiles([]string{n}) - } - } + bbutil.ShredFiles(names) } else { warned := false for _, n := range names { @@ -498,16 +492,13 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { } // Decrypt - err = decryptMany(bx, names, overwrite, false, 0) - if err != nil { + if err := decryptMany(bx, names, overwrite, false, 0); err != nil { return fmt.Errorf("reencrypt failed decrypt: %w", err) } - err = encryptMany(bx, names, false) - if err != nil { + if err := encryptMany(bx, names, false); err != nil { return fmt.Errorf("reencrypt failed encrypt: %w", err) } - err = bbutil.ShredFiles(names) - if err != nil { + if err := bbutil.ShredFiles(names); err != nil { return fmt.Errorf("reencrypt failed shred: %w", err) } diff --git a/pkg/crypters/gnupg/gnupg.go b/pkg/crypters/gnupg/gnupg.go index b5c06979..d2c961ee 100644 --- a/pkg/crypters/gnupg/gnupg.go +++ b/pkg/crypters/gnupg/gnupg.go @@ -141,6 +141,9 @@ func (crypt CrypterHandle) AddNewKey(keyname, repobasedir, sourcedir, destdir st args = append(args, keyname) crypt.logDebug.Printf("ADDNEWKEY: Extracting key=%v: gpg, %v\n", keyname, args) pubkey, err := bbutil.RunBashOutput("gpg", args...) + if err != nil { + return nil, err + } if len(pubkey) == 0 { return nil, fmt.Errorf("Nothing found when %q exported from %q", keyname, sourcedir) } diff --git a/pkg/makesafe/makesafe.go b/pkg/makesafe/makesafe.go index 729b2af2..ce8eec16 100644 --- a/pkg/makesafe/makesafe.go +++ b/pkg/makesafe/makesafe.go @@ -143,7 +143,7 @@ func oct() func(r rune) string { // to include on a command line. // // Redacted chars are changed to "X". -// If anything is redacted, the string is surounded by double quotes +// If anything is redacted, the string is surrounded by double quotes // ("air quotes") and the string "(redacted)" is added to the end. // If nothing is redacted, but it contains spaces, it is surrounded // by double quotes. From f886ea5a441b37fe7b9627d7c3a68d173509aab8 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Tue, 14 Jul 2020 14:00:34 -0400 Subject: [PATCH 64/69] mDocs --- docs/git-tips.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/git-tips.md b/docs/git-tips.md index 23895786..2db72d33 100644 --- a/docs/git-tips.md +++ b/docs/git-tips.md @@ -19,5 +19,4 @@ It's possible to tell Git to decrypt versions of the file before running them th textconv = gpg --use-agent -q --batch --decrypt ```` -And now commands like `git log -p file.gpg` will show a nice log of the changes in the encrypted file. - +Commands like `git log -p file.gpg` and `git diff master --` will display as expected. From ccc2439aad0e1e8b6065f64dd9f984e12d69c370 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Fri, 17 Jul 2020 11:31:09 -0400 Subject: [PATCH 65/69] fix reencrypt commit messages --- pkg/box/boxutils.go | 2 +- pkg/box/verbs.go | 43 ++++++++++++++++++++++++++++++---------- pkg/makesafe/makesafe.go | 4 ++-- 3 files changed, 35 insertions(+), 14 deletions(-) diff --git a/pkg/box/boxutils.go b/pkg/box/boxutils.go index 4540d9b1..6497292f 100644 --- a/pkg/box/boxutils.go +++ b/pkg/box/boxutils.go @@ -218,7 +218,7 @@ func PrettyCommitMessage(verb string, files []string) string { rfiles := makesafe.RedactMany(files) m, truncated := makesafe.FirstFewFlag(rfiles) if truncated { - return verb + ": " + m + " " + strings.Join(rfiles, "\n ") + "\n" + return verb + ": " + m } return verb + ": " + m } diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 7362364d..fc73395b 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -230,12 +230,19 @@ func (bx *Box) Encrypt(names []string, shred bool) error { names = bx.Files } - return encryptMany(bx, names, shred) + enames, err := encryptMany(bx, names, shred) + + bx.Vcs.NeedsCommit( + PrettyCommitMessage("ENCRYPTED", names), + bx.RepoBaseDir, + enames, + ) + + return err } -func encryptMany(bx *Box, names []string, shred bool) error { +func encryptMany(bx *Box, names []string, shred bool) ([]string, error) { var enames []string - bx.logErr.Printf("EncryptMany = %q\n", names) for _, name := range names { fmt.Printf("========== ENCRYPTING %q\n", name) if !bx.FilesSet[name] { @@ -246,7 +253,6 @@ func encryptMany(bx *Box, names []string, shred bool) error { bx.logErr.Printf("Skipping. Plaintext does not exist: %q", name) continue } - bx.logErr.Printf("EncryptMany Crypt = %q\n", name) ename, err := bx.Crypter.Encrypt(name, bx.Umask, bx.Admins) if err != nil { bx.logErr.Printf("Failed to encrypt %q: %v", name, err) @@ -258,12 +264,7 @@ func encryptMany(bx *Box, names []string, shred bool) error { } } - bx.Vcs.NeedsCommit( - PrettyCommitMessage("REENCRYPTED", enames), - bx.RepoBaseDir, - enames, - ) - return nil + return enames, nil } // FileAdd enrolls files. @@ -452,6 +453,8 @@ func (bx *Box) Init(yes, vcsname string) error { // Reencrypt decrypts and reencrypts files. func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { + allFiles := false + if err := anyGpg(names); err != nil { return err } @@ -463,6 +466,7 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { } if len(names) == 0 { names = bx.Files + allFiles = true } if bulkpause { @@ -495,13 +499,30 @@ func (bx *Box) Reencrypt(names []string, overwrite bool, bulkpause bool) error { if err := decryptMany(bx, names, overwrite, false, 0); err != nil { return fmt.Errorf("reencrypt failed decrypt: %w", err) } - if err := encryptMany(bx, names, false); err != nil { + enames, err := encryptMany(bx, names, false) + if err != nil { return fmt.Errorf("reencrypt failed encrypt: %w", err) } if err := bbutil.ShredFiles(names); err != nil { return fmt.Errorf("reencrypt failed shred: %w", err) } + if allFiles { + // If the "--all" flag was used, don't try to list all the files. + bx.Vcs.NeedsCommit( + "REENCRYPT all files", + bx.RepoBaseDir, + enames, + ) + } else { + bx.Vcs.NeedsCommit( + PrettyCommitMessage("REENCRYPT", names), + bx.RepoBaseDir, + enames, + ) + + } + return nil } diff --git a/pkg/makesafe/makesafe.go b/pkg/makesafe/makesafe.go index ce8eec16..0830950b 100644 --- a/pkg/makesafe/makesafe.go +++ b/pkg/makesafe/makesafe.go @@ -276,10 +276,10 @@ func FirstFew(sl []string) string { // FirstFewFlag is like FirstFew but returns true if truncation done. func FirstFewFlag(sl []string) (string, bool) { - const maxitems = 4 + const maxitems = 2 const maxlen = 70 if len(sl) < maxitems || len(strings.Join(sl, " ")) < maxlen { return strings.Join(sl, " "), false } - return strings.Join(sl[:maxitems], " ") + " ...", true + return strings.Join(sl[:maxitems], " ") + " (and others)", true } From 92e791a0be3fbb26ad42c6af230fb30a8d6d5f83 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 18 Jul 2020 10:00:16 -0400 Subject: [PATCH 66/69] Fix --- pkg/box/verbs.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index fc73395b..2105514d 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -145,6 +145,7 @@ func decryptMany(bx *Box, names []string, overwrite bool, groupchange bool, gid fmt.Printf("========== DECRYPTING %q\n", name) if !bx.FilesSet[name] { bx.logErr.Printf("Skipping %q: File not registered with Blackbox", name) + continue } if (!overwrite) && bbutil.FileExistsOrProblem(name) { bx.logErr.Printf("Skipping %q: Will not overwrite existing file", name) From 9e088db7233ce528b336b339643f73de205b36d0 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 18 Jul 2020 10:57:23 -0400 Subject: [PATCH 67/69] Rewrite shred detector --- pkg/bbutil/filestats.go | 62 ----------------------------------------- pkg/box/verbs.go | 5 ++-- 2 files changed, 3 insertions(+), 64 deletions(-) diff --git a/pkg/bbutil/filestats.go b/pkg/bbutil/filestats.go index 524f87e2..6daed66d 100644 --- a/pkg/bbutil/filestats.go +++ b/pkg/bbutil/filestats.go @@ -4,7 +4,6 @@ import ( "fmt" "io/ioutil" "os" - "os/exec" "path/filepath" "sort" "strings" @@ -51,67 +50,6 @@ func Touch(name string) error { return os.Chtimes(name, currentTime, currentTime) } -var shredPath, shredFlag string // Memoization cache - -// shredCmd determines which command to use to securely erase a file. It returns -// the command to run and what flags to use with it. Determining the answer -// can be slow, therefore the answer is memoized and returned to future callers. -func shredCmd() (string, string) { - // Use the memoized result. - if shredPath != "" { - return shredPath, shredFlag - } - - var path string - var err error - if path, err = exec.LookPath("shred"); err == nil { - shredPath, shredFlag = path, "-u" - } else if path, err = exec.LookPath("srm"); err == nil { - shredPath, shredFlag = path, "-f" - } else if path, err = exec.LookPath("rm"); err == nil { - shredPath, shredFlag = path, "-f" - // Does this command support the "-P" flag? - tmpfile, err := ioutil.TempFile("", "rmtest") - if err != nil { - shredFlag = "-f" - } else { - defer os.Remove(tmpfile.Name()) // clean up - err = RunBash("rm", "-P", tmpfile.Name()) - if err != nil { - shredFlag = "-Pf" - } - } - } - - // Single exit, so we don't have to repeat the memoization code. - return shredPath, shredFlag -} - -// ShredFiles securely erases a list of files. -func ShredFiles(names []string) error { - - // TODO(tlim) DO the shredding in parallel like in v1. - - path, flag := shredCmd() - var eerr error - for _, n := range names { - _, err := os.Stat(n) - if err != nil { - if os.IsNotExist(err) { - fmt.Printf("======= already gone: %q\n", n) - continue - } - } - fmt.Printf("========== SHREDDING: %q\n", n) - e := RunBash(path, flag, n) - if e != nil { - eerr = e - fmt.Printf("ERROR: %v\n", e) - } - } - return eerr -} - // ReadFileLines is like ioutil.ReadFile() but returns an []string. func ReadFileLines(filename string) ([]string, error) { b, err := ioutil.ReadFile(filename) diff --git a/pkg/box/verbs.go b/pkg/box/verbs.go index 2105514d..62a5938e 100644 --- a/pkg/box/verbs.go +++ b/pkg/box/verbs.go @@ -379,11 +379,13 @@ func (bx *Box) Info() error { } fmt.Println("BLACKBOX:") + fmt.Printf(" Debug: %v\n", bx.Debug) fmt.Printf(" Team: %q\n", bx.Team) fmt.Printf(" RepoBaseDir: %q\n", bx.RepoBaseDir) fmt.Printf(" ConfigPath: %q\n", bx.ConfigPath) fmt.Printf(" Umask: %04o\n", bx.Umask) - fmt.Printf(" Edditor: %v\n", bx.Editor) + fmt.Printf(" Editor: %v\n", bx.Editor) + fmt.Printf(" Shredder: %v\n", bbutil.ShredInfo()) fmt.Printf(" Admins: count=%v\n", len(bx.Admins)) fmt.Printf(" Files: count=%v\n", len(bx.Files)) fmt.Printf(" FilesSet: count=%v\n", len(bx.FilesSet)) @@ -391,7 +393,6 @@ func (bx *Box) Info() error { fmt.Printf(" VcsName: %q\n", bx.Vcs.Name()) fmt.Printf(" Crypter: %v\n", bx.Crypter) fmt.Printf(" CrypterName: %q\n", bx.Crypter.Name()) - fmt.Printf(" Debug: %v\n", bx.Debug) return nil } From 4c47af8ac17bf41db76636ab1b54ad9a76a1c710 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 18 Jul 2020 10:57:39 -0400 Subject: [PATCH 68/69] do some integration tests from a funny subdir --- integrationTest/integration_test.go | 212 ++++++++++++++++------------ 1 file changed, 122 insertions(+), 90 deletions(-) diff --git a/integrationTest/integration_test.go b/integrationTest/integration_test.go index a2a33c28..a96517b8 100644 --- a/integrationTest/integration_test.go +++ b/integrationTest/integration_test.go @@ -142,96 +142,128 @@ func TestHard(t *testing.T) { // is just garbage. compile(t) setup(t) - makeHomeDir(t, "BasicAlice") - plaintextFoo := "I am the foo.txt file!\n" - plainAltered := "I am the altered file!\n" - - runBB(t, "testing_init") // Runs "git init" or equiv - assertFileExists(t, ".git") - runBB(t, "init", "yes") // Creates .blackbox or equiv + for _, cx := range []struct{ subname, prefix string }{ + //{subname: ".", prefix: "."}, + {subname: "mysub", prefix: ".."}, + } { + subname := cx.subname + prefix := cx.prefix + _ = prefix + + phase("========== SUBDIR = " + subname + " ==========") + + makeHomeDir(t, "BasicAlice") + + plaintextFoo := "I am the foo.txt file!\n" + plainAltered := "I am the altered file!\n" + + runBB(t, "testing_init") // Runs "git init" or equiv + assertFileExists(t, ".git") + runBB(t, "init", "yes") // Creates .blackbox or equiv + + if subname != "." { + err := os.Mkdir(subname, 0770) + if err != nil { + t.Fatal(fmt.Errorf("hard-mk-home %q: %v", subname, err)) + } + } + olddir, err := os.Getwd() + if err != nil { + t.Fatal(err) + } + + os.Chdir(subname) + os.Chdir(olddir) + + phase("Alice creates a GPG key") + gpgdir := makeAdmin(t, "alice", "Alice Example", "alice@example.com") + become(t, "alice") + + phase("Alice enrolls as an admin") + //os.Chdir(subname) + runBB(t, "admin", "add", "alice@example.com", gpgdir) + //os.Chdir(olddir) + + // encrypt + phase("Alice registers foo.txt") + makeFile(t, "foo.txt", plaintextFoo) + //os.Chdir(subname) + //runBB(t, "file", "add", "--shred", filepath.Join(prefix, "foo.txt")) + runBB(t, "file", "add", "--shred", "foo.txt") + //os.Chdir(olddir) + // "file add" encrypts the file. + // We shred the plaintext so that we are sure that when Decrypt runs, + // we can verify the contents wasn't just sitting there all the time. + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + + phase("Alice decrypts foo.txt") + // decrypt + //os.Chdir(subname) + runBB(t, "decrypt", "foo.txt") + //runBB(t, "decrypt", filepath.Join(prefix, "foo.txt")) + //os.Chdir(olddir) + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + assertFileContents(t, "foo.txt", plaintextFoo) + + // encrypts (without shredding) + phase("Alice encrypts foo.txt (again)") + runBB(t, "encrypt", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + assertFileContents(t, "foo.txt", plaintextFoo) + + // reencrypt + phase("Alice reencrypts") + checkOutput("basic-status.txt", t, "status") + runBB(t, "reencrypt", "--overwrite", "foo.txt") + + // Test variations of cat + + // foo.txt=plain result=plain + phase("Alice cats plain:plain") + makeFile(t, "foo.txt", plaintextFoo) + assertFileExists(t, "foo.txt") + runBB(t, "encrypt", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + + // foo.txt=altered result=plain + phase("Alice cats altered:plain") + makeFile(t, "foo.txt", plainAltered) + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileExists(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + + // foo.txt=missing result=plain + phase("Alice cats missing:plain") + removeFile(t, "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") + assertFileMissing(t, "foo.txt") + assertFileExists(t, "foo.txt.gpg") + + // Chapter 2: Bob + // Alice adds Bob. + // Bob encrypts a file. + // Bob makes sure he can decrypt alice's file. + // Bob removes Alice. + // Alice verifies she CAN'T decrypt files. + // Bob adds Alice back. + // Alice verifies she CAN decrypt files. + // Bob adds an encrypted file by mistake, "bb add" and fixes it. + // Bob corrupts the blackbox-admins.txt file, verifies that commands fail. - phase("Alice creates a repo. Creates secret.txt.") - makeFile(t, "secret.txt", "this is my secret") - - phase("Alice creates a GPG key") - gpgdir := makeAdmin(t, "alice", "Alice Example", "alice@example.com") - become(t, "alice") - - phase("Alice enrolls as an admin") - runBB(t, "admin", "add", "alice@example.com", gpgdir) - - // encrypt - phase("Alice registers foo.txt") - makeFile(t, "foo.txt", plaintextFoo) - runBB(t, "file", "add", "--shred", "foo.txt") - // "file add" encrypts the file. - // We shred the plaintext so that we are sure that when Decrypt runs, - // we can verify the contents wasn't just sitting there all the time. - assertFileMissing(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - - phase("Alice decrypts foo.txt") - // decrypt - runBB(t, "decrypt", "foo.txt") - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - assertFileContents(t, "foo.txt", plaintextFoo) - - // encrypts (without shredding) - phase("Alice encrypts foo.txt (again)") - runBB(t, "encrypt", "foo.txt") - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - assertFileContents(t, "foo.txt", plaintextFoo) - - // reencrypt - phase("Alice reencrypts") - checkOutput("basic-status.txt", t, "status") - runBB(t, "reencrypt", "--overwrite", "foo.txt") - - // Test variations of cat - - // foo.txt=plain result=plain - phase("Alice cats plain:plain") - makeFile(t, "foo.txt", plaintextFoo) - assertFileExists(t, "foo.txt") - runBB(t, "encrypt", "foo.txt") - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - - // foo.txt=altered result=plain - phase("Alice cats altered:plain") - makeFile(t, "foo.txt", plainAltered) - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") - assertFileExists(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - - // foo.txt=missing result=plain - phase("Alice cats missing:plain") - removeFile(t, "foo.txt") - assertFileMissing(t, "foo.txt") - assertFileMissing(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - checkOutput("alice-cat-plain.txt", t, "cat", "foo.txt") - assertFileMissing(t, "foo.txt") - assertFileExists(t, "foo.txt.gpg") - - // Chapter 2: Bob - // Alice adds Bob. - // Bob encrypts a file. - // Bob makes sure he can decrypt alice's file. - // Bob removes Alice. - // Alice verifies she CAN'T decrypt files. - // Bob adds Alice back. - // Alice verifies she CAN decrypt files. - // Bob adds an encrypted file by mistake, "bb add" and fixes it. - // Bob corrupts the blackbox-admins.txt file, verifies that commands fail. + } } @@ -274,8 +306,8 @@ func TestEvilFilenames(t *testing.T) { `smile☺`, `dub𝓦`, "my/path/to/relsecrets.txt", - "my/../my/path/../path/to/myother.txt", - "other/../my//path/../path/to/otherother.txt", + //"my/../my/path/../path/to/myother.txt", // Not permitted yet + //"other/../my//path/../path/to/otherother.txt", // Not permitted yet //"new\nnew.txt", // \n not permitted //"two\n", // \n not permitted (yet) //"four\U0010FFFF", // Illegal byte sequence. git won't accept. From 053b8a6e017f8bdff810e72d60303b6c83547a15 Mon Sep 17 00:00:00 2001 From: Tom Limoncelli Date: Sat, 18 Jul 2020 10:57:49 -0400 Subject: [PATCH 69/69] Rewrite shred detector --- pkg/bbutil/shred.go | 109 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 pkg/bbutil/shred.go diff --git a/pkg/bbutil/shred.go b/pkg/bbutil/shred.go new file mode 100644 index 00000000..329c45c6 --- /dev/null +++ b/pkg/bbutil/shred.go @@ -0,0 +1,109 @@ +package bbutil + +// Pick an appropriate secure erase command for this operating system +// or just delete the file with os.Remove(). + +// Code rewritten based https://codereview.stackexchange.com/questions/245072 + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" +) + +var shredCmds = []struct { + name, opts string +}{ + {"sdelete", "-a"}, + {"shred", "-u"}, + {"srm", "-f"}, + {"rm", "-Pf"}, +} + +func shredTemp(path, opts string) error { + file, err := ioutil.TempFile("", "shredTemp.") + if err != nil { + return err + } + filename := file.Name() + defer os.Remove(filename) + defer file.Close() + + err = file.Close() + if err != nil { + return err + } + err = RunBash(path, opts, filename) + if err != nil { + return err + } + return nil +} + +var shredPath, shredOpts = func() (string, string) { + for _, cmd := range shredCmds { + path, err := exec.LookPath(cmd.name) + if err != nil { + continue + } + err = shredTemp(path, cmd.opts) + if err == nil { + return path, cmd.opts + } + } + return "", "" +}() + +// ShredInfo reveals the shred command and flags (for "blackbox info") +func ShredInfo() string { + return shredPath + " " + shredOpts +} + +// shredFile shreds one file. +func shredFile(filename string) error { + fi, err := os.Stat(filename) + if err != nil { + return err + } + if !fi.Mode().IsRegular() { + err := fmt.Errorf("filename is not mode regular") + return err + } + + if shredPath == "" { + // No secure erase command found. Default to a normal file delete. + // TODO(tlim): Print a warning? Have a flag that causes this to be an error? + return os.Remove(filename) + } + + err = RunBash(shredPath, shredOpts, filename) + if err != nil { + return err + } + return nil +} + +// ShredFiles securely erases a list of files. +func ShredFiles(names []string) error { + + // TODO(tlim) DO the shredding in parallel like in v1. + + var eerr error + for _, n := range names { + _, err := os.Stat(n) + if err != nil { + if os.IsNotExist(err) { + fmt.Printf("======= already gone: %q\n", n) + continue + } + } + fmt.Printf("========== SHREDDING: %q\n", n) + e := shredFile(n) + if e != nil { + eerr = e + fmt.Printf("ERROR: %v\n", e) + } + } + return eerr +}