A structured-data shell in Gleam, inspired by Nushell
1import gleam/string
2import gleeunit
3import gleshell/color
4import gleshell/display
5import gleshell/env
6import gleshell/eval
7import gleshell/lexer
8import gleshell/parser
9import gleshell/value.{Bool, Int, List, Nothing, Record, String, Table}
10
11pub fn main() -> Nil {
12 gleeunit.main()
13}
14
15// --- lexer ---
16
17pub fn lexer_pipeline_test() {
18 let assert Ok(tokens) = lexer.tokenize("ls | where type == file")
19 let assert [lexer.Ident("ls"), lexer.Pipe, lexer.Ident("where"), ..] = tokens
20 Nil
21}
22
23pub fn lexer_string_and_number_test() {
24 let assert Ok(tokens) = lexer.tokenize("echo \"hi\" 42 3.14 true")
25 let assert [
26 lexer.Ident("echo"),
27 lexer.StringLit("hi"),
28 lexer.IntLit(42),
29 lexer.FloatLit(3.14),
30 lexer.BoolLit(True),
31 lexer.Eof,
32 ] = tokens
33 Nil
34}
35
36// --- parser ---
37
38pub fn parse_pipeline_test() {
39 let assert Ok(parser.Expr(parser.Pipeline(cmds))) =
40 parser.parse("ls | first 3")
41 let assert [
42 parser.Command("ls", [], False),
43 parser.Command("first", args, False),
44 ] = cmds
45 let assert [parser.ValueArg(parser.Lit(Int(3)))] = args
46 Nil
47}
48
49pub fn parse_let_test() {
50 let assert Ok(parser.Let(
51 "x",
52 parser.Pipeline([parser.Command("echo", args, False)]),
53 )) = parser.parse("let x = echo 1")
54 let assert [parser.ValueArg(parser.Lit(Int(1)))] = args
55 Nil
56}
57
58pub fn parse_where_ops_test() {
59 let assert Ok(parser.Expr(parser.Pipeline([
60 parser.Command("where", args, False),
61 ]))) = parser.parse("where type == file")
62 let assert [
63 parser.ValueArg(parser.Lit(String("type"))),
64 parser.ValueArg(parser.Lit(String("=="))),
65 parser.ValueArg(parser.Lit(String("file"))),
66 ] = args
67 Nil
68}
69
70pub fn parse_list_and_record_test() {
71 let assert Ok(parser.Expr(parser.Pipeline([
72 parser.Command("echo", args, False),
73 ]))) = parser.parse("echo [1 2] {a: true}")
74 let assert [
75 parser.ValueArg(parser.ListExpr([parser.Lit(Int(1)), parser.Lit(Int(2))])),
76 parser.ValueArg(parser.RecordExpr([#("a", parser.Lit(Bool(True)))])),
77 ] = args
78 Nil
79}
80
81// --- value helpers ---
82
83pub fn table_from_records_test() {
84 let rows = [
85 Record([#("name", String("a")), #("n", Int(1))]),
86 Record([#("name", String("b")), #("n", Int(2))]),
87 ]
88 let assert Table(
89 ["name", "n"],
90 [[String("a"), Int(1)], [String("b"), Int(2)]],
91 ) = value.table_from_records(rows)
92 Nil
93}
94
95// --- eval ---
96
97pub fn eval_echo_and_range_test() {
98 let env = env.new()
99 let assert eval.Continue(_, List([Int(0), Int(1), Int(2)])) =
100 eval.eval_source(env, "range 3")
101 let assert eval.Continue(_, String("hello")) =
102 eval.eval_source(env, "echo hello")
103 Nil
104}
105
106pub fn eval_pipeline_reverse_first_test() {
107 let env = env.new()
108 let assert eval.Continue(_, Int(2)) =
109 eval.eval_source(env, "range 3 | reverse | first")
110 Nil
111}
112
113pub fn eval_let_and_var_test() {
114 let env = env.new()
115 let assert eval.Continue(env2, Int(7)) =
116 eval.eval_source(env, "let n = echo 7")
117 let assert eval.Continue(_, Int(7)) = eval.eval_source(env2, "echo $n")
118 Nil
119}
120
121pub fn eval_where_select_test() {
122 let env = env.new()
123 // build table via records in a list, convert with table
124 let assert eval.Continue(_, result) =
125 eval.eval_source(
126 env,
127 "echo [{name: a, n: 1} {name: b, n: 2} {name: c, n: 3}] | table | where n > 1 | select name",
128 )
129 let assert Table(["name"], rows) = result
130 let assert [[String("b")], [String("c")]] = rows
131 Nil
132}
133
134pub fn eval_from_json_test() {
135 let env = env.new()
136 let assert eval.Continue(_, Record(fields)) =
137 eval.eval_source(env, "echo \"{\\\"x\\\": 1}\" | from json")
138 let assert True = list_has_field(fields, "x", Int(1))
139 Nil
140}
141
142pub fn eval_to_json_pretty_test() {
143 let env = env.new()
144 // Nushell-style multi-word `to json` — pretty by default
145 let assert eval.Continue(_, String(pretty)) =
146 eval.eval_source(env, "echo [1 2 3] | to json")
147 let assert True = string.contains(pretty, "\n")
148 let assert True = string.contains(pretty, "1")
149 // `--raw` matches Nu: compact, no trailing newline
150 let assert eval.Continue(_, String(raw)) =
151 eval.eval_source(env, "echo [1 2 3] | to json --raw")
152 let assert "[1,2,3]" = raw
153 Nil
154}
155
156pub fn eval_to_json_record_test() {
157 let env = env.new()
158 let assert eval.Continue(_, String(raw)) =
159 eval.eval_source(env, "echo {a: 1, b: true} | to json -r")
160 let assert True = string.contains(raw, "\"a\":1")
161 let assert True = string.contains(raw, "\"b\":true")
162 Nil
163}
164
165fn list_has_field(
166 fields: List(#(String, value.Value)),
167 key: String,
168 expected: value.Value,
169) -> Bool {
170 case fields {
171 [] -> False
172 [#(k, v), ..rest] ->
173 case k == key {
174 True -> value.equals(v, expected)
175 False -> list_has_field(rest, key, expected)
176 }
177 }
178}
179
180pub fn eval_length_test() {
181 let env = env.new()
182 let assert eval.Continue(_, Int(4)) =
183 eval.eval_source(env, "range 4 | length")
184 Nil
185}
186
187pub fn value_nothing_falsey_test() {
188 let assert False = value.is_truthy(Nothing)
189 let assert True = value.is_truthy(Int(1))
190 Nil
191}
192
193// --- display / color ---
194
195pub fn display_plain_no_ansi_test() {
196 let text = display.render_with(False, Bool(True))
197 let assert "true" = text
198 let assert False = string_contains(text, "\u{001b}")
199 Nil
200}
201
202pub fn display_colored_has_ansi_test() {
203 let text = display.render_with(True, Bool(True))
204 let assert True = string_contains(text, "\u{001b}")
205 let assert True = string_contains(text, "true")
206 Nil
207}
208
209pub fn display_table_headers_colored_test() {
210 let text =
211 display.render_with(
212 True,
213 Table(["name", "type"], [[String("src"), String("dir")]]),
214 )
215 // bold green header + bright-blue dir name
216 let assert True = string_contains(text, "\u{001b}[1;32m")
217 let assert True = string_contains(text, "\u{001b}[94m")
218 let assert True = string_contains(text, "src")
219 Nil
220}
221
222pub fn color_visible_length_strips_ansi_test() {
223 let painted = color.paint(True, "\u{001b}[32m", "hi")
224 let assert 2 = color.visible_length(painted)
225 let assert 2 = color.visible_length("hi")
226 Nil
227}
228
229fn string_contains(haystack: String, needle: String) -> Bool {
230 case string.split(haystack, needle) {
231 [_] -> False
232 _ -> True
233 }
234}