github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/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 "sort" 25 "strconv" 26 "strings" 27 "unicode/utf8" 28 ) 29 30 // Convert the given size in btes to a readable string 31 func SizeToStr(size int64) string { 32 suffixes := []string{"B", "kB", "MB", "GB", "TB", "PB", "EB"} 33 for _, suf := range suffixes { 34 if size < 1000 { 35 return fmt.Sprintf("%d%s", size, suf) 36 } 37 size /= 1000 38 } 39 panic("SizeToStr got a size bigger than math.MaxInt64") 40 } 41 42 // Quoted formats a slice of strings to a quoted list of 43 // comma-separated strings, e.g. `"snap1", "snap2"` 44 func Quoted(names []string) string { 45 quoted := make([]string, len(names)) 46 for i, name := range names { 47 quoted[i] = strconv.Quote(name) 48 } 49 50 return strings.Join(quoted, ", ") 51 } 52 53 // ListContains determines whether the given string is contained in the 54 // given list of strings. 55 func ListContains(list []string, str string) bool { 56 for _, k := range list { 57 if k == str { 58 return true 59 } 60 } 61 return false 62 } 63 64 // SortedListContains determines whether the given string is contained 65 // in the given list of strings, which must be sorted. 66 func SortedListContains(list []string, str string) bool { 67 i := sort.SearchStrings(list, str) 68 if i >= len(list) { 69 return false 70 } 71 return list[i] == str 72 } 73 74 // SortedListsUniqueMerge merges the two given sorted lists of strings, 75 // repeated values will appear once in the result. 76 func SortedListsUniqueMerge(sl1, sl2 []string) []string { 77 n1 := len(sl1) 78 n2 := len(sl2) 79 sz := n1 80 if n2 > sz { 81 sz = n2 82 } 83 if sz == 0 { 84 return nil 85 } 86 m := make([]string, 0, sz) 87 appendUnique := func(s string) { 88 if l := len(m); l > 0 && m[l-1] == s { 89 return 90 } 91 m = append(m, s) 92 } 93 i, j := 0, 0 94 for i < n1 && j < n2 { 95 var s string 96 if sl1[i] < sl2[j] { 97 s = sl1[i] 98 i++ 99 } else { 100 s = sl2[j] 101 j++ 102 } 103 appendUnique(s) 104 } 105 if i < n1 { 106 for ; i < n1; i++ { 107 appendUnique(sl1[i]) 108 } 109 } else if j < n2 { 110 for ; j < n2; j++ { 111 appendUnique(sl2[j]) 112 } 113 } 114 return m 115 } 116 117 // TruncateOutput truncates input data by maxLines, imposing maxBytes limit (total) for them. 118 // The maxLines may be 0 to avoid the constraint on number of lines. 119 func TruncateOutput(data []byte, maxLines, maxBytes int) []byte { 120 if maxBytes > len(data) { 121 maxBytes = len(data) 122 } 123 lines := maxLines 124 bytes := maxBytes 125 for i := len(data) - 1; i >= 0; i-- { 126 if data[i] == '\n' { 127 lines-- 128 } 129 if lines == 0 || bytes == 0 { 130 return data[i+1:] 131 } 132 bytes-- 133 } 134 return data 135 } 136 137 // SplitUnit takes a string of the form "123unit" and splits 138 // it into the number and non-number parts (123,"unit"). 139 func SplitUnit(inp string) (number int64, unit string, err error) { 140 // go after the number first, break on first non-digit 141 nonDigit := -1 142 for i, c := range inp { 143 // ASCII digits and - only 144 if (c < '0' || c > '9') && c != '-' { 145 nonDigit = i 146 break 147 } 148 } 149 var prefix string 150 switch { 151 case nonDigit == 0: 152 return 0, "", fmt.Errorf("no numerical prefix") 153 case nonDigit == -1: 154 // no unit 155 prefix = inp 156 default: 157 unit = inp[nonDigit:] 158 prefix = inp[:nonDigit] 159 } 160 number, err = strconv.ParseInt(prefix, 10, 64) 161 if err != nil { 162 return 0, "", fmt.Errorf("%q is not a number", prefix) 163 } 164 165 return number, unit, nil 166 } 167 168 // ParseByteSize parses a value like 500kB and returns the number 169 // in bytes. The case of the unit will be ignored for user convenience. 170 func ParseByteSize(inp string) (int64, error) { 171 unitMultiplier := map[string]int64{ 172 "B": 1, 173 // strictly speaking this is "kB" but we ignore cases 174 "KB": 1000, 175 "MB": 1000 * 1000, 176 "GB": 1000 * 1000 * 1000, 177 "TB": 1000 * 1000 * 1000 * 1000, 178 "PB": 1000 * 1000 * 1000 * 1000 * 1000, 179 "EB": 1000 * 1000 * 1000 * 1000 * 1000 * 1000, 180 } 181 182 errPrefix := fmt.Sprintf("cannot parse %q: ", inp) 183 184 val, unit, err := SplitUnit(inp) 185 if err != nil { 186 return 0, fmt.Errorf(errPrefix+"%s", err) 187 } 188 if unit == "" { 189 return 0, fmt.Errorf(errPrefix + "need a number with a unit as input") 190 } 191 if val < 0 { 192 return 0, fmt.Errorf(errPrefix + "size cannot be negative") 193 } 194 195 mul, ok := unitMultiplier[strings.ToUpper(unit)] 196 if !ok { 197 return 0, fmt.Errorf(errPrefix + "try 'kB' or 'MB'") 198 } 199 200 return val * mul, nil 201 } 202 203 // CommaSeparatedList takes a comman-separated series of identifiers, 204 // and returns a slice of the space-trimmed identifiers, without empty 205 // entries. 206 // So " foo ,, bar,baz" -> {"foo", "bar", "baz"} 207 func CommaSeparatedList(str string) []string { 208 fields := strings.FieldsFunc(str, func(r rune) bool { return r == ',' }) 209 filtered := fields[:0] 210 for _, field := range fields { 211 field = strings.TrimSpace(field) 212 if field != "" { 213 filtered = append(filtered, field) 214 } 215 } 216 return filtered 217 } 218 219 // ElliptRight returns a string that is at most n runes long, 220 // replacing the last rune with an ellipsis if necessary. If N is less 221 // than 1 it's treated as a 1. 222 func ElliptRight(str string, n int) string { 223 if n < 1 { 224 n = 1 225 } 226 if utf8.RuneCountInString(str) <= n { 227 return str 228 } 229 230 // this is expensive; look into a cheaper way maybe sometime 231 return string([]rune(str)[:n-1]) + "…" 232 } 233 234 // ElliptLeft returns a string that is at most n runes long, 235 // replacing the first rune with an ellipsis if necessary. If N is less 236 // than 1 it's treated as a 1. 237 func ElliptLeft(str string, n int) string { 238 if n < 1 { 239 n = 1 240 } 241 // this is expensive; look into a cheaper way maybe sometime 242 rstr := []rune(str) 243 if len(rstr) <= n { 244 return str 245 } 246 247 return "…" + string(rstr[len(rstr)-n+1:]) 248 }