init
This commit is contained in:
@@ -0,0 +1,131 @@
|
||||
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) }
|
||||
Reference in New Issue
Block a user