benry-actionrunner 0.1.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.
@@ -0,0 +1,38 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "benry-actionrunner"
5
+ spec.version = "$Release: 0.1.0 $".split()[1]
6
+ spec.author = "kwatch"
7
+ spec.email = "kwatch@gmail.com"
8
+ spec.platform = Gem::Platform::RUBY
9
+ spec.homepage = "https://kwatch.github.io/benry-ruby/benry-actionrunner.html"
10
+ spec.summary = "Action runner or Task runner, like Rake or Gulp."
11
+ spec.description = <<-"END"
12
+ Benry-ActionRunner is a Action runner or Task runner, like Rake or Gulp.
13
+
14
+ Compared to Rake, actions of Benry-ActionRunner can take their own options and arguments.
15
+ For example, `arun hello --lang=fr Alice` runs `hello` action with an option `--lang=fr` and an argument `Alice`.
16
+
17
+ Benry-ActionRunner is also an example application of Benry-CmdApp framework.
18
+
19
+ See #{spec.homepage} for details.
20
+ END
21
+ spec.license = "MIT"
22
+ spec.files = Dir[
23
+ "README.md", "MIT-LICENSE", "CHANGES.md",
24
+ "#{spec.name}.gemspec",
25
+ "lib/**/*.rb", "test/**/*.rb", "bin/*",
26
+ #"doc/*.html", "doc/css/*.css",
27
+ ]
28
+ #spec.executables = []
29
+ spec.bindir = "bin"
30
+ spec.require_path = "lib"
31
+ spec.test_file = "test/run_all.rb"
32
+ #spec.extra_rdoc_files = ["README.md", "CHANGES.md"]
33
+
34
+ spec.required_ruby_version = ">= 2.3"
35
+ spec.add_runtime_dependency "benry-cmdapp" , "~> 1"
36
+ spec.add_runtime_dependency "benry-unixcommand" , "~> 1"
37
+ spec.add_development_dependency "oktest" , "~> 1"
38
+ end
data/bin/arun ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # -*- coding: utf-8 -*-
3
+ # frozen_string_literal: true
4
+
5
+ require 'benry/actionrunner'
6
+
7
+ exit Benry::ActionRunner.main()
@@ -0,0 +1,556 @@
1
+ # -*- coding: utf-8 -*-
2
+ # frozen_string_literal: true
3
+
4
+ ###
5
+ ### $Release: 0.1.0 $
6
+ ### $Copyright: copyright(c) 2023 kwatch@gmail.com $
7
+ ### $License: MIT License $
8
+ ###
9
+
10
+
11
+ require 'benry/cmdapp'
12
+ require 'benry/unixcommand'
13
+
14
+
15
+ #$DRYRUN_MODE = false
16
+
17
+
18
+ module Benry::ActionRunner
19
+
20
+
21
+ VERSION = "$Release: 0.1.0 $".split()[1]
22
+ DOCUMENT_URL = "https://kwatch.github.io/benry-ruby/benry-actionrunner.html"
23
+ DEFAULT_FILENAME = "Actionfile.rb"
24
+
25
+
26
+ class Action < Benry::CmdApp::Action
27
+ include Benry::UnixCommand
28
+
29
+ #def prompt()
30
+ # return "[#{CONFIG.app_command}]$ "
31
+ #end
32
+
33
+ end
34
+
35
+
36
+ app_desc = "Action runner (or task runner), much better than Rake"
37
+ CONFIG = Benry::CmdApp::Config.new(app_desc, VERSION).tap do |config|
38
+ action_file = DEFAULT_FILENAME
39
+ command = File.basename($0)
40
+ config.app_command = command
41
+ #config.app_detail = nil
42
+ x = command
43
+ example = <<END
44
+ $ #{x} -h | less # print help message
45
+ $ #{x} -g # generate action file ('#{action_file}')
46
+ $ less #{action_file} # confirm action file
47
+ $ #{x} # list actions (or: `#{x} -l`)
48
+ $ #{x} -h hello # show help message for 'hello' action
49
+ $ #{x} hello Alice # run 'hello' action with arguments
50
+ Hello, Alice!
51
+ $ #{x} hello Alice -l fr # run 'hello' action with args and options
52
+ Bonjour, Alice!
53
+ $ #{x} : # list prefixes of actions (or '::', ':::')
54
+ $ #{x} xxxx: # list actions starting with 'xxxx:'
55
+ END
56
+ config.help_postamble = {
57
+ "Example:" => example,
58
+ "Document:" => " #{DOCUMENT_URL}\n",
59
+ }
60
+ end
61
+
62
+
63
+ GLOBAL_OPTION_SCHEMA = Benry::CmdApp::GLOBAL_OPTION_SCHEMA_CLASS.new(nil).tap do |schema|
64
+ topics = ["action", "actions", "alias", "aliases",
65
+ "category", "categories", "abbrev", "abbrevs",
66
+ "category1", "categories1", "category2", "categories2",
67
+ "category3", "categories3", "category4", "categories4",
68
+ "metadata"]
69
+ schema.add(:help , "-h, --help", "print help message (of action if specified)")
70
+ schema.add(:version , "-V" , "print version")
71
+ schema.add(:list , "-l" , "list actions")
72
+ schema.add(:topic , "-L <topic>", "topic list (actions|aliases|prefixes|abbrevs)", enum: topics)
73
+ schema.add(:all , "-a" , "list all actions/options including hidden ones")
74
+ schema.add(:file , "-f <file>" , "actionfile name (default: '#{DEFAULT_FILENAME}')")
75
+ schema.add(:search , "-u" , "search for actionfile in parent or upper dir")
76
+ schema.add(:chdir , "-w" , "change current dir to where action file exists")
77
+ schema.add(:searchdir, "-s" , "same as '-up'", hidden: true)
78
+ schema.add(:generate , "-g" , "generate actionfile ('#{DEFAULT_FILENAME}') with example code")
79
+ schema.add(:verbose , "-v" , "verbose mode")
80
+ schema.add(:quiet , "-q" , "quiet mode")
81
+ schema.add(:color , "-c" , "enable color mode")
82
+ schema.add(:nocolor , "-C" , "disable color mode")
83
+ schema.add(:debug , "-D" , "debug mode")
84
+ schema.add(:trace , "-T" , "trace mode")
85
+ schema.add(:dryrun , "-X" , "dry-run mode (not run; just echoback)")
86
+ end
87
+
88
+
89
+ module ApplicationHelpBuilderModule
90
+ def section_options(*args, **kwargs)
91
+ #; [!xsfzi] adds '--<name>=<value>' to help message.
92
+ arr = ["--<name>=<value>", "set a global variable (value can be in JSON format)"]
93
+ s = super
94
+ s += (@config.format_option % arr) + "\n"
95
+ return s
96
+ end
97
+ end
98
+ Benry::CmdApp::APPLICATION_HELP_BUILDER_CLASS.prepend(ApplicationHelpBuilderModule)
99
+
100
+
101
+ class GlobalOptionParser < Benry::CmdApp::GLOBAL_OPTION_PARSER_CLASS
102
+
103
+ def initialize(schema, &callback)
104
+ super
105
+ @callback = callback
106
+ end
107
+
108
+ def handle_unknown_long_option(optstr, name, value)
109
+ return super if value == nil
110
+ return super if @callback == nil
111
+ @callback.call(name, value, optstr)
112
+ end
113
+
114
+ end
115
+
116
+
117
+ def self.main(argv=ARGV)
118
+ #; [!wmcup] handles '$ACRIONRUNNER_OPTION' value.
119
+ envstr = ENV["ACTIONRUNNER_OPTION"]
120
+ if envstr && ! envstr.empty?
121
+ argv = envstr.split() + argv
122
+ end
123
+ app = MainApplication.new(CONFIG, GLOBAL_OPTION_SCHEMA)
124
+ status_code = app.main(argv)
125
+ #; [!hujvl] returns status code.
126
+ return status_code
127
+ end
128
+
129
+
130
+ class MainApplication < Benry::CmdApp::Application
131
+
132
+ def initialize(*args, **kwargs)
133
+ super
134
+ @flag_search = false # true when '-s' option specified
135
+ @flag_chdir = false # true when '-w' option specified
136
+ @action_file = DEFAULT_FILENAME # ex: 'Actionfile.rb'
137
+ @global_vars = {} # names and values of global vars
138
+ @_loaded = false # true when action file loaded
139
+ end
140
+
141
+ protected
142
+
143
+ def parse_global_options(args)
144
+ #; [!bpedh] parses `--<name>=<val>` as global variables.
145
+ @global_vars = {}
146
+ parser = GlobalOptionParser.new(@option_schema) do |name, value, _optstr|
147
+ @global_vars[name] = value
148
+ end
149
+ #; [!0tz4j] stops parsing options when any argument found.
150
+ global_opts = parser.parse(args, all: false) # raises OptionError
151
+ #; [!gkp9b] returns global options.
152
+ return global_opts
153
+ end
154
+
155
+ def toggle_global_options(global_opts) # override
156
+ #; [!3kdds] global option '-C' sets `$COLOR_mode = false`.
157
+ global_opts[:color] = false if global_opts[:nocolor]
158
+ super
159
+ d = global_opts
160
+ #; [!1882x] global option '-u' sets instance var.
161
+ #; [!bokge] global option '-w' sets instance var.
162
+ @flag_search = true if d[:search] || d[:searchdir]
163
+ @flag_chdir = true if d[:chdir] || d[:searchdir]
164
+ #; [!4sk24] global option '-f' changes filename.
165
+ @action_file = d[:file] if d[:file]
166
+ #; [!9u400] sets `$BENRY_ECHOBACK = true` if option `-v` specified.
167
+ #; [!jp2mw] sets `$BENRY_ECHOBACK = false` if option `-q` specified.
168
+ $BENRY_ECHOBACK = true if d[:verbose]
169
+ $BENRY_ECHOBACK = false if d[:quiet]
170
+ nil
171
+ end
172
+
173
+ def handle_global_options(global_opts, args) # override
174
+ #; [!psrmp] loads action file (if exists) before displaying help message.
175
+ if global_opts[:help]
176
+ load_action_file(required: false)
177
+ return super
178
+ end
179
+ #; [!9wfaw] loads action file before listing actions by '-l' or '-L' option.
180
+ #; [!qanx2] option '-l' and '-L' requires action file.
181
+ if global_opts[:list] || global_opts[:topic]
182
+ load_action_file(required: true)
183
+ return super
184
+ end
185
+ #; [!7995e] option '-g' generates action file.
186
+ if global_opts[:generate]
187
+ generate_action_file()
188
+ return 0
189
+ end
190
+ #; [!k5nuk] option '-h' or '--help' prints help message.
191
+ #; [!dmxt2] option '-V' prints version number.
192
+ #; [!i4qm5] option '-v' sets `$VERBOSE_MODE = true`.
193
+ #; [!5nwnv] option '-q' sets `$QUIET_MODE = true`.
194
+ #; [!klxkr] option '-c' sets `$COLOR_MODE = true`.
195
+ #; [!kqbwd] option '-C' sets `$COLOR_MODE = false`.
196
+ #; [!oce46] option '-D' sets `$DEBUG_MODE = true`.
197
+ #; [!mq5ko] option '-T' enables trace mode.
198
+ #; [!jwah3] option '-X' sets `$DRYRUN_MODE = true`.
199
+ return super
200
+ end
201
+
202
+ def handle_action(args, global_opts)
203
+ #; [!qdrui] loads action file before performing actions.
204
+ #; [!4992c] raises error if action specified but action file not exist.
205
+ load_action_file(required: (args[0] != "help"))
206
+ return super
207
+ end
208
+
209
+ def skip_backtrace?(bt) # override
210
+ #; [!k8ddu] ignores backtrace of 'actionrunner.rb'.
211
+ #; [!89mz3] ignores backtrace of 'kernel_require.rb'.
212
+ #; [!ttt98] ignores backtrace of 'arun'.
213
+ return true if bt.include?(__FILE__)
214
+ return true if bt.include?('/core_ext/kernel_require.rb')
215
+ return true if bt.include?('/arun:')
216
+ #; [!z72yj] not ignore backtrace of others.
217
+ return false
218
+ end
219
+
220
+ private
221
+
222
+ def load_action_file(required: true)
223
+ #; [!nx22j] returns false if action file already loaded.
224
+ return false if @_loaded
225
+ #; [!aov55] loads action file if exists.
226
+ filename = @action_file or raise "** internal error"
227
+ brownie = Brownie.new(@config)
228
+ filepath = brownie.search_and_load_action_file(filename, @flag_search, @flag_chdir)
229
+ #; [!ssmww] raises error when `required: true` and action file not exist.
230
+ filepath != nil || ! required or
231
+ raise Benry::CmdApp::CommandError,
232
+ "Action file ('#{filename}') not found." \
233
+ " Create it by `#{@config.app_command} -g` command firstly."
234
+ #; [!vwtwe] it is AFTER loading action file to set global variables.
235
+ brownie.populate_global_variables(@global_vars)
236
+ @global_vars.clear()
237
+ #; [!8i55a] prevents to load action file more than once.
238
+ @_loaded = true
239
+ #; [!f68yv] returns true if action file loaded successfully.
240
+ return true
241
+ end
242
+
243
+ def generate_action_file(quiet: $QUIET_MODE)
244
+ #; [!dta7r] generates action file.
245
+ filename = @action_file or raise "** internal error"
246
+ brownie = Brownie.new(@config)
247
+ content = brownie.render_action_file_content(filename)
248
+ #; [!tmlqt] prints action file content if filename is '-'.
249
+ #; [!ymrjh] prints action file content if stdout is not a tty.
250
+ if filename == "-" || ! $stdout.tty?
251
+ print content
252
+ #; [!9e3c0] returns nil if action file is not generated.
253
+ return nil
254
+ end
255
+ #; [!685cq] raises error if action file already exist.
256
+ ! File.exist?(filename) or
257
+ raise Benry::CmdApp::CommandError,
258
+ "Action file ('#{filename}') already exists." \
259
+ " If you want to generate a new one, delete it first."
260
+ File.write(filename, content, encoding: 'utf-8')
261
+ #; [!n09pl] reports result if action file generated successfully.
262
+ #; [!iq0p2] reports nothing if option '-q' specified.
263
+ puts "[OK] Action file '#{filename}' generated." unless quiet
264
+ #; [!bf60l] returns action file name if generated.
265
+ return filename
266
+ end
267
+
268
+ end
269
+
270
+
271
+ class Brownie
272
+
273
+ def initialize(config)
274
+ @config = config
275
+ end
276
+
277
+ def search_and_load_action_file(filename, flag_search, flag_chdir, _pwd: Dir.pwd())
278
+ #; [!c9e1h] if action file exists in current dir, load it regardless of options.
279
+ #; [!m5oj7] if action file exists in parent dir, find and load it if option '-s' specified.
280
+ if File.exist?(filename) ; dir = "."
281
+ elsif flag_search ; dir = _search_dir_where_file_exist(filename, _pwd)
282
+ else ; dir = nil
283
+ end
284
+ #; [!079xs] returns nil if action file not found.
285
+ #; [!7simq] changes current dir to where action file exists if option '-w' specified.
286
+ #; [!dg2qv] action file can has directory path.
287
+ if dir == nil ; return nil
288
+ elsif dir == "." ; fpath = filename
289
+ elsif flag_chdir ; fpath = filename ; _chdir(dir)
290
+ else ; fpath = File.join(dir, filename)
291
+ end
292
+ #; [!d987b] loads action file if exists.
293
+ abspath = File.absolute_path(fpath)
294
+ require abspath
295
+ #; [!x9xxl] returns absolute path of action file if exists.
296
+ return abspath
297
+ end
298
+
299
+ private
300
+
301
+ def _search_dir_where_file_exist(filename, dir=Dir.pwd(), max=20)
302
+ n = -1
303
+ found = while (n += 1) < max
304
+ break true if File.exist?(File.join(dir, filename))
305
+ parent = File.dirname(dir)
306
+ break false if parent == dir
307
+ dir = parent
308
+ end
309
+ return nil unless found
310
+ return "." if n == 0
311
+ return ".." if n == 1
312
+ return ("../" * n).chomp("/")
313
+ end
314
+
315
+ def _chdir(dir)
316
+ Action.new(@config).instance_eval { cd(dir) }
317
+ nil
318
+ end
319
+
320
+ public
321
+
322
+ def populate_global_variables(global_vars)
323
+ #; [!cr2ph] sets global variables.
324
+ #; [!3kow3] normalizes global variable names.
325
+ #; [!03x7t] decodes JSON string into Ruby object.
326
+ #; [!1ol4a] print global variables if debug mode is on.
327
+ global_vars.each do |name, str|
328
+ var = name.gsub(/[^\w]/, '_')
329
+ val = _decode_value(str)
330
+ eval "$#{var} = #{val.inspect}"
331
+ _debug_global_var(var, val) if $DEBUG_MODE
332
+ end
333
+ nil
334
+ end
335
+
336
+ private
337
+
338
+ def _decode_value(str)
339
+ #; [!omxyf] decodes string as JSON format.
340
+ require 'json' unless defined?(JSON)
341
+ return JSON.load(str)
342
+ rescue JSON::ParserError
343
+ #; [!tvwvn] returns string as it is if failed to parse as JSON.
344
+ return str
345
+ end
346
+
347
+ def _debug_global_var(var, val)
348
+ #; [!05l5f] prints var name and it's value.
349
+ #; [!7lwvz] colorizes output if color mode enabled.
350
+ msg = "[DEBUG] $#{var} = #{val.inspect}"
351
+ msg = @config.deco_debug % msg if Benry::CmdApp::Util.color_mode?
352
+ puts msg
353
+ $stdout.flush()
354
+ end
355
+
356
+ public
357
+
358
+ def render_action_file_content(filename)
359
+ #; [!oc03q] returns content of action file.
360
+ #content = DATA.read()
361
+ content = File.read(__FILE__, encoding: 'utf-8').split(/\n__END__\n/)[-1]
362
+ content = content.gsub('%COMMAND%', @config.app_command)
363
+ content = content.gsub('%ACTIONFILE%', filename)
364
+ return content
365
+ end
366
+
367
+ end
368
+
369
+
370
+ module Export
371
+
372
+ CONFIG = Benry::ActionRunner::CONFIG
373
+ Action = Benry::ActionRunner::Action
374
+
375
+ module_function
376
+
377
+ def define_alias(alias_name, action_name, tag: nil, important: nil, hidden: nil)
378
+ return Benry::CmdApp.define_alias(alias_name, action_name, tag: tag, important: important, hidden: hidden)
379
+ end
380
+
381
+ def undef_alias(alias_name)
382
+ return Benry::CmdApp.undef_alias(alias_name)
383
+ end
384
+
385
+ def undef_action(action_name)
386
+ return Benry::CmdApp.undef_action(action_name)
387
+ end
388
+
389
+ def define_abbrev(abbrev, prefix)
390
+ return Benry::CmdApp.define_abbrev(abbrev, prefix)
391
+ end
392
+
393
+ def undef_abbrev(abbrev)
394
+ return Benry::CmdApp.undef_abbrev(abbrev)
395
+ end
396
+
397
+ def current_app()
398
+ return Benry::CmdApp.current_app()
399
+ end
400
+
401
+ end
402
+
403
+
404
+ end
405
+
406
+
407
+ if __FILE__ == $0
408
+ exit Benry::ActionRunner.main()
409
+ end
410
+
411
+
412
+ __END__
413
+ # -*- coding: utf-8 -*-
414
+ # frozen_string_literal: true
415
+
416
+
417
+ ##
418
+ ## @(#) Action file for '%COMMAND%' command.
419
+ ##
420
+ ## Example:
421
+ ##
422
+ ## $ %COMMAND% -h | less # print help message
423
+ ## $ %COMMAND% -g # generate action file ('%ACTIONFILE%')
424
+ ## $ less %ACTIONFILE% # confirm action file
425
+ ## $ %COMMAND% # list actions (or: `%COMMAND% -l`)
426
+ ##
427
+ ## $ %COMMAND% -h hello # show help message for 'hello' action
428
+ ## $ %COMMAND% hello Alice # run 'hello' action with arguments
429
+ ## Hello, Alice!
430
+ ## $ %COMMAND% hello -l fr # run 'hello' action with options
431
+ ## Bonjour, world!
432
+ ##
433
+ ## $ %COMMAND% : # list prefixes of actions (or '::', ':::', etc)
434
+ ## $ %COMMAND% git: # list actions filtered by prefix "git:"
435
+ ## $ %COMMAND% git # run 'git' action (or alias)
436
+ ##
437
+
438
+
439
+ require 'benry/actionrunner'
440
+
441
+ include Benry::ActionRunner::Export
442
+
443
+
444
+ ##
445
+ ## Define actions
446
+ ##
447
+ class MyAction < Action
448
+
449
+ @action.("print greeting message")
450
+ @option.(:lang, "-l, --lang=<lang>", "language (en/fr/it)")
451
+ def hello(name="world", lang: "en")
452
+ case lang
453
+ when "en" ; puts "Hello, #{name}!"
454
+ when "fr" ; puts "Bonjour, #{name}!"
455
+ when "it" ; puts "Chao, #{name}!"
456
+ else
457
+ raise "#{lang}: Unknown language."
458
+ end
459
+ end
460
+
461
+ @action.("delete garbage files (and product files too if '-a')")
462
+ @option.(:all, "-a, --all", "delete product files, too")
463
+ def clean(all: false)
464
+ rm :rf, GARBAGE_FILES if ! GARBAGE_FILES.empty?
465
+ rm :rf, PRODUCT_FILES if ! PRODUCT_FILES.empty? && all == true
466
+ end
467
+
468
+ end
469
+
470
+ GARBAGE_FILES = ["*~", "*.tmp"] # will be deleted by `arun clean`
471
+ PRODUCT_FILES = ["*.gem"] # will be deleted by `arun clean --all`
472
+
473
+
474
+ ##
475
+ ## Define action with prefix ('git:')
476
+ ##
477
+ class GitAction < Action
478
+ #prefix "git:"
479
+ #prefix "git:", action: "status:here" # rename 'git:status:here' action to 'git'
480
+ category "git:", alias_for: "status:here" # define 'git' as an alias of 'git:status:here' action
481
+
482
+ @action.("show status in compact format")
483
+ def status(*files)
484
+ sys "git status -sb #{files.join(' ')}"
485
+ end
486
+
487
+ @action.("show status of current directory")
488
+ def status__here() # method name 'x__y__z' => action name 'x:y:z'
489
+ sys "git status -sb ."
490
+ end
491
+
492
+ @action.("put changes of files into staging area")
493
+ @option.(:interactive, "-i", "select changes interactively")
494
+ def stage(file, *files, interactive: false)
495
+ opts = []
496
+ opts << " -i" if interactive
497
+ sys "git add#{opts.join(' ')} #{file.join(' ')}"
498
+ end
499
+
500
+ @action.("show changes in staging area")
501
+ def staged()
502
+ sys "git diff --cached"
503
+ end
504
+
505
+ @action.("remove changes from staging area")
506
+ def unstage()
507
+ sys "git reset HEAD"
508
+ end
509
+
510
+ end
511
+
512
+
513
+ ##
514
+ ## Example of aliases
515
+ ##
516
+ define_alias "stage" , "git:stage"
517
+ define_alias "staged" , "git:staged"
518
+ define_alias "unstage" , "git:unstage"
519
+
520
+
521
+ ##
522
+ ## More example
523
+ ##
524
+ $project = "example"
525
+ $release = "1.0.0"
526
+
527
+ class BuildAction < Action
528
+ category "build:", action: "all"
529
+ #prefix "build:", alias_of: "all"
530
+
531
+ def target_name()
532
+ return "#{$project}-#{$release}"
533
+ end
534
+
535
+ ## hidden action
536
+ @action.("prepare directory", hidden: true) # hidden action
537
+ def prepare()
538
+ dir = target_name()
539
+ mkdir dir unless File.directory?(dir)
540
+ end
541
+
542
+ @action.("create zip file")
543
+ def zip_() # last '_' char avoids to override existing method
544
+ run_once "prepare" # run prerequisite action only once
545
+ dir = target_name()
546
+ store "README.md", "Rakefile.rb", "lib/**/*", "test/**/*", to: dir
547
+ sys "zip -r #{dir}.zip #{dir}"
548
+ sys "unzip -l #{dir}.zip"
549
+ end
550
+
551
+ @action.("create all")
552
+ def all()
553
+ run_once "zip" # run prerequisite action only once
554
+ end
555
+
556
+ end