github.com/rigado/snapd@v2.42.5-go-mod+incompatible/strutil/strutil.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2014-2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package strutil
    21  
    22  import (
    23  	"fmt"
    24  	"math/rand"
    25  	"sort"
    26  	"strconv"
    27  	"strings"
    28  	"time"
    29  	"unicode/utf8"
    30  )
    31  
    32  func init() {
    33  	// golang does not init Seed() itself
    34  	rand.Seed(time.Now().UTC().UnixNano())
    35  }
    36  
    37  const letters = "BCDFGHJKLMNPQRSTVWXYbcdfghjklmnpqrstvwxy0123456789"
    38  
    39  // MakeRandomString returns a random string of length length
    40  //
    41  // The vowels are omitted to avoid that words are created by pure
    42  // chance. Numbers are included.
    43  func MakeRandomString(length int) string {
    44  	out := ""
    45  	for i := 0; i < length; i++ {
    46  		out += string(letters[rand.Intn(len(letters))])
    47  	}
    48  
    49  	return out
    50  }
    51  
    52  // Convert the given size in btes to a readable string
    53  func SizeToStr(size int64) string {
    54  	suffixes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"}
    55  	for _, suf := range suffixes {
    56  		if size < 1000 {
    57  			return fmt.Sprintf("%d%s", size, suf)
    58  		}
    59  		size /= 1000
    60  	}
    61  	panic("SizeToStr got a size bigger than math.MaxInt64")
    62  }
    63  
    64  // Quoted formats a slice of strings to a quoted list of
    65  // comma-separated strings, e.g. `"snap1", "snap2"`
    66  func Quoted(names []string) string {
    67  	quoted := make([]string, len(names))
    68  	for i, name := range names {
    69  		quoted[i] = strconv.Quote(name)
    70  	}
    71  
    72  	return strings.Join(quoted, ", ")
    73  }
    74  
    75  // ListContains determines whether the given string is contained in the
    76  // given list of strings.
    77  func ListContains(list []string, str string) bool {
    78  	for _, k := range list {
    79  		if k == str {
    80  			return true
    81  		}
    82  	}
    83  	return false
    84  }
    85  
    86  // SortedListContains determines whether the given string is contained
    87  // in the given list of strings, which must be sorted.
    88  func SortedListContains(list []string, str string) bool {
    89  	i := sort.SearchStrings(list, str)
    90  	if i >= len(list) {
    91  		return false
    92  	}
    93  	return list[i] == str
    94  }
    95  
    96  // TruncateOutput truncates input data by maxLines, imposing maxBytes limit (total) for them.
    97  // The maxLines may be 0 to avoid the constraint on number of lines.
    98  func TruncateOutput(data []byte, maxLines, maxBytes int) []byte {
    99  	if maxBytes > len(data) {
   100  		maxBytes = len(data)
   101  	}
   102  	lines := maxLines
   103  	bytes := maxBytes
   104  	for i := len(data) - 1; i >= 0; i-- {
   105  		if data[i] == '\n' {
   106  			lines--
   107  		}
   108  		if lines == 0 || bytes == 0 {
   109  			return data[i+1:]
   110  		}
   111  		bytes--
   112  	}
   113  	return data
   114  }
   115  
   116  // SplitUnit takes a string of the form "123unit" and splits
   117  // it into the number and non-number parts (123,"unit").
   118  func SplitUnit(inp string) (number int64, unit string, err error) {
   119  	// go after the number first, break on first non-digit
   120  	nonDigit := -1
   121  	for i, c := range inp {
   122  		// ASCII digits and - only
   123  		if (c < '0' || c > '9') && c != '-' {
   124  			nonDigit = i
   125  			break
   126  		}
   127  	}
   128  	var prefix string
   129  	switch {
   130  	case nonDigit == 0:
   131  		return 0, "", fmt.Errorf("no numerical prefix")
   132  	case nonDigit == -1:
   133  		// no unit
   134  		prefix = inp
   135  	default:
   136  		unit = inp[nonDigit:]
   137  		prefix = inp[:nonDigit]
   138  	}
   139  	number, err = strconv.ParseInt(prefix, 10, 64)
   140  	if err != nil {
   141  		return 0, "", fmt.Errorf("%q is not a number", prefix)
   142  	}
   143  
   144  	return number, unit, nil
   145  }
   146  
   147  // ParseByteSize parses a value like 500kB and returns the number
   148  // in bytes. The case of the unit will be ignored for user convenience.
   149  func ParseByteSize(inp string) (int64, error) {
   150  	unitMultiplier := map[string]int64{
   151  		"B": 1,
   152  		// strictly speaking this is "kB" but we ignore cases
   153  		"KB": 1000,
   154  		"MB": 1000 * 1000,
   155  		"GB": 1000 * 1000 * 1000,
   156  		"TB": 1000 * 1000 * 1000 * 1000,
   157  		"PB": 1000 * 1000 * 1000 * 1000 * 1000,
   158  		"EB": 1000 * 1000 * 1000 * 1000 * 1000 * 1000,
   159  	}
   160  
   161  	errPrefix := fmt.Sprintf("cannot parse %q: ", inp)
   162  
   163  	val, unit, err := SplitUnit(inp)
   164  	if err != nil {
   165  		return 0, fmt.Errorf(errPrefix+"%s", err)
   166  	}
   167  	if unit == "" {
   168  		return 0, fmt.Errorf(errPrefix + "need a number with a unit as input")
   169  	}
   170  	if val < 0 {
   171  		return 0, fmt.Errorf(errPrefix + "size cannot be negative")
   172  	}
   173  
   174  	mul, ok := unitMultiplier[strings.ToUpper(unit)]
   175  	if !ok {
   176  		return 0, fmt.Errorf(errPrefix + "try 'kB' or 'MB'")
   177  	}
   178  
   179  	return val * mul, nil
   180  }
   181  
   182  // CommaSeparatedList takes a comman-separated series of identifiers,
   183  // and returns a slice of the space-trimmed identifiers, without empty
   184  // entries.
   185  // So " foo ,, bar,baz" -> {"foo", "bar", "baz"}
   186  func CommaSeparatedList(str string) []string {
   187  	fields := strings.FieldsFunc(str, func(r rune) bool { return r == ',' })
   188  	filtered := fields[:0]
   189  	for _, field := range fields {
   190  		field = strings.TrimSpace(field)
   191  		if field != "" {
   192  			filtered = append(filtered, field)
   193  		}
   194  	}
   195  	return filtered
   196  }
   197  
   198  // ElliptRight returns a string that is at most n runes long,
   199  // replacing the last rune with an ellipsis if necessary. If N is less
   200  // than 1 it's treated as a 1.
   201  func ElliptRight(str string, n int) string {
   202  	if n < 1 {
   203  		n = 1
   204  	}
   205  	if utf8.RuneCountInString(str) <= n {
   206  		return str
   207  	}
   208  
   209  	// this is expensive; look into a cheaper way maybe sometime
   210  	return string([]rune(str)[:n-1]) + "…"
   211  }
   212  
   213  // ElliptLeft returns a string that is at most n runes long,
   214  // replacing the first rune with an ellipsis if necessary. If N is less
   215  // than 1 it's treated as a 1.
   216  func ElliptLeft(str string, n int) string {
   217  	if n < 1 {
   218  		n = 1
   219  	}
   220  	// this is expensive; look into a cheaper way maybe sometime
   221  	rstr := []rune(str)
   222  	if len(rstr) <= n {
   223  		return str
   224  	}
   225  
   226  	return "…" + string(rstr[len(rstr)-n+1:])
   227  }