mirror of
https://github.com/johnkerl/miller.git
synced 2026-07-18 00:45:47 +00:00
* Rebuild docs: help-catalog index count drifted on main (666 -> 667)
Pre-existing drift from a recent catalog addition; surfaced by make dev.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Remove os.Exit callsites below the entrypoint: phase 1 (plans/exit.md)
Phase 1 of plans/exit.md: the mechanical swaps, plus the single-exit-point
scaffolding.
- New sentinel lib.ExitRequest{Code} (io.EOF-style control flow): returned by
code paths that have already printed what they wanted (--version, -h,
'put --explain' on a valid expression, 'put -x') instead of exiting mid-stack.
- pkg/entrypoint: new exitOnError() maps errors to exit codes in one place:
ErrHelpRequested -> 0, ErrUsagePrinted -> 1, ExitRequest -> its code,
anything else printed (JSON if --errors-json) -> 1.
- pkg/climain: version/help/usage/validation exits become returned errors;
the ErrHelpRequested/ErrUsagePrinted -> exit translation moves up to the
entrypoint; loadMlrrcOrDie becomes error-returning loadMlrrcFiles.
- Transformer ParseCLI/constructor exits -> returned errors (cut,
having_fields, nest, reorder, merge_fields, reshape, rename, split, tee,
subs, put_or_filter). merge_fields' bad-regex message formerly mis-reported
itself as coming from the cut verb; tee's unrecognized-option exit was
formerly silent; split/tee constructor failures formerly exited without
printing the constructor's error.
- YAML/JSON record-writer marshal errors -> returned through Write, matching
the CSV writer's error path.
- lib.WriteTempFileOrDie -> WriteTempFile (string, error); its sole caller
(regtest diff helper) degrades gracefully.
Stderr messages and exit codes are byte-identical for all regression-covered
paths (4779 cases pass); runtime Transform-path exits are phase 3.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
* Add lib.NewExitZeroRequest() constructor for exit-0 sentinel returns
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
202 lines
5 KiB
Go
202 lines
5 KiB
Go
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 WriteTempFile(contents string) (string, error) {
|
|
// 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 {
|
|
return "", fmt.Errorf("could not create temp file: %w", err)
|
|
}
|
|
|
|
_, err = handle.WriteString(contents)
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not populate temp file: %w", err)
|
|
}
|
|
|
|
err = handle.Close()
|
|
if err != nil {
|
|
return "", fmt.Errorf("could not finish write of temp file: %w", err)
|
|
}
|
|
return handle.Name(), nil
|
|
}
|
|
|
|
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)
|
|
}
|