runfile 0.12.0.pre.rc1 → 1.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -28
  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 +96 -21
  40. data/bin/run +0 -10
  41. data/bin/run! +0 -16
  42. data/lib/runfile/docopt_helper.rb +0 -128
  43. data/lib/runfile/dsl.rb +0 -90
  44. data/lib/runfile/refinements.rb +0 -22
  45. data/lib/runfile/runfile_helper.rb +0 -165
  46. data/lib/runfile/settings.rb +0 -34
  47. data/lib/runfile/setup.rb +0 -19
  48. data/lib/runfile/templates/Runfile +0 -16
  49. 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