From bf841d00967c3047bff88dbe070cb56319e1fe38 Mon Sep 17 00:00:00 2001 From: JP Appel Date: Fri, 13 Jun 2025 00:22:17 -0400 Subject: Refactor commandline flag and argument parsing --- cmd/atlas.go | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 167 insertions(+), 27 deletions(-) (limited to 'cmd/atlas.go') diff --git a/cmd/atlas.go b/cmd/atlas.go index cbc21d4..a6d161a 100644 --- a/cmd/atlas.go +++ b/cmd/atlas.go @@ -1,62 +1,202 @@ package main import ( + "context" "flag" "fmt" "log/slog" "os" + "runtime" + "strings" + "time" "github.com/jpappel/atlas/pkg/data" "github.com/jpappel/atlas/pkg/index" + "github.com/jpappel/atlas/pkg/query" ) -const ExitCommand = 2 // exit because of a command parsing error +const ExitCommand = 2 // exit because of a command parsing error +const dateFormat = time.RFC3339 // TODO: make a flag -var commands = []string{"query", "index", "version", "help"} +type GlobalFlags struct { + IndexRoot string + DBPath string + LogLevel string + LogJson bool + NumWorkers uint +} + +func addGlobalFlagUsage(fs *flag.FlagSet) func() { + return func() { + f := fs.Output() + fmt.Fprintln(f, "Usage of", fs.Name()) + fs.PrintDefaults() + fmt.Fprintln(f, "\nGlobal Flags:") + flag.PrintDefaults() + } +} + +func printHelp() { + fmt.Println("atlas is a note indexing and querying tool") + fmt.Printf("\nUsage:\n %s \n\n", os.Args[0]) + fmt.Println("Commands:") + fmt.Println(" index - build, update, or modify the index") + fmt.Println(" query - search against the index") + fmt.Println(" help - print this help then exit") +} func main() { - // global opts - indexRoot := flag.String("root", "/home/goose/src/atlas/test", "root directory for indexing") - docDB := flag.String("db", "/home/goose/src/atlas/test.db", "path to document database") - logLevel := flag.String("logLevel", "error", "set log level (debug, info, warn, error)") + globalFlags := GlobalFlags{} + flag.StringVar(&globalFlags.IndexRoot, "root", "/home/goose/src/atlas/test", "root `directory` for indexing") + flag.StringVar(&globalFlags.DBPath, "db", "/home/goose/src/atlas/test.db", "`path` to document database") + flag.StringVar(&globalFlags.LogLevel, "logLevel", "error", "set log `level` (debug, info, warn, error)") + flag.BoolVar(&globalFlags.LogJson, "logJson", false, "log to json") + flag.UintVar(&globalFlags.NumWorkers, "numWorkers", uint(runtime.NumCPU()), "number of worker threads to use (defaults to core count)") - // command specific opts - // TODO: parse a list of fitlers - docFilters := index.DefaultFilters() + indexFs := flag.NewFlagSet("index flags", flag.ExitOnError) + queryFs := flag.NewFlagSet("query flags", flag.ExitOnError) + + indexFs.Usage = addGlobalFlagUsage(indexFs) + queryFs.Usage = addGlobalFlagUsage(queryFs) flag.Parse() + args := flag.Args() + + queryFlags := struct { + Output query.Outputer + CustomFormat string + }{} + indexFlags := struct { + ignoreBadDates bool + Filters []index.DocFilter + }{} + + if len(args) < 2 { + fmt.Fprintln(os.Stderr, "No Command provided") + printHelp() + fmt.Fprintln(flag.CommandLine.Output(), "\nGlobal Flags:") + flag.PrintDefaults() + os.Exit(ExitCommand) + } + command := args[0] + + switch command { + case "query": + // NOTE: providing `-outFormat` before `-outCustomFormat` might ignore user specified format + queryFs.Func("outFormat", "output `format` for queries (default, json, custom)", + func(arg string) error { + if arg == "default" { + queryFlags.Output = query.DefaultOutput{} + return nil + } else if arg == "json" { + queryFlags.Output = query.JsonOutput{} + return nil + } else if arg == "custom" { + var err error + queryFlags.Output, err = query.NewCustomOutput(queryFlags.CustomFormat, dateFormat) + return err + } + return fmt.Errorf("Unrecognized output format: %s", arg) + }) + queryFs.StringVar(&queryFlags.CustomFormat, "outCustomFormat", query.DefaultOutputFormat, "format string for --outFormat custom, see EXAMPLES for more details") + + queryFs.Parse(args[1:]) + case "index": + indexFs.BoolVar(&indexFlags.ignoreBadDates, "ignoreBadDates", false, "ignore malformed dates while indexing") + + customFilters := false + indexFlags.Filters = index.DefaultFilters() + indexFs.Func("filter", + "accept or reject files from indexing, applied in supplied order"+ + "\n(default Ext_.md, MaxSize_204800, YAMLHeader, ExcludeParent_templates)\n"+ + index.FilterHelp, + func(s string) error { + if !customFilters { + indexFlags.Filters = indexFlags.Filters[:0] + } + + filter, err := index.ParseFilter(s) + if err != nil { + return err + } + indexFlags.Filters = append(indexFlags.Filters, filter) + + return nil + }) + + indexFs.Parse(args[1:]) + case "help", "--help", "-help": + printHelp() + flag.PrintDefaults() + os.Exit(0) + default: + fmt.Fprintln(os.Stderr, "Unrecognized command: ", command) + printHelp() + os.Exit(ExitCommand) + } slogLevel := &slog.LevelVar{} - if *logLevel == "debug" { + switch globalFlags.LogLevel { + case "debug": slogLevel.Set(slog.LevelDebug) - } else if *logLevel == "info" { + case "info": slogLevel.Set(slog.LevelInfo) - } else if *logLevel == "warn" { + case "warn": slogLevel.Set(slog.LevelWarn) - } else if *logLevel == "error" { + case "error": slogLevel.Set(slog.LevelError) - } else { - fmt.Fprintln(os.Stderr, "Unrecognized log level:", *logLevel) + default: + fmt.Fprintln(os.Stderr, "Unrecognized log level:", globalFlags.LogLevel) os.Exit(ExitCommand) } loggerOpts := &slog.HandlerOptions{Level: slogLevel} - logger := slog.New(slog.NewTextHandler(os.Stderr, loggerOpts)) + var logHandler slog.Handler + if globalFlags.LogJson { + logHandler = slog.NewJSONHandler(os.Stderr, loggerOpts) + } else { + logHandler = slog.NewTextHandler(os.Stderr, loggerOpts) + } + logger := slog.New(logHandler) slog.SetDefault(logger) - query := data.NewQuery(*docDB) - defer query.Close() + querier := data.NewQuery(globalFlags.DBPath) + defer querier.Close() + + // command specific + switch command { + case "query": + // TODO: evaluate query + s, err := queryFlags.Output.Output(nil) + if err != nil { + slog.Error("Error while outputing query results", slog.String("err", err.Error())) + return + } + fmt.Print(s) + case "index": + idx := index.Index{Root: globalFlags.IndexRoot, Filters: indexFlags.Filters} + if logger.Enabled(context.Background(), slog.LevelDebug) { + filterNames := make([]string, 0, len(indexFlags.Filters)) + for _, filter := range indexFlags.Filters { + filterNames = append(filterNames, filter.Name) + } + logger.Debug("index", + slog.String("indexRoot", globalFlags.IndexRoot), + slog.String("filters", strings.Join(filterNames, ", ")), + ) + } - idx := index.Index{Root: *indexRoot, Filters: docFilters} - fmt.Println("index:", idx) + traversedFiles := idx.Traverse(globalFlags.NumWorkers) + fmt.Print("Crawled ", len(traversedFiles)) - traversedFiles := idx.Traverse(4) - fmt.Println("traversed files:", traversedFiles) + filteredFiles := idx.Filter(traversedFiles, globalFlags.NumWorkers) + fmt.Print(", Filtered ", len(filteredFiles)) - filteredFiles := idx.Filter(traversedFiles, 4) - fmt.Println("filtered files:", filteredFiles) + idx.Documents = index.ParseDocs(filteredFiles, globalFlags.NumWorkers, indexFlags.ignoreBadDates) + fmt.Print(", Parsed ", len(idx.Documents), "\n") - fmt.Println("Putting index") - if err := query.Put(idx); err != nil { - panic(err) + if err := querier.Put(idx); err != nil { + panic(err) + } } + } -- cgit v1.2.3