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
@@ -0,0 +1,48 @@
1
+ #include <ruby.h>
2
+ #include <string.h>
3
+ #ifdef __linux__
4
+ #include <sys/prctl.h>
5
+ #endif
6
+
7
+ static VALUE ident_end(VALUE self, VALUE str, VALUE offv) {
8
+ Check_Type(str, T_STRING);
9
+ long off = NUM2LONG(offv);
10
+ long len = RSTRING_LEN(str);
11
+ const unsigned char *p = (const unsigned char *)RSTRING_PTR(str);
12
+ if (off < 0 || off > len) rb_raise(rb_eArgError, "offset out of range");
13
+
14
+ long i = off;
15
+ while (i < len) {
16
+ unsigned char c = p[i];
17
+ if (!(c == '_' || (c >= 'a' && c <= 'z') ||
18
+ (c >= 'A' && c <= 'Z') || (c >= '0' && c <= '9'))) break;
19
+ i++;
20
+ }
21
+ return LONG2NUM(i);
22
+ }
23
+
24
+ static VALUE ascii_space(VALUE self, VALUE chv) {
25
+ int c = NUM2INT(chv);
26
+ return (c == ' ' || c == '\t' || c == '\r' || c == '\n') ? Qtrue : Qfalse;
27
+ }
28
+
29
+ static VALUE set_process_name(VALUE self, VALUE namev) {
30
+ #ifdef __linux__
31
+ const char *name = StringValueCStr(namev);
32
+ char short_name[16];
33
+ memset(short_name, 0, sizeof(short_name));
34
+ strncpy(short_name, name, sizeof(short_name) - 1);
35
+ return prctl(PR_SET_NAME, (unsigned long)short_name, 0, 0, 0) == 0 ? Qtrue : Qfalse;
36
+ #else
37
+ (void)self;
38
+ (void)namev;
39
+ return Qfalse;
40
+ #endif
41
+ }
42
+
43
+ void Init_srsh_native(void) {
44
+ VALUE m = rb_define_module("SrshNative");
45
+ rb_define_singleton_method(m, "ident_end", ident_end, 2);
46
+ rb_define_singleton_method(m, "ascii_space?", ascii_space, 1);
47
+ rb_define_singleton_method(m, "set_process_name", set_process_name, 1);
48
+ }
@@ -0,0 +1,670 @@
1
+ # RSH language guide
2
+
3
+ RSH is the programming language inside Simple Ruby Shell. It is meant for shell work first, but it is not restricted to shell-shaped programs.
4
+
5
+ There are two rules worth knowing before the syntax dump:
6
+
7
+ 1. normal Unix commands should still look normal;
8
+ 2. when RSH crosses a boundary, the syntax should tell you which boundary it crossed.
9
+
10
+ That is why `|`, `|>`, `$()` and `bridge` are different things instead of one overloaded mega-pipe.
11
+
12
+ ## Commands and values
13
+
14
+ This is a Unix pipeline:
15
+
16
+ ```rsh
17
+ printf 'c\na\nb\n' | sort
18
+ ```
19
+
20
+ This is an RSH value pipeline:
21
+
22
+ ```rsh
23
+ [3, 1, 2] |> sort |> map(::x => x * 10)
24
+ ```
25
+
26
+ This brings command output into RSH:
27
+
28
+ ```rsh
29
+ kernel := $(uname -r)
30
+ ```
31
+
32
+ This calls C without starting another process:
33
+
34
+ ```rsh
35
+ bridge c from "@self"
36
+ strlen(cstr) -> usize
37
+ end
38
+
39
+ = c.strlen("abc")
40
+ ```
41
+
42
+ Those are four different execution/data models, so they are four visibly different forms.
43
+
44
+ ## Bindings
45
+
46
+ ```rsh
47
+ name := "Robert"
48
+ count := 4
49
+ $counted := "environment value"
50
+ $EDITOR := "vim"
51
+
52
+ count += 1
53
+ name ++= "!"
54
+ ```
55
+
56
+ `:=` always creates a binding in the current lexical scope. Compound assignment updates the nearest existing local.
57
+
58
+ A leading `$` on an assignment means the process environment. Worker tasks are not allowed to mutate the process environment behind the owner thread's back.
59
+
60
+ At the shell-command layer, locals and environment variables can be expanded with `$name`, `${name}`, `$1`, `$?`, and `$!`.
61
+
62
+ ## Literals
63
+
64
+ ```rsh
65
+ 42
66
+ -12
67
+ 0xff
68
+ 0b1010
69
+ 0o755
70
+ 1_000_000
71
+ 3.14159
72
+ 2.5e6
73
+
74
+ yes
75
+ no
76
+ void
77
+
78
+ "hello #{name}"
79
+ 'literal #{name}'
80
+ [[raw text #{name}]]
81
+
82
+ [1, 2, 3]
83
+ %[name: "srsh", ready: yes]
84
+ 1 .. 10
85
+ 0 ..< 10
86
+ ```
87
+
88
+ Double strings interpolate full RSH expressions. Single and raw strings do not.
89
+
90
+ `%[...]` is a map literal. Braces are intentionally not block syntax in RSH, which keeps shell text and language blocks from fighting over the same punctuation.
91
+
92
+ ## Operators
93
+
94
+ From loose to tight, the useful groups are roughly:
95
+
96
+ ```text
97
+ ??
98
+ or ||
99
+ and &&
100
+ == != === !== =~ !~ in
101
+ < <= > >=
102
+ |>
103
+ .. ..<
104
+ + - ++
105
+ * / %
106
+ **
107
+ ```
108
+
109
+ `++` is string concatenation. `+` stays numeric unless coercion makes sense.
110
+
111
+ `===` / `!==` are strict comparisons. `==` / `!=` keep the shell-friendly numeric comparison behavior.
112
+
113
+ Regex operators use a string pattern:
114
+
115
+ ```rsh
116
+ ? name =~ "^[A-Z]" => = "starts uppercase"
117
+ ```
118
+
119
+ ## Value pipelines
120
+
121
+ `|>` sends the left value to the function on the right as its first argument:
122
+
123
+ ```rsh
124
+ files := glob("src/**/*.rb")
125
+ |> reject(::p => contains(p, "/vendor/"))
126
+ |> map(::p => basename(p))
127
+ |> uniq
128
+ |> sort
129
+ ```
130
+
131
+ The parser treats `|>` as an expression continuation, so long pipelines can be laid out vertically without backslashes.
132
+
133
+ Normal `|` is reserved for process pipelines. That separation is one of the main RSH design choices.
134
+
135
+ ## Functions and lambdas
136
+
137
+ Readable function:
138
+
139
+ ```rsh
140
+ fn scale(x, by := 2)
141
+ return x * by
142
+ end
143
+ ```
144
+
145
+ Expression function:
146
+
147
+ ```rsh
148
+ fn scale(x, by := 2) => x * by
149
+ ```
150
+
151
+ The expression can start on the next physical line too, which is handy while pasting:
152
+
153
+ ```rsh
154
+ fn scale(x, by := 2) =>
155
+ x * by
156
+ ```
157
+
158
+ Hot spelling:
159
+
160
+ ```rsh
161
+ :: scale(x, by := 2) => x * by
162
+ ```
163
+
164
+ Anonymous callables:
165
+
166
+ ```rsh
167
+ ::x => x * 2
168
+ ::(a, b) => a + b
169
+ ::(head, *tail) => tail |> len
170
+ :: => clock()
171
+ ```
172
+
173
+ Named functions are first-class values:
174
+
175
+ ```rsh
176
+ fn square(x) => x * x
177
+ = 1 .. 10 |> map(square) |> sum
178
+ ```
179
+
180
+ Closures capture lexical values.
181
+
182
+ ## If / guards
183
+
184
+ Readable:
185
+
186
+ ```rsh
187
+ if score >= 90
188
+ emit "great"
189
+ else
190
+ emit "keep going"
191
+ end
192
+ ```
193
+
194
+ One line:
195
+
196
+ ```rsh
197
+ if score >= 90 => emit "great"
198
+ ```
199
+
200
+ Hot form:
201
+
202
+ ```rsh
203
+ ? score >= 90
204
+ emit "great"
205
+ :?
206
+ emit "keep going"
207
+ .?
208
+ ```
209
+
210
+ Hot guard:
211
+
212
+ ```rsh
213
+ ? score >= 90 => emit "great"
214
+ ```
215
+
216
+ ## Loops
217
+
218
+ Readable:
219
+
220
+ ```rsh
221
+ each users -> user
222
+ = user.name
223
+ end
224
+ ```
225
+
226
+ Hot:
227
+
228
+ ```rsh
229
+ @ users -> user
230
+ = user.name
231
+ .@
232
+ ```
233
+
234
+ One-line forms:
235
+
236
+ ```rsh
237
+ each users -> user => = user.name
238
+ @ users -> user => = user.name
239
+ ```
240
+
241
+ Pairs can destructure in the loop head:
242
+
243
+ ```rsh
244
+ each %[a: 1, b: 2] -> key, value
245
+ = "#{key}=#{value}"
246
+ end
247
+ ```
248
+
249
+ Integers iterate `0...N`.
250
+
251
+ While loops:
252
+
253
+ ```rsh
254
+ while pending
255
+ work()
256
+ end
257
+ ```
258
+
259
+ or:
260
+
261
+ ```rsh
262
+ @? pending
263
+ work()
264
+ .@
265
+ ```
266
+
267
+ `break` / `continue` have hot aliases `^!` / `^>`.
268
+
269
+ ## Destructuring
270
+
271
+ ```rsh
272
+ head, second, *rest := [10, 20, 30, 40]
273
+ ```
274
+
275
+ A single rest name is allowed and must be last.
276
+
277
+ ## Pattern matching
278
+
279
+ ```rsh
280
+ match status
281
+ | 200..299 => = "ok"
282
+ | [401,403] => = "auth"
283
+ | ? it >= 500 => = "server"
284
+ | _ => = "other"
285
+ end
286
+ ```
287
+
288
+ Hot form:
289
+
290
+ ```rsh
291
+ ?? status
292
+ | 200..299 ->
293
+ = "ok"
294
+ | _ ->
295
+ = "other"
296
+ .??
297
+ ```
298
+
299
+ Patterns can be values, ranges, lists, partial maps, prototype references, or guard expressions beginning with `?` / `when`.
300
+
301
+ ## Safe access and null coalescing
302
+
303
+ ```rsh
304
+ cfg := json(readfile("config.json"))
305
+ host := cfg?.server?.host ?? "localhost"
306
+ first := cfg?.hosts?[0] ?? host
307
+ ```
308
+
309
+ Normal `.` and `[]` are strict. `?.` and `?[]` return `void` when the access cannot be completed.
310
+
311
+ ## Functional collection toolbox
312
+
313
+ ```text
314
+ map filter reject fold find any all count sum
315
+ each sort uniq flat zip enumerate take drop chunk group
316
+ tap partial compose
317
+ ```
318
+
319
+ Most collection operations also have method form:
320
+
321
+ ```rsh
322
+ = [1,2,3,4].filter(::x => x % 2 == 0).map(::x => x ** 2).sum()
323
+ ```
324
+
325
+ Use whichever reads better.
326
+
327
+ ## Prototypes and traits
328
+
329
+ RSH uses composable prototypes rather than class inheritance.
330
+
331
+ ```rsh
332
+ trait Printable
333
+ fn show()
334
+ = "#{self.name}=#{self.value}"
335
+ end
336
+ end
337
+
338
+ proto Counter(name, start := 0) with Printable
339
+ slot name := name
340
+ slot value := start
341
+
342
+ fn inc(by := 1)
343
+ self.value += by
344
+ return self
345
+ end
346
+ end
347
+
348
+ counter := Counter("requests", 10)
349
+ counter.inc()
350
+ ```
351
+
352
+ `self` is implicit in methods. Slot compound updates use synchronized object updates.
353
+
354
+ Reflection helpers include `fields()`, `methods()`, `protoof()`, `is()` and `clone()`.
355
+
356
+ ## Namespaces
357
+
358
+ Inline namespace:
359
+
360
+ ```rsh
361
+ space build
362
+ root := "out"
363
+ fn artifact(name) => root ++ "/" ++ name
364
+ end
365
+
366
+ = build.artifact("app")
367
+ ```
368
+
369
+ A `space` can hold bindings, functions, tasks, prototypes, traits, nested spaces, bridges, modules and code declarations.
370
+
371
+ ## Modules
372
+
373
+ ```rsh
374
+ use "./lib/net.rsh" as net
375
+ = net.fetch(url)
376
+ ```
377
+
378
+ The imported file runs inside a namespace rather than dumping its locals into the caller.
379
+
380
+ Relative paths are resolved against the current script when SRSH can determine one. Recursive import cycles are rejected.
381
+
382
+ ## Cleanup with `defer`
383
+
384
+ Single cleanup:
385
+
386
+ ```rsh
387
+ fn work()
388
+ tmp := make_tmp()
389
+ defer rmfile(tmp)
390
+ return use_tmp(tmp)
391
+ end
392
+ ```
393
+
394
+ Block cleanup:
395
+
396
+ ```rsh
397
+ defer
398
+ unlock()
399
+ rmfile(tmp)
400
+ end
401
+ ```
402
+
403
+ Defers are LIFO and run when the current script/function execution scope leaves, including through `return` and exceptions.
404
+
405
+ ## Structured errors
406
+
407
+ ```rsh
408
+ try
409
+ cfg := json(readfile("config.json"))
410
+ catch err
411
+ = err.message
412
+ cfg := %[]
413
+ finally
414
+ audit("attempted")
415
+ end
416
+ ```
417
+
418
+ The caught error is an RSH map-like value with at least `.type` and `.message`.
419
+
420
+ Hot/value style:
421
+
422
+ ```rsh
423
+ result := attempt(:: => risky())
424
+ ? !result.ok => = result.error.message
425
+ ```
426
+
427
+ `fail(message)` raises an RSH runtime error. `assert(condition, message)` is available too.
428
+
429
+ ## Async tasks
430
+
431
+ ```rsh
432
+ task fetch(url)
433
+ return cmd("curl", "-fsS", url).check().out
434
+ end
435
+
436
+ jobs := urls |> map(fetch)
437
+ results := await_all(jobs)
438
+ ```
439
+
440
+ Expression task:
441
+
442
+ ```rsh
443
+ task square_later(x) => x * x
444
+ ```
445
+
446
+ and, like functions, the body can be on the next physical line after `=>`.
447
+
448
+ Spawn a callable immediately:
449
+
450
+ ```rsh
451
+ job := &:: => expensive_io()
452
+ = job.await(2.0)
453
+ ```
454
+
455
+ Task methods:
456
+
457
+ ```text
458
+ .await([timeout])
459
+ .done()
460
+ .status()
461
+ .cancel()
462
+ ```
463
+
464
+ `race(tasks)` returns the winner and cancels losers. `await_all(tasks)` preserves order and cancels still-running siblings when one fails.
465
+
466
+ ## Atoms and channels
467
+
468
+ Shared scalar-ish state:
469
+
470
+ ```rsh
471
+ hits := atom(0)
472
+ hits.swap(::n => n + 1)
473
+ = hits.get()
474
+ ```
475
+
476
+ Channels:
477
+
478
+ ```rsh
479
+ ch := chan(16)
480
+ ch.send(value)
481
+ value := ch.recv(1.0)
482
+ ch.close()
483
+ ```
484
+
485
+ A capacity of zero is currently an unbounded queue; positive capacities apply backpressure.
486
+
487
+ ## Thread and process parallelism
488
+
489
+ ```rsh
490
+ rows := parallel(inputs, fetch, 8)
491
+ ```
492
+
493
+ `parallel` uses Ruby threads. Good for files, network calls and subprocess waits.
494
+
495
+ ```rsh
496
+ hashes := pmap(files, hash_one, cpu_count())
497
+ ```
498
+
499
+ `pmap` uses Unix fork workers when available, so CPU-heavy Ruby work can use multiple cores even under CRuby's GVL. Results preserve input order.
500
+
501
+ ## Shell command values
502
+
503
+ ```rsh
504
+ job := cmd("git", "rev-parse", "--verify", ref)
505
+ result := job.result()
506
+ ```
507
+
508
+ Methods:
509
+
510
+ ```text
511
+ .argv() copy of argv
512
+ .run() inherit terminal, return status
513
+ .result() capture stdout/stderr/status
514
+ .capture() stdout as a string
515
+ .check() result, but raise on nonzero status
516
+ .task() run structured command in an RSH task
517
+ ```
518
+
519
+ Use `cmd()` when filenames/URLs/data should stay argv and should not be interpreted as shell code.
520
+
521
+ ## C ABI bridges
522
+
523
+ Readable declaration:
524
+
525
+ ```rsh
526
+ bridge libc from "libc.so.6"
527
+ getpid() -> i32
528
+ strlen(cstr) -> usize
529
+ gethostname(ptr, usize) -> i32
530
+ end
531
+ ```
532
+
533
+ Call it like a namespace:
534
+
535
+ ```rsh
536
+ = libc.getpid()
537
+ = libc.strlen("hello")
538
+ ```
539
+
540
+ `@self` binds against the current process:
541
+
542
+ ```rsh
543
+ bridge c from "@self"
544
+ strlen(cstr) -> usize
545
+ end
546
+ ```
547
+
548
+ Writable memory:
549
+
550
+ ```rsh
551
+ buf := cbuf(256)
552
+ libc.gethostname(buf, buf.size())
553
+ = buf.string()
554
+ ```
555
+
556
+ `cbuf` methods:
557
+
558
+ ```text
559
+ .size()
560
+ .address()
561
+ .ptr()
562
+ .read([offset], [length])
563
+ .write(string, [offset])
564
+ .string([max])
565
+ .clear()
566
+ ```
567
+
568
+ ABI type names:
569
+
570
+ ```text
571
+ void bool
572
+ i8 u8 i16 u16 i32 u32 i64 u64
573
+ isize usize
574
+ f32 f64
575
+ cstr ptr
576
+ ```
577
+
578
+ Bridges are intentionally thin. They resolve the dynamic symbol once and then call the native function directly. Read the security document: the wrong signature can crash the process.
579
+
580
+ ## Files, strings, JSON and paths
581
+
582
+ Useful hot-script helpers include:
583
+
584
+ ```text
585
+ readfile writefile appendfile
586
+ exists file dir glob stat
587
+ mkdirp rmfile cpfile mvfile
588
+ basename dirname ext
589
+ json json_dump
590
+ lines words replace
591
+ upper lower trim split join
592
+ shellquote
593
+ ```
594
+
595
+ These are for values. Normal shell commands remain available when a dedicated Unix tool is the better choice.
596
+
597
+ ## Metascripting
598
+
599
+ Parsed code block:
600
+
601
+ ```rsh
602
+ code cleanup
603
+ rm -rf build/tmp
604
+ end
605
+ ```
606
+
607
+ Inspect or execute later:
608
+
609
+ ```rsh
610
+ = sourceof(cleanup)
611
+ run(cleanup)
612
+ ```
613
+
614
+ Dynamic expression/code helpers:
615
+
616
+ ```text
617
+ eval(string)
618
+ code(string)
619
+ run(code_or_string)
620
+ valid(string [, "expr"])
621
+ sourceof(code)
622
+ locals()
623
+ fns()
624
+ protos()
625
+ traits()
626
+ ```
627
+
628
+ Parsed `code ... end` is preferable when the source is known ahead of time.
629
+
630
+ ## Shell strictness options
631
+
632
+ At the command prompt or in startup config:
633
+
634
+ ```sh
635
+ option pipefail yes
636
+ option nounset yes
637
+ option noclobber yes
638
+ ```
639
+
640
+ Shortcut:
641
+
642
+ ```sh
643
+ option strict yes
644
+ ```
645
+
646
+ `strict` means `pipefail + nounset`. RSH deliberately does not copy Bash `set -e`; its context-sensitive behavior is too easy to misunderstand. Use structured errors/checking when failure really matters.
647
+
648
+ ## Interactive multiline input
649
+
650
+ The REPL asks the parser whether the input is complete. This works for blocks *and* expressions:
651
+
652
+ ```text
653
+ > values := [
654
+ ... 1,
655
+ ... 2,
656
+ ... 3
657
+ ... ]
658
+ ```
659
+
660
+ It also handles a command ending in `|`, `&&`, `||`, redirection, an unclosed quote, command substitution, or a trailing backslash as incomplete shell input.
661
+
662
+ Background jobs use the same process-group model as the interactive shell. `jobs`, `fg`, `bg`, and `wait %N` operate on those jobs; `exec command ...` replaces SRSH with a command when a wrapper no longer needs to stay around.
663
+
664
+ That behavior is what makes pasting larger snippets practical; the shell no longer has to recognize every possible multiline construct with a pile of prompt regexes.
665
+
666
+ ## Compatibility forms
667
+
668
+ The original 0.8 spellings still exist, including `if/else/end`, `while/end`, `times/end` and old `fn name args ... end` functions.
669
+
670
+ The newer hot forms are there for short code, not to force old scripts into a new costume.