rubycom 0.3.2 → 0.4.0

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 (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