srsh 0.7.1 → 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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +18 -14
  3. data/README.md +351 -134
  4. data/bin/srsh +53 -1473
  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 +1 -2
  47. data/lib/srsh.rb +15 -0
  48. metadata +59 -11
@@ -0,0 +1,316 @@
1
+ require_relative '../errors'
2
+ require_relative 'token'
3
+
4
+ module Srsh
5
+ module Language
6
+ class Lexer
7
+ MAX_TOKENS = 100_000
8
+ TWO = %w[== != <= >= ++ ** ?? =~ !~ .. && || |>].freeze
9
+ THREE = %w[..< === !==].freeze
10
+
11
+ def initialize(source, line: 1)
12
+ @s = source.to_s
13
+ @i = 0
14
+ @line = line
15
+ @column = 1
16
+ @tokens = 0
17
+ end
18
+
19
+ def next_token
20
+ skip_space
21
+ return token(:eof, nil) if eof?
22
+ raise ParseError.new('expression has too many tokens', line: @line, column: @column) if (@tokens += 1) > MAX_TOKENS
23
+
24
+ c = peek
25
+ return read_number if digit?(c)
26
+ return read_string if c == '"' || c == "'"
27
+ return read_raw if c == '[' && peek(1) == '['
28
+ return read_dollar if c == '$'
29
+ return read_percent_map if c == '%' && peek(1) == '['
30
+ return read_ident if ident_start?(c)
31
+
32
+ start = @column
33
+ three = @s[@i, 3]
34
+ if THREE.include?(three)
35
+ advance(3)
36
+ return Token.new(:op, three, @line, start)
37
+ end
38
+
39
+ two = @s[@i, 2]
40
+ case two
41
+ when '::'
42
+ advance(2)
43
+ return Token.new(:lambda, two, @line, start)
44
+ when '=>'
45
+ advance(2)
46
+ return Token.new(:fat_arrow, two, @line, start)
47
+ when '?.'
48
+ advance(2)
49
+ return Token.new(:safe_dot, two, @line, start)
50
+ when '?['
51
+ advance(2)
52
+ return Token.new(:safe_lbracket, two, @line, start)
53
+ end
54
+
55
+ if TWO.include?(two)
56
+ advance(2)
57
+ return Token.new(:op, two, @line, start)
58
+ end
59
+
60
+ type = case c
61
+ when '(' then :lparen
62
+ when ')' then :rparen
63
+ when '[' then :lbracket
64
+ when ']' then :rbracket
65
+ when ',' then :comma
66
+ when ':' then :colon
67
+ when '.' then :dot
68
+ else :op
69
+ end
70
+ advance
71
+ Token.new(type, c, @line, start)
72
+ end
73
+
74
+ private
75
+
76
+ def token(type, value, line = @line, col = @column) = Token.new(type, value, line, col)
77
+ def eof? = @i >= @s.length
78
+ def peek(offset = 0) = @s[@i + offset]
79
+ def digit?(c) = c && c >= '0' && c <= '9'
80
+ def ident_start?(c) = c && (c == '_' || c.match?(/[A-Za-z]/))
81
+ def ident_char?(c) = c && (c == '_' || c.match?(/[A-Za-z0-9]/))
82
+
83
+ def advance(n = 1)
84
+ n.times do
85
+ break if eof?
86
+ @i += 1
87
+ @column += 1
88
+ end
89
+ end
90
+
91
+ def skip_space
92
+ advance while (c = peek) && (c == ' ' || c == "\t" || c == "\r")
93
+ end
94
+
95
+ def read_number
96
+ line, col, start = @line, @column, @i
97
+ if peek == '0' && %w[x X b B o O].include?(peek(1))
98
+ base_mark = peek(1).downcase
99
+ advance(2)
100
+ digit_re = { 'x' => /[0-9A-Fa-f]/, 'b' => /[01]/, 'o' => /[0-7]/ }.fetch(base_mark)
101
+ saw_digit = false
102
+ while (c = peek) && (c == '_' || c.match?(digit_re))
103
+ saw_digit ||= c != '_'
104
+ advance
105
+ end
106
+ raise ParseError.new('bad base-prefixed number', line: line, column: col) unless saw_digit
107
+ if (c = peek) && c.match?(/[A-Za-z0-9_]/)
108
+ raise ParseError.new('invalid digit in number', line: line, column: col)
109
+ end
110
+ else
111
+ advance while (c = peek) && (digit?(c) || c == '_')
112
+ if peek == '.' && digit?(peek(1))
113
+ advance
114
+ advance while (c = peek) && (digit?(c) || c == '_')
115
+ end
116
+ if %w[e E].include?(peek)
117
+ advance
118
+ advance if %w[+ -].include?(peek)
119
+ raise ParseError.new('bad exponent', line: line, column: col) unless digit?(peek)
120
+ advance while (c = peek) && (digit?(c) || c == '_')
121
+ end
122
+ end
123
+ raw = @s[start...@i]
124
+ if raw.match?(/(?:\A_|_\z|__|_[.eE+\-]|[.eE+\-]_|[xXbBoO]_)/)
125
+ raise ParseError.new('bad underscore placement in number', line: line, column: col)
126
+ end
127
+ Token.new(:number, raw, line, col)
128
+ end
129
+
130
+ def read_string
131
+ quote = peek
132
+ line, col = @line, @column
133
+ advance
134
+ text = +''
135
+ segments = []
136
+ closed = false
137
+
138
+ until eof?
139
+ c = peek
140
+
141
+ if c == quote
142
+ advance
143
+ closed = true
144
+ break
145
+ end
146
+
147
+ if c == '\\'
148
+ advance
149
+ raise IncompleteInput.new('unterminated escape', line: line, column: col) if eof?
150
+ e = peek
151
+ advance
152
+ text << case e
153
+ when 'n' then "\n"
154
+ when 'r' then "\r"
155
+ when 't' then "\t"
156
+ when '0' then "\0"
157
+ when '\\' then '\\'
158
+ when '"' then '"'
159
+ when "'" then "'"
160
+ when '#' then '#'
161
+ else e
162
+ end
163
+ next
164
+ end
165
+
166
+ if quote == '"' && c == '#' && peek(1) == '{'
167
+ segments << [:text, text] unless text.empty?
168
+ text = +''
169
+ segments << [:expr, read_interpolation(line, col)]
170
+ next
171
+ end
172
+
173
+ text << c
174
+ advance
175
+ end
176
+
177
+ raise IncompleteInput.new('unterminated string', line: line, column: col) unless closed
178
+ if segments.empty?
179
+ Token.new(:string, text, line, col)
180
+ else
181
+ segments << [:text, text] unless text.empty?
182
+ Token.new(:template, segments, line, col)
183
+ end
184
+ end
185
+
186
+ def read_interpolation(line, col)
187
+ # cursor is on '#{' here
188
+ advance(2)
189
+ start = @i
190
+ depth = 1
191
+ quote = nil
192
+ escaped = false
193
+
194
+ until eof?
195
+ c = peek
196
+ if escaped
197
+ escaped = false
198
+ elsif quote
199
+ if c == '\\'
200
+ escaped = true
201
+ elsif c == quote
202
+ quote = nil
203
+ end
204
+ elsif c == '"' || c == "'"
205
+ quote = c
206
+ elsif c == '{'
207
+ depth += 1
208
+ elsif c == '}'
209
+ depth -= 1
210
+ if depth.zero?
211
+ out = @s[start...@i]
212
+ advance
213
+ raise ParseError.new('empty string interpolation', line: line, column: col) if out.strip.empty?
214
+ return out
215
+ end
216
+ end
217
+ advance
218
+ end
219
+
220
+ raise IncompleteInput.new('unterminated string interpolation', line: line, column: col)
221
+ end
222
+
223
+ def read_raw
224
+ line, col = @line, @column
225
+ advance(2)
226
+ start = @i
227
+ idx = @s.index(']]', @i)
228
+ raise IncompleteInput.new('unterminated raw string', line: line, column: col) unless idx
229
+ out = @s[start...idx]
230
+ advance(idx - @i + 2)
231
+ Token.new(:string, out, line, col)
232
+ end
233
+
234
+ def read_dollar
235
+ line, col = @line, @column
236
+ advance
237
+
238
+ if peek == '('
239
+ advance
240
+ return Token.new(:capture, read_capture(line, col), line, col)
241
+ end
242
+ if peek == '?'
243
+ advance
244
+ return Token.new(:status, '?', line, col)
245
+ end
246
+ if digit?(peek)
247
+ start = @i
248
+ advance while digit?(peek)
249
+ return Token.new(:positional, @s[start...@i].to_i, line, col)
250
+ end
251
+ raise ParseError.new('expected environment variable after $', line: line, column: col) unless ident_start?(peek)
252
+ start = @i
253
+ advance while ident_char?(peek)
254
+ Token.new(:env, @s[start...@i], line, col)
255
+ end
256
+
257
+ def read_capture(line, col)
258
+ start = @i
259
+ depth = 1
260
+ single = false
261
+ double = false
262
+ escaped = false
263
+
264
+ until eof?
265
+ c = peek
266
+ if escaped
267
+ escaped = false
268
+ elsif c == '\\' && !single
269
+ escaped = true
270
+ elsif c == "'" && !double
271
+ single = !single
272
+ elsif c == '"' && !single
273
+ double = !double
274
+ elsif !single && !double && c == '('
275
+ depth += 1
276
+ elsif !single && !double && c == ')'
277
+ depth -= 1
278
+ if depth.zero?
279
+ out = @s[start...@i]
280
+ advance
281
+ return out
282
+ end
283
+ end
284
+ advance
285
+ end
286
+
287
+ raise IncompleteInput.new('unterminated command capture', line: line, column: col)
288
+ end
289
+
290
+ def read_percent_map
291
+ line, col = @line, @column
292
+ advance(2)
293
+ Token.new(:map_open, '%[', line, col)
294
+ end
295
+
296
+ def read_ident
297
+ line, col, start = @line, @column, @i
298
+ if defined?(::SrshNative) && @s.ascii_only? && ::SrshNative.respond_to?(:ident_end)
299
+ finish = ::SrshNative.ident_end(@s, @i)
300
+ advance(finish - @i)
301
+ else
302
+ advance while ident_char?(peek)
303
+ end
304
+ value = @s[start...@i]
305
+ type = case value
306
+ when 'yes', 'true' then :true
307
+ when 'no', 'false' then :false
308
+ when 'void', 'nil' then :void
309
+ when 'and', 'or', 'not', 'in' then :op
310
+ else :ident
311
+ end
312
+ Token.new(type, value, line, col)
313
+ end
314
+ end
315
+ end
316
+ end