132 lines
3.8 KiB
Go
132 lines
3.8 KiB
Go
package cliutil
|
|
|
|
import (
|
|
"bufio"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// ParseDuration accepts Go's duration syntax plus the day, week and year
|
|
// suffixes an operator actually types for a key lifetime ("90d", "1y").
|
|
//
|
|
// A year is 365 days and a day is 24 hours: no calendar arithmetic, no time
|
|
// zones, no leap seconds. The value becomes an absolute expiry timestamp on the
|
|
// client precisely so a difference of a few hours cannot matter.
|
|
func ParseDuration(s string) (time.Duration, error) {
|
|
s = strings.TrimSpace(s)
|
|
if s == "" {
|
|
return 0, errors.New("empty duration")
|
|
}
|
|
var mult time.Duration
|
|
switch s[len(s)-1] {
|
|
case 'd':
|
|
mult = 24 * time.Hour
|
|
case 'w':
|
|
mult = 7 * 24 * time.Hour
|
|
case 'y':
|
|
mult = 365 * 24 * time.Hour
|
|
default:
|
|
d, err := time.ParseDuration(s)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid duration %q: use 30s, 5m, 12h, 90d, 2w or 1y", s)
|
|
}
|
|
return d, nil
|
|
}
|
|
n, err := strconv.ParseFloat(s[:len(s)-1], 64)
|
|
if err != nil {
|
|
return 0, fmt.Errorf("invalid duration %q: use 30s, 5m, 12h, 90d, 2w or 1y", s)
|
|
}
|
|
return time.Duration(n * float64(mult)), nil
|
|
}
|
|
|
|
// ParseBytes accepts a plain byte count or one with a unit suffix: "1048576",
|
|
// "1MiB", "256M", "1.5G".
|
|
//
|
|
// K, M, G and T all mean the binary multiple, whether or not the suffix is
|
|
// spelled with an "i". Disk-quota flags are set to round binary numbers, and a
|
|
// tool that quietly read "256MB" as 256,000,000 would produce a limit its user
|
|
// did not ask for.
|
|
func ParseBytes(s string) (int64, error) {
|
|
t := strings.TrimSpace(s)
|
|
if t == "" {
|
|
return 0, errors.New("empty size")
|
|
}
|
|
digits := strings.TrimRight(t, "bBiIkKmMgGtT")
|
|
// Uppercase before trimming so "MiB", "MIB" and "mib" all reduce to "M".
|
|
unit := strings.ToUpper(t[len(digits):])
|
|
unit = strings.TrimSuffix(unit, "B")
|
|
unit = strings.TrimSuffix(unit, "I")
|
|
|
|
n, err := strconv.ParseFloat(strings.TrimSpace(digits), 64)
|
|
if err != nil || n < 0 {
|
|
return 0, fmt.Errorf("invalid size %q: use a byte count or 256MiB, 2GiB", s)
|
|
}
|
|
var mult float64 = 1
|
|
switch unit {
|
|
case "":
|
|
case "K":
|
|
mult = 1 << 10
|
|
case "M":
|
|
mult = 1 << 20
|
|
case "G":
|
|
mult = 1 << 30
|
|
case "T":
|
|
mult = 1 << 40
|
|
default:
|
|
return 0, fmt.Errorf("invalid size %q: unknown unit %q", s, unit)
|
|
}
|
|
return int64(n * mult), nil
|
|
}
|
|
|
|
// KeyValue splits a "k=v" argument. The value may contain further '=' signs.
|
|
func KeyValue(s string) (key, value string, err error) {
|
|
k, v, ok := strings.Cut(s, "=")
|
|
if !ok || k == "" {
|
|
return "", "", fmt.Errorf("expected key=value, got %q", s)
|
|
}
|
|
return k, v, nil
|
|
}
|
|
|
|
// ErrAborted is returned when the user declines a confirmation prompt.
|
|
var ErrAborted = errors.New("aborted")
|
|
|
|
// Confirm asks for a yes before a destructive operation.
|
|
//
|
|
// It refuses rather than prompts when stdin is not a terminal: a pipeline that
|
|
// blocks forever on a prompt nobody can see is worse than one that fails and
|
|
// tells the operator to pass --yes.
|
|
func Confirm(in io.Reader, out io.Writer, prompt string) error {
|
|
if f, ok := in.(*os.File); ok && !isTerminal(f) {
|
|
return fmt.Errorf("%w: refusing to prompt for confirmation with no terminal; pass --yes", ErrAborted)
|
|
}
|
|
fmt.Fprintf(out, "%s [y/N]: ", prompt)
|
|
line, err := bufio.NewReader(in).ReadString('\n')
|
|
if err != nil && line == "" {
|
|
return ErrAborted
|
|
}
|
|
switch strings.ToLower(strings.TrimSpace(line)) {
|
|
case "y", "yes":
|
|
return nil
|
|
}
|
|
return ErrAborted
|
|
}
|
|
|
|
// isTerminal reports whether f is a character device. This is the cheap
|
|
// stdlib-only approximation of a tty check; it is used to decide whether to
|
|
// prompt and whether to draw progress, never for anything security-relevant.
|
|
func isTerminal(f *os.File) bool {
|
|
fi, err := f.Stat()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return fi.Mode()&os.ModeCharDevice != 0
|
|
}
|
|
|
|
// IsTerminal reports whether f is attached to a terminal.
|
|
func IsTerminal(f *os.File) bool { return isTerminal(f) }
|