From 4c6c8209bafd3dc3a998b7b49296d0b107fce55a Mon Sep 17 00:00:00 2001 From: John Kerl Date: Wed, 17 Nov 2021 22:42:21 -0500 Subject: [PATCH 1/2] Explicitly support approximate-match help --- Makefile | 1 + internal/pkg/auxents/help/entry.go | 231 ++++++++++++------ internal/pkg/auxents/regtest/entry.go | 7 +- internal/pkg/auxents/regtest/regtester.go | 5 +- internal/pkg/auxents/repl/verbs.go | 61 ++++- internal/pkg/cli/flag_types.go | 54 +++- .../pkg/dsl/cst/builtin_function_manager.go | 29 ++- internal/pkg/dsl/cst/keyword_usage.go | 12 + .../pkg/transformers/aaa_transformer_table.go | 26 +- todo.txt | 10 +- 10 files changed, 326 insertions(+), 110 deletions(-) diff --git a/Makefile b/Makefile index 4fe53f20f..2c8b6a997 100644 --- a/Makefile +++ b/Makefile @@ -6,6 +6,7 @@ build: mlr: go build github.com/johnkerl/miller/cmd/mlr +# For interactive use, 'mlr regtest' offers more options and transparency. check: unit_test regression_test # Unit tests (small number) diff --git a/internal/pkg/auxents/help/entry.go b/internal/pkg/auxents/help/entry.go index 93f8ea9db..64418bad8 100644 --- a/internal/pkg/auxents/help/entry.go +++ b/internal/pkg/auxents/help/entry.go @@ -19,7 +19,7 @@ import ( // ================================================================ type tZaryHandlerFunc func() -type tUnaryHandlerFunc func(arg string) +type tVarArgHandlerFunc func(args []string) type tHandlerLookupTable struct { sections []tHandlerInfoSection @@ -35,9 +35,9 @@ type tHandlerInfoSection struct { } type tHandlerInfo struct { - name string - zaryHandlerFunc tZaryHandlerFunc - unaryHandlerFunc tUnaryHandlerFunc + name string + zaryHandlerFunc tZaryHandlerFunc + varArgHandlerFunc tVarArgHandlerFunc } type tShorthandTable struct { @@ -80,7 +80,7 @@ func init() { handlerInfos: []tHandlerInfo{ {name: "list-verbs", zaryHandlerFunc: listVerbs}, {name: "usage-verbs", zaryHandlerFunc: usageVerbs}, - {name: "verb", unaryHandlerFunc: helpForVerb}, + {name: "verb", varArgHandlerFunc: helpForVerb}, }, }, { @@ -88,10 +88,10 @@ func init() { handlerInfos: []tHandlerInfo{ {name: "list-functions", zaryHandlerFunc: listFunctions}, {name: "list-function-classes", zaryHandlerFunc: listFunctionClasses}, - {name: "list-functions-in-class", unaryHandlerFunc: listFunctionsInClass}, + {name: "list-functions-in-class", varArgHandlerFunc: listFunctionsInClass}, {name: "usage-functions", zaryHandlerFunc: usageFunctions}, {name: "usage-functions-by-class", zaryHandlerFunc: usageFunctionsByClass}, - {name: "function", unaryHandlerFunc: helpForFunction}, + {name: "function", varArgHandlerFunc: helpForFunction}, }, }, { @@ -99,7 +99,7 @@ func init() { handlerInfos: []tHandlerInfo{ {name: "list-keywords", zaryHandlerFunc: listKeywords}, {name: "usage-keywords", zaryHandlerFunc: usageKeywords}, - {name: "keyword", unaryHandlerFunc: helpForKeyword}, + {name: "keyword", varArgHandlerFunc: helpForKeyword}, }, }, { @@ -117,16 +117,16 @@ func init() { handlerInfos: []tHandlerInfo{ {name: "flag-table-nil-check", zaryHandlerFunc: flagTableNilCheck}, {name: "list-flag-sections", zaryHandlerFunc: listFlagSections}, - {name: "list-flags-for-section", unaryHandlerFunc: listFlagsForSection}, + {name: "list-flags-for-section", varArgHandlerFunc: listFlagsForSection}, {name: "list-functions-as-paragraph", zaryHandlerFunc: listFunctionsAsParagraph}, {name: "list-functions-as-table", zaryHandlerFunc: listFunctionsAsTable}, {name: "list-keywords-as-paragraph", zaryHandlerFunc: listKeywordsAsParagraph}, {name: "list-verbs-as-paragraph", zaryHandlerFunc: listVerbsAsParagraph}, - {name: "print-info-for-section", unaryHandlerFunc: printInfoForSection}, - {name: "show-headline-for-flag", unaryHandlerFunc: showHeadlineForFlag}, - {name: "show-help-for-flag", unaryHandlerFunc: showHelpForFlag}, - {name: "show-help-for-section", unaryHandlerFunc: showHelpForSection}, - {name: "show-help-for-section-via-downdash", unaryHandlerFunc: showHelpForSectionViaDowndash}, + {name: "print-info-for-section", varArgHandlerFunc: printInfoForSection}, + {name: "show-headline-for-flag", varArgHandlerFunc: showHeadlineForFlag}, + {name: "show-help-for-flag", varArgHandlerFunc: showHelpForFlag}, + {name: "show-help-for-section", varArgHandlerFunc: showHelpForSection}, + {name: "show-help-for-section-via-downdash", varArgHandlerFunc: showHelpForSectionViaDowndash}, }, }, }, @@ -158,7 +158,7 @@ func init() { name: downdashSectionName, // Make a function which passes in "csv-only-flags" etc. to the FLAG_TABLE. zaryHandlerFunc: func() { - showHelpForSectionViaDowndash(downdashSectionName) + showHelpForSectionViaDowndash([]string{downdashSectionName}) }, } handlerLookupTable.sections[i].handlerInfos = append(handlerLookupTable.sections[i].handlerInfos, entry) @@ -193,6 +193,19 @@ func HelpMain(args []string) int { return 0 } + // 'mlr help find x' searches for all things (flags, transformers, + // functions, keywords) with an "x" in their name, as a substring. + if args[0] == "find" { + args = args[1:] + if len(args) > 0 { + helpByApproximateSearch(args) + return 0 + } else { + fmt.Printf("mlr help find: need one or more things to search for.\n") + return 1 + } + } + // "mlr help something" where we recognize the something name := args[0] for _, section := range handlerLookupTable.sections { @@ -206,26 +219,26 @@ func HelpMain(args []string) int { info.zaryHandlerFunc() return 0 } - if info.unaryHandlerFunc != nil { + if info.varArgHandlerFunc != nil { if len(args) < 2 { fmt.Printf("mlr help %s takes at least one required argument.\n", name) return 0 } - for _, arg := range args[1:] { - info.unaryHandlerFunc(arg) - } + info.varArgHandlerFunc(args[1:]) return 0 } } } } - if helpBySearch(name) { + // 'mlr help x' searches for all things (flags, transformers, functions, keywords) named "x". + if helpByExactSearch(args) { return 0 } // "mlr help something" where we do not recognize the something - fmt.Printf("No help found for \"%s\" -- please try 'mlr help topics'.\n", name) + fmt.Printf("No help found for \"%s\". Please try 'mlr help find %s' for approximate match.\n", name, name) + fmt.Printf("See also 'mlr help topics'.\n") return 0 } @@ -482,51 +495,63 @@ func listFlagSections() { cli.FLAG_TABLE.ListFlagSections() } -func printInfoForSection(sectionName string) { - if !cli.FLAG_TABLE.PrintInfoForSection(sectionName) { - fmt.Printf( - "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", - sectionName) +func printInfoForSection(sectionNames []string) { + for _, sectionName := range sectionNames { + if !cli.FLAG_TABLE.PrintInfoForSection(sectionName) { + fmt.Printf( + "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", + sectionName) + } } } -func listFlagsForSection(sectionName string) { - if !cli.FLAG_TABLE.ListFlagsForSection(sectionName) { - fmt.Printf( - "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", - sectionName) +func listFlagsForSection(sectionNames []string) { + for _, sectionName := range sectionNames { + if !cli.FLAG_TABLE.ListFlagsForSection(sectionName) { + fmt.Printf( + "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", + sectionName) + } } } // For manpage autogen: just produce text -func showHelpForSection(sectionName string) { - if !cli.FLAG_TABLE.ShowHelpForSection(sectionName) { - fmt.Printf( - "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", - sectionName) +func showHelpForSection(sectionNames []string) { + for _, sectionName := range sectionNames { + if !cli.FLAG_TABLE.ShowHelpForSection(sectionName) { + fmt.Printf( + "mlr: flag-section \"%s\" not found. Please use \"mlr help list-flag-sections\" for a list.\n", + sectionName) + } } } // For on-the-fly `mlr help foo-bar-flags` where `Foo-bar flags` is the name of // a section in the FLAG_TABLE. See the func-init block at the top of this // file. -func showHelpForSectionViaDowndash(downdashSectionName string) { - if !cli.FLAG_TABLE.ShowHelpForSectionViaDowndash(downdashSectionName) { - fmt.Printf("mlr: flag-section \"%s\" not found.\n", downdashSectionName) +func showHelpForSectionViaDowndash(downdashSectionNames []string) { + for _, downdashSectionName := range downdashSectionNames { + if !cli.FLAG_TABLE.ShowHelpForSectionViaDowndash(downdashSectionName) { + fmt.Printf("mlr: flag-section \"%s\" not found.\n", downdashSectionName) + } } } // For webdocs autogen: we want the headline separately so we can backtick it. -func showHeadlineForFlag(flagName string) { - if !cli.FLAG_TABLE.ShowHeadlineForFlag(flagName) { - fmt.Printf("mlr: flag \"%s\" not found..\n", flagName) +func showHeadlineForFlag(flagNames []string) { + for _, flagName := range flagNames { + if !cli.FLAG_TABLE.ShowHeadlineForFlag(flagName) { + fmt.Printf("mlr: flag \"%s\" not found..\n", flagName) + } } } // For webdocs autogen -func showHelpForFlag(flagName string) { - if !cli.FLAG_TABLE.ShowHelpForFlag(flagName) { - fmt.Printf("mlr: flag \"%s\" not found..\n", flagName) +func showHelpForFlag(flagNames []string) { + for _, flagName := range flagNames { + if !cli.FLAG_TABLE.ShowHelpForFlag(flagName) { + fmt.Printf("mlr: flag \"%s\" not found..\n", flagName) + } } } @@ -543,14 +568,13 @@ func listVerbsAsParagraph() { transformers.ListVerbNamesAsParagraph() } -func helpForVerb(arg string) { - transformerSetup := transformers.LookUp(arg) - if transformerSetup != nil { - transformerSetup.UsageFunc(os.Stdout, true, 0) - } else { - fmt.Printf( - "mlr: verb \"%s\" not found. Please use \"mlr help list-verbs\" for a list.\n", - arg) +func helpForVerb(args []string) { + for _, arg := range args { + if !transformers.ShowHelpForTransformer(arg) { + fmt.Printf( + "mlr: verb \"%s\" not found. Please use \"mlr help list-verbs\" for a list.\n", + arg) + } } } @@ -571,8 +595,10 @@ func listFunctionClasses() { cst.BuiltinFunctionManagerInstance.ListBuiltinFunctionClasses() } -func listFunctionsInClass(class string) { - cst.BuiltinFunctionManagerInstance.ListBuiltinFunctionsInClass(class) +func listFunctionsInClass(classes []string) { + for _, class := range classes { + cst.BuiltinFunctionManagerInstance.ListBuiltinFunctionsInClass(class) + } } func listFunctionsAsParagraph() { @@ -591,45 +617,88 @@ func usageFunctionsByClass() { cst.BuiltinFunctionManagerInstance.ListBuiltinFunctionUsagesByClass() } -func helpForFunction(arg string) { - cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(arg, true) +func helpForFunction(args []string) { + for _, arg := range args { + cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(arg) + } } -// TODO: comment -// xxx polymorphic looker-upper: try: -// o flag -// o verb -// o function -// o keyword -// xxx note 'mlr help sort' finds verb before DSL function w/ same name ... -// xxx 'mlr help verb sort' vs 'mlr help function sort' -func helpBySearch(thing string) bool { +func helpByExactSearch(things []string) bool { + foundAny := false + for _, thing := range things { + foundThisOne := helpByExactSearchOne(thing) + foundAny = foundAny || foundThisOne + } + + return foundAny +} + +// We need to look various places, e.g. "sec2gmt" is the name of a verb as well +// as a DSL function. +func helpByExactSearchOne(thing string) bool { + found := false // flag - if cli.FLAG_TABLE.ShowHelpForFlag(thing) { - return true + if cli.FLAG_TABLE.ShowHelpForFlagWithName(thing) { + found = true } // verb - transformerSetup := transformers.LookUp(thing) - if transformerSetup != nil { - transformerSetup.UsageFunc(os.Stdout, true, 0) - return true + if transformers.ShowHelpForTransformer(thing) { + found = true } // function - // to do: parameterize inexact-match printing ... - if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(thing, false) { - return true + if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(thing) { + found = true } // keyword if cst.TryUsageForKeyword(thing) { - return true + found = true } - // not found - return false + return found +} + +func helpByApproximateSearch(things []string) bool { + foundAny := false + for _, thing := range things { + foundThisOne := helpByApproximateSearchOne(thing) + foundAny = foundAny || foundThisOne + } + + return foundAny +} + +func helpByApproximateSearchOne(thing string) bool { + found := false + + // flag + if cli.FLAG_TABLE.ShowHelpForFlagApproximateWithName(thing) { + found = true + } + + // verb + if transformers.ShowHelpForTransformerApproximate(thing) { + found = true + } + + // function + if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsageApproximate(thing) { + found = true + } + + // keyword + if cst.TryUsageForKeywordApproximate(thing) { + found = true + } + + if !found { + fmt.Printf("No help found for \"%s\". Please try 'mlr help find %s' for approximate match.\n", thing, thing) + fmt.Printf("See also 'mlr help topics'.\n") + } + return found } // ---------------------------------------------------------------- @@ -649,8 +718,10 @@ func usageKeywords() { cst.UsageKeywords() } -func helpForKeyword(arg string) { - cst.UsageForKeyword(arg) +func helpForKeyword(args []string) { + for _, arg := range args { + cst.UsageForKeyword(arg) + } } // ---------------------------------------------------------------- diff --git a/internal/pkg/auxents/regtest/entry.go b/internal/pkg/auxents/regtest/entry.go index 6c509a005..74d5ce0e3 100644 --- a/internal/pkg/auxents/regtest/entry.go +++ b/internal/pkg/auxents/regtest/entry.go @@ -12,11 +12,13 @@ import ( "strings" ) +const defaultPath = "./test/cases" + // ================================================================ func regTestUsage(verbName string, o *os.File, exitCode int) { exeName := path.Base(os.Args[0]) fmt.Fprintf(o, "Usage: %s %s [options] [one or more directories/files]\n", exeName, verbName) - fmt.Fprintf(o, "If no directories/files are specified, the directory %s is used by default.\n", DefaultPath) + fmt.Fprintf(o, "If no directories/files are specified, the directory %s is used by default.\n", defaultPath) fmt.Fprintf(o, "Recursively walks the directory/ies looking for foo.cmd files having Miller command-lines,\n") fmt.Fprintf(o, "with foo.expout and foo.experr files having expected stdout and stderr, respectively.\n") fmt.Fprintf(o, "If foo.should-fail exists and is a file, the command is expected to exit non-zero back to\n") @@ -94,6 +96,9 @@ func RegTestMain(args []string) int { } } casePaths := args[argi:] + if len(casePaths) == 0 { + casePaths = []string{defaultPath} + } regtester := NewRegTester( exeName, diff --git a/internal/pkg/auxents/regtest/regtester.go b/internal/pkg/auxents/regtest/regtester.go index 73e2dbf1a..31cd1c150 100644 --- a/internal/pkg/auxents/regtest/regtester.go +++ b/internal/pkg/auxents/regtest/regtester.go @@ -69,7 +69,6 @@ import ( "github.com/johnkerl/miller/internal/pkg/lib" ) -const DefaultPath = "./cases" const CmdName = "cmd" const MlrName = "mlr" const EnvName = "env" @@ -163,9 +162,7 @@ func (regtester *RegTester) Execute( regtester.resetCounts() - if len(casePaths) == 0 { - casePaths = []string{DefaultPath} - } + lib.InternalCodingErrorIf(len(casePaths) == 0) if !regtester.plainMode { fmt.Println("REGRESSION TEST:") diff --git a/internal/pkg/auxents/repl/verbs.go b/internal/pkg/auxents/repl/verbs.go index a941846f3..551b6519b 100644 --- a/internal/pkg/auxents/repl/verbs.go +++ b/internal/pkg/auxents/repl/verbs.go @@ -70,6 +70,31 @@ func (repl *Repl) findHandler(verbName string) *handlerInfo { return nil } +func (repl *Repl) showUsageForHandler(verbName string) bool { + nonDSLHandler := repl.findHandler(verbName) + if nonDSLHandler != nil { + fmt.Println(colorizer.MaybeColorizeHelp(verbName, true)) + nonDSLHandler.usageFunc(repl) + return true + } else { + return false + } +} + +func (repl *Repl) showUsageForHandlerApproximate(searchString string) bool { + foundAny := false + for _, entry := range handlerLookupTable { + for _, entryVerbName := range entry.verbNames { + if strings.Contains(entryVerbName, searchString) { + fmt.Println(colorizer.MaybeColorizeHelp(entryVerbName, true)) + entry.usageFunc(repl) + foundAny = true + } + } + } + return foundAny +} + // ---------------------------------------------------------------- // Handles a single non-DSL statement like ':open foo.dat' or ':help'. func (repl *Repl) handleNonDSLLine(trimmedLine string) bool { @@ -786,7 +811,7 @@ func usageHelp(repl *Repl) { fmt.Println(":help prompt") fmt.Println(":help function-names") fmt.Println(":help function-details") - fmt.Println(":help {function name}, e.g. :help sec2gmt") + fmt.Println(":help find {substring}, e.g. :help gmt or :help map") fmt.Println(":help {function name}, e.g. :help sec2gmt") } @@ -797,6 +822,14 @@ func handleHelp(repl *Repl, args []string) bool { return true } + if len(args) >= 1 && args[0] == "find" { + args = args[1:] + for _, arg := range args { + handleHelpFindSingle(repl, arg) + } + return true + } + for _, arg := range args { handleHelpSingle(repl, arg) } @@ -804,6 +837,26 @@ func handleHelp(repl *Repl, args []string) bool { return true } +func handleHelpFindSingle(repl *Repl, arg string) { + foundAny := false + + if cst.TryUsageForKeywordApproximate(arg) { + foundAny = true + } + + if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsageApproximate(arg) { + foundAny = true + } + + if repl.showUsageForHandlerApproximate(arg) { + foundAny = true + } + + if !foundAny { + fmt.Printf("No help available for %s. Try \":help find %s\" to search for matches\n", arg, arg) + } +} + func handleHelpSingle(repl *Repl, arg string) { if arg == "intro" { showREPLIntro(repl) @@ -872,13 +925,11 @@ func handleHelpSingle(repl *Repl, arg string) { return } - if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(arg, true) { + if cst.BuiltinFunctionManagerInstance.TryListBuiltinFunctionUsage(arg) { return } - nonDSLHandler := repl.findHandler(arg) - if nonDSLHandler != nil { - nonDSLHandler.usageFunc(repl) + if repl.showUsageForHandler(arg) { return } diff --git a/internal/pkg/cli/flag_types.go b/internal/pkg/cli/flag_types.go index caeea95c6..4a285962c 100644 --- a/internal/pkg/cli/flag_types.go +++ b/internal/pkg/cli/flag_types.go @@ -258,9 +258,25 @@ func (ft *FlagTable) ShowHeadlineForFlag(flagName string) bool { // webdoc usage where the browser does dynamic line-wrapping, as the user // resizes the browser window. func (ft *FlagTable) ShowHelpForFlag(flagName string) bool { + return ft.showHelpForFlagMaybeWithName(flagName, false) +} + +// ShowHelpForFlagWithName prints the flag's name colorized, then flag's +// help-string all on one line. This is for on-line help usage. +func (ft *FlagTable) ShowHelpForFlagWithName(flagName string) bool { + return ft.showHelpForFlagMaybeWithName(flagName, true) +} + +// showHelpForFlagMaybeWithName supports ShowHelpForFlag and ShowHelpForFlagWithName. +// webdoc usage where the browser does dynamic line-wrapping, as the user +// resizes the browser window. +func (ft *FlagTable) showHelpForFlagMaybeWithName(flagName string, showName bool) bool { for _, fs := range ft.sections { for _, flag := range fs.flags { if flag.Owns(flagName) { + if showName { + fmt.Println(colorizer.MaybeColorizeHelp(flagName, true)) + } fmt.Println(flag.GetHelpOneLine()) return true } @@ -269,6 +285,20 @@ func (ft *FlagTable) ShowHelpForFlag(flagName string) bool { return false } +// ShowHelpForFlagApproximateWithName is like ShowHelpForFlagWithName +// but allows substring matches. This is for on-line help usage. +func (ft *FlagTable) ShowHelpForFlagApproximateWithName(searchString string) bool { + for _, fs := range ft.sections { + for _, flag := range fs.flags { + if flag.Matches(searchString) { + fmt.Println(colorizer.MaybeColorizeHelp(flag.name, true)) + fmt.Println(flag.GetHelpOneLine()) + } + } + } + return false +} + // Map "CSV-only flags" to "csv-only-flags" etc. for the benefit of per-section // help in `mlr help topics`. func (ft *FlagTable) GetDowndashSectionNames() []string { @@ -361,14 +391,32 @@ func (fs *FlagSection) NilCheck() { // ================================================================ // Flag methods -// Owns determines whether this object handles a command-line flag such as "--foo". +// Owns determines whether this object handles a command-line flag such as +// "--foo". This is used for command-line parsing, as well as for on-line help +// with exact match on flag name. func (flag *Flag) Owns(input string) bool { - if input == flag.name { + if flag.name == input { return true } if flag.altNames != nil { for _, name := range flag.altNames { - if input == name { + if name == input { + return true + } + } + } + return false +} + +// Matches is like Owns but is for substring matching, for on-line help with +// approximate match on flag name. +func (flag *Flag) Matches(input string) bool { + if strings.Contains(flag.name, input) { + return true + } + if flag.altNames != nil { + for _, name := range flag.altNames { + if strings.Contains(name, input) { return true } } diff --git a/internal/pkg/dsl/cst/builtin_function_manager.go b/internal/pkg/dsl/cst/builtin_function_manager.go index cf42a7e84..d90eec326 100644 --- a/internal/pkg/dsl/cst/builtin_function_manager.go +++ b/internal/pkg/dsl/cst/builtin_function_manager.go @@ -1906,27 +1906,35 @@ func (manager *BuiltinFunctionManager) ListBuiltinFunctionUsagesByClass() { } func (manager *BuiltinFunctionManager) ListBuiltinFunctionUsage(functionName string) { - // xxx remove bool-arg code smell - if !manager.TryListBuiltinFunctionUsage(functionName, true) { + if !manager.TryListBuiltinFunctionUsage(functionName) { fmt.Fprintf(os.Stderr, "Function \"%s\" not found.\n", functionName) } } func (manager *BuiltinFunctionManager) TryListBuiltinFunctionUsage( functionName string, - showApproximate bool, ) bool { builtinFunctionInfo := manager.LookUp(functionName) if builtinFunctionInfo == nil { - if showApproximate { - manager.listBuiltinFunctionUsageApproximate(functionName) - } return false } manager.listBuiltinFunctionUsageExact(builtinFunctionInfo) return true } +func (manager *BuiltinFunctionManager) TryListBuiltinFunctionUsageApproximate( + searchString string, +) bool { + found := false + for _, builtinFunctionInfo := range *manager.lookupTable { + if strings.Contains(builtinFunctionInfo.name, searchString) { + manager.showSingleUsage(&builtinFunctionInfo) + found = true + } + } + return found +} + func (manager *BuiltinFunctionManager) listBuiltinFunctionUsageExact( builtinFunctionInfo *BuiltinFunctionInfo, ) { @@ -1955,10 +1963,9 @@ func (manager *BuiltinFunctionManager) showSingleUsage( } } -func (manager *BuiltinFunctionManager) listBuiltinFunctionUsageApproximate( +func (manager *BuiltinFunctionManager) ListBuiltinFunctionUsageApproximate( text string, -) { - fmt.Printf("No exact match for \"%s\". Inexact matches:\n", text) +) bool { found := false for _, builtinFunctionInfo := range *manager.lookupTable { if strings.Contains(builtinFunctionInfo.name, text) { @@ -1966,9 +1973,7 @@ func (manager *BuiltinFunctionManager) listBuiltinFunctionUsageApproximate( found = true } } - if !found { - fmt.Println("None found.") - } + return found } func describeNargs(info *BuiltinFunctionInfo) string { diff --git a/internal/pkg/dsl/cst/keyword_usage.go b/internal/pkg/dsl/cst/keyword_usage.go index 807c8527d..7c80830ed 100644 --- a/internal/pkg/dsl/cst/keyword_usage.go +++ b/internal/pkg/dsl/cst/keyword_usage.go @@ -2,6 +2,7 @@ package cst import ( "fmt" + "strings" "github.com/johnkerl/miller/internal/pkg/colorizer" "github.com/johnkerl/miller/internal/pkg/lib" @@ -102,6 +103,17 @@ func TryUsageForKeyword(name string) bool { return false } +func TryUsageForKeywordApproximate(searchString string) bool { + for _, entry := range KEYWORD_USAGE_TABLE { + if strings.Contains(entry.name, searchString) { + fmt.Printf("%s: ", colorizer.MaybeColorizeHelp(entry.name, true)) + entry.usageFunc() + return true + } + } + return false +} + // ---------------------------------------------------------------- func ListKeywordsVertically() { for _, entry := range KEYWORD_USAGE_TABLE { diff --git a/internal/pkg/transformers/aaa_transformer_table.go b/internal/pkg/transformers/aaa_transformer_table.go index 3c2a0acaf..ed6c0a84d 100644 --- a/internal/pkg/transformers/aaa_transformer_table.go +++ b/internal/pkg/transformers/aaa_transformer_table.go @@ -3,6 +3,7 @@ package transformers import ( "fmt" "os" + "strings" "github.com/johnkerl/miller/internal/pkg/colorizer" "github.com/johnkerl/miller/internal/pkg/lib" @@ -71,7 +72,28 @@ var TRANSFORMER_LOOKUP_TABLE = []TransformerSetup{ UnsparsifySetup, } -// ---------------------------------------------------------------- +func ShowHelpForTransformer(verb string) bool { + transformerSetup := LookUp(verb) + if transformerSetup != nil { + fmt.Println(colorizer.MaybeColorizeHelp(transformerSetup.Verb, true)) + transformerSetup.UsageFunc(os.Stdout, false, 0) + return true + } + return false +} + +func ShowHelpForTransformerApproximate(searchString string) bool { + found := false + for _, transformerSetup := range TRANSFORMER_LOOKUP_TABLE { + if strings.Contains(transformerSetup.Verb, searchString) { + fmt.Println(colorizer.MaybeColorizeHelp(transformerSetup.Verb, true)) + transformerSetup.UsageFunc(os.Stdout, false, 0) + found = true + } + } + return found +} + func LookUp(verb string) *TransformerSetup { for _, transformerSetup := range TRANSFORMER_LOOKUP_TABLE { if transformerSetup.Verb == verb { @@ -81,14 +103,12 @@ func LookUp(verb string) *TransformerSetup { return nil } -// ---------------------------------------------------------------- func ListVerbNamesVertically() { for _, transformerSetup := range TRANSFORMER_LOOKUP_TABLE { fmt.Printf("%s\n", transformerSetup.Verb) } } -// ---------------------------------------------------------------- func ListVerbNamesAsParagraph() { verbNames := make([]string, len(TRANSFORMER_LOOKUP_TABLE)) diff --git a/todo.txt b/todo.txt index f6031e43f..339ddd7c9 100644 --- a/todo.txt +++ b/todo.txt @@ -1,7 +1,13 @@ ================================================================ PUNCHDOWN LIST -* conda issue +* help approx-match even if exact exists (e.g. 'map') + k red names x all , x mh & mrpl: just UT + k 'mlr help foo bar' should work -- just UT + k mrpl ':help foo bar' should work -- just UT + > be sure 'mlr help' and mrpl ':help' are both clear on discoverability of help-find + > approxes @ kw + * blockers: - keep checking issues - verslink old relnotes @@ -12,7 +18,7 @@ PUNCHDOWN LIST - csv irs lf/crlf ignores -- ? already is so? - `mlr put` -> coverart - 0b1011 olh/webdoc - - help approx-match even if exact exists (e.g. 'map') + * release ordering? conda brew macports chocolatey From bff6890a9ed926cc2c48e03ab032c25eec77e259 Mon Sep 17 00:00:00 2001 From: John Kerl Date: Wed, 17 Nov 2021 23:22:41 -0500 Subject: [PATCH 2/2] Unit tests for exact and approximate help --- docs/src/manpage.txt | 9 ++-- docs/src/online-help.md | 76 +++++++++++++++++++++------ docs/src/online-help.md.in | 10 +++- internal/pkg/auxents/help/entry.go | 14 ++--- internal/pkg/auxents/repl/verbs.go | 2 +- internal/pkg/dsl/cst/keyword_usage.go | 5 +- man/manpage.txt | 9 ++-- man/mlr.1 | 11 ++-- test/cases/help/0001/cmd | 1 + test/cases/help/0001/experr | 0 test/cases/help/0001/expout | 1 + test/cases/help/0002/cmd | 1 + test/cases/help/0002/experr | 0 test/cases/help/0002/expout | 2 + test/cases/help/0003/cmd | 1 + test/cases/help/0003/experr | 0 test/cases/help/0003/expout | 2 + test/cases/help/0004/cmd | 1 + test/cases/help/0004/experr | 0 test/cases/help/0004/expout | 3 ++ test/cases/help/0005/cmd | 1 + test/cases/help/0005/experr | 0 test/cases/help/0005/expout | 3 ++ test/cases/help/0006/cmd | 1 + test/cases/help/0006/experr | 0 test/cases/help/0006/expout | 4 ++ test/cases/help/0007/cmd | 1 + test/cases/help/0007/experr | 0 test/cases/help/0007/expout | 8 +++ test/cases/help/0008/cmd | 1 + test/cases/help/0008/experr | 0 test/cases/help/0008/expout | 2 + test/cases/help/0009/cmd | 1 + test/cases/help/0009/experr | 0 test/cases/help/0009/expout | 6 +++ test/cases/help/0010/cmd | 1 + test/cases/help/0010/experr | 0 test/cases/help/0010/expout | 3 ++ test/cases/help/0011/cmd | 1 + test/cases/help/0011/experr | 0 test/cases/help/0011/expout | 15 ++++++ test/cases/help/0012/cmd | 1 + test/cases/help/0012/experr | 0 test/cases/help/0012/expout | 45 ++++++++++++++++ test/cases/help/0013/cmd | 1 + test/cases/help/0013/experr | 0 test/cases/help/0013/expout | 2 + test/cases/help/0014/cmd | 1 + test/cases/help/0014/experr | 0 test/cases/help/0014/expout | 44 ++++++++++++++++ test/cases/help/0015/cmd | 1 + test/cases/help/0015/experr | 0 test/cases/help/0015/expout | 19 +++++++ test/cases/help/0016/cmd | 1 + test/cases/help/0016/experr | 0 test/cases/help/0016/expout | 30 +++++++++++ test/cases/help/0017/cmd | 1 + test/cases/help/0017/experr | 0 test/cases/help/0017/expout | 2 + test/cases/help/0018/cmd | 1 + test/cases/help/0018/experr | 0 test/cases/help/0018/expout | 6 +++ test/cases/repl-help/0001/cmd | 1 + test/cases/repl-help/0001/experr | 0 test/cases/repl-help/0001/expout | 1 + test/cases/repl-help/0001/input | 1 + test/cases/repl-help/0002/cmd | 1 + test/cases/repl-help/0002/experr | 0 test/cases/repl-help/0002/expout | 2 + test/cases/repl-help/0002/input | 1 + test/cases/repl-help/0003/cmd | 1 + test/cases/repl-help/0003/experr | 0 test/cases/repl-help/0003/expout | 1 + test/cases/repl-help/0003/input | 1 + test/cases/repl-help/0004/cmd | 1 + test/cases/repl-help/0004/experr | 0 test/cases/repl-help/0004/expout | 2 + test/cases/repl-help/0004/input | 1 + test/cases/repl-help/0005/cmd | 1 + test/cases/repl-help/0005/experr | 0 test/cases/repl-help/0005/expout | 2 + test/cases/repl-help/0005/input | 1 + test/cases/repl-help/0006/cmd | 1 + test/cases/repl-help/0006/experr | 0 test/cases/repl-help/0006/expout | 4 ++ test/cases/repl-help/0006/input | 1 + test/cases/repl-help/0007/cmd | 1 + test/cases/repl-help/0007/experr | 0 test/cases/repl-help/0007/expout | 8 +++ test/cases/repl-help/0007/input | 1 + test/cases/repl-help/0008/cmd | 1 + test/cases/repl-help/0008/experr | 0 test/cases/repl-help/0008/expout | 1 + test/cases/repl-help/0008/input | 1 + test/cases/repl-help/0009/cmd | 1 + test/cases/repl-help/0009/experr | 0 test/cases/repl-help/0009/expout | 5 ++ test/cases/repl-help/0009/input | 1 + test/cases/repl-help/0010/cmd | 1 + test/cases/repl-help/0010/experr | 0 test/cases/repl-help/0010/expout | 5 ++ test/cases/repl-help/0010/input | 1 + test/cases/repl-help/0011/cmd | 1 + test/cases/repl-help/0011/experr | 0 test/cases/repl-help/0011/expout | 15 ++++++ test/cases/repl-help/0011/input | 1 + test/cases/repl-help/0012/cmd | 1 + test/cases/repl-help/0012/experr | 0 test/cases/repl-help/0012/expout | 15 ++++++ test/cases/repl-help/0012/input | 1 + test/cases/repl-help/0013/cmd | 1 + test/cases/repl-help/0013/experr | 0 test/cases/repl-help/0013/expout | 1 + test/cases/repl-help/0013/input | 1 + test/cases/repl-help/0014/cmd | 1 + test/cases/repl-help/0014/experr | 0 test/cases/repl-help/0014/expout | 22 ++++++++ test/cases/repl-help/0014/input | 1 + test/cases/repl-help/0015/cmd | 1 + test/cases/repl-help/0015/experr | 0 test/cases/repl-help/0015/expout | 5 ++ test/cases/repl-help/0015/input | 1 + test/cases/repl-help/0016/cmd | 1 + test/cases/repl-help/0016/experr | 0 test/cases/repl-help/0016/expout | 8 +++ test/cases/repl-help/0016/input | 1 + todo.txt | 14 +---- 127 files changed, 438 insertions(+), 56 deletions(-) create mode 100644 test/cases/help/0001/cmd create mode 100644 test/cases/help/0001/experr create mode 100644 test/cases/help/0001/expout create mode 100644 test/cases/help/0002/cmd create mode 100644 test/cases/help/0002/experr create mode 100644 test/cases/help/0002/expout create mode 100644 test/cases/help/0003/cmd create mode 100644 test/cases/help/0003/experr create mode 100644 test/cases/help/0003/expout create mode 100644 test/cases/help/0004/cmd create mode 100644 test/cases/help/0004/experr create mode 100644 test/cases/help/0004/expout create mode 100644 test/cases/help/0005/cmd create mode 100644 test/cases/help/0005/experr create mode 100644 test/cases/help/0005/expout create mode 100644 test/cases/help/0006/cmd create mode 100644 test/cases/help/0006/experr create mode 100644 test/cases/help/0006/expout create mode 100644 test/cases/help/0007/cmd create mode 100644 test/cases/help/0007/experr create mode 100644 test/cases/help/0007/expout create mode 100644 test/cases/help/0008/cmd create mode 100644 test/cases/help/0008/experr create mode 100644 test/cases/help/0008/expout create mode 100644 test/cases/help/0009/cmd create mode 100644 test/cases/help/0009/experr create mode 100644 test/cases/help/0009/expout create mode 100644 test/cases/help/0010/cmd create mode 100644 test/cases/help/0010/experr create mode 100644 test/cases/help/0010/expout create mode 100644 test/cases/help/0011/cmd create mode 100644 test/cases/help/0011/experr create mode 100644 test/cases/help/0011/expout create mode 100644 test/cases/help/0012/cmd create mode 100644 test/cases/help/0012/experr create mode 100644 test/cases/help/0012/expout create mode 100644 test/cases/help/0013/cmd create mode 100644 test/cases/help/0013/experr create mode 100644 test/cases/help/0013/expout create mode 100644 test/cases/help/0014/cmd create mode 100644 test/cases/help/0014/experr create mode 100644 test/cases/help/0014/expout create mode 100644 test/cases/help/0015/cmd create mode 100644 test/cases/help/0015/experr create mode 100644 test/cases/help/0015/expout create mode 100644 test/cases/help/0016/cmd create mode 100644 test/cases/help/0016/experr create mode 100644 test/cases/help/0016/expout create mode 100644 test/cases/help/0017/cmd create mode 100644 test/cases/help/0017/experr create mode 100644 test/cases/help/0017/expout create mode 100644 test/cases/help/0018/cmd create mode 100644 test/cases/help/0018/experr create mode 100644 test/cases/help/0018/expout create mode 100644 test/cases/repl-help/0001/cmd create mode 100644 test/cases/repl-help/0001/experr create mode 100644 test/cases/repl-help/0001/expout create mode 100644 test/cases/repl-help/0001/input create mode 100644 test/cases/repl-help/0002/cmd create mode 100644 test/cases/repl-help/0002/experr create mode 100644 test/cases/repl-help/0002/expout create mode 100644 test/cases/repl-help/0002/input create mode 100644 test/cases/repl-help/0003/cmd create mode 100644 test/cases/repl-help/0003/experr create mode 100644 test/cases/repl-help/0003/expout create mode 100644 test/cases/repl-help/0003/input create mode 100644 test/cases/repl-help/0004/cmd create mode 100644 test/cases/repl-help/0004/experr create mode 100644 test/cases/repl-help/0004/expout create mode 100644 test/cases/repl-help/0004/input create mode 100644 test/cases/repl-help/0005/cmd create mode 100644 test/cases/repl-help/0005/experr create mode 100644 test/cases/repl-help/0005/expout create mode 100644 test/cases/repl-help/0005/input create mode 100644 test/cases/repl-help/0006/cmd create mode 100644 test/cases/repl-help/0006/experr create mode 100644 test/cases/repl-help/0006/expout create mode 100644 test/cases/repl-help/0006/input create mode 100644 test/cases/repl-help/0007/cmd create mode 100644 test/cases/repl-help/0007/experr create mode 100644 test/cases/repl-help/0007/expout create mode 100644 test/cases/repl-help/0007/input create mode 100644 test/cases/repl-help/0008/cmd create mode 100644 test/cases/repl-help/0008/experr create mode 100644 test/cases/repl-help/0008/expout create mode 100644 test/cases/repl-help/0008/input create mode 100644 test/cases/repl-help/0009/cmd create mode 100644 test/cases/repl-help/0009/experr create mode 100644 test/cases/repl-help/0009/expout create mode 100644 test/cases/repl-help/0009/input create mode 100644 test/cases/repl-help/0010/cmd create mode 100644 test/cases/repl-help/0010/experr create mode 100644 test/cases/repl-help/0010/expout create mode 100644 test/cases/repl-help/0010/input create mode 100644 test/cases/repl-help/0011/cmd create mode 100644 test/cases/repl-help/0011/experr create mode 100644 test/cases/repl-help/0011/expout create mode 100644 test/cases/repl-help/0011/input create mode 100644 test/cases/repl-help/0012/cmd create mode 100644 test/cases/repl-help/0012/experr create mode 100644 test/cases/repl-help/0012/expout create mode 100644 test/cases/repl-help/0012/input create mode 100644 test/cases/repl-help/0013/cmd create mode 100644 test/cases/repl-help/0013/experr create mode 100644 test/cases/repl-help/0013/expout create mode 100644 test/cases/repl-help/0013/input create mode 100644 test/cases/repl-help/0014/cmd create mode 100644 test/cases/repl-help/0014/experr create mode 100644 test/cases/repl-help/0014/expout create mode 100644 test/cases/repl-help/0014/input create mode 100644 test/cases/repl-help/0015/cmd create mode 100644 test/cases/repl-help/0015/experr create mode 100644 test/cases/repl-help/0015/expout create mode 100644 test/cases/repl-help/0015/input create mode 100644 test/cases/repl-help/0016/cmd create mode 100644 test/cases/repl-help/0016/experr create mode 100644 test/cases/repl-help/0016/expout create mode 100644 test/cases/repl-help/0016/input diff --git a/docs/src/manpage.txt b/docs/src/manpage.txt index 56201690b..08db5cd67 100644 --- a/docs/src/manpage.txt +++ b/docs/src/manpage.txt @@ -151,11 +151,10 @@ HELP OPTIONS mlr -F = mlr help usage-functions mlr -k = mlr help list-keywords mlr -K = mlr help usage-keywords - Lastly, 'mlr help ...' will search for your text '...' using the sources of + Lastly, 'mlr help ...' will search for your exact text '...' using the sources of 'mlr help flag', 'mlr help verb', 'mlr help function', and 'mlr help keyword'. - For things appearing in more than one place, e.g. 'sec2gmt' which is the name of a - verb as well as a function, use `mlr help verb sec2gmt' or `mlr help function sec2gmt' - to disambiguate. + Use 'mlr help find ...' for approximate (substring) matches, e.g. 'mlr help find map' + for all things with "map" in their names. VERB LIST altkv bar bootstrap cat check clean-whitespace count-distinct count @@ -2959,4 +2958,4 @@ SEE ALSO - 2021-11-15 MILLER(1) + 2021-11-18 MILLER(1) diff --git a/docs/src/online-help.md b/docs/src/online-help.md index cce2cdde2..4bea51ab4 100644 --- a/docs/src/online-help.md +++ b/docs/src/online-help.md @@ -90,18 +90,70 @@ Shorthands: mlr -F = mlr help usage-functions mlr -k = mlr help list-keywords mlr -K = mlr help usage-keywords -Lastly, 'mlr help ...' will search for your text '...' using the sources of +Lastly, 'mlr help ...' will search for your exact text '...' using the sources of 'mlr help flag', 'mlr help verb', 'mlr help function', and 'mlr help keyword'. -For things appearing in more than one place, e.g. 'sec2gmt' which is the name of a -verb as well as a function, use `mlr help verb sec2gmt' or `mlr help function sec2gmt' -to disambiguate. +Use 'mlr help find ...' for approximate (substring) matches, e.g. 'mlr help find map' +for all things with "map" in their names. +If you know the name of the thing you're looking for, use `mlr help`: +
-mlr help functions
+mlr help map
 
-No help found for "functions" -- please try 'mlr help topics'.
+map: declares an map-valued local variable in the current curly-braced scope.
+Type-checking happens at assignment: 'map b = 0' is an error. map b = {} is
+always OK. map b = a is OK or not depending on whether a is a map.
+
+ +To search by substring, use `mlr help find`: + +
+mlr help find gmt
+
+
+sec2gmtdate
+Usage: ../c/mlr sec2gmtdate {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT year-month-day timestamp; leaves non-numbers as-is.
+This is nothing more than a keystroke-saver for the sec2gmtdate function:
+  ../c/mlr sec2gmtdate time1,time2
+is the same as
+  ../c/mlr put '$time1=sec2gmtdate($time1);$time2=sec2gmtdate($time2)'
+sec2gmt
+Usage: mlr sec2gmt [options] {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT timestamp; leaves non-numbers as-is. This is nothing
+more than a keystroke-saver for the sec2gmt function:
+  mlr sec2gmt time1,time2
+is the same as
+  mlr put '$time1 = sec2gmt($time1); $time2 = sec2gmt($time2)'
+Options:
+-1 through -9: format the seconds using 1..9 decimal places, respectively.
+--millis Input numbers are treated as milliseconds since the epoch.
+--micros Input numbers are treated as microseconds since the epoch.
+--nanos  Input numbers are treated as nanoseconds since the epoch.
+-h|--help Show this message.
+gmt2localtime  (class=time #args=1,2) Convert from a GMT-time string to a local-time string. Consulting $TZ unless second argument is supplied.
+Examples:
+gmt2localtime("1999-12-31T22:00:00Z") = "2000-01-01 00:00:00" with TZ="Asia/Istanbul"
+gmt2localtime("1999-12-31T22:00:00Z", "Asia/Istanbul") = "2000-01-01 00:00:00"
+gmt2sec  (class=time #args=1) Parses GMT timestamp as integer seconds since the epoch.
+Example:
+gmt2sec("2001-02-03T04:05:06Z") = 981173106
+localtime2gmt  (class=time #args=1,2) Convert from a local-time string to a GMT-time string. Consults $TZ unless second argument is supplied.
+Examples:
+localtime2gmt("2000-01-01 00:00:00") = "1999-12-31T22:00:00Z" with TZ="Asia/Istanbul"
+localtime2gmt("2000-01-01 00:00:00", "Asia/Istanbul") = "1999-12-31T22:00:00Z"
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
+sec2gmtdate  (class=time #args=1) Formats seconds since epoch (integer part) as GMT timestamp with year-month-date. Leaves non-numbers as-is.
+Example:
+sec2gmtdate(1440768801.7) = "2015-08-28".
 
Etc. @@ -143,6 +195,7 @@ Options: mlr help verb sort
+sort
 Usage: mlr sort {flags}
 Sorts records primarily by the first specified field, secondarily by the second
 field, and so on.  (Any records not having all specified sort keys will appear
@@ -180,18 +233,9 @@ Given the name of a DSL function (from `mlr -f`) you can use `mlr help function`
 append  (class=collections #args=2) Appends second argument to end of first argument, which must be an array.
 
-
+
 mlr help function split
 
-
-No exact match for "split". Inexact matches:
-  splita
-  splitax
-  splitkv
-  splitkvx
-  splitnv
-  splitnvx
-
 mlr help function splita
diff --git a/docs/src/online-help.md.in b/docs/src/online-help.md.in
index e75ebf2dd..1b51c8ddb 100644
--- a/docs/src/online-help.md.in
+++ b/docs/src/online-help.md.in
@@ -14,8 +14,16 @@ GENMD-RUN-COMMAND
 mlr help topics
 GENMD-EOF
 
+If you know the name of the thing you're looking for, use `mlr help`:
+
 GENMD-RUN-COMMAND
-mlr help functions
+mlr help map
+GENMD-EOF
+
+To search by substring, use `mlr help find`:
+
+GENMD-RUN-COMMAND
+mlr help find gmt
 GENMD-EOF
 
 Etc.
diff --git a/internal/pkg/auxents/help/entry.go b/internal/pkg/auxents/help/entry.go
index 64418bad8..7e8c14597 100644
--- a/internal/pkg/auxents/help/entry.go
+++ b/internal/pkg/auxents/help/entry.go
@@ -236,9 +236,6 @@ func HelpMain(args []string) int {
 		return 0
 	}
 
-	// "mlr help something" where we do not recognize the something
-	fmt.Printf("No help found for \"%s\". Please try 'mlr help find %s' for approximate match.\n", name, name)
-	fmt.Printf("See also 'mlr help topics'.\n")
 	return 0
 }
 
@@ -302,11 +299,10 @@ func listTopics() {
 	for _, info := range shorthandLookupTable.shorthandInfos {
 		fmt.Printf("  mlr %s = mlr help %s\n", info.shorthand, info.longhand)
 	}
-	fmt.Printf("Lastly, 'mlr help ...' will search for your text '...' using the sources of\n")
+	fmt.Printf("Lastly, 'mlr help ...' will search for your exact text '...' using the sources of\n")
 	fmt.Printf("'mlr help flag', 'mlr help verb', 'mlr help function', and 'mlr help keyword'.\n")
-	fmt.Printf("For things appearing in more than one place, e.g. 'sec2gmt' which is the name of a\n")
-	fmt.Printf("verb as well as a function, use `mlr help verb sec2gmt' or `mlr help function sec2gmt'\n")
-	fmt.Printf("to disambiguate.\n")
+	fmt.Printf("Use 'mlr help find ...' for approximate (substring) matches, e.g. 'mlr help find map'\n")
+	fmt.Printf("for all things with \"map\" in their names.\n")
 }
 
 // ----------------------------------------------------------------
@@ -628,6 +624,10 @@ func helpByExactSearch(things []string) bool {
 	for _, thing := range things {
 		foundThisOne := helpByExactSearchOne(thing)
 		foundAny = foundAny || foundThisOne
+		if !foundThisOne {
+			fmt.Printf("No help found for \"%s\". Please try 'mlr help find %s' for approximate match.\n", thing, thing)
+			fmt.Printf("See also 'mlr help topics'.\n")
+		}
 	}
 
 	return foundAny
diff --git a/internal/pkg/auxents/repl/verbs.go b/internal/pkg/auxents/repl/verbs.go
index 551b6519b..e36ac24ac 100644
--- a/internal/pkg/auxents/repl/verbs.go
+++ b/internal/pkg/auxents/repl/verbs.go
@@ -811,7 +811,7 @@ func usageHelp(repl *Repl) {
 	fmt.Println(":help prompt")
 	fmt.Println(":help function-names")
 	fmt.Println(":help function-details")
-	fmt.Println(":help find {substring}, e.g. :help gmt or :help map")
+	fmt.Println(":help find {substring}, e.g. :help find gmt or :help find map")
 	fmt.Println(":help {function name}, e.g. :help sec2gmt")
 }
 
diff --git a/internal/pkg/dsl/cst/keyword_usage.go b/internal/pkg/dsl/cst/keyword_usage.go
index 7c80830ed..0972cbce8 100644
--- a/internal/pkg/dsl/cst/keyword_usage.go
+++ b/internal/pkg/dsl/cst/keyword_usage.go
@@ -104,14 +104,15 @@ func TryUsageForKeyword(name string) bool {
 }
 
 func TryUsageForKeywordApproximate(searchString string) bool {
+	foundAny := false
 	for _, entry := range KEYWORD_USAGE_TABLE {
 		if strings.Contains(entry.name, searchString) {
 			fmt.Printf("%s: ", colorizer.MaybeColorizeHelp(entry.name, true))
 			entry.usageFunc()
-			return true
+			foundAny = true
 		}
 	}
-	return false
+	return foundAny
 }
 
 // ----------------------------------------------------------------
diff --git a/man/manpage.txt b/man/manpage.txt
index 56201690b..08db5cd67 100644
--- a/man/manpage.txt
+++ b/man/manpage.txt
@@ -151,11 +151,10 @@ HELP OPTIONS
          mlr -F = mlr help usage-functions
          mlr -k = mlr help list-keywords
          mlr -K = mlr help usage-keywords
-       Lastly, 'mlr help ...' will search for your text '...' using the sources of
+       Lastly, 'mlr help ...' will search for your exact text '...' using the sources of
        'mlr help flag', 'mlr help verb', 'mlr help function', and 'mlr help keyword'.
-       For things appearing in more than one place, e.g. 'sec2gmt' which is the name of a
-       verb as well as a function, use `mlr help verb sec2gmt' or `mlr help function sec2gmt'
-       to disambiguate.
+       Use 'mlr help find ...' for approximate (substring) matches, e.g. 'mlr help find map'
+       for all things with "map" in their names.
 
 VERB LIST
        altkv bar bootstrap cat check clean-whitespace count-distinct count
@@ -2959,4 +2958,4 @@ SEE ALSO
 
 
 
-                                  2021-11-15                         MILLER(1)
+                                  2021-11-18                         MILLER(1)
diff --git a/man/mlr.1 b/man/mlr.1
index ca61710c5..315f86366 100644
--- a/man/mlr.1
+++ b/man/mlr.1
@@ -2,12 +2,12 @@
 .\"     Title: mlr
 .\"    Author: [see the "AUTHOR" section]
 .\" Generator: ./mkman.rb
-.\"      Date: 2021-11-15
+.\"      Date: 2021-11-18
 .\"    Manual: \ \&
 .\"    Source: \ \&
 .\"  Language: English
 .\"
-.TH "MILLER" "1" "2021-11-15" "\ \&" "\ \&"
+.TH "MILLER" "1" "2021-11-18" "\ \&" "\ \&"
 .\" -----------------------------------------------------------------
 .\" * Portability definitions
 .\" ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -186,11 +186,10 @@ Shorthands:
   mlr -F = mlr help usage-functions
   mlr -k = mlr help list-keywords
   mlr -K = mlr help usage-keywords
-Lastly, 'mlr help ...' will search for your text '...' using the sources of
+Lastly, 'mlr help ...' will search for your exact text '...' using the sources of
 \(cqmlr help flag', 'mlr help verb', 'mlr help function', and 'mlr help keyword'.
-For things appearing in more than one place, e.g. 'sec2gmt' which is the name of a
-verb as well as a function, use `mlr help verb sec2gmt' or `mlr help function sec2gmt'
-to disambiguate.
+Use 'mlr help find ...' for approximate (substring) matches, e.g. 'mlr help find map'
+for all things with "map" in their names.
 .fi
 .if n \{\
 .RE
diff --git a/test/cases/help/0001/cmd b/test/cases/help/0001/cmd
new file mode 100644
index 000000000..4c30096cb
--- /dev/null
+++ b/test/cases/help/0001/cmd
@@ -0,0 +1 @@
+mlr help cos
diff --git a/test/cases/help/0001/experr b/test/cases/help/0001/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0001/expout b/test/cases/help/0001/expout
new file mode 100644
index 000000000..59ee1e4fb
--- /dev/null
+++ b/test/cases/help/0001/expout
@@ -0,0 +1 @@
+cos  (class=math #args=1) Trigonometric cosine.
diff --git a/test/cases/help/0002/cmd b/test/cases/help/0002/cmd
new file mode 100644
index 000000000..02371dc8f
--- /dev/null
+++ b/test/cases/help/0002/cmd
@@ -0,0 +1 @@
+mlr help cos sin
diff --git a/test/cases/help/0002/experr b/test/cases/help/0002/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0002/expout b/test/cases/help/0002/expout
new file mode 100644
index 000000000..1529b7199
--- /dev/null
+++ b/test/cases/help/0002/expout
@@ -0,0 +1,2 @@
+cos  (class=math #args=1) Trigonometric cosine.
+sin  (class=math #args=1) Trigonometric sine.
diff --git a/test/cases/help/0003/cmd b/test/cases/help/0003/cmd
new file mode 100644
index 000000000..938fd5ac4
--- /dev/null
+++ b/test/cases/help/0003/cmd
@@ -0,0 +1 @@
+mlr help nonesuch
diff --git a/test/cases/help/0003/experr b/test/cases/help/0003/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0003/expout b/test/cases/help/0003/expout
new file mode 100644
index 000000000..b39493a58
--- /dev/null
+++ b/test/cases/help/0003/expout
@@ -0,0 +1,2 @@
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/help/0004/cmd b/test/cases/help/0004/cmd
new file mode 100644
index 000000000..8f4d5fef8
--- /dev/null
+++ b/test/cases/help/0004/cmd
@@ -0,0 +1 @@
+mlr help nonesuch sin
diff --git a/test/cases/help/0004/experr b/test/cases/help/0004/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0004/expout b/test/cases/help/0004/expout
new file mode 100644
index 000000000..2957a47dd
--- /dev/null
+++ b/test/cases/help/0004/expout
@@ -0,0 +1,3 @@
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
+sin  (class=math #args=1) Trigonometric sine.
diff --git a/test/cases/help/0005/cmd b/test/cases/help/0005/cmd
new file mode 100644
index 000000000..5dacd53eb
--- /dev/null
+++ b/test/cases/help/0005/cmd
@@ -0,0 +1 @@
+mlr help cos nonesuch
diff --git a/test/cases/help/0005/experr b/test/cases/help/0005/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0005/expout b/test/cases/help/0005/expout
new file mode 100644
index 000000000..706bbda4c
--- /dev/null
+++ b/test/cases/help/0005/expout
@@ -0,0 +1,3 @@
+cos  (class=math #args=1) Trigonometric cosine.
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/help/0006/cmd b/test/cases/help/0006/cmd
new file mode 100644
index 000000000..5ab70547a
--- /dev/null
+++ b/test/cases/help/0006/cmd
@@ -0,0 +1 @@
+mlr help find cos
diff --git a/test/cases/help/0006/experr b/test/cases/help/0006/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0006/expout b/test/cases/help/0006/expout
new file mode 100644
index 000000000..7e39509e9
--- /dev/null
+++ b/test/cases/help/0006/expout
@@ -0,0 +1,4 @@
+acos  (class=math #args=1) Inverse trigonometric cosine.
+acosh  (class=math #args=1) Inverse hyperbolic cosine.
+cos  (class=math #args=1) Trigonometric cosine.
+cosh  (class=math #args=1) Hyperbolic cosine.
diff --git a/test/cases/help/0007/cmd b/test/cases/help/0007/cmd
new file mode 100644
index 000000000..9f8308c45
--- /dev/null
+++ b/test/cases/help/0007/cmd
@@ -0,0 +1 @@
+mlr help find cos sin
diff --git a/test/cases/help/0007/experr b/test/cases/help/0007/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0007/expout b/test/cases/help/0007/expout
new file mode 100644
index 000000000..c6c0b22c8
--- /dev/null
+++ b/test/cases/help/0007/expout
@@ -0,0 +1,8 @@
+acos  (class=math #args=1) Inverse trigonometric cosine.
+acosh  (class=math #args=1) Inverse hyperbolic cosine.
+cos  (class=math #args=1) Trigonometric cosine.
+cosh  (class=math #args=1) Hyperbolic cosine.
+asin  (class=math #args=1) Inverse trigonometric sine.
+asinh  (class=math #args=1) Inverse hyperbolic sine.
+sin  (class=math #args=1) Trigonometric sine.
+sinh  (class=math #args=1) Hyperbolic sine.
diff --git a/test/cases/help/0008/cmd b/test/cases/help/0008/cmd
new file mode 100644
index 000000000..21289ba04
--- /dev/null
+++ b/test/cases/help/0008/cmd
@@ -0,0 +1 @@
+mlr help find nonesuch
diff --git a/test/cases/help/0008/experr b/test/cases/help/0008/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0008/expout b/test/cases/help/0008/expout
new file mode 100644
index 000000000..b39493a58
--- /dev/null
+++ b/test/cases/help/0008/expout
@@ -0,0 +1,2 @@
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/help/0009/cmd b/test/cases/help/0009/cmd
new file mode 100644
index 000000000..fd26d1f11
--- /dev/null
+++ b/test/cases/help/0009/cmd
@@ -0,0 +1 @@
+mlr help find nonesuch sin
diff --git a/test/cases/help/0009/experr b/test/cases/help/0009/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0009/expout b/test/cases/help/0009/expout
new file mode 100644
index 000000000..1e09eb9ec
--- /dev/null
+++ b/test/cases/help/0009/expout
@@ -0,0 +1,6 @@
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
+asin  (class=math #args=1) Inverse trigonometric sine.
+asinh  (class=math #args=1) Inverse hyperbolic sine.
+sin  (class=math #args=1) Trigonometric sine.
+sinh  (class=math #args=1) Hyperbolic sine.
diff --git a/test/cases/help/0010/cmd b/test/cases/help/0010/cmd
new file mode 100644
index 000000000..5dacd53eb
--- /dev/null
+++ b/test/cases/help/0010/cmd
@@ -0,0 +1 @@
+mlr help cos nonesuch
diff --git a/test/cases/help/0010/experr b/test/cases/help/0010/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0010/expout b/test/cases/help/0010/expout
new file mode 100644
index 000000000..706bbda4c
--- /dev/null
+++ b/test/cases/help/0010/expout
@@ -0,0 +1,3 @@
+cos  (class=math #args=1) Trigonometric cosine.
+No help found for "nonesuch". Please try 'mlr help find nonesuch' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/help/0011/cmd b/test/cases/help/0011/cmd
new file mode 100644
index 000000000..79e4e3cdc
--- /dev/null
+++ b/test/cases/help/0011/cmd
@@ -0,0 +1 @@
+mlr help for
diff --git a/test/cases/help/0011/experr b/test/cases/help/0011/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0011/expout b/test/cases/help/0011/expout
new file mode 100644
index 000000000..9f5c6b71f
--- /dev/null
+++ b/test/cases/help/0011/expout
@@ -0,0 +1,15 @@
+for: defines a for-loop using one of three styles. The body statements must
+be wrapped in curly braces.
+For-loop over stream record:
+
+  Example:  'for (k, v in $*) { ... }'
+
+For-loop over out-of-stream variables:
+
+  Example: 'for (k, v in @counts) { ... }'
+  Example: 'for ((k1, k2), v in @counts) { ... }'
+  Example: 'for ((k1, k2, k3), v in @*) { ... }'
+
+C-style for-loop:
+
+  Example:  'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }'
diff --git a/test/cases/help/0012/cmd b/test/cases/help/0012/cmd
new file mode 100644
index 000000000..814997851
--- /dev/null
+++ b/test/cases/help/0012/cmd
@@ -0,0 +1 @@
+mlr help find for
diff --git a/test/cases/help/0012/experr b/test/cases/help/0012/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0012/expout b/test/cases/help/0012/expout
new file mode 100644
index 000000000..245e2ab33
--- /dev/null
+++ b/test/cases/help/0012/expout
@@ -0,0 +1,45 @@
+format-values
+Usage: mlr format-values [options]
+Applies format strings to all field values, depending on autodetected type.
+* If a field value is detected to be integer, applies integer format.
+* Else, if a field value is detected to be float, applies float format.
+* Else, applies string format.
+
+Note: this is a low-keystroke way to apply formatting to many fields. To get
+finer control, please see the fmtnum function within the mlr put DSL.
+
+Note: this verb lets you apply arbitrary format strings, which can produce
+undefined behavior and/or program crashes.  See your system's "man printf".
+
+Options:
+-i {integer format} Defaults to "%d".
+                    Examples: "%06lld", "%08llx".
+                    Note that Miller integers are long long so you must use
+                    formats which apply to long long, e.g. with ll in them.
+                    Undefined behavior results otherwise.
+-f {float format}   Defaults to "%f".
+                    Examples: "%8.3lf", "%.6le".
+                    Note that Miller floats are double-precision so you must
+                    use formats which apply to double, e.g. with l[efg] in them.
+                    Undefined behavior results otherwise.
+-s {string format}  Defaults to "%s".
+                    Examples: "_%s", "%08s".
+                    Note that you must use formats which apply to string, e.g.
+                    with s in them. Undefined behavior results otherwise.
+-n                  Coerce field values autodetected as int to float, and then
+                    apply the float format.
+for: defines a for-loop using one of three styles. The body statements must
+be wrapped in curly braces.
+For-loop over stream record:
+
+  Example:  'for (k, v in $*) { ... }'
+
+For-loop over out-of-stream variables:
+
+  Example: 'for (k, v in @counts) { ... }'
+  Example: 'for ((k1, k2), v in @counts) { ... }'
+  Example: 'for ((k1, k2, k3), v in @*) { ... }'
+
+C-style for-loop:
+
+  Example:  'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }'
diff --git a/test/cases/help/0013/cmd b/test/cases/help/0013/cmd
new file mode 100644
index 000000000..b3cc86b0b
--- /dev/null
+++ b/test/cases/help/0013/cmd
@@ -0,0 +1 @@
+mlr help sec2
diff --git a/test/cases/help/0013/experr b/test/cases/help/0013/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0013/expout b/test/cases/help/0013/expout
new file mode 100644
index 000000000..39d075259
--- /dev/null
+++ b/test/cases/help/0013/expout
@@ -0,0 +1,2 @@
+No help found for "sec2". Please try 'mlr help find sec2' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/help/0014/cmd b/test/cases/help/0014/cmd
new file mode 100644
index 000000000..990a00af1
--- /dev/null
+++ b/test/cases/help/0014/cmd
@@ -0,0 +1 @@
+mlr help find sec2
diff --git a/test/cases/help/0014/experr b/test/cases/help/0014/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0014/expout b/test/cases/help/0014/expout
new file mode 100644
index 000000000..97977a3db
--- /dev/null
+++ b/test/cases/help/0014/expout
@@ -0,0 +1,44 @@
+sec2gmtdate
+Usage: ../c/mlr sec2gmtdate {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT year-month-day timestamp; leaves non-numbers as-is.
+This is nothing more than a keystroke-saver for the sec2gmtdate function:
+  ../c/mlr sec2gmtdate time1,time2
+is the same as
+  ../c/mlr put '$time1=sec2gmtdate($time1);$time2=sec2gmtdate($time2)'
+sec2gmt
+Usage: mlr sec2gmt [options] {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT timestamp; leaves non-numbers as-is. This is nothing
+more than a keystroke-saver for the sec2gmt function:
+  mlr sec2gmt time1,time2
+is the same as
+  mlr put '$time1 = sec2gmt($time1); $time2 = sec2gmt($time2)'
+Options:
+-1 through -9: format the seconds using 1..9 decimal places, respectively.
+--millis Input numbers are treated as milliseconds since the epoch.
+--micros Input numbers are treated as microseconds since the epoch.
+--nanos  Input numbers are treated as nanoseconds since the epoch.
+-h|--help Show this message.
+fsec2dhms  (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s"
+fsec2hms  (class=time #args=1) Formats floating-point seconds as in fsec2hms(5000.25) = "01:23:20.250000"
+sec2dhms  (class=time #args=1) Formats integer seconds as in sec2dhms(500000) = "5d18h53m20s"
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
+sec2gmtdate  (class=time #args=1) Formats seconds since epoch (integer part) as GMT timestamp with year-month-date. Leaves non-numbers as-is.
+Example:
+sec2gmtdate(1440768801.7) = "2015-08-28".
+sec2hms  (class=time #args=1) Formats integer seconds as in sec2hms(5000) = "01:23:20"
+sec2localdate  (class=time #args=1,2) Formats seconds since epoch (integer part) as local timestamp with year-month-date. Leaves non-numbers as-is. Consults $TZ environment variable unless second argument is supplied.
+Examples:
+sec2localdate(1440768801.7) = "2015-08-28" with TZ="Asia/Istanbul"
+sec2localdate(1440768801.7, "Asia/Istanbul") = "2015-08-28"
+sec2localtime  (class=time #args=1,2,3) Formats seconds since epoch (integer part) as local timestamp. Consults $TZ environment variable unless third argument is supplied. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part
+Examples:
+sec2localtime(1234567890)           = "2009-02-14 01:31:30"        with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456)    = "2009-02-14 01:31:30"        with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456, 6) = "2009-02-14 01:31:30.123456" with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456, 6, "Asia/Istanbul") = "2009-02-14 01:31:30.123456"
diff --git a/test/cases/help/0015/cmd b/test/cases/help/0015/cmd
new file mode 100644
index 000000000..dd88ca8b5
--- /dev/null
+++ b/test/cases/help/0015/cmd
@@ -0,0 +1 @@
+mlr help sec2gmt
diff --git a/test/cases/help/0015/experr b/test/cases/help/0015/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0015/expout b/test/cases/help/0015/expout
new file mode 100644
index 000000000..cf4bafab7
--- /dev/null
+++ b/test/cases/help/0015/expout
@@ -0,0 +1,19 @@
+sec2gmt
+Usage: mlr sec2gmt [options] {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT timestamp; leaves non-numbers as-is. This is nothing
+more than a keystroke-saver for the sec2gmt function:
+  mlr sec2gmt time1,time2
+is the same as
+  mlr put '$time1 = sec2gmt($time1); $time2 = sec2gmt($time2)'
+Options:
+-1 through -9: format the seconds using 1..9 decimal places, respectively.
+--millis Input numbers are treated as milliseconds since the epoch.
+--micros Input numbers are treated as microseconds since the epoch.
+--nanos  Input numbers are treated as nanoseconds since the epoch.
+-h|--help Show this message.
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
diff --git a/test/cases/help/0016/cmd b/test/cases/help/0016/cmd
new file mode 100644
index 000000000..2fa6ad66c
--- /dev/null
+++ b/test/cases/help/0016/cmd
@@ -0,0 +1 @@
+mlr help find sec2gmt
diff --git a/test/cases/help/0016/experr b/test/cases/help/0016/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0016/expout b/test/cases/help/0016/expout
new file mode 100644
index 000000000..87aa76721
--- /dev/null
+++ b/test/cases/help/0016/expout
@@ -0,0 +1,30 @@
+sec2gmtdate
+Usage: ../c/mlr sec2gmtdate {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT year-month-day timestamp; leaves non-numbers as-is.
+This is nothing more than a keystroke-saver for the sec2gmtdate function:
+  ../c/mlr sec2gmtdate time1,time2
+is the same as
+  ../c/mlr put '$time1=sec2gmtdate($time1);$time2=sec2gmtdate($time2)'
+sec2gmt
+Usage: mlr sec2gmt [options] {comma-separated list of field names}
+Replaces a numeric field representing seconds since the epoch with the
+corresponding GMT timestamp; leaves non-numbers as-is. This is nothing
+more than a keystroke-saver for the sec2gmt function:
+  mlr sec2gmt time1,time2
+is the same as
+  mlr put '$time1 = sec2gmt($time1); $time2 = sec2gmt($time2)'
+Options:
+-1 through -9: format the seconds using 1..9 decimal places, respectively.
+--millis Input numbers are treated as milliseconds since the epoch.
+--micros Input numbers are treated as microseconds since the epoch.
+--nanos  Input numbers are treated as nanoseconds since the epoch.
+-h|--help Show this message.
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
+sec2gmtdate  (class=time #args=1) Formats seconds since epoch (integer part) as GMT timestamp with year-month-date. Leaves non-numbers as-is.
+Example:
+sec2gmtdate(1440768801.7) = "2015-08-28".
diff --git a/test/cases/help/0017/cmd b/test/cases/help/0017/cmd
new file mode 100644
index 000000000..a525d1fb4
--- /dev/null
+++ b/test/cases/help/0017/cmd
@@ -0,0 +1 @@
+mlr help --csv
diff --git a/test/cases/help/0017/experr b/test/cases/help/0017/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0017/expout b/test/cases/help/0017/expout
new file mode 100644
index 000000000..9c3ab866f
--- /dev/null
+++ b/test/cases/help/0017/expout
@@ -0,0 +1,2 @@
+--csv
+Use CSV format for input and output data.
diff --git a/test/cases/help/0018/cmd b/test/cases/help/0018/cmd
new file mode 100644
index 000000000..0da8741c9
--- /dev/null
+++ b/test/cases/help/0018/cmd
@@ -0,0 +1 @@
+mlr help find --csv
diff --git a/test/cases/help/0018/experr b/test/cases/help/0018/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/help/0018/expout b/test/cases/help/0018/expout
new file mode 100644
index 000000000..560401525
--- /dev/null
+++ b/test/cases/help/0018/expout
@@ -0,0 +1,6 @@
+--csv
+Use CSV format for input and output data.
+--csvlite
+Use CSV-lite format for input and output data.
+No help found for "--csv". Please try 'mlr help find --csv' for approximate match.
+See also 'mlr help topics'.
diff --git a/test/cases/repl-help/0001/cmd b/test/cases/repl-help/0001/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0001/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0001/experr b/test/cases/repl-help/0001/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0001/expout b/test/cases/repl-help/0001/expout
new file mode 100644
index 000000000..59ee1e4fb
--- /dev/null
+++ b/test/cases/repl-help/0001/expout
@@ -0,0 +1 @@
+cos  (class=math #args=1) Trigonometric cosine.
diff --git a/test/cases/repl-help/0001/input b/test/cases/repl-help/0001/input
new file mode 100644
index 000000000..11118bb2e
--- /dev/null
+++ b/test/cases/repl-help/0001/input
@@ -0,0 +1 @@
+:help cos
diff --git a/test/cases/repl-help/0002/cmd b/test/cases/repl-help/0002/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0002/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0002/experr b/test/cases/repl-help/0002/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0002/expout b/test/cases/repl-help/0002/expout
new file mode 100644
index 000000000..1529b7199
--- /dev/null
+++ b/test/cases/repl-help/0002/expout
@@ -0,0 +1,2 @@
+cos  (class=math #args=1) Trigonometric cosine.
+sin  (class=math #args=1) Trigonometric sine.
diff --git a/test/cases/repl-help/0002/input b/test/cases/repl-help/0002/input
new file mode 100644
index 000000000..fe91e6745
--- /dev/null
+++ b/test/cases/repl-help/0002/input
@@ -0,0 +1 @@
+:help cos sin
diff --git a/test/cases/repl-help/0003/cmd b/test/cases/repl-help/0003/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0003/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0003/experr b/test/cases/repl-help/0003/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0003/expout b/test/cases/repl-help/0003/expout
new file mode 100644
index 000000000..6c499ebae
--- /dev/null
+++ b/test/cases/repl-help/0003/expout
@@ -0,0 +1 @@
+No help available for nonesuch
diff --git a/test/cases/repl-help/0003/input b/test/cases/repl-help/0003/input
new file mode 100644
index 000000000..07b3f318d
--- /dev/null
+++ b/test/cases/repl-help/0003/input
@@ -0,0 +1 @@
+:help nonesuch
diff --git a/test/cases/repl-help/0004/cmd b/test/cases/repl-help/0004/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0004/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0004/experr b/test/cases/repl-help/0004/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0004/expout b/test/cases/repl-help/0004/expout
new file mode 100644
index 000000000..3519be25c
--- /dev/null
+++ b/test/cases/repl-help/0004/expout
@@ -0,0 +1,2 @@
+No help available for nonesuch
+sin  (class=math #args=1) Trigonometric sine.
diff --git a/test/cases/repl-help/0004/input b/test/cases/repl-help/0004/input
new file mode 100644
index 000000000..7bd372580
--- /dev/null
+++ b/test/cases/repl-help/0004/input
@@ -0,0 +1 @@
+:help nonesuch sin
diff --git a/test/cases/repl-help/0005/cmd b/test/cases/repl-help/0005/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0005/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0005/experr b/test/cases/repl-help/0005/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0005/expout b/test/cases/repl-help/0005/expout
new file mode 100644
index 000000000..8369d3d44
--- /dev/null
+++ b/test/cases/repl-help/0005/expout
@@ -0,0 +1,2 @@
+cos  (class=math #args=1) Trigonometric cosine.
+No help available for nonesuch
diff --git a/test/cases/repl-help/0005/input b/test/cases/repl-help/0005/input
new file mode 100644
index 000000000..44f267d0b
--- /dev/null
+++ b/test/cases/repl-help/0005/input
@@ -0,0 +1 @@
+:help cos nonesuch
diff --git a/test/cases/repl-help/0006/cmd b/test/cases/repl-help/0006/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0006/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0006/experr b/test/cases/repl-help/0006/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0006/expout b/test/cases/repl-help/0006/expout
new file mode 100644
index 000000000..7e39509e9
--- /dev/null
+++ b/test/cases/repl-help/0006/expout
@@ -0,0 +1,4 @@
+acos  (class=math #args=1) Inverse trigonometric cosine.
+acosh  (class=math #args=1) Inverse hyperbolic cosine.
+cos  (class=math #args=1) Trigonometric cosine.
+cosh  (class=math #args=1) Hyperbolic cosine.
diff --git a/test/cases/repl-help/0006/input b/test/cases/repl-help/0006/input
new file mode 100644
index 000000000..40e82b097
--- /dev/null
+++ b/test/cases/repl-help/0006/input
@@ -0,0 +1 @@
+:help find cos
diff --git a/test/cases/repl-help/0007/cmd b/test/cases/repl-help/0007/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0007/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0007/experr b/test/cases/repl-help/0007/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0007/expout b/test/cases/repl-help/0007/expout
new file mode 100644
index 000000000..c6c0b22c8
--- /dev/null
+++ b/test/cases/repl-help/0007/expout
@@ -0,0 +1,8 @@
+acos  (class=math #args=1) Inverse trigonometric cosine.
+acosh  (class=math #args=1) Inverse hyperbolic cosine.
+cos  (class=math #args=1) Trigonometric cosine.
+cosh  (class=math #args=1) Hyperbolic cosine.
+asin  (class=math #args=1) Inverse trigonometric sine.
+asinh  (class=math #args=1) Inverse hyperbolic sine.
+sin  (class=math #args=1) Trigonometric sine.
+sinh  (class=math #args=1) Hyperbolic sine.
diff --git a/test/cases/repl-help/0007/input b/test/cases/repl-help/0007/input
new file mode 100644
index 000000000..1bc0dae93
--- /dev/null
+++ b/test/cases/repl-help/0007/input
@@ -0,0 +1 @@
+:help find cos sin
diff --git a/test/cases/repl-help/0008/cmd b/test/cases/repl-help/0008/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0008/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0008/experr b/test/cases/repl-help/0008/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0008/expout b/test/cases/repl-help/0008/expout
new file mode 100644
index 000000000..61bce8170
--- /dev/null
+++ b/test/cases/repl-help/0008/expout
@@ -0,0 +1 @@
+No help available for nonesuch. Try ":help find nonesuch" to search for matches
diff --git a/test/cases/repl-help/0008/input b/test/cases/repl-help/0008/input
new file mode 100644
index 000000000..6758fb508
--- /dev/null
+++ b/test/cases/repl-help/0008/input
@@ -0,0 +1 @@
+:help find nonesuch
diff --git a/test/cases/repl-help/0009/cmd b/test/cases/repl-help/0009/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0009/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0009/experr b/test/cases/repl-help/0009/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0009/expout b/test/cases/repl-help/0009/expout
new file mode 100644
index 000000000..1a1a7ecd6
--- /dev/null
+++ b/test/cases/repl-help/0009/expout
@@ -0,0 +1,5 @@
+No help available for nonesuch. Try ":help find nonesuch" to search for matches
+asin  (class=math #args=1) Inverse trigonometric sine.
+asinh  (class=math #args=1) Inverse hyperbolic sine.
+sin  (class=math #args=1) Trigonometric sine.
+sinh  (class=math #args=1) Hyperbolic sine.
diff --git a/test/cases/repl-help/0009/input b/test/cases/repl-help/0009/input
new file mode 100644
index 000000000..2c1ceade8
--- /dev/null
+++ b/test/cases/repl-help/0009/input
@@ -0,0 +1 @@
+:help find nonesuch sin
diff --git a/test/cases/repl-help/0010/cmd b/test/cases/repl-help/0010/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0010/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0010/experr b/test/cases/repl-help/0010/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0010/expout b/test/cases/repl-help/0010/expout
new file mode 100644
index 000000000..cf7c24ab1
--- /dev/null
+++ b/test/cases/repl-help/0010/expout
@@ -0,0 +1,5 @@
+acos  (class=math #args=1) Inverse trigonometric cosine.
+acosh  (class=math #args=1) Inverse hyperbolic cosine.
+cos  (class=math #args=1) Trigonometric cosine.
+cosh  (class=math #args=1) Hyperbolic cosine.
+No help available for nonesuch. Try ":help find nonesuch" to search for matches
diff --git a/test/cases/repl-help/0010/input b/test/cases/repl-help/0010/input
new file mode 100644
index 000000000..4735fd0e8
--- /dev/null
+++ b/test/cases/repl-help/0010/input
@@ -0,0 +1 @@
+:help find cos nonesuch
diff --git a/test/cases/repl-help/0011/cmd b/test/cases/repl-help/0011/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0011/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0011/experr b/test/cases/repl-help/0011/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0011/expout b/test/cases/repl-help/0011/expout
new file mode 100644
index 000000000..9f5c6b71f
--- /dev/null
+++ b/test/cases/repl-help/0011/expout
@@ -0,0 +1,15 @@
+for: defines a for-loop using one of three styles. The body statements must
+be wrapped in curly braces.
+For-loop over stream record:
+
+  Example:  'for (k, v in $*) { ... }'
+
+For-loop over out-of-stream variables:
+
+  Example: 'for (k, v in @counts) { ... }'
+  Example: 'for ((k1, k2), v in @counts) { ... }'
+  Example: 'for ((k1, k2, k3), v in @*) { ... }'
+
+C-style for-loop:
+
+  Example:  'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }'
diff --git a/test/cases/repl-help/0011/input b/test/cases/repl-help/0011/input
new file mode 100644
index 000000000..3b352ca73
--- /dev/null
+++ b/test/cases/repl-help/0011/input
@@ -0,0 +1 @@
+:help for
diff --git a/test/cases/repl-help/0012/cmd b/test/cases/repl-help/0012/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0012/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0012/experr b/test/cases/repl-help/0012/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0012/expout b/test/cases/repl-help/0012/expout
new file mode 100644
index 000000000..9f5c6b71f
--- /dev/null
+++ b/test/cases/repl-help/0012/expout
@@ -0,0 +1,15 @@
+for: defines a for-loop using one of three styles. The body statements must
+be wrapped in curly braces.
+For-loop over stream record:
+
+  Example:  'for (k, v in $*) { ... }'
+
+For-loop over out-of-stream variables:
+
+  Example: 'for (k, v in @counts) { ... }'
+  Example: 'for ((k1, k2), v in @counts) { ... }'
+  Example: 'for ((k1, k2, k3), v in @*) { ... }'
+
+C-style for-loop:
+
+  Example:  'for (var i = 0, var b = 1; i < 10; i += 1, b *= 2) { ... }'
diff --git a/test/cases/repl-help/0012/input b/test/cases/repl-help/0012/input
new file mode 100644
index 000000000..57c25123e
--- /dev/null
+++ b/test/cases/repl-help/0012/input
@@ -0,0 +1 @@
+:help find for
diff --git a/test/cases/repl-help/0013/cmd b/test/cases/repl-help/0013/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0013/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0013/experr b/test/cases/repl-help/0013/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0013/expout b/test/cases/repl-help/0013/expout
new file mode 100644
index 000000000..4f922d64b
--- /dev/null
+++ b/test/cases/repl-help/0013/expout
@@ -0,0 +1 @@
+No help available for sec2
diff --git a/test/cases/repl-help/0013/input b/test/cases/repl-help/0013/input
new file mode 100644
index 000000000..94ea514c3
--- /dev/null
+++ b/test/cases/repl-help/0013/input
@@ -0,0 +1 @@
+:help sec2
diff --git a/test/cases/repl-help/0014/cmd b/test/cases/repl-help/0014/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0014/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0014/experr b/test/cases/repl-help/0014/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0014/expout b/test/cases/repl-help/0014/expout
new file mode 100644
index 000000000..a710a4f26
--- /dev/null
+++ b/test/cases/repl-help/0014/expout
@@ -0,0 +1,22 @@
+fsec2dhms  (class=time #args=1) Formats floating-point seconds as in fsec2dhms(500000.25) = "5d18h53m20.250000s"
+fsec2hms  (class=time #args=1) Formats floating-point seconds as in fsec2hms(5000.25) = "01:23:20.250000"
+sec2dhms  (class=time #args=1) Formats integer seconds as in sec2dhms(500000) = "5d18h53m20s"
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
+sec2gmtdate  (class=time #args=1) Formats seconds since epoch (integer part) as GMT timestamp with year-month-date. Leaves non-numbers as-is.
+Example:
+sec2gmtdate(1440768801.7) = "2015-08-28".
+sec2hms  (class=time #args=1) Formats integer seconds as in sec2hms(5000) = "01:23:20"
+sec2localdate  (class=time #args=1,2) Formats seconds since epoch (integer part) as local timestamp with year-month-date. Leaves non-numbers as-is. Consults $TZ environment variable unless second argument is supplied.
+Examples:
+sec2localdate(1440768801.7) = "2015-08-28" with TZ="Asia/Istanbul"
+sec2localdate(1440768801.7, "Asia/Istanbul") = "2015-08-28"
+sec2localtime  (class=time #args=1,2,3) Formats seconds since epoch (integer part) as local timestamp. Consults $TZ environment variable unless third argument is supplied. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part
+Examples:
+sec2localtime(1234567890)           = "2009-02-14 01:31:30"        with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456)    = "2009-02-14 01:31:30"        with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456, 6) = "2009-02-14 01:31:30.123456" with TZ="Asia/Istanbul"
+sec2localtime(1234567890.123456, 6, "Asia/Istanbul") = "2009-02-14 01:31:30.123456"
diff --git a/test/cases/repl-help/0014/input b/test/cases/repl-help/0014/input
new file mode 100644
index 000000000..dee6765ad
--- /dev/null
+++ b/test/cases/repl-help/0014/input
@@ -0,0 +1 @@
+:help find sec2
diff --git a/test/cases/repl-help/0015/cmd b/test/cases/repl-help/0015/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0015/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0015/experr b/test/cases/repl-help/0015/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0015/expout b/test/cases/repl-help/0015/expout
new file mode 100644
index 000000000..e0b453374
--- /dev/null
+++ b/test/cases/repl-help/0015/expout
@@ -0,0 +1,5 @@
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
diff --git a/test/cases/repl-help/0015/input b/test/cases/repl-help/0015/input
new file mode 100644
index 000000000..c1b7fa9de
--- /dev/null
+++ b/test/cases/repl-help/0015/input
@@ -0,0 +1 @@
+:help sec2gmt
diff --git a/test/cases/repl-help/0016/cmd b/test/cases/repl-help/0016/cmd
new file mode 100644
index 000000000..f17dba06d
--- /dev/null
+++ b/test/cases/repl-help/0016/cmd
@@ -0,0 +1 @@
+mlr repl < ./${CASEDIR}/input
diff --git a/test/cases/repl-help/0016/experr b/test/cases/repl-help/0016/experr
new file mode 100644
index 000000000..e69de29bb
diff --git a/test/cases/repl-help/0016/expout b/test/cases/repl-help/0016/expout
new file mode 100644
index 000000000..ad1e26c87
--- /dev/null
+++ b/test/cases/repl-help/0016/expout
@@ -0,0 +1,8 @@
+sec2gmt  (class=time #args=1,2) Formats seconds since epoch as GMT timestamp. Leaves non-numbers as-is. With second integer argument n, includes n decimal places for the seconds part.
+Examples:
+sec2gmt(1234567890)           = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456)    = "2009-02-13T23:31:30Z"
+sec2gmt(1234567890.123456, 6) = "2009-02-13T23:31:30.123456Z"
+sec2gmtdate  (class=time #args=1) Formats seconds since epoch (integer part) as GMT timestamp with year-month-date. Leaves non-numbers as-is.
+Example:
+sec2gmtdate(1440768801.7) = "2015-08-28".
diff --git a/test/cases/repl-help/0016/input b/test/cases/repl-help/0016/input
new file mode 100644
index 000000000..ce7f831e8
--- /dev/null
+++ b/test/cases/repl-help/0016/input
@@ -0,0 +1 @@
+:help find sec2gmt
diff --git a/todo.txt b/todo.txt
index 339ddd7c9..bb42991e3 100644
--- a/todo.txt
+++ b/todo.txt
@@ -1,13 +1,6 @@
 ================================================================
 PUNCHDOWN LIST
 
-* help approx-match even if exact exists (e.g. 'map')
-  k red names x all , x mh & mrpl: just UT
-  k 'mlr help foo bar' should work -- just UT
-  k mrpl ':help foo bar' should work -- just UT
-  > be sure 'mlr help' and mrpl ':help' are both clear on discoverability of help-find
-  > approxes @ kw
-
 * blockers:
   - keep checking issues
   - verslink old relnotes
@@ -52,12 +45,7 @@ PUNCHDOWN LIST
       strptime("1970-01-01T00:00:00.Z", "%Y-%m-%dT%H:%M:%SZ")
       (error)
 
-* olh
-  o kw olh for func and funct both
-  o better help-searching:
-    ? mlr help search foo -- including flag-search
-    ? mlr -f foo
-    ? mlr -F foo == mlr help function foo
+* kw olh for func and funct both
 
 * pre-release:
   o check issues