tusktsk 2.0.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +38 -0
- data/LICENSE +14 -0
- data/README.md +759 -0
- data/cli/main.rb +1488 -0
- data/exe/tsk +10 -0
- data/lib/peanut_config.rb +621 -0
- data/lib/tusk/license.rb +303 -0
- data/lib/tusk/protection.rb +180 -0
- data/lib/tusk_lang/shell_storage.rb +104 -0
- data/lib/tusk_lang/tsk.rb +501 -0
- data/lib/tusk_lang/tsk_parser.rb +234 -0
- data/lib/tusk_lang/tsk_parser_enhanced.rb +563 -0
- data/lib/tusk_lang/version.rb +5 -0
- data/lib/tusk_lang.rb +14 -0
- metadata +249 -0
@@ -0,0 +1,563 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'time'
|
5
|
+
|
6
|
+
module TuskLang
|
7
|
+
# TuskLang Enhanced Parser for Ruby
|
8
|
+
# "We don't bow to any king" - Support ALL syntax styles
|
9
|
+
#
|
10
|
+
# Features:
|
11
|
+
# - Multiple grouping: [], {}, <>
|
12
|
+
# - $global vs section-local variables
|
13
|
+
# - Cross-file communication
|
14
|
+
# - Database queries (placeholder adapters)
|
15
|
+
# - All @ operators
|
16
|
+
# - Maximum flexibility
|
17
|
+
#
|
18
|
+
# DEFAULT CONFIG: peanut.tsk (the bridge of language grace)
|
19
|
+
class TSKParserEnhanced
|
20
|
+
attr_reader :data, :global_variables, :section_variables
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@data = {}
|
24
|
+
@global_variables = {}
|
25
|
+
@section_variables = {}
|
26
|
+
@cache = {}
|
27
|
+
@cross_file_cache = {}
|
28
|
+
@current_section = ''
|
29
|
+
@in_object = false
|
30
|
+
@object_key = ''
|
31
|
+
@peanut_loaded = false
|
32
|
+
|
33
|
+
# Standard peanut.tsk locations
|
34
|
+
@peanut_locations = [
|
35
|
+
'./peanut.tsk',
|
36
|
+
'../peanut.tsk',
|
37
|
+
'../../peanut.tsk',
|
38
|
+
'/etc/tusklang/peanut.tsk',
|
39
|
+
File.join(Dir.home, '.config/tusklang/peanut.tsk'),
|
40
|
+
ENV['TUSKLANG_CONFIG']
|
41
|
+
].compact
|
42
|
+
end
|
43
|
+
|
44
|
+
# Load peanut.tsk if available
|
45
|
+
def load_peanut
|
46
|
+
return if @peanut_loaded
|
47
|
+
|
48
|
+
@peanut_loaded = true # Mark first to prevent recursion
|
49
|
+
|
50
|
+
@peanut_locations.each do |location|
|
51
|
+
next if location.empty?
|
52
|
+
|
53
|
+
if File.exist?(location)
|
54
|
+
puts "# Loading universal config from: #{location}"
|
55
|
+
parse_file(location)
|
56
|
+
return
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
# Parse TuskLang value with all syntax support
|
62
|
+
def parse_value(value)
|
63
|
+
value = value.strip
|
64
|
+
|
65
|
+
# Remove optional semicolon
|
66
|
+
value = value.chomp(';').strip if value.end_with?(';')
|
67
|
+
|
68
|
+
# Basic types
|
69
|
+
case value
|
70
|
+
when 'true' then return true
|
71
|
+
when 'false' then return false
|
72
|
+
when 'null' then return nil
|
73
|
+
end
|
74
|
+
|
75
|
+
# Numbers
|
76
|
+
return value.to_i if value.match?(/^-?\d+$/)
|
77
|
+
return value.to_f if value.match?(/^-?\d+\.\d+$/)
|
78
|
+
|
79
|
+
# $variable references (global)
|
80
|
+
if value.match?(/^\$[a-zA-Z_][a-zA-Z0-9_]*$/)
|
81
|
+
var_name = value[1..-1]
|
82
|
+
return @global_variables[var_name] || ''
|
83
|
+
end
|
84
|
+
|
85
|
+
# Section-local variable references
|
86
|
+
if !@current_section.empty? && value.match?(/^[a-zA-Z_][a-zA-Z0-9_]*$/)
|
87
|
+
section_key = "#{@current_section}.#{value}"
|
88
|
+
return @section_variables[section_key] if @section_variables.key?(section_key)
|
89
|
+
end
|
90
|
+
|
91
|
+
# @date function
|
92
|
+
date_match = value.match(/^@date\(['"](.*)['"]?\)$/)
|
93
|
+
if date_match
|
94
|
+
format_str = date_match[1]
|
95
|
+
return execute_date(format_str)
|
96
|
+
end
|
97
|
+
|
98
|
+
# @env function with default
|
99
|
+
env_match = value.match(/^@env\(['"]([^'"]*)['"]?(?:,\s*(.+))?\)$/)
|
100
|
+
if env_match
|
101
|
+
env_var = env_match[1]
|
102
|
+
default_val = env_match[2]&.strip&.tr('"\'', '') || ''
|
103
|
+
return ENV[env_var] || default_val
|
104
|
+
end
|
105
|
+
|
106
|
+
# Ranges: 8000-9000
|
107
|
+
range_match = value.match(/^(\d+)-(\d+)$/)
|
108
|
+
if range_match
|
109
|
+
return {
|
110
|
+
'min' => range_match[1].to_i,
|
111
|
+
'max' => range_match[2].to_i,
|
112
|
+
'type' => 'range'
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
# Arrays
|
117
|
+
if value.start_with?('[') && value.end_with?(']')
|
118
|
+
return parse_array(value)
|
119
|
+
end
|
120
|
+
|
121
|
+
# Objects
|
122
|
+
if value.start_with?('{') && value.end_with?('}')
|
123
|
+
return parse_object(value)
|
124
|
+
end
|
125
|
+
|
126
|
+
# Cross-file references: @file.tsk.get('key')
|
127
|
+
cross_get_match = value.match(/^@([a-zA-Z0-9_-]+)\.tsk\.get\(['"](.*)['"]?\)$/)
|
128
|
+
if cross_get_match
|
129
|
+
file_name = cross_get_match[1]
|
130
|
+
key = cross_get_match[2]
|
131
|
+
return cross_file_get(file_name, key)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Cross-file set: @file.tsk.set('key', value)
|
135
|
+
cross_set_match = value.match(/^@([a-zA-Z0-9_-]+)\.tsk\.set\(['"]([^'"]*)['"]\?,\s*(.+)\)$/)
|
136
|
+
if cross_set_match
|
137
|
+
file_name = cross_set_match[1]
|
138
|
+
key = cross_set_match[2]
|
139
|
+
val = cross_set_match[3]
|
140
|
+
return cross_file_set(file_name, key, val)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @query function
|
144
|
+
query_match = value.match(/^@query\(['"](.*)['"]?(.*)\)$/)
|
145
|
+
if query_match
|
146
|
+
query = query_match[1]
|
147
|
+
return execute_query(query)
|
148
|
+
end
|
149
|
+
|
150
|
+
# @ operators
|
151
|
+
operator_match = value.match(/^@([a-zA-Z_][a-zA-Z0-9_]*)\((.+)\)$/)
|
152
|
+
if operator_match
|
153
|
+
operator_name = operator_match[1]
|
154
|
+
parameters = operator_match[2]
|
155
|
+
return execute_operator(operator_name, parameters)
|
156
|
+
end
|
157
|
+
|
158
|
+
# String concatenation
|
159
|
+
if value.include?(' + ')
|
160
|
+
parts = value.split(' + ')
|
161
|
+
result = ''
|
162
|
+
parts.each do |part|
|
163
|
+
part = part.strip.tr('"\'', '')
|
164
|
+
if !part.start_with?('"')
|
165
|
+
parsed_part = parse_value(part)
|
166
|
+
result += parsed_part.to_s
|
167
|
+
else
|
168
|
+
result += part[1..-2] if part.length > 1
|
169
|
+
end
|
170
|
+
end
|
171
|
+
return result
|
172
|
+
end
|
173
|
+
|
174
|
+
# Conditional/ternary: condition ? true_val : false_val
|
175
|
+
ternary_match = value.match(/(.+?)\s*\?\s*(.+?)\s*:\s*(.+)/)
|
176
|
+
if ternary_match
|
177
|
+
condition = ternary_match[1].strip
|
178
|
+
true_val = ternary_match[2].strip
|
179
|
+
false_val = ternary_match[3].strip
|
180
|
+
|
181
|
+
if evaluate_condition(condition)
|
182
|
+
return parse_value(true_val)
|
183
|
+
else
|
184
|
+
return parse_value(false_val)
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
# Remove quotes from strings
|
189
|
+
if (value.start_with?('"') && value.end_with?('"')) ||
|
190
|
+
(value.start_with?("'") && value.end_with?("'"))
|
191
|
+
return value[1..-2]
|
192
|
+
end
|
193
|
+
|
194
|
+
# Return as-is
|
195
|
+
value
|
196
|
+
end
|
197
|
+
|
198
|
+
# Parse array syntax
|
199
|
+
def parse_array(value)
|
200
|
+
content = value[1..-2].strip
|
201
|
+
return [] if content.empty?
|
202
|
+
|
203
|
+
items = []
|
204
|
+
current = ''
|
205
|
+
depth = 0
|
206
|
+
in_string = false
|
207
|
+
quote_char = nil
|
208
|
+
|
209
|
+
content.each_char do |ch|
|
210
|
+
if (ch == '"' || ch == "'") && !in_string
|
211
|
+
in_string = true
|
212
|
+
quote_char = ch
|
213
|
+
elsif ch == quote_char && in_string
|
214
|
+
in_string = false
|
215
|
+
quote_char = nil
|
216
|
+
end
|
217
|
+
|
218
|
+
unless in_string
|
219
|
+
case ch
|
220
|
+
when '[', '{'
|
221
|
+
depth += 1
|
222
|
+
when ']', '}'
|
223
|
+
depth -= 1
|
224
|
+
when ','
|
225
|
+
if depth == 0
|
226
|
+
items << parse_value(current.strip)
|
227
|
+
current = ''
|
228
|
+
next
|
229
|
+
end
|
230
|
+
end
|
231
|
+
end
|
232
|
+
|
233
|
+
current += ch
|
234
|
+
end
|
235
|
+
|
236
|
+
items << parse_value(current.strip) unless current.strip.empty?
|
237
|
+
items
|
238
|
+
end
|
239
|
+
|
240
|
+
# Parse object syntax
|
241
|
+
def parse_object(value)
|
242
|
+
content = value[1..-2].strip
|
243
|
+
return {} if content.empty?
|
244
|
+
|
245
|
+
pairs = []
|
246
|
+
current = ''
|
247
|
+
depth = 0
|
248
|
+
in_string = false
|
249
|
+
quote_char = nil
|
250
|
+
|
251
|
+
content.each_char do |ch|
|
252
|
+
if (ch == '"' || ch == "'") && !in_string
|
253
|
+
in_string = true
|
254
|
+
quote_char = ch
|
255
|
+
elsif ch == quote_char && in_string
|
256
|
+
in_string = false
|
257
|
+
quote_char = nil
|
258
|
+
end
|
259
|
+
|
260
|
+
unless in_string
|
261
|
+
case ch
|
262
|
+
when '[', '{'
|
263
|
+
depth += 1
|
264
|
+
when ']', '}'
|
265
|
+
depth -= 1
|
266
|
+
when ','
|
267
|
+
if depth == 0
|
268
|
+
pairs << current.strip
|
269
|
+
current = ''
|
270
|
+
next
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
current += ch
|
276
|
+
end
|
277
|
+
|
278
|
+
pairs << current.strip unless current.strip.empty?
|
279
|
+
|
280
|
+
obj = {}
|
281
|
+
pairs.each do |pair|
|
282
|
+
if pair.include?(':')
|
283
|
+
colon_index = pair.index(':')
|
284
|
+
key = pair[0...colon_index].strip.tr('"\'', '')
|
285
|
+
val = pair[(colon_index + 1)..-1].strip
|
286
|
+
obj[key] = parse_value(val)
|
287
|
+
elsif pair.include?('=')
|
288
|
+
equals_index = pair.index('=')
|
289
|
+
key = pair[0...equals_index].strip.tr('"\'', '')
|
290
|
+
val = pair[(equals_index + 1)..-1].strip
|
291
|
+
obj[key] = parse_value(val)
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
obj
|
296
|
+
end
|
297
|
+
|
298
|
+
# Evaluate conditions for ternary expressions
|
299
|
+
def evaluate_condition(condition)
|
300
|
+
condition = condition.strip
|
301
|
+
|
302
|
+
# Simple equality check
|
303
|
+
if condition.include?('==')
|
304
|
+
left, right = condition.split('==', 2).map(&:strip)
|
305
|
+
return parse_value(left).to_s == parse_value(right).to_s
|
306
|
+
end
|
307
|
+
|
308
|
+
# Not equal
|
309
|
+
if condition.include?('!=')
|
310
|
+
left, right = condition.split('!=', 2).map(&:strip)
|
311
|
+
return parse_value(left).to_s != parse_value(right).to_s
|
312
|
+
end
|
313
|
+
|
314
|
+
# Greater than
|
315
|
+
if condition.include?('>')
|
316
|
+
left, right = condition.split('>', 2).map(&:strip)
|
317
|
+
left_val = parse_value(left)
|
318
|
+
right_val = parse_value(right)
|
319
|
+
|
320
|
+
if left_val.is_a?(Numeric) && right_val.is_a?(Numeric)
|
321
|
+
return left_val > right_val
|
322
|
+
end
|
323
|
+
return left_val.to_s > right_val.to_s
|
324
|
+
end
|
325
|
+
|
326
|
+
# Default: check if truthy
|
327
|
+
value = parse_value(condition)
|
328
|
+
case value
|
329
|
+
when true, false
|
330
|
+
value
|
331
|
+
when String
|
332
|
+
!value.empty? && value != 'false' && value != 'null' && value != '0'
|
333
|
+
when Numeric
|
334
|
+
value != 0
|
335
|
+
when nil
|
336
|
+
false
|
337
|
+
else
|
338
|
+
true
|
339
|
+
end
|
340
|
+
end
|
341
|
+
|
342
|
+
# Get value from another TSK file
|
343
|
+
def cross_file_get(file_name, key)
|
344
|
+
cache_key = "#{file_name}:#{key}"
|
345
|
+
|
346
|
+
# Check cache
|
347
|
+
return @cross_file_cache[cache_key] if @cross_file_cache.key?(cache_key)
|
348
|
+
|
349
|
+
# Find file
|
350
|
+
directories = ['.', './config', '..', '../config']
|
351
|
+
file_path = nil
|
352
|
+
|
353
|
+
directories.each do |directory|
|
354
|
+
potential_path = File.join(directory, "#{file_name}.tsk")
|
355
|
+
if File.exist?(potential_path)
|
356
|
+
file_path = potential_path
|
357
|
+
break
|
358
|
+
end
|
359
|
+
end
|
360
|
+
|
361
|
+
return '' unless file_path
|
362
|
+
|
363
|
+
# Parse file and get value
|
364
|
+
temp_parser = TSKParserEnhanced.new
|
365
|
+
temp_parser.parse_file(file_path)
|
366
|
+
|
367
|
+
value = temp_parser.get(key)
|
368
|
+
|
369
|
+
# Cache result
|
370
|
+
@cross_file_cache[cache_key] = value
|
371
|
+
|
372
|
+
value
|
373
|
+
end
|
374
|
+
|
375
|
+
# Set value in another TSK file (cache only for now)
|
376
|
+
def cross_file_set(file_name, key, value)
|
377
|
+
cache_key = "#{file_name}:#{key}"
|
378
|
+
parsed_value = parse_value(value)
|
379
|
+
@cross_file_cache[cache_key] = parsed_value
|
380
|
+
parsed_value
|
381
|
+
end
|
382
|
+
|
383
|
+
# Execute @date function
|
384
|
+
def execute_date(format_str)
|
385
|
+
now = Time.now
|
386
|
+
|
387
|
+
# Convert PHP-style format to Ruby
|
388
|
+
case format_str
|
389
|
+
when 'Y'
|
390
|
+
now.strftime('%Y')
|
391
|
+
when 'Y-m-d'
|
392
|
+
now.strftime('%Y-%m-%d')
|
393
|
+
when 'Y-m-d H:i:s'
|
394
|
+
now.strftime('%Y-%m-%d %H:%M:%S')
|
395
|
+
when 'c'
|
396
|
+
now.iso8601
|
397
|
+
else
|
398
|
+
now.strftime('%Y-%m-%d %H:%M:%S')
|
399
|
+
end
|
400
|
+
end
|
401
|
+
|
402
|
+
# Execute database query (placeholder for now)
|
403
|
+
def execute_query(query)
|
404
|
+
load_peanut
|
405
|
+
|
406
|
+
# Determine database type
|
407
|
+
db_type = get('database.default') || 'sqlite'
|
408
|
+
|
409
|
+
# Placeholder implementation
|
410
|
+
"[Query: #{query} on #{db_type}]"
|
411
|
+
end
|
412
|
+
|
413
|
+
# Execute @ operators
|
414
|
+
def execute_operator(operator_name, parameters)
|
415
|
+
case operator_name
|
416
|
+
when 'cache'
|
417
|
+
# Simple cache implementation
|
418
|
+
parts = parameters.split(',', 2)
|
419
|
+
if parts.length == 2
|
420
|
+
ttl = parts[0].strip.tr('"\'', '')
|
421
|
+
value = parts[1].strip
|
422
|
+
return parse_value(value)
|
423
|
+
end
|
424
|
+
''
|
425
|
+
when 'learn', 'optimize', 'metrics', 'feature'
|
426
|
+
# Placeholders for advanced features
|
427
|
+
"@#{operator_name}(#{parameters})"
|
428
|
+
else
|
429
|
+
"@#{operator_name}(#{parameters})"
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
# Parse a single line
|
434
|
+
def parse_line(line)
|
435
|
+
trimmed = line.strip
|
436
|
+
|
437
|
+
# Skip empty lines and comments
|
438
|
+
return if trimmed.empty? || trimmed.start_with?('#')
|
439
|
+
|
440
|
+
# Remove optional semicolon
|
441
|
+
trimmed = trimmed.chomp(';').strip if trimmed.end_with?(';')
|
442
|
+
|
443
|
+
# Check for section declaration []
|
444
|
+
section_match = trimmed.match(/^\[([a-zA-Z_][a-zA-Z0-9_]*)\]$/)
|
445
|
+
if section_match
|
446
|
+
@current_section = section_match[1]
|
447
|
+
@in_object = false
|
448
|
+
return
|
449
|
+
end
|
450
|
+
|
451
|
+
# Check for angle bracket object >
|
452
|
+
angle_open_match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*>$/)
|
453
|
+
if angle_open_match
|
454
|
+
@in_object = true
|
455
|
+
@object_key = angle_open_match[1]
|
456
|
+
return
|
457
|
+
end
|
458
|
+
|
459
|
+
# Check for closing angle bracket <
|
460
|
+
if trimmed == '<'
|
461
|
+
@in_object = false
|
462
|
+
@object_key = ''
|
463
|
+
return
|
464
|
+
end
|
465
|
+
|
466
|
+
# Check for curly brace object {
|
467
|
+
brace_open_match = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_]*)\s*\{$/)
|
468
|
+
if brace_open_match
|
469
|
+
@in_object = true
|
470
|
+
@object_key = brace_open_match[1]
|
471
|
+
return
|
472
|
+
end
|
473
|
+
|
474
|
+
# Check for closing curly brace }
|
475
|
+
if trimmed == '}'
|
476
|
+
@in_object = false
|
477
|
+
@object_key = ''
|
478
|
+
return
|
479
|
+
end
|
480
|
+
|
481
|
+
# Parse key-value pairs (both : and = supported)
|
482
|
+
kv_match = trimmed.match(/^([\$]?[a-zA-Z_][a-zA-Z0-9_-]*)\s*[:=]\s*(.+)$/)
|
483
|
+
if kv_match
|
484
|
+
key = kv_match[1]
|
485
|
+
value = kv_match[2]
|
486
|
+
parsed_value = parse_value(value)
|
487
|
+
|
488
|
+
# Determine storage location
|
489
|
+
storage_key = if @in_object && !@object_key.empty?
|
490
|
+
if !@current_section.empty?
|
491
|
+
"#{@current_section}.#{@object_key}.#{key}"
|
492
|
+
else
|
493
|
+
"#{@object_key}.#{key}"
|
494
|
+
end
|
495
|
+
elsif !@current_section.empty?
|
496
|
+
"#{@current_section}.#{key}"
|
497
|
+
else
|
498
|
+
key
|
499
|
+
end
|
500
|
+
|
501
|
+
# Store the value
|
502
|
+
@data[storage_key] = parsed_value
|
503
|
+
|
504
|
+
# Handle global variables
|
505
|
+
if key.start_with?('$')
|
506
|
+
var_name = key[1..-1]
|
507
|
+
@global_variables[var_name] = parsed_value
|
508
|
+
elsif !@current_section.empty? && !key.start_with?('$')
|
509
|
+
# Store section-local variable
|
510
|
+
section_key = "#{@current_section}.#{key}"
|
511
|
+
@section_variables[section_key] = parsed_value
|
512
|
+
end
|
513
|
+
end
|
514
|
+
end
|
515
|
+
|
516
|
+
# Parse TuskLang content
|
517
|
+
def parse(content)
|
518
|
+
lines = content.split("\n")
|
519
|
+
|
520
|
+
lines.each { |line| parse_line(line) }
|
521
|
+
|
522
|
+
@data
|
523
|
+
end
|
524
|
+
|
525
|
+
# Parse a TSK file
|
526
|
+
def parse_file(file_path)
|
527
|
+
content = File.read(file_path)
|
528
|
+
parse(content)
|
529
|
+
end
|
530
|
+
|
531
|
+
# Get a value by key
|
532
|
+
def get(key)
|
533
|
+
@data[key]
|
534
|
+
end
|
535
|
+
|
536
|
+
# Set a value
|
537
|
+
def set(key, value)
|
538
|
+
@data[key] = value
|
539
|
+
end
|
540
|
+
|
541
|
+
# Get all keys
|
542
|
+
def keys
|
543
|
+
@data.keys.sort
|
544
|
+
end
|
545
|
+
|
546
|
+
# Get all key-value pairs
|
547
|
+
def items
|
548
|
+
@data.dup
|
549
|
+
end
|
550
|
+
|
551
|
+
# Convert to JSON
|
552
|
+
def to_json
|
553
|
+
JSON.pretty_generate(@data)
|
554
|
+
end
|
555
|
+
|
556
|
+
# Load configuration from peanut.tsk
|
557
|
+
def self.load_from_peanut
|
558
|
+
parser = new
|
559
|
+
parser.load_peanut
|
560
|
+
parser
|
561
|
+
end
|
562
|
+
end
|
563
|
+
end
|
data/lib/tusk_lang.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# TuskLang Ruby SDK
|
4
|
+
# The official TuskLang Configuration SDK for Ruby
|
5
|
+
|
6
|
+
require_relative "tusk_lang/version"
|
7
|
+
require_relative "tusk_lang/tsk"
|
8
|
+
require_relative "tusk_lang/tsk_parser"
|
9
|
+
require_relative "tusk_lang/shell_storage"
|
10
|
+
|
11
|
+
module TuskLang
|
12
|
+
class Error < StandardError; end
|
13
|
+
# Your code goes here...
|
14
|
+
end
|