From 344c6526a8d6f490fc7628ddc7d2dd06ed1a07c1 Mon Sep 17 00:00:00 2001 From: JP Appel Date: Tue, 22 Jul 2025 15:41:03 -0400 Subject: Separate program entry point from commands --- cmd/atlas.go | 181 ----------------------------------------------------- cmd/cmd.go | 44 +++++++++++++ cmd/completions.go | 7 +++ cmd/help.go | 29 +++++++++ cmd/index.go | 25 +++++--- cmd/query.go | 50 ++++++++++++++- cmd/server.go | 27 +++++--- 7 files changed, 165 insertions(+), 198 deletions(-) delete mode 100644 cmd/atlas.go create mode 100644 cmd/cmd.go create mode 100644 cmd/completions.go create mode 100644 cmd/help.go (limited to 'cmd') diff --git a/cmd/atlas.go b/cmd/atlas.go deleted file mode 100644 index 422218b..0000000 --- a/cmd/atlas.go +++ /dev/null @@ -1,181 +0,0 @@ -package main - -import ( - "errors" - "flag" - "fmt" - "io" - "io/fs" - "log/slog" - "os" - "runtime" - "strings" - "time" - - "github.com/adrg/xdg" - "github.com/jpappel/atlas/pkg/data" - "github.com/jpappel/atlas/pkg/query" - "github.com/jpappel/atlas/pkg/shell" -) - -const VERSION = "0.0.1" -const ExitCommand = 2 // exit because of a command parsing error - -type GlobalFlags struct { - IndexRoot string - DBPath string - LogLevel string - LogJson bool - NumWorkers uint - DateFormat string -} - -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 [global-flags] \n\n", os.Args[0]) - fmt.Println("Commands:") - fmt.Println(" index - build, update, or modify an index") - fmt.Println(" query - search against an index") - fmt.Println(" shell - start a debug shell") - fmt.Println(" server - start an http query server (EXPERIMENTAL)") - fmt.Println(" help - print this help then exit") -} - -func main() { - home, _ := os.UserHomeDir() - dataHome := xdg.DataHome - if dataHome == "" { - dataHome = strings.Join([]string{home, ".local", "share"}, string(os.PathSeparator)) - } - dataHome += string(os.PathSeparator) + "atlas" - if err := os.Mkdir(dataHome, 0755); errors.Is(err, fs.ErrExist) { - } else if err != nil { - panic(err) - } - - globalFlags := GlobalFlags{} - flag.StringVar(&globalFlags.IndexRoot, "root", xdg.UserDirs.Documents, "root `directory` for indexing") - flag.StringVar(&globalFlags.DBPath, "db", dataHome+string(os.PathSeparator)+"default.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)") - flag.StringVar(&globalFlags.DateFormat, "dateFormat", time.RFC3339, "format for dates (see https://pkg.go.dev/time#Layout for more details)") - - indexFs := flag.NewFlagSet("index", flag.ExitOnError) - queryFs := flag.NewFlagSet("query", flag.ExitOnError) - shellFs := flag.NewFlagSet("debug", flag.ExitOnError) - serverFs := flag.NewFlagSet("server", flag.ExitOnError) - - indexFs.Usage = addGlobalFlagUsage(indexFs) - queryFs.Usage = addGlobalFlagUsage(queryFs) - shellFs.Usage = addGlobalFlagUsage(shellFs) - serverFs.Usage = addGlobalFlagUsage(serverFs) - - flag.Parse() - args := flag.Args() - - queryFlags := QueryFlags{Outputer: query.DefaultOutput{}} - indexFlags := IndexFlags{} - serverFlags := ServerFlags{Port: 8080} - - if len(args) < 1 { - 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", "q": - setupQueryFlags(args[1:], queryFs, &queryFlags, globalFlags.DateFormat) - case "index": - setupIndexFlags(args[1:], indexFs, &indexFlags) - case "server": - setupServerFlags(args[1:], serverFs, &serverFlags) - case "help": - printHelp() - flag.PrintDefaults() - return - case "shell": - shellFs.Parse(args[1:]) - default: - fmt.Fprintln(os.Stderr, "Unrecognized command: ", command) - printHelp() - os.Exit(ExitCommand) - } - - slogLevel := &slog.LevelVar{} - loggerOpts := &slog.HandlerOptions{Level: slogLevel} - switch globalFlags.LogLevel { - case "debug": - slogLevel.Set(slog.LevelDebug) - loggerOpts.AddSource = true - case "info": - slogLevel.Set(slog.LevelInfo) - case "warn": - slogLevel.Set(slog.LevelWarn) - case "error": - slogLevel.Set(slog.LevelError) - default: - fmt.Fprintln(os.Stderr, "Unrecognized log level:", globalFlags.LogLevel) - os.Exit(ExitCommand) - } - var logHandler slog.Handler - if globalFlags.LogJson { - logHandler = slog.NewJSONHandler(os.Stderr, loggerOpts) - } else { - // strip time - loggerOpts.ReplaceAttr = func(groups []string, a slog.Attr) slog.Attr { - if a.Key == slog.TimeKey && len(groups) == 0 { - return slog.Attr{} - } - return a - } - logHandler = slog.NewTextHandler(os.Stderr, loggerOpts) - } - logger := slog.New(logHandler) - slog.SetDefault(logger) - - querier := data.NewQuery(globalFlags.DBPath) - - // command specific - var exitCode int - switch command { - case "query", "q": - searchQuery := strings.Join(queryFs.Args(), " ") - exitCode = int(runQuery(globalFlags, queryFlags, querier, searchQuery)) - case "index": - exitCode = int(runIndex(globalFlags, indexFlags, querier)) - case "server": - exitCode = int(runServer(serverFlags, querier)) - case "shell": - state := make(shell.State) - env := make(map[string]string) - - env["workers"] = fmt.Sprint(globalFlags.NumWorkers) - env["db_path"] = globalFlags.DBPath - env["index_root"] = globalFlags.IndexRoot - env["version"] = VERSION - - interpreter := shell.NewInterpreter(state, env, globalFlags.NumWorkers, querier) - if err := interpreter.Run(); err != nil && err != io.EOF { - slog.Error("Fatal error occured", slog.String("err", err.Error())) - exitCode = 1 - } - } - - querier.Close() - os.Exit(exitCode) -} diff --git a/cmd/cmd.go b/cmd/cmd.go new file mode 100644 index 0000000..5a4f942 --- /dev/null +++ b/cmd/cmd.go @@ -0,0 +1,44 @@ +package cmd + +import ( + "errors" + "flag" + "io/fs" + "os" + "runtime" + "strings" + "time" + + "github.com/adrg/xdg" +) + +type GlobalFlags struct { + IndexRoot string + DBPath string + LogLevel string + LogJson bool + NumWorkers uint + DateFormat string + LogFile string +} + +func SetupGlobalFlags(fs_ *flag.FlagSet, flags *GlobalFlags) { + home, _ := os.UserHomeDir() + dataHome := xdg.DataHome + if dataHome == "" { + dataHome = strings.Join([]string{home, ".local", "share"}, string(os.PathSeparator)) + } + dataHome += string(os.PathSeparator) + "atlas" + if err := os.Mkdir(dataHome, 0755); errors.Is(err, fs.ErrExist) { + } else if err != nil { + panic(err) + } + + flag.StringVar(&flags.IndexRoot, "root", xdg.UserDirs.Documents, "root `directory` for indexing") + flag.StringVar(&flags.DBPath, "db", dataHome+string(os.PathSeparator)+"default.db", "`path` to document database") + flag.StringVar(&flags.LogLevel, "logLevel", "error", "set log `level` (debug, info, warn, error)") + flag.BoolVar(&flags.LogJson, "logJson", false, "log to json") + flag.UintVar(&flags.NumWorkers, "numWorkers", uint(runtime.NumCPU()), "number of worker threads to use (defaults to core count)") + flag.StringVar(&flags.DateFormat, "dateFormat", time.RFC3339, "`format` for dates (see https://pkg.go.dev/time#Layout for more details)") + flag.StringVar(&flags.LogFile, "logFile", "", "`file` to log errors to, use '-' for stdout and empty for stderr") +} diff --git a/cmd/completions.go b/cmd/completions.go new file mode 100644 index 0000000..e169952 --- /dev/null +++ b/cmd/completions.go @@ -0,0 +1,7 @@ +package cmd + +import "fmt" + +func ZshCompletions() { + fmt.Println("Not implemented yet!") +} diff --git a/cmd/help.go b/cmd/help.go new file mode 100644 index 0000000..b3844ac --- /dev/null +++ b/cmd/help.go @@ -0,0 +1,29 @@ +package cmd + +import ( + "fmt" + "os" +) + +var CommandHelp map[string]string + +func PrintHelp() { + fmt.Println("atlas is a note indexing and querying tool") + fmt.Printf("\nUsage:\n %s [global-flags] \n\n", os.Args[0]) + fmt.Println("Commands:") + fmt.Println(" index - build, update, or modify an index") + fmt.Println(" query - search against an index") + fmt.Println(" shell - start a debug shell") + fmt.Println(" server - start an http query server (EXPERIMENTAL)") + fmt.Println(" help - print this help then exit") +} + +func init() { + CommandHelp = make(map[string]string) + CommandHelp["query"] = "" + CommandHelp["index"] = "" + CommandHelp["server"] = "" + CommandHelp["completions"] = "" + CommandHelp["shell"] = "" + CommandHelp["help"] = "" +} diff --git a/cmd/index.go b/cmd/index.go index dde9550..5454a8a 100644 --- a/cmd/index.go +++ b/cmd/index.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "context" @@ -18,7 +18,7 @@ type IndexFlags struct { index.ParseOpts } -func setupIndexFlags(args []string, fs *flag.FlagSet, flags *IndexFlags) { +func SetupIndexFlags(args []string, fs *flag.FlagSet, flags *IndexFlags) { flags.ParseLinks = true flags.ParseMeta = true fs.BoolVar(&flags.IgnoreDateError, "ignoreBadDates", false, "ignore malformed dates while indexing") @@ -77,7 +77,7 @@ func setupIndexFlags(args []string, fs *flag.FlagSet, flags *IndexFlags) { } } -func runIndex(gFlags GlobalFlags, iFlags IndexFlags, db *data.Query) byte { +func RunIndex(gFlags GlobalFlags, iFlags IndexFlags, db *data.Query) byte { switch iFlags.Subcommand { case "build", "update": @@ -99,19 +99,27 @@ func runIndex(gFlags GlobalFlags, iFlags IndexFlags, db *data.Query) byte { filteredFiles := idx.Filter(traversedFiles, gFlags.NumWorkers) fmt.Print(", Filtered ", len(filteredFiles)) - idx.Documents = index.ParseDocs(filteredFiles, gFlags.NumWorkers, iFlags.ParseOpts) + var errCnt uint64 + idx.Documents, errCnt = index.ParseDocs(filteredFiles, gFlags.NumWorkers, iFlags.ParseOpts) fmt.Print(", Parsed ", len(idx.Documents), "\n") + if errCnt > 0 { + fmt.Printf("Encountered %d document parse errors", errCnt) + if !slog.Default().Enabled(context.Background(), slog.LevelWarn) { + fmt.Print(" (set log level to warn for more info)") + } + fmt.Println() + } var err error // switch in order to appease gopls... switch iFlags.Subcommand { - case "index": + case "build": err = db.Put(idx) case "update": err = db.Update(idx) } if err != nil { - fmt.Fprintln(os.Stderr, err) + fmt.Fprintln(os.Stderr, "Error modifying index:", err) return 1 } case "tidy": @@ -120,9 +128,12 @@ func runIndex(gFlags GlobalFlags, iFlags IndexFlags, db *data.Query) byte { return 1 } default: - fmt.Fprintln(os.Stderr, "Unrecognised index subcommands: ", iFlags.Subcommand) + fmt.Fprintln(os.Stderr, "Unrecognized index subcommands: ", iFlags.Subcommand) return 2 } return 0 } + +func init() { +} diff --git a/cmd/query.go b/cmd/query.go index 649b9ea..d00a792 100644 --- a/cmd/query.go +++ b/cmd/query.go @@ -1,9 +1,12 @@ -package main +package cmd import ( "flag" "fmt" + "log/slog" "os" + "slices" + "strings" "github.com/jpappel/atlas/pkg/data" "github.com/jpappel/atlas/pkg/index" @@ -16,9 +19,11 @@ type QueryFlags struct { ListSeparator string CustomFormat string OptimizationLevel int + SortBy string + SortDesc bool } -func setupQueryFlags(args []string, fs *flag.FlagSet, flags *QueryFlags, dateFormat string) { +func SetupQueryFlags(args []string, fs *flag.FlagSet, flags *QueryFlags, dateFormat string) { // NOTE: providing `-outFormat` before `-outCustomFormat` might ignore user specified format fs.Func("outFormat", "output `format` for queries (default, json, pathonly, custom)", func(arg string) error { @@ -39,6 +44,9 @@ func setupQueryFlags(args []string, fs *flag.FlagSet, flags *QueryFlags, dateFor } return fmt.Errorf("Unrecognized output format: %s", arg) }) + + fs.StringVar(&flags.SortBy, "sortBy", "", "category to sort by (path,title,date,filetime,meta)") + fs.BoolVar(&flags.SortDesc, "sortDesc", false, "sort in descending order") fs.StringVar(&flags.CustomFormat, "outCustomFormat", query.DefaultOutputFormat, "format string for --outFormat custom, see Output Format for more details") fs.IntVar(&flags.OptimizationLevel, "optLevel", 0, "optimization `level` for queries, 0 is automatic, <0 to disable") fs.StringVar(&flags.DocumentSeparator, "docSeparator", "\n", "separator for custom output format") @@ -81,7 +89,7 @@ func setupQueryFlags(args []string, fs *flag.FlagSet, flags *QueryFlags, dateFor fs.Parse(args) } -func runQuery(gFlags GlobalFlags, qFlags QueryFlags, db *data.Query, searchQuery string) byte { +func RunQuery(gFlags GlobalFlags, qFlags QueryFlags, db *data.Query, searchQuery string) byte { tokens := query.Lex(searchQuery) clause, err := query.Parse(tokens) if err != nil { @@ -114,6 +122,42 @@ func runQuery(gFlags GlobalFlags, qFlags QueryFlags, db *data.Query, searchQuery outputableResults = append(outputableResults, v) } + var docCmp func(a, b *index.Document) int + descMod := 1 + if qFlags.SortDesc { + descMod = -1 + } + switch qFlags.SortBy { + case "": + case "path": + docCmp = func(a, b *index.Document) int { + return descMod * strings.Compare(a.Path, b.Path) + } + case "title": + docCmp = func(a, b *index.Document) int { + return descMod * strings.Compare(a.Title, b.Title) + } + case "date": + docCmp = func(a, b *index.Document) int { + return descMod * a.Date.Compare(b.Date) + } + case "filetime": + docCmp = func(a, b *index.Document) int { + return descMod * a.FileTime.Compare(b.FileTime) + } + case "meta": + docCmp = func(a, b *index.Document) int { + return descMod * strings.Compare(a.OtherMeta, b.OtherMeta) + } + default: + slog.Error("Unrecognized category to sort by, leaving documents unsorted") + qFlags.SortBy = "" + } + + if qFlags.SortBy != "" { + slices.SortFunc(outputableResults, docCmp) + } + _, err = qFlags.Outputer.OutputTo(os.Stdout, outputableResults) if err != nil { fmt.Fprintln(os.Stderr, "Error while outputting results: ", err) diff --git a/cmd/server.go b/cmd/server.go index 21f4661..be072cf 100644 --- a/cmd/server.go +++ b/cmd/server.go @@ -1,4 +1,4 @@ -package main +package cmd import ( "context" @@ -8,6 +8,7 @@ import ( "net/http" "os" "os/signal" + "strings" "syscall" "time" @@ -20,17 +21,26 @@ type ServerFlags struct { Port int } -func setupServerFlags(args []string, fs *flag.FlagSet, flags *ServerFlags) { - fs.StringVar(&flags.Address, "address", "", "the address to listen on") +func SetupServerFlags(args []string, fs *flag.FlagSet, flags *ServerFlags) { + fs.StringVar(&flags.Address, "address", "", "the address to listen on, prefix with 'unix:' to create a unixsocket") fs.IntVar(&flags.Port, "port", 8080, "the port to bind to") fs.Parse(args) } -func runServer(sFlags ServerFlags, db *data.Query) byte { - addr := fmt.Sprintf("%s:%d", sFlags.Address, sFlags.Port) +func RunServer(sFlags ServerFlags, db *data.Query) byte { - s := http.Server{Addr: addr, Handler: server.New(db)} + var addr string + var s server.Server + if after, ok := strings.CutPrefix(sFlags.Address, "unix:"); ok { + slog.Debug("Preparing unix domain socket") + addr = after + s = &server.UnixServer{Addr: addr, Db: db} + } else { + slog.Debug("Preparing http server") + addr = fmt.Sprintf("%s:%d", sFlags.Address, sFlags.Port) + s = &http.Server{Addr: addr, Handler: server.NewMux(db)} + } serverErrors := make(chan error, 1) exit := make(chan os.Signal, 1) @@ -42,13 +52,16 @@ func runServer(sFlags ServerFlags, db *data.Query) byte { if err := s.ListenAndServe(); err != nil { serverErrors <- err } + close(serverErrors) }(serverErrors) select { case <-exit: slog.Info("Recieved signal to shutdown") case err := <-serverErrors: - slog.Error("Server error", slog.String("err", err.Error())) + if err != nil { + slog.Error("Server error", slog.String("err", err.Error())) + } } slog.Info("Shutting down server") -- cgit v1.2.3