package cli import ( "bufio" "errors" "fmt" "io" "os" "strings" "git.eeqj.de/sneak/secret/internal/vault" "github.com/spf13/cobra" "golang.org/x/term" ) // Sentinel errors for asking the user to confirm a removal var ( errNoTerminal = errors.New("stdin is not a terminal, so there is " + "nobody to ask for confirmation; pass --force to remove without asking") errNotConfirmed = errors.New("cancelled; nothing was removed") errChangedWhileAsking = errors.New("what was to be removed changed " + "while waiting for the answer; nothing was removed") ) // askThenLock asks the user to confirm a removal, unless force is set, and // then takes the state directory lock and returns the function that // releases it. find makes the command's checks, keeps what it found for // the caller to remove, and returns the question that names it. find runs // before the question, which is asked without the lock so that no other // command waits while the user answers, and runs again once the lock is // taken. That run is the last, so the caller removes what find found under // the lock. If its question then differs from the one the user answered, // something changed in between, and askThenLock fails. func (cli *Instance) askThenLock( cmd *cobra.Command, force bool, find func() (string, error), ) (func(), error) { asked := "" if !force { question, err := find() if err != nil { return nil, err } err = cli.confirm(cmd, question) if err != nil { return nil, err } asked = question } release, err := vault.LockStateDir(cli.fs, cli.stateDir) if err != nil { return nil, err } question, err := find() if err == nil && !force && question != asked { err = errChangedWhileAsking } if err != nil { release() return nil, err } return release, nil } // confirm asks question and returns nil only when the user answers y or // yes; any other answer, a bare Enter included, cancels. When stdin is not // a terminal it asks nothing and fails at once: nobody is there to answer, // and waiting for an answer would hang a script. Stdin decides, not // stdout, because the answer is read from stdin: `secret rm foo | tee log` // still asks. The question goes to stderr. func (cli *Instance) confirm(cmd *cobra.Command, question string) error { answers := cli.terminal if answers == nil { answers = cmd.InOrStdin() if !isTerminal(answers) { return errNoTerminal } } _, _ = fmt.Fprintf(cmd.ErrOrStderr(), "%s [y/N] ", question) answer, err := bufio.NewReader(answers).ReadString('\n') if err != nil && !errors.Is(err, io.EOF) { return fmt.Errorf("failed to read the answer: %w", err) } switch strings.ToLower(strings.TrimSpace(answer)) { case "y", "yes": return nil default: return errNotConfirmed } } // isTerminal reports whether r is a terminal. func isTerminal(r io.Reader) bool { file, ok := r.(*os.File) return ok && term.IsTerminal(int(file.Fd())) }