From ee864b09f621e0e3924987cfb9ab22448d42cb58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sun, 10 Aug 2025 16:27:02 -0700 Subject: [PATCH 01/10] Add support for GLJPATH env var --- README.md | 10 +++++ pkg/gljmain/gljmain.go | 6 +++ pkg/runtime/rtcompat.go | 27 ++++++++++++ test/glojure/test_glojure/cli.glj | 68 +++++++++++++++++++------------ 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index 44091a45..9d387b83 100644 --- a/README.md +++ b/README.md @@ -125,6 +125,16 @@ $ glj server.glj Server starting on :8080... ``` +### Environment Variables + +Glojure recognizes the following environment variables: + +- **`GLJPATH`** + + Colon-separated list of directories to search for `.glj` libraries. + This allows you to organize Glojure libraries in a custom manner. + Note: Glojure will always search the current directory for `.glj` libraries. + ### Embedding Glojure in Go Applications You can also embed Glojure as a scripting language within your Go applications. diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index cd2c1ca0..8dd0697b 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -26,6 +26,9 @@ Options: -h, --help Show this help message --version Show version information +Environment Variables: + GLJPATH PATH of directories for .glj libraries + Examples: glj # Start REPL glj -e "(+ 1 2)" # Evaluate expression @@ -40,6 +43,9 @@ For more information, visit: https://github.com/glojurelang/glojure func Main(args []string) { runtime.AddLoadPath(os.DirFS(".")) + // Add GLJPATH directories to load path if set + runtime.AddLoadPaths(os.Getenv("GLJPATH")) + if len(args) == 0 { repl.Start() } else if args[0] == "--version" { diff --git a/pkg/runtime/rtcompat.go b/pkg/runtime/rtcompat.go index 7f848693..79f95578 100644 --- a/pkg/runtime/rtcompat.go +++ b/pkg/runtime/rtcompat.go @@ -4,6 +4,7 @@ import ( "fmt" "io" "io/fs" + "os" "reflect" "strings" "sync" @@ -31,6 +32,32 @@ func AddLoadPath(fs fs.FS) { loadPath = append(loadPath, fs) } +// AddLoadPaths adds each element from a colon-separated path string to the +// front of the load path +func AddLoadPaths(loadPaths string) { + if loadPaths == "" { + return + } + + // Turn the list of strings into a list of dirs + paths := strings.Split(loadPaths, ":") + var newPaths []fs.FS + for i := 0; i < len(paths); i++ { + path := paths[i] + if path != "" { + // Skip non-existent path directories + if _, err := os.Stat(path); err == nil { + newPaths = append(newPaths, os.DirFS(path)) + } + } + } + + // Now add all the new paths to the front + loadPathLock.Lock() + defer loadPathLock.Unlock() + loadPath = append(newPaths, loadPath...) +} + // RT is a struct with methods that map to Clojure's RT class' static // methods. This approach is used to make translation of core.clj to // Glojure easier. diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index e6d78300..3b7d597b 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -31,6 +31,20 @@ (first (str/split-lines out)) (throw (Exception. (str "Failed to find glj bin: " err)))))) +(deftest help-flag-test + (test-that + "glj --help flag works correctly" + (let [[out err] (run-cli-cmd glj "--help")] + (is (re-matches + #"(?s).*Glojure v\d+\.\d+\.\d+.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*" + out) + "Command should output help information") + (is (re-matches + #"(?s).*Environment Variables:.*GLJPATH.*PATH of directories for \.glj libraries.*" + out) + "Help should document GLJPATH environment variable") + (is (empty? err) "Command should not return an error")))) + (deftest e-flag-test (test-that "glj -e flag works correctly" @@ -46,32 +60,36 @@ "Command should output version") (is (empty? err) "Command should not return an error")))) -(deftest glojure-version-test - (test-that - "*glojure-version* should be set correctly" - (let [[out err] (run-cli-cmd glj "-e" "*glojure-version*")] - (is (= out "{:major 0, :minor 3, :incremental 0, :qualifier nil}\n") - "Version should match expected format") - (is (empty? err) "Command should not return an error")))) - -(deftest help-flag-test +(deftest gljpath-test (test-that - "glj --help flag works correctly" - (let [[out err] (run-cli-cmd glj "--help")] - (is (re-matches - #"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*" - out) - "Command should output help information") - (is (empty? err) "Command should not return an error")))) - -(deftest short-help-flag-test - (test-that - "glj -h flag works correctly" - (let [[out err] (run-cli-cmd glj "-h")] - (is (re-matches - #"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*" - out) - "Command should output help information") + "GLJPATH can load libraries from specified directories" + (let [lib1-dir (str (os.Getenv "PWD") "/test_gljpath_priority1") + lib2-dir (str (os.Getenv "PWD") "/test_gljpath_priority2") + lib1-file (str lib1-dir "/conflict.glj") + lib2-file (str lib2-dir "/conflict.glj") + test-script (str (os.Getenv "PWD") "/test_gljpath_priority_script.glj") + ;; Create conflicting libraries with same name but different content + _ (do + (os.MkdirAll lib1-dir 0755) + (os.MkdirAll lib2-dir 0755) + (os.WriteFile lib1-file + (str "(ns conflict)\n(defn version [] \"version 1\")") + 0644) + (os.WriteFile lib2-file + (str "(ns conflict)\n(defn version [] \"version 2\")") + 0644) + (os.WriteFile test-script + (str "(ns main)\n(use 'conflict)\n(println (version))") + 0644)) + ;; Test that first directory in GLJPATH takes precedence + [out err] (run-cli-cmd "sh" "-c" + (str "GLJPATH=" lib1-dir ":" lib2-dir " " glj " " test-script)) + ;; Cleanup + _ (do + (os.RemoveAll lib1-dir) + (os.RemoveAll lib2-dir) + (os.Remove test-script))] + (is (= out "version 1\n") "First directory in GLJPATH should take precedence") (is (empty? err) "Command should not return an error")))) (run-tests) From 2760ed1687ac84771df4e4097cddd767c587f2fd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Thu, 14 Aug 2025 17:03:27 +0000 Subject: [PATCH 02/10] Refactor code for adding GLJPATH to load path --- pkg/gljmain/gljmain.go | 21 +++++++++++++++++---- pkg/runtime/rtcompat.go | 35 ++++++----------------------------- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index 8dd0697b..c413a342 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -41,10 +41,23 @@ For more information, visit: https://github.com/glojurelang/glojure } func Main(args []string) { - runtime.AddLoadPath(os.DirFS(".")) - - // Add GLJPATH directories to load path if set - runtime.AddLoadPaths(os.Getenv("GLJPATH")) + // Add current directory to end of load path + runtime.AddLoadPath(os.DirFS("."), false) + + // Add GLJPATH directories to front of load path if set + loadPaths := os.Getenv("GLJPATH") + if loadPaths != "" { + paths := strings.Split(loadPaths, ":") + for i := len(paths) - 1; i >= 0; i-- { + path := paths[i] + if path != "" { + // Skip non-existent path directories + if _, err := os.Stat(path); err == nil { + runtime.AddLoadPath(os.DirFS(path), true) + } + } + } + } if len(args) == 0 { repl.Start() diff --git a/pkg/runtime/rtcompat.go b/pkg/runtime/rtcompat.go index 79f95578..ddf658ba 100644 --- a/pkg/runtime/rtcompat.go +++ b/pkg/runtime/rtcompat.go @@ -4,7 +4,6 @@ import ( "fmt" "io" "io/fs" - "os" "reflect" "strings" "sync" @@ -24,38 +23,16 @@ var ( loadPathLock sync.Mutex ) -// AddLoadPath adds a filesystem to the load path. -func AddLoadPath(fs fs.FS) { +// AddLoadPath adds a filesystem to the front or end of the load path. +func AddLoadPath(_fs fs.FS, front bool) { loadPathLock.Lock() defer loadPathLock.Unlock() - loadPath = append(loadPath, fs) -} - -// AddLoadPaths adds each element from a colon-separated path string to the -// front of the load path -func AddLoadPaths(loadPaths string) { - if loadPaths == "" { - return - } - - // Turn the list of strings into a list of dirs - paths := strings.Split(loadPaths, ":") - var newPaths []fs.FS - for i := 0; i < len(paths); i++ { - path := paths[i] - if path != "" { - // Skip non-existent path directories - if _, err := os.Stat(path); err == nil { - newPaths = append(newPaths, os.DirFS(path)) - } - } + if front { + loadPath = append([]fs.FS{_fs}, loadPath...) + } else { + loadPath = append(loadPath, _fs) } - - // Now add all the new paths to the front - loadPathLock.Lock() - defer loadPathLock.Unlock() - loadPath = append(newPaths, loadPath...) } // RT is a struct with methods that map to Clojure's RT class' static From 21024d56da85f576790d322b79784300f4c23803 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Thu, 14 Aug 2025 18:17:42 +0000 Subject: [PATCH 03/10] Refactor arg parsing to a separate function --- pkg/gljmain/gljmain.go | 85 ++++++++++++++++++++++++++++++++---------- 1 file changed, 65 insertions(+), 20 deletions(-) diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index c413a342..7aad8080 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -16,6 +16,43 @@ import ( "github.com/glojurelang/glojure/pkg/runtime" ) +// Args represents the parsed command line arguments +type Args struct { + Mode string // "repl", "version", "help", "eval", "file" + Expression string // for eval mode + Filename string // for file mode + CommandArgs []string // remaining arguments after parsing +} + +// parseArgs parses command line arguments and returns an Args struct +func parseArgs(args []string) (*Args, error) { + if len(args) == 0 { + return &Args{Mode: "repl"}, nil + } + + switch args[0] { + case "--version": + return &Args{Mode: "version"}, nil + case "--help", "-h": + return &Args{Mode: "help"}, nil + case "-e": + if len(args) < 2 { + return nil, fmt.Errorf("glj: -e requires an expression") + } + return &Args{ + Mode: "eval", + Expression: args[1], + CommandArgs: args[2:], + }, nil + default: + return &Args{ + Mode: "file", + Filename: args[0], + CommandArgs: args[1:], + }, nil + } +} + func printHelp() { fmt.Printf(`Glojure v%s @@ -59,29 +96,34 @@ func Main(args []string) { } } - if len(args) == 0 { + parsedArgs, err := parseArgs(args) + if err != nil { + log.Fatal(err) + } + + switch parsedArgs.Mode { + case "repl": repl.Start() - } else if args[0] == "--version" { + case "version": fmt.Printf("glojure v%s\n", runtime.VERSION) return - } else if args[0] == "--help" || args[0] == "-h" { + case "help": printHelp() return - } else if args[0] == "-e" { + case "eval": // Evaluate expression from command line - if len(args) < 2 { - log.Fatal("glj: -e requires an expression") - } - expr := args[1] env := lang.GlobalEnv // Set command line args (everything after -e and the expression) core := lang.FindNamespace(lang.NewSymbol("glojure.core")) - core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[2:])) - - rdr := reader.New(strings.NewReader(expr), reader.WithGetCurrentNS(func() *lang.Namespace { - return env.CurrentNamespace() - })) + core.FindInternedVar(lang.NewSymbol("*command-line-args*")). + BindRoot(lang.Seq(parsedArgs.CommandArgs)) + + rdr := reader.New( + strings.NewReader(parsedArgs.Expression), + reader.WithGetCurrentNS(func() *lang.Namespace { + return env.CurrentNamespace() + })) var lastResult interface{} for { val, err := rdr.ReadOne() @@ -101,20 +143,23 @@ func Main(args []string) { if !lang.IsNil(lastResult) { fmt.Println(lang.PrintString(lastResult)) } - } else { + case "file": // Execute file - file, err := os.Open(args[0]) + file, err := os.Open(parsedArgs.Filename) if err != nil { log.Fatal(err) } env := lang.GlobalEnv core := lang.FindNamespace(lang.NewSymbol("glojure.core")) - core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[1:])) - - rdr := reader.New(bufio.NewReader(file), reader.WithGetCurrentNS(func() *lang.Namespace { - return env.CurrentNamespace() - })) + core.FindInternedVar(lang.NewSymbol("*command-line-args*")). + BindRoot(lang.Seq(parsedArgs.CommandArgs)) + + rdr := reader.New( + bufio.NewReader(file), + reader.WithGetCurrentNS(func() *lang.Namespace { + return env.CurrentNamespace() + })) for { val, err := rdr.ReadOne() if err == reader.ErrEOF { From 580116dccd6bc54f9abfd938a63113be382c3918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Thu, 14 Aug 2025 18:39:56 +0000 Subject: [PATCH 04/10] Add -I CLI flag for adding to load path --- pkg/gljmain/gljmain.go | 117 ++++++++++++++++++++++-------- test/glojure/test_glojure/cli.glj | 2 +- 2 files changed, 88 insertions(+), 31 deletions(-) diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index 7aad8080..053ee995 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -18,10 +18,11 @@ import ( // Args represents the parsed command line arguments type Args struct { - Mode string // "repl", "version", "help", "eval", "file" - Expression string // for eval mode - Filename string // for file mode - CommandArgs []string // remaining arguments after parsing + Mode string // "repl", "version", "help", "eval", "file" + Expression string // for eval mode + Filename string // for file mode + IncludePaths []string // for -I flags + CommandArgs []string // remaining arguments after parsing } // parseArgs parses command line arguments and returns an Args struct @@ -30,27 +31,67 @@ func parseArgs(args []string) (*Args, error) { return &Args{Mode: "repl"}, nil } - switch args[0] { - case "--version": - return &Args{Mode: "version"}, nil - case "--help", "-h": - return &Args{Mode: "help"}, nil - case "-e": - if len(args) < 2 { - return nil, fmt.Errorf("glj: -e requires an expression") + // First pass: collect all -I flags and their paths + var includePaths []string + var remainingArgs []string + var mode string + var expression string + var filename string + + i := 0 + for i < len(args) { + switch args[i] { + case "--version": + if mode == "" { + mode = "version" + } + i++ + case "--help", "-h": + if mode == "" { + mode = "help" + } + i++ + case "-e": + if mode == "" { + mode = "eval" + if i+1 >= len(args) { + return nil, fmt.Errorf("glj: -e requires an expression") + } + expression = args[i+1] + i += 2 + } else { + remainingArgs = append(remainingArgs, args[i]) + i++ + } + case "-I": + if i+1 >= len(args) { + return nil, fmt.Errorf("glj: -I requires a path") + } + includePaths = append(includePaths, args[i+1]) + i += 2 + default: + if mode == "" { + mode = "file" + filename = args[i] + } else { + remainingArgs = append(remainingArgs, args[i]) + } + i++ } - return &Args{ - Mode: "eval", - Expression: args[1], - CommandArgs: args[2:], - }, nil - default: - return &Args{ - Mode: "file", - Filename: args[0], - CommandArgs: args[1:], - }, nil } + + // If no explicit mode was set, default to repl + if mode == "" { + mode = "repl" + } + + return &Args{ + Mode: mode, + Expression: expression, + Filename: filename, + IncludePaths: includePaths, + CommandArgs: remainingArgs, + }, nil } func printHelp() { @@ -60,14 +101,18 @@ Usage: glj [options] [file] Options: -e Evaluate expression from command line - -h, --help Show this help message + -I Add directory to front of library search path + --version Show version information + -h, --help Show this help message Environment Variables: GLJPATH PATH of directories for .glj libraries Examples: glj # Start REPL + glj -I /path/to/lib # Add library path and start REPL + glj -I /lib1 -I /lib2 # Add multiple library paths glj -e "(+ 1 2)" # Evaluate expression glj script.glj # Run script file glj --version # Show version @@ -78,8 +123,22 @@ For more information, visit: https://github.com/glojurelang/glojure } func Main(args []string) { - // Add current directory to end of load path - runtime.AddLoadPath(os.DirFS("."), false) + parsedArgs, err := parseArgs(args) + if err != nil { + log.Fatal(err) + } + + // Process -I include paths first (add to front of load path) + // Process in reverse order so first -I on command line gets highest priority + for i := len(parsedArgs.IncludePaths) - 1; i >= 0; i-- { + path := parsedArgs.IncludePaths[i] + if path != "" { + // Skip non-existent path directories + if _, err := os.Stat(path); err == nil { + runtime.AddLoadPath(os.DirFS(path), true) + } + } + } // Add GLJPATH directories to front of load path if set loadPaths := os.Getenv("GLJPATH") @@ -96,10 +155,8 @@ func Main(args []string) { } } - parsedArgs, err := parseArgs(args) - if err != nil { - log.Fatal(err) - } + // Add current directory to end of load path + runtime.AddLoadPath(os.DirFS("."), false) switch parsedArgs.Mode { case "repl": diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index 3b7d597b..8630803e 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -36,7 +36,7 @@ "glj --help flag works correctly" (let [[out err] (run-cli-cmd glj "--help")] (is (re-matches - #"(?s).*Glojure v\d+\.\d+\.\d+.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*" + #"(?s).*Glojure v\d+\.\d+\.\d+.*Usage: glj.*Options:.*-e.*--version.*-h.*--help.*Examples:.*" out) "Command should output help information") (is (re-matches From fbd9c4e58393b12f72f7bedd1ca8931a0e78f893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Thu, 14 Aug 2025 19:11:41 +0000 Subject: [PATCH 05/10] Some refactoring of gljmain.go to be more idiomatic --- .gitignore | 7 +++- pkg/gljmain/gljmain.go | 64 +++++++++++++++++-------------- test/glojure/test_glojure/cli.glj | 34 +++++++++++++--- 3 files changed, 68 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index 36f5ee90..dadda129 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,5 @@ -bin -doc/repl/glj.wasm +/.clj-kondo/ +/.lsp/ +/bin/ +/doc/repl/glj.wasm + diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index 053ee995..25362314 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -94,6 +94,39 @@ func parseArgs(args []string) (*Args, error) { }, nil } +// setupLoadPaths configures the library search path in order of priority +func setupLoadPaths(includePaths []string) { + // Add GLJPATH directories to front of load path if set + loadPaths := os.Getenv("GLJPATH") + if loadPaths != "" { + paths := strings.Split(loadPaths, ":") + for i := len(paths) - 1; i >= 0; i-- { + path := paths[i] + if path != "" { + // Skip non-existent path directories + if _, err := os.Stat(path); err == nil { + runtime.AddLoadPath(os.DirFS(path), true) + } + } + } + } + + // Process -I include paths last (add to front of load path, highest priority) + // Process in reverse order so first -I on command line gets highest priority + for i := len(includePaths) - 1; i >= 0; i-- { + path := includePaths[i] + if path != "" { + // Skip non-existent path directories + if _, err := os.Stat(path); err == nil { + runtime.AddLoadPath(os.DirFS(path), true) + } + } + } + + // Add current directory to end of load path + runtime.AddLoadPath(os.DirFS("."), false) +} + func printHelp() { fmt.Printf(`Glojure v%s @@ -128,35 +161,8 @@ func Main(args []string) { log.Fatal(err) } - // Process -I include paths first (add to front of load path) - // Process in reverse order so first -I on command line gets highest priority - for i := len(parsedArgs.IncludePaths) - 1; i >= 0; i-- { - path := parsedArgs.IncludePaths[i] - if path != "" { - // Skip non-existent path directories - if _, err := os.Stat(path); err == nil { - runtime.AddLoadPath(os.DirFS(path), true) - } - } - } - - // Add GLJPATH directories to front of load path if set - loadPaths := os.Getenv("GLJPATH") - if loadPaths != "" { - paths := strings.Split(loadPaths, ":") - for i := len(paths) - 1; i >= 0; i-- { - path := paths[i] - if path != "" { - // Skip non-existent path directories - if _, err := os.Stat(path); err == nil { - runtime.AddLoadPath(os.DirFS(path), true) - } - } - } - } - - // Add current directory to end of load path - runtime.AddLoadPath(os.DirFS("."), false) + // Setup library search paths + setupLoadPaths(parsedArgs.IncludePaths) switch parsedArgs.Mode { case "repl": diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index 8630803e..528ecff6 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -62,34 +62,56 @@ (deftest gljpath-test (test-that - "GLJPATH can load libraries from specified directories" + "GLJPATH and -I flags can load libraries from specified directories" (let [lib1-dir (str (os.Getenv "PWD") "/test_gljpath_priority1") lib2-dir (str (os.Getenv "PWD") "/test_gljpath_priority2") + lib3-dir (str (os.Getenv "PWD") "/test_gljpath_priority3") lib1-file (str lib1-dir "/conflict.glj") lib2-file (str lib2-dir "/conflict.glj") + lib3-file (str lib3-dir "/conflict.glj") test-script (str (os.Getenv "PWD") "/test_gljpath_priority_script.glj") ;; Create conflicting libraries with same name but different content _ (do (os.MkdirAll lib1-dir 0755) (os.MkdirAll lib2-dir 0755) + (os.MkdirAll lib3-dir 0755) (os.WriteFile lib1-file (str "(ns conflict)\n(defn version [] \"version 1\")") 0644) (os.WriteFile lib2-file (str "(ns conflict)\n(defn version [] \"version 2\")") 0644) + (os.WriteFile lib3-file + (str "(ns conflict)\n(defn version [] \"version 3\")") + 0644) (os.WriteFile test-script (str "(ns main)\n(use 'conflict)\n(println (version))") 0644)) ;; Test that first directory in GLJPATH takes precedence - [out err] (run-cli-cmd "sh" "-c" - (str "GLJPATH=" lib1-dir ":" lib2-dir " " glj " " test-script)) + [out1 err1] (run-cli-cmd "sh" "-c" + (str "GLJPATH=" lib1-dir ":" lib2-dir " " + glj " " test-script)) + ;; Test that -I flags take precedence over GLJPATH + [out2 err2] (run-cli-cmd "sh" "-c" + (str "GLJPATH=" lib2-dir ":" lib3-dir " " + glj " -I " lib1-dir " " test-script)) + ;; Test that first -I flag takes precedence over later ones + [out3 err3] (run-cli-cmd "sh" "-c" + (str glj " -I " lib3-dir " -I " lib2-dir + " -I " lib1-dir " " test-script)) ;; Cleanup _ (do (os.RemoveAll lib1-dir) (os.RemoveAll lib2-dir) + (os.RemoveAll lib3-dir) (os.Remove test-script))] - (is (= out "version 1\n") "First directory in GLJPATH should take precedence") - (is (empty? err) "Command should not return an error")))) - + (is (= out1 "version 1\n") + "First directory in GLJPATH should take precedence") + (is (empty? err1) "First command should not return an error") + (is (= out2 "version 1\n") + "-I flag should take precedence over GLJPATH") + (is (empty? err2) "Second command should not return an error") + (is (= out3 "version 3\n") + "First -I flag should take precedence over later ones") + (is (empty? err3) "Third command should not return an error")))) (run-tests) From 3700d1ec464f378b29c6ac12b4d2d5ffabae8acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 15 Aug 2025 11:26:20 -0700 Subject: [PATCH 06/10] Add some entries to .gitignore To avoid adding things that are often lying around... The .clj and .glj are to ignore top level scripts for testing stuff. The GNUmakefile and .cache are for alternate Makefile stuff like I do in https://github.com/glojurelang/glojure/pull/75 If that doesn't get merged I can just symlink to it since GNUmakefile has precedence over Makefile (for GNU make). --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index dadda129..f68ec44d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ +/*.glj +/*.clj +/.cache/ /.clj-kondo/ /.lsp/ /bin/ /doc/repl/glj.wasm - +/GNUmakefile From f4fd0ac373d16b6e5310761b07362819fb1a8531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sat, 16 Aug 2025 16:19:36 +0000 Subject: [PATCH 07/10] Use os.TempDir for temporary file location in tests --- test/glojure/test_glojure/cli.glj | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index 528ecff6..290da682 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -63,13 +63,14 @@ (deftest gljpath-test (test-that "GLJPATH and -I flags can load libraries from specified directories" - (let [lib1-dir (str (os.Getenv "PWD") "/test_gljpath_priority1") - lib2-dir (str (os.Getenv "PWD") "/test_gljpath_priority2") - lib3-dir (str (os.Getenv "PWD") "/test_gljpath_priority3") + (let [temp-dir (os.TempDir) + lib1-dir (str temp-dir "/test_gljpath_priority1") + lib2-dir (str temp-dir "/test_gljpath_priority2") + lib3-dir (str temp-dir "/test_gljpath_priority3") lib1-file (str lib1-dir "/conflict.glj") lib2-file (str lib2-dir "/conflict.glj") lib3-file (str lib3-dir "/conflict.glj") - test-script (str (os.Getenv "PWD") "/test_gljpath_priority_script.glj") + test-script (str temp-dir "/test_gljpath_priority_script.glj") ;; Create conflicting libraries with same name but different content _ (do (os.MkdirAll lib1-dir 0755) From 8a21eeb335ef645a3ae9fe0378d621ddc0ed60e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sat, 16 Aug 2025 16:35:57 +0000 Subject: [PATCH 08/10] Use flag library to parse CLI args --- pkg/gljmain/gljmain.go | 124 ++++++++++-------------------- test/glojure/test_glojure/cli.glj | 55 ------------- 2 files changed, 42 insertions(+), 137 deletions(-) diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index 25362314..fc189d6c 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -2,6 +2,7 @@ package gljmain import ( "bufio" + "flag" "fmt" "log" "os" @@ -18,84 +19,58 @@ import ( // Args represents the parsed command line arguments type Args struct { - Mode string // "repl", "version", "help", "eval", "file" - Expression string // for eval mode - Filename string // for file mode - IncludePaths []string // for -I flags - CommandArgs []string // remaining arguments after parsing + Mode string // "repl", "version", "help", "eval", "file" + Expression string // for eval mode + Filename string // for file mode + CommandArgs []string // remaining arguments after parsing } // parseArgs parses command line arguments and returns an Args struct func parseArgs(args []string) (*Args, error) { - if len(args) == 0 { - return &Args{Mode: "repl"}, nil + fs := flag.NewFlagSet("glj", flag.ExitOnError) + + var ( + helpFlag = fs.Bool("help", false, "Show this help message") + versionFlag = fs.Bool("version", false, "Show version information") + evalFlag = fs.String("e", "", "Evaluate expression from command line") + ) + + fs.Bool("h", false, "Show this help message (shorthand)") + + if err := fs.Parse(args); err != nil { + return nil, err } - // First pass: collect all -I flags and their paths - var includePaths []string - var remainingArgs []string - var mode string - var expression string - var filename string - - i := 0 - for i < len(args) { - switch args[i] { - case "--version": - if mode == "" { - mode = "version" - } - i++ - case "--help", "-h": - if mode == "" { - mode = "help" - } - i++ - case "-e": - if mode == "" { - mode = "eval" - if i+1 >= len(args) { - return nil, fmt.Errorf("glj: -e requires an expression") - } - expression = args[i+1] - i += 2 - } else { - remainingArgs = append(remainingArgs, args[i]) - i++ - } - case "-I": - if i+1 >= len(args) { - return nil, fmt.Errorf("glj: -I requires a path") - } - includePaths = append(includePaths, args[i+1]) - i += 2 - default: - if mode == "" { - mode = "file" - filename = args[i] - } else { - remainingArgs = append(remainingArgs, args[i]) - } - i++ - } + if *helpFlag || fs.Lookup("h").Value.String() == "true" { + return &Args{Mode: "help"}, nil + } + + if *versionFlag { + return &Args{Mode: "version"}, nil + } + + if *evalFlag != "" { + return &Args{ + Mode: "eval", + Expression: *evalFlag, + CommandArgs: fs.Args(), + }, nil } - // If no explicit mode was set, default to repl - if mode == "" { - mode = "repl" + remainingArgs := fs.Args() + if len(remainingArgs) > 0 { + return &Args{ + Mode: "file", + Filename: remainingArgs[0], + CommandArgs: remainingArgs[1:], + }, nil } - return &Args{ - Mode: mode, - Expression: expression, - Filename: filename, - IncludePaths: includePaths, - CommandArgs: remainingArgs, - }, nil + return &Args{Mode: "repl"}, nil } -// setupLoadPaths configures the library search path in order of priority -func setupLoadPaths(includePaths []string) { +// setupLoadPaths configures the library search path +func setupLoadPaths() { // Add GLJPATH directories to front of load path if set loadPaths := os.Getenv("GLJPATH") if loadPaths != "" { @@ -111,18 +86,6 @@ func setupLoadPaths(includePaths []string) { } } - // Process -I include paths last (add to front of load path, highest priority) - // Process in reverse order so first -I on command line gets highest priority - for i := len(includePaths) - 1; i >= 0; i-- { - path := includePaths[i] - if path != "" { - // Skip non-existent path directories - if _, err := os.Stat(path); err == nil { - runtime.AddLoadPath(os.DirFS(path), true) - } - } - } - // Add current directory to end of load path runtime.AddLoadPath(os.DirFS("."), false) } @@ -134,7 +97,6 @@ Usage: glj [options] [file] Options: -e Evaluate expression from command line - -I Add directory to front of library search path --version Show version information -h, --help Show this help message @@ -144,8 +106,6 @@ Environment Variables: Examples: glj # Start REPL - glj -I /path/to/lib # Add library path and start REPL - glj -I /lib1 -I /lib2 # Add multiple library paths glj -e "(+ 1 2)" # Evaluate expression glj script.glj # Run script file glj --version # Show version @@ -162,7 +122,7 @@ func Main(args []string) { } // Setup library search paths - setupLoadPaths(parsedArgs.IncludePaths) + setupLoadPaths() switch parsedArgs.Mode { case "repl": diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index 290da682..3c635c1f 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -60,59 +60,4 @@ "Command should output version") (is (empty? err) "Command should not return an error")))) -(deftest gljpath-test - (test-that - "GLJPATH and -I flags can load libraries from specified directories" - (let [temp-dir (os.TempDir) - lib1-dir (str temp-dir "/test_gljpath_priority1") - lib2-dir (str temp-dir "/test_gljpath_priority2") - lib3-dir (str temp-dir "/test_gljpath_priority3") - lib1-file (str lib1-dir "/conflict.glj") - lib2-file (str lib2-dir "/conflict.glj") - lib3-file (str lib3-dir "/conflict.glj") - test-script (str temp-dir "/test_gljpath_priority_script.glj") - ;; Create conflicting libraries with same name but different content - _ (do - (os.MkdirAll lib1-dir 0755) - (os.MkdirAll lib2-dir 0755) - (os.MkdirAll lib3-dir 0755) - (os.WriteFile lib1-file - (str "(ns conflict)\n(defn version [] \"version 1\")") - 0644) - (os.WriteFile lib2-file - (str "(ns conflict)\n(defn version [] \"version 2\")") - 0644) - (os.WriteFile lib3-file - (str "(ns conflict)\n(defn version [] \"version 3\")") - 0644) - (os.WriteFile test-script - (str "(ns main)\n(use 'conflict)\n(println (version))") - 0644)) - ;; Test that first directory in GLJPATH takes precedence - [out1 err1] (run-cli-cmd "sh" "-c" - (str "GLJPATH=" lib1-dir ":" lib2-dir " " - glj " " test-script)) - ;; Test that -I flags take precedence over GLJPATH - [out2 err2] (run-cli-cmd "sh" "-c" - (str "GLJPATH=" lib2-dir ":" lib3-dir " " - glj " -I " lib1-dir " " test-script)) - ;; Test that first -I flag takes precedence over later ones - [out3 err3] (run-cli-cmd "sh" "-c" - (str glj " -I " lib3-dir " -I " lib2-dir - " -I " lib1-dir " " test-script)) - ;; Cleanup - _ (do - (os.RemoveAll lib1-dir) - (os.RemoveAll lib2-dir) - (os.RemoveAll lib3-dir) - (os.Remove test-script))] - (is (= out1 "version 1\n") - "First directory in GLJPATH should take precedence") - (is (empty? err1) "First command should not return an error") - (is (= out2 "version 1\n") - "-I flag should take precedence over GLJPATH") - (is (empty? err2) "Second command should not return an error") - (is (= out3 "version 3\n") - "First -I flag should take precedence over later ones") - (is (empty? err3) "Third command should not return an error")))) (run-tests) From e5b9a643a8d054da8e6efc88f021418cda58be62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sat, 16 Aug 2025 17:08:48 +0000 Subject: [PATCH 09/10] Refactor to use PrependLoadPath --- pkg/gljmain/gljmain.go | 4 ++-- pkg/runtime/rtcompat.go | 18 +++++++++++------- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index fc189d6c..d9f20f09 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -80,14 +80,14 @@ func setupLoadPaths() { if path != "" { // Skip non-existent path directories if _, err := os.Stat(path); err == nil { - runtime.AddLoadPath(os.DirFS(path), true) + runtime.PrependLoadPath(os.DirFS(path)) } } } } // Add current directory to end of load path - runtime.AddLoadPath(os.DirFS("."), false) + runtime.AddLoadPath(os.DirFS(".")) } func printHelp() { diff --git a/pkg/runtime/rtcompat.go b/pkg/runtime/rtcompat.go index ddf658ba..d7335c16 100644 --- a/pkg/runtime/rtcompat.go +++ b/pkg/runtime/rtcompat.go @@ -23,16 +23,20 @@ var ( loadPathLock sync.Mutex ) -// AddLoadPath adds a filesystem to the front or end of the load path. -func AddLoadPath(_fs fs.FS, front bool) { +// AddLoadPath adds a filesystem to the end of the load path. +func AddLoadPath(fs fs.FS) { loadPathLock.Lock() defer loadPathLock.Unlock() - if front { - loadPath = append([]fs.FS{_fs}, loadPath...) - } else { - loadPath = append(loadPath, _fs) - } + loadPath = append(loadPath, fs) +} + +// PrependLoadPath adds a filesystem to the front of the load path. +func PrependLoadPath(_fs fs.FS) { + loadPathLock.Lock() + defer loadPathLock.Unlock() + + loadPath = append([]fs.FS{_fs}, loadPath...) } // RT is a struct with methods that map to Clojure's RT class' static From c3eb30778dcf200330b6d066ac8253322c0b3bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Sat, 16 Aug 2025 11:23:57 -0700 Subject: [PATCH 10/10] Support a *glojure-load-path* variable $ GLJPATH=foo:bar ./bin/linux_amd64/glj -e '*glojure-load-path*' ("foo" "bar" "" ".") --- internal/deps/embed.go | 2 +- pkg/gljmain/gljmain.go | 15 +++-- pkg/runtime/envinit.go | 6 ++ pkg/runtime/environment.go | 1 + pkg/runtime/rtcompat.go | 76 +++++++++++++++++++++++--- test/glojure/test_glojure/loadpath.glj | 21 +++++++ 6 files changed, 106 insertions(+), 15 deletions(-) create mode 100644 test/glojure/test_glojure/loadpath.glj diff --git a/internal/deps/embed.go b/internal/deps/embed.go index efe1f7f1..5a8b97c3 100644 --- a/internal/deps/embed.go +++ b/internal/deps/embed.go @@ -67,7 +67,7 @@ func (d *Deps) Embed() error { fmt.Fprintf(b, "func init() {\n") for _, pkg := range d.Pkgs { fsName := mungePath(pkg) + "FS" - fmt.Fprintf(b, "\truntime.AddLoadPath(subfs(%s, %q))\n", fsName, pkg) + fmt.Fprintf(b, "\truntime.AddLoadPath(subfs(%s, %q), %q)\n", fsName, pkg, pkg) } fmt.Fprintf(b, "}\n") diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index d9f20f09..57e0c82f 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -78,16 +78,21 @@ func setupLoadPaths() { for i := len(paths) - 1; i >= 0; i-- { path := paths[i] if path != "" { - // Skip non-existent path directories - if _, err := os.Stat(path); err == nil { - runtime.PrependLoadPath(os.DirFS(path)) - } + runtime.PrependLoadPathString(path) } } } // Add current directory to end of load path - runtime.AddLoadPath(os.DirFS(".")) + runtime.AddLoadPathString(".") + + // Update the *glojure-load-path* dynamic var with the current load path + if core := lang.FindNamespace(lang.NewSymbol("glojure.core")); core != nil { + loadPathVar := core.FindInternedVar(lang.NewSymbol("*glojure-load-path*")) + if loadPathVar != nil { + loadPathVar.BindRoot(lang.Seq(runtime.GetLoadPath())) + } + } } func printHelp() { diff --git a/pkg/runtime/envinit.go b/pkg/runtime/envinit.go index 153114de..f6e5529b 100644 --- a/pkg/runtime/envinit.go +++ b/pkg/runtime/envinit.go @@ -155,6 +155,12 @@ func NewEnvironment(opts ...EvalOption) lang.Environment { versionVar.BindRoot(ParseVersion(VERSION)) } + // Set the glojure load path + loadPathVar := core.FindInternedVar(lang.NewSymbol("*glojure-load-path*")) + if loadPathVar != nil { + loadPathVar.BindRoot(lang.Seq(GetLoadPath())) + } + return env } diff --git a/pkg/runtime/environment.go b/pkg/runtime/environment.go index f79cc29c..b8372a62 100644 --- a/pkg/runtime/environment.go +++ b/pkg/runtime/environment.go @@ -64,6 +64,7 @@ func newEnvironment(ctx context.Context, stdout, stderr io.Writer) *environment "print-dup", "read-eval", "glojure-version", + "glojure-load-path", } { coreNS.InternWithValue(lang.NewSymbol("*"+dyn+"*"), nil, true).SetDynamic() } diff --git a/pkg/runtime/rtcompat.go b/pkg/runtime/rtcompat.go index d7335c16..f4c4bb8f 100644 --- a/pkg/runtime/rtcompat.go +++ b/pkg/runtime/rtcompat.go @@ -1,9 +1,11 @@ package runtime import ( + "embed" "fmt" "io" "io/fs" + "os" "reflect" "strings" "sync" @@ -16,27 +18,83 @@ import ( . "github.com/glojurelang/glojure/pkg/lang" ) +// loadPathEntry represents a single entry in the load path +type loadPathEntry struct { + fs fs.FS + name string +} + var ( RT = &RTMethods{} - loadPath = []fs.FS{stdlib.StdLib} + loadPath = []loadPathEntry{{fs: stdlib.StdLib, name: ""}} loadPathLock sync.Mutex ) -// AddLoadPath adds a filesystem to the end of the load path. -func AddLoadPath(fs fs.FS) { +// AddLoadPath adds a filesystem with a name to the end of the load path. +func AddLoadPath(fs fs.FS, name string) { + loadPathLock.Lock() + defer loadPathLock.Unlock() + + loadPath = append(loadPath, loadPathEntry{fs: fs, name: name}) +} + +// AddLoadPathString adds a directory path to the end of the load path. +func AddLoadPathString(path string) { + AddLoadPath(os.DirFS(path), path) +} + +// PrependLoadPathString adds a directory path to the front of the load path. +func PrependLoadPathString(path string) { loadPathLock.Lock() defer loadPathLock.Unlock() - loadPath = append(loadPath, fs) + loadPath = append([]loadPathEntry{{fs: os.DirFS(path), name: path}}, loadPath...) } -// PrependLoadPath adds a filesystem to the front of the load path. -func PrependLoadPath(_fs fs.FS) { +// GetLoadPath returns a copy of the current load path as a slice of strings. +// Each string represents a filesystem path that can be used by Glojure. +func GetLoadPath() []string { loadPathLock.Lock() defer loadPathLock.Unlock() - loadPath = append([]fs.FS{_fs}, loadPath...) + result := make([]string, len(loadPath)) + for i, entry := range loadPath { + if entry.name != "" { + result[i] = entry.name + } else { + // Fallback to the old logic for unnamed filesystems + switch v := entry.fs.(type) { + case embed.FS: + result[i] = "" + default: + if name := getFSName(v); name != "" { + result[i] = name + } else { + result[i] = fmt.Sprintf("<%v>", v) + } + } + } + } + return result +} + +// getFSName attempts to extract a meaningful name from a filesystem. +// This is a helper function for GetLoadPath. +func getFSName(fsys fs.FS) string { + // Try to read the root directory to see if it's a named filesystem + if entries, err := fs.ReadDir(fsys, "."); err == nil && len(entries) > 0 { + // If it's a subdirectory filesystem, try to get the parent name + if subFS, ok := fsys.(interface{ Name() string }); ok { + return subFS.Name() + } + // For embedded filesystems, we might be able to infer the name + // from the directory structure + if len(entries) == 1 && entries[0].IsDir() { + return entries[0].Name() + } + } + return "" } // RT is a struct with methods that map to Clojure's RT class' static @@ -146,8 +204,8 @@ func (rt *RTMethods) Load(scriptBase string) { loadPathLock.Lock() lp := loadPath loadPathLock.Unlock() - for _, fs := range lp { - buf, err = readFile(fs, filename) + for _, entry := range lp { + buf, err = readFile(entry.fs, filename) if err == nil { break } diff --git a/test/glojure/test_glojure/loadpath.glj b/test/glojure/test_glojure/loadpath.glj new file mode 100644 index 00000000..3f88ad3e --- /dev/null +++ b/test/glojure/test_glojure/loadpath.glj @@ -0,0 +1,21 @@ +(ns glojure.test-glojure.load-path + (:use glojure.test)) + +(deftest LoadPath + (is (some? *glojure-load-path*) "glojure-load-path dynamic var exists") + (is (seq? *glojure-load-path*) "glojure-load-path is a sequence") + (is (> (count *glojure-load-path*) 0) "glojure-load-path has elements") + (is (some? (first *glojure-load-path*)) "glojure-load-path first element exists") + + (is (some #(= % "") *glojure-load-path*) "glojure-load-path contains stdlib") + + (let [original-path *glojure-load-path*] + (binding [*glojure-load-path* ["test-path"]] + (is (= *glojure-load-path* ["test-path"]) "binding works correctly") + (is (= (count *glojure-load-path*) 1) "binding changes count")) + (is (= *glojure-load-path* original-path) "binding is restored")) + + (doseq [path *glojure-load-path*] + (is (string? path) (str "Path element is not a string: " path)))) + +(run-tests)