runfile 0.12.0 → 1.0.0.rc1

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +108 -37
  3. data/bin/runn +5 -0
  4. data/examples/default-action/runfile +9 -0
  5. data/examples/default-action-2/runfile +11 -0
  6. data/examples/default-action-2/server.runfile +6 -0
  7. data/examples/different-name/runfile.rb +8 -0
  8. data/examples/example/runfile +10 -0
  9. data/examples/example-multiline/runfile +16 -0
  10. data/examples/execute/runfile +14 -0
  11. data/examples/execute/server.runfile +11 -0
  12. data/examples/full/runfile +24 -0
  13. data/examples/import/more_tasks/spec.runfile +7 -0
  14. data/examples/import/runfile +10 -0
  15. data/examples/import/tasks/server.runfile +13 -0
  16. data/examples/minimal/runfile +4 -0
  17. data/examples/multiple-runfiles/runfile +11 -0
  18. data/examples/multiple-runfiles/server.runfile +13 -0
  19. data/examples/named-only/deploy.runfile +7 -0
  20. data/examples/named-only/server.runfile +11 -0
  21. data/examples/naval-fate/runfile +47 -0
  22. data/examples/shortcut/runfile +31 -0
  23. data/lib/runfile/action.rb +40 -14
  24. data/lib/runfile/concerns/dsl.rb +132 -0
  25. data/lib/runfile/concerns/inspectable.rb +13 -0
  26. data/lib/runfile/concerns/renderable.rb +16 -0
  27. data/lib/runfile/entrypoint.rb +50 -0
  28. data/lib/runfile/exceptions.rb +13 -0
  29. data/lib/runfile/gem_finder.rb +29 -0
  30. data/lib/runfile/initiator.rb +87 -0
  31. data/lib/runfile/meta.rb +65 -0
  32. data/lib/runfile/runner.rb +10 -173
  33. data/lib/runfile/templates/runfile +13 -0
  34. data/lib/runfile/userfile.rb +111 -0
  35. data/lib/runfile/version.rb +2 -2
  36. data/lib/runfile/views/initiator.gtx +28 -0
  37. data/lib/runfile/views/userfile.gtx +93 -0
  38. data/lib/runfile.rb +13 -10
  39. metadata +98 -24
  40. data/bin/run +0 -10
  41. data/bin/run! +0 -16
  42. data/lib/runfile/compatibility.rb +0 -84
  43. data/lib/runfile/docopt_helper.rb +0 -128
  44. data/lib/runfile/dsl.rb +0 -90
  45. data/lib/runfile/refinements.rb +0 -22
  46. data/lib/runfile/runfile_helper.rb +0 -165
  47. data/lib/runfile/settings.rb +0 -34
  48. data/lib/runfile/setup.rb +0 -19
  49. data/lib/runfile/templates/Runfile +0 -16
  50. data/lib/runfile/terminal.rb +0 -32
@@ -0,0 +1,132 @@
1
+ module Runfile
2
+ module DSL
3
+ # Commands
4
+
5
+ attr_reader :default_action
6
+
7
+ def action(name = nil, shortcut = nil, &block)
8
+ current_action.block = block
9
+ current_action.name = name
10
+ current_action.shortcut = shortcut if shortcut
11
+ current_action.prefix = action_prefix if action_prefix
12
+
13
+ # if default_action && name
14
+ # raise SyntaxError, <<~ERROR
15
+ # Cannot define action nub`#{name}` since a default action was already defined
16
+ # ERROR
17
+ # end
18
+
19
+ actions[name || :default] = current_action
20
+ @default_action = current_action unless name
21
+ @current_action = nil
22
+ end
23
+
24
+ def env_var(name, help)
25
+ env_vars[name] = help
26
+ end
27
+
28
+ def example(text)
29
+ examples.push text
30
+ end
31
+
32
+ def help(message)
33
+ current_action.help = message
34
+ end
35
+
36
+ def import(pathspec)
37
+ imports.push pathspec
38
+ end
39
+
40
+ def import_gem(gem_name, runfile_path, context = nil)
41
+ if context && !context.is_a?(Hash)
42
+ raise SyntaxError, <<~ERROR
43
+ The third argument to nub`import_gem` must be a hash
44
+ got rb`#{context.inspect}`
45
+ ERROR
46
+ end
47
+
48
+ path = GemFinder.find gem_name, runfile_path
49
+ imports.push path
50
+ contexts[runfile_path] = context if context
51
+ end
52
+
53
+ def option(name, help)
54
+ options[name] = help
55
+ end
56
+
57
+ def param(name, help)
58
+ params[name] = help
59
+ end
60
+
61
+ def shortcut(name)
62
+ current_action.shortcut = name
63
+ end
64
+
65
+ def summary(text = nil)
66
+ return @summary unless text
67
+
68
+ @summary = text
69
+ end
70
+
71
+ def title(text = nil)
72
+ return @title unless text
73
+
74
+ @title = text
75
+ end
76
+
77
+ def usage(message)
78
+ message = "#{action_prefix} #{message}" if action_prefix
79
+ current_action.usages.push message
80
+ end
81
+
82
+ def version(text = nil)
83
+ return @version unless text
84
+
85
+ @version = text
86
+ end
87
+
88
+ def execute(command_line)
89
+ run Shellwords.split(command_line)
90
+ end
91
+
92
+ # Evaluation Artifacts
93
+
94
+ def action_prefix
95
+ nil
96
+ end
97
+
98
+ def contexts
99
+ @contexts ||= {}
100
+ end
101
+
102
+ def actions
103
+ @actions ||= {}
104
+ end
105
+
106
+ def params
107
+ @params ||= {}
108
+ end
109
+
110
+ def env_vars
111
+ @env_vars ||= {}
112
+ end
113
+
114
+ def examples
115
+ @examples ||= []
116
+ end
117
+
118
+ def options
119
+ @options ||= {}
120
+ end
121
+
122
+ def imports
123
+ @imports ||= []
124
+ end
125
+
126
+ private
127
+
128
+ def current_action
129
+ @current_action ||= Action.new
130
+ end
131
+ end
132
+ end
@@ -0,0 +1,13 @@
1
+ module Runfile
2
+ module Inspectable
3
+ def inspect
4
+ return %[#<#{self.class}>] unless inspectable
5
+
6
+ %[#<#{self.class} #{inspectable.map { |k, v| "#{k}=#{v.inspect}" }.join(', ')}>]
7
+ end
8
+
9
+ def inspectable
10
+ nil
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ module Runfile
2
+ module Renderable
3
+ include Colsole
4
+
5
+ def render(view, context: nil)
6
+ path = "#{base_view_path}/#{view}.gtx"
7
+ GTX.render_file path, context: context, filename: path
8
+ end
9
+
10
+ private
11
+
12
+ def base_view_path
13
+ @base_view_path ||= ::File.expand_path '../views', __dir__
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,50 @@
1
+ module Runfile
2
+ # Serves as the initial entrypoint when running +run+.
3
+ class Entrypoint
4
+ include Inspectable
5
+ include Renderable
6
+ include Colsole
7
+
8
+ attr_reader :argv
9
+
10
+ def initialize(argv = ARGV)
11
+ @argv = argv
12
+ end
13
+
14
+ def inspectable
15
+ { argv: argv }
16
+ end
17
+
18
+ def run
19
+ meta.handler(argv).run argv
20
+ end
21
+
22
+ def run!
23
+ run
24
+ rescue Runfile::ExitWithUsage => e
25
+ say e.message
26
+ 1
27
+ rescue Runfile::UserError => e
28
+ puts e.backtrace.reverse if ENV['DEBUG']
29
+ say! "mib` #{e.class} `"
30
+ say! e.message
31
+ 1
32
+ rescue Interrupt
33
+ say! 'm`Goodbye`', replace: true
34
+ 1
35
+ rescue => e
36
+ puts e.backtrace.reverse if ENV['DEBUG']
37
+ origin = e.backtrace_locations.first
38
+ location = "#{origin.path}:#{origin.lineno}"
39
+ say! "rib` #{e.class} ` in nu`#{location}`"
40
+ say! e.message
41
+ 1
42
+ end
43
+
44
+ private
45
+
46
+ def meta
47
+ @meta ||= Meta.new
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module Runfile
2
+ # All exceptions are rescued in bin/run, but using different styling
3
+ # Note that UserError types are displayed without location and are intended
4
+ # to be used when location is not important.
5
+
6
+ class Error < StandardError; end
7
+ class UserError < Error; end
8
+ class ExitWithUsage < Error; end
9
+
10
+ class ActionNotFound < UserError; end
11
+ class GemNotFound < UserError; end
12
+ class SyntaxError < UserError; end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Runfile
2
+ # Finds the path of an installed or bundled gem
3
+ # Adapted from Rubocop <https://github.com/rubocop/rubocop/blob/master/lib/rubocop/config_loader_resolver.rb#L268>
4
+ class GemFinder
5
+ class << self
6
+ def find(gem_name, file = nil)
7
+ gem_path = find_gem_path gem_name
8
+ file ? File.join(gem_path, file) : gem_path
9
+ rescue Gem::LoadError
10
+ raise GemNotFound, "Cannot import gem nub`#{gem_name}`\nTry running g`gem install #{gem_name}`"
11
+ end
12
+
13
+ private
14
+
15
+ def find_gem_path(gem_name)
16
+ if bundler?
17
+ gem = Bundler.load.specs[gem_name].first
18
+ gem_path = gem.full_gem_path if gem
19
+ end
20
+
21
+ gem_path || Gem::Specification.find_by_name(gem_name).gem_dir
22
+ end
23
+
24
+ def bundler?
25
+ defined? Bundler
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,87 @@
1
+ require 'fileutils'
2
+
3
+ module Runfile
4
+ # Creates a new runfile when running +run new+ in a directory without
5
+ # runfiles.
6
+ class Initiator
7
+ include Inspectable
8
+ include Renderable
9
+
10
+ def run(argv = ARGV)
11
+ Runner.run docopt, argv: argv, version: VERSION do |args|
12
+ execute_command args
13
+ end
14
+ end
15
+
16
+ private
17
+
18
+ def execute_command(args)
19
+ return create_new_file if args['new']
20
+
21
+ if args['NAME']
22
+ create_example args['NAME']
23
+ else
24
+ list_examples
25
+ end
26
+ end
27
+
28
+ def create_new_file
29
+ template = File.expand_path 'templates/runfile', __dir__
30
+ FileUtils.cp template, '.'
31
+ say 'Created g`runfile`'
32
+ say_tip
33
+ end
34
+
35
+ def list_examples
36
+ say "nu`Available Examples`\n"
37
+ examples.each { |x| puts " #{x}" }
38
+ say ''
39
+ say <<~TIP
40
+ Run g`run example EXAMPLE` with any of these examples to copy the files
41
+ to the current directory.
42
+
43
+ If you are not sure, try g`run example naval-fate`.
44
+ TIP
45
+ end
46
+
47
+ def create_example(name)
48
+ dir = "#{examples_dir}/#{name}"
49
+ files = Dir["#{dir}/{runfile,*.runfile,*.rb}"]
50
+ raise UserError, "No such example: nu`#{name}`" if files.empty?
51
+
52
+ files.each { |file| safe_copy file }
53
+ say_tip
54
+ end
55
+
56
+ def say_tip
57
+ say ''
58
+ say 'Run g`run` or g`run --help` to see your runfile'
59
+ say 'Delete the copied files to go back to the initial state'
60
+ end
61
+
62
+ def safe_copy(file)
63
+ target = File.basename file
64
+ # This will never happen since if there is a runfile, the initiator will
65
+ # not be called. Nonetheless, keep it for safety
66
+ return if File.exist? target
67
+
68
+ FileUtils.cp file, '.'
69
+ say "Copied g`#{target}`"
70
+ end
71
+
72
+ def examples
73
+ @examples ||= Dir["#{examples_dir}/*"]
74
+ .select { |f| File.directory? f }
75
+ .map { |f| File.basename f }
76
+ .sort
77
+ end
78
+
79
+ def examples_dir
80
+ @examples_dir ||= File.expand_path '../../examples', __dir__
81
+ end
82
+
83
+ def docopt
84
+ @docopt ||= render('initiator', context: self)
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,65 @@
1
+ module Runfile
2
+ # Holds meta information about the state of runfiles in a given directory
3
+ class Meta
4
+ include Inspectable
5
+
6
+ MASTERFILE_NAMES = %w[runfile Runfile runfile.rb]
7
+
8
+ def masterfile_path
9
+ @masterfile_path ||= begin
10
+ result = nil
11
+
12
+ MASTERFILE_NAMES.each do |name|
13
+ if File.exist? name
14
+ result = name
15
+ break
16
+ end
17
+ end
18
+
19
+ result
20
+ end
21
+ end
22
+
23
+ def handler(_argv = ARGV)
24
+ masterfile || dummy || initiator
25
+ end
26
+
27
+ # If there are external files and no masterfile, we will use a dummy empty
28
+ # runfile as a masterfile to serve as the access point to the named
29
+ # runfiles
30
+ def dummy
31
+ return nil unless external_files.any?
32
+
33
+ Userfile.new
34
+ end
35
+
36
+ def initiator
37
+ Initiator.new
38
+ end
39
+
40
+ def masterfile
41
+ return nil unless masterfile_path
42
+
43
+ @masterfile ||= Userfile.load_file masterfile_path
44
+ end
45
+
46
+ # def title
47
+ # masterfile&.title
48
+ # end
49
+
50
+ # def summary
51
+ # masterfile&.summary
52
+ # end
53
+
54
+ def globs
55
+ @globs ||= (masterfile ? ['*'] + masterfile.imports : ['*'])
56
+ end
57
+
58
+ def external_files
59
+ @external_files ||= globs
60
+ .map { |glob| Dir["#{glob}.runfile"].sort }
61
+ .flatten
62
+ .to_h { |file| [File.basename(file, '.runfile'), Userfile.load_file(file)] }
63
+ end
64
+ end
65
+ end
@@ -1,183 +1,20 @@
1
- require 'singleton'
2
- require 'docopt'
3
-
4
1
  module Runfile
5
-
6
- # The Runner class is the main workhorse behind Runfile.
7
- # It handles all the Runfile DSL commands and executes the
8
- # Runfile with the help of two more specialized classes:
9
- # 1. DocoptHelper - for deeper docopt related actions
10
- # 2. RunfileHelper - for Runfile creation and system wide search
2
+ # Executes docopt.
11
3
  class Runner
12
- include Singleton
13
- include SettingsMixin
14
-
15
- attr_accessor :last_usage, :last_help, :name, :version,
16
- :summary, :namespace, :superspace, :actions, :examples, :options,
17
- :params, :env_vars
18
-
19
- # Initialize all variables to sensible defaults.
20
- def initialize
21
- @superspace = nil # used when filename != Runfile
22
- @last_usage = nil # dsl: usage
23
- @last_help = nil # dsl: help
24
- @namespace = nil # dsl: command
25
- @actions = {} # dsl: action
26
- @options = {} # dsl: option
27
- @params = {} # dsl: param
28
- @examples = [] # dsl: example
29
- @env_vars = {} # dsl: env_var
30
- @name = "Runfile" # dsl: name
31
- @version = false # dsl: version
32
- @summary = false # dsl: summary
33
- end
34
-
35
- # Load and execute a Runfile call.
36
- def execute(argv, filename='Runfile')
37
- @ignore_settings = !filename
38
- argv = expand_shortcuts argv
39
- filename and File.file?(filename) or handle_no_runfile argv
40
-
41
- begin
42
- load settings.helper if settings.helper
43
- load filename
44
- rescue => ex
45
- abort "Runfile error:\n#{ex.message}\n#{ex.backtrace[0]}"
46
- end
47
- run(*argv)
48
- end
49
-
50
- # Add an action to the @actions array, and use the last known
51
- # usage and help messages sent by the DSL.
52
- def add_action(name, altname = nil, &block)
53
- if @last_usage.nil?
54
- @last_usage = altname ? "(#{name}|#{altname})" : name
55
- end
56
- [@namespace, @superspace].each do |prefix|
57
- prefix or next
58
- name = "#{prefix}_#{name}"
59
- @last_usage = "#{prefix} #{last_usage}" unless @last_usage == false
60
- end
61
- name = name.to_sym
62
- @actions[name] = Action.new(block, @last_usage, @last_help)
63
- @last_usage = nil
64
- @last_help = nil
65
- if altname
66
- @last_usage = false
67
- add_action(altname, nil, &block)
68
- end
69
- end
70
-
71
- # Add an option flag and its help text.
72
- def add_option(flag, text, scope = nil)
73
- scope ||= 'Options'
74
- @options[scope] ||= {}
75
- @options[scope][flag] = text
76
- end
77
-
78
- # Add a patameter and its help text.
79
- def add_param(name, text, scope = nil)
80
- scope ||= 'Parameters'
81
- @params[scope] ||= {}
82
- @params[scope][name] = text
83
- end
84
-
85
- # Add env_var command.
86
- def add_env_var(name, text, scope = nil)
87
- scope ||= 'Environment Variables'
88
- @env_vars[scope] ||= {}
89
- @env_vars[scope][name] = text
90
- end
91
-
92
- # Add example command.
93
- def add_example(command)
94
- @examples << (@namespace ? "#{@namespace} #{command}" : command)
95
- end
96
-
97
- # Run the command. This is a wrapper around docopt. It will
98
- # generate the docopt document on the fly, using all the
99
- # information collected so far.
100
- def run(*argv)
101
- begin
102
- docopt_exec argv
103
- rescue Docopt::Exit => ex
104
- puts ex.message
105
- exit 2
4
+ class << self
5
+ def run(docopt, version: nil, argv: nil)
6
+ args = call_docopt docopt, argv: argv, version: version
7
+ yield args if block_given?
8
+ args
106
9
  end
107
- end
108
-
109
- # Invoke action from another action. Used by the DSL's #execute
110
- # function. Expects to get a single string that looks as if
111
- # it was typed in the command prompt.
112
- def cross_call(command_string)
113
- argv = command_string.split(/\s(?=(?:[^"]|"[^"]*")*$)/)
114
- begin
115
- docopt_exec argv
116
- rescue Docopt::Exit => ex
117
- puts "Cross call failed: #{command_string}"
118
- abort ex.message
119
- end
120
- end
121
10
 
122
11
  private
123
12
 
124
- # Call the docopt parser and execute the action with the
125
- # parsed arguments.
126
- # This should always be called in a begin...rescue block and
127
- # you should handle the Docopt::Exit exception.
128
- def docopt_exec(argv)
129
- helper = DocoptHelper.new(self)
130
- args = helper.args argv
131
- action = find_action argv
132
- action or abort "Runfile error: Action not found"
133
- @actions[action].execute args
134
- end
135
-
136
- # Inspect the first three arguments in the argv and look for
137
- # a matching action.
138
- # We will first look for a_b_c action, then for a_b and
139
- # finally for a. This is intended to allow "overloading" of
140
- # the command as an action (e.g. also allow a global action
141
- # called 'a').
142
- # if no command was found, but we have a :global command,
143
- # assume this is the requested one (since we will not reach
144
- # this point unless the usage pattern matches).
145
- def find_action(argv)
146
- 3.downto(1).each do |count|
147
- next unless argv.size >= count
148
- action = argv[0..count-1].join('_').to_sym
149
- return action if @actions.has_key? action
150
- end
151
- return :global if @actions.has_key? :global
152
- return "#{superspace}_global".to_sym if @superspace and @actions.has_key? "#{superspace}_global".to_sym
153
- return false
154
- end
155
-
156
- # When `run` is called without a Runfile (or runfile not
157
- # found), hand over handling to the RunfileHelper class.
158
- # If will either return false if no further handling is needed
159
- # on our part, or the name of a runfile to execute.
160
- def handle_no_runfile(argv)
161
- maker = RunfileHelper.new
162
- maker.purge_settings if @ignore_settings
163
- runfile = maker.handle argv
164
-
165
- exit 3 unless runfile
166
-
167
- @superspace = argv[0]
168
- execute argv, runfile
169
- exit
170
- end
171
-
172
- def expand_shortcuts(argv)
173
- possible_candidate = argv[0]
174
- if settings.shortcuts and settings.shortcuts[possible_candidate]
175
- shortcut_value = settings.shortcuts[argv[0]]
176
- expanded = shortcut_value.split ' '
177
- argv.shift
178
- argv = expanded + argv
13
+ def call_docopt(docopt, version: nil, argv: nil)
14
+ Docopt.docopt docopt, argv: argv, version: version
15
+ rescue Docopt::Exit => e
16
+ raise ExitWithUsage, e.message
179
17
  end
180
- argv
181
18
  end
182
19
  end
183
20
  end
@@ -0,0 +1,13 @@
1
+ title 'Greeter'
2
+ summary 'A sample runfile'
3
+
4
+ usage 'hello [NAME --shout]'
5
+ help 'Say hello'
6
+ option '--shout, -s', 'Greet louder'
7
+ action 'hello' do |args|
8
+ name = args['NAME'] || 'You...'
9
+ message = "Hello #{name}"
10
+ message = "#{message.upcase}!" if args['--shout']
11
+
12
+ say "gu`#{message}`"
13
+ end