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 }