diff --git a/Makefile b/Makefile
index ef05f707..3dfd6998 100644
--- a/Makefile
+++ b/Makefile
@@ -6,6 +6,28 @@ STDLIB := $(STDLIB_ORIGINALS:scripts/rewrite-core/originals/%=%)
STDLIB_ORIGINALS := $(addprefix scripts/rewrite-core/originals/,$(STDLIB))
STDLIB_TARGETS := $(addprefix pkg/stdlib/glojure/,$(STDLIB:.clj=.glj))
+OS-TYPE := $(shell bash -c 'echo $$OSTYPE')
+OS-NAME := \
+ $(if $(findstring darwin,$(OS-TYPE))\
+ ,macos,$(if $(findstring linux,$(OS-TYPE)),linux,))
+ARCH-TYPE := $(shell bash -c 'echo $$MACHTYPE')
+ARCH-NAME := \
+ $(if $(or $(findstring arm64,$(ARCH-TYPE)),\
+ $(findstring aarch64,$(ARCH-TYPE)))\
+ ,arm64,$(if $(findstring x86_64,$(ARCH-TYPE)),int64,))
+
+ifdef OS-NAME
+ifdef ARCH-NAME
+OS-ARCH := $(OS-NAME)-$(ARCH-NAME)
+OA-linux-arm64 := linux_arm64
+OA-linux-int64 := linux_amd64
+OA-macos-arm64 := darwin_arm64
+OA-macos-int64 := darwin_amd64
+OA := $(OA-$(OS-ARCH))
+GLJ := bin/$(OA)/glj
+endif
+endif
+
TEST_FILES := $(shell find ./test -name '*.glj' | sort)
TEST_TARGETS := $(addsuffix .test,$(TEST_FILES))
@@ -31,6 +53,13 @@ gocmd:
generate:
@go generate ./...
+.PHONY: build
+build: $(GLJ)
+
+.PHONY: clean
+clean:
+ $(RM) -r bin/
+
pkg/gen/gljimports/gljimports_%.go: ./scripts/gen-gljimports.sh ./cmd/gen-import-interop/main.go ./internal/genpkg/genpkg.go \
$(wildcard ./pkg/lang/*.go) $(wildcard ./pkg/runtime/*.go)
@echo "Generating $@"
@@ -56,7 +85,7 @@ vet:
@go vet ./...
.PHONY: $(TEST_TARGETS)
-$(TEST_TARGETS): gocmd
+$(TEST_TARGETS): gocmd $(GLJ)
@$(GO_CMD) run ./cmd/glj/main.go $(basename $@)
.PHONY: test
diff --git a/README.md b/README.md
index 80406fc7..44091a45 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,8 @@

-[Try it in your browser!](https://glojurelang.github.io/glojure/) (fair warning: startup on the web is slow)
+[Try it in your browser!](https://glojurelang.github.io/glojure/)
+(fair warning: startup on the web is slow)
@@ -51,14 +52,28 @@ user=>
## Usage
-Glojure can be used in two ways: as a standalone command-line tool (`glj`) or embedded within Go applications.
+Glojure can be used in two ways: as a standalone command-line tool (`glj`) or
+embedded within Go applications.
### Using the `glj` Command
The `glj` command provides a traditional Clojure development experience:
+**Show the help:**
+```
+$ glj --help # or glj -h
+```
+
+**Show the version:**
+```
+$ glj --version
+glojure v0.3.0
+```
+
**Start a REPL (interactive session):**
```
+user=> *glojure-version*
+{:major 0, :minor 3, :incremental 0, :qualifier nil}
$ glj
user=> (+ 1 2 3)
6
@@ -67,6 +82,18 @@ Hello from Glojure!
nil
```
+**Evaluate expressions:**
+```
+$ glj -e '(println "Hello, World!")'
+Hello, World!
+$ glj -e '(apply + (range 3 10))'
+42
+$ glj -e '
+(defn factorial [n] (if (<= n 1) 1 (* n (factorial (dec n)))))
+(factorial 5)'
+120
+```
+
**Run a Clojure script:**
```clojure
;; hello.glj
@@ -100,7 +127,8 @@ Server starting on :8080...
### Embedding Glojure in Go Applications
-You can also embed Glojure as a scripting language within your Go applications. This is useful when you want to:
+You can also embed Glojure as a scripting language within your Go applications.
+This is useful when you want to:
- Add scriptable configuration to your Go application
- Allow users to extend your application with Clojure plugins
- Mix Go's performance with Clojure's expressiveness
@@ -185,6 +213,7 @@ runtime.ReadEval(`
- Writing standalone Clojure programs
- Interactive development with the REPL
- Running Clojure scripts
+- Evaluating expressions directly from the command line
- Learning Clojure with Go interop
**Embed Glojure for:**
diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go
index 5f56197c..cd2c1ca0 100644
--- a/pkg/gljmain/gljmain.go
+++ b/pkg/gljmain/gljmain.go
@@ -2,8 +2,10 @@ package gljmain
import (
"bufio"
+ "fmt"
"log"
"os"
+ "strings"
// bootstrap the runtime
_ "github.com/glojurelang/glojure/pkg/glj"
@@ -14,20 +16,82 @@ import (
"github.com/glojurelang/glojure/pkg/runtime"
)
+func printHelp() {
+ fmt.Printf(`Glojure v%s
+
+Usage: glj [options] [file]
+
+Options:
+ -e Evaluate expression from command line
+ -h, --help Show this help message
+ --version Show version information
+
+Examples:
+ glj # Start REPL
+ glj -e "(+ 1 2)" # Evaluate expression
+ glj script.glj # Run script file
+ glj --version # Show version
+ glj --help # Show this help
+
+For more information, visit: https://github.com/glojurelang/glojure
+`, runtime.VERSION)
+}
+
func Main(args []string) {
runtime.AddLoadPath(os.DirFS("."))
if len(args) == 0 {
repl.Start()
+ } else if args[0] == "--version" {
+ fmt.Printf("glojure v%s\n", runtime.VERSION)
+ return
+ } else if args[0] == "--help" || args[0] == "-h" {
+ printHelp()
+ return
+ } else if args[0] == "-e" {
+ // 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()
+ }))
+ var lastResult interface{}
+ for {
+ val, err := rdr.ReadOne()
+ if err == reader.ErrEOF {
+ break
+ }
+ if err != nil {
+ log.Fatal(err)
+ }
+ result, err := env.Eval(val)
+ if err != nil {
+ log.Fatal(err)
+ }
+ lastResult = result
+ }
+ // Print only the final result unless it's nil
+ if !lang.IsNil(lastResult) {
+ fmt.Println(lang.PrintString(lastResult))
+ }
} else {
- file, err := os.Open(os.Args[1])
+ // Execute file
+ file, err := os.Open(args[0])
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(os.Args[2:]))
+ 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()
diff --git a/pkg/runtime/envinit.go b/pkg/runtime/envinit.go
index 94f41fa4..153114de 100644
--- a/pkg/runtime/envinit.go
+++ b/pkg/runtime/envinit.go
@@ -5,6 +5,7 @@ import (
"fmt"
"io"
"os"
+ "strconv"
"strings"
"github.com/glojurelang/glojure/pkg/lang"
@@ -12,6 +13,40 @@ import (
"github.com/glojurelang/glojure/pkg/stdlib"
)
+// The current version of Glojure
+const VERSION = "0.3.0"
+
+// ParseVersion parses the VERSION string and returns a map with major, minor,
+// incremental, and qualifier
+func ParseVersion(version string) lang.IPersistentMap {
+ parts := strings.Split(version, ".")
+
+ major, _ := strconv.Atoi(parts[0])
+ minor, _ := strconv.Atoi(parts[1])
+
+ incremental := 0
+ qualifier := interface{}(nil)
+
+ if len(parts) > 2 {
+ // Check if the third part contains a qualifier (e.g., "0-alpha")
+ incrementalPart := parts[2]
+ if strings.Contains(incrementalPart, "-") {
+ qualifierParts := strings.SplitN(incrementalPart, "-", 2)
+ incremental, _ = strconv.Atoi(qualifierParts[0])
+ qualifier = qualifierParts[1]
+ } else {
+ incremental, _ = strconv.Atoi(incrementalPart)
+ }
+ }
+
+ return lang.NewMap(
+ lang.NewKeyword("major"), major,
+ lang.NewKeyword("minor"), minor,
+ lang.NewKeyword("incremental"), incremental,
+ lang.NewKeyword("qualifier"), qualifier,
+ )
+}
+
type Program struct {
nodes []interface{}
}
@@ -113,6 +148,13 @@ func NewEnvironment(opts ...EvalOption) lang.Environment {
evalFile("glojure/core.glj")
}
+ // Set the glojure version
+ core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
+ versionVar := core.FindInternedVar(lang.NewSymbol("*glojure-version*"))
+ if versionVar != nil {
+ versionVar.BindRoot(ParseVersion(VERSION))
+ }
+
return env
}
diff --git a/pkg/runtime/environment.go b/pkg/runtime/environment.go
index 7d858019..f79cc29c 100644
--- a/pkg/runtime/environment.go
+++ b/pkg/runtime/environment.go
@@ -63,6 +63,7 @@ func newEnvironment(ctx context.Context, stdout, stderr io.Writer) *environment
"print-meta",
"print-dup",
"read-eval",
+ "glojure-version",
} {
coreNS.InternWithValue(lang.NewSymbol("*"+dyn+"*"), nil, true).SetDynamic()
}
diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj
new file mode 100644
index 00000000..e6d78300
--- /dev/null
+++ b/test/glojure/test_glojure/cli.glj
@@ -0,0 +1,77 @@
+(ns glojure.test-glojure.cli
+ (:use glojure.test)
+ (:require [glojure.string :as str]))
+
+(defmacro #^{:private true} test-that
+ "Provides a useful way for specifying the purpose of tests. If the first-level
+ forms are lists that make a call to a glojure.test function, it supplies the
+ purpose as the msg argument to those functions. Otherwise, the purpose just
+ acts like a comment and the forms are run unchanged."
+ [purpose & test-forms]
+ (let [tests (map
+ #(if (= (:ns (meta (resolve (first %))))
+ (the-ns 'glojure.test))
+ (concat % (list purpose))
+ %)
+ test-forms)]
+ `(do ~@tests)))
+
+(defn run-cli-cmd [& args]
+ (let [bytes-to-string (fn [bytes]
+ (if (nil? bytes)
+ ""
+ (apply str (map char (seq bytes)))))
+ cmd (apply os$exec.Command args)
+ [output err] (.CombinedOutput cmd)]
+ [(bytes-to-string output) (bytes-to-string err)]))
+
+(def glj
+ (let [[out err] (run-cli-cmd "find" "bin" "-name" "glj" "-executable")]
+ (if (and (seq out) (empty? err))
+ (first (str/split-lines out))
+ (throw (Exception. (str "Failed to find glj bin: " err))))))
+
+(deftest e-flag-test
+ (test-that
+ "glj -e flag works correctly"
+ (let [[out err] (run-cli-cmd glj "-e" "(* 6 7)")]
+ (is (= out "42\n") "Command should output 42")
+ (is (empty? err) "Command should not return an error"))))
+
+(deftest version-flag-test
+ (test-that
+ "glj --version flag works correctly"
+ (let [[out err] (run-cli-cmd glj "--version")]
+ (is (re-matches #"glojure v\d+\.\d+\.\d+\n" out)
+ "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
+ (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")
+ (is (empty? err) "Command should not return an error"))))
+
+(run-tests)