rexe 0.12.0 → 0.13.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 (7) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE.txt +201 -21
  4. data/README.md +140 -53
  5. data/exe/rexe +254 -164
  6. data/rexe.gemspec +2 -0
  7. metadata +17 -3
data/exe/rexe CHANGED
@@ -10,115 +10,141 @@ require 'optparse'
10
10
  require 'ostruct'
11
11
  require 'shellwords'
12
12
 
13
- class Rexe < Struct.new(
14
- :input_format,
15
- :input_mode,
16
- :loads,
17
- :output_format,
18
- :requires,
19
- :log_format,
20
- :noop)
13
+ class Rexe
21
14
 
15
+ VERSION = '0.13.0'
22
16
 
23
- VERSION = '0.12.0'
17
+ PROJECT_URL = 'https://github.com/keithrbennett/rexe'
24
18
 
25
19
 
26
- def initialize
27
- clear_options
28
- end
20
+ class Options < Struct.new(
21
+ :input_format,
22
+ :input_mode,
23
+ :loads,
24
+ :output_format,
25
+ :requires,
26
+ :log_format,
27
+ :noop)
29
28
 
30
29
 
31
- def input_modes
32
- @input_modes ||= {
33
- 'l' => :line,
34
- 'e' => :enumerator,
35
- 'b' => :one_big_string,
36
- 'n' => :no_input
37
- }
38
- end
30
+ def initialize
31
+ super
32
+ clear
33
+ end
39
34
 
40
35
 
41
- def input_formats
42
- @input_formats ||= {
43
- 'j' => :json,
44
- 'm' => :marshal,
45
- 'n' => :none,
46
- 'y' => :yaml,
47
- }
36
+ def clear
37
+ self.input_format = :none
38
+ self.input_mode = :no_input
39
+ self.output_format = :puts
40
+ self.loads = []
41
+ self.requires = []
42
+ self.log_format = :none
43
+ self.noop = false
44
+ end
48
45
  end
49
46
 
50
47
 
51
- def input_parsers
52
- @input_parsers ||= {
53
- json: ->(obj) { JSON.parse(obj) },
54
- marshal: ->(obj) { Marshal.load(obj) },
55
- none: ->(obj) { obj },
56
- yaml: ->(obj) { YAML.load(obj) },
57
- }
58
- end
59
48
 
60
49
 
61
- def output_formats
62
- @output_formats ||= {
63
- 'a' => :awesome_print,
64
- 'i' => :inspect,
65
- 'j' => :json,
66
- 'J' => :pretty_json,
67
- 'm' => :marshal,
68
- 'n' => :no_output,
69
- 'p' => :puts, # default
70
- 'P' => :pretty_print,
71
- 's' => :to_s,
72
- 'y' => :yaml,
73
- }
74
- end
75
50
 
51
+ class Lookups
52
+ def input_modes
53
+ @input_modes ||= {
54
+ 'l' => :line,
55
+ 'e' => :enumerator,
56
+ 'b' => :one_big_string,
57
+ 'n' => :no_input
58
+ }
59
+ end
60
+
61
+
62
+ def input_formats
63
+ @input_formats ||= {
64
+ 'j' => :json,
65
+ 'm' => :marshal,
66
+ 'n' => :none,
67
+ 'y' => :yaml,
68
+ }
69
+ end
76
70
 
77
- def formatters
78
- @formatters ||= {
79
- awesome_print: ->(obj) { obj.ai },
80
- inspect: ->(obj) { obj.inspect + "\n" },
81
- json: ->(obj) { obj.to_json },
82
- marshal: ->(obj) { Marshal.dump(obj) },
83
- no_output: ->(_obj) { nil },
84
- pretty_json: ->(obj) { JSON.pretty_generate(obj) },
85
- pretty_print: ->(obj) { obj.pretty_inspect },
86
- puts: ->(obj) { sio = StringIO.new; sio.puts(obj); sio.string }, # default
87
- to_s: ->(obj) { obj.to_s + "\n" },
88
- yaml: ->(obj) { obj.to_yaml },
89
- }
71
+
72
+ def input_parsers
73
+ @input_parsers ||= {
74
+ json: ->(obj) { JSON.parse(obj) },
75
+ marshal: ->(obj) { Marshal.load(obj) },
76
+ none: ->(obj) { obj },
77
+ yaml: ->(obj) { YAML.load(obj) },
78
+ }
79
+ end
80
+
81
+
82
+ def output_formats
83
+ @output_formats ||= {
84
+ 'a' => :awesome_print,
85
+ 'i' => :inspect,
86
+ 'j' => :json,
87
+ 'J' => :pretty_json,
88
+ 'm' => :marshal,
89
+ 'n' => :no_output,
90
+ 'p' => :puts, # default
91
+ 'P' => :pretty_print,
92
+ 's' => :to_s,
93
+ 'y' => :yaml,
94
+ }
95
+ end
96
+
97
+
98
+ def formatters
99
+ @formatters ||= {
100
+ awesome_print: ->(obj) { obj.ai },
101
+ inspect: ->(obj) { obj.inspect + "\n" },
102
+ json: ->(obj) { obj.to_json },
103
+ marshal: ->(obj) { Marshal.dump(obj) },
104
+ no_output: ->(_obj) { nil },
105
+ pretty_json: ->(obj) { JSON.pretty_generate(obj) },
106
+ pretty_print: ->(obj) { obj.pretty_inspect },
107
+ puts: ->(obj) { sio = StringIO.new; sio.puts(obj); sio.string }, # default
108
+ to_s: ->(obj) { obj.to_s + "\n" },
109
+ yaml: ->(obj) { obj.to_yaml },
110
+ }
111
+ end
112
+
113
+ def format_requires
114
+ @format_requires ||= {
115
+ json: 'json',
116
+ pretty_json: 'json',
117
+ awesome_print: 'awesome_print',
118
+ pretty_print: 'pp',
119
+ yaml: 'yaml'
120
+ }
121
+ end
90
122
  end
91
123
 
92
124
 
93
- def require_format_require(format)
94
- @format_requires ||= {
95
- json: 'json',
96
- pretty_json: 'json',
97
- awesome_print: 'awesome_print',
98
- pretty_print: 'pp',
99
- yaml: 'yaml'
100
- }
101
- r = @format_requires[format]
102
- require(r) if r
125
+
126
+ attr_reader :callable, :input_parser, :lookups, :options, :output_formatter,
127
+ :log_formatter, :start_time, :user_source_code
128
+
129
+
130
+ def initialize
131
+ @start_time = DateTime.now
132
+ @options = Options.new
133
+ @lookups = Lookups.new
103
134
  end
104
135
 
105
136
 
106
- # Used as an initializer and also when `-!` is specified on the command line.
107
- def clear_options
108
- self.input_format = :none
109
- self.input_mode = :no_input
110
- self.output_format = :puts
111
- self.loads = []
112
- self.requires = []
113
- self.log_format = :none
114
- self.noop = false
137
+ # Requires the 'require' appropriate to the specified format.
138
+ private def require_format_require(format)
139
+ r = lookups.format_requires[format]
140
+ require!(r) if r
115
141
  end
116
142
 
117
143
 
118
144
  def help_text
119
145
  <<~HEREDOC
120
146
 
121
- rexe -- Ruby Command Line Executor/Filter -- v#{VERSION} -- https://github.com/keithrbennett/rexe
147
+ rexe -- Ruby Command Line Executor/Filter -- v#{VERSION} -- #{PROJECT_URL}
122
148
 
123
149
  Executes Ruby code on the command line, optionally taking standard input and writing to standard output.
124
150
 
@@ -134,7 +160,8 @@ class Rexe < Struct.new(
134
160
  -im Marshal
135
161
  -in None
136
162
  -iy YAML
137
- -l, --load RUBY_FILE(S) Ruby file(s) to load, comma separated, or ! to clear
163
+ -l, --load RUBY_FILE(S) Ruby file(s) to load, comma separated;
164
+ ! to clear all, or precede a name with '-' to remove
138
165
  -m, --input_mode MODE Mode with which to handle input (i.e. what `self` will be in your code):
139
166
  -ml line mode; each line is ingested as a separate string
140
167
  -me enumerator mode
@@ -151,7 +178,8 @@ class Rexe < Struct.new(
151
178
  -op Puts (default)
152
179
  -os to_s
153
180
  -oy YAML
154
- -r, --require REQUIRE(S) Gems and built-in libraries to require, comma separated, or ! to clear
181
+ -r, --require REQUIRE(S) Gems and built-in libraries to require, comma separated;
182
+ ! to clear all, or precede a name with '-' to remove
155
183
 
156
184
  If there is an .rexerc file in your home directory, it will be run as Ruby code
157
185
  before processing the input.
@@ -163,7 +191,8 @@ class Rexe < Struct.new(
163
191
  end
164
192
 
165
193
 
166
- def prepend_environment_options
194
+ # Inserts contents of REXE_OPTIONS environment variable at the beginning of ARGV.
195
+ private def prepend_environment_options
167
196
  env_opt_string = ENV['REXE_OPTIONS']
168
197
  if env_opt_string
169
198
  args_to_prepend = Shellwords.shellsplit(env_opt_string)
@@ -172,15 +201,38 @@ class Rexe < Struct.new(
172
201
  end
173
202
 
174
203
 
175
- def parse_command_line
204
+ def open_resource(resource_identifier)
205
+ command = case (`uname`.chomp)
206
+ when 'Darwin'
207
+ 'open'
208
+ when 'Linux'
209
+ 'xdg-open'
210
+ else
211
+ 'start'
212
+ end
213
+
214
+ `#{command} #{resource_identifier}`
215
+ end
216
+
217
+
218
+ def add_format_requires_to_requires_list
219
+ formats = [options.input_format, options.output_format, options.log_format]
220
+ requires = formats.map { |format| lookups.format_requires[format] }.uniq.compact
221
+ requires.each { |r| options.requires << r }
222
+ end
223
+
224
+
225
+ # Using 'optparse', parses the command line.
226
+ # Settings go into this instance's properties (see Struct declaration).
227
+ private def parse_command_line
176
228
 
177
229
  OptionParser.new do |parser|
178
230
 
179
231
  parser.on('-g', '--log_format FORMAT', 'Log format, logs to stderr, defaults to none (see -o for format options)') do |v|
180
- self.log_format = output_formats[v]
181
- if self.log_format.nil?
232
+ options.log_format = lookups.output_formats[v]
233
+ if options.log_format.nil?
182
234
  puts help_text
183
- raise "Output mode was '#{v}' but must be one of #{output_formats.keys}."
235
+ raise "Output mode was '#{v}' but must be one of #{lookups.output_formats.keys}."
184
236
  end
185
237
  end
186
238
 
@@ -192,58 +244,68 @@ class Rexe < Struct.new(
192
244
  parser.on('-i', '--input_format FORMAT',
193
245
  'Mode with which to parse input values (n = none (default), j = JSON, m = Marshal, y = YAML') do |v|
194
246
 
195
- self.input_format = input_formats[v]
196
- if self.input_format.nil?
247
+ options.input_format = lookups.input_formats[v]
248
+ if options.input_format.nil?
197
249
  puts help_text
198
- raise "Input mode was '#{v}' but must be one of #{input_formats.keys}."
250
+ raise "Input mode was '#{v}' but must be one of #{lookups.input_formats.keys}."
199
251
  end
200
252
  end
201
253
 
202
254
  parser.on('-l', '--load RUBY_FILE(S)', 'Ruby file(s) to load, comma separated, or ! to clear') do |v|
203
255
  if v == '!'
204
- self.loads.clear
256
+ options.loads.clear
205
257
  else
206
258
  loadfiles = v.split(',').map(&:strip)
207
- existent, nonexistent = loadfiles.partition { |filespec| File.exists?(filespec) }
259
+ removes, adds = loadfiles.partition { |filespec| filespec[0] == '-' }
260
+
261
+ existent, nonexistent = adds.partition { |filespec| File.exists?(filespec) }
208
262
  if nonexistent.any?
209
- raise("\nDid not find the following files to load: #{nonexistent.to_s}\n\n")
263
+ raise("\nDid not find the following files to load: #{nonexistent}\n\n")
210
264
  else
211
- existent.each { |filespec| self.loads << filespec }
265
+ existent.each { |filespec| options.loads << filespec }
212
266
  end
267
+
268
+ removes.each { |filespec| options.loads -= [filespec[1..-1]] }
213
269
  end
214
270
  end
215
271
 
216
272
  parser.on('-m', '--input_mode MODE',
217
273
  'Mode with which to handle input (-ml, -me, -mb, -mn (default)') do |v|
218
274
 
219
- self.input_mode = input_modes[v]
220
- if self.input_mode.nil?
275
+ options.input_mode = lookups.input_modes[v]
276
+ if options.input_mode.nil?
221
277
  puts help_text
222
- raise "Input mode was '#{v}' but must be one of #{input_modes.keys}."
278
+ raise "Input mode was '#{v}' but must be one of #{lookups.input_modes.keys}."
223
279
  end
224
280
  end
225
281
 
226
282
  parser.on('-o', '--output_format FORMAT',
227
283
  'Mode with which to format values for output (`-o` + [aijJmnpsy])') do |v|
228
284
 
229
- self.output_format = output_formats[v]
230
- if self.output_format.nil?
285
+ options.output_format = lookups.output_formats[v]
286
+ if options.output_format.nil?
231
287
  puts help_text
232
- raise "Output mode was '#{v}' but must be one of #{output_formats.keys}."
288
+ raise "Output mode was '#{v}' but must be one of #{lookups.output_formats.keys}."
233
289
  end
234
290
  end
235
291
 
236
292
  parser.on('-r', '--require REQUIRE(S)',
237
293
  'Gems and built-in libraries (e.g. shellwords, yaml) to require, comma separated, or ! to clear') do |v|
238
294
  if v == '!'
239
- self.requires.clear
295
+ options.requires.clear
240
296
  else
241
- v.split(',').map(&:strip).each { |r| self.requires << r }
297
+ v.split(',').map(&:strip).each do |r|
298
+ if r[0] == '-'
299
+ options.requires -= [r[1..-1]]
300
+ else
301
+ options.requires << r
302
+ end
303
+ end
242
304
  end
243
305
  end
244
306
 
245
307
  parser.on('-c', '--clear_options', "Clear all previous command line options") do |v|
246
- clear_options
308
+ options.clear
247
309
  end
248
310
 
249
311
  # See https://stackoverflow.com/questions/54576873/ruby-optionparser-short-code-for-boolean-option
@@ -251,34 +313,55 @@ class Rexe < Struct.new(
251
313
  # According to the answer, valid options are:
252
314
  # -n no, -n yes, -n false, -n true, -n n, -n y, -n +, but not -n -.
253
315
  parser.on('-n', '--[no-]noop [FLAG]', TrueClass, "Do not execute the code (useful with -g)") do |v|
254
- self.noop = (v.nil? ? true : v)
316
+ options.noop = (v.nil? ? true : v)
255
317
  end
256
318
 
257
319
  parser.on('-v', '--version', 'Print version') do
258
320
  puts VERSION
259
321
  exit
260
322
  end
323
+
324
+ # Undocumented feature
325
+ parser.on('', '--open-project') do
326
+ open_resource(PROJECT_URL)
327
+ exit(0)
328
+ end
329
+
261
330
  end.parse!
262
331
 
263
- requires.uniq!
264
- loads.uniq!
332
+ # We want to do this after all options have been processed because we don't want any clearing of the
333
+ # options (by '-c', etc.) to result in exclusion of these needed requires.
334
+ add_format_requires_to_requires_list
335
+
336
+ options.requires.uniq!
337
+ options.loads.uniq!
265
338
 
266
339
  end
267
340
 
268
341
 
269
- def load_global_config_if_exists
342
+ private def load_global_config_if_exists
270
343
  filespec = File.join(Dir.home, '.rexerc')
271
344
  load(filespec) if File.exists?(filespec)
272
345
  end
273
346
 
274
347
 
275
- def execute(eval_context_object, code)
276
- if input_format != :none && input_mode != :no_input
348
+ private def init_parser_and_formatters
349
+ @input_parser = lookups.input_parsers[options.input_format]
350
+ @output_formatter = lookups.formatters[options.output_format]
351
+ @log_formatter = lookups.formatters[options.log_format]
352
+ end
353
+
354
+
355
+ # Executes the user specified code in the manner appropriate to the input mode.
356
+ # Performs any optionally specified parsing on input and formatting on output.
357
+ private def execute(eval_context_object, code)
358
+ if options.input_format != :none && options.input_mode != :no_input
277
359
  eval_context_object = input_parser.(eval_context_object)
278
360
  end
279
361
 
280
362
  value = eval_context_object.instance_eval(&code)
281
- unless output_format == :no_output
363
+
364
+ unless options.output_format == :no_output
282
365
  print output_formatter.(value)
283
366
  end
284
367
  rescue Errno::EPIPE
@@ -286,84 +369,91 @@ class Rexe < Struct.new(
286
369
  end
287
370
 
288
371
 
289
- def call
290
- start_time = DateTime.now
372
+ # The global $RC (Rexe Context) OpenStruct is available in your user code.
373
+ # In order to make it possible to access this object in your loaded files, we are not creating
374
+ # it here; instead we add properties to it. This way, you can initialize an OpenStruct yourself
375
+ # in your loaded code and it will still work. If you do that, beware, any properties you add will be
376
+ # included in the log output. If the to_s of your added objects is large, that might be a pain.
377
+ private def init_rexe_context
378
+ $RC ||= OpenStruct.new
379
+ $RC.count = 0
380
+ $RC.rexe_version = VERSION
381
+ $RC.start_time = start_time.iso8601
382
+ $RC.source_code = user_source_code
383
+ $RC.options = options.to_h
291
384
 
292
- prepend_environment_options
293
- parse_command_line
385
+ def $RC.i; count end # `i` aliases `count` so you can more concisely get the count in your user code
386
+ end
294
387
 
295
- user_source_code = ARGV.join(' ')
296
- if user_source_code.empty?
388
+
389
+ private def create_callable
390
+ if user_source_code.empty? && (! options.noop)
297
391
  STDERR.puts "No source code provided. Use -h to display help."
298
392
  exit(-1)
299
393
  end
300
394
 
301
- requires.each { |r| require(r) }
302
- load_global_config_if_exists
303
- loads.each { |file| load(file) }
395
+ eval("Proc.new { #{user_source_code} }")
396
+ end
304
397
 
305
- callable = eval("Proc.new { #{user_source_code} }")
306
398
 
307
- actions = {
399
+ private def lookup_action(mode)
400
+ {
308
401
  line: -> { STDIN.each { |l| execute(l.chomp, callable); $RC.count += 1 } },
309
402
  enumerator: -> { execute(STDIN.each_line, callable); $RC.count += 1 },
310
403
  one_big_string: -> { big_string = STDIN.read; execute(big_string, callable); $RC.count += 1 },
311
404
  no_input: -> { execute(Object.new, callable) }
312
- }
313
-
314
- # This global $RC (Rexe Context) OpenStruct is available in your user code.
315
- # In order to make it possible to access this hash in your loaded files, we are not initializing
316
- # the hash here; instead we add key/value pairs to it. This way, you can initialize a hash yourself
317
- # in your loaded code.
318
- $RC ||= OpenStruct.new
319
- $RC.count = 0
320
- $RC.rexe_version = VERSION
321
- $RC.start_time = start_time.iso8601
322
- $RC.source_code = user_source_code
323
- $RC.options = self.to_h
324
-
325
- def $RC.i; count end # `i` aliases `count` so you can more concisely get the count in your user code
405
+ }[mode]
406
+ end
326
407
 
327
- actions[input_mode].() unless self.noop
328
408
 
329
- if log_format != :none
409
+ private def output_log_entry
410
+ if options.log_format != :none
330
411
  $RC.duration_secs = Time.now - start_time.to_time
331
412
  STDERR.puts(log_formatter.($RC.to_h))
332
413
  end
333
414
  end
334
- end
335
415
 
336
416
 
337
- def input_parser
338
- if @input_parser.nil?
339
- require_format_require(input_format)
340
- @input_parser = input_parsers[input_format]
417
+ # Bypasses Bundler's restriction on loading gems
418
+ # (see https://stackoverflow.com/questions/55144094/bundler-doesnt-permit-using-gems-in-project-home-directory)
419
+ private def require!(the_require)
420
+ begin
421
+ require the_require
422
+ rescue LoadError => error
423
+ gem_path = `gem which #{the_require}`
424
+ if gem_path.chomp.strip.empty?
425
+ raise error # re-raise the error, can't fix it
426
+ else
427
+ load_dir = File.dirname(gem_path)
428
+ $LOAD_PATH << load_dir
429
+ require the_require
430
+ end
431
+ end
341
432
  end
342
- @input_parser
343
- end
344
433
 
345
434
 
346
- def output_formatter
347
- if @output_formatter.nil?
348
- require_format_require(output_format)
349
- @output_formatter = formatters[output_format]
350
- end
351
- @output_formatter
352
- end
435
+ # This class' entry point.
436
+ def call
437
+
438
+ prepend_environment_options
439
+ parse_command_line
353
440
 
441
+ options.requires.each { |r| require!(r) }
442
+ load_global_config_if_exists
443
+ options.loads.each { |file| load(file) }
444
+
445
+ @user_source_code = ARGV.join(' ')
446
+ @callable = create_callable
447
+
448
+ init_rexe_context
449
+ init_parser_and_formatters
354
450
 
355
- def log_formatter
356
- if @log_formatter.nil?
357
- require_format_require(log_format)
358
- @log_formatter = formatters[log_format]
451
+ # This is where the user's source code will be executed; the action will in turn call `execute`.
452
+ lookup_action(options.input_mode).call unless options.noop
453
+
454
+ output_log_entry
359
455
  end
360
- @log_formatter
361
456
  end
362
457
 
363
458
 
364
- # This is needed because the gemspec file loads this file to access Rexe::VERSION
365
- # and must not have it run at that time:
366
- called_as_script = (File.basename($0) == File.basename(__FILE__))
367
- if called_as_script
368
- Bundler.with_clean_env { Rexe.new.call }
369
- end
459
+ Bundler.with_clean_env { Rexe.new.call }