rexe 0.12.0 → 0.13.0

Sign up to get free protection for your applications and to get access to all the features.
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 }