diff --git a/cmd/subcommands/acci-ping/acci-ping.go b/cmd/subcommands/acci-ping/acci-ping.go index 146ca61..1de7990 100644 --- a/cmd/subcommands/acci-ping/acci-ping.go +++ b/cmd/subcommands/acci-ping/acci-ping.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2024-2025 Lexer747 +// Copyright 2024-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -41,7 +41,7 @@ type Config struct { func GetFlags(info *application.BuildInfo) *Config { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, false, "") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "") sf := application.NewSharedFlags(tf) ret := &Config{ BuildInfo: info, @@ -49,7 +49,7 @@ func GetFlags(info *application.BuildInfo) *Config { FlagSet: tf, filePath: tf.String("file", "", "the file to write the pings into. (default data not saved)", - tabflags.AutoComplete{WantsFile: true, FileExt: ".pings"}), + tabflags.AutoComplete{Completion: tabflags.File, FileExt: ".pings"}), hideHelpOnStart: tf.Bool("hide-help", false, "if this flag is used the help box will be hidden by default"), pingBufferingLimit: new(int), pingsPerMinute: tf.Float64("pings-per-minute", 60.0, @@ -64,7 +64,7 @@ func GetFlags(info *application.BuildInfo) *Config { "There's also the builtin themes:\n"+strings.Join(themes.DescribeBuiltins(), "\n")+ "\nSee the docs "+ansi.Blue("https://github.com/Lexer747/acci-ping/blob/main/docs/themes.md")+ " for how to create custom themes.", - tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true, FileExt: ".json"}), + tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File, FileExt: ".json"}), debuggingTermSize: tf.String("debug-term-size", "", "switches the terminal to fixed mode and no iteractivity", tabflags.AutoComplete{Choices: []string{"15x80", "20x85", "HxW"}}), followingOnStart: tf.Bool("follow", false, "if this flag is used the graph will be shown in following mode immediately"), diff --git a/cmd/subcommands/drawframe/drawframe.go b/cmd/subcommands/drawframe/drawframe.go index 9351148..85bf634 100644 --- a/cmd/subcommands/drawframe/drawframe.go +++ b/cmd/subcommands/drawframe/drawframe.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2024-2025 Lexer747 +// Copyright 2024-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -43,7 +43,7 @@ type Config struct { func GetFlags(info *application.BuildInfo) *Config { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, true, ".pings") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.File|tabflags.Folder, ".pings") sf := application.NewSharedFlags(tf) ret := &Config{ BuildInfo: info, @@ -60,7 +60,7 @@ func GetFlags(info *application.BuildInfo) *Config { "There's also the builtin themes:\n"+strings.Join(themes.DescribeBuiltins(), "\n")+ "\nSee the docs "+ansi.Blue("https://github.com/Lexer747/acci-ping/blob/main/docs/themes.md")+ " for how to create custom themes.", - tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true, FileExt: ".json"}), + tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File, FileExt: ".json"}), yAxisScale: tf.Bool("log-scale", false, "switches the y-axis to be in logarithmic scaling instead of linear"), } f.Usage = func() { diff --git a/cmd/subcommands/ping/ping.go b/cmd/subcommands/ping/ping.go index 6006468..f281911 100644 --- a/cmd/subcommands/ping/ping.go +++ b/cmd/subcommands/ping/ping.go @@ -26,7 +26,7 @@ type Config struct { func GetFlags() *Config { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, false, "") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "") ret := &Config{ url: tf.String("url", "www.google.com", "the url to target for ping testing", tabflags.AutoComplete{}), count: tf.Int("n", 4, "the number of packets to send. 0 or smaller means continuous running."), diff --git a/cmd/subcommands/rawdata/rawdata.go b/cmd/subcommands/rawdata/rawdata.go index 356005f..853bc39 100644 --- a/cmd/subcommands/rawdata/rawdata.go +++ b/cmd/subcommands/rawdata/rawdata.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2024-2025 Lexer747 +// Copyright 2024-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -27,7 +27,7 @@ type Config struct { func GetFlags() *Config { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, true, ".pings") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.File, ".pings") ret := &Config{ FlagSet: tf, printAll: tf.Bool("all", false, "prints all raw values otherwise only summarises '.pings' files"), diff --git a/cmd/subcommands/version/version.go b/cmd/subcommands/version/version.go index e4d984b..51f4b01 100644 --- a/cmd/subcommands/version/version.go +++ b/cmd/subcommands/version/version.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2025 Lexer747 +// Copyright 2025-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -23,7 +23,7 @@ type Config struct { func GetFlags(info *application.BuildInfo) *Config { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, false, "") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "") ret := &Config{ BuildInfo: info, FlagSet: tf, diff --git a/cmd/tab_completion/acci-ping b/cmd/tab_completion/acci-ping index fb67b66..a3c93a3 100755 --- a/cmd/tab_completion/acci-ping +++ b/cmd/tab_completion/acci-ping @@ -1,9 +1,13 @@ _acci-ping() { # $COMP_WORDS # an array of all the words typed after the name of the program the compspec belongs to - # $COMP_CWORD # an index of the COMP_WORDS array pointing to the word the current cursor is at - in other words, the index of the word the cursor was when the tab key was pressed + # $COMP_CWORD # an index of the COMP_WORDS array pointing to the word the current cursor is at - in other + # words, the index of the word the cursor was when the tab key was pressed # $COMP_LINE # the current command line + # These hashes are just the sha256 of `autoCompleteString` and is instead of using a delimiter which is + # more normal, comma, spaces, etc. we use this delimiter which is incredibly unlikely to clash and there by + # reducing the need to escape inputs to zero. local reply reply=$(acci-ping b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "$COMP_CWORD" b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "${COMP_WORDS[*]}" b2827fc8fc8c8267cb15f5a925de7e4712aa04ef2fbd43458326b595d66a36d9 "$COMP_LINE") IFS=" " read -r -a COMPREPLY <<< "$reply" diff --git a/cmd/tab_completion/tab_completion.go b/cmd/tab_completion/tab_completion.go index 79c86b4..9d18b8e 100644 --- a/cmd/tab_completion/tab_completion.go +++ b/cmd/tab_completion/tab_completion.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2025 Lexer747 +// Copyright 2025-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -45,9 +45,13 @@ func Run(args []string, base Command, subCommands []Command) { } // Catch panic's in the case of a mistake in this algorithm simply return no autocomplete suggestions. defer func() { - if err := recover(); err != nil { - slog.Error("caught panic", "err", err) - tabExitFailure() + if recovered := recover(); recovered != nil { + if err, ok := recovered.(error); ok { + tabExitFailure(errors.Wrap(err, "panicked")) + } else { + slog.Error("caught panic", "recovered", recovered) + tabExitFailure(errors.Errorf("panicked")) + } } }() slog.Debug("auto complete called with", "args", args) @@ -67,14 +71,14 @@ func Run(args []string, base Command, subCommands []Command) { index, err := strconv.Atoi(_COMP_CWORD) if err != nil { - tabExitFailure() + tabExitFailure(err) } slog.Debug("inputs", "index", index, "_COMP_CWORD", _COMP_CWORD, "_COMP_LINE", _COMP_LINE, "_COMP_WORDS", _COMP_WORDS) choices, err := getChoices(index, _COMP_LINE, base, subCommands) slog.Debug("results", "choices", choices, "err", err) if err != nil { - tabExitFailure() + tabExitFailure(err) return } tabExit(choices) @@ -100,11 +104,11 @@ func getChoices(index int, _COMP_LINE []string, base Command, cmds []Command) ([ // determine if we have a subcommand selected or are embracing the base and selecting flags isBase := strings.HasPrefix(_COMP_LINE[1], "-") var flags *tabflags.FlagSet - var wantsFile bool + var wants tabflags.Wants var curCommand, fileExt string if isBase { flags = base.Fs - wantsFile = base.Fs.WantsFile() + wants = base.Fs.Wants() curCommand = base.Cmd fileExt = base.Fs.FileExt() } else { @@ -115,14 +119,13 @@ func getChoices(index int, _COMP_LINE []string, base Command, cmds []Command) ([ } cmd := choice[0] flags = cmd.Fs - wantsFile = cmd.Fs.WantsFile() + wants = cmd.Fs.Wants() curCommand = cmd.Cmd fileExt = cmd.Fs.FileExt() } cur := _COMP_LINE[len(_COMP_LINE)-1] - return suggestionAutoComplete(index, _COMP_LINE, flags, wantsFile, fileExt, cur, curCommand), nil + return suggestionAutoComplete(index, _COMP_LINE, flags, wants, fileExt, cur, curCommand) } - slog.Error("unable to solve") return nil, errors.Errorf("unexpected inputs") } @@ -130,16 +133,13 @@ func suggestionAutoComplete( index int, _COMP_LINE []string, flags *tabflags.FlagSet, - wantsFile bool, + wants tabflags.Wants, fileExt, cur, curCommand string, -) []string { +) ([]string, error) { alreadySet := _COMP_LINE[1:index] - var files []string - if wantsFile { - files = getWorkingDirFiles() - if fileExt != "" { - files = sliceutils.Filter(files, func(path string) bool { return filepath.Ext(path) == fileExt }) - } + files, err := getLocalOptions(wants, fileExt) + if err != nil { + return nil, err } prev := _COMP_LINE[index-1] @@ -147,40 +147,83 @@ func suggestionAutoComplete( if ac == nil { includeDebug := strings.HasPrefix(cur, "-d") names := flags.GetNames(includeDebug, alreadySet) - return filterByPrefix(slices.Concat(names, files), cur) + return filterByPrefix(slices.Concat(names, files), cur), nil } // suggest continuation if prev == curCommand { - return returnFlagSetNames(cur, flags, files, alreadySet) + return returnFlagSetNames(cur, flags, files, alreadySet), nil } toSuggest := *ac - if toSuggest.WantsFile { - files = getWorkingDirFiles() - if toSuggest.FileExt != "" { - files = sliceutils.Filter(files, func(path string) bool { return filepath.Ext(path) == toSuggest.FileExt }) - } + files, err = getLocalOptions(toSuggest.Completion, toSuggest.FileExt) + if err != nil { + return nil, err } if len(toSuggest.Choices) == 0 { - return returnFlagSetNames(cur, flags, files, alreadySet) + return returnFlagSetNames(cur, flags, files, alreadySet), nil } - return filterByPrefix(slices.Concat(toSuggest.Choices, files), cur) + return filterByPrefix(slices.Concat(toSuggest.Choices, files), cur), nil } -func getWorkingDirFiles() (files []string) { +// getLocalOptions searches the working dir for the given matching set of possible continuations. The +// continuations depend on the parameters passed, see [tabflags.Wants] and for files, further filtered by the +// file extension. +func getLocalOptions(wants tabflags.Wants, fileExt string) ([]string, error) { + var files []string + if wants.WantsFolderOrFile() { + entries, err := getWorkingDirFiles() + if err != nil { + return files, err + } + var filterMap func(path os.DirEntry) (string, bool) + switch { + case fileExt != "" && wants.IsSet(tabflags.Folder): + // folders are requested so don't filter them out + filterMap = func(path os.DirEntry) (string, bool) { + n := path.Name() + return n, path.IsDir() || filepath.Ext(n) == fileExt + } + case wants.IsSet(tabflags.Folder): + filterMap = func(path os.DirEntry) (string, bool) { + n := path.Name() + return n, path.IsDir() + } + case fileExt != "": + filterMap = func(path os.DirEntry) (string, bool) { + n := path.Name() + return n, filepath.Ext(n) == fileExt + } + default: + filterMap = func(path os.DirEntry) (string, bool) { return path.Name(), true } + } + files = sliceutils.FilterMap(entries, filterMap) + } + return files, nil +} + +func getWorkingDirFiles() (entries []os.DirEntry, err error) { f, err := os.Open("./") if err != nil { - slog.Error("failed to get files", "err", err) + return entries, errors.Wrapf(err, "failed to open working dir %q", currentDirErrorString()) } - files, err = f.Readdirnames(0) + entries, err = f.ReadDir(0) if err != nil { - slog.Error("failed to get files", "err", err) + return entries, errors.Wrapf(err, "failed to get entries %q", currentDirErrorString()) } err = f.Close() if err != nil { - slog.Error("failed to close dir", "err", err) + return entries, errors.Wrapf(err, "failed to close working dir %q", currentDirErrorString()) + } + return entries, nil +} + +func currentDirErrorString() string { + dir, err := os.Getwd() + if err == nil { + return dir + } else { + return "unknown dir: err(" + err.Error() + ")" } - return files } func returnFlagSetNames(cur string, flags *tabflags.FlagSet, files, alreadySet []string) []string { @@ -253,7 +296,8 @@ func tabExit(results []string) { fmt.Fprint(os.Stdout, strings.Join(results, " ")) exit.Success() } -func tabExitFailure() { +func tabExitFailure(err error) { + slog.Error("tabExitFailure", "err", err) tabExit([]string{}) } diff --git a/cmd/tab_completion/tab_completion_test.go b/cmd/tab_completion/tab_completion_test.go index 561d34e..7302fa8 100644 --- a/cmd/tab_completion/tab_completion_test.go +++ b/cmd/tab_completion/tab_completion_test.go @@ -35,7 +35,7 @@ var subCommands = []Command{ //nolint:goconst // constants are not desirable here func TestGetChoices(t *testing.T) { t.Parallel() - t.Run("tab", func(t *testing.T) { + t.Run("", func(t *testing.T) { t.Parallel() actual, err := runGetChoices("acci-ping") assert.NilError(t, err) @@ -69,13 +69,12 @@ func TestGetChoices(t *testing.T) { expectedFlags := acciPingNonDebugFlags() assertEqual(t, actual, expectedFlags) }) - t.Run("start drawframe tab", func(t *testing.T) { + t.Run("start drawframe ", func(t *testing.T) { t.Parallel() actual, err := runGetChoices("acci-ping", "drawframe", "") assert.NilError(t, err) - expectedFlags := drawframeNonDebugFlags() - expectedFlags = slices.Concat(expectedFlags, filesByExt(".go")) + expectedFlags := drawframeNonDebugFlagsAll() assertEqual(t, actual, expectedFlags) }) t.Run("start drawframe -t", func(t *testing.T) { @@ -167,6 +166,24 @@ func TestGetChoices(t *testing.T) { actual, err := runGetChoices("acci-ping", "-theme", "c") assert.NilError(t, err) + assertEqual(t, actual, expectedFlags) + }) + t.Run("- drawframe", func(t *testing.T) { + t.Parallel() + expectedFlags := acciPingNonDebugFlags() + + actual, err := getChoices(1, []string{"acci-ping", "-", "drawframe"}, accipingFlags, subCommands) + assert.NilError(t, err) + + assertEqual(t, actual, expectedFlags) + }) + t.Run("drawframe ", func(t *testing.T) { + t.Parallel() + expectedFlags := drawframeNonDebugFlagsAll() + + actual, err := getChoices(3, []string{"acci-ping", "drawframe", ""}, accipingFlags, subCommands) + assert.NilError(t, err) + assertEqual(t, actual, expectedFlags) }) } @@ -183,16 +200,16 @@ type boolFlag interface { //nolint:staticcheck // ST1003 underscores are fine here we want casing to be correct for the command func make_acciping_Flags() Command { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, false, "") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.Nothing, "") _ = application.NewSharedFlags(tf) _ = tf.String("file", "", "skipped for test", - tabflags.AutoComplete{WantsFile: true, FileExt: ".go"}) + tabflags.AutoComplete{Completion: tabflags.File, FileExt: ".go"}) _ = tf.Bool("hide-help", false, "skipped for test") _ = tf.Bool("debug-error-creator", false, "skipped for test") _ = tf.String("url", "www.google.com", "skipped for test", tabflags.AutoComplete{}) _ = tf.String("theme", "", "skipped for test", - tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true}) + tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File}) _ = tf.String("debug-term-size", "", "skipped for test", tabflags.AutoComplete{Choices: []string{"15x80", "20x85", "HxW"}}) _ = tf.Bool("follow", false, "skipped for test") _ = tf.Int("debug-fps", 240, "skipped for test") @@ -203,13 +220,13 @@ func make_acciping_Flags() Command { //nolint:staticcheck func make_drawframe_Flags() Command { f := flag.NewFlagSet("", flag.ContinueOnError) - tf := tabflags.NewAutoCompleteFlagSet(f, true, ".go") + tf := tabflags.NewAutoCompleteFlagSet(f, tabflags.File|tabflags.Folder, ".go") _ = application.NewSharedFlags(tf) _ = tf.Bool("debug-follow", false, "skipped for test") _ = tf.String("term-size", "", "skipped for test", tabflags.AutoComplete{Choices: []string{"15x80", "20x85", "HxW"}}) _ = tf.String("theme", "", "skipped for test", - tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), WantsFile: true}) + tabflags.AutoComplete{Choices: themes.GetBuiltInNames(), Completion: tabflags.File}) _ = tf.Bool("log-scale", false, "skipped for test") return Command{Cmd: "drawframe", Fs: tf} } @@ -224,8 +241,20 @@ func filesByExt(ext string) []string { if err != nil { log.Fatal(err) } - files := sliceutils.Map(entries, func(d os.DirEntry) string { return d.Name() }) - return sliceutils.Filter(files, func(f string) bool { return ext == "" || filepath.Ext(f) == ext }) + return sliceutils.FilterMap(entries, func(d os.DirEntry) (string, bool) { + n := d.Name() + return n, ext == "" || filepath.Ext(n) == ext + }) +} + +func folders() []string { + entries, err := os.ReadDir("./") + if err != nil { + log.Fatal(err) + } + return sliceutils.FilterMap(entries, func(d os.DirEntry) (string, bool) { + return d.Name(), d.IsDir() + }) } func assertEqual(t *testing.T, expected, actual []string) { @@ -256,3 +285,10 @@ func drawframeNonDebugFlags() []string { }) return expectedFlags } + +func drawframeNonDebugFlagsAll() []string { + expectedFlags := drawframeNonDebugFlags() + expectedFlags = slices.Concat(expectedFlags, filesByExt(".go")) + expectedFlags = slices.Concat(expectedFlags, folders()) + return expectedFlags +} diff --git a/cmd/tab_completion/tabflags/tabflags.go b/cmd/tab_completion/tabflags/tabflags.go index 8a99e4a..276827f 100644 --- a/cmd/tab_completion/tabflags/tabflags.go +++ b/cmd/tab_completion/tabflags/tabflags.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2025 Lexer747 +// Copyright 2025-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -17,9 +17,9 @@ import ( ) type AutoComplete struct { - FileExt string - Choices []string - WantsFile bool + FileExt string + Choices []string + Completion Wants } type Flag struct { @@ -36,31 +36,30 @@ type FlagSet struct { nameToAc map[string]*AutoComplete o *sync.Once - fileExt string - flags []Flag - wantsFile bool + fileExt string + flags []Flag + completion Wants } -// NewAutoCompleteFlagSet wraps a [flag.FlagSet] with autocomplete configuration, if [wantsFile] is set then +// NewAutoCompleteFlagSet wraps a [flag.FlagSet] with autocomplete configuration, if [completion] is set then // it's expected that the overall command wants a file so autocomplete suggestions from the working dir will // be given. If [fileExt] is set then only the files which match the extension are suggested. -func NewAutoCompleteFlagSet(f *flag.FlagSet, wantsFile bool, fileExt string) *FlagSet { +func NewAutoCompleteFlagSet(f *flag.FlagSet, completion Wants, fileExt string) *FlagSet { return &FlagSet{ - FlagSet: f, - flags: []Flag{}, - nameToAc: map[string]*AutoComplete{}, - wantsFile: wantsFile, - fileExt: fileExt, - o: &sync.Once{}, + FlagSet: f, + flags: []Flag{}, + nameToAc: map[string]*AutoComplete{}, + completion: completion, + fileExt: fileExt, + o: &sync.Once{}, } } -// WantsFile returns true if this flag set wants a file as a free form arg. -func (f *FlagSet) WantsFile() bool { - return f.wantsFile +func (f *FlagSet) Wants() Wants { + return f.completion } -// FileExt returns the specified file extensions to look for if [FlagSet.WantsFile] returns true. +// FileExt returns the specified file extensions to look for if [FlagSet.Wants] returns File. func (f *FlagSet) FileExt() string { return f.fileExt } diff --git a/cmd/tab_completion/tabflags/wants.go b/cmd/tab_completion/tabflags/wants.go new file mode 100644 index 0000000..0b237cc --- /dev/null +++ b/cmd/tab_completion/tabflags/wants.go @@ -0,0 +1,23 @@ +// Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. +// +// Copyright 2026 Lexer747 +// +// SPDX-License-Identifier: GPL-2.0-only + +package tabflags + +type Wants byte + +const ( + Nothing Wants = 0b00000000 + File Wants = 0b00000001 + Folder Wants = 0b00000010 +) + +func (b Wants) WantsFolderOrFile() bool { + return b.IsSet(File) || b.IsSet(Folder) +} + +func (b Wants) IsSet(desc Wants) bool { + return b&desc == desc +} diff --git a/utils/application/application.go b/utils/application/application.go index a508740..1d375dc 100644 --- a/utils/application/application.go +++ b/utils/application/application.go @@ -1,6 +1,6 @@ // Use of this source code is governed by a GPL-2 license that can be found in the LICENSE file. // -// Copyright 2025 Lexer747 +// Copyright 2025-2026 Lexer747 // // SPDX-License-Identifier: GPL-2.0-only @@ -80,11 +80,12 @@ type SharedFlags struct { func NewSharedFlags(tf *tabflags.FlagSet) *SharedFlags { return &SharedFlags{ - cpuprofile: tf.String("debug-cpuprofile", "", "write cpu profile to `file`", tabflags.AutoComplete{WantsFile: true}), + cpuprofile: tf.String("debug-cpuprofile", "", "write cpu profile to `file`", tabflags.AutoComplete{Completion: tabflags.File}), debugStrict: tf.Bool("debug-strict", false, "enables more strict operation in which warnings turn into crashes."), - logFile: tf.String("debug-log", "", "write logs to `file`. (default no logs written)", tabflags.AutoComplete{WantsFile: true}), - memprofile: tf.String("debug-memprofile", "", "write memory profile to `file`", tabflags.AutoComplete{WantsFile: true}), - helpDebug: tf.Bool("help-debug", false, "prints all additional debug arguments"), + logFile: tf.String("debug-log", "", "write logs to `file`. (default no logs written)", + tabflags.AutoComplete{Completion: tabflags.File}), + memprofile: tf.String("debug-memprofile", "", "write memory profile to `file`", tabflags.AutoComplete{Completion: tabflags.File}), + helpDebug: tf.Bool("help-debug", false, "prints all additional debug arguments"), } } diff --git a/utils/sliceutils/sliceutils.go b/utils/sliceutils/sliceutils.go index 6a36410..5e838c0 100644 --- a/utils/sliceutils/sliceutils.go +++ b/utils/sliceutils/sliceutils.go @@ -122,6 +122,18 @@ func Filter[S ~[]T, T any](slice S, useInOutput func(T) bool) S { return ret } +// FilterMap returns a shallow clone of [slice] where only the values which return true from the predicate are +// included. Converted by [Map], this is more efficient than calling them in sequence. +func FilterMap[S ~[]IN, IN, OUT any](slice S, useInOutput func(IN) (OUT, bool)) []OUT { + ret := make([]OUT, 0) + for _, item := range slice { + if out, ok := useInOutput(item); ok { + ret = append(ret, out) + } + } + return ret +} + // TakeRandom returns a random element from the slice, or the zero value if the slice is empty. // // This is not a cryptographically secure random, do not use this for security.