rubycom 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +162 -146
  3. data/Rakefile +12 -12
  4. data/lib/rubycom.rb +156 -226
  5. data/lib/rubycom/arg_parse.rb +252 -0
  6. data/lib/rubycom/command_interface.rb +97 -0
  7. data/lib/rubycom/completions.rb +62 -0
  8. data/lib/rubycom/error_handler.rb +15 -0
  9. data/lib/rubycom/executor.rb +23 -0
  10. data/lib/rubycom/helpers.rb +98 -0
  11. data/lib/rubycom/output_handler.rb +15 -0
  12. data/lib/rubycom/parameter_extract.rb +262 -0
  13. data/lib/rubycom/singleton_commands.rb +78 -0
  14. data/lib/rubycom/sources.rb +99 -0
  15. data/lib/rubycom/version.rb +1 -1
  16. data/lib/rubycom/yard_doc.rb +146 -0
  17. data/rubycom.gemspec +14 -16
  18. data/test/rubycom/arg_parse_test.rb +247 -0
  19. data/test/rubycom/command_interface_test.rb +293 -0
  20. data/test/rubycom/completions_test.rb +94 -0
  21. data/test/rubycom/error_handler_test.rb +72 -0
  22. data/test/rubycom/executor_test.rb +64 -0
  23. data/test/rubycom/helpers_test.rb +467 -0
  24. data/test/rubycom/output_handler_test.rb +76 -0
  25. data/test/rubycom/parameter_extract_test.rb +141 -0
  26. data/test/rubycom/rubycom_test.rb +290 -548
  27. data/test/rubycom/singleton_commands_test.rb +122 -0
  28. data/test/rubycom/sources_test.rb +59 -0
  29. data/test/rubycom/util_test_bin.rb +8 -0
  30. data/test/rubycom/util_test_composite.rb +23 -20
  31. data/test/rubycom/util_test_module.rb +142 -112
  32. data/test/rubycom/util_test_no_singleton.rb +2 -2
  33. data/test/rubycom/util_test_sub_module.rb +13 -0
  34. data/test/rubycom/yard_doc_test.rb +165 -0
  35. metadata +61 -24
  36. data/lib/rubycom/arguments.rb +0 -133
  37. data/lib/rubycom/commands.rb +0 -63
  38. data/lib/rubycom/documentation.rb +0 -212
  39. data/test/rubycom/arguments_test.rb +0 -289
  40. data/test/rubycom/commands_test.rb +0 -51
  41. data/test/rubycom/documentation_test.rb +0 -186
  42. data/test/rubycom/util_test_job.yaml +0 -21
  43. data/test/rubycom/utility_tester.rb +0 -17
checksums.yaml CHANGED
@@ -1,15 +1,15 @@
1
1
  ---
2
2
  !binary "U0hBMQ==":
3
3
  metadata.gz: !binary |-
4
- NzljMjJkOTA5OGI4NTlmNTNjMjQ0NDdkMzQ1NzY5YjYxMGRkMmQxNw==
4
+ ZThmMjdiYjc1YjlkMDUxODYxNGQyMmI2YzYzNGI0YTQwMDQ2MGI3Mg==
5
5
  data.tar.gz: !binary |-
6
- MTY3NjdiMTkyOTBhM2UyNjZiNjc4ODE5YzQ0MDJiNzQzNmNlOGJiOQ==
6
+ OTY2ZWQzMzI1NGNhOTEyYzM5YTVhZGUwOTgyMTcxMjAyM2Y3MzliYw==
7
7
  !binary "U0hBNTEy":
8
8
  metadata.gz: !binary |-
9
- NDRmZWFhNTRhMWFmMTBjZjY1ZjE3N2JhODFjNThiMDc3MDk3YjQzMzc3NWM4
10
- OWQwNWE3NzA3ZjgzMDdlODAzZjQzZmY3NWFjNDdhYjMxNDU5YzE2ZjkyM2Jl
11
- OWU4ODYzMWFlZWY0NzYzOGQ5YzQwYWZlM2VmMDlmMzcxYzdmZTk=
9
+ ZDhlMWRhMDNiODI2Y2RmMjMxNGVhZDU4NDk4YTc3MjljMmE2OWEyNWQ5YTBj
10
+ OWZkM2JiZjE2MThjMTRjMzE0NjM2YzI5YzliMmU3NzZjZmViZWE1ODgyODMw
11
+ MTM5YTYyZjYyYjcxNzkyY2ZjYWUzZDA1NWNjNjlmYjk0MzlhYzk=
12
12
  data.tar.gz: !binary |-
13
- Njg1YWIxMzI5ODA1NGViMTA1NGMyMjYxOGViNDI0NTZjOWQyY2U1OTE3Mzgx
14
- YmQ0NGNhOGVkNTg0OGE2OWNjMTBkNWI2YWE5ZjJkNjZjYWY0YzlmN2Q2ZWVm
15
- ZTRmMTVlMDdhMDUxY2NjYzg4NjA1NjMwZWNkZmQyNzUxNzUxZGE=
13
+ ZWEyZThiOGJiMzU2NDYzYzlhMmFlZTM3Yjc2YjY3YTE3YzI5MTUzMGQyYmJh
14
+ Yjc3ZmE2MWE0MGJiNzY0Njg5Mzc5YzUyYmNlNDZhZDRlN2IxMmM4MTNhZGFm
15
+ ZDI0MWYzNDE1M2IzMjJhNjQzYmI4NjEwNjUzZjYxYWQ5NjhhZDc=
data/README.md CHANGED
@@ -1,146 +1,162 @@
1
- Rubycom
2
- ---------------
3
-
4
- © Danny Purcell 2013 | MIT license
5
-
6
- Makes creating command line tools as easy as writing a function library.
7
-
8
- When a module is run from the terminal and includes Rubycom, Rubycom will parse ARGV for a command name,
9
- match the command name to a public singleton method (self.method_name()) in the including module, and run the method
10
- with the given arguments.
11
-
12
- Features
13
- ---------------
14
-
15
- Allows the user to write a properly documented module/class as a function library and convert it to a command line tool
16
- by simply including Rubycom at the bottom.
17
-
18
- * Provides a Command Line Interface for any function library simply by stating `include Rubycom` at the bottom.
19
- * Public singleton methods are made accessible from the terminal. Usage documentation is pulled from method comments.
20
- * Method parameters become required CLI arguments. Optional (defaulted) parameters become CLI options.
21
- * Command consoles can be built up by including other modules before including Rubycom.
22
- * Included modules become commands, their public singleton methods become sub-commands.
23
- * Built in tab completion support for all commands.
24
- * Users may call `./path/to/my_command.rb register_completions` then `source ~/.bash_profile` to register completions.
25
-
26
- Usage
27
- ---------------
28
-
29
- Write your module of methods, document them as you normally would. `include Rubycom` at the bottom.
30
- Optionally `#!/usr/bin/env ruby` at the top.
31
-
32
- Now any singleton methods `def self.method_name` will be available to call from the terminal.
33
-
34
- Calling `ruby ./path/to/module.rb <command_name>` will automatically discover and run your `<command_name>` singleton method.
35
- If no method is found by the given name, a usage print out will be given including a summary of each command available
36
- and it's description from the corresponding method's comments.
37
-
38
- Calling a valid command with incorrect arguments will produce a usage print out for the matched method.
39
- Rubycom will include as much documentation on the command line as you provide in your method comments. Currently Rubycom
40
- only handles @param and @return annotation style comments for discovering the method comments regarding params and return values.
41
- All other commentary will be included as part of the command description. In the absence of @param or @return comments,
42
- Rubycom will also leave off the corresponding Param: and Return: markers in the usage output.
43
-
44
- #####Special commands
45
-
46
- | Command | Description | Options |
47
- | ------- |:-----------:| -------:|
48
- | `ruby ./path/to/module.rb help [command_name]` | Will print out usage for the module or optionally the specified command.||
49
- | `ruby ./path/to/module.rb job </path/to/job.yaml>` | Runs the specified job file. See: "Jobs" below. | `--test` Prints the steps and context without running the commands. |
50
-
51
-
52
- ###Arguments
53
-
54
- * Arguments are automatically parsed from the command line using Ruby's core Yaml module.
55
- * Arguments will be passed to your method in order of their appearance on the command line.
56
- * If you specify a default value for a parameter in your method, then Rubycom will look for a named argument matching
57
- the parameter's name.
58
- * Users may call out option parameters in any order using `--<param_name>=<value>` or `--<param_name> <value>`
59
- * Currently Rubycom does not yet support sort names for optional parameters so specifying `-<param_name>`
60
- is equivalent to `--<param_name>`
61
- * In the absence of a named option, any optional parameters still unfilled will be filled by unnamed arguments in
62
- order of appearance.
63
- * Optional parameters which do not get overridden either by a named option specification or an available unnamed
64
- argument will be filled by their default as usual.
65
- * If a rest parameter `*param_name` is defined in the method being called, any remaining arguments will be passed to the
66
- rest parameter after the required and optional parameters are filled.
67
-
68
- ###Jobs
69
-
70
- Jobs are a higher order orchestration mechanism for command line utilities. Rubycom provides a simple job runner to every
71
- command line utility. by calling `ruby ./path/to/module.rb job </path/to/job.yaml>` with a valid job yaml. Rubycom will
72
- run your job.
73
-
74
- * A valid job file is a Yaml file which specifies a `steps` node and any number of valid numbered child nodes
75
- * Optionally, an `env` node may specified.
76
- * If specified, `env` should include child nodes which are `key: value` pairs Ex: `working_dir: ./test/rubycom`
77
- * If an `env` is specified, values may be inserted into commands in the `steps` node as such: `env['key']`
78
- * Ex: `ruby env[working_dir]/util_test_composite.rb test_composite_command env[test_msg]`
79
- * A valid `steps` child node is a numbered node `1:` with a `cmd:` child node and optionally several context `desc:`
80
- child nodes.
81
- * A `cmd:` node should specify the command string to run. Ex: `cmd: ls ./test_folder`
82
- * A context node should specify some text to be placed with the node's key in a formatted
83
- logging context Ex: `desc: Run test_composite_command`
84
-
85
- Below is an example job file which demonstrates the format Rubycom supports.
86
-
87
- ---
88
- env:
89
- test_msg: Hello World
90
- test_arg: 123
91
- working_dir: ./test/rubycom
92
- steps:
93
- 1:
94
- desc: Run test_composite_command with environment variable
95
- cmd: ruby env[working_dir]/util_test_composite.rb test_composite_command env[test_msg]
96
- 2:
97
- Var: Run UtilTestModule/test_command_options_arr with environment variable
98
- cmd: ruby env[working_dir]/util_test_composite.rb UtilTestModule test_command_options_arr '["Hello World", world2]'
99
- 3:
100
- Context: Run test_command_with_args with environment variable
101
- cmd: ruby env[working_dir]/util_test_module.rb test_command_with_args env[test_msg] env[test_arg]
102
- 4:
103
- Cmd: Run ls for arbitrary command support
104
- cmd: ls
105
- 5:
106
- Arbitrary_Context: Run ls with environment variable
107
- cmd: ls env[working_dir]
108
-
109
-
110
- Raison d'etre
111
- ---------------
112
-
113
- * From scratch command line scripts often include redundant ARGV parsing code, little to no testing, slim documentation.
114
- * OptionParser and the like help script authors define options for a script.
115
- They provide structure to the redundant code and slightly easier argument parsing.
116
- * Thor and the like provide a framework the script author will extend to create command line tools.
117
- Prescriptive approach creates consistency but requires the script author to learn the framework and conform.
118
-
119
- While these are things are nice, we are still writing redundant code and
120
- tightly coupling the functional code to the interface which presents it.
121
-
122
- At it's core a terminal command is a function. Rather than requiring the authors to make concessions for the presentation and
123
- tightly couple the functional code to the interface, it would be nice if the author could simply write a function library
124
- and attach the interface to it.
125
-
126
- How it works
127
- ---------------
128
- Rubycom attaches the CLI to the functional code. The author is free to write the functional code as any other.
129
- If a set of functions needs to be accessible from the terminal, just `include Rubycom` at the bottom and run the ruby file.
130
-
131
- * Public singleton methods are made accessible from the terminal.
132
- * ARGV is parsed for a method to run and arguments.
133
- * Usage documentation is pulled from method comments.
134
- * Method parameters become required CLI arguments.
135
- * Optional (defaulted) parameters become CLI options.
136
- * Tab completion support if the user has registered it for the file.
137
-
138
- The result is a function library which can be consumed easily from other classes/modules and which is accessible from the command line.
139
-
140
- Coming Soon
141
- ---------------
142
- * Support for piping.
143
- * Build a job yaml by running each command in sequence with a special option --job_add <path_to_yaml>[:step_number]
144
- * Edit job files from the command line using special options.
145
- * --job_update <path_to_yaml>[:step_number]
146
- * --job_remove <path_to_yaml>[:step_number]
1
+ Rubycom
2
+ ---------------
3
+
4
+ &copy; Danny Purcell 2013 | MIT license
5
+
6
+ Makes creating command line tools as easy as including Rubycom.
7
+
8
+ When a Module which has included Rubycom is run from the terminal, Rubycom will parse ARGV for a command name,
9
+ match the command name to a method in the including module, and run the method with the given arguments.
10
+
11
+ Features
12
+ ---------------
13
+
14
+ Allows the user to write a properly documented module/class and convert it to a command line tool
15
+ by simply including Rubycom at the bottom.
16
+
17
+ * Provides a Command Line Interface for any library simply by stating `include Rubycom` at the bottom.
18
+ * Public singleton methods are made accessible from the terminal. Usage documentation is pulled from method comments.
19
+ * Method parameters become required CLI arguments. Optional (defaulted) parameters become CLI options.
20
+ * Command consoles can be built up by including other modules before including Rubycom.
21
+ * Included modules become commands, their public singleton methods become sub-commands.
22
+ * Built in tab completion support for all commands.
23
+ * Users may call `./path/to/my_command.rb register_completions` then `source ~/.bash_profile` to register completions.
24
+ * Customize Rubycom's functionality by calling `Rubycom.run_command` with custom plugin modules.
25
+ * When calling run_command, functionality can be easily modified by providing a custom module for one of the following
26
+ keys `:arguments, :discover, :documentation, :source, :parameters, :executor, :output, :interface, :error` in plugins_options.
27
+
28
+ Installation
29
+ ---------------
30
+
31
+ #### Install with Gem
32
+ * Available on [Rubygems](https://rubygems.org/gems/rubycom)
33
+ * Be sure one of your gem sources is `source 'https://rubygems.org'`
34
+ * Run `gem install rubycom`
35
+
36
+ #### Building locally
37
+ * Fork the repository if you wish
38
+ * Clone repository locally
39
+ * If using the main repo: `git clone https://github.com/dannypurcell/rubycom.git`
40
+ * Run `rake install` if installing for the first time
41
+ * If updating to the latest version run the following commands
42
+ * `git checkout master`
43
+ * `git pull origin master`
44
+ * If that causes any problems `git reset --hard origin/master`
45
+ * `rake upgrade`
46
+
47
+ Usage
48
+ ---------------
49
+
50
+ Write your library, document them as you normally would. `include Rubycom` at the bottom.
51
+ Optionally `#!/usr/bin/env ruby` at the top.
52
+
53
+ Now any singleton methods `def self.method_name` will be available to call from the terminal.
54
+
55
+ Calling `ruby ./path/to/module.rb <command_name>` will automatically discover and run your `<command_name>` singleton method.
56
+ If no method is found by the given name, a usage print out will be given including a summary of each command available
57
+ and it's description from the corresponding method's comments.
58
+
59
+ Calling a valid command with incorrect arguments will produce a usage print out for the matched method.
60
+ Rubycom will include as much documentation on the command line as you provide in your method comments. Currently Rubycom
61
+ only handles YardDoc style comments for discovering the parameter and return documentation. All other commentary will be
62
+ included as part of the command description. In the absence of YardDoc annotations, Rubycom will generate a clean usage
63
+ text which may work for your method doc even though Rubycom is not specifically parsing it.
64
+
65
+
66
+ #####Special commands
67
+
68
+ | Command | Description | Options |
69
+ | ------- |:-----------:| -------:|
70
+ | `ruby ./path/to/module.rb help [command_name]` | Will print out usage for the module or optionally the specified command.||
71
+ | `ruby ./path/to/module.rb register_completions ` | Setup bash tab completion ||
72
+ | `ruby ./path/to/module.rb tab_complete [text]` | Print a list of possible matches for a given word ||
73
+
74
+ ###Arguments
75
+
76
+ When using Rubycom's default modules:
77
+ * Arguments are automatically parsed from the command line using Rubycom's ArgParse module and converted to Ruby types
78
+ by Ruby's core Yaml module.
79
+ * Arguments will be passed to your method in order of their appearance on the command line. With smart parsing for
80
+ option arguments and flags.
81
+ * If you specify a default value for a parameter in your method, then Rubycom will look for a named option argument in
82
+ the command line which matches the parameter's name or the first letter in the parameter name if it is unique among the
83
+ other method parameters.
84
+ * Users may call out option parameters in any order using `--<param_name>=<value>`, `--<param_name> = <value>`, or
85
+ `--<param_name> <value>`
86
+ * Rubycom attempts to handle short names for optional parameters so specifying `-<p> <value>` or `-<param> <value>`
87
+ is equivalent to `--<parameter> <value>` if the characters uniquely match a parameter name in the called method.
88
+ * Any parameter which is not mentioned in the command line will receive one of the remaining, unnamed arguments in order
89
+ of appearance.
90
+ * Optional parameters which do not get overridden either by a named optional argument or an available unnamed command line
91
+ argument will be filled by their default as usual.
92
+ * If a rest parameter `*param_name` is defined in the method being called, any remaining arguments will be passed to the
93
+ rest parameter after the required and optional parameters are filled.
94
+
95
+ Raison d'etre
96
+ ---------------
97
+
98
+ * Command line scripts written from scratch often include redundant ARGV parsing code, little or no testing, and slim documentation.
99
+ Development speed is important and setting up a properly documented and tested terminal interface takes a while.
100
+ * OptionParser and the like help script authors define options for a script.
101
+ They provide structure to the redundant code and slightly easier argument specification.
102
+ * Thor and the like provide a framework the script author will extend to create command line tools.
103
+ The Prescriptive approach creates consistency but requires the script author to learn the framework and conform.
104
+
105
+ While these are things do help, we are still writing redundant code and tightly coupling the functional code to the
106
+ interface which presents it. We also lack a generic command line parser which, if available, could help encourage
107
+ Rubyists to standardize command line inputs.
108
+
109
+ So, what to do?
110
+
111
+ ...Ruby is interpreted...use the source.
112
+
113
+ Rather than making concessions for the presentation and tightly coupling the functional code
114
+ to the interface, it would be nice if a script author could simply write their code and attach the interface to it.
115
+
116
+
117
+ How it works
118
+ ---------------
119
+ Rubycom attaches the CLI to the functional code. The author is free to write the functional code as any other.
120
+ If a library needs to be accessible from the terminal, just `include Rubycom` at the bottom of the main Module and run
121
+ the ruby file.
122
+
123
+ * Methods are made accessible from the terminal.
124
+ * ARGV is parsed for a method to run and arguments.
125
+ * Usage documentation is pulled from method comments.
126
+ * Method parameters become required CLI arguments.
127
+ * Optional (defaulted) parameters become CLI options.
128
+ * Tab completion support if the user has registered it for the file.
129
+
130
+ The result is a library which can be consumed easily from other classes/modules and which is accessible from the command line.
131
+
132
+ Customizing Rubycom
133
+ ---------------
134
+
135
+ Note: The plugin_options hash is currently taking Modules and calling specific methods on them. This will change to a
136
+ Symbol => Proc mapping soon. Please log an issue on [GitHub](https://github.com/dannypurcell/rubycom/issues) if you
137
+ want this right away.
138
+
139
+ Rubycom is designed to fit several different ways of calling command line utilities and to respect many of the
140
+ strong conventions regarding command line semantics. While Rubycom's default functionality should fit many common use
141
+ cases it is also built in a modular fashion such that the core functionality can be easily adapted to fit specific
142
+ requirements or user preferences.
143
+
144
+ * Calling Rubycom via `include Rubycom` will attempt to execute the default functionality.
145
+ * Alternately, calling `Rubycom.run_command(base, args=[], plugins_options={})` directly enables the user to inject
146
+ custom modules for specific portions of the execution via the plugin_options parameter.
147
+
148
+ #####Plugin Module Contracts
149
+
150
+ | Key | Expected Inputs | Expected Outputs |
151
+ | -------------- |:---------------:|:----------------:|
152
+ | :arguments | ARGV | A data structure representing the arguments, options, and flags |
153
+ | :discover | The Module which included Rubycom and a parsed command line | A Method or Module representing the command which should be run |
154
+ | :documentation | The command to run and the :source plugin | The command matched to it's documentation |
155
+ | :source | A Module or Method object | The source code for that reference |
156
+ | :parameters | A command, a parsed command line, and the command documentation | The command parameters matched to their values for this run |
157
+ | :executor | A command to execute and the command parameters matched to their values for this run | The result of a call to the given method with the given parameters |
158
+ | :output | The command result | Some output handling action |
159
+ | :interface | A command and it's documentation | A string representing the usage text to present in a terminal |
160
+ | :error | An Error and a String representing usage text | Some error handling action |
161
+
162
+
data/Rakefile CHANGED
@@ -11,11 +11,11 @@ task :clean do
11
11
  end
12
12
 
13
13
  task :bundle do
14
- system("bundle install")
14
+ system('bundle install')
15
15
  end
16
16
 
17
17
  Rake::TestTask.new do |t|
18
- t.libs << "test"
18
+ t.libs << 'test'
19
19
  t.test_files = FileList['test/*/*_test.rb']
20
20
  t.verbose = true
21
21
  end
@@ -23,10 +23,10 @@ end
23
23
  YARD::Rake::YardocTask.new
24
24
 
25
25
  task :package => [:clean, :bundle, :test, :yard] do
26
- gem_specs = Dir.glob("**/*.gemspec")
26
+ gem_specs = Dir.glob('**/*.gemspec')
27
27
  gem_specs.each { |gem_spec|
28
28
  system("gem build #{gem_spec}")
29
- raise "Error during build phase" if $?.exitstatus != 0
29
+ raise 'Error during build phase' if $?.exitstatus != 0
30
30
  }
31
31
  end
32
32
 
@@ -36,12 +36,12 @@ task :install => :package do
36
36
  end
37
37
 
38
38
  task :upgrade => :package do
39
- system("gem uninstall rubycom -a")
39
+ system('gem uninstall rubycom -a')
40
40
  load "#{File.expand_path(File.dirname(__FILE__))}/lib/rubycom/version.rb"
41
41
  system("gem install #{File.expand_path(File.dirname(__FILE__))}/rubycom-#{Rubycom::VERSION}.gem")
42
42
  end
43
43
 
44
- task :version_set, [:version] do |t, args|
44
+ task :version_set, [:version] do |_, args|
45
45
  raise "Must provide a version.\n If you called 'rake version_set 1.2.3', try 'rake version_set[1.2.3]'" if args[:version].nil? || args[:version].empty?
46
46
 
47
47
  version_file = <<-END.gsub(/^ {4}/, '')
@@ -54,21 +54,21 @@ task :version_set, [:version] do |t, args|
54
54
  file.write(version_file)
55
55
  }
56
56
  file_text = File.read("#{File.expand_path(File.dirname(__FILE__))}/lib/rubycom/version.rb")
57
- raise "Could not update version file" if file_text != version_file
57
+ raise 'Could not update version file' if file_text != version_file
58
58
  end
59
59
 
60
- task :release, [:version] => [:version_set, :package] do |t, args|
61
- system("git clean -f")
62
- system("git add .")
60
+ task :release, [:version] => [:version_set, :package] do |_, args|
61
+ system('git clean -f')
62
+ system('git add .')
63
63
  system("git commit -m\"Version to #{args[:version]}\"")
64
64
  if $?.exitstatus == 0
65
65
  system("git tag -a v#{args[:version]} -m\"Version #{args[:version]} Release\"")
66
66
  if $?.exitstatus == 0
67
- system("git push origin master --tags")
67
+ system('git push origin master --tags')
68
68
  if $?.exitstatus == 0
69
69
  load "#{File.expand_path(File.dirname(__FILE__))}/lib/rubycom/version.rb"
70
70
  system("gem push #{File.expand_path(File.dirname(__FILE__))}/rubycom-#{Rubycom::VERSION}.gem")
71
71
  end
72
72
  end
73
73
  end
74
- end
74
+ end
@@ -1,226 +1,156 @@
1
- require "#{File.dirname(__FILE__)}/rubycom/arguments.rb"
2
- require "#{File.dirname(__FILE__)}/rubycom/commands.rb"
3
- require "#{File.dirname(__FILE__)}/rubycom/documentation.rb"
4
- require "#{File.dirname(__FILE__)}/rubycom/version.rb"
5
-
6
- require 'yaml'
7
-
8
- # Upon inclusion in another Module, Rubycom will attempt to call a method in the including module by parsing
9
- # ARGV for a method name and a list of arguments.
10
- # If found Rubycom will call the method specified in ARGV with the parameters parsed from the remaining arguments
11
- # If a Method match can not be made, Rubycom will print help instead by parsing source comments from the including
12
- # module or it's included modules.
13
- module Rubycom
14
- class CLIError < StandardError;
15
- end
16
-
17
- # Detects that Rubycom was included in another module and calls Rubycom#run
18
- #
19
- # @param [Module] base the module which invoked 'include Rubycom'
20
- def self.included(base)
21
- base_file_path = caller.first.gsub(/:\d+:.+/, '')
22
- if base.class == Module && (base_file_path == $0 || self.is_executed_by_gem?(base_file_path))
23
- base.module_eval {
24
- Rubycom.run(base, ARGV)
25
- }
26
- end
27
- end
28
-
29
- # Determines whether the including module was executed by a gem binary
30
- #
31
- # @param [String] base_file_path the path to the including module's source file
32
- def self.is_executed_by_gem?(base_file_path)
33
- Gem.loaded_specs.map{|k,s|
34
- {k => {name: "#{s.name}-#{s.version}", executables: s.executables}}
35
- }.reduce(&:merge).map{|k,s|
36
- base_file_path.include?(s[:name]) && s[:executables].include?(File.basename(base_file_path))
37
- }.flatten.reduce(&:|)
38
- end
39
-
40
- # Looks up the command specified in the first arg and executes with the rest of the args
41
- #
42
- # @param [Module] base the module which invoked 'include Rubycom'
43
- # @param [Array] args a String Array representing the command to run followed by arguments to be passed
44
- def self.run(base, args=[])
45
- begin
46
- raise CLIError, "Invalid base class invocation: #{base}" if base.nil?
47
- command = args[0] || nil
48
- arguments = args[1..-1] || []
49
-
50
- case command
51
- when 'register_completions'
52
- puts self.register_completions(base)
53
- when 'tab_complete'
54
- puts self.tab_complete(base, args)
55
- when 'help'
56
- help_topic = arguments[0]
57
- if help_topic.nil?
58
- usage = Documentation.get_usage(base)
59
- default_usage = Documentation.get_default_commands_usage
60
- puts usage
61
- puts default_usage
62
- return usage+"\n"+default_usage
63
- elsif help_topic == 'job'
64
- usage = Documentation.get_job_usage(base)
65
- puts usage
66
- return usage
67
- elsif help_topic == 'register_completions'
68
- usage = Documentation.get_register_completions_usage(base)
69
- puts usage
70
- return usage
71
- elsif help_topic == 'tab_complete'
72
- usage = Documentation.get_tab_complete_usage(base)
73
- puts usage
74
- return usage
75
- else
76
- cmd_usage = Documentation.get_command_usage(base, help_topic, arguments[1..-1])
77
- puts cmd_usage
78
- return cmd_usage
79
- end
80
- when 'job'
81
- begin
82
- raise CLIError, 'No job specified' if arguments[0].nil? || arguments[0].empty?
83
- job_hash = YAML.load_file(arguments[0])
84
- job_hash = {} if job_hash.nil?
85
- STDOUT.sync = true
86
- if arguments.delete('-test') || arguments.delete('--test')
87
- puts "[Test Job #{arguments[0]}]"
88
- job_hash['steps'].each { |step, step_hash|
89
- step = "[Step: #{step}/#{job_hash['steps'].length}]"
90
- context = step_hash.select { |key| key!="cmd" }.map { |key, val| "[#{key}: #{val}]" }.join(' ')
91
- env = job_hash['env'] || {}
92
- env.each { |key, val| step_hash['cmd'].gsub!("env[#{key}]", "#{((val.class == String)&&(val.match(/\w+/))) ? "\"#{val}\"" : val}") }
93
- cmd = "[cmd: #{step_hash['cmd']}]"
94
- puts "#{[step, context, cmd].join(' ')}"
95
- }
96
- else
97
- puts "[Job #{arguments[0]}]"
98
- job_hash['steps'].each { |step, step_hash|
99
- step = "[Step: #{step}/#{job_hash['steps'].length}]"
100
- context = step_hash.select { |key| key!="cmd" }.map { |key, val| "[#{key}: #{val}]" }.join(' ')
101
- env = job_hash['env'] || {}
102
- env.each { |key, val| step_hash['cmd'].gsub!("env[#{key}]", "#{((val.class == String)&&(val.match(/\w+/))) ? "\"#{val}\"" : val}") }
103
- cmd = "[cmd: #{step_hash['cmd']}]"
104
- puts "#{[step, context, cmd].join(' ')}"
105
- system(step_hash['cmd'])
106
- }
107
- end
108
- rescue CLIError => e
109
- $stderr.puts e
110
- end
111
- else
112
- output = self.run_command(base, command, arguments)
113
- std_output = nil
114
- std_output = output.to_yaml unless [String, NilClass, TrueClass, FalseClass, Fixnum, Float, Symbol].include?(output.class)
115
- puts std_output || output
116
- return output
117
- end
118
-
119
- rescue CLIError => e
120
- $stderr.puts e
121
- $stderr.puts Documentation.get_summary(base)
122
- end
123
- end
124
-
125
- # Handles the method call according to the given arguments. If the specified command is a Module then a recursive search
126
- # is performed until a Method is found in the specified arguments.
127
- #
128
- # @param [Module] base the module which invoked 'include Rubycom'
129
- # @param [String] command the name of the command to call, may be a Module name or a Method
130
- # @param [Array] arguments a String Array representing the arguments for the given command
131
- def self.run_command(base, command, arguments=[])
132
- arguments = [] if arguments.nil?
133
- raise CLIError, 'No command specified.' if command.nil? || command.length == 0
134
- begin
135
- raise CLIError, "Invalid Command: #{command}" unless Commands.get_top_level_commands(base).include? command.to_sym
136
- if base.included_modules.map { |mod| mod.name.to_sym }.include?(command.to_sym)
137
- self.run_command(eval(command), arguments[0], arguments[1..-1])
138
- else
139
- self.call_method(base, command, arguments)
140
- end
141
- rescue CLIError => e
142
- $stderr.puts e
143
- $stderr.puts Documentation.get_command_usage(base, command, arguments)
144
- end
145
- end
146
-
147
- # Calls the given command on the given Module after parsing the given Array of arguments
148
- #
149
- # @param [Module] base the module wherein the specified command is defined
150
- # @param [String] command the name of the Method to call
151
- # @param [Array] arguments a String Array representing the arguments for the given command
152
- # @return the result of the specified Method call
153
- def self.call_method(base, command, arguments=[])
154
- method = base.public_method(command.to_sym)
155
- raise CLIError, "No public method found for symbol: #{command.to_sym}" if method.nil?
156
- param_defs = Arguments.get_param_definitions(method)
157
- args = Arguments.resolve(param_defs, arguments)
158
- flatten = false
159
- params = method.parameters.map { |arr| flatten = true if arr[0]==:rest; args[arr[1]] }
160
- if flatten
161
- rest_arr = params.delete_at(-1)
162
- if rest_arr.respond_to?(:each)
163
- rest_arr.each { |arg| params << arg }
164
- else
165
- params << rest_arr
166
- end
167
- end
168
- (arguments.nil? || arguments.empty?) ? method.call : method.call(*params)
169
- end
170
-
171
- # Inserts a tab completion into the current user's .bash_profile with a command entry to register the function for
172
- # the current running ruby file
173
- #
174
- # @param [Module] base the module which invoked 'include Rubycom'
175
- # @return [String] a message indicating the result of the command
176
- def self.register_completions(base)
177
- completion_function = <<-END.gsub(/^ {4}/, '')
178
-
179
- _#{base}_complete() {
180
- COMPREPLY=()
181
- local completions="$(ruby #{File.absolute_path($0)} tab_complete ${COMP_WORDS[*]} 2>/dev/null)"
182
- COMPREPLY=( $(compgen -W "$completions") )
183
- }
184
- complete -o bashdefault -o default -o nospace -F _#{base}_complete #{$0.split('/').last}
185
- END
186
-
187
- already_registered = File.readlines("#{Dir.home}/.bash_profile").map { |line| line.include?("_#{base}_complete()") }.reduce(:|) rescue false
188
- if already_registered
189
- "Completion function for #{base} already registered."
190
- else
191
- File.open("#{Dir.home}/.bash_profile", 'a+') { |file|
192
- file.write(completion_function)
193
- }
194
- "Registration complete, run 'source #{Dir.home}/.bash_profile' to enable auto-completion."
195
- end
196
- end
197
-
198
- # Discovers a list of possible matches to the given arguments
199
- # Intended for use with bash tab completion
200
- #
201
- # @param [Module] base the module which invoked 'include Rubycom'
202
- # @param [Array] arguments a String Array representing the arguments to be matched
203
- # @return [Array] a String Array including the possible matches for the given arguments
204
- def self.tab_complete(base, arguments)
205
- arguments = [] if arguments.nil?
206
- args = (arguments.include?("tab_complete")) ? arguments[2..-1] : arguments
207
- matches = ['']
208
- if args.nil? || args.empty?
209
- matches = Rubycom::Commands.get_top_level_commands(base).map { |sym| sym.to_s }
210
- elsif args.length == 1
211
- matches = Rubycom::Commands.get_top_level_commands(base).map { |sym| sym.to_s }.select { |word| !word.match(/^#{args[0]}/).nil? }
212
- if matches.size == 1 && matches[0] == args[0]
213
- matches = self.tab_complete(Kernel.const_get(args[0].to_sym), args[1..-1])
214
- end
215
- elsif args.length > 1
216
- begin
217
- matches = self.tab_complete(Kernel.const_get(args[0].to_sym), args[1..-1])
218
- rescue Exception
219
- matches = ['']
220
- end
221
- end unless base.nil?
222
- matches = [''] if matches.nil? || matches.include?(args[0])
223
- matches
224
- end
225
-
226
- end
1
+ require "#{File.dirname(__FILE__)}/rubycom/completions.rb"
2
+ require "#{File.dirname(__FILE__)}/rubycom/arg_parse.rb"
3
+ require "#{File.dirname(__FILE__)}/rubycom/singleton_commands.rb"
4
+ require "#{File.dirname(__FILE__)}/rubycom/sources.rb"
5
+ require "#{File.dirname(__FILE__)}/rubycom/yard_doc.rb"
6
+ require "#{File.dirname(__FILE__)}/rubycom/parameter_extract.rb"
7
+ require "#{File.dirname(__FILE__)}/rubycom/executor.rb"
8
+ require "#{File.dirname(__FILE__)}/rubycom/output_handler.rb"
9
+ require "#{File.dirname(__FILE__)}/rubycom/command_interface.rb"
10
+ require "#{File.dirname(__FILE__)}/rubycom/error_handler.rb"
11
+
12
+ require 'yaml'
13
+
14
+ # Upon inclusion in another Module, Rubycom will attempt to call a method in the including module by parsing
15
+ # ARGV for a method name and a list of arguments.
16
+ # If found Rubycom will call the method specified in ARGV with the parameters parsed from the remaining arguments
17
+ # If a Method match can not be made, Rubycom will print help instead by parsing code comments from the including
18
+ # module.
19
+ module Rubycom
20
+
21
+ # Base class for all Rubycom errors
22
+ class RubycomError < StandardError
23
+ end
24
+ # To be thrown in case of an error while parsing arguments
25
+ class ArgParseError < RubycomError;
26
+ end
27
+ # To be thrown in case of an error while executing a method
28
+ class ExecutorError < RubycomError;
29
+ end
30
+ # To be thrown in case of an error while extracting parameters
31
+ class ParameterExtractError < RubycomError;
32
+ end
33
+
34
+
35
+ # Determines whether the including module was executed by a gem binary
36
+ #
37
+ # @param [String] base_file_path the path to the including module's source file
38
+ # @return [Boolean] true|false
39
+ def self.is_executed_by_gem?(base_file_path)
40
+ Gem.loaded_specs.map { |k, s|
41
+ {k => {name: "#{s.name}-#{s.version}", executables: s.executables}}
42
+ }.reduce({}, &:merge).map { |_, s|
43
+ base_file_path.include?(s[:name]) && s[:executables].include?(File.basename(base_file_path))
44
+ }.flatten.reduce(&:|)
45
+ end
46
+
47
+ # Detects that Rubycom was included in another module and calls Rubycom#run
48
+ #
49
+ # @param [Module] base the module which invoked 'include Rubycom'
50
+ def self.included(base)
51
+ base_file_path = caller.first.gsub(/:\d+:.+/, '')
52
+ if base.class == Module && (base_file_path == $0 || self.is_executed_by_gem?(base_file_path))
53
+ base.module_eval {
54
+ Rubycom.run(base, ARGV)
55
+ }
56
+ end
57
+ nil
58
+ end
59
+
60
+ # Main entry point for Rubycom. Uses #run_command to discover and run commands
61
+ #
62
+ # @param [Module] base this will be used to determine available commands
63
+ # @param [Array] args a String Array representing the command to run followed by arguments to be passed
64
+ # @return [Object] the result of calling #run_command! or a String representing a default help message
65
+ def self.run(base, args=[])
66
+ begin
67
+ raise RubycomError, "base should should not be nil" if base.nil?
68
+ case args[0]
69
+ when 'register_completions'
70
+ puts Rubycom::Completions.register_completions(base)
71
+ when 'tab_complete'
72
+ puts Rubycom::Completions.tab_complete(base, args, Rubycom::SingletonCommands)
73
+ when 'help'
74
+ help_topic = args[1]
75
+ if help_topic == 'register_completions'
76
+ puts "Usage: #{base} register_completions"
77
+ elsif help_topic == 'tab_complete'
78
+ usage = "Usage: #{base} tab_complete <word>\nParameters:\n [String] word the word or partial word to find matches for"
79
+ puts usage
80
+ return usage
81
+ else
82
+ self.run_command(base, (args[1..-1] << '-h'))
83
+ $stderr.puts <<-END.gsub(/^ {12}/, '')
84
+ Default Commands:
85
+ help - prints this help page
86
+ register_completions - setup bash tab completion
87
+ tab_complete - print a list of possible matches for a given word
88
+ END
89
+ end
90
+ else
91
+ self.run_command(base, args)
92
+ end
93
+ rescue RubycomError => e
94
+ $stderr.puts e
95
+ end
96
+ end
97
+
98
+ # Calls the given process method with the given base, args, and steps.
99
+ #
100
+ # @param [Module] base the Module containing the Method or sub Module to run
101
+ # @param [Array] args a String Array representing the command to run followed by arguments to be passed
102
+ # @param [Hash] steps should have the following keys mapped to Methods or Procs which will be called by the process method
103
+ # :arguments, :discover, :documentation, :source, :parameters, :executor, :output, :interface, :error
104
+ # @param [Method|Proc] process a Method or Proc which calls the step_methods in order to parse args and run a command on base
105
+ # @return [Object] the result of calling the method selected by the :discover method using the args from the :arguments method
106
+ # matched to parameters by the :parameters method
107
+ def self.run_command(base, args=[], steps={}, process=Rubycom.public_method(:process))
108
+ process.call(base, args, steps)
109
+ end
110
+
111
+ # Calls the given steps with the required parameters and ordering to locate and call a method on base or one of it's
112
+ # included modules. This method expresses a procedure and calls the methods in steps to execute each step in the procedure.
113
+ # If not overridden in steps, then method called for each step will be determined by the return from #step_methods.
114
+ #
115
+ # @param [Module] base the Module containing the Method or sub Module to run
116
+ # @param [Array] args a String Array representing the command to run followed by arguments to be passed
117
+ # @param [Hash] steps should have the following keys mapped to Methods or Procs which will be called by the process method
118
+ # :arguments, :discover, :documentation, :source, :parameters, :executor, :output, :interface, :error
119
+ # @return [Object] the result of calling the method selected by the :discover method using the args from the :arguments method
120
+ # matched to parameters by the :parameters method
121
+ def self.process(base, args=[], steps={})
122
+ steps = self.step_methods.merge(steps)
123
+
124
+ parsed_command_line = steps[:arguments].call(args)
125
+ command = steps[:discover].call(base, parsed_command_line)
126
+ begin
127
+ command_doc = steps[:documentation].call(command, steps[:source])
128
+ parameters = steps[:parameters].call(command, parsed_command_line, command_doc)
129
+ command_result = steps[:executor].call(command, parameters)
130
+ steps[:output].call(command_result)
131
+ rescue RubycomError => e
132
+ cli_output = steps[:interface].call(command, command_doc)
133
+ steps[:error].call(e, cli_output)
134
+ end
135
+ command_result
136
+ end
137
+
138
+ # Convenience call for use with #process when the default Rubycom functionality is required.
139
+ #
140
+ # @return [Hash] mapping :arguments, :discover, :documentation, :source, :parameters, :executor, :output, :interface, :error
141
+ # to the default methods which carry out the step referred to by the key.
142
+ def self.step_methods()
143
+ {
144
+ arguments: Rubycom::ArgParse.public_method(:parse_command_line),
145
+ discover: Rubycom::SingletonCommands.public_method(:discover_command),
146
+ documentation: Rubycom::YardDoc.public_method(:document_command),
147
+ source: Rubycom::Sources.public_method(:source_command),
148
+ parameters: Rubycom::ParameterExtract.public_method(:extract_parameters),
149
+ executor: Rubycom::Executor.public_method(:execute_command),
150
+ output: Rubycom::OutputHandler.public_method(:process_output),
151
+ interface: Rubycom::CommandInterface.public_method(:build_interface),
152
+ error: Rubycom::ErrorHandler.public_method(:handle_error)
153
+ }
154
+ end
155
+
156
+ end