runfile 0.12.0 → 1.0.0.rc2

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 +109 -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 +8 -0
  14. data/examples/import/runfile +10 -0
  15. data/examples/import/tasks/server.runfile +14 -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 +116 -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 +103 -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,116 @@
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
+ actions[name || :default] = current_action
14
+ @default_action = current_action unless name
15
+ @current_action = nil
16
+ end
17
+
18
+ def env_var(name, help)
19
+ env_vars[name] = help
20
+ end
21
+
22
+ def example(text)
23
+ examples.push text
24
+ end
25
+
26
+ def help(message)
27
+ current_action.help = message
28
+ end
29
+
30
+ def import(pathspec, context = nil)
31
+ imports[pathspec] = context
32
+ end
33
+
34
+ def import_gem(pathspec, context = nil)
35
+ gem_name, glob = pathspec.split('/', 2)
36
+ glob ||= '*'
37
+ path = GemFinder.find gem_name, glob
38
+ imports[path] = context
39
+ end
40
+
41
+ def option(name, help)
42
+ options[name] = help
43
+ end
44
+
45
+ def param(name, help)
46
+ params[name] = help
47
+ end
48
+
49
+ def shortcut(name)
50
+ current_action.shortcut = name
51
+ end
52
+
53
+ def summary(text = nil)
54
+ return @summary unless text
55
+
56
+ @summary = text
57
+ end
58
+
59
+ def title(text = nil)
60
+ return @title unless text
61
+
62
+ @title = text
63
+ end
64
+
65
+ def usage(message)
66
+ message = "#{action_prefix} #{message}" if action_prefix
67
+ current_action.usages.push message
68
+ end
69
+
70
+ def version(text = nil)
71
+ return @version unless text
72
+
73
+ @version = text
74
+ end
75
+
76
+ def execute(command_line)
77
+ run Shellwords.split(command_line)
78
+ end
79
+
80
+ # Evaluation Artifacts
81
+
82
+ def action_prefix
83
+ nil
84
+ end
85
+
86
+ def actions
87
+ @actions ||= {}
88
+ end
89
+
90
+ def params
91
+ @params ||= {}
92
+ end
93
+
94
+ def env_vars
95
+ @env_vars ||= {}
96
+ end
97
+
98
+ def examples
99
+ @examples ||= []
100
+ end
101
+
102
+ def options
103
+ @options ||= {}
104
+ end
105
+
106
+ def imports
107
+ @imports ||= {}
108
+ end
109
+
110
+ private
111
+
112
+ def current_action
113
+ @current_action ||= Action.new
114
+ end
115
+ end
116
+ 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 globs
47
+ @globs ||= (masterfile ? ['*'] + masterfile.imports.keys : ['*'])
48
+ end
49
+
50
+ def external_files
51
+ @external_files ||= begin
52
+ result = []
53
+ globs.each do |glob|
54
+ Dir["#{glob}.runfile"].sort.each do |file|
55
+ userfile = Userfile.load_file(file)
56
+ userfile.context = masterfile.imports[glob] if masterfile
57
+ result.push userfile
58
+ end
59
+ end
60
+
61
+ result.to_h { |file| [file.name, file] }
62
+ end
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
@@ -0,0 +1,103 @@
1
+ require 'shellwords'
2
+
3
+ module Runfile
4
+ # Represents a single runfile.
5
+ class Userfile
6
+ include Renderable
7
+ include Inspectable
8
+ include DSL
9
+
10
+ attr_reader :code, :name, :path
11
+ attr_writer :context
12
+ alias action_prefix name
13
+
14
+ class << self
15
+ def load_file(path)
16
+ if masterfile? path
17
+ name = nil
18
+ else
19
+ name = File.basename path, '.runfile'
20
+ path = "#{path}.runfile" unless path.end_with? '.runfile'
21
+ end
22
+
23
+ code = File.read path
24
+ new code, name: name, path: path
25
+ end
26
+
27
+ def masterfile?(path)
28
+ Meta::MASTERFILE_NAMES.include? path
29
+ end
30
+ end
31
+
32
+ def initialize(code = nil, name: nil, path: nil)
33
+ @code = code
34
+ @name = name
35
+ @path = path
36
+
37
+ return unless @code
38
+
39
+ if path
40
+ instance_eval @code, @path
41
+ else
42
+ instance_eval @code
43
+ end
44
+ end
45
+
46
+ def inspectable
47
+ { name: name, path: path, context: context }
48
+ end
49
+
50
+ def run(argv = [])
51
+ found_delegate = delegates[argv[0]]
52
+ if found_delegate
53
+ found_delegate.run argv
54
+ else
55
+ run_local argv
56
+ end
57
+ end
58
+
59
+ def context
60
+ @context ||= {}
61
+ end
62
+
63
+ def implicit_title
64
+ title || name
65
+ end
66
+
67
+ # Returns an array of actions that have help defined
68
+ def commands
69
+ actions.values.select(&:help)
70
+ end
71
+
72
+ def delegates
73
+ @delegates ||= (name ? {} : meta.external_files)
74
+ end
75
+
76
+ private
77
+
78
+ def find_action(args)
79
+ acts = actions.values
80
+ acts.find { |a| args[a.name] } ||
81
+ acts.find { |a| args[a.shortcut] } ||
82
+ acts.find { |a| args[a.prefix] } ||
83
+ actions[:default]
84
+ end
85
+
86
+ def run_local(argv)
87
+ Runner.run docopt, argv: argv, version: version do |args|
88
+ action = find_action(args)
89
+ raise ActionNotFound, 'Cannot find action. Is it properly defined?' unless action
90
+
91
+ action.run args
92
+ end
93
+ end
94
+
95
+ def meta
96
+ @meta ||= Meta.new
97
+ end
98
+
99
+ def docopt
100
+ @docopt ||= render 'userfile', context: self
101
+ end
102
+ end
103
+ end