Skip to content

Commit a2bebb9

Browse files
authored
Add command line options for glj -e --version --help -h (#88)
* Support 'glj -e ...' commands * Add *glojure-version* dynamic variable * Support 'glj --version' * Support --help and -h CLI options * Fix 'make test', add 'make build', 'make clean' The CLI tests require 'bin/<os-arch>/glc' binary to be built in order to run 'glj ...' commands and test the output.
1 parent 96bd617 commit a2bebb9

File tree

6 files changed

+248
-6
lines changed

6 files changed

+248
-6
lines changed

Makefile

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,28 @@ STDLIB := $(STDLIB_ORIGINALS:scripts/rewrite-core/originals/%=%)
66
STDLIB_ORIGINALS := $(addprefix scripts/rewrite-core/originals/,$(STDLIB))
77
STDLIB_TARGETS := $(addprefix pkg/stdlib/glojure/,$(STDLIB:.clj=.glj))
88

9+
OS-TYPE := $(shell bash -c 'echo $$OSTYPE')
10+
OS-NAME := \
11+
$(if $(findstring darwin,$(OS-TYPE))\
12+
,macos,$(if $(findstring linux,$(OS-TYPE)),linux,))
13+
ARCH-TYPE := $(shell bash -c 'echo $$MACHTYPE')
14+
ARCH-NAME := \
15+
$(if $(or $(findstring arm64,$(ARCH-TYPE)),\
16+
$(findstring aarch64,$(ARCH-TYPE)))\
17+
,arm64,$(if $(findstring x86_64,$(ARCH-TYPE)),int64,))
18+
19+
ifdef OS-NAME
20+
ifdef ARCH-NAME
21+
OS-ARCH := $(OS-NAME)-$(ARCH-NAME)
22+
OA-linux-arm64 := linux_arm64
23+
OA-linux-int64 := linux_amd64
24+
OA-macos-arm64 := darwin_arm64
25+
OA-macos-int64 := darwin_amd64
26+
OA := $(OA-$(OS-ARCH))
27+
GLJ := bin/$(OA)/glj
28+
endif
29+
endif
30+
931
TEST_FILES := $(shell find ./test -name '*.glj' | sort)
1032
TEST_TARGETS := $(addsuffix .test,$(TEST_FILES))
1133

@@ -31,6 +53,13 @@ gocmd:
3153
generate:
3254
@go generate ./...
3355

56+
.PHONY: build
57+
build: $(GLJ)
58+
59+
.PHONY: clean
60+
clean:
61+
$(RM) -r bin/
62+
3463
pkg/gen/gljimports/gljimports_%.go: ./scripts/gen-gljimports.sh ./cmd/gen-import-interop/main.go ./internal/genpkg/genpkg.go \
3564
$(wildcard ./pkg/lang/*.go) $(wildcard ./pkg/runtime/*.go)
3665
@echo "Generating $@"
@@ -56,7 +85,7 @@ vet:
5685
@go vet ./...
5786

5887
.PHONY: $(TEST_TARGETS)
59-
$(TEST_TARGETS): gocmd
88+
$(TEST_TARGETS): gocmd $(GLJ)
6089
@$(GO_CMD) run ./cmd/glj/main.go $(basename $@)
6190

6291
.PHONY: test

README.md

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,8 @@
22

33
![example workflow](https://github.com/glojurelang/glojure/actions/workflows/ci.yml/badge.svg)
44

5-
[Try it in your browser!](https://glojurelang.github.io/glojure/) (fair warning: startup on the web is slow)
5+
[Try it in your browser!](https://glojurelang.github.io/glojure/)
6+
(fair warning: startup on the web is slow)
67

78
<img alt="Gopher image" src="./doc/logo.png" width="512" />
89

@@ -51,14 +52,28 @@ user=>
5152

5253
## Usage
5354

54-
Glojure can be used in two ways: as a standalone command-line tool (`glj`) or embedded within Go applications.
55+
Glojure can be used in two ways: as a standalone command-line tool (`glj`) or
56+
embedded within Go applications.
5557

5658
### Using the `glj` Command
5759

5860
The `glj` command provides a traditional Clojure development experience:
5961

62+
**Show the help:**
63+
```
64+
$ glj --help # or glj -h
65+
```
66+
67+
**Show the version:**
68+
```
69+
$ glj --version
70+
glojure v0.3.0
71+
```
72+
6073
**Start a REPL (interactive session):**
6174
```
75+
user=> *glojure-version*
76+
{:major 0, :minor 3, :incremental 0, :qualifier nil}
6277
$ glj
6378
user=> (+ 1 2 3)
6479
6
@@ -67,6 +82,18 @@ Hello from Glojure!
6782
nil
6883
```
6984

85+
**Evaluate expressions:**
86+
```
87+
$ glj -e '(println "Hello, World!")'
88+
Hello, World!
89+
$ glj -e '(apply + (range 3 10))'
90+
42
91+
$ glj -e '
92+
(defn factorial [n] (if (<= n 1) 1 (* n (factorial (dec n)))))
93+
(factorial 5)'
94+
120
95+
```
96+
7097
**Run a Clojure script:**
7198
```clojure
7299
;; hello.glj
@@ -100,7 +127,8 @@ Server starting on :8080...
100127

101128
### Embedding Glojure in Go Applications
102129

103-
You can also embed Glojure as a scripting language within your Go applications. This is useful when you want to:
130+
You can also embed Glojure as a scripting language within your Go applications.
131+
This is useful when you want to:
104132
- Add scriptable configuration to your Go application
105133
- Allow users to extend your application with Clojure plugins
106134
- Mix Go's performance with Clojure's expressiveness
@@ -185,6 +213,7 @@ runtime.ReadEval(`
185213
- Writing standalone Clojure programs
186214
- Interactive development with the REPL
187215
- Running Clojure scripts
216+
- Evaluating expressions directly from the command line
188217
- Learning Clojure with Go interop
189218

190219
**Embed Glojure for:**

pkg/gljmain/gljmain.go

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package gljmain
22

33
import (
44
"bufio"
5+
"fmt"
56
"log"
67
"os"
8+
"strings"
79

810
// bootstrap the runtime
911
_ "github.com/glojurelang/glojure/pkg/glj"
@@ -14,20 +16,82 @@ import (
1416
"github.com/glojurelang/glojure/pkg/runtime"
1517
)
1618

19+
func printHelp() {
20+
fmt.Printf(`Glojure v%s
21+
22+
Usage: glj [options] [file]
23+
24+
Options:
25+
-e <expr> Evaluate expression from command line
26+
-h, --help Show this help message
27+
--version Show version information
28+
29+
Examples:
30+
glj # Start REPL
31+
glj -e "(+ 1 2)" # Evaluate expression
32+
glj script.glj # Run script file
33+
glj --version # Show version
34+
glj --help # Show this help
35+
36+
For more information, visit: https://github.com/glojurelang/glojure
37+
`, runtime.VERSION)
38+
}
39+
1740
func Main(args []string) {
1841
runtime.AddLoadPath(os.DirFS("."))
1942

2043
if len(args) == 0 {
2144
repl.Start()
45+
} else if args[0] == "--version" {
46+
fmt.Printf("glojure v%s\n", runtime.VERSION)
47+
return
48+
} else if args[0] == "--help" || args[0] == "-h" {
49+
printHelp()
50+
return
51+
} else if args[0] == "-e" {
52+
// Evaluate expression from command line
53+
if len(args) < 2 {
54+
log.Fatal("glj: -e requires an expression")
55+
}
56+
expr := args[1]
57+
env := lang.GlobalEnv
58+
59+
// Set command line args (everything after -e and the expression)
60+
core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
61+
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[2:]))
62+
63+
rdr := reader.New(strings.NewReader(expr), reader.WithGetCurrentNS(func() *lang.Namespace {
64+
return env.CurrentNamespace()
65+
}))
66+
var lastResult interface{}
67+
for {
68+
val, err := rdr.ReadOne()
69+
if err == reader.ErrEOF {
70+
break
71+
}
72+
if err != nil {
73+
log.Fatal(err)
74+
}
75+
result, err := env.Eval(val)
76+
if err != nil {
77+
log.Fatal(err)
78+
}
79+
lastResult = result
80+
}
81+
// Print only the final result unless it's nil
82+
if !lang.IsNil(lastResult) {
83+
fmt.Println(lang.PrintString(lastResult))
84+
}
2285
} else {
23-
file, err := os.Open(os.Args[1])
86+
// Execute file
87+
file, err := os.Open(args[0])
2488
if err != nil {
2589
log.Fatal(err)
2690
}
2791
env := lang.GlobalEnv
2892

2993
core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
30-
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(os.Args[2:]))
94+
core.FindInternedVar(lang.NewSymbol("*command-line-args*")).BindRoot(lang.Seq(args[1:]))
3195

3296
rdr := reader.New(bufio.NewReader(file), reader.WithGetCurrentNS(func() *lang.Namespace {
3397
return env.CurrentNamespace()

pkg/runtime/envinit.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,48 @@ import (
55
"fmt"
66
"io"
77
"os"
8+
"strconv"
89
"strings"
910

1011
"github.com/glojurelang/glojure/pkg/lang"
1112
"github.com/glojurelang/glojure/pkg/reader"
1213
"github.com/glojurelang/glojure/pkg/stdlib"
1314
)
1415

16+
// The current version of Glojure
17+
const VERSION = "0.3.0"
18+
19+
// ParseVersion parses the VERSION string and returns a map with major, minor,
20+
// incremental, and qualifier
21+
func ParseVersion(version string) lang.IPersistentMap {
22+
parts := strings.Split(version, ".")
23+
24+
major, _ := strconv.Atoi(parts[0])
25+
minor, _ := strconv.Atoi(parts[1])
26+
27+
incremental := 0
28+
qualifier := interface{}(nil)
29+
30+
if len(parts) > 2 {
31+
// Check if the third part contains a qualifier (e.g., "0-alpha")
32+
incrementalPart := parts[2]
33+
if strings.Contains(incrementalPart, "-") {
34+
qualifierParts := strings.SplitN(incrementalPart, "-", 2)
35+
incremental, _ = strconv.Atoi(qualifierParts[0])
36+
qualifier = qualifierParts[1]
37+
} else {
38+
incremental, _ = strconv.Atoi(incrementalPart)
39+
}
40+
}
41+
42+
return lang.NewMap(
43+
lang.NewKeyword("major"), major,
44+
lang.NewKeyword("minor"), minor,
45+
lang.NewKeyword("incremental"), incremental,
46+
lang.NewKeyword("qualifier"), qualifier,
47+
)
48+
}
49+
1550
type Program struct {
1651
nodes []interface{}
1752
}
@@ -113,6 +148,13 @@ func NewEnvironment(opts ...EvalOption) lang.Environment {
113148
evalFile("glojure/core.glj")
114149
}
115150

151+
// Set the glojure version
152+
core := lang.FindNamespace(lang.NewSymbol("glojure.core"))
153+
versionVar := core.FindInternedVar(lang.NewSymbol("*glojure-version*"))
154+
if versionVar != nil {
155+
versionVar.BindRoot(ParseVersion(VERSION))
156+
}
157+
116158
return env
117159
}
118160

pkg/runtime/environment.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ func newEnvironment(ctx context.Context, stdout, stderr io.Writer) *environment
6363
"print-meta",
6464
"print-dup",
6565
"read-eval",
66+
"glojure-version",
6667
} {
6768
coreNS.InternWithValue(lang.NewSymbol("*"+dyn+"*"), nil, true).SetDynamic()
6869
}

test/glojure/test_glojure/cli.glj

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
(ns glojure.test-glojure.cli
2+
(:use glojure.test)
3+
(:require [glojure.string :as str]))
4+
5+
(defmacro #^{:private true} test-that
6+
"Provides a useful way for specifying the purpose of tests. If the first-level
7+
forms are lists that make a call to a glojure.test function, it supplies the
8+
purpose as the msg argument to those functions. Otherwise, the purpose just
9+
acts like a comment and the forms are run unchanged."
10+
[purpose & test-forms]
11+
(let [tests (map
12+
#(if (= (:ns (meta (resolve (first %))))
13+
(the-ns 'glojure.test))
14+
(concat % (list purpose))
15+
%)
16+
test-forms)]
17+
`(do ~@tests)))
18+
19+
(defn run-cli-cmd [& args]
20+
(let [bytes-to-string (fn [bytes]
21+
(if (nil? bytes)
22+
""
23+
(apply str (map char (seq bytes)))))
24+
cmd (apply os$exec.Command args)
25+
[output err] (.CombinedOutput cmd)]
26+
[(bytes-to-string output) (bytes-to-string err)]))
27+
28+
(def glj
29+
(let [[out err] (run-cli-cmd "find" "bin" "-name" "glj" "-executable")]
30+
(if (and (seq out) (empty? err))
31+
(first (str/split-lines out))
32+
(throw (Exception. (str "Failed to find glj bin: " err))))))
33+
34+
(deftest e-flag-test
35+
(test-that
36+
"glj -e flag works correctly"
37+
(let [[out err] (run-cli-cmd glj "-e" "(* 6 7)")]
38+
(is (= out "42\n") "Command should output 42")
39+
(is (empty? err) "Command should not return an error"))))
40+
41+
(deftest version-flag-test
42+
(test-that
43+
"glj --version flag works correctly"
44+
(let [[out err] (run-cli-cmd glj "--version")]
45+
(is (re-matches #"glojure v\d+\.\d+\.\d+\n" out)
46+
"Command should output version")
47+
(is (empty? err) "Command should not return an error"))))
48+
49+
(deftest glojure-version-test
50+
(test-that
51+
"*glojure-version* should be set correctly"
52+
(let [[out err] (run-cli-cmd glj "-e" "*glojure-version*")]
53+
(is (= out "{:major 0, :minor 3, :incremental 0, :qualifier nil}\n")
54+
"Version should match expected format")
55+
(is (empty? err) "Command should not return an error"))))
56+
57+
(deftest help-flag-test
58+
(test-that
59+
"glj --help flag works correctly"
60+
(let [[out err] (run-cli-cmd glj "--help")]
61+
(is (re-matches
62+
#"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*"
63+
out)
64+
"Command should output help information")
65+
(is (empty? err) "Command should not return an error"))))
66+
67+
(deftest short-help-flag-test
68+
(test-that
69+
"glj -h flag works correctly"
70+
(let [[out err] (run-cli-cmd glj "-h")]
71+
(is (re-matches
72+
#"(?s).*Glojure v0\.3\.0.*Usage: glj.*Options:.*-e.*-h.*--help.*--version.*Examples:.*"
73+
out)
74+
"Command should output help information")
75+
(is (empty? err) "Command should not return an error"))))
76+
77+
(run-tests)

0 commit comments

Comments
 (0)