Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CLI Enhancements #3897

Merged
merged 36 commits into from
Feb 12, 2018
Merged

CLI Enhancements #3897

merged 36 commits into from
Feb 12, 2018

Conversation

calvn
Copy link
Contributor

@calvn calvn commented Feb 2, 2018

  • Automatically disable colored UI if stdout no a tty
  • Add -no-color flag to all leaf subcommands (caveat: Warnings printed by DeprecatedCommand is always colored, since flags gets parsed afterwards)
  • Add -format to all applicable subcommands
  • Ignore warnings and return only structured responses for non-table outputs (toggleable). (This is not done since the current behavior for existing commands that supports formatted output do print warnings; e.g. token_renew.go).

Fixes #3869
Closes #3914

@vishalnayak vishalnayak added this to the 0.9.4 milestone Feb 6, 2018
@calvn calvn changed the title [WIP] CLI Enhancements CLI Enhancements Feb 7, 2018
@calvn calvn requested a review from jefferai February 7, 2018 16:21
@calvn
Copy link
Contributor Author

calvn commented Feb 7, 2018

/cc @sethvargo, it'd be great to get your feedback on this :)

This PR mainly aims to address two main points:

  • Ability to automatically detect and disable colored UI in a non-tty environment, with the option to disable globally as well.
  • Add -format option to all commands that returns any sort of non-error output from the client API. The bulk of the formatting logic/changes is in format.go, and commands simply use that helper function to print desired formatted output.

@sethvargo
Copy link
Contributor

Hey @calvn

Thank you for the ping. This looks really good, but I do have a few comments.

  • It's worth noting that with "raw", we specifically do not use the CLI and just output to the underlying io.Writer in whatever CLI is given. Would it be possible to extend that further and just feed all the output functions through that?

  • The repeated if c.flagNoColor scares me a bit. It seems like we might be able to centralize that in the base command (untested) or at least a shared helper. Alternatively, we could create our own UI that just wraps ColoredUI and does the terminal.IsTerminal check on Write calls. (I think this would work really well and would always ensure that we don't include colors when piping). If you want some guidance on how this might look, feel free to ping me - happy to talk though it or sketch it out.

  • Having a CLI flag to disable color seems like more hassle than it's worth IMO. If we solve the "pipe" problem like mentioned above and give folks an envvar to disable globally, I think that covers most use cases. The challenge (as you've identified) with a CLI flag is that the CLI and UI are tightly coupled. If we just make it an envvar (not a CLI option), the code becomes a lot simpler and solves for that 1% use case that isn't covered by TTY and raw.

command/main.go Outdated
@@ -23,6 +23,10 @@ func Run(args []string) int {
args = []string{"version"}
break
}

if arg == "-no-color" {
os.Setenv("VAULT_OUTPUT_NO_COLOR", "true")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think VAULT_CLI_NO_COLOR would be better here

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to remove this flag entirely. If we do the os.Getenv logic check on command.go's init() func as discussed, the envvar will be read before this gets applied.

Optionally, we could keep this if we change init() to a func that needs to be called explicitly (and call it after the args have been parsed) in main.go's Run().

@calvn
Copy link
Contributor Author

calvn commented Feb 7, 2018

@sethvargo thanks for taking a look! I think I've got a path forward with points 2 and 3, as we will probably end up removing the no-color flag in BaseCommand and use env-var at a higher level (probably in command.go's init() func) to toggle (which will determine what kind of UI to use).

I am still unclear on the first point though, and why we'd need to feed the output functions through PrintRaw.

@sethvargo
Copy link
Contributor

Hey @calvn

Sorry - I'm not suggesting we need to use PrintRaw - I was just showcasing an example of how we can:

  1. Detect if there's a TTY
  2. Grab the underlying io.Writer out of a CLI object and write directly to it

@calvn
Copy link
Contributor Author

calvn commented Feb 7, 2018

Ahh, I see. I am using a pretty much the same logic to detect whether there's a TTY, but instead of grabbing the io.Writter, it's used to determine whether we need to wrap the UI around ColoredUI.

vault/command/commands.go

Lines 126 to 138 in 8f2a229

if terminal.IsTerminal(int(os.Stdout.Fd())) {
ui = &cli.ColoredUi{
ErrorColor: cli.UiColorRed,
WarnColor: cli.UiColorYellow,
Ui: ui,
}
serverCmdUi = &cli.ColoredUi{
ErrorColor: cli.UiColorRed,
WarnColor: cli.UiColorYellow,
Ui: serverCmdUi,
}
}

@sethvargo
Copy link
Contributor

Yea, so the problem is that, even if you don't use color, you'll still include a newline at the end of the line if piping to another process with the non-colored UI. The only way to avoid that is to use the underlying io.Writer object. I was envisioning just making our own like this:

type VaultUI struct {
  cli.ColoredUi
}

func (u *VaultUI) Output(m string) {
  terminal.IsTerminal(int(os.Stdout.Fd())) {
    c.ColoredUi.Output(m)
  } else {
    getWriter(c).Write(m)
  }
}

Then we just basically never think about it. We instantiate exactly 1 type of CLI, and that CLI decides, based on each output, whether there's a TTY present and "does the right thing". We could add the envvar logic in there too. If we want to be efficient, we could use an initializer and parse the envvar and tty check once and cache it on the struct.

@calvn
Copy link
Contributor Author

calvn commented Feb 7, 2018

Ah, I see what you mean, but I don't think that the additional newline would be an issue here as the intention for the no-color option was to avoid having ANSI color escape sequences specifically.

I think for most cases people still end up using awk, jq, or similar in their scripts to filter out values from the output before using any values. For instance, a script to get the unseal keys from vault operator init command would go through awk (or if -format=json is provided, jq), before the values are used, so having an extra newline in there wouldn't matter. In the cases where people might pipe directly, (e.g. operator generate-root), PrintRaw is already being used to output to the writer directly.

Jeff suggested that in the future we may add a -format=raw option that would output response as it would appear from curl, and in that case it would definitely make sense to use the underlying io.Writer. Maybe @jefferai could chime in on this.

switch data.(type) {
case *api.Secret:
secret := data.(*api.Secret)
// Best-guess effort that is this a list, so parse out the keys
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for doing this? There could very well be "keys" coming back from various different backends, including k/v.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so in that case you output things that are inside secret.Data[“keys”]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OutputWithFormat is basically the combined functionality of OutputSecret and OutputList

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right, so in that case you output things that are inside secret.Data[“keys”]

But....why? :-)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the original OutputList behavior: https://github.com/hashicorp/vault/pull/3897/files#diff-3e4377911a05a6f261016d1d5e1fec91L27

If you just passed in the secret, listing keys would be not formatted correctly:

Key                 Value
---                 -----
keys               [foo]

Instead of:

$ vault list secret/
Keys
----
foo

@calvn calvn mentioned this pull request Feb 9, 2018
command/base.go Outdated
flagField string
flagFormat string
flagField string
flagNoColor bool
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can remove this and the var below if we are just using the envvar

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea was to allow it in a flag but not advertise it. Now that you mention autocorrect below, I wonder if it's better to just remove it rather than have people get it autocorrected and then complain that it's not documented. What do you think?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the code will be a lot simpler if we drop the flag entirely. People are more likely to "never want colored output" that "not want colored output for this specific command", and even in the latter case, they can specify the envvar first.

@@ -58,6 +67,14 @@ var Formatters = map[string]Formatter{
"yml": YamlFormatter{},
}

func Format() string {
format := os.Getenv(EnvVaultFormat)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we retrieve this as part of an init function instead of doing the envvar lookup each time?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We just got away from init functions in favor of being able to intercept the args earlier in the main run function. This isn't going to be meaningfully slow for interactive uses, is there a reason not to just call this a couple of times?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've experienced Getenv being slow (in relative terms), particularly on Windows systems. I wonder if we could use a Once instead and cache the result?

command/main.go Outdated
@@ -23,8 +47,85 @@ func Run(args []string) int {
args = []string{"version"}
break
}

if arg == "-no-color" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

UX-ey thing. We should be nice and add || arg == "-nocolor". Even if we never advertise that, people will forget the dash and "auto-correcting them" will be a nice UX.

@jefferai
Copy link
Member

@sethvargo Your comments should be addressed in 9b62480

command/main.go Outdated
@@ -23,7 +48,83 @@ func Run(args []string) int {
args = []string{"version"}
break
}

// Parse a given flag here, which overrides the env var
if strings.HasPrefix(arg, "-format=") {
Copy link
Contributor Author

@calvn calvn Feb 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Haven't tested, but does this handle --format as well? (L53-L59)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Didn't even know we supported that. Will address.

command/main.go Outdated
}

// setupEnv parses args and may replace them and sets some env vars to known
// values based on color/format options
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't handle color (anymore..?)

envVaultFormat := os.Getenv(EnvVaultFormat)
// If we did not parse a value, fetch the env var
if format == "" && envVaultFormat != "" {
format = envVaultFormat
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We can probably do format = strings.ToLower(envVaultFormat) directly.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'd still need it outside the if block so it doesn't buy us anything.

}

func Run(args []string) int {
args = setupEnv(args)
Copy link
Contributor Author

@calvn calvn Feb 12, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably should rename setupEnv to setupEnvFormat.

@calvn calvn merged commit 3189278 into master Feb 12, 2018
@calvn calvn deleted the cli-enhancements branch February 12, 2018 23:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants