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 @@ ![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,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)