srsh 0.8.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +12 -3
  3. data/README.md +446 -8
  4. data/bin/srsh +71 -0
  5. data/docs/assets/slut.txt +4 -0
  6. data/docs/assets/srsh-mark.svg +12 -0
  7. data/docs/css/style.css +696 -0
  8. data/docs/index.html +703 -0
  9. data/docs/js/app.js +203 -0
  10. data/examples/bridge.rsh +8 -0
  11. data/examples/calculator.rsh +253 -0
  12. data/examples/defer.rsh +14 -0
  13. data/examples/hot.rsh +14 -0
  14. data/examples/meta.rsh +20 -0
  15. data/examples/modules/text.rsh +6 -0
  16. data/examples/modules.rsh +6 -0
  17. data/examples/paste.rsh +15 -0
  18. data/examples/plugin.rb +8 -0
  19. data/examples/power.rsh +65 -0
  20. data/examples/tour.rsh +38 -0
  21. data/ext/srsh_native/extconf.rb +3 -0
  22. data/ext/srsh_native/srsh_native.c +48 -0
  23. data/language-docs/LANGUAGE.md +670 -0
  24. data/language-docs/MIGRATION.md +44 -0
  25. data/language-docs/SECURITY.md +44 -0
  26. data/lib/srsh/app.rb +261 -0
  27. data/lib/srsh/builtins.rb +492 -0
  28. data/lib/srsh/editor.rb +530 -0
  29. data/lib/srsh/errors.rb +23 -0
  30. data/lib/srsh/history.rb +74 -0
  31. data/lib/srsh/language/evaluator.rb +1175 -0
  32. data/lib/srsh/language/lexer.rb +316 -0
  33. data/lib/srsh/language/parser.rb +997 -0
  34. data/lib/srsh/language/token.rb +5 -0
  35. data/lib/srsh/language/values.rb +392 -0
  36. data/lib/srsh/paths.rb +29 -0
  37. data/lib/srsh/plugins.rb +59 -0
  38. data/lib/srsh/process_identity.rb +38 -0
  39. data/lib/srsh/security.rb +38 -0
  40. data/lib/srsh/shell/executor.rb +1182 -0
  41. data/lib/srsh/shell/job.rb +101 -0
  42. data/lib/srsh/shell/lexer.rb +114 -0
  43. data/lib/srsh/shell/terminal.rb +26 -0
  44. data/lib/srsh/state.rb +136 -0
  45. data/lib/srsh/theme.rb +108 -0
  46. data/lib/srsh/version.rb +3 -0
  47. data/lib/srsh.rb +11 -5
  48. metadata +61 -14
  49. data/exe/srsh +0 -6
  50. data/lib/srsh/runner.rb +0 -2416
data/docs/js/app.js ADDED
@@ -0,0 +1,203 @@
1
+ (function () {
2
+ "use strict";
3
+
4
+ const repository = "https://github.com/RobertFlexx/RSH";
5
+ const api = "https://api.github.com/repos/RobertFlexx/RSH/releases/latest";
6
+ const pages = new Map();
7
+
8
+ document.querySelectorAll("[data-page]").forEach(function (page) {
9
+ pages.set(page.getAttribute("data-page"), page);
10
+ });
11
+
12
+ function routeFromHash() {
13
+ const raw = (window.location.hash || "#/").slice(1);
14
+ const pieces = raw.split("#");
15
+ const path = (pieces[0] || "/").replace(/^\//, "");
16
+ return {
17
+ page: path || "home",
18
+ anchor: pieces[1] || ""
19
+ };
20
+ }
21
+
22
+ function showRoute() {
23
+ const route = routeFromHash();
24
+ const pageName = pages.has(route.page) ? route.page : "home";
25
+
26
+ pages.forEach(function (page, name) {
27
+ page.classList.toggle("active", name === pageName);
28
+ });
29
+
30
+ document.querySelectorAll("[data-route]").forEach(function (link) {
31
+ const current = link.getAttribute("data-route") === pageName;
32
+ link.classList.toggle("current", current);
33
+ if (current) link.setAttribute("aria-current", "page");
34
+ else link.removeAttribute("aria-current");
35
+ });
36
+
37
+ const titles = {
38
+ home: "SRSH 1.0: Simple Ruby Shell",
39
+ manual: "RSH language manual | SRSH 1.0",
40
+ download: "Download SRSH 1.0",
41
+ examples: "RSH examples | SRSH 1.0",
42
+ project: "Project information | SRSH 1.0"
43
+ };
44
+ document.title = titles[pageName] || titles.home;
45
+
46
+ document.querySelectorAll(".toc a").forEach(function (link) {
47
+ link.classList.toggle("current", Boolean(route.anchor) && link.getAttribute("href").endsWith("#" + route.anchor));
48
+ });
49
+
50
+ if (route.anchor) {
51
+ window.setTimeout(function () {
52
+ const target = document.getElementById(route.anchor);
53
+ if (target) target.scrollIntoView({ block: "start" });
54
+ }, 0);
55
+ } else {
56
+ window.scrollTo(0, 0);
57
+ }
58
+ }
59
+
60
+ window.addEventListener("hashchange", showRoute);
61
+ showRoute();
62
+
63
+ const notice = document.querySelector("[data-copy-notice]");
64
+ let noticeTimer;
65
+
66
+ function showNotice(message) {
67
+ if (!notice) return;
68
+ notice.textContent = message;
69
+ notice.hidden = false;
70
+ window.clearTimeout(noticeTimer);
71
+ noticeTimer = window.setTimeout(function () {
72
+ notice.hidden = true;
73
+ }, 1400);
74
+ }
75
+
76
+ function fallbackCopy(text) {
77
+ const field = document.createElement("textarea");
78
+ field.value = text;
79
+ field.setAttribute("readonly", "");
80
+ field.style.position = "fixed";
81
+ field.style.left = "-10000px";
82
+ document.body.appendChild(field);
83
+ field.select();
84
+
85
+ try {
86
+ document.execCommand("copy");
87
+ showNotice("Copied.");
88
+ } catch (error) {
89
+ showNotice("Copy failed.");
90
+ }
91
+
92
+ field.remove();
93
+ }
94
+
95
+ document.addEventListener("click", function (event) {
96
+ const button = event.target.closest("[data-copy-target]");
97
+ if (!button) return;
98
+
99
+ const target = document.getElementById(button.getAttribute("data-copy-target"));
100
+ if (!target) return;
101
+ const text = target.textContent.replace(/\n$/, "");
102
+
103
+ if (navigator.clipboard && window.isSecureContext) {
104
+ navigator.clipboard.writeText(text).then(function () {
105
+ showNotice("Copied.");
106
+ }).catch(function () {
107
+ fallbackCopy(text);
108
+ });
109
+ } else {
110
+ fallbackCopy(text);
111
+ }
112
+ });
113
+
114
+ const searchForm = document.querySelector("[data-doc-search]");
115
+ const searchResults = document.querySelector("[data-search-results]");
116
+
117
+ if (searchForm && searchResults) {
118
+ searchForm.addEventListener("submit", function (event) {
119
+ event.preventDefault();
120
+ const input = searchForm.querySelector("input[type='search']");
121
+ const query = input ? input.value.trim().toLowerCase() : "";
122
+ searchResults.replaceChildren();
123
+
124
+ if (!query) {
125
+ window.location.hash = "#/manual";
126
+ searchResults.hidden = true;
127
+ return;
128
+ }
129
+
130
+ const matches = Array.from(document.querySelectorAll("[data-doc-section]")).filter(function (section) {
131
+ return section.textContent.toLowerCase().includes(query);
132
+ }).slice(0, 10);
133
+
134
+ const heading = document.createElement("strong");
135
+ heading.textContent = matches.length ? "Manual entries:" : "No manual entries found.";
136
+ searchResults.appendChild(heading);
137
+
138
+ if (matches.length) {
139
+ const list = document.createElement("ul");
140
+ matches.forEach(function (section) {
141
+ const item = document.createElement("li");
142
+ const link = document.createElement("a");
143
+ link.href = "#/manual#" + section.id;
144
+ link.textContent = section.getAttribute("data-title") || section.id;
145
+ item.appendChild(link);
146
+ list.appendChild(item);
147
+ });
148
+ searchResults.appendChild(list);
149
+ }
150
+
151
+ searchResults.hidden = false;
152
+ });
153
+ }
154
+
155
+ function findAsset(release, preferredName, pattern) {
156
+ const assets = Array.isArray(release.assets) ? release.assets : [];
157
+ return assets.find(function (asset) {
158
+ return asset.name === preferredName;
159
+ }) || assets.find(function (asset) {
160
+ return pattern.test(asset.name);
161
+ });
162
+ }
163
+
164
+ function setDownload(kind, url) {
165
+ if (!url) return;
166
+ document.querySelectorAll("[data-download='" + kind + "']").forEach(function (link) {
167
+ link.href = url;
168
+ });
169
+ }
170
+
171
+ fetch(api, {
172
+ headers: { "Accept": "application/vnd.github+json" }
173
+ }).then(function (response) {
174
+ if (!response.ok) throw new Error("No published release");
175
+ return response.json();
176
+ }).then(function (release) {
177
+ const tag = String(release.tag_name || "1.0.0");
178
+ const version = tag.replace(/^v/, "");
179
+ const gem = findAsset(release, "srsh.gem", /^srsh-[0-9].*\.gem$/);
180
+ const source = findAsset(release, "srsh-source.tar.gz", /^srsh-[0-9].*\.tar\.gz$/);
181
+ const checksums = findAsset(release, "SHA256SUMS", /^SHA256SUMS$/);
182
+
183
+ document.querySelectorAll("[data-latest-version]").forEach(function (element) {
184
+ element.textContent = version;
185
+ });
186
+
187
+ setDownload("gem", gem && gem.browser_download_url);
188
+ setDownload("source", source && source.browser_download_url);
189
+ setDownload("checksums", checksums && checksums.browser_download_url);
190
+
191
+ const date = release.published_at ? new Date(release.published_at).toLocaleDateString() : "";
192
+ document.querySelectorAll("[data-release-status]").forEach(function (element) {
193
+ element.textContent = date ? "Published " + date + "." : "Published on GitHub.";
194
+ });
195
+ }).catch(function () {
196
+ document.querySelectorAll("[data-release-status]").forEach(function (element) {
197
+ element.textContent = "Release details are available from the GitHub release page.";
198
+ });
199
+ setDownload("gem", repository + "/releases/latest/download/srsh.gem");
200
+ setDownload("source", repository + "/releases/latest/download/srsh-source.tar.gz");
201
+ setDownload("checksums", repository + "/releases/latest/download/SHA256SUMS");
202
+ });
203
+ })();
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ # @self is handy for libc-ish symbols already visible in the current process.
4
+ bridge c from "@self"
5
+ strlen(cstr) -> usize
6
+ end
7
+
8
+ = "strlen('simple ruby shell')=#{c.strlen("simple ruby shell")}"
@@ -0,0 +1,253 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ emit "SRSH Calculator"
4
+ emit "type help for commands"
5
+ emit ""
6
+
7
+ $CALC_SRC := ""
8
+ $CALC_POS := 0
9
+ $CALC_ERR := ""
10
+ $CALC_ANS := 0
11
+ $CALC_RUN := "yes"
12
+
13
+ :: peek()
14
+ ? int($CALC_POS) >= len($CALC_SRC)
15
+ ^ ""
16
+ .?
17
+ ^ $CALC_SRC[int($CALC_POS)]
18
+ .::
19
+
20
+ :: bump()
21
+ $CALC_POS := int($CALC_POS) + 1
22
+ ^ 0
23
+ .::
24
+
25
+ :: skip()
26
+ @? int($CALC_POS) < len($CALC_SRC)
27
+ c := peek()
28
+ ? c == " " or c == "\t"
29
+ ignored := bump()
30
+ :?
31
+ ^!
32
+ .?
33
+ .@
34
+ ^ 0
35
+ .::
36
+
37
+ :: number()
38
+ ignored := skip()
39
+ text := ""
40
+ dots := 0
41
+ digits := 0
42
+
43
+ @? int($CALC_POS) < len($CALC_SRC)
44
+ c := peek()
45
+ ? contains("0123456789", c)
46
+ text ++= c
47
+ digits += 1
48
+ ignored := bump()
49
+ :?
50
+ ? c == "." and dots == 0
51
+ text ++= c
52
+ dots := 1
53
+ ignored := bump()
54
+ :?
55
+ ^!
56
+ .?
57
+ .?
58
+ .@
59
+
60
+ ? digits == 0
61
+ $CALC_ERR := "expected number at column " ++ str(int($CALC_POS) + 1)
62
+ ^ 0
63
+ .?
64
+
65
+ ^ float(text)
66
+ .::
67
+
68
+ :: ipow(base, exponent)
69
+ n := int(exponent)
70
+ ? float(n) != float(exponent)
71
+ $CALC_ERR := "^ requires an integer exponent"
72
+ ^ 0
73
+ .?
74
+
75
+ negative := no
76
+ ? n < 0
77
+ negative := yes
78
+ n := 0 - n
79
+ .?
80
+
81
+ result := 1.0
82
+ @ n -> i
83
+ result *= base
84
+ .@
85
+
86
+ ? negative
87
+ ? result == 0
88
+ $CALC_ERR := "division by zero"
89
+ ^ 0
90
+ .?
91
+ result := 1.0 / result
92
+ .?
93
+ ^ result
94
+ .::
95
+
96
+ :: primary()
97
+ ignored := skip()
98
+ c := peek()
99
+
100
+ ? c == ""
101
+ $CALC_ERR := "expected value"
102
+ ^ 0
103
+ .?
104
+
105
+ ? c == "+"
106
+ ignored := bump()
107
+ ^ primary()
108
+ .?
109
+
110
+ ? c == "-"
111
+ ignored := bump()
112
+ ^ 0 - primary()
113
+ .?
114
+
115
+ ? c == "("
116
+ ignored := bump()
117
+ value := expression()
118
+ ignored := skip()
119
+ ? peek() != ")"
120
+ ? $CALC_ERR == ""
121
+ $CALC_ERR := "missing ')' at column " ++ str(int($CALC_POS) + 1)
122
+ .?
123
+ ^ value
124
+ .?
125
+ ignored := bump()
126
+ ^ value
127
+ .?
128
+
129
+ ^ number()
130
+ .::
131
+
132
+ :: power()
133
+ left := primary()
134
+ ? $CALC_ERR != ""
135
+ ^ left
136
+ .?
137
+
138
+ ignored := skip()
139
+ ? peek() == "^"
140
+ ignored := bump()
141
+ right := power()
142
+ ^ ipow(left, right)
143
+ .?
144
+ ^ left
145
+ .::
146
+
147
+ :: term()
148
+ value := power()
149
+
150
+ @? $CALC_ERR == ""
151
+ ignored := skip()
152
+ op := peek()
153
+ ? op != "*" and op != "/" and op != "%"
154
+ ^!
155
+ .?
156
+
157
+ ignored := bump()
158
+ rhs := power()
159
+ ? $CALC_ERR != ""
160
+ ^ value
161
+ .?
162
+
163
+ ? op == "*"
164
+ value *= rhs
165
+ :?
166
+ ? op == "/"
167
+ ? rhs == 0
168
+ $CALC_ERR := "division by zero"
169
+ ^ value
170
+ .?
171
+ value /= rhs
172
+ :?
173
+ ? rhs == 0
174
+ $CALC_ERR := "modulo by zero"
175
+ ^ value
176
+ .?
177
+ value := int(value) % int(rhs)
178
+ .?
179
+ .?
180
+ .@
181
+ ^ value
182
+ .::
183
+
184
+ :: expression()
185
+ value := term()
186
+
187
+ @? $CALC_ERR == ""
188
+ ignored := skip()
189
+ op := peek()
190
+ ? op != "+" and op != "-"
191
+ ^!
192
+ .?
193
+
194
+ ignored := bump()
195
+ rhs := term()
196
+ ? $CALC_ERR != ""
197
+ ^ value
198
+ .?
199
+
200
+ ? op == "+"
201
+ value += rhs
202
+ :?
203
+ value -= rhs
204
+ .?
205
+ .@
206
+ ^ value
207
+ .::
208
+
209
+ :: evaluate()
210
+ $CALC_POS := 0
211
+ $CALC_ERR := ""
212
+ value := expression()
213
+ ignored := skip()
214
+
215
+ ? $CALC_ERR == "" and int($CALC_POS) < len($CALC_SRC)
216
+ $CALC_ERR := "unexpected '" ++ peek() ++ "' at column " ++ str(int($CALC_POS) + 1)
217
+ .?
218
+
219
+ ? $CALC_ERR != ""
220
+ emit "error: " ++ $CALC_ERR
221
+ ^ 0
222
+ .?
223
+
224
+ $CALC_ANS := value
225
+ emit "= " ++ str(value)
226
+ ^ value
227
+ .::
228
+
229
+ @? $CALC_RUN == "yes"
230
+ printf "calc> "
231
+ read CALC_SRC
232
+ ?? $CALC_SRC
233
+ | "" ->
234
+ ignored := 0
235
+ | "q" ->
236
+ $CALC_RUN := "no"
237
+ | "quit" ->
238
+ $CALC_RUN := "no"
239
+ | "exit" ->
240
+ $CALC_RUN := "no"
241
+ | "ans" ->
242
+ emit "= " ++ str(float($CALC_ANS))
243
+ | "clear" ->
244
+ clear
245
+ | "help" ->
246
+ emit "operators: + - * / % ^ and parentheses"
247
+ emit "commands: ans, clear, help, quit"
248
+ | _ ->
249
+ ignored := evaluate()
250
+ .??
251
+ .@
252
+
253
+ emit "bye :P"
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ tmp := "/tmp/srsh-defer-example-#{status()}"
4
+
5
+ fn demo(path)
6
+ writefile(path, "temporary")
7
+ defer rmfile(path)
8
+
9
+ = "inside: #{exists(path)}"
10
+ return readfile(path)
11
+ end
12
+
13
+ = demo(tmp)
14
+ = "after: #{exists(tmp)}"
data/examples/hot.rsh ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ root := $1
4
+ ? root == "" => root := "."
5
+ branch := $(git branch --show-current 2>/dev/null)
6
+ ? branch == "" => branch := "no-branch"
7
+
8
+ ruby := glob(root ++ "/**/*.rb")
9
+ |> reject(::p => contains(p, "/vendor/"))
10
+ |> map(::p => %[path: p, bytes: len(readfile(p))])
11
+ |> sort(::x => 0 - x.bytes)
12
+
13
+ = "#{len(ruby)} ruby files on #{branch}"
14
+ @ ruby -> item => = "#{item.bytes} #{item.path}"
data/examples/meta.rsh ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ name := $1
4
+ ? name == "" => name := "gang"
5
+ dry := $2 == "--dry"
6
+
7
+ code greeting
8
+ emit "wsg #{name}"
9
+ emit "cwd=#{cwd()}"
10
+ end
11
+
12
+ if dry
13
+ emit "would run:"
14
+ = sourceof(greeting)
15
+ else
16
+ run(greeting)
17
+ end
18
+
19
+ formula := "len(name) * 2"
20
+ = "meta expression result=#{eval(formula)}"
@@ -0,0 +1,6 @@
1
+ prefix := "srsh"
2
+
3
+ fn tag(value) => "[#{prefix}] #{value}"
4
+
5
+ fn clean(lines_in) =>
6
+ lines_in |> map(::x => x.trim()) |> reject(::x => x == "")
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ use "./modules/text.rsh" as text
4
+
5
+ rows := [" hello ", "", "world"] |> text.clean
6
+ @ rows -> row => = text.tag(row)
@@ -0,0 +1,15 @@
1
+ # This file is intentionally shaped like something you'd paste at the prompt.
2
+ space sys
3
+ task kernel() =>
4
+ cmd("uname", "-srmo").check().out.trim()
5
+
6
+ task uptime() =>
7
+ cmd("uptime", "-p").check().out.trim()
8
+ end
9
+
10
+ jobs := [
11
+ sys.kernel(),
12
+ sys.uptime()
13
+ ]
14
+
15
+ = await_all(jobs)
@@ -0,0 +1,8 @@
1
+ SRSH.builtin('wsg') do |args|
2
+ puts "wsg #{args[1] || 'gng'} :P"
3
+ 0
4
+ end
5
+
6
+ SRSH.hook(:post_cmd) do |command, status|
7
+ # Ruby plugins have the full power of Ruby. Treat them as trusted code.
8
+ end
@@ -0,0 +1,65 @@
1
+ #!/usr/bin/env srsh
2
+
3
+ # Objects, functions, tasks and safe process values in one script.
4
+
5
+ space mathx
6
+ fn square(x) => x * x
7
+ fn positive(x) => x > 0
8
+ end
9
+
10
+ trait Labeled
11
+ fn label() => "#{self.name}:#{self.bias}"
12
+ end
13
+
14
+ proto Worker(name, bias := 0) with Labeled
15
+ slot name := name
16
+ slot bias := bias
17
+
18
+ fn score(x) => mathx.square(x) + self.bias
19
+
20
+ task score_later(x)
21
+ sleep(0.002)
22
+ return self.score(x)
23
+ end
24
+ end
25
+
26
+ workers := [0,1,2,3] |> map(::i => Worker("w#{i}", i))
27
+ jobs := enumerate(workers) |> map(::pair => pair[1].score_later(pair[0] + 2))
28
+ = "async scores=#{await_all(jobs)}"
29
+
30
+ hits := atom(0)
31
+ parallel(0 ..< 64, ::i => hits.swap(::n => n + 1), 8)
32
+ = "thread-safe hits=#{hits.get()}"
33
+
34
+ ch := chan(1)
35
+ task send_one(out, value)
36
+ out.send(value)
37
+ return value
38
+ end
39
+ producer := send_one(ch, "hello from task")
40
+ = ch.recv(1)
41
+ producer.await(1)
42
+ ch.close()
43
+
44
+ head, second, *tail := [10,20,30,40]
45
+ = "destructure=#{head + second}; rest=#{tail}"
46
+
47
+ try
48
+ assert(workers |> len == 4, "worker count changed")
49
+ = workers |> map(::w => w.label())
50
+ catch err
51
+ = "error: #{err.message}"
52
+ finally
53
+ emit "checked workers"
54
+ end
55
+
56
+ safe := cmd("printf", "%s", "argv stays data")
57
+ = safe.capture()
58
+
59
+ code later
60
+ = "code values are parsed before they run"
61
+ end
62
+ run(later)
63
+
64
+ fn cube(x) => x ** 3
65
+ = "functional=#{[1,2,3,4] |> filter(mathx.positive) |> map(cube) |> sum}"
data/examples/tour.rsh ADDED
@@ -0,0 +1,38 @@
1
+ # RSH 1.0 language tour
2
+ name := "Ruby shell"
3
+ ports := [22, 80, 443]
4
+ server := %[name: "main", tls: yes]
5
+
6
+ emit "hello from " ++ name
7
+
8
+ ? 443 in ports
9
+ emit "https enabled"
10
+ :?
11
+ emit "no https"
12
+ .?
13
+
14
+ @ 3 -> tick
15
+ emit "tick " ++ str(tick + 1)
16
+ .@
17
+
18
+ :: greet(who, count := 2)
19
+ @ int(count) -> i
20
+ emit "hey " ++ str(who) ++ " #" ++ str(i + 1)
21
+ .@
22
+ ^ count
23
+ .::
24
+
25
+ greet("gang", 3)
26
+
27
+ platform := env("OSTYPE") ?? "unknown"
28
+ ?? platform
29
+ | "darwin" ->
30
+ emit "mac"
31
+ | "linux" ->
32
+ emit "linux"
33
+ | _ ->
34
+ emit "some unix-ish thing"
35
+ .??
36
+
37
+ # Normal shell remains normal shell.
38
+ printf 'srsh\n' | tr a-z A-Z
@@ -0,0 +1,3 @@
1
+ require 'mkmf'
2
+ $CFLAGS << ' -O3' unless $CFLAGS.include?('-O')
3
+ create_makefile('srsh_native')