From e0fd9cab8ffdaada633503aee7b271b6aa3fb946 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 8 Aug 2025 14:07:18 +0000 Subject: [PATCH 1/5] Support 'glj -e ...' commands --- README.md | 22 ++++++++++++++--- pkg/gljmain/gljmain.go | 41 +++++++++++++++++++++++++++++-- test/glojure/test_glojure/cli.glj | 41 +++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 5 deletions(-) create mode 100644 test/glojure/test_glojure/cli.glj diff --git a/README.md b/README.md index 80406fc7..4d2c3184 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ ![example workflow](https://github.com/glojurelang/glojure/actions/workflows/ci.yml/badge.svg) -[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) Gopher image @@ -51,7 +52,8 @@ 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 @@ -67,6 +69,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 +114,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 +200,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..63c43ed4 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" @@ -19,15 +21,50 @@ func Main(args []string) { if len(args) == 0 { repl.Start() + } 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/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj new file mode 100644 index 00000000..2131ecfe --- /dev/null +++ b/test/glojure/test_glojure/cli.glj @@ -0,0 +1,41 @@ +(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] + (apply str (map char (seq bytes)))) + cmd (apply os$exec.Command args) + [output err] (.CombinedOutput cmd)] + [(bytes-to-string output) (bytes-to-string err)])) + +(defn find-glj-bin [] + (let [[out err] (run-cli-cmd "find" "bin" "-name" "glj")] + (if (and (seq out) (empty? err)) + (first (str/split-lines out)) + (throw (Exception. (str "Failed to find glj bin: " err)))))) + +(def glj (find-glj-bin)) + +(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")))) + +(run-tests) From f2a43354ca73dd1144c0dfd729ac42bd4c50d59a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 8 Aug 2025 17:32:37 +0000 Subject: [PATCH 2/5] Add *glojure-version* dynamic variable --- README.md | 2 ++ pkg/runtime/envinit.go | 42 +++++++++++++++++++++++++++++++ pkg/runtime/environment.go | 1 + test/glojure/test_glojure/cli.glj | 18 +++++++++---- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4d2c3184..40711784 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,8 @@ The `glj` command provides a traditional Clojure development experience: **Start a REPL (interactive session):** ``` +user=> *glojure-version* +{:major 0, :minor 3, :incremental 0, :qualifier nil} $ glj user=> (+ 1 2 3) 6 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 index 2131ecfe..d2e7528b 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -18,19 +18,19 @@ (defn run-cli-cmd [& args] (let [bytes-to-string (fn [bytes] - (apply str (map char (seq 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)])) -(defn find-glj-bin [] - (let [[out err] (run-cli-cmd "find" "bin" "-name" "glj")] +(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)))))) -(def glj (find-glj-bin)) - (deftest e-flag-test (test-that "glj -e flag works correctly" @@ -38,4 +38,12 @@ (is (= out "42\n") "Command should output 42") (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")))) + (run-tests) From 8c17b4c16e463b0147375b99b57ea77ab3db4234 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 8 Aug 2025 11:59:07 -0700 Subject: [PATCH 3/5] Support 'glj --version' --- README.md | 6 ++++++ pkg/gljmain/gljmain.go | 3 +++ test/glojure/test_glojure/cli.glj | 8 ++++++++ 3 files changed, 17 insertions(+) diff --git a/README.md b/README.md index 40711784..8bd87e36 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,12 @@ embedded within Go applications. The `glj` command provides a traditional Clojure development experience: +**Show the version:** +``` +$ glj --version +glojure v0.3.0 +``` + **Start a REPL (interactive session):** ``` user=> *glojure-version* diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index 63c43ed4..c4cf594f 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -21,6 +21,9 @@ func Main(args []string) { if len(args) == 0 { repl.Start() + } else if args[0] == "--version" { + fmt.Printf("glojure v%s\n", runtime.VERSION) + return } else if args[0] == "-e" { // Evaluate expression from command line if len(args) < 2 { diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index d2e7528b..0a2735c7 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -38,6 +38,14 @@ (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" From 8a1ff0f957ec5509bf1abeb855a70aeecd77ac36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 8 Aug 2025 20:44:08 +0000 Subject: [PATCH 4/5] Support --help and -h CLI options --- README.md | 5 +++++ pkg/gljmain/gljmain.go | 24 ++++++++++++++++++++++++ test/glojure/test_glojure/cli.glj | 30 +++++++++++++++++++++++++----- 3 files changed, 54 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 8bd87e36..44091a45 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,11 @@ embedded within Go applications. The `glj` command provides a traditional Clojure development experience: +**Show the help:** +``` +$ glj --help # or glj -h +``` + **Show the version:** ``` $ glj --version diff --git a/pkg/gljmain/gljmain.go b/pkg/gljmain/gljmain.go index c4cf594f..cd2c1ca0 100644 --- a/pkg/gljmain/gljmain.go +++ b/pkg/gljmain/gljmain.go @@ -16,6 +16,27 @@ 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(".")) @@ -24,6 +45,9 @@ func Main(args []string) { } 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 { diff --git a/test/glojure/test_glojure/cli.glj b/test/glojure/test_glojure/cli.glj index 0a2735c7..e6d78300 100644 --- a/test/glojure/test_glojure/cli.glj +++ b/test/glojure/test_glojure/cli.glj @@ -9,11 +9,11 @@ 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)] + #(if (= (:ns (meta (resolve (first %)))) + (the-ns 'glojure.test)) + (concat % (list purpose)) + %) + test-forms)] `(do ~@tests))) (defn run-cli-cmd [& args] @@ -54,4 +54,24 @@ "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) From d18fcaf1ec11946aacd044f89905ad61af66b94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ingy=20d=C3=B6t=20Net?= Date: Fri, 8 Aug 2025 21:09:45 +0000 Subject: [PATCH 5/5] Fix 'make test', add 'make build', 'make clean' The CLI tests require 'bin//glc' binary to be built in order to run 'glj ...' commands and test the output. --- Makefile | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) 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