package lib import ( "fmt" "os" "slices" "sort" "strconv" "strings" "unicode/utf8" "github.com/rivo/uniseg" ) func BooleanXOR(a, b bool) bool { return a != b } func BoolToInt(b bool) int64 { if !b { return 0 } return 1 } func Plural(n int) string { if n == 1 { return "" } return "s" } // In Go as in all languages I'm aware of with a string-split, "a,b,c" splits // on "," to ["a", "b", "c" and "a" splits to ["a"], both of which are fine -- // but "" splits to [""] when I wish it were []. This function does the latter. func SplitString(input string, separator string) []string { if input == "" { return []string{} } return strings.Split(input, separator) } func StringListToSet(stringList []string) map[string]bool { if stringList == nil { return nil } stringSet := make(map[string]bool) for _, s := range stringList { stringSet[s] = true } return stringSet } // SortedStrings returns a new slice containing the strings in strs in ascending order. func SortedStrings(strs []string) []string { result := make([]string, len(strs)) copy(result, strs) slices.Sort(result) return result } func IntMin2(a, b int64) int64 { if a < b { return a } return b } // TryIntFromString tries decimal, hex, octal, and binary. func TryIntFromString(input string) (int64, bool) { // Go's strconv parses "1_2" as 12; not OK for Miller syntax. (Also not valid JSON.) for i := 0; i < len(input); i++ { if input[i] == '_' { return 0, false } } // Following twos-complement formatting familiar from all manner of // languages, including C which was Miller's original implementation // language, we want to allow 0x00....00 through 0x7f....ff as positive // 64-bit integers and 0x80....00 through 0xff....ff as negative ones. Go's // signed-int parsing explicitly doesn't allow that, but we don't want Go // semantics to dictate Miller semantics. So, we try signed-int parsing // for 0x00....00 through 0x7f....ff, as well as positive or negative // decimal. Failing that, we try unsigned-int parsing for 0x80....00 // through 0xff....ff. i64, ierr := strconv.ParseInt(input, 0 /* check all*/, 64) if ierr == nil { return i64, true } u64, uerr := strconv.ParseUint(input, 0 /* check all*/, 64) if uerr == nil { return int64(u64), true } return 0, false } // TryIntFromStringWithBase allows the user to choose the base that's used, // rather than inferring from 0x prefix, etc as TryIntFromString does. func TryIntFromStringWithBase(input string, base int64) (int64, bool) { // Go's strconv parses "1_2" as 12; not OK for Miller syntax. (Also not valid JSON.) for i := 0; i < len(input); i++ { if input[i] == '_' { return 0, false } } i64, ierr := strconv.ParseInt(input, int(base), 64) if ierr == nil { return i64, true } u64, uerr := strconv.ParseUint(input, int(base), 64) if uerr == nil { return int64(u64), true } return 0, false } func TryFloatFromString(input string) (float64, bool) { // Go's strconv parses "1_2.3_4" as 12.34; not OK for Miller syntax. (Also not valid JSON.) for i := 0; i < len(input); i++ { if input[i] == '_' { return 0, false } } fval, err := strconv.ParseFloat(input, 64) if err == nil { return fval, true } return 0, false } func TryBoolFromBoolString(input string) (bool, bool) { if input == "true" { return true, true } if input == "false" { return false, true } return false, false } // Go doesn't preserve insertion order in its arrays, so here we make an // accessor for getting the keys in sorted order for the benefit of // map-printers. func GetArrayKeysSorted(input map[string]string) []string { keys := make([]string, len(input)) i := 0 for key := range input { keys[i] = key i++ } sort.Strings(keys) return keys } // WriteTempFile places the contents string into a temp file, which the caller // must remove. func WriteTempFileOrDie(contents string) string { // Use "" as first argument to os.CreateTemp to use default directory. // Nominally "/tmp" or somesuch on all unix-like systems, but not for Windows. handle, err := os.CreateTemp("", "mlr-temp") if err != nil { fmt.Printf("mlr: could not create temp file.\n") os.Exit(1) } _, err = handle.WriteString(contents) if err != nil { fmt.Printf("mlr: could not populate temp file.\n") os.Exit(1) } err = handle.Close() if err != nil { fmt.Printf("mlr: could not finish write of temp file.\n") os.Exit(1) } return handle.Name() } func StripEmpties(input []string) []string { output := make([]string, 0, len(input)) for _, e := range input { if e != "" { output = append(output, e) } } return output } func UTF8Strlen(s string) int64 { return int64(utf8.RuneCountInString(s)) } // DisplayWidth returns the number of terminal columns occupied by s, taking // into account East-Asian fullwidth characters (which occupy two columns) and // zero-width combining marks. Used for column alignment in pprint/markdown/xtab // output. Note: zero-width-joiner emoji sequences and ambiguous-width characters // remain terminal-dependent. func DisplayWidth(s string) int { return uniseg.StringWidth(s) }