Skip to content
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,8 @@
bin
doc/repl/glj.wasm
/*.glj
/*.clj
/.cache/
/.clj-kondo/
/.lsp/
/bin/
/doc/repl/glj.wasm
/GNUmakefile
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion internal/deps/embed.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down
132 changes: 112 additions & 20 deletions pkg/gljmain/gljmain.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package gljmain

import (
"bufio"
"flag"
"fmt"
"log"
"os"
Expand All @@ -16,15 +17,97 @@ 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) {
Comment on lines +28 to +29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i should have called this out in the prior command line flag PR, but as this is getting more involved, let's use the standard library's flag parsing: https://pkg.go.dev/flag

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 8a21eeb

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
}

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
}

remainingArgs := fs.Args()
if len(remainingArgs) > 0 {
return &Args{
Mode: "file",
Filename: remainingArgs[0],
CommandArgs: remainingArgs[1:],
}, nil
}

return &Args{Mode: "repl"}, nil
}

// setupLoadPaths configures the library search path
func setupLoadPaths() {
// 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 != "" {
runtime.PrependLoadPathString(path)
}
}
}

// Add current directory to end of load path
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() {
fmt.Printf(`Glojure v%s

Usage: glj [options] [file]

Options:
-e <expr> Evaluate expression from command line
-h, --help Show this help message

--version Show version information
-h, --help Show this help message

Environment Variables:
GLJPATH PATH of directories for .glj libraries

Examples:
glj # Start REPL
Expand All @@ -38,31 +121,37 @@ For more information, visit: https://github.com/glojurelang/glojure
}

func Main(args []string) {
runtime.AddLoadPath(os.DirFS("."))
parsedArgs, err := parseArgs(args)
if err != nil {
log.Fatal(err)
}

if len(args) == 0 {
// Setup library search paths
setupLoadPaths()

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:]))
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).
BindRoot(lang.Seq(parsedArgs.CommandArgs))

rdr := reader.New(strings.NewReader(expr), reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
}))
rdr := reader.New(
strings.NewReader(parsedArgs.Expression),
reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
}))
var lastResult interface{}
for {
val, err := rdr.ReadOne()
Expand All @@ -82,20 +171,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:]))
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()
}))
rdr := reader.New(
bufio.NewReader(file),
reader.WithGetCurrentNS(func() *lang.Namespace {
return env.CurrentNamespace()
}))
for {
val, err := rdr.ReadOne()
if err == reader.ErrEOF {
Expand Down
6 changes: 6 additions & 0 deletions pkg/runtime/envinit.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}

Expand Down
1 change: 1 addition & 0 deletions pkg/runtime/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
78 changes: 72 additions & 6 deletions pkg/runtime/rtcompat.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package runtime

import (
"embed"
"fmt"
"io"
"io/fs"
"os"
"reflect"
"strings"
"sync"
Expand All @@ -16,19 +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: "<StdLib>"}}
loadPathLock sync.Mutex
)

// AddLoadPath adds a filesystem to 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([]loadPathEntry{{fs: os.DirFS(path), name: path}}, loadPath...)
}

// 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(loadPath, fs)
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] = "<StdLib>"
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
Expand Down Expand Up @@ -138,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
}
Expand Down
Loading