// Package argmask renders a command line for messages with the values of // secret-named long flags masked; bonfire's wrong-directory hint and // summer's delegate errors share it. package argmask import "strings" // secretWords mark a long flag whose value is masked. var secretWords = []string{"password", "passwd", "secret", "token", "key"} // secretFlag reports whether the long flag name (without "--") looks like it // carries a secret. func secretFlag(name string) bool { name = strings.ToLower(name) for _, w := range secretWords { if strings.Contains(name, w) { return true } } return false } // Join renders args as one line, separated by single spaces and each // quoted for a shell (Quote). The value of a long flag named like a secret // becomes ***, in the "--flag=value" form and in the "--flag value" form, // so a password typed on the command line does not reach stderr and CI // logs. In the "--flag value" form the next argument is masked whatever its // first character (pflag takes "-Xk9" as the value too); only "--" is left // alone. Join cannot see flag types, so a secret-named boolean flag // over-masks the argument after it, which is the safe side. func Join(args []string) string { var b strings.Builder for i := 0; i < len(args); i++ { if i > 0 { b.WriteByte(' ') } a := args[i] name, long := strings.CutPrefix(a, "--") flag, _, inline := strings.Cut(name, "=") if !long || flag == "" || !secretFlag(flag) { b.WriteString(Quote(a)) continue } if inline { b.WriteString(Quote("--"+flag) + "=***") continue } b.WriteString(Quote(a)) if i+1 < len(args) && args[i+1] != "--" { b.WriteString(" ***") i++ } } return b.String() } // Quote single-quotes s when a shell would split or expand it. func Quote(s string) string { if s != "" && !strings.ContainsAny(s, " \t\n'\"\\$`*?[]{}()<>|&;#~!") { return s } return "'" + strings.ReplaceAll(s, "'", `'\''`) + "'" }