benry-actionrunner 0.1.0

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