├── .gemrc ├── .gemtest ├── .gitignore ├── .rbenv-gemsets ├── .ruby-version ├── Gemfile ├── LICENCE ├── README.md ├── Rakefile ├── _spike └── bug_fix.rb ├── autotest └── discover.rb ├── bin └── commandable ├── commandable.gemspec ├── lib ├── commandable.rb └── commandable │ ├── app_controller.rb │ ├── argument_parser.rb │ ├── coloring.rb │ ├── command_parser.rb │ ├── controller.rb │ ├── exceptions.rb │ ├── execution_controller.rb │ ├── help_text.rb │ └── version.rb └── spec ├── commandable ├── 00_run_first_spec.rb ├── app_controller_spec.rb ├── attr_accessor_spec.rb ├── coloring_spec.rb ├── command_line_execution_spec.rb ├── commandable_spec.rb ├── execution_controller_spec.rb ├── help_generator_spec.rb ├── helpers_spec.rb ├── instance_methods_spec.rb ├── kernel_exit_spec.rb ├── required_default_spec.rb ├── reset_spec.rb └── xor_groups_spec.rb ├── source_code_examples ├── attr_accessor.rb ├── attr_accessor_multi.rb ├── class_command_no_command.rb ├── class_methods.rb ├── class_methods_nested.rb ├── command_no_command.rb ├── deep_class.rb ├── default_method.rb ├── default_method_multiparameters.rb ├── default_method_no_params.rb ├── instance_methods.rb ├── instance_methods2.rb ├── kernel_exit_class.rb ├── multi_line_description.rb ├── multi_line_description_no_params.rb ├── no_description.rb ├── parameter_class.rb ├── parameter_free.rb ├── required_default.rb ├── required_methods.rb ├── run_first.rb ├── run_first_class.rb ├── super_deep_class.rb ├── test_class.rb └── xor_class.rb ├── source_code_for_errors ├── class_bad.rb ├── default_method_bad.rb └── private_methods_bad.rb └── spec_helper.rb /.gemrc: -------------------------------------------------------------------------------- 1 | test_options: 2 | auto_test_on_install: false 3 | test_on_install: true 4 | install_development_dependencies: true 5 | test_development_dependencies: false 6 | upload_results: true 7 | force_install: false 8 | force_uninstall_on_failure: false -------------------------------------------------------------------------------- /.gemtest: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mikbe/commandable/3f471917f7208f6d8ff5853b356aed404c049549/.gemtest -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | Gemfile.lock 4 | pkg/* 5 | .DS_Store 6 | doc/**/* 7 | rdoc/**/* 8 | vendor/**/* 9 | tmp/**/* -------------------------------------------------------------------------------- /.rbenv-gemsets: -------------------------------------------------------------------------------- 1 | commandable 2 | global 3 | -------------------------------------------------------------------------------- /.ruby-version: -------------------------------------------------------------------------------- 1 | 1.9.3-p392 2 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source "http://rubygems.org" 2 | gemspec 3 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Mike Bethany 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Commandable 2 | The easiest way to add command line control to your Ruby app. 3 | 4 | Stop wasting time writing WET (Write Everything Twice) command line interpreters, or repeatedly writing code for existing ones like optparser, then writing help/usage methods that you constantly must update as your code changes. Now you can add a single line above an existing method and that method will be available from the command line. 5 | 6 | Best of all the help/usage instructions are automatically generated using the method itself! When you change your methods the help instructions change automajically! There's no extra effort needed on your part. 7 | 8 | The whole process can take as little as four lines of code: 9 | 10 | * Add a `require 'commandable'` line somewhere (I'd put it in my bin). 11 | * Then an `extend Commandable` inside your class. 12 | * Put a `command "I do something!"` line above your method. 13 | * And finally make a call to `Commandable.execute(ARGV)` in your bin file. 14 | 15 | Now any method you want to make accessible from the command line requires just a single line of code and it's right where your method is so it's easy to find when you do want to change some functionality. 16 | 17 | Don't think of **Commandable** as a way to add command line switches, think of it as a way to allow your app to be driven directly from the command line in a natural language kind of way. 18 | 19 | You can now "use your words" to let people interact with your apps in a natural way. No more confusing switches that mean one thing in one program and something completely different in another. Can you believe some apps actually use `-v` for something other than "version" and `-h` for something other than "help?" Madness I say! Madness! 20 | 21 | 22 | ## Latest version 23 | 24 | 2013-03-04 - Version: 0.3.2 25 | 26 | Verified works with Ruby 2.0 27 | 28 | ## Principle of Least Surprise 29 | 30 | I've tried to follow the principle of least surprise so Commandable should just work like you would expect it to. As long as you expect it to work the same way as I do. 31 | 32 | ## Requirements ## 33 | 34 | * Ruby 1.9.3 or greater (Ruby 2.0 supported) 35 | * OS X or any Posix OS (It works, mostly, on Windows but it won't pass the tests because of some Unix commands I use in the tests but no in the code base) 36 | 37 | ## Installation 38 | From the command line: 39 | 40 | $ [sudo] gem install commandable 41 | 42 | 43 | ## Usage Instructions 44 | 45 | After installing the **Commandable** gem require it somewhere that gets loaded before your class does: 46 | 47 | require 'commandable' 48 | 49 | Extend your class with the **Commandable** module: 50 | 51 | require 'commandable' 52 | 53 | class Foo 54 | extend Commandable 55 | 56 | end 57 | 58 | Then put `command` and a description above the method you want to make accessible. The description is optional but can be helpful 59 | since it's used when automatically building your help/usage instructions. 60 | 61 | require 'commandable' 62 | 63 | class Foo 64 | extend Commandable 65 | 66 | command "Explain what the command does" 67 | def bar 68 | puts "Foo::bar" 69 | end 70 | end 71 | 72 | ### The "`command`" command and its options 73 | 74 | command ["description"], [:required], [:default], [:priority=>(0...n)], [:xor[=>:group_name]] 75 | 76 | _**command**_ _(required)_ 77 | This is the only thing that's required. It tells **Commandable** to add the method that follows to the list of methods available from the command line. 78 | 79 | _**description**_ [optional] 80 | As you would imagine this is a short description of what the method does. You can have multiple lines by using a new line, `\n`, in the description and your description will be lined up properly. This prints in the help/usage instructions when a user calls your programing using the command "help" or if they try to issue a command that doesn't exist. Help instructions will also print if they try to use your app without any parameters (if there isn't a default method that doesn't require parameters.). 81 | 82 | _**:required**_ [optional] 83 | You can mark a method as required and the user must specify this command and any required parameters every time they run your app. You can also have a method marked as both :default and :required which allows you to run a method with a required parameter but you don't have to type the method name. 84 | 85 | _**:default**_ [optional] 86 | You can have one and only one default method. This method will be called if your app is called with just parameters or if the first command line parameter isn't a command. The user can still give more commands after the parameters for the default command too. 87 | 88 | For instance say your default method is :foo that takes one parameter and you have another method called :bar that also takes one parameter. A user could do this: 89 | 90 | yourapp "Some Parameter" bar "A parameter for bar" 91 | 92 | Just be aware that if they give an option that has the same name as a function the app will think it's a command. 93 | 94 | _**priority=>n**_ [optional] 95 | This optional setting allows you to assign priorities to your methods so if you need them to be executed in a specific order, regardless of how the user specifies them on the command line, you can use this. Then when you execute the command line or ask for a queue of commands they will be sorted for you by priority. 96 | 97 | The higher the priority the sooner the method will be executed. If you do not specify a priority a method will have a priority of 0, the lowest priority. 98 | 99 | Note that you can have a default method with a lower priority than a non-default method. 100 | 101 | _**:xor[=>:whatever]**_ [optional] 102 | The :xor parameter allows you to configure a group of methods as mutually exclusive, i.e. if method1 and method2 are in the same :xor group the user of your application can only call one of them at a time. 103 | 104 | You can use just the :xor symbol and the method will be put into the default XOR group, called :xor so :xor=>:xor, but if you need multiple XOR groups you can specify a group name by using a hash instead of just the :xor symbol. 105 | 106 | The XOR group name will be printed in the front to the description text so it's probably a good idea to use :xor as the prefix. 107 | 108 | 109 | ### Parameter lists 110 | When building the help/usage instructions a list of parameters for each command is automatically created using the names you give the parameters in your method; make sure you use descriptive names. 111 | 112 | Also keep in mind that all command line parameters are strings so you need to deal with that inside your methods if what you really want is a number. 113 | 114 | If none of your methods have parameters then there won't be any reference to parameters in the help/usage instructions. The description text will be moved over to be closer to your commands. 115 | 116 | ### A complete class 117 | 118 | A complete class might look like this: 119 | 120 | require 'commandable' 121 | 122 | class Widget 123 | extend Commandable 124 | 125 | command "create a new widget", :default, :priority=>10 126 | def new(name) 127 | "You made a widget named: #{name}" 128 | end 129 | 130 | command "destroy an existing widget", :xor 131 | def delete(name) 132 | "No disassemble #{name}! #{name} is alive!" 133 | end 134 | 135 | command "spend lots of money to update a widget", :xor 136 | def upgrade(name) 137 | "You just gave #{name} a nice new coat of paint!" 138 | end 139 | 140 | command "your security key must be entered for every command", :required 141 | def key(security) 142 | # blah, blah, blah 143 | end 144 | 145 | end 146 | 147 | #### Class methods 148 | You can also use it on class methods: 149 | 150 | require "commandable" 151 | 152 | class ClassMethods 153 | extend Commandable 154 | 155 | command 'does some stuff' 156 | def self.class_method(string_arg1) 157 | ... 158 | end 159 | 160 | # The first class method will get the command 161 | command "another one" 162 | class << self 163 | def class_method2(integer_arg1) 164 | ... 165 | end 166 | end 167 | end 168 | 169 | If you want to do a block of class commands using `class << self` you need to put `extend Commandable` inside the block: 170 | 171 | require "commandable" 172 | 173 | class ClassMethodsNested 174 | class << self 175 | extend Commandable 176 | 177 | command "hello world" 178 | def class_foo(int_arg1, number_arg2) 179 | [int_arg1, number_arg2] 180 | end 181 | 182 | command "look a function" 183 | def class_bar(int_arg1, string_arg2="Number 42") 184 | [int_arg1, string_arg2] 185 | end 186 | end 187 | end 188 | 189 | 190 | Note: Class methods are called directly on the class while instance methods have an instance created for all calls to that class. This means if you set an instance variable in one method it will be available to any other method calls to that same class; just as if you had created an instance of your class and called methods on it. 191 | 192 | ### Automatic usage/help generation ### 193 | 194 | One of the great features of **Commandable** is that it automatically creates usage instructions based on your methods and the descriptions you provide for them. It then adds a `help` command for you that will print out these usage instructions when called. 195 | 196 | If your app has no `:default` method or it has a default command that requires parameters the help/usage instructions will be printed if a user just runs your app without any input. 197 | 198 | A typical help output looks something like this: 199 | 200 | Commandable - The easiest way to add command line control to your app. 201 | Copyrighted free software - Copyright (c) 2011 Mike Bethany. 202 | Version: 0.2.0 203 | 204 | Usage: commandable [parameters] [ [parameters]...] 205 | 206 | Command Parameters Description 207 | error : Will raise a programmer error, not a user error 208 | so you see what happens when you have bad code 209 | examples [path] : Copies the test classes to a folder so 210 | you can see a bunch of small examples 211 | readme : displays the readme file (default) 212 | v : Application Version 213 | version : Application Version 214 | help : you're looking at it now 215 | 216 | 217 | ### Complete demonstration app and some example classes ### 218 | **Commandable** uses **Commandable** for its own command line options so you can look at the `lib/commandable/app_controller.rb` class for an example. 219 | 220 | If you would like to see a bunch of simple classes that demonstrate how to use **Commandable** run: 221 | 222 | $ commandable examples [path] 223 | 224 | ### Commandable Options 225 | 226 | These are the basic options you'll want to be aware of. Specifically you really want to set `Commandable#app_exe` and `Commandable#app_info` so that the help/usage instructions are fully fleshed out. 227 | 228 | **Commandable.app\_exe** 229 | _default = ""_ 230 | This is what a user would type to run your app; don't set it to "My App" set it to "myapp". When this is configured the help instructions will include a usage line with your executable's name. 231 | 232 | **Commandable.app\_info** 233 | _default = ""_ 234 | This is informational text that will print above the help/usage instructions. 235 | 236 | **Commandable.verbose\_parameters** 237 | _default = false_ 238 | If set to true help instructions will include the default values in the parameter list. 239 | **Important!** This should only be set once, after you `require 'commandable'` but before any of your code files load. Changing the value after files are loaded will make no difference since parameters are only parsed when the source file loads. 240 | 241 | Commandable.verbose_parameters = true 242 | # Will print: 243 | command arg1 [arg2="default value"] 244 | 245 | Commandable.verbose_parameters = false 246 | # Will print: 247 | command arg1 [arg2] 248 | 249 | ###Screen Clearing Options 250 | 251 | **Commandable.clear\_screen** 252 | _default = false_ 253 | Set to true to enable clearing the screen when printing help/usage instructions. 254 | 255 | **Commandable.color\clear\_screen\_code** 256 | _default = "\e[H\e[2J" 257 | The escape codes used to clear the screen. 258 | 259 | ### Colorized Output Options 260 | 261 | The help information can be colored using the standard ANSI escape commands found in the `term-ansicolor` gem. The `term-ansicolor` gem is installed as a dependency but just in case you have problems you can install it yourself by running: 262 | 263 | $ [sudo] gem install term-ansicolor 264 | 265 | Then you can do something like this: 266 | 267 | require 'term/ansicolor' 268 | 269 | c = Term::ANSIColor 270 | 271 | Commandable.color_app_info = c.intense_white + c.bold 272 | Commandable.color_app_exe = c.intense_green + c.bold 273 | Commandable.color_command = c.intense_yellow 274 | Commandable.color_description = c.intense_white 275 | Commandable.color_parameter = c.intense_cyan 276 | Commandable.color_usage = c.intense_black + c.bold 277 | 278 | Commandable.color_error_word = c.intense_black + c.bold 279 | Commandable.color_error_name = c.intense_red + c.bold 280 | Commandable.color_error_description = c.intense_white + c.bold 281 | 282 | ###Color options 283 | 284 | **Commandable.color\_output** 285 | _default = false_ 286 | Set to true to enable colorized help/usage instructions. 287 | 288 | **Commandable.color\_app\_info** 289 | _default = intense\_white_ + bold 290 | The color the app_info text will be in the help message 291 | 292 | **Commandable.color\_app\_name** 293 | _default = intense\_green_ + bold 294 | The color the app_exe will be in the usage line in the help message 295 | 296 | **Commandable.color\_command** 297 | _default = intense\_yellow_ 298 | The color the word "command" and the commands themselves will be in the help message 299 | 300 | **Commandable.color\_description** 301 | _default = intense\_white_ 302 | The color the word "command" and the commands themselves will be in the help message 303 | 304 | **Commandable.color\_parameter** 305 | _default = intense\_cyan_ 306 | The color the word "parameter" and the parameters themselves will be in the help message 307 | 308 | **Commandable.color\_usage** 309 | _default = intense\_black_ + bold 310 | The color the word "Usage:" will be in the help message. 311 | 312 | **Commandable.color\_error\_word** 313 | _default = intense\_white_ 314 | The color the word "Error:" text will be in error messages 315 | 316 | **Commandable.color\_error\_name** 317 | _default = intense\_cyan_ 318 | The color the friendly name of the error will be in error messages 319 | 320 | **Commandable.color\_error\_description** 321 | \_default = intense\_black_ + bold 322 | The color the error description will be in error messages 323 | 324 | The best way to see what all this means it just type `commandable help` and you'll see the help instructions in color. 325 | 326 | ### Executing the Command Line 327 | 328 | There are two ways of using **Commandable** to run your methods. You can use its built in execute method to automatically run whatever is entered on the command line or you can have **Commandable** build an array of procs that you can execute yourself. This allows you to have finer grain control over the execution of the commands as you can deal with the return values as you run each command. 329 | 330 | ### The Easy way 331 | 332 | **Commandable#execution_queue(ARGV)** 333 | 334 | After you've added a command method and a description above a method you can then get an array of command you should execute by sending the command line arguments (ARGV) to `Commandable#execution_queue`. That method will generate an array of procs to run based on the user's input. They're sorted by the order of priority you specified when creating the commands or if they all have the same priority they sort by how they were entered. 335 | 336 | # execution_queue returns an array of hashes which 337 | # in turn contains the method name keyed to :method 338 | # and a proc keyed to, you guessed it, :proc 339 | # It looks like this: 340 | # [{:method => :method_name, :xor=>(:xor group or nil), :parameters=>[...], :priority=>0, :proc => #}, ...] 341 | # 342 | # The array is automatically sorted by priority (higher numbers first, 10 > 0) 343 | 344 | # First get the array of commands 345 | command_queue = Commandable.execution_queue(ARGV) # #ARGV will be cloned so it won't change it 346 | 347 | # Loop through the array calling the commands and dealing with the results 348 | command_queue.each do |cmd| 349 | 350 | # If you need more data about the method you can 351 | # get the method properties from Commandable[] 352 | method_name = cmd[:method] 353 | description = Commandable[method_name][:description] 354 | puts description 355 | 356 | return_values = cmd[:proc].call 357 | 358 | case method_name 359 | when :some_method 360 | # do something with the return values 361 | # based on it being some_method 362 | when :some_other_method 363 | # do something with the return values 364 | # based on it being some_other_method 365 | else 366 | # do something by default 367 | end 368 | 369 | end 370 | 371 | ### The even easier way 372 | 373 | **Commandable.execute(ARGV)** 374 | 375 | The easiest way to use **Commandable** is to just let it do all the work. This works great if all you need to do is make your methods available from the command line. You can also design a controller class with **Commandable** in mind and run all you commands from there. 376 | 377 | When you call the `Commandable#execute` method it will print out the return values for each method called automatically. This method is really meant to be the super easy way to do things. No muss, no fuss. Fire and forget. Shooting fish in a barrel... etc. 378 | 379 | You just need to configure your bin file with the app settings and then run `Commandable#execute`: 380 | 381 | **[your Ruby app directory]/bin/your\_app\_name** 382 | 383 | #!/usr/bin/env ruby 384 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 385 | require 'commandable' 386 | Commandable.verbose_parameters = false 387 | Commandable.color_output = true 388 | Commandable.app_exe = "myapp" 389 | Commandable.app_info = 390 | """ 391 | My App - It does stuff and things! 392 | Copyright (c) 2011 Acme Inc. 393 | """ 394 | 395 | # Make sure you require your app after Commandable, or use 396 | # a configuration file to load the settings then your app 397 | require 'yourappname' 398 | Commandable.execute(ARGV) 399 | 400 | # If you don't want the output from your methods to be printed automatically 401 | # for instance if your method already outputs some text, you can add :silent 402 | # to the execute command like this: 403 | # Commandable.execute(ARGV, :silent) 404 | 405 | 406 | I actually prefer to create a separate file for my **Commandable** configuration and load it in my main app file in the `lib` directory. 407 | 408 | ## In closing... ## 409 | 410 | One really cool thing about this design is you can dynamically extend another app and add your own command line controls without having to crack open their code. The other app doesn't even have to use **Commandable**. You can just write your own methods that call the methods of the original program. 411 | 412 | I should also say the code is really, really ugly right now. Thats the very next thing I will be working on for this project. This is the "rough draft" version that works perfectly well but is very ugly code-wise. I needed to use it right now so am putting it out as is. 413 | 414 | If you have any questions about how the code works I've tried to give as much info in these docs as possible but I am also an OCD level commenter so you should be able to find fairly good explanations of what I'm doing in the code. 415 | 416 | Most of all it should be simple to use so if you have any problems please drop me a line. Also if you make any changes please send me a pull request. I hate when people don't respond to them, even to deny them, so I'm pretty good about that sort of thing. 417 | 418 | 419 | ## Version History ## 420 | 421 | 2013-04-04 - Version: 0.3.2 422 | 423 | * Works with Ruby 2.0 424 | 425 | 2012-02-07 - Version: 0.3.1 426 | 427 | * Fixed bug where an error was raised if a non-commandable method was before a commandable method (uninitialized class variable @@command_options) 428 | 429 | 2012-01-20 - Version: 0.3.0 430 | 431 | * All features are off by default to make Commandable more friendly across platforms. 432 | * Added ability to disable screen clearing and to set your own screen clear escape code. (Based on [John Sumsion](https://github.com/jdsumsion)'s idea) 433 | * Decoupled screen clearing from coloring; you can have neither, either, or both. 434 | * Fixed bug that disabled help output in silent mode. (fixed by [John Sumsion](https://github.com/jdsumsion)) 435 | * Fixed bug that didn't label the default method if screen colors were off. 436 | * Removed monkey patch from FileUtils. It was more philosophical than necessary. 437 | * Changed terminal coloring gem back to official the term-ansicolor from my own fork now that my changes have been pulled into it. 438 | * Code clean up. While mostly not a real refactor I've separated out the code into functional groups (aka files). 439 | * Removed Widget gem example - it was making the examples too tightly coupled to HashModel. 440 | * Removed Cucumber features. 441 | 442 | 443 | 2011-09-27 - Version: 0.2.3 444 | 445 | * Removed annoying error message print out. 446 | 447 | 2011-08-31 - Version: 0.2.2 448 | 449 | * Added ability to do use Kernel.exit without Commandable trapping the error. (Andrew Vos) 450 | * Kernel.exit properly forwards exit status if given one. (Andrew Vos) 451 | 452 | 2011-04-04 - Version: 0.2.1 453 | 454 | * Added ability to use attr\_accessor and att\_writer as commands. You can only write to them of course but it's an easy way to set values. 455 | * Instance methods now retain state between different calls. In other words if you set an instance variable in one method it will be available to any other instance method calls for that class. It's as if you created an instance of your class and called the methods yourself. You can access the class instances using the hash Commandable.class\_cache. It uses the class name, a string, as the key and the instance of the class as the value. {"ClassName"=>#} 456 | * You can now have the execute command work without outputting anything. Just add :silent (or anything other than nil or false) to the execution method. For instance `Commandable.execute(ARGV,:silent)`. 457 | * Clarified error message for default methods that are also required. If you don't give anything it tells you you need to give a parameter but you don't specifically have to give the switch. 458 | 459 | 2011-03-23 - Version: 0.2.0 460 | 461 | * First public release. It's 0.2.0 because 0.1.0, going by the name of Cloptions, wasn't released. 462 | 463 | ### Possible future upgrades/improvements 464 | 465 | * See if there's a elegant way to add nested argument trees without case blocks. e.g. a command that takes set of commands like `foo remote set "ftp_svr" 10.250.1.100` and `foo remote delete "ftp_svr"`. 466 | * Add a way to use or discover version numbers. Might have to force standardization and not allow configuration since it should be DRY. 467 | * Add a generator to automatically add Commandable support to your app. 468 | * Try to figure out how to trap `alias` so you don't have to use an additional `command`. 469 | * Use comments below `command` as the description text so you don't have to repeat yourself when documenting your code. 470 | * Clean up RSpecs. I'm doing too many ugly tests instead of specifying behavior. 471 | * Allow sorting of commands alphabetically or by priority in the help output 472 | * Make the help/usage directions format available to programmers without having to hack the code. 473 | * More edge case testing. 474 | * Allow optional parameters values to be reloaded so changing things like verbose_parameters makes the command list change. (**very** low priority) 475 | * Accepting your suggestions... 476 | 477 | 478 | . -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler' 2 | Bundler::GemHelper.install_tasks 3 | 4 | require 'rake' 5 | require 'rdoc/task' 6 | require 'rspec/core/rake_task' 7 | 8 | desc "Run all examples" 9 | RSpec::Core::RakeTask.new(:spec) do |t| 10 | t.rspec_opts = %w[--color] 11 | t.verbose = true 12 | end 13 | 14 | task :default => [:test, :build] 15 | task :test =>[:spec] 16 | 17 | task :clobber do 18 | rm_rf 'pkg' 19 | rm_rf 'tmp' 20 | rm_rf 'coverage' 21 | rm_rf 'rdoc' 22 | end 23 | 24 | Rake::RDocTask.new do |rdoc| 25 | rdoc.rdoc_dir = 'rdoc' 26 | rdoc.title = "Commandable #{Commandable::VERSION}" 27 | rdoc.rdoc_files.exclude('autotest/*') 28 | rdoc.rdoc_files.exclude('features/*') 29 | rdoc.rdoc_files.exclude('pkg/*') 30 | rdoc.rdoc_files.exclude('spec/**/*') 31 | rdoc.rdoc_files.exclude('vendor/*') 32 | rdoc.rdoc_files.include('README*') 33 | rdoc.rdoc_files.include('lib/**/*.rb') 34 | end -------------------------------------------------------------------------------- /_spike/bug_fix.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path((File.dirname(__FILE__) + '/../lib')) 2 | require 'commandable' 3 | 4 | Commandable.color_output = true 5 | Commandable.app_exe = "bug_fix" 6 | Commandable.app_info = 7 | """ 8 | Testing - Testing Commandable 9 | Copyleft (c) 2012 Mike Bethany 10 | http://mikeb.tk 11 | """ 12 | 13 | class TestIt 14 | extend Commandable 15 | 16 | def non_command_method 17 | puts "test" 18 | end 19 | 20 | command "A directory or file to convert", :default 21 | def command_method(value="blah") 22 | puts "value: #{value}" 23 | end 24 | 25 | end 26 | 27 | Commandable.execute -------------------------------------------------------------------------------- /autotest/discover.rb: -------------------------------------------------------------------------------- 1 | #:nodoc: 2 | Autotest.add_discovery {"rspec2"} -------------------------------------------------------------------------------- /bin/commandable: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | $LOAD_PATH.unshift File.expand_path(File.dirname(__FILE__) + "/../lib") 3 | require 'commandable' 4 | Commandable.color_output = true 5 | Commandable.clear_screen = true 6 | Commandable.verbose_parameters = false 7 | Commandable.app_exe = "commandable" 8 | Commandable.app_info = 9 | """ 10 | \e[92mCommandable\e[0m - The easiest way to add command line control to your app. 11 | Copyrighted free software - Copyright (c) 2011 Mike Bethany. 12 | Version: #{Commandable::VERSION::STRING} 13 | """ 14 | 15 | # App controller has to be loaded after commandable settings 16 | # or it won't be able to use the settings 17 | require 'commandable/app_controller' 18 | 19 | Commandable.execute(ARGV) 20 | -------------------------------------------------------------------------------- /commandable.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | $:.push File.expand_path("../lib", __FILE__) 3 | require "commandable/version" 4 | 5 | Gem::Specification.new do |s| 6 | s.name = "commandable" 7 | s.required_ruby_version = "~>1.9.2" 8 | s.version = Commandable::VERSION::STRING 9 | s.platform = Gem::Platform::RUBY 10 | s.required_ruby_version = '>= 1.9.2' 11 | s.authors = ["Mike Bethany"] 12 | s.email = ["mikbe.tk@gmail.com"] 13 | s.homepage = "http://mikbe.tk" 14 | s.summary = %q{The easiest way to add command line control to your Ruby apps.} 15 | s.description = <1.1") 23 | 24 | s.add_development_dependency("rspec", "~>2.13") 25 | 26 | s.files = `git ls-files`.split("\n") 27 | s.test_files = `git ls-files -- {spec,autotest}/*`.split("\n") 28 | s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } 29 | s.require_paths = ["lib"] 30 | end 31 | -------------------------------------------------------------------------------- /lib/commandable.rb: -------------------------------------------------------------------------------- 1 | # This library allows you to incredibly easily make 2 | # your methods directly available from the command line. 3 | # 4 | # Author:: Mike Bethany (mailto:mikbe.tk@gmail.com) 5 | # Copyright:: Copyright (c) 2012 Mike Bethany 6 | # License:: Distributed under the MIT licence (See LICENCE file) 7 | # website:: http://mikbe.kt 8 | 9 | require 'commandable/version' 10 | require 'commandable/exceptions' 11 | 12 | require 'commandable/argument_parser' 13 | require 'commandable/coloring' 14 | require 'commandable/help_text' 15 | require 'commandable/command_parser' 16 | require 'commandable/execution_controller' 17 | require 'commandable/controller' 18 | -------------------------------------------------------------------------------- /lib/commandable/app_controller.rb: -------------------------------------------------------------------------------- 1 | # Extending your class with this module allows you to 2 | # use the #command method above your method. 3 | # This makes them executable from the command line. 4 | module Commandable 5 | 6 | # A helper to display the read me file and generate an example app 7 | class AppController 8 | 9 | class << self 10 | extend Commandable 11 | 12 | # Displays the readme file 13 | command "displays the readme file", :default 14 | def readme 15 | `open #{File.expand_path((File.dirname(__FILE__) + '/../../readme.md'))}` 16 | end 17 | 18 | command "Copies the test classes to a folder so\nyou can see a bunch of small examples" 19 | # Copies the test classes to a folder so you can see a bunch of small examples 20 | def examples(path="./examples") 21 | copy_dir(File.expand_path(File.dirname(__FILE__) + '/../../spec/source_code_examples'),path) 22 | end 23 | 24 | command "Will raise a programmer error, not a user error\nso you can see what happens when you have bad code" 25 | # Causes an error so you can see what it will look like if you have an error in your code. 26 | def error 27 | raise Exception, "An example of what an error looks like if you make a mistake using Commandable." 28 | end 29 | 30 | command "Application Version", :xor 31 | # Version 32 | def v 33 | puts "Commandable: #{Commandable::VERSION}" 34 | end 35 | command "Application Version", :xor 36 | alias :version :v 37 | 38 | private 39 | 40 | # Copy a directy and its contents 41 | def copy_dir(source, dest) 42 | files = Dir.glob("#{source}/**") 43 | mkdir_p dest 44 | cp_r files, dest 45 | end 46 | 47 | end 48 | 49 | end 50 | 51 | end -------------------------------------------------------------------------------- /lib/commandable/argument_parser.rb: -------------------------------------------------------------------------------- 1 | module Commandable 2 | 3 | # Parse a method's parameters building the argument list for printing help/usage 4 | def parse_arguments(parameters) 5 | parameter_string = "" 6 | method_definition = nil 7 | parameters.each do |parameter| 8 | arg_type = parameter[0] 9 | arg = parameter[1] 10 | case arg_type 11 | when :req 12 | parameter_string += " #{arg}" 13 | when :opt 14 | if Commandable.verbose_parameters 15 | # figure out what the default value is 16 | method_definition ||= readline(@@method_file, @@method_line) 17 | default = parse_optional(method_definition, arg) 18 | parameter_string += " [#{arg}=#{default}]" 19 | else 20 | parameter_string += " [#{arg}]" 21 | end 22 | when :rest 23 | parameter_string += " *#{arg}" 24 | when :block 25 | parameter_string += " &#{arg}" 26 | end 27 | end 28 | parameter_string.strip 29 | end 30 | 31 | # Reads a line from a source code file. 32 | def readline(file, line_number) 33 | current_line = 0 34 | File.open(file).each do |line_text| 35 | current_line += 1 36 | return line_text.strip if current_line == line_number 37 | end 38 | end 39 | 40 | # Parses a method defition for the optional values of given argument. 41 | def parse_optional(method_def, argument) 42 | method_def.scan(/#{argument}\s*=\s*("[^"\r\n]*"|'[^'\r\n]*'|[0-9]*)/)[0][0] 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /lib/commandable/coloring.rb: -------------------------------------------------------------------------------- 1 | require 'term/ansicolor' 2 | 3 | module Commandable 4 | 5 | class << self 6 | 7 | # If the output will be colorized or not 8 | attr_accessor :color_output 9 | 10 | # What color the app_info text will be in the help message 11 | attr_accessor :color_app_info 12 | # What color the app_exe will be in the usage line in the help message 13 | attr_accessor :color_app_exe 14 | # What color the word "command" and the commands themselves will be in the help message 15 | attr_accessor :color_command 16 | # What color the description column header and text will be in the help message 17 | attr_accessor :color_description 18 | # What color the word "parameter" and the parameters themselves will be in the help message 19 | attr_accessor :color_parameter 20 | # What color the word "Usage:" will be in the help message 21 | attr_accessor :color_usage 22 | 23 | # What color the word "Error:" text will be in error messages 24 | attr_accessor :color_error_word 25 | # What color the friendly name of the error will be in error messages 26 | attr_accessor :color_error_name 27 | # What color the error description will be in error messages 28 | attr_accessor :color_error_description 29 | 30 | # If the screen should be cleared before printing help 31 | attr_accessor :clear_screen 32 | 33 | # What escape code will be used to clear the screen 34 | attr_accessor :clear_screen_code 35 | 36 | # Resets colors to their default values and disables color output 37 | def reset_colors 38 | @color_output = false 39 | 40 | #Term::ANSIColor.coloring = true 41 | c = Term::ANSIColor 42 | @color_app_info = c.intense_white + c.bold 43 | @color_app_exe = c.intense_green + c.bold 44 | @color_command = c.intense_yellow 45 | @color_description = c.intense_white 46 | @color_parameter = c.intense_cyan 47 | @color_usage = c.intense_black + c.bold 48 | 49 | @color_error_word = c.intense_black + c.bold 50 | @color_error_name = c.intense_red + c.bold 51 | @color_error_description = c.intense_white + c.bold 52 | 53 | @color_bold = c.bold 54 | @color_reset = c.reset 55 | end 56 | 57 | # Resets the escape code used for screen clearing and disables screen clearing. 58 | def reset_screen_clearing 59 | @clear_screen = false 60 | @clear_screen_code = "\e[H\e[2J" 61 | end 62 | 63 | private 64 | 65 | # Changes the colors used when printing the help/usage instructions to those set by the user. 66 | def set_colors 67 | if @color_output 68 | @c_app_info = @color_app_info 69 | @c_app_exe = @color_app_exe 70 | @c_command = @color_command 71 | @c_description = @color_description 72 | @c_parameter = @color_parameter 73 | @c_usage = @color_usage 74 | 75 | @c_error_word = @color_error_word 76 | @c_error_name = @color_error_name 77 | @c_error_description = @color_error_description 78 | 79 | @c_bold = @color_bold 80 | @c_reset = @color_reset 81 | else 82 | @c_app_info, @c_app_exe, @c_command, 83 | @c_description, @c_parameter, @c_usage, 84 | @c_bold, @c_reset, @c_error_word, 85 | @c_error_name, @c_error_description = [""]*11 86 | end 87 | end 88 | 89 | # Changes the screen clearing code used when printing the help/usage instructions to the one set by the user. 90 | def set_screen_clear 91 | if @clear_screen 92 | @s_clear_screen_code = @clear_screen_code 93 | else 94 | @s_clear_screen_code = "" 95 | end 96 | end 97 | 98 | end 99 | 100 | end -------------------------------------------------------------------------------- /lib/commandable/command_parser.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | module Commandable 4 | 5 | class << self 6 | 7 | # An array of methods that can be executed from the command line 8 | def commands 9 | @@commands.dup 10 | end 11 | 12 | # A hash of instances created when calling instance methods 13 | # It's keyed using the class name: {"ClassName"=>#} 14 | def class_cache 15 | @@class_cache 16 | end 17 | 18 | # Access the command array using the method name (symbol or string) 19 | def [](index) 20 | raise AccessorError unless index.is_a? String or index.is_a? Symbol 21 | @@commands[index.to_sym] 22 | end 23 | 24 | # Clears all methods from the list of available commands 25 | # This is mostly useful for testing. 26 | def clear_commands 27 | @@command_options = nil 28 | @@commands = HELP_COMMAND.dup 29 | end 30 | 31 | # Convenience method to iterate over the array of commands using the Commandable module 32 | def each(&block) 33 | @@commands.each do |key, value| 34 | yield key => value 35 | end 36 | end 37 | 38 | end 39 | 40 | private 41 | 42 | def init_properties 43 | @@attribute = nil 44 | @@method_file = nil 45 | @@method_line = nil 46 | @@command_options = {} 47 | end 48 | 49 | def parse_parameters(params) 50 | while (param = params.shift) 51 | case param 52 | when Symbol 53 | if param == :xor 54 | @@command_options.merge!(param=>:xor) 55 | else 56 | @@command_options.merge!(param=>true) 57 | end 58 | when Hash 59 | @@command_options.merge!(param) 60 | when String 61 | @@command_options.merge!(:description=>param) 62 | end 63 | end 64 | @@command_options[:priority] ||= 0 65 | end 66 | 67 | # This is where the magic happens! 68 | # It lets you add a method to the list of command line methods 69 | def command(*cmd_parameters) 70 | 71 | init_properties 72 | 73 | # Include Commandable in singleton classes so class level methods work 74 | include Commandable unless self.include? Commandable 75 | 76 | # parse command parameters 77 | parse_parameters(cmd_parameters) 78 | 79 | # only one default allowed 80 | raise ConfigurationError, "Only one default method is allowed." if @@default_method and @@command_options[:default] 81 | 82 | set_trace_func proc { |event, file, line, id, binding, classname| 83 | 84 | @@attribute = id if [:attr_accessor, :attr_writer].include?(id) 85 | 86 | # Traps the line where the method is defined so we can look up 87 | # the method source code later if there are optional parameters 88 | if event == "line" and !@@method_file 89 | @@method_file = file 90 | @@method_line = line 91 | end 92 | 93 | # Raise an error if there is no method following a command definition 94 | if event == "end" 95 | set_trace_func(nil) 96 | raise SyntaxError, "A command was specified but no method follows" 97 | end 98 | } 99 | 100 | end 101 | 102 | # Add a method to the list of available command line methods 103 | def add_command(meth) 104 | @@commands.delete(:help) 105 | 106 | if @@attribute 107 | argument_list = "value" 108 | meth = meth.to_s.delete("=").to_sym if @@attribute == :attr_writer 109 | else 110 | argument_list = parse_arguments(@@command_options[:parameters]) 111 | end 112 | @@command_options.merge!(:argument_list=>argument_list,:class => self.name) 113 | 114 | @@commands.merge!(meth => @@command_options) 115 | @@default_method = {meth => @@command_options} if @@command_options[:default] 116 | 117 | @@commands.sort.each {|com| @@commands.merge!(com[0]=>@@commands.delete(com[0]))} 118 | 119 | @@commands.merge!(HELP_COMMAND.dup) # makes sure the help command is always last 120 | @@command_options = nil 121 | @@attribute = nil 122 | end 123 | 124 | # Trap method creation after a command call 125 | def method_added(meth) 126 | set_trace_func(nil) 127 | return super(meth) if meth == :initialize || @@command_options == nil 128 | 129 | if @@attribute 130 | #synthesize parameter 131 | @@command_options.merge!(:parameters=>[[:writer, :value]],:class_method=>false) 132 | else 133 | # create parameter 134 | @@command_options.merge!(:parameters=>self.instance_method(meth).parameters,:class_method=>false) 135 | end 136 | 137 | add_command(meth) 138 | end 139 | 140 | # Trap class methods too 141 | def singleton_method_added(meth) 142 | set_trace_func(nil) 143 | return super(meth) if meth == :initialize || @@command_options == nil 144 | @@command_options.merge!(:parameters=>method(meth).parameters, :class_method=>true) 145 | add_command(meth) 146 | end 147 | 148 | end -------------------------------------------------------------------------------- /lib/commandable/controller.rb: -------------------------------------------------------------------------------- 1 | module Commandable 2 | 3 | class << self 4 | 5 | # Resets the class to default values clearing any commands 6 | # and setting the colors back to their default values. 7 | def reset_all 8 | clear_commands 9 | reset_colors 10 | reset_screen_clearing 11 | 12 | @app_info = nil 13 | @app_exe = nil 14 | @verbose_parameters = true 15 | @@default_method = nil 16 | @@class_cache = {} 17 | end 18 | 19 | end 20 | 21 | # Inititializes Commandable's settings when it's first loaded 22 | reset_all 23 | 24 | end -------------------------------------------------------------------------------- /lib/commandable/exceptions.rb: -------------------------------------------------------------------------------- 1 | module Commandable 2 | 3 | # Programmer errors 4 | 5 | # An error made by the programmer when specifiying a command 6 | class SyntaxError < StandardError 7 | end 8 | 9 | # A programmer's error raised if setting are specifiying incorrectly, 10 | # For example if you set more than one default method. 11 | class ConfigurationError < StandardError 12 | end 13 | 14 | # A programmer's error raised if the list of commands is accessed using something other than a string or :symbol 15 | # This is meant to catch meta-programming errors. 16 | class AccessorError < StandardError 17 | # Create a new instance of the AccessorError class 18 | def initialize(msg = "You may only access Commandable[] using a string or :symbol" ) 19 | super(msg) 20 | end 21 | end 22 | 23 | # User errors 24 | 25 | # An error raised if a user does not provide a required command 26 | class MissingRequiredCommandError < ScriptError 27 | # Returns a more print friendly error name 28 | def friendly_name 29 | "Missing Required Command" 30 | end 31 | # Create a new instance of the MissingRequiredCommandError class 32 | def initialize(msg) 33 | super("The required command \"#{msg}\" is missing.") 34 | end 35 | end 36 | 37 | 38 | # An error raised if a user does not provide a required command 39 | class MissingRequiredParameterError < ScriptError 40 | # Returns a more print friendly error name 41 | def friendly_name 42 | "Missing Required Parmeter" 43 | end 44 | # Create a new instance of the MissingRequiredParameterError class 45 | def initialize(msg) 46 | super(""" 47 | The #{"default " if msg[:default]}command \"#{msg[:method]}\" is missing the required parameter \"#{msg[:parameters]}\". 48 | #{"You don't have to specifically say \"#{msg[:method]}\" but you do have to give the parameter." if msg[:default]}" 49 | ) 50 | end 51 | end 52 | 53 | 54 | # This error is raised if a user gives two or more commands from the same exclusive group 55 | class ExclusiveMethodClashError < ScriptError 56 | # Returns a more print friendly error name 57 | def friendly_name 58 | "Exclusive " 59 | end 60 | # Create a new instance of the MissingRequiredCommandError class 61 | def initialize(msg) 62 | super("You may only run one of these commands at a time: \"#{msg}\"") 63 | end 64 | end 65 | 66 | # A error raised if a user tries to run a command that is not in the commands array 67 | class UnknownCommandError < ScriptError 68 | # Returns a more print friendly error name 69 | def friendly_name 70 | "Unknown Command" 71 | end 72 | # Create a new instance of the MissingRequiredCommandError class 73 | def initialize(msg) 74 | super("There is no \"#{msg}\" command") 75 | end 76 | end 77 | 78 | end -------------------------------------------------------------------------------- /lib/commandable/execution_controller.rb: -------------------------------------------------------------------------------- 1 | require 'set' 2 | 3 | # Extending your class with this module allows you to 4 | # use the #command method above your method. 5 | # This makes them executable from the command line. 6 | module Commandable 7 | 8 | class << self 9 | 10 | # A wrapper for the execution_queue that runs the queue and traps errors. 11 | # If an error occurs inside this method it will print out a complete list 12 | # of availavle methods with usage instructions and exit gracefully. 13 | # 14 | # If you do not want the output from your methods to be printed out automatically 15 | # run the execute command with silent set to anything other than false or nil. 16 | def execute(argv=ARGV.clone, silent=false) 17 | begin 18 | command_queue = execution_queue(argv) 19 | command_queue.each do |com| 20 | return_value = com[:proc].call 21 | puts return_value if !silent || com[:method] == :help 22 | end 23 | rescue SystemExit => kernel_exit 24 | Kernel.exit kernel_exit.status 25 | rescue Exception => exception 26 | if exception.respond_to?(:friendly_name) 27 | set_colors 28 | puts help("\n #{@c_error_word}Error:#{@c_reset} #{@c_error_name}#{exception.friendly_name}#{@c_reset}\n #{@c_error_description}#{exception.message}#{@c_reset}\n\n") 29 | else 30 | puts exception.inspect 31 | puts exception.backtrace.collect{|line| " #{line}"} 32 | end 33 | end 34 | end 35 | 36 | # Returns an array of executable procs based on the given array of commands and parameters 37 | # Normally this would come from the command line parameters in the ARGV array. 38 | def execution_queue(argv) 39 | arguments = argv.dup 40 | method_hash = {} 41 | last_method = nil 42 | 43 | if arguments.empty? 44 | arguments << @@default_method.keys[0] if @@default_method and !@@default_method.values[0][:parameters].flatten.include?(:req) 45 | end 46 | arguments << "help" if arguments.empty? 47 | 48 | # Parse the command line into methods and their parameters 49 | 50 | arguments.each do |arg| 51 | if Commandable[arg] 52 | last_method = arg.to_sym 53 | method_hash.merge!(last_method=>[]) 54 | else 55 | unless last_method 56 | default = find_by_subkey(:default) 57 | 58 | # Raise an error if there is no default method and the first item isn't a method 59 | raise UnknownCommandError, arguments.first if default.empty? 60 | 61 | # Raise an error if there is a default method but it doesn't take any parameters 62 | raise UnknownCommandError, (arguments.first) if default.values[0][:argument_list] == "" 63 | 64 | last_method = default.keys.first 65 | method_hash.merge!(last_method=>[]) 66 | end 67 | method_hash[last_method] << arg 68 | end 69 | # Test for missing required switches 70 | @@commands.select do |key, value| 71 | if value[:required] and method_hash[key].nil? 72 | # If the required switch is also a default have the error be a missing parameter instead of a missing command 73 | if value[:default] 74 | method_hash.merge!(key=>[]) 75 | else 76 | raise MissingRequiredCommandError, key 77 | end 78 | end 79 | end 80 | end 81 | 82 | # Build an array of procs to be called for each method and its given parameters 83 | proc_array = [] 84 | method_hash.each do |meth, params| 85 | command = @@commands[meth] 86 | 87 | if command[:parameters] && !command[:parameters].empty? 88 | 89 | #Change the method name for attr_writers 90 | meth = "#{meth}=" if command[:parameters][0][0] == :writer 91 | 92 | # Get a list of required parameters and make sure all of them were provided 93 | required = command[:parameters].select{|param| [:req, :writer].include?(param[0])} 94 | required.shift(params.count) 95 | raise MissingRequiredParameterError, {:method=>meth, :parameters=>required.collect!{|meth| meth[1]}.to_s[1...-1].gsub(":",""), :default=>command[:default]} unless required.empty? 96 | end 97 | 98 | # Test for duplicate XORs 99 | proc_array.select{|x| x[:xor] and x[:xor]==command[:xor] }.each {|bad| raise ExclusiveMethodClashError, "#{meth}, #{bad[:method]}"} 100 | 101 | klass = Object 102 | command[:class].split(/::/).each { |name| klass = klass.const_get(name) } 103 | ## Look for class in class cache 104 | unless command[:class_method] 105 | klass = (@@class_cache[klass.name] ||= klass.new) 106 | end 107 | proc_array << {:method=>meth, :xor=>command[:xor], :parameters=>params, :priority=>command[:priority], :proc=>lambda{klass.send(meth, *params)}} 108 | end 109 | proc_array.sort{|a,b| a[:priority] <=> b[:priority]}.reverse 110 | 111 | end 112 | 113 | private 114 | 115 | # Look through commands for a specific subkey 116 | def find_by_subkey(key, value=true) 117 | @@commands.select {|meth, meth_value| meth_value[key]==value} 118 | end 119 | 120 | end 121 | 122 | end -------------------------------------------------------------------------------- /lib/commandable/help_text.rb: -------------------------------------------------------------------------------- 1 | module Commandable 2 | 3 | # Default command that always gets added to end of the command list 4 | HELP_COMMAND = {:help => {:description => "you're looking at it now", :argument_list => "", :class=>"Commandable", :class_method=>true}} 5 | 6 | class << self 7 | 8 | # Describes your application, printed at the top of help/usage messages 9 | attr_accessor :app_info 10 | 11 | # Used when building the usage line, e.g. Usage: app_exe [command] [parameters] 12 | attr_accessor :app_exe 13 | 14 | # If optional parameters show default values, true by default 15 | attr_accessor :verbose_parameters 16 | 17 | # Generates an array of the available commands with a 18 | # list of their parameters and the method's description. 19 | # This includes the applicaiton info and app name if given. 20 | # It's meant to be printed to the command line. 21 | def help(additional_info=nil) 22 | 23 | set_colors 24 | set_screen_clear 25 | 26 | cmd_length = "Command".length 27 | parm_length = "Parameters".length 28 | max_command = [(@@commands.keys.max_by{|key| key.to_s.length }).to_s.length, cmd_length].max 29 | max_parameter = @@commands[@@commands.keys.max_by{|key| @@commands[key][:argument_list].length }][:argument_list].length 30 | max_parameter = [parm_length, max_parameter].max if max_parameter > 0 31 | 32 | usage_text = " #{@c_usage}Usage:#{@c_reset} " 33 | 34 | if Commandable.app_exe 35 | cmd_text = "<#{@c_command + @c_bold}command#{@c_reset}>" 36 | parm_text = " [#{@c_parameter + @c_bold}parameters#{@c_reset}]" if max_parameter > 0 37 | usage_text += "#{@c_app_exe + app_exe + @c_reset} #{cmd_text}#{parm_text} [#{cmd_text}#{parm_text}...]" 38 | end 39 | 40 | array = [usage_text, ""] 41 | 42 | array.unshift additional_info if additional_info 43 | array.unshift (@c_app_info + Commandable.app_info + @c_reset) if Commandable.app_info 44 | array.unshift @s_clear_screen_code 45 | 46 | header_text = " #{" "*(max_command-cmd_length)}#{@c_command + @c_bold}Command#{@c_reset} " 47 | header_text += "#{@c_parameter + @c_bold}Parameters #{@c_reset}#{" "*(max_parameter-parm_length)}" if max_parameter > 0 48 | header_text += "#{@c_description + @c_bold}Description#{@c_reset}" 49 | 50 | array << header_text 51 | 52 | array += @@commands.keys.collect do |key| 53 | is_default = (@@default_method and key == @@default_method.keys[0]) 54 | default_color = is_default ? @c_bold : "" 55 | 56 | help_line = " #{" "*(max_command-key.length)}#{@c_command + default_color + key.to_s + @c_reset}"+ 57 | " #{default_color + @c_parameter + @@commands[key][:argument_list] + @c_reset}" 58 | help_line += "#{" "*(max_parameter-@@commands[key][:argument_list].length)} " if max_parameter > 0 59 | 60 | # indent new lines 61 | description = @@commands[key][:description].gsub("\n", "\n" + (" "*(max_command + max_parameter + (max_parameter > 0 ? 1 : 0) + 4))) 62 | 63 | help_line += ": #{default_color + @c_description}#{"<#{@@commands[key][:xor]}> " if @@commands[key][:xor]}" + 64 | "#{description}" + 65 | "#{" (default)" if is_default}#{@c_reset}" 66 | end 67 | array << nil 68 | end 69 | 70 | 71 | end 72 | 73 | end -------------------------------------------------------------------------------- /lib/commandable/version.rb: -------------------------------------------------------------------------------- 1 | # :nodoc: 2 | module Commandable 3 | module VERSION 4 | MAJOR = 0 5 | MINOR = 3 6 | TINY = 2 7 | PRE = nil 8 | 9 | STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.') 10 | 11 | SUMMARY = "Commandable #{STRING}" 12 | end 13 | end -------------------------------------------------------------------------------- /spec/commandable/00_run_first_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | # These tests MUST run first to be valid 4 | 5 | describe Commandable do 6 | 7 | it 'should not raise an error if a non-command method is before a command method' do 8 | load 'run_first.rb' 9 | capture_output{Commandable.execute([])}.to_s.should_not match(/Error:/) 10 | end 11 | 12 | it 'should not raise an error if a non-command class method is before a command method' do 13 | Commandable.reset_all 14 | load 'run_first_class.rb' 15 | capture_output{Commandable.execute([])}.to_s.should_not match(/Error:/) 16 | end 17 | 18 | 19 | end -------------------------------------------------------------------------------- /spec/commandable/app_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { 6 | Commandable.reset_all 7 | Commandable.color_output = true 8 | Commandable.verbose_parameters = false 9 | Commandable.app_exe = "commandable" 10 | Commandable.app_info = 11 | """ 12 | \e[92mCommandable\e[0m - The easiest way to add command line control to your app. 13 | Copyrighted free software - Copyright (c) 2011 Mike Bethany. 14 | Version: #{Commandable::VERSION::STRING} 15 | """ 16 | 17 | # App controller has to be loaded after commandable settings 18 | # or it won't be able to use the settings 19 | load 'commandable/app_controller.rb' 20 | } 21 | 22 | it "should have a test for examples" 23 | 24 | it "should have a test for the readme file" 25 | 26 | end -------------------------------------------------------------------------------- /spec/commandable/attr_accessor_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { 6 | Commandable.reset_all 7 | Commandable.color_output = true 8 | Commandable.verbose_parameters = false 9 | Commandable.app_exe = "fake_app" 10 | Commandable.app_info = 11 | """ 12 | \e[92mFake App!\e[0m - It's not real! 13 | """ 14 | } 15 | 16 | 17 | context "when command is used on an attr_accessor" do 18 | 19 | context 'when there is a single accessor per line' do 20 | 21 | before(:each) { load 'attr_accessor.rb' } 22 | 23 | context "when testing syntax" do 24 | 25 | it "should work with an attr_accessor" do 26 | lambda{execute_queue(["some_accesor", "a value"])}.should_not raise_error 27 | end 28 | 29 | it "should work with an attr_writer" do 30 | lambda{execute_queue(["some_writer", "used the writer"])}.should_not raise_error 31 | end 32 | 33 | it "should work with an attr_writer and an attr_accessor" do 34 | lambda{execute_queue(["some_accesor", "a value", "some_writer", "used the writer"])}.should_not raise_error 35 | end 36 | 37 | it "should raise an error is a value isn't given" do 38 | lambda{execute_queue(["some_accesor"])}.should raise_error(Commandable::MissingRequiredParameterError) 39 | end 40 | 41 | it "should raise an error is a value isn't given" do 42 | lambda{execute_queue(["some_writer"])}.should raise_error(Commandable::MissingRequiredParameterError) 43 | end 44 | 45 | it "should raise an error is a value isn't given" do 46 | lambda{execute_queue(["some_accesor", "some_writer", "used the writer"])}.should raise_error(Commandable::MissingRequiredParameterError) 47 | end 48 | 49 | it "should raise an error is a value isn't given" do 50 | lambda{execute_queue(["some_accesor", "a value", "some_writer"])}.should raise_error(Commandable::MissingRequiredParameterError) 51 | end 52 | end 53 | 54 | it {execute_output_s(["some_accesor", "a value"]).should include("a value")} 55 | it {execute_output_s(["some_writer", "blah blah blah"]).should include("blah blah blah")} 56 | 57 | end 58 | 59 | context 'when there is more than one accessor on the same line' do 60 | 61 | before(:each) { load 'attr_accessor_multi.rb' } 62 | 63 | context "when testing syntax" do 64 | it "should use the first accessor" do 65 | lambda{execute_queue(["first_accessor", "a value"])}.should_not raise_error 66 | end 67 | 68 | it "should ignore the second accessor" do 69 | lambda{execute_queue(["second_accessor", "blipidy"])}.should raise_error 70 | end 71 | 72 | it "should use the first writer" do 73 | lambda{execute_queue(["first_writer", "a value"])}.should_not raise_error 74 | end 75 | 76 | it "should ignore the second writer" do 77 | lambda{execute_queue(["second_writer", "blipidy"])}.should raise_error 78 | end 79 | end 80 | 81 | it {execute_output_s(["first_accessor", "my very own accessor"]).should include("my very own accessor")} 82 | it {execute_output_s(["first_writer", "Look ma! A writer!"]).should include("Look ma! A writer!")} 83 | 84 | end 85 | 86 | end 87 | 88 | end -------------------------------------------------------------------------------- /spec/commandable/coloring_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) do 6 | Commandable.reset_all 7 | load 'private_methods_bad.rb' 8 | Commandable.app_exe = "mycoolapp" 9 | Commandable.app_info = 10 | """ My Cool App - It does stuff and things! 11 | Copyright (c) 2011 Acme Inc.""" 12 | end 13 | 14 | let(:c) {Term::ANSIColor} 15 | 16 | context "when screen clearing" do 17 | 18 | it "should clear the screen if screen clearing is on" do 19 | Commandable.clear_screen = true 20 | clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}") 21 | Commandable.help.join.should match(clear_screen_code) 22 | end 23 | 24 | it "should not clear the screen if screen clearing is off" do 25 | Commandable.clear_screen = true 26 | clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}") 27 | Commandable.clear_screen = false 28 | Commandable.help.join.should_not match(clear_screen_code) 29 | end 30 | 31 | it "should change how the screen is cleared" do 32 | Commandable.clear_screen = true 33 | clear_code = "FlabbityJibbity" 34 | Commandable.clear_screen_code = clear_code 35 | Commandable.help.join.should match(clear_code) 36 | end 37 | 38 | it "should not be disabled when color output is disabled" do 39 | Commandable.clear_screen = true 40 | clear_screen_code = Regexp.escape("#{Commandable.clear_screen_code}") 41 | Commandable.color_output = false 42 | Commandable.help.join.should match(clear_screen_code) 43 | end 44 | 45 | end 46 | 47 | context "when a setting color is changed" do 48 | 49 | before(:each) { Commandable.color_output = true } 50 | 51 | it "should include colors if colored output is enabled" do 52 | Commandable.color_output = true 53 | Commandable.help.join.should match(Regexp.escape(Commandable.color_app_info)) 54 | end 55 | 56 | it "should not include colors if colored output is disabled" do 57 | Commandable.color_output = false 58 | Commandable.help.join.should_not match(Regexp.escape(Commandable.color_app_info)) 59 | end 60 | 61 | # This seems ripe for meta-zation 62 | context "and app_info is changed" do 63 | specify {lambda {Commandable.color_app_info = c.black}.should change{Commandable.help}} 64 | end 65 | 66 | context "and app_exe is changed" do 67 | specify {lambda {Commandable.color_app_exe = c.black}.should change{Commandable.help}} 68 | end 69 | 70 | context "and color_command is changed" do 71 | specify {lambda {Commandable.color_command = c.black}.should change{Commandable.help}} 72 | end 73 | 74 | context "and color_description is changed" do 75 | specify {lambda {Commandable.color_description = c.black}.should change{Commandable.help}} 76 | end 77 | 78 | context "and color_parameter is changed" do 79 | specify {lambda {Commandable.color_parameter = c.black}.should change{Commandable.help}} 80 | end 81 | 82 | context "and color_usage is changed" do 83 | specify {lambda {Commandable.color_usage = c.black}.should change{Commandable.help}} 84 | end 85 | 86 | context "and there is an error" do 87 | 88 | specify { lambda {Commandable.color_error_word = c.magenta}.should change{capture_output{Commandable.execute(["fly", "navy"])}}} 89 | specify { lambda {Commandable.color_error_name = c.intense_red}.should change{capture_output{Commandable.execute(["fly", "navy"])}}} 90 | specify { lambda {Commandable.color_error_description = c.black + c.bold}.should change{capture_output{Commandable.execute(["fly", "navy"])}}} 91 | 92 | end 93 | 94 | end 95 | 96 | end -------------------------------------------------------------------------------- /spec/commandable/command_line_execution_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { 6 | Commandable.reset_all 7 | Commandable.app_exe = "mycoolapp" 8 | Commandable.app_info = 9 | """ 10 | My Cool App - It does stuff and things! 11 | Copyright (c) 2011 Acme Inc. 12 | """ 13 | 14 | load 'parameter_class.rb' 15 | } 16 | 17 | context "when parsing arguments" do 18 | 19 | context "and there is an error in the command line" do 20 | 21 | context "because a required parameter is not given" do 22 | 23 | context "with a class that doesn't have a default method" do 24 | specify{lambda{execute_queue(["zaz"]).should raise_error(MissingRequiredParameterError)}} 25 | specify{lambda{execute_output_s(["zaz"]).should include(/Missing Required/)}} 26 | specify{lambda{execute_queue(["zaz", "potato"]).should raise_error(MissingRequiredParameterError)}} 27 | specify{lambda{execute_output_s(["zaz", "potato"]).should include(/Missing Required/)}} 28 | specify{lambda{execute_queue(["zaz", "potato", "gump"]).should_not raise_error}} 29 | specify{lambda{execute_output_s(["zaz", "potato", "gump"]).should_not include(/Missing Required/)}} 30 | end 31 | 32 | # When a default method has a required parameter and nothing is given on the command 33 | # line Commandable will run help so you won't get a MissingRequiredParameterError 34 | 35 | end 36 | 37 | context "and the default method does not accept parameters" do 38 | 39 | before(:each) {load 'default_method_no_params.rb'} 40 | 41 | context "but something that isn't a method is the first thing on the command line" do 42 | specify {lambda{execute_queue(["potato"])}.should raise_error(Commandable::UnknownCommandError)} 43 | specify { execute_output_s(["potato"]).should match(/Unknown Command/)} 44 | end 45 | 46 | end 47 | 48 | context "and running procs from the execution queue" do 49 | it "raises an error if there is an invalid command given" do 50 | lambda{execute_queue(["fly", "navy"])}.should raise_error(Commandable::UnknownCommandError) 51 | end 52 | end 53 | 54 | context "and even when silent is on" do 55 | it "prints usage/help instructions" do 56 | capture_output{Commandable.execute(["help"], :silent)}.to_s.should match(/Usage:/) 57 | end 58 | end 59 | 60 | context "and running commands automatically via execute" do 61 | 62 | context "and an unknown command is sent" do 63 | it "traps errors" do 64 | capture_output{lambda{Commandable.execute(["fly", "navy"])}.should_not raise_error} 65 | end 66 | it "prints the error" do 67 | capture_output{Commandable.execute(["fly", "navy"])}.to_s.should 68 | match(/Error: Unknown Command(.*)There is no \\\"fly\\\" command/) 69 | end 70 | it "prints usage/help instructions" do 71 | capture_output{Commandable.execute(["fly", "navy"])}.to_s.should match(/Usage:/) 72 | end 73 | end 74 | 75 | context "and an unknown command is sent" do 76 | 77 | before(:each) { load 'required_methods.rb' } 78 | it "traps errors" do 79 | capture_output{lambda{Commandable.execute(["non_required_method", "blorp"])}.should_not raise_error} 80 | end 81 | it "prints the error" do 82 | capture_output{Commandable.execute(["non_required_method", "blorp"])}.to_s.should 83 | match(/Error: Missing Required Command(.*)The required command \\\"required_method\\\" is missing/) 84 | end 85 | it "prints usage/help instructions" do 86 | capture_output{Commandable.execute(["non_required_method", "blorp"])}.to_s.should match(/Usage:/) 87 | end 88 | end 89 | 90 | end 91 | 92 | end 93 | 94 | context "and the command line is valid" do 95 | 96 | it "does not raise an error" do 97 | capture_output{Commandable.execute(["foo", "1", "2"])}.to_s.should_not match(/Error:/) 98 | end 99 | 100 | context "and automatically executing commands" do 101 | it {execute_output_ary(["foo", "1", "2.4"]).should == ["1", "2.4"]} 102 | it {execute_output_ary(["bar", "234"]).should == ["234", "Number 42"]} 103 | it {execute_output_ary(["bar", "39", "potato"]).should == ["39", "potato"]} 104 | it {execute_output_ary(["qux"]).should == ["1492", "I'm a tricky one"]} 105 | it {execute_output_ary(["qux", "991"]).should == ["991", "I'm a tricky one"]} 106 | it {execute_output_ary(["qux", "1821", "Look I've got %special\"characters\" in me"]).should == 107 | ["1821", "Look I've got %special\"characters\" in me"]} 108 | 109 | context "when specifying that it should be silent" do 110 | it {execute_output_s(["foo", "1", "2.4"], :silent).should be_empty} 111 | end 112 | 113 | end 114 | 115 | context "and using the execution_queue command" do 116 | 117 | context "and only one command is given" do 118 | 119 | it "only has one command in the array" do 120 | command_queue = Commandable.execution_queue(["foo", "1", "2.4"]) 121 | command_queue.length.should == 1 122 | end 123 | 124 | it "properly parses parameters" do 125 | command_queue = Commandable.execution_queue(["foo", "1", "2.4"]) 126 | command_queue.first[:parameters].should == ["1", "2.4"] 127 | end 128 | 129 | end 130 | 131 | context "and more than one command is given" do 132 | 133 | it "has the correct number of commands in the array" do 134 | command_queue = Commandable.execution_queue(["foo", "1", "2.4", "bar", "71"]) 135 | command_queue.length.should == 2 136 | end 137 | 138 | it "properly parses parameters" do 139 | command_queue = Commandable.execution_queue(["foo", "1", "2.4", "bar", "71"]) 140 | command_queue.each do |command| 141 | command[:parameters].should == ["71"] if command[:method] == :bar 142 | command[:parameters].should == ["1", "2.4"] if command[:method] == :foo 143 | end 144 | end 145 | 146 | end 147 | 148 | end 149 | 150 | context "and there are greedy parameters" do 151 | # number_arg1, string_arg2 = "blorp", *array_arg3 152 | specify {execute_output_ary(["baz", "9"]).should == ["9", "blorp"]} 153 | specify {execute_output_ary(["baz", "81", "Fish"]).should == ["81", "Fish"]} 154 | specify {execute_output_ary(["baz", "3278", "Bubba", "Blip"]).should == ["3278", "Bubba", "Blip"]} 155 | specify {execute_output_ary(["baz", "0.0234", "Yellow", "Mellow", "Fellow", "bellow", "elbow"]).should == 156 | ["0.0234", "Yellow", "Mellow", "Fellow", "bellow", "elbow"] 157 | } 158 | 159 | context "and it has multiple commands" do 160 | 161 | specify {execute_output_ary(["baz", "0.0234", "Yellow", "Mellow", "elbow", "qux"]).should == 162 | ["1492", "I'm a tricky one", 163 | "0.0234", "Yellow", "Mellow", "elbow"] 164 | } 165 | 166 | specify {execute_output_ary(["baz", "0.0234", "Yellow", "Mellow", "elbow", "foo", "17", "432", "qux"]).should == 167 | ["1492", "I'm a tricky one", 168 | "17", "432", 169 | "0.0234", "Yellow", "Mellow", "elbow"] 170 | } 171 | 172 | end 173 | 174 | end 175 | 176 | context "when a default method name isn't specified but the required switch is properly given" do 177 | before(:each){load "default_method.rb"} 178 | specify{lambda{execute_queue(["phish"])}.should_not raise_error} 179 | specify{lambda{execute_output_s(["phish"]).should include(/default method called with: phish/)}} 180 | specify{execute_queue(["not_a_default_method", "potato"]).should include("not a default method, called with: Cleveland, potato") } 181 | specify{execute_output_s(["not_a_default_method", "phish"]).should include("not a default method, called with: Cleveland, phish")} 182 | end 183 | 184 | end 185 | 186 | end 187 | 188 | end 189 | -------------------------------------------------------------------------------- /spec/commandable/commandable_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) {Commandable.reset_all} 6 | 7 | context "when using Commandable directly" do 8 | 9 | before(:each) { load 'test_class.rb' } 10 | 11 | context "when clearing values" do 12 | specify { lambda{Commandable.clear_commands}.should_not raise_error} 13 | specify { lambda{Commandable.reset_all}.should change{Commandable.commands.length}.from(2).to(1) } 14 | end 15 | 16 | context "when adding command to methods" do 17 | specify { lambda{load 'test_class.rb'}.should_not raise_error } 18 | end 19 | 20 | context "when checking syntax" do 21 | 22 | it "shouldn't raise an error if nothing more than 'switch' is used" do 23 | lambda{load 'no_description.rb'}.should_not raise_error(Commandable::SyntaxError) 24 | end 25 | 26 | it "rasies an error if no method follows a description" do 27 | lambda{load 'class_bad.rb'}.should raise_error(Commandable::SyntaxError) 28 | end 29 | 30 | end 31 | 32 | context "when enumerating" do 33 | 34 | it "is enumerable using #each" do 35 | lambda{Commandable.each {|x|}}.should_not raise_error 36 | end 37 | 38 | it "allows access to methods using []" do 39 | lambda{Commandable["test_method"]}.should_not raise_error 40 | end 41 | 42 | context "when using []" do 43 | 44 | specify { Commandable["test_method"].should_not be_nil } 45 | specify { Commandable[:test_method].should_not be_nil } 46 | specify { lambda{Commandable[1]}.should raise_error } 47 | specify { lambda{Commandable[String]}.should raise_error } 48 | 49 | context "when using a variable" do 50 | 51 | specify { 52 | method = "method" 53 | Commandable[:test_method].should_not be_nil 54 | } 55 | 56 | specify { 57 | method = :method 58 | Commandable[:test_method].should_not be_nil 59 | } 60 | 61 | end 62 | 63 | end 64 | 65 | end 66 | 67 | context "when accessing method information" do 68 | 69 | it "uses a string in #[] to access methods by name" do 70 | lambda{Commandable["test_method"]}.should_not raise_error 71 | end 72 | 73 | end 74 | 75 | context "when accessing method descriptions" do 76 | 77 | it "uses :descriptions" do 78 | Commandable["test_method"][:description].should == "does some stuff" 79 | end 80 | 81 | end 82 | 83 | context "when reading class names" do 84 | 85 | it "uses :class" do 86 | Commandable["test_method"][:class].should == "TestClass" 87 | end 88 | 89 | it "reads the fully qualified name including parent modules and classes" do 90 | load 'deep_class.rb' 91 | Commandable["deep_method"][:class].should == "TopModule::ParentClass::DeepClass" 92 | end 93 | 94 | end 95 | 96 | end 97 | 98 | context "when reading the method parameters" do 99 | 100 | before(:each) { load 'parameter_class.rb' } 101 | 102 | context "when it has required parameters" do 103 | specify{ Commandable[:foo][:argument_list].should == "int_arg1 number_arg2" } 104 | end 105 | 106 | context "when it has optional parameters" do 107 | specify {Commandable[:bar][:argument_list].should == %q{int_arg1 [string_arg2="Number 42"]} } 108 | specify {Commandable[:qux][:argument_list].should == %q{[string_arg1="1492"] [string_arg2="I'm a tricky one"]} } 109 | end 110 | 111 | context "when it has greedy parameters" do 112 | specify {Commandable[:baz][:argument_list].should == %q{number_arg1 [string_arg2="blorp"] *array_arg3} } 113 | end 114 | 115 | context "when it has an instance method" do 116 | specify {Commandable[:foo][:class_method].should be_false} 117 | end 118 | 119 | it "saves the list of parameters" do 120 | Commandable[:foo][:parameters].should == [[:req, :int_arg1], [:req, :number_arg2]] 121 | end 122 | 123 | end 124 | 125 | context "when there are methods using command and methods not using command" do 126 | 127 | context "and they are instance methods" do 128 | 129 | before(:each) {load 'command_no_command.rb'} 130 | 131 | specify {Commandable.commands.should include(:command_method1)} 132 | specify {Commandable.commands.should include(:command_method2)} 133 | specify {Commandable.commands.should_not include(:no_command_method1)} 134 | specify {Commandable.commands.should_not include(:no_command_method2)} 135 | 136 | end 137 | 138 | context "and they are class methods" do 139 | 140 | before(:each) {load 'class_command_no_command.rb'} 141 | 142 | specify {Commandable.commands.should include(:class_command_method1)} 143 | specify {Commandable.commands.should include(:class_command_method2)} 144 | specify {Commandable.commands.should_not include(:class_no_command_method1)} 145 | specify {Commandable.commands.should_not include(:class_no_command_method2)} 146 | 147 | end 148 | 149 | end 150 | 151 | context "when using class methods" do 152 | 153 | before(:each) { load 'class_methods.rb' } 154 | 155 | specify { Commandable["class_method"].should_not be_nil } 156 | specify { Commandable["class_method"][:argument_list].should == "string_arg1" } 157 | specify { Commandable["class_method2"][:argument_list].should == "integer_arg1" } 158 | 159 | context "grouped class methods" do 160 | 161 | before(:each) { 162 | load 'class_methods_nested.rb' 163 | } 164 | 165 | specify{ Commandable[:class_foo][:argument_list].should == "int_arg1 number_arg2" } 166 | specify {Commandable[:class_bar][:argument_list].should == %q{int_arg1 [string_arg2="Number 42"]} } 167 | specify {Commandable[:class_qux][:argument_list].should == %q{[string_arg1="1492"] [string_arg2="I'm a tricky one"]} } 168 | specify {Commandable[:class_baz][:argument_list].should == %q{number_arg1 [string_arg2="blorp"] *array_arg3} } 169 | 170 | end 171 | 172 | it "knows it is a class method" do 173 | Commandable["class_method"][:class_method].should be_true 174 | end 175 | 176 | end 177 | 178 | context "when there is a default command" do 179 | 180 | before (:each) { load 'default_method.rb' } 181 | 182 | it "sets a command as default" do 183 | Commandable[:default_method][:default].should == true 184 | end 185 | 186 | it "raises an error if more than one command is set as default" do 187 | lambda{load 'default_method_bad.rb'}.should raise_error(Commandable::ConfigurationError) 188 | end 189 | 190 | it "executes the default command if no command is given" do 191 | execute_output_ary(["Klaatu"]).should include("default method called with: Klaatu") 192 | end 193 | 194 | it "executes a default method and a second command" do 195 | execute_output_ary(["Klaatu", "not_a_default_method", "28"]).should include( 196 | "default method called with: Klaatu", 197 | "not a default method, called with: Cleveland, 28") 198 | end 199 | 200 | end 201 | 202 | context "when there are no command line parameters" do 203 | 204 | it "prints help info when there isn't a default command" do 205 | execute_output_s([]).should match(/Usage:/) 206 | end 207 | 208 | context "when there is a default command" do 209 | it "runs the default command if there are no require parameters " do 210 | load "default_method.rb" 211 | execute_output_s([]).should match(/Usage:/) 212 | end 213 | it "prints help/usage info if there are require parameters" do 214 | load "default_method_no_params.rb" 215 | execute_output_s([]).should match(/default method called, has no params/) 216 | end 217 | end 218 | end 219 | 220 | context "when there is a required command" do 221 | 222 | before(:each) {load "required_methods.rb"} 223 | 224 | it "raises an error when a required command isn't given" do 225 | lambda{ Commandable.execution_queue(["non_required_method", "this is some input"]) }.should raise_error(Commandable::MissingRequiredCommandError) 226 | end 227 | 228 | end 229 | 230 | context "when there are deeply nested classes and modules" do 231 | 232 | it "executes the command" do 233 | load "super_deep_class.rb" 234 | lambda{Commandable.execution_queue(["super_deep_method"]).first[:proc].call}.should_not raise_error 235 | end 236 | 237 | it "returns the correct value" do 238 | load "super_deep_class.rb" 239 | Commandable.execution_queue(["super_deep_method"]).first[:proc].call.should == "you called a deep method" 240 | end 241 | 242 | end 243 | 244 | end -------------------------------------------------------------------------------- /spec/commandable/execution_controller_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) {Commandable.reset_all} 6 | 7 | context "when using Commandable directly" do 8 | 9 | before(:each) { load 'test_class.rb' } 10 | 11 | it "should use ARGV by default" do 12 | expect {capture_output{Commandable.execute()}}.to_not raise_error(ArgumentError) 13 | end 14 | 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /spec/commandable/help_generator_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { Commandable.reset_all } 6 | 7 | # Mostly de-brittled these tests... 8 | 9 | context "when setting verbose_parameters" do 10 | 11 | it "prints short parameters when false" do 12 | Commandable.verbose_parameters = false 13 | load 'parameter_class.rb' 14 | Commandable.help.to_s.should_not match("1492") 15 | end 16 | 17 | it "prints short parameters when false" do 18 | Commandable.verbose_parameters = true 19 | load 'parameter_class.rb' 20 | Commandable.help.to_s.should match("1492") 21 | end 22 | 23 | end 24 | 25 | context "when generating the help command" do 26 | 27 | it "has a help command when no commands have been added" do 28 | Commandable.commands.first[0].should == :help 29 | end 30 | 31 | it "still has the help command after Commandable is cleared" do 32 | load 'parameter_class.rb' 33 | Commandable.clear_commands 34 | Commandable.commands.first[0].should == :help 35 | end 36 | 37 | it "always has the help command as the last command (so it's pretty)" do 38 | load 'parameter_class.rb' 39 | Commandable.help.compact.last.should match(/help/) 40 | end 41 | 42 | end 43 | 44 | context "when formating the help message" do 45 | 46 | it "formats a basic help message with commands sorted alphabetically (help last)" do 47 | load 'parameter_class.rb' 48 | Commandable.help.to_s.should match(/Usage:(.*)Command(.*)bar(.*)baz(.*)foo(.*)qux(.*)help/) 49 | end 50 | 51 | it "adds the application name to the help output if it's given" do 52 | Commandable.color_output = false 53 | load 'parameter_class.rb' 54 | Commandable.app_exe = "mycoolapp" 55 | Commandable.help.to_s.should match(/Usage: mycoolapp /) 56 | end 57 | 58 | it "adds appliction information if given" do 59 | load 'parameter_class.rb' 60 | Commandable.app_exe = "mycoolapp" 61 | app_info = 62 | """ 63 | My Cool App - It does stuff and things! 64 | Copyright (c) 2011 Acme Inc. 65 | """ 66 | Commandable.app_info = app_info 67 | Commandable.help.inspect.should match(/My Cool App - It does stuff and things(.*)Copyright \(c\) 2011 Acme Inc/) 68 | end 69 | 70 | it "adds (default) to the end of the default command description when printing" do 71 | load 'default_method.rb' 72 | Commandable.color_output = true 73 | Commandable.help.join.should match(/\(default\)/) 74 | Commandable.color_output = false 75 | Commandable.help.join.should match(/\(default\)/) 76 | end 77 | 78 | context "and there are no parameters" do 79 | 80 | before(:each){load 'parameter_free.rb' } 81 | 82 | it "hides the Parameters header" do 83 | execute_output_s([]).should_not match(/Command Parameters/) 84 | end 85 | 86 | it "hides the doesn't show [parameters] in the usage instructions" do 87 | Commandable.app_exe = "fakeapp" 88 | execute_output_s([]).should_not match(/\[parameters\]/) 89 | end 90 | 91 | end 92 | 93 | context "and there is a new line in a description" do 94 | 95 | it "indents the new line to match the preceding line" do 96 | load("multi_line_description.rb") 97 | execute_output_s(["blah"]).should match(/ so you can have really long descriptions\n And another line/) 98 | end 99 | 100 | it "indents the new line to match the preceding line" do 101 | load("multi_line_description_no_params.rb") 102 | execute_output_s(["blah"]).should match(/ so you can have really long descriptions\n And another line/) 103 | end 104 | 105 | end 106 | 107 | end 108 | 109 | end 110 | -------------------------------------------------------------------------------- /spec/commandable/helpers_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | context "when parsing optional parameters" do 6 | 7 | before(:each) {load 'private_methods_bad.rb'} 8 | 9 | specify {PrivateMethodsBad.send(:parse_optional, "def bar(x=14243)", "x").should == "14243"} 10 | specify {PrivateMethodsBad.send(:parse_optional,"def bar x = 144444", "x").should == "144444"} 11 | specify {PrivateMethodsBad.send(:parse_optional,"def bar x=12", "x").should == "12"} 12 | specify {PrivateMethodsBad.send(:parse_optional,'def bar (x="42", y)', "x").should == "\"42\""} 13 | specify {PrivateMethodsBad.send(:parse_optional,'def bar(y="kjljlj",x="I love Ruby")', "x").should == "\"I love Ruby\""} 14 | 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /spec/commandable/instance_methods_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | context "when calling instance methods" do 6 | 7 | before(:each) do 8 | @first_value = "the first value saved" 9 | @second_value = "the second saved value" 10 | end 11 | 12 | it "saves instance variables between method calls" do 13 | load 'instance_methods.rb' 14 | values = execute_queue(["set_value", @first_value, "get_value"]) 15 | values[1].should == @first_value 16 | end 17 | 18 | context 'when multiple classes are involved' do 19 | 20 | it "saves instance variables between method calls" do 21 | load 'instance_methods.rb' 22 | load 'instance_methods2.rb' 23 | values2 = execute_queue(["set_value2", @second_value, "get_value2"]) 24 | values2[1].should == values2[0] 25 | execute_queue(["get_value"])[0].should == @first_value 26 | end 27 | 28 | end 29 | 30 | context "when running the execution queue manually" do 31 | 32 | it "should give a programmer access to any instances created" do 33 | Commandable.reset_all 34 | load 'instance_methods.rb' 35 | load 'instance_methods2.rb' 36 | execute_queue(["set_value", @first_value, "get_value"]) 37 | execute_queue(["set_value2", @second_value, "get_value2"]) 38 | Commandable.class_cache.keys.should include("InstanceMethods", "InstanceMethods2") 39 | end 40 | 41 | end 42 | 43 | end 44 | 45 | end -------------------------------------------------------------------------------- /spec/commandable/kernel_exit_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { 6 | Commandable.reset_all 7 | Commandable.app_exe = "mycoolapp" 8 | Commandable.app_info = 9 | """ 10 | My Cool App - It does stuff and things! 11 | Copyright (c) 2011 Acme Inc. 12 | """ 13 | 14 | load 'kernel_exit_class.rb' 15 | } 16 | 17 | it "should not trap kernel exits" do 18 | lambda{execute_queue(['exit'])}.should raise_error(SystemExit) 19 | end 20 | 21 | it "should forward kernel exit status codes" do 22 | lambda{execute_queue(['exit', '40'])}.should raise_error(SystemExit) { |error| 23 | error.status.should == 40 24 | } 25 | end 26 | 27 | end -------------------------------------------------------------------------------- /spec/commandable/required_default_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) { 6 | Commandable.reset_all 7 | Commandable.color_output = true 8 | Commandable.verbose_parameters = false 9 | Commandable.app_exe = "fake_app" 10 | Commandable.app_info = 11 | """ 12 | \e[92mFake App!\e[0m - It's not real! 13 | """ 14 | load "required_default.rb" 15 | } 16 | 17 | 18 | context "when there is a required/default command" do 19 | 20 | context "and it has a required parameter" do 21 | 22 | context "but nothing is given on the command line" do 23 | 24 | it "should say a required parameter is missing" do 25 | lambda{execute_queue([])}.should raise_error(Commandable::MissingRequiredParameterError) 26 | end 27 | 28 | it "should say a required parameter is missing" do 29 | execute_output_s([]).should include("default command") 30 | end 31 | 32 | end 33 | 34 | end 35 | 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /spec/commandable/reset_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) do 6 | Commandable.reset_all 7 | load 'parameter_class.rb' 8 | Commandable.app_exe = "mycoolapp" 9 | Commandable.app_info = 10 | """ My Cool App - It does stuff and things! 11 | Copyright (c) 2011 Acme Inc.""" 12 | end 13 | 14 | context "when reseting all values" do 15 | 16 | specify {lambda{Commandable.reset_all}.should change{Commandable.app_exe}.from("mycoolapp").to(nil)} 17 | specify {lambda{Commandable.reset_all}.should change{Commandable.app_info}. 18 | from(%{ My Cool App - It does stuff and things!\n Copyright (c) 2011 Acme Inc.}). 19 | to(nil) 20 | } 21 | specify {lambda{Commandable.reset_all}.should change{Commandable.commands.length}.from(6).to(1)} 22 | 23 | 24 | 25 | end 26 | end -------------------------------------------------------------------------------- /spec/commandable/xor_groups_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | describe Commandable do 4 | 5 | before(:each) do 6 | Commandable.reset_all 7 | Commandable.color_output = true 8 | load 'xor_class.rb' 9 | end 10 | 11 | context "when exclusive methods are specified" do 12 | 13 | # this seems like I'm testing internal state instead of specifying behavior... 14 | specify {Commandable.commands[:xor_method1][:xor].should == :xor} 15 | specify {Commandable.commands[:normal_method][:xor].should be_nil} 16 | specify {Commandable.commands[:xor_method3][:xor].should == :xor_group} 17 | 18 | context "when more than one members of an exclusive group is used" do 19 | 20 | specify{lambda{Commandable.execution_queue(["xor_method1", "xor_method2"])}.should raise_error(Commandable::ExclusiveMethodClashError)} 21 | specify{lambda{Commandable.execution_queue(["xor_method3", "xor_method4"])}.should raise_error(Commandable::ExclusiveMethodClashError)} 22 | specify{lambda{Commandable.execution_queue(["xor_method1", "xor_method3"])}.should_not raise_error} 23 | specify{lambda{Commandable.execution_queue(["normal_method", "xor_method3"])}.should_not raise_error} 24 | specify{lambda{Commandable.execution_queue(["normal_method", "normal_method2"])}.should_not raise_error} 25 | 26 | end 27 | 28 | context "when printing help" do 29 | 30 | it "puts the default xor group :xor in the description" do 31 | execute_output_s(["help"]).should match(/xor/) 32 | end 33 | 34 | it "puts the xor group :xor_group in the description" do 35 | execute_output_s(["help"]).should match(/xor/) 36 | end 37 | 38 | end 39 | 40 | 41 | end 42 | 43 | end -------------------------------------------------------------------------------- /spec/source_code_examples/attr_accessor.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class AttrAccessor 4 | extend Commandable 5 | 6 | command 'uses an attr_accessor' 7 | attr_accessor :some_accesor 8 | 9 | command 'uses an attr_writer' 10 | attr_writer :some_writer 11 | 12 | 13 | end -------------------------------------------------------------------------------- /spec/source_code_examples/attr_accessor_multi.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class AttrAccessorMulti 4 | extend Commandable 5 | 6 | # You can do this just realize the second accessor is ignored 7 | command 'only runs the first accessor' 8 | attr_accessor :first_accessor, :second_accessor 9 | 10 | command 'only runs the first writer' 11 | attr_writer :first_writer, :second_writer 12 | 13 | end -------------------------------------------------------------------------------- /spec/source_code_examples/class_command_no_command.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ClassCommandNoCommand 4 | extend Commandable 5 | 6 | command 'a method with a command' 7 | def class_command_method1 8 | "class_command_method1" 9 | end 10 | 11 | # this method should not be in the commands list 12 | def class_no_command_method1(flappity) 13 | "class_no_command_method1: #{flappity}" 14 | end 15 | 16 | command 'another method with a command' 17 | def class_command_method2(some_parameter) 18 | "class_command_method2: #{some_parameter}" 19 | end 20 | 21 | # this method shouldn't be in the list either 22 | def class_no_command_method2(flippity) 23 | "class_no_command_method2: #{flippity}" 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /spec/source_code_examples/class_methods.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ClassMethods 4 | 5 | extend Commandable 6 | 7 | command 'does some stuff' 8 | def self.class_method(string_arg1) 9 | string_arg1 10 | end 11 | 12 | command "another one" 13 | class << self 14 | def class_method2(integer_arg1) 15 | integer_arg1 16 | end 17 | end 18 | 19 | end -------------------------------------------------------------------------------- /spec/source_code_examples/class_methods_nested.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ClassMethodsNested 4 | 5 | class << self 6 | extend Commandable 7 | 8 | command "class foo, look at you!" 9 | def class_foo(int_arg1, number_arg2) 10 | [int_arg1, number_arg2] 11 | end 12 | 13 | command "classy bar? probably not" 14 | def class_bar(int_arg1, string_arg2="Number 42") 15 | [int_arg1, string_arg2] 16 | end 17 | 18 | command "run me for stuff to happen" 19 | def class_qux string_arg1 ="1492", string_arg2 = "I'm a tricky one" 20 | [string_arg1, string_arg2] 21 | end 22 | 23 | command "I'm another function!" 24 | def class_baz number_arg1, string_arg2 = "blorp", *array_arg3 25 | [number_arg1, string_arg2, array_arg3] 26 | end 27 | 28 | end 29 | 30 | end -------------------------------------------------------------------------------- /spec/source_code_examples/command_no_command.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class CommandNoCommand 4 | extend Commandable 5 | 6 | command 'a method with a command' 7 | def command_method1 8 | "command_method1" 9 | end 10 | 11 | # this method should not be in the commands list 12 | def no_command_method1(flappity) 13 | "no_command_method1" 14 | end 15 | 16 | command 'another method with a command' 17 | def command_method2(some_parameter) 18 | "command_method2" 19 | end 20 | 21 | # this method shouldn't be in the list either 22 | def no_command_method2(flippity) 23 | "no_command_method2" 24 | end 25 | 26 | end -------------------------------------------------------------------------------- /spec/source_code_examples/deep_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | module TopModule 4 | class ParentClass 5 | class DeepClass 6 | extend Commandable 7 | command 'this is a deep method call' 8 | def deep_method 9 | "you called a deep method" 10 | end 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /spec/source_code_examples/default_method.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class DefaultMethods 4 | extend Commandable 5 | 6 | command 'the default method', :default 7 | def default_method(name) 8 | "default method called with: #{name}" 9 | end 10 | 11 | command 'does other stuff' 12 | def not_a_default_method(name="Cleveland", age) 13 | "not a default method, called with: #{name}, #{age}" 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_examples/default_method_multiparameters.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class DefaultMethodsMultiParameters 4 | extend Commandable 5 | 6 | command 'the default method', :default 7 | def default_method_multi(name, optional="optional") 8 | "multi default method called with: #{name}, #{optional}" 9 | end 10 | 11 | command 'does other stuff' 12 | def not_a_default_method_multi(name="Cleveland", age) 13 | ["multi not a default method, called with: #{name}", age] 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_examples/default_method_no_params.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class DefaultMethodNoParams 4 | extend Commandable 5 | 6 | command 'does some stuff', :default 7 | def default_no_params 8 | "default method called, has no params" 9 | end 10 | 11 | command 'does other stuff' 12 | def not_a_default_method(name="Cleveland", age) 13 | ["not a default method: #{name}", age] 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_examples/instance_methods.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class InstanceMethods 4 | extend Commandable 5 | 6 | command "save a value", :priority=>2 7 | def set_value(value) 8 | @value = value 9 | end 10 | 11 | command "retreive a value" 12 | def get_value 13 | @value 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_examples/instance_methods2.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class InstanceMethods2 4 | extend Commandable 5 | 6 | command "save a value", :priority=>2 7 | def set_value2(value) 8 | @value = value 9 | end 10 | 11 | command "retreive a value" 12 | def get_value2 13 | @value 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_examples/kernel_exit_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class KernelExitClass 4 | 5 | extend Commandable 6 | command 'does some stuff' 7 | def exit(exit_code = 0) 8 | Kernel.exit exit_code.to_i 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/source_code_examples/multi_line_description.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class MultiLineDescription 4 | extend Commandable 5 | 6 | command "this description will be on multiple lines\nso you can have really long descriptions\nAnd another line\nAnd another" 7 | def multiline_function1 8 | "multiline_function1" 9 | end 10 | 11 | command "Line one is short line two blah, blah! Oh, and blah!" 12 | def multiline_function2(foo) 13 | "multiline_function2: #{foo}" 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/source_code_examples/multi_line_description_no_params.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class MultiLineDescriptionNoParams 4 | extend Commandable 5 | 6 | command "this description will be on multiple lines\nso you can have really long descriptions\nAnd another line\nAnd another" 7 | def multiline_function_no_params1 8 | "multiline_function_no_params1" 9 | end 10 | 11 | command "Line one is short line two blah, blah! Oh, and blah!" 12 | def multiline_function_no_params2 13 | "multiline_function_no_params2" 14 | end 15 | 16 | end 17 | -------------------------------------------------------------------------------- /spec/source_code_examples/no_description.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class NoDescription 4 | extend Commandable 5 | command 6 | def method_with_no_description 7 | "called method_with_no_description" 8 | end 9 | end -------------------------------------------------------------------------------- /spec/source_code_examples/parameter_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ParameterClass 4 | extend Commandable 5 | 6 | def non_command_method 7 | puts "this shouldnt matter" 8 | end 9 | 10 | command "hello world" 11 | def foo(int_arg1, number_arg2) 12 | [int_arg1, number_arg2] 13 | end 14 | 15 | command "look a function" 16 | def bar(int_arg1, string_arg2="Number 42") 17 | [int_arg1, string_arg2] 18 | end 19 | 20 | command "run me for stuff to happen" 21 | def qux string_arg1 ="1492", string_arg2 = "I'm a tricky one" 22 | [string_arg1, string_arg2] 23 | end 24 | 25 | command "I'm another function!" 26 | def baz number_arg1, string_arg2 = "blorp", *array_arg3 27 | [number_arg1, string_arg2, array_arg3] 28 | end 29 | 30 | command "a method with a required parameter" 31 | def zaz(required_param, another_required) 32 | [required_param, another_required] 33 | end 34 | 35 | end 36 | -------------------------------------------------------------------------------- /spec/source_code_examples/parameter_free.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ParameterFree 4 | extend Commandable 5 | 6 | command 'this method has no params' 7 | def no_parms1 8 | "no_parms1" 9 | end 10 | 11 | command 'none here either' 12 | def no_parms2 13 | "no_parms2" 14 | end 15 | 16 | command 'nope, still none' 17 | def no_parms3 18 | "no_parms3" 19 | end 20 | 21 | end -------------------------------------------------------------------------------- /spec/source_code_examples/required_default.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class RequiredDefault 4 | extend Commandable 5 | 6 | command "the default action", :default, :required 7 | def required_default(required_value) 8 | value 9 | end 10 | 11 | end -------------------------------------------------------------------------------- /spec/source_code_examples/required_methods.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class RequiredMethods 4 | 5 | extend Commandable 6 | 7 | command :required, 'does some stuff' 8 | def required_method(gimmie) 9 | gimmie 10 | end 11 | 12 | command "another one" 13 | def non_required_method(meh) 14 | meh 15 | end 16 | 17 | end -------------------------------------------------------------------------------- /spec/source_code_examples/run_first.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | Commandable.color_output = true 4 | Commandable.app_exe = "bug_fix" 5 | Commandable.app_info = 6 | """ 7 | Testing - Testing Commandable 8 | Copyleft (c) 2012 Mike Bethany 9 | http://mikeb.tk 10 | """ 11 | 12 | class RunFirst 13 | extend Commandable 14 | 15 | def non_command_method 16 | puts "non_command_method" 17 | end 18 | 19 | command "default method", :default 20 | def command_method 21 | puts "#command_method" 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /spec/source_code_examples/run_first_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | Commandable.color_output = true 4 | Commandable.app_exe = "bug_fix" 5 | Commandable.app_info = 6 | """ 7 | Testing - Testing Commandable 8 | Copyleft (c) 2012 Mike Bethany 9 | http://mikeb.tk 10 | """ 11 | 12 | class RunFirstClass 13 | extend Commandable 14 | 15 | def self.non_command_method 16 | puts "class non_command_method" 17 | end 18 | 19 | command "default method", :default 20 | def command_method 21 | puts "#command_method" 22 | end 23 | 24 | end -------------------------------------------------------------------------------- /spec/source_code_examples/super_deep_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | module TopModule 4 | class ParentClass 5 | module WhatWhat 6 | class OhNoYouDian 7 | module OhYesIDid 8 | module TwoSnapsUpIn 9 | class ACircle 10 | module Exclamation 11 | module OKEvenImBoredNow 12 | class DeepDeepClass 13 | extend Commandable 14 | command 'this is a really deep method call' 15 | def super_deep_method 16 | "you called a deep method" 17 | end 18 | end 19 | end 20 | end 21 | end 22 | end 23 | end 24 | end 25 | end 26 | end 27 | end -------------------------------------------------------------------------------- /spec/source_code_examples/test_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class TestClass 4 | extend Commandable 5 | command 'does some stuff' 6 | def test_method 7 | "test_method" 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /spec/source_code_examples/xor_class.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class XorClass 4 | extend Commandable 5 | 6 | command 'a normal method' 7 | def normal_method 8 | "normal_method" 9 | end 10 | 11 | command 'another normal method' 12 | def normal_method2 13 | "normal_method2" 14 | end 15 | 16 | command 'part of the default :xor group', :xor, :default 17 | def xor_method1 18 | "xor_method1" 19 | end 20 | 21 | command "also in the default :xor group", :xor 22 | def xor_method2 23 | "xor_method2" 24 | end 25 | 26 | command 'you can also make your own groups', :xor => :xor_group 27 | def xor_method3 28 | "xor_method3" 29 | end 30 | 31 | command "use wantever you want as group names", :xor => :xor_group 32 | def xor_method4 33 | "xor_method4" 34 | end 35 | 36 | end -------------------------------------------------------------------------------- /spec/source_code_for_errors/class_bad.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class ClassBad 4 | extend Commandable 5 | command 'this should fail because their is no method' 6 | end -------------------------------------------------------------------------------- /spec/source_code_for_errors/default_method_bad.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | class DefaultMethodsBad 4 | extend Commandable 5 | 6 | command 'does some stuff', :default 7 | def default_method 8 | "default_method" 9 | end 10 | 11 | command 'does some other stuff', :default 12 | def default_method2 13 | "default_method2" 14 | end 15 | 16 | end -------------------------------------------------------------------------------- /spec/source_code_for_errors/private_methods_bad.rb: -------------------------------------------------------------------------------- 1 | require "commandable" 2 | 3 | # This is just a not so clever way of getting at the instance methods of Commandable. 4 | # Accessing the private methods of a class/module is a bad idea but I really need to 5 | # test them. Plus making a helper module just to test them is also against best practices 6 | # so... 7 | class PrivateMethodsBad 8 | extend Commandable 9 | end -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | $:.unshift File.expand_path((File.dirname(__FILE__) + '/../lib')) 2 | $:.unshift File.expand_path((File.dirname(__FILE__) + '/source_code_examples')) 3 | $:.unshift File.expand_path((File.dirname(__FILE__) + '/source_code_for_errors')) 4 | $:.unshift File.expand_path(File.dirname(__FILE__)) 5 | 6 | require 'pp' 7 | require 'term/ansicolor' 8 | require 'rspec/mocks' 9 | require 'rspec' 10 | require 'commandable' 11 | #require 'commandable/app_controller' 12 | 13 | # A note on the specs: 14 | # Since Commandable uses singletons the tests sometimes get confused about their state. 15 | # I'm working on a way to properly clear everything out of memory before each test but tests 16 | # doesn't pass in autotest try running the test again or run rspec yourself and they'll pass. 17 | 18 | # Debug print 19 | module Kernel 20 | def dp(value) 21 | puts "" 22 | puts "*" * 40 23 | puts "value: #{value}" 24 | puts "&" * 40 25 | puts "" 26 | end 27 | def dpi(value) 28 | puts "" 29 | puts "*" * 40 30 | pp value 31 | puts "&" * 40 32 | puts "" 33 | end 34 | end 35 | 36 | # Trap STDOUT and STDERR for testing what a method prints to the terminal 37 | def capture_output 38 | begin 39 | require 'stringio' 40 | $o_stdout, $o_stderr = $stdout, $stderr 41 | $stdout, $stderr = StringIO.new, StringIO.new 42 | yield 43 | {:stdout => $stdout.string, :stderr => $stderr.string} 44 | ensure 45 | $stdout, $stderr = $o_stdout, $o_stderr 46 | end 47 | end 48 | 49 | # Executes a command capturing STDOUT and STDERR as an array representing each line 50 | # Traps errors so not to be used for testing lambad{}.should_not raise_error 51 | # or should raise_error since you won't get the error 52 | def execute_output_ary(argv, silent=false) 53 | execute_output_s(argv, silent).split(%r{\n}) 54 | end 55 | 56 | # Executes a command capturing STDOUT and STDERR as one string 57 | # Traps errors so not to be used for testing lambad{}.should_not raise_error 58 | # or should raise_error since you won't get the error 59 | def execute_output_s(argv, silent=false) 60 | output = capture_output{Commandable.execute(argv, silent)} 61 | output[:stdout] + output[:stderr] 62 | end 63 | 64 | # Exectues a command queue returning the results 65 | # Use when you want to make sure you don't raise an error 66 | def execute_queue(argv) 67 | queue = Commandable.execution_queue(argv) 68 | queue.collect{|meth| meth[:proc].call} 69 | end --------------------------------------------------------------------------------