From 7de92528b70e627190e0f8888cc6d1899efed4c3 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Tue, 3 Sep 2024 18:07:13 -0400 Subject: [PATCH 01/11] Support output formats Adds support for printing CSV, JSON, Markdown, and LaTeX. Includes support for new --output-format CLI argument as well as new system functions )output.* for toggling output formats interactively at the REPL. --- README.md | 8 +- cmd/ari/input.go | 13 +++- cmd/ari/root.go | 161 +++++++++++++++++++++++++++++++++-------- goal.go | 18 ++++- testing/table.goal | 1 - vendor-goal/out.goal | 21 ++++++ vendor-goal/table.goal | 5 +- 7 files changed, 185 insertions(+), 42 deletions(-) create mode 100644 vendor-goal/out.goal diff --git a/README.md b/README.md index e295d8f..a40c63a 100644 --- a/README.md +++ b/README.md @@ -50,19 +50,19 @@ Non-exhaustive list: - TODO: Test coverage. - TODO: Correct usage of `goal.NewError` vs. `goal.NewPanicError` - TODO: Option for raw REPL (modeled on Goal's) with better input performance, no auto-complete etc. -- TODO: Goal function to return auto-complete results (esp. if raw REPL is being used). -- TODO: Looser auto-complete, not just prefix-based - TODO: Functions to conveniently populate SQL tables with Goal values. - TODO: Support plots/charts (consider https://github.com/wcharczuk/go-chart) - TODO: User commands (as found in [APL](https://aplwiki.com/wiki/User_command)), executable from Goal or SQL modes I plan to support the above items. The following are stretch goals or nice-to-have's: +- TODO: Use custom table functions via replacement scan to query Goal tables from DuckDB. +- TODO: Looser auto-complete, not just prefix-based +- TODO: `)help` - TODO: Functions leveraging [time.Time](https://pkg.go.dev/time@go1.22.5) -- TODO: `tui.` functions in CLI mode using https://github.com/charmbracelet/lipgloss (already a transitive dependency) for colored output, etc. +- IN PROGRESS: `tui.` functions in CLI mode using https://github.com/charmbracelet/lipgloss (already a transitive dependency) for colored output, etc. - TODO: Implement a subset of [q](https://code.kx.com/q/) functions to extend what Goal already has. - Specific user commands: - - TODO: Choosing output format (e.g., as JSON, perhaps all the ones DuckDB supports) - TODO: Toggle pretty-printing - TODO: Toggle paging at the REPL (as found in [PicoLisp](https://picolisp.com/wiki/?home)) - TODO: Toggle colored output diff --git a/cmd/ari/input.go b/cmd/ari/input.go index dd4069f..93a4ca1 100644 --- a/cmd/ari/input.go +++ b/cmd/ari/input.go @@ -397,7 +397,18 @@ func (autoCompleter *AutoCompleter) cacheSystemCommands() { func systemCommands() (map[string]string, []string) { m := map[string]string{ - ")goal": "Goal array language mode", ")sql": "Read-only SQL mode (querying)", ")sql!": "Read/write SQL mode", + ")goal": "Goal array language mode", + // TODO Output formats: https://duckdb.org/docs/api/cli/output_formats.html + // In particular csv, json, markdown, latex, and one of the boxed ones + ")output.csv": "Print results as CSV", + ")output.goal": "Print results as Goal values (default)", + ")output.json": "Print results as JSON", + ")output.json+pretty": "Print results as JSON with indentation", + ")output.latex": "Print results as LaTeX", + ")output.markdown": "Print results as Markdown", + ")output.tsv": "Print results as TSV", + ")sql": "Read-only SQL mode (querying)", + ")sql!": "Read/write SQL mode", } // Prepare sorted keys ahead of time keys := make([]string, 0, len(m)) diff --git a/cmd/ari/root.go b/cmd/ari/root.go index 29fc601..637e6a8 100644 --- a/cmd/ari/root.go +++ b/cmd/ari/root.go @@ -33,6 +33,18 @@ const ( cliModeSQLReadWrite ) +type outputFormat int + +const ( + outputFormatGoal outputFormat = iota + outputFormatCSV + outputFormatJSON + outputFormatJSONPretty + outputFormatLatex + outputFormatMarkdown + outputFormatTSV +) + const ( cliModeGoalPrompt = " " cliModeGoalNextPrompt = " " @@ -43,11 +55,12 @@ const ( ) type CliSystem struct { + ariContext *ari.Context + autoCompleter *AutoCompleter cliEditor *bubbline.Editor cliMode cliMode - autoCompleter *AutoCompleter - ariContext *ari.Context debug bool + outputFormat outputFormat programName string } @@ -138,7 +151,28 @@ func cliModeFromString(s string) (cliMode, error) { case "sql!": return cliModeSQLReadWrite, nil default: - return 0, errors.New("unsupported ari mode: " + s) + return 0, errors.New("unsupported --mode: " + s) + } +} + +func outputFormatFromString(s string) (outputFormat, error) { + switch s { + case "csv": + return outputFormatCSV, nil + case "goal": + return outputFormatGoal, nil + case "json": + return outputFormatJSON, nil + case "json+pretty": + return outputFormatJSONPretty, nil + case "latex": + return outputFormatLatex, nil + case "markdown": + return outputFormatMarkdown, nil + case "tsv": + return outputFormatTSV, nil + default: + return 0, errors.New("unsupported --output-format: " + s) } } @@ -191,16 +225,27 @@ func ariMain(cmd *cobra.Command, args []string) int { defer pprof.StopCPUProfile() } + // Defaults to outputFormatGoal + startupOutputFormatString := viper.GetString("output-format") + startupOutputFormat, err := outputFormatFromString(startupOutputFormatString) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: %v\n", err) + return 1 + } + mainCliSystem.outputFormat = startupOutputFormat + // MUST PRECEDE EXECUTE/REPL goalFilesToLoad := viper.GetStringSlice("load") for _, f := range goalFilesToLoad { - err = runScript(&mainCliSystem, f) + _, err = runScript(&mainCliSystem, f) if err != nil { fmt.Fprintf(os.Stderr, "Failed to load file %q with error: %v", f, err) return 1 } } + // By default, we don't print the final return value of a script, but this flag supports that. + printFinalValue := viper.GetBool("println") // Support file argument both with -e and standalone. hasFileArgument := len(args) > 0 @@ -211,13 +256,16 @@ func ariMain(cmd *cobra.Command, args []string) int { return 1 } if programToExecute != "" { - err = runCommand(&mainCliSystem, programToExecute) - if err != nil { + goalV, errr := runCommand(&mainCliSystem, programToExecute) + if errr != nil { fmt.Fprintf(os.Stderr, "Failed to execute program:\n%q\n with error:\n%v\n", programToExecute, err) return 1 } // Support -e/--execute along with a file argument. if !hasFileArgument { + if printFinalValue { + printInOutputFormat(ariContext.GoalContext, mainCliSystem.outputFormat, goalV) + } return 0 } } @@ -230,11 +278,14 @@ func ariMain(cmd *cobra.Command, args []string) int { fmt.Fprintf(os.Stderr, "File %q is not recognized as a path on your system: %v", f, err) } ariContext.GoalContext.AssignGlobal("FILE", goal.NewS(path)) - err = runScript(&mainCliSystem, f) - if err != nil { + goalV, errr := runScript(&mainCliSystem, f) + if errr != nil { fmt.Fprintf(os.Stderr, "Failed to run file %q with error: %v", f, err) return 1 } + if printFinalValue { + printInOutputFormat(ariContext.GoalContext, mainCliSystem.outputFormat, goalV) + } return 0 } @@ -332,18 +383,54 @@ func (cliSystem *CliSystem) replEvalGoal(line string) { } if !goalContext.AssignedLast() { - ariPrintFn := cliSystem.detectAriPrint() + // In the REPL, make it easy to get the value of the _p_revious expression + // just evaluated. Equivalent of *1 in Lisp REPLs. Skip assignments. + printInOutputFormat(goalContext, cliSystem.outputFormat, value) + } + + cliSystem.detectAriPrompt() +} + +func printInOutputFormat(goalContext *goal.Context, outputFormat outputFormat, value goal.V) { + goalContext.AssignGlobal("ari.p", value) + switch outputFormat { + case outputFormatGoal: + ariPrintFn := detectAriPrint(goalContext) if ariPrintFn != nil { ariPrintFn(value) } else { fmt.Fprintln(os.Stdout, value.Sprint(goalContext, false)) } - // In the REPL, make it easy to get the value of the _p_revious expression - // just evaluated. Equivalent of *1 in Lisp REPLs. Skip assignments. - goalContext.AssignGlobal("ari.p", value) + case outputFormatCSV: + evalThen(goalContext, value, `csv ari.p`) + case outputFormatJSON: + evalThen(goalContext, value, `""json ari.p`) + case outputFormatJSONPretty: + evalThen(goalContext, value, `" "json ari.p`) + case outputFormatLatex: + evalThen(goalContext, value, `out.ltx[ari.p;"%.2f"]`) + case outputFormatMarkdown: + evalThen(goalContext, value, `out.md[ari.p;"%.2f"]`) + case outputFormatTSV: + evalThen(goalContext, value, `"\t"csv ari.p`) } +} - cliSystem.detectAriPrompt() +// evalThen evaluates the given goalProgram for side effects, with ari.p already bound to previous evaluation. +func evalThen(goalContext *goal.Context, value goal.V, goalProgram string) { + nextValue, err := goalContext.Eval(goalProgram) + if err != nil { + formatREPLError(err) + } + if value.IsError() { + formatREPLError(newExitError(goalContext, value.Error())) + } + switch jsonS := nextValue.BV().(type) { + case goal.S: + fmt.Fprintln(os.Stdout, string(jsonS)) + default: + formatREPLError(errors.New("developer error: json must produce a string")) + } } // ExitError is returned by Cmd when the program returns a Goal error value. @@ -411,8 +498,7 @@ func (cliSystem *CliSystem) detectAriPrompt() { } // detectAriPrint returns a function for printing values at the REPL in goal mode. -func (cliSystem *CliSystem) detectAriPrint() func(goal.V) { - goalContext := cliSystem.ariContext.GoalContext +func detectAriPrint(goalContext *goal.Context) func(goal.V) { printFn, found := goalContext.GetGlobal("ari.print") if found { if printFn.IsCallable() { @@ -459,9 +545,22 @@ func (cliSystem *CliSystem) replEvalSystemCommand(line string) error { cmdAndArgs := strings.Split(line, " ") systemCommand := cmdAndArgs[0] switch systemCommand { - // IDEA )help that doesn't require quoting case ")goal": return cliSystem.switchMode(cliModeGoal, nil) + case ")output.goal": + cliSystem.outputFormat = outputFormatGoal + case ")output.csv": + cliSystem.outputFormat = outputFormatCSV + case ")output.json": + cliSystem.outputFormat = outputFormatJSON + case ")output.json+pretty": + cliSystem.outputFormat = outputFormatJSONPretty + case ")output.latex": + cliSystem.outputFormat = outputFormatLatex + case ")output.markdown": + cliSystem.outputFormat = outputFormatMarkdown + case ")output.tsv": + cliSystem.outputFormat = outputFormatTSV case ")sql": return cliSystem.switchMode(cliModeSQLReadOnly, cmdAndArgs[1:]) case ")sql!": @@ -510,15 +609,15 @@ func debugPrintStack(ctx *goal.Context, programName string) { } // Adapted from Goal's implementation. -func runCommand(cliSystem *CliSystem, cmd string) error { +func runCommand(cliSystem *CliSystem, cmd string) (goal.V, error) { return runSource(cliSystem, cmd, "") } // Adapted from Goal's implementation. -func runScript(cliSystem *CliSystem, fname string) error { +func runScript(cliSystem *CliSystem, fname string) (goal.V, error) { bs, err := os.ReadFile(fname) if err != nil { - return fmt.Errorf("%s: %w", cliSystem.programName, err) + return goal.NewGap(), fmt.Errorf("%s: %w", cliSystem.programName, err) } // We avoid redundant copy in bytes->string conversion. source := unsafe.String(unsafe.SliceData(bs), len(bs)) @@ -526,27 +625,27 @@ func runScript(cliSystem *CliSystem, fname string) error { } // Adapted from Goal's implementation. -func runSource(cliSystem *CliSystem, source, loc string) error { +func runSource(cliSystem *CliSystem, source, loc string) (goal.V, error) { goalContext := cliSystem.ariContext.GoalContext err := goalContext.Compile(source, loc, "") if err != nil { if cliSystem.debug { printProgram(goalContext, cliSystem.programName) } - return formatError(cliSystem.programName, err) + return goal.NewGap(), formatError(cliSystem.programName, err) } if cliSystem.debug { printProgram(goalContext, cliSystem.programName) - return nil + return goal.NewGap(), nil } r, err := goalContext.Run() if err != nil { - return formatError(cliSystem.programName, err) + return r, formatError(cliSystem.programName, err) } if r.IsError() { - return fmt.Errorf("%s", formatGoalError(goalContext, r)) + return r, fmt.Errorf("%s", formatGoalError(goalContext, r)) } - return nil + return r, nil } // printProgram prints debug information about the context and any compiled @@ -655,21 +754,15 @@ working with SQL and HTTP APIs.`, var cfgFile string cobra.OnInitialize(initConfigFn(cfgFile)) - // Here you will define your flags and configuration settings. - // Cobra supports persistent flags, which, if defined here, - // will be global for your application. - home, err := os.UserHomeDir() cobra.CheckErr(err) cfgDir := path.Join(home, ".config", "ari") - defaultHistFile := path.Join(cfgDir, "ari-history.txt") defaultCfgFile := path.Join(cfgDir, "ari-config.yaml") // Config file has processing in initConfigFn outside of viper lifecycle, so it's a separate variable. rootCmd.PersistentFlags().StringVar(&cfgFile, "config", defaultCfgFile, "ari configuration") - // Everything else should go through viper for consistency. pFlags := rootCmd.PersistentFlags() flagNameHistory := "history" @@ -692,6 +785,12 @@ working with SQL and HTTP APIs.`, rootCmd.Flags().StringP("mode", "m", "goal", "language mode at startup") err = viper.BindPFlag("mode", rootCmd.Flags().Lookup("mode")) cobra.CheckErr(err) + rootCmd.Flags().StringP("output-format", "f", "goal", "evaluation output format") + err = viper.BindPFlag("output-format", rootCmd.Flags().Lookup("output-format")) + cobra.CheckErr(err) + rootCmd.Flags().BoolP("println", "p", false, "print final value of the script + newline") + err = viper.BindPFlag("println", rootCmd.Flags().Lookup("println")) + cobra.CheckErr(err) rootCmd.Flags().BoolP("version", "v", false, "print version info and exit") // NB: MUST be last in this method. diff --git a/goal.go b/goal.go index 9199af9..4938128 100644 --- a/goal.go +++ b/goal.go @@ -23,15 +23,24 @@ const ( // goalLoadExtendedPreamble loads the goalSource* snippets below, // loading them into the Goal context. func goalLoadExtendedPreamble(ctx *goal.Context) error { - goalPackages := map[string]string{ - "": goalSourceShape + goalSourceTable, + corePackages := map[string]string{ + "": goalSourceShape + "\n" + goalSourceTable, + } + additionalPackages := map[string]string{ "fmt": goalSourceFmt, "html": goalSourceHTML, "k": goalSourceK, + "out": goalSourceOut, "math": goalSourceMath, "mods": goalSourceMods, } - for pkg, source := range goalPackages { + for pkg, source := range corePackages { + _, err := ctx.EvalPackage(source, "", pkg) + if err != nil { + return err + } + } + for pkg, source := range additionalPackages { _, err := ctx.EvalPackage(source, "", pkg) if err != nil { return err @@ -58,6 +67,9 @@ var goalSourceMods string //go:embed vendor-goal/shape.goal var goalSourceShape string +//go:embed vendor-goal/out.goal +var goalSourceOut string + //go:embed vendor-goal/table.goal var goalSourceTable string diff --git a/testing/table.goal b/testing/table.goal index cb5e333..7e2dd93 100644 --- a/testing/table.goal +++ b/testing/table.goal @@ -1,3 +1,2 @@ t:csv.t ","csv 'read"data/starwars.csv";(#*t;!t) / (87;"name""height""mass""hair_color""skin_color""eye_color""birth_year""sex""gender""homeworld""species""films""vehicles""starships") t:json.t@json rq/[{"a":1,"b":2},{"a":10,"b":20},{"a":100,"b":200}]/;(#*t;!t) / (3;"a""b") - diff --git a/vendor-goal/out.goal b/vendor-goal/out.goal new file mode 100644 index 0000000..df900bb --- /dev/null +++ b/vendor-goal/out.goal @@ -0,0 +1,21 @@ +/ TODO Determine right way to define istbl globally. +istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} +md.tbl:{[t;fmt] + k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v + (k;v):(-w)!'´(k;v); "|"+("|\n|"/,/"|"/(k;"-"*w;+v))+"|"} +md.depths:{[l;ind] ,/(..?[(@x)¿"ANSI";md.depths[x;p.ind+1];p.ind])'l} +md.lst:{[l;fmt] + ds:md.depths[l;0]; v:(..?["n"=@x;p.fmt$x;$'x])',//l + ind:..x*" "; pfx:(ind'ds)+"- " + "\n"/v;"\n"/pfx+v} +md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} +ltx.lq:{sub[rq/ "/;" ``"]sub[rx/(?m)^"/;"``"]x} +ltx.rq:{sub[rq/" /;"'' "]sub[rx/(?m)"$/;"''"]x} +ltx:{[t;fmt] + algn:!"r l"; algns:""/algn["S"=@'t] + k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v + (k;v):(-w)!'´(k;v); rs:" \\\\\n"/,/" & "/(k;"\\hline %";+v) + ltx.lq@ltx.rq@qq`\\begin{tabular}{|$algns|}\n\\hline + $rs \\\\\n\\hline\n\\end{tabular}` +} +1 diff --git a/vendor-goal/table.goal b/vendor-goal/table.goal index 6219638..a93c382 100644 --- a/vendor-goal/table.goal +++ b/vendor-goal/table.goal @@ -1,2 +1,3 @@ -csv.t:{(*'x)!(1_'x)} -json.t:{ks:!*x; vs:@[;ks]'x; ks!+vs} +istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} +csv.tbl:{(*'x)!(1_'x)} +json.tbl:{ks:!*x; vs:@[;ks]'x; ks!+vs} From 068e99b844e98933493e9694f99343b3eb5dc761 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Tue, 3 Sep 2024 21:30:42 -0400 Subject: [PATCH 02/11] Correct tests after rename --- testing/table.goal | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/table.goal b/testing/table.goal index 7e2dd93..feffc46 100644 --- a/testing/table.goal +++ b/testing/table.goal @@ -1,2 +1,2 @@ -t:csv.t ","csv 'read"data/starwars.csv";(#*t;!t) / (87;"name""height""mass""hair_color""skin_color""eye_color""birth_year""sex""gender""homeworld""species""films""vehicles""starships") -t:json.t@json rq/[{"a":1,"b":2},{"a":10,"b":20},{"a":100,"b":200}]/;(#*t;!t) / (3;"a""b") +t:csv.tbl ","csv 'read"data/starwars.csv";(#*t;!t) / (87;"name""height""mass""hair_color""skin_color""eye_color""birth_year""sex""gender""homeworld""species""films""vehicles""starships") +t:json.tbl@json rq/[{"a":1,"b":2},{"a":10,"b":20},{"a":100,"b":200}]/;(#*t;!t) / (3;"a""b") From 652d2f03c3a7f8d0ccba51235ebce6495e4297c5 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Tue, 3 Sep 2024 21:46:20 -0400 Subject: [PATCH 03/11] Put all Goal code for Ari into single, top-level file --- vendor-goal/out.goal => ari.goal | 21 ++++++++++++--------- goal.go | 24 ++++++------------------ vendor-goal/README.md | 6 ------ vendor-goal/shape.goal | 4 ---- vendor-goal/table.goal | 3 --- 5 files changed, 18 insertions(+), 40 deletions(-) rename vendor-goal/out.goal => ari.goal (54%) delete mode 100644 vendor-goal/shape.goal delete mode 100644 vendor-goal/table.goal diff --git a/vendor-goal/out.goal b/ari.goal similarity index 54% rename from vendor-goal/out.goal rename to ari.goal index df900bb..6cdfe3f 100644 --- a/vendor-goal/out.goal +++ b/ari.goal @@ -1,21 +1,24 @@ -/ TODO Determine right way to define istbl globally. -istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} -md.tbl:{[t;fmt] +istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} / is x a dictionary-as-table +reshape:{((*/x)#y){(-y)$x}/|1_x} / Implementation by anaseto, shared on Matrix +shape:{-1_#:'*:\x} / Implementation by John Earnest, shared on k-tree +md.tbl:{[t;fmt] / helper k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v (k;v):(-w)!'´(k;v); "|"+("|\n|"/,/"|"/(k;"-"*w;+v))+"|"} -md.depths:{[l;ind] ,/(..?[(@x)¿"ANSI";md.depths[x;p.ind+1];p.ind])'l} -md.lst:{[l;fmt] +md.depths:{[l;ind] ,/(..?[(@x)¿"ANSI";md.depths[x;p.ind+1];p.ind])'l} / helper +md.lst:{[l;fmt] / helper ds:md.depths[l;0]; v:(..?["n"=@x;p.fmt$x;$'x])',//l ind:..x*" "; pfx:(ind'ds)+"- " "\n"/v;"\n"/pfx+v} -md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} -ltx.lq:{sub[rq/ "/;" ``"]sub[rx/(?m)^"/;"``"]x} -ltx.rq:{sub[rq/" /;"'' "]sub[rx/(?m)"$/;"''"]x} -ltx:{[t;fmt] +md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} / Markdown output +ltx.lq:{sub[rq/ "/;" ``"]sub[rx/(?m)^"/;"``"]x} / helper +ltx.rq:{sub[rq/" /;"'' "]sub[rx/(?m)"$/;"''"]x} / helper +ltx:{[t;fmt] / LaTeX output algn:!"r l"; algns:""/algn["S"=@'t] k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v (k;v):(-w)!'´(k;v); rs:" \\\\\n"/,/" & "/(k;"\\hline %";+v) ltx.lq@ltx.rq@qq`\\begin{tabular}{|$algns|}\n\\hline $rs \\\\\n\\hline\n\\end{tabular}` } +csv.tbl:{(*'x)!(1_'x)} / table from csv parsing, assumes header +json.tbl:{ks:!*x; vs:@[;ks]'x; ks!+vs} / table from parsing json array of like objects 1 diff --git a/goal.go b/goal.go index 4938128..fc83ef9 100644 --- a/goal.go +++ b/goal.go @@ -23,29 +23,23 @@ const ( // goalLoadExtendedPreamble loads the goalSource* snippets below, // loading them into the Goal context. func goalLoadExtendedPreamble(ctx *goal.Context) error { - corePackages := map[string]string{ - "": goalSourceShape + "\n" + goalSourceTable, - } additionalPackages := map[string]string{ "fmt": goalSourceFmt, "html": goalSourceHTML, "k": goalSourceK, - "out": goalSourceOut, "math": goalSourceMath, "mods": goalSourceMods, } - for pkg, source := range corePackages { - _, err := ctx.EvalPackage(source, "", pkg) - if err != nil { - return err - } - } for pkg, source := range additionalPackages { _, err := ctx.EvalPackage(source, "", pkg) if err != nil { return err } } + _, err := ctx.EvalPackage(goalSourceAri, "", "") + if err != nil { + return err + } return nil } @@ -64,14 +58,8 @@ var goalSourceMath string //go:embed vendor-goal/mods.goal var goalSourceMods string -//go:embed vendor-goal/shape.goal -var goalSourceShape string - -//go:embed vendor-goal/out.goal -var goalSourceOut string - -//go:embed vendor-goal/table.goal -var goalSourceTable string +//go:embed ari.goal +var goalSourceAri string // Goal functions implemented in Go diff --git a/vendor-goal/README.md b/vendor-goal/README.md index 02a3f64..a5d83f8 100644 --- a/vendor-goal/README.md +++ b/vendor-goal/README.md @@ -2,12 +2,6 @@ Goal code that is loaded by default into the Ari runtime is located here as files, embedded into the Ari Go program using `//go:embed` directives. The sections below describe where the code has been taken from. -## This repo - -- [shape.goal](shape.goal) - - Implementation of `reshape` by discodoug, shared [on Matrix](https://matrix.to/#/!laJBzNwLcAOMAbAEeQ:matrix.org/$VnF4KPl4GZKc7F0kYXQ3nUq_4mQJaUIiXNrg0ziHU08?via=matrix.org&via=t2bot.io&via=matrix.fedibird.com) - - Implementation of `shape` by John Earnest, shared [in the k tree StackExchange chat](https://chat.stackexchange.com/transcript/message/54070438#54070438) - ## [goal] repo Last fetched at commit `a7797629ad427b77f3608f1c3a0c725e5cb692d3`: diff --git a/vendor-goal/shape.goal b/vendor-goal/shape.goal deleted file mode 100644 index 0b9d28f..0000000 --- a/vendor-goal/shape.goal +++ /dev/null @@ -1,4 +0,0 @@ -/ Implementation by anaseto, shared on Matrix -reshape:{((*/x)#y){(-y)$x}/|1_x} -/ Implementation by John Earnest, shared on k-tree -shape:{-1_#:'*:\x} \ No newline at end of file diff --git a/vendor-goal/table.goal b/vendor-goal/table.goal deleted file mode 100644 index a93c382..0000000 --- a/vendor-goal/table.goal +++ /dev/null @@ -1,3 +0,0 @@ -istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} -csv.tbl:{(*'x)!(1_'x)} -json.tbl:{ks:!*x; vs:@[;ks]'x; ks!+vs} From 1a986b5e2a11356c2b1c65e61721a2023cc89bbe Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Tue, 3 Sep 2024 21:46:37 -0400 Subject: [PATCH 04/11] Add CREDITS.md for detailed acknowledgements --- CREDITS.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 CREDITS.md diff --git a/CREDITS.md b/CREDITS.md new file mode 100644 index 0000000..49fa69e --- /dev/null +++ b/CREDITS.md @@ -0,0 +1,4 @@ +# Credits + +- Implementation of `reshape` by anaseto, shared [on Matrix](https://matrix.to/#/#aplfarm-k:matrix.org) +- Implementation of `shape` by John Earnest, shared [in the k tree StackExchange chat](https://chat.stackexchange.com/transcript/message/54070438#54070438) From b01e65ed2b25e868acd6a956a932be6057fef4df Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 11:10:42 -0400 Subject: [PATCH 05/11] Upgrade to latest Goal --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index b6e0645..6fe37d2 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.21 toolchain go1.22.0 require ( - codeberg.org/anaseto/goal v0.43.1-0.20240826114907-518e4cd38255 + codeberg.org/anaseto/goal v0.43.1-0.20240904143145-df99d8e0051b github.com/charmbracelet/lipgloss v0.13.0 github.com/go-resty/resty/v2 v2.14.0 github.com/jarcoal/httpmock v1.3.1 diff --git a/go.sum b/go.sum index 7a76fc4..460a142 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -codeberg.org/anaseto/goal v0.43.1-0.20240826114907-518e4cd38255 h1:IexlZ26Okt0SC84R85FxOFPAAbXObfjPeHFHBVoq9Yg= -codeberg.org/anaseto/goal v0.43.1-0.20240826114907-518e4cd38255/go.mod h1:oipi4mkQiwXW9Td2IxNhuMV0Ewq4obs6EvkqpyZ6qMs= +codeberg.org/anaseto/goal v0.43.1-0.20240904143145-df99d8e0051b h1:P3prjSjmz8wYHT4iTg3ZYivbcpaHmL7G5KrW37Xqx04= +codeberg.org/anaseto/goal v0.43.1-0.20240904143145-df99d8e0051b/go.mod h1:oipi4mkQiwXW9Td2IxNhuMV0Ewq4obs6EvkqpyZ6qMs= github.com/apache/arrow/go/v17 v17.0.0 h1:RRR2bdqKcdbss9Gxy2NS/hK8i4LDMh23L6BbkN5+F54= github.com/apache/arrow/go/v17 v17.0.0/go.mod h1:jR7QHkODl15PfYyjM2nU+yTLScZ/qfj7OSUZmJ8putc= github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4= From 5f1b9db131baea966cfd5d8ec2de91eb28e1f666 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 11:10:55 -0400 Subject: [PATCH 06/11] Complete LaTeX output format --- ari.goal | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/ari.goal b/ari.goal index 6cdfe3f..daf0788 100644 --- a/ari.goal +++ b/ari.goal @@ -1,24 +1,37 @@ istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} / is x a dictionary-as-table reshape:{((*/x)#y){(-y)$x}/|1_x} / Implementation by anaseto, shared on Matrix shape:{-1_#:'*:\x} / Implementation by John Earnest, shared on k-tree +depths:{[l;ind] ,/(..?[(@x)¿"ANSI";q.depths[x;p.ind+1];p.ind])'l} +{ind:1; ..?[(@x)¿"ANSI";q.depths[x;p.ind+1];p.ind]} +/ depth of each item relative to root md.tbl:{[t;fmt] / helper k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v (k;v):(-w)!'´(k;v); "|"+("|\n|"/,/"|"/(k;"-"*w;+v))+"|"} -md.depths:{[l;ind] ,/(..?[(@x)¿"ANSI";md.depths[x;p.ind+1];p.ind])'l} / helper md.lst:{[l;fmt] / helper - ds:md.depths[l;0]; v:(..?["n"=@x;p.fmt$x;$'x])',//l + ds:depths[l;0]; v:(..?["n"=@x;p.fmt$x;$'x])',//l ind:..x*" "; pfx:(ind'ds)+"- " "\n"/v;"\n"/pfx+v} md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} / Markdown output ltx.lq:{sub[rq/ "/;" ``"]sub[rx/(?m)^"/;"``"]x} / helper ltx.rq:{sub[rq/" /;"'' "]sub[rx/(?m)"$/;"''"]x} / helper -ltx:{[t;fmt] / LaTeX output +ltx.tbl:{[t;fmt] / helper algn:!"r l"; algns:""/algn["S"=@'t] k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v (k;v):(-w)!'´(k;v); rs:" \\\\\n"/,/" & "/(k;"\\hline %";+v) ltx.lq@ltx.rq@qq`\\begin{tabular}{|$algns|}\n\\hline $rs \\\\\n\\hline\n\\end{tabular}` } +ltx.be:"\\begin{enumerate}"; ltx.ee:"\\end{enumerate}" / helpers +ltx.lstenv:{ / helper + (n;sig):(abs x;sign x); + ?[sig=-1;"\n"+(""/(n-1)#"$ltx.be\n\\item\n")+"$ltx.be\n\\item %v\n" + sig=0;"\\item %v\n" + "\n"+(""/n#"$ltx.ee\n")+"\n\\item %v\n"]} +ltx.lst:{[l;fmt] / helper + v:(..?["n"=@x;p.fmt$x;$'x])',//l; + ds:depths[l;0]; cs:(»ds)-ds; fs:,/ltx.lstenv'cs + "$ltx.be\n"+(""/fs$'v)+"$ltx.ee"} +ltx:{[x;fmt]?[istbl x;ltx.tbl[x;fmt]; (@x)¿"ANSI";ltx.lst[x;fmt]; "n"=@x;fmt$x; $x]} / LaTeX output csv.tbl:{(*'x)!(1_'x)} / table from csv parsing, assumes header json.tbl:{ks:!*x; vs:@[;ks]'x; ks!+vs} / table from parsing json array of like objects 1 From 7669e3649e4be2a6eec8f97a591e163f5dfb5291 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 11:11:07 -0400 Subject: [PATCH 07/11] Copy newest lib/ from Goal --- goal.go | 18 +++++++++++++----- vendor-goal/README.md | 8 ++++---- vendor-goal/k.goal | 2 ++ vendor-goal/mods.goal | 19 ++++++++++--------- vendor-goal/path.goal | 25 +++++++++++++++++++++++++ vendor-goal/table.goal | 18 ++++++++++++++++++ 6 files changed, 72 insertions(+), 18 deletions(-) create mode 100644 vendor-goal/path.goal create mode 100644 vendor-goal/table.goal diff --git a/goal.go b/goal.go index fc83ef9..aa1089b 100644 --- a/goal.go +++ b/goal.go @@ -24,11 +24,13 @@ const ( // loading them into the Goal context. func goalLoadExtendedPreamble(ctx *goal.Context) error { additionalPackages := map[string]string{ - "fmt": goalSourceFmt, - "html": goalSourceHTML, - "k": goalSourceK, - "math": goalSourceMath, - "mods": goalSourceMods, + "fmt": goalSourceFmt, + "html": goalSourceHTML, + "k": goalSourceK, + "math": goalSourceMath, + "mods": goalSourceMods, + "path": goalSourcePath, + "table": goalSourceTable, } for pkg, source := range additionalPackages { _, err := ctx.EvalPackage(source, "", pkg) @@ -58,6 +60,12 @@ var goalSourceMath string //go:embed vendor-goal/mods.goal var goalSourceMods string +//go:embed vendor-goal/path.goal +var goalSourcePath string + +//go:embed vendor-goal/table.goal +var goalSourceTable string + //go:embed ari.goal var goalSourceAri string diff --git a/vendor-goal/README.md b/vendor-goal/README.md index a5d83f8..e2231a1 100644 --- a/vendor-goal/README.md +++ b/vendor-goal/README.md @@ -4,14 +4,14 @@ Goal code that is loaded by default into the Ari runtime is located here as file ## [goal] repo -Last fetched at commit `a7797629ad427b77f3608f1c3a0c725e5cb692d3`: +Last fetched at commit [`123b0b2e2432bcb4b8518eea734cf3c3428017b2`](https://codeberg.org/anaseto/goal/commit/123b0b2e2432bcb4b8518eea734cf3c3428017b2): ``` -commit a7797629ad427b77f3608f1c3a0c725e5cb692d3 +commit 123b0b2e2432bcb4b8518eea734cf3c3428017b2 Author: Yon -Date: Mon Jul 15 15:53:23 2024 +0000 +Date: Wed Sep 4 07:42:25 2024 +0000 - add a couple of examples + lib/table.goal: minor tweak in join & more testing ``` The Goal code vendored in this repository is licensed as follows: diff --git a/vendor-goal/k.goal b/vendor-goal/k.goal index febd035..76f48a9 100644 --- a/vendor-goal/k.goal +++ b/vendor-goal/k.goal @@ -4,4 +4,6 @@ group:{(&x!¿c)!=(!#x)!c:%x} by:{(&y!¿c)!=x!c:%y} / freq X returns count by distinct values. freq:{(&x!¿c)!=c:%x} +/ reshape[X;y] rearranges (*/X)#y into shape X. +reshape:{((*/x)#y){(-y)$x}/|1_x} 1 diff --git a/vendor-goal/mods.goal b/vendor-goal/mods.goal index fa0aff9..db0773e 100644 --- a/vendor-goal/mods.goal +++ b/vendor-goal/mods.goal @@ -1,10 +1,11 @@ -/ This file implements adverbial-like modifiers as user-defined functions. -er:{[f;x;y]f[x;]'y} / each-right x f/:y -eer:{[f;x;y]er[f';x;y]} / each each-right x f'/: y -eel:{[f;x;y]x f'`y} / each each-left x f'\: y -ere:{[f;x;y]x er[f;;]'y} / each-right each x f/:' y -ele:{[f;x;y]x f`'y} / each-left each x f\:' y -erel:{[f;x;y]x er[f;;]`y} / each-right each-left x f/:\:y -eler:{[f;x;y]er[f`;x;y]} / each-left each-right x f\:/:y -swp:{[f;x;y]f[y;x]} / argument swap +/ This file implements typical adverbial combinations involving each-right as +/ user-defined functions. +/ If you cannot type each-right's non-ASCII symbol ´ easily, you might want to +/ import or copy-paste them. The equivalent form in ngn/k is shown in the +/ comments. +er:{[f;x;y]x f´y} / each-right x f/:y +eer:{[f;x;y]x f'´y} / each each-right x f'/: y +ere:{[f;x;y]x f´'y} / each-right each x f/:' y +erel:{[f;x;y]x f´`y} / each-right each-left x f/:\:y +eler:{[f;x;y]x f`´y} / each-left each-right x f\:/:y 1 diff --git a/vendor-goal/path.goal b/vendor-goal/path.goal new file mode 100644 index 0000000..47a9687 --- /dev/null +++ b/vendor-goal/path.goal @@ -0,0 +1,25 @@ +/ base[s] returns the last element of a path. +base:{[path] + path or:"." + path:{"/"~x -1}(-1_)/path + ?["/"¿path;rx`[^/]*$`[path;"s"];path]or"/" +} +/ clean[s] returns the shortest equivalent path. +clean:{[path] + elts:in[;"."]^"/"\path / get non-. non-empty path elements + elts:(0#""){?[and[y~"..";x;~".."~*|x];-1_x;x,y]}/elts / clean .. after non-.. dir + ?[isAbs path + "/"+"/"/&elts!~&\elts=".." / remove .. elements that begin a rooted path + ("/"/elts)or"."] +} +/ dir[s] returns all but the last element of a path, after cleaning. +dir:{[path]clean@*split path} +/ ext[s] returns the filename extension. +ext:{[path]rx`\.[^/.]*$`[path;"s"]} +/ isAbs[s] reports whether the path is absolute. +isAbs:{[path]path[;1]~"/"} +/ join[S] joins non-empty path elements into a cleaned slash-separated path. +join:{[elts]?[+/&elts;clean@"/"/(""=)^elts;""]} +/ split[s] splits a path just after any final slash into a (dir;filename) pair. +split:{[path]1_rx`^(.*?/?)([^/]*)$`path} +1 diff --git a/vendor-goal/table.goal b/vendor-goal/table.goal new file mode 100644 index 0000000..3a902d4 --- /dev/null +++ b/vendor-goal/table.goal @@ -0,0 +1,18 @@ +/ by[c;f;t] groups by column(s) named c, joining summary table f[t;by], where +/ parameter "by" contains the grouping indices. +by:{[c;f;t]t[&¿by;c],f[t;by:%?["A"~@g:t c;{(1+|/'x)/x}@%'g;g]]} +/ sort[f;t] sorts table t by function(s) f; f takes a table argument. +sort:{[f;t]?["A"~@f;t{x@y x}/|f;t@f t]} +/ ij[c;t1;t2] returns the inner join of tables t1 and t2, using a single common +/ column named c, and assuming t1 c contains distinct values. It gives t2 +/ priority for any other common columns. +ij:{[c;t1;t2]i:(k:t1 c)?t2 c; (((c=!:)^t1)@&i!m),(1;m:i<#k)#t2} +/ join is a more general variant of ij that handles duplicate entries in the c +/ column. Implementation is based on code shared by @Marshall on the matrix +/ chat. +join:{[c;t1;t2],/(t1;t2)@'{(i;j):(0,#x)_%x,y; (&(=j)@i;,/(=(!#j)!j)@i)}[t1 c;t2 c]} +/ oj[c;t1;t2] returns the outer join of tables t1 and t2, using a single common +/ column named c, and assuming t1 c contains distinct values. It gives t2 +/ priority for any other common columns. It uses zero-values, instead of +/ nulls/NaNs, for missing fields in t1. +oj:{[c;t1;t2]i:(t1 c)?t2 c; (((c=!:)^t1)i),t2} From 3e8c9ee658ca4890ca00e0fa69309ab16fd58a56 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 11:11:35 -0400 Subject: [PATCH 08/11] Update README with recent additions --- README.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a40c63a..5d034ea 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Ari stands for **A**rray **R**elational **I**nteractive programming environment. -Ari is a set of extensions to the [Goal] programming language, also providing an extensible CLI with dedicated SQL mode. +Ari is a set of extensions to the [Goal] programming language with an extensible CLI and dedicated SQL mode. ## Installation @@ -26,6 +26,7 @@ Then run `ari` for a REPL or `ari --help` to see CLI options. - Runtime configuration: - Configure the REPL prompt by setting string values for the `ari.prompt` and `ari.nextprompt` (for multiline input) globals - Replace default REPL printing by setting a function value for the `ari.print` global (function receives a single Goal value to print) + - Configure the output format with `--output-format` or using one of the `)output.` system commands at the REPL. Formats include CSV/TSV, JSON, Markdown, and LaTeX. - `ari.p` is bound to the previous result (value from last evaluation at the REPL) - Extensible help system - `help"help"` for an overview @@ -37,7 +38,10 @@ Then run `ari` for a REPL or `ari --help` to see CLI options. - New Goal functions: - `http.` functions for HTTP requests using [Resty] - `sql.` functions for SQL queries and commands -- Dedicated SQL mode for DuckDB + - _(WIP)_ `time.` functions for more extensive date/time handling + - `tui.` functions for basic terminal UI styling (colors, padding/margin, borders) +- Dedicated SQL mode + - The ari CLI uses DuckDB, but the `github.com/semperos/ari` Go package doesn't directly depend on a specific SQL database driver, so you can BYODB. - Activate with `)sql` for read-only, `)sql!` for read/write modes. Execute `)goal` to return to the default Goal mode. - Auto-completion of SQL keywords - Help entries for SQL keywords (shown during auto-complete, still WIP) From af994a2e8823661f9ec22231e3e6b7e18e439fc4 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 16:14:20 -0400 Subject: [PATCH 09/11] Rename Goal output format functions --- ari.goal | 6 +++--- cmd/ari/root.go | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ari.goal b/ari.goal index daf0788..906d208 100644 --- a/ari.goal +++ b/ari.goal @@ -1,7 +1,7 @@ istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} / is x a dictionary-as-table reshape:{((*/x)#y){(-y)$x}/|1_x} / Implementation by anaseto, shared on Matrix shape:{-1_#:'*:\x} / Implementation by John Earnest, shared on k-tree -depths:{[l;ind] ,/(..?[(@x)¿"ANSI";q.depths[x;p.ind+1];p.ind])'l} +depths:{[l;ind] ,/(..?[(@x)¿"ANSI";p.o[x;p.ind+1];p.ind])'l} {ind:1; ..?[(@x)¿"ANSI";q.depths[x;p.ind+1];p.ind]} / depth of each item relative to root md.tbl:{[t;fmt] / helper @@ -11,7 +11,7 @@ md.lst:{[l;fmt] / helper ds:depths[l;0]; v:(..?["n"=@x;p.fmt$x;$'x])',//l ind:..x*" "; pfx:(ind'ds)+"- " "\n"/v;"\n"/pfx+v} -md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} / Markdown output +sprintf.md:{[x;fmt]?[istbl x;md.tbl[x;fmt]; (@x)¿"ANSI";md.lst[x;fmt]; "n"=@x;fmt$x; $x]} / String print to Markdown ltx.lq:{sub[rq/ "/;" ``"]sub[rx/(?m)^"/;"``"]x} / helper ltx.rq:{sub[rq/" /;"'' "]sub[rx/(?m)"$/;"''"]x} / helper ltx.tbl:{[t;fmt] / helper @@ -31,7 +31,7 @@ ltx.lst:{[l;fmt] / helper v:(..?["n"=@x;p.fmt$x;$'x])',//l; ds:depths[l;0]; cs:(»ds)-ds; fs:,/ltx.lstenv'cs "$ltx.be\n"+(""/fs$'v)+"$ltx.ee"} -ltx:{[x;fmt]?[istbl x;ltx.tbl[x;fmt]; (@x)¿"ANSI";ltx.lst[x;fmt]; "n"=@x;fmt$x; $x]} / LaTeX output +sprintf.ltx:{[x;fmt]?[istbl x;ltx.tbl[x;fmt]; (@x)¿"ANSI";ltx.lst[x;fmt]; "n"=@x;fmt$x; $x]} / LaTeX output csv.tbl:{(*'x)!(1_'x)} / table from csv parsing, assumes header json.tbl:{ks:!*x; vs:@[;ks]'x; ks!+vs} / table from parsing json array of like objects 1 diff --git a/cmd/ari/root.go b/cmd/ari/root.go index 637e6a8..2c8b575 100644 --- a/cmd/ari/root.go +++ b/cmd/ari/root.go @@ -408,9 +408,9 @@ func printInOutputFormat(goalContext *goal.Context, outputFormat outputFormat, v case outputFormatJSONPretty: evalThen(goalContext, value, `" "json ari.p`) case outputFormatLatex: - evalThen(goalContext, value, `out.ltx[ari.p;"%.2f"]`) + evalThen(goalContext, value, `sprintf.ltx[ari.p;"%.2f"]`) case outputFormatMarkdown: - evalThen(goalContext, value, `out.md[ari.p;"%.2f"]`) + evalThen(goalContext, value, `sprintf.md[ari.p;"%.2f"]`) case outputFormatTSV: evalThen(goalContext, value, `"\t"csv ari.p`) } From b641dfe5b1916282aed40273906e5f61223d3ccb Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 23:25:17 -0400 Subject: [PATCH 10/11] Remove debug code --- ari.goal | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ari.goal b/ari.goal index 906d208..ac97b85 100644 --- a/ari.goal +++ b/ari.goal @@ -1,9 +1,7 @@ istbl:{and["d"=@x;&/"s"=@'!x;&/{(@'x)¿"ANSI"}x;&/(*ls)=ls:#'x]} / is x a dictionary-as-table reshape:{((*/x)#y){(-y)$x}/|1_x} / Implementation by anaseto, shared on Matrix shape:{-1_#:'*:\x} / Implementation by John Earnest, shared on k-tree -depths:{[l;ind] ,/(..?[(@x)¿"ANSI";p.o[x;p.ind+1];p.ind])'l} -{ind:1; ..?[(@x)¿"ANSI";q.depths[x;p.ind+1];p.ind]} -/ depth of each item relative to root +depths:{[l;ind] ,/(..?[(@x)¿"ANSI";p.o[x;p.ind+1];p.ind])'l} / list depths md.tbl:{[t;fmt] / helper k:!t; v:(..?[(@x)¿"nN";p.fmt$x;$'x])'.t; w:(-1+""#k)|(|/-1+""#)'v (k;v):(-w)!'´(k;v); "|"+("|\n|"/,/"|"/(k;"-"*w;+v))+"|"} From b37f294a096722209bf0f07aafc5ff9e671be0b5 Mon Sep 17 00:00:00 2001 From: Daniel Gregoire Date: Wed, 4 Sep 2024 23:25:31 -0400 Subject: [PATCH 11/11] Support --raw for raw REPL --- cmd/ari/root.go | 65 +++++++++++++++++++++++++++++++++++++------------ 1 file changed, 50 insertions(+), 15 deletions(-) diff --git a/cmd/ari/root.go b/cmd/ari/root.go index 2c8b575..f80a4d3 100644 --- a/cmd/ari/root.go +++ b/cmd/ari/root.go @@ -1,6 +1,7 @@ package main import ( + "bufio" "errors" "fmt" "io" @@ -308,7 +309,12 @@ func ariMain(cmd *cobra.Command, args []string) int { } // REPL - readEvalPrintLoop(mainCliSystem) + useRawREPL := viper.GetBool("raw") + if useRawREPL { + rawREPL(mainCliSystem) + } else { + editorREPL(mainCliSystem) + } return 0 } @@ -319,13 +325,17 @@ func registerCliGoalBindings(ariContext *ari.Context) { goalContext.RegisterDyad("tui.render", VFTuiRender) } -func readEvalPrintLoop(mainCliSystem CliSystem) { - cliEditor := mainCliSystem.cliEditor +func rawREPL(cliSystem CliSystem) { + runStdin(cliSystem.ariContext.GoalContext, cliSystem.outputFormat) +} + +func editorREPL(cliSystem CliSystem) { + cliEditor := cliSystem.cliEditor for { line, err := cliEditor.GetLine() if err != nil { if errors.Is(err, io.EOF) { - mainCliSystem.shutdown() + cliSystem.shutdown() break } if errors.Is(err, bubbline.ErrInterrupted) { @@ -346,20 +356,20 @@ func readEvalPrintLoop(mainCliSystem CliSystem) { // Future: Consider user commands with ] if matchesSystemCommand(line) { - err = mainCliSystem.replEvalSystemCommand(line) + err = cliSystem.replEvalSystemCommand(line) if err != nil { fmt.Fprintf(os.Stderr, "Failed to execute system command %q with error: %v\n", line, err) } continue } - switch mainCliSystem.cliMode { + switch cliSystem.cliMode { case cliModeGoal: - mainCliSystem.replEvalGoal(line) + cliSystem.replEvalGoal(line) case cliModeSQLReadOnly: - mainCliSystem.replEvalSQLReadOnly(line) + cliSystem.replEvalSQLReadOnly(line) case cliModeSQLReadWrite: - mainCliSystem.replEvalSQLReadWrite(line) + cliSystem.replEvalSQLReadWrite(line) } } } @@ -638,14 +648,36 @@ func runSource(cliSystem *CliSystem, source, loc string) (goal.V, error) { printProgram(goalContext, cliSystem.programName) return goal.NewGap(), nil } - r, err := goalContext.Run() + value, err := goalContext.Run() if err != nil { - return r, formatError(cliSystem.programName, err) + return value, formatError(cliSystem.programName, err) } - if r.IsError() { - return r, fmt.Errorf("%s", formatGoalError(goalContext, r)) + if value.IsError() { + return value, fmt.Errorf("%s", formatGoalError(goalContext, value)) + } + return value, nil +} + +// Adapted from Goal's implementation. +func runStdin(ctx *goal.Context, outputFormat outputFormat) { + sc := &scanner{r: bufio.NewReader(os.Stdin)} + for { + fmt.Fprint(os.Stdout, " ") + s, err := sc.readLine() + s = strings.TrimRight(s, "\n\r") + if err != nil && s == "" { + return + } + value, err := ctx.Eval(s) + if err != nil { + formatREPLError(err) + continue + } + assigned := ctx.AssignedLast() + if !assigned && value != (goal.V{}) { + printInOutputFormat(ctx, outputFormat, value) + } } - return r, nil } // printProgram prints debug information about the context and any compiled @@ -776,8 +808,8 @@ working with SQL and HTTP APIs.`, err = viper.BindPFlag(flagNameDatabase, pFlags.Lookup(flagNameDatabase)) cobra.CheckErr(err) - rootCmd.Flags().Bool("debug", false, "enable detailed debugging output on panic") rootCmd.Flags().Bool("cpu-profile", false, "write CPU profile to file") + rootCmd.Flags().Bool("debug", false, "enable detailed debugging output on panic") rootCmd.Flags().StringP("execute", "e", "", "string of Goal code to execute, last result not printed automatically") rootCmd.Flags().StringArrayP("load", "l", nil, "Goal source files to load on startup") err = viper.BindPFlag("load", rootCmd.Flags().Lookup("load")) @@ -791,6 +823,9 @@ working with SQL and HTTP APIs.`, rootCmd.Flags().BoolP("println", "p", false, "print final value of the script + newline") err = viper.BindPFlag("println", rootCmd.Flags().Lookup("println")) cobra.CheckErr(err) + rootCmd.Flags().BoolP("raw", "r", false, "raw REPL w/out history or auto-complete") + err = viper.BindPFlag("raw", rootCmd.Flags().Lookup("raw")) + cobra.CheckErr(err) rootCmd.Flags().BoolP("version", "v", false, "print version info and exit") // NB: MUST be last in this method.