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  }