# cmds cmds tries to make it easier to read, write and remember using shell commands in Ruby. it treats generating shell the in a similar fashion to generating SQL or HTML. ## status eh, it's kinda starting to work... i'll be using it for stuff and seeing how it goes, but no promises until `1.0` of course. ## installation Add this line to your application's Gemfile: ``` gem 'cmds' ``` And then execute: ``` $ bundle ``` Or install it yourself as: ``` $ gem install cmds ``` ## real-world examples instead of ``` `psql -U #{ db_config['username'] || ENV['USER'] } #{ db_config['database']} < #{ filepath.shellescape }` ``` write ``` Cmds 'psql %{opts} %{db} < %{dump}', db: db_config['database'], dump: filepath, opts: { username: db_config['username'] || ENV['USER'] } ``` instead of ``` `aws s3 sync s3://#{ PROD_APP_NAME } #{ s3_path.shellescape }` ``` write ``` Cmds 'aws s3 sync %{uri} %{path}', uri: "s3://#{ PROD_APP_NAME }" path: s3_path ``` instead of ``` `PGPASSWORD=#{ config[:password].shellescape } pg_dump -U #{ config[:username].shellescape } -h #{ config[:host].shellescape } -p #{ config[:port] } #{ config[:database].shellescape } > #{ filepath.shellescape }` ``` write ``` Cmds 'PGPASSWORD=%{password} pg_dump %{opts} %{database} > %{filepath}', password: config[:password], database: config[:database], filepath: filepath, opts: { username: config[:username], host: config[:host], port: config[:port], } ``` ## substitutions substitutions can be positional, keyword, or both. ### positional positional arguments can be substituted in order using the `arg` method call: ``` Cmds.sub "psql <%= arg %> <%= arg %> < <%= arg %>", [ { username: "bingo bob", host: "localhost", port: 12345, }, "blah", "/where ever/it/is.psql", ] # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql' ``` internally this translates to calling `@args.fetch(@arg_index)` and increments `@arg_index` by 1. this will raise an error if it's called after using the last positional argument, but will not complain if all positional arguments are not used. this prevents using a keyword arguments named `arg` without accessing the keywords hash directly. the arguments may also be accessed directly though the bound class's `@args` instance variable: ``` Cmds.sub "psql <%= @args[2] %> <%= @args[0] %> < <%= @args[1] %>", [ "blah", "/where ever/it/is.psql", { username: "bingo bob", host: "localhost", port: 12345, }, ] # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql' ``` note that `@args` is a standard Ruby array and will simply return `nil` if there is no value at that index (though you can use `args.fetch(i)` to get the same behavior as the `arg` method with a specific index `i`). ### keyword keyword arguments can be accessed by making a method call with their key: ``` Cmds.sub "psql <%= opts %> <%= database %> < <%= filepath %>", [], database: "blah", filepath: "/where ever/it/is.psql", opts: { username: "bingo bob", host: "localhost", port: 12345, } # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql' ``` this translates to a call of `@kwds.fetch(key)`, which will raise an error if `key` isn't present. there are four key names that may not be accessed this way due to method definition on the context object: * `arg` (see above) * `initialize` * `get_binding` * `method_missing` though keys with those names may be accessed directly via `@kwds.fetch(key)` and the like. to test for a key's presence or optionally include a value, append `?` to the method name: ``` c = Cmds.new <<-BLOCK defaults <% if current_host? %> -currentHost <%= current_host %> <% end %> export <%= domain %> <%= filepath %> BLOCK c.call domain: 'com.nrser.blah', filepath: '/tmp/export.plist' # defaults export com.nrser.blah /tmp/export.plist c.call current_host: 'xyz', domain: 'com.nrser.blah', filepath: '/tmp/export.plist' # defaults -currentHost xyz export com.nrser.blah /tmp/export.plist ``` ### both both positional and keyword substitutions may be provided: ``` Cmds.sub "psql <%= opts %> <%= arg %> < <%= filepath %>", ["blah"], filepath: "/where ever/it/is.psql", opts: { username: "bingo bob", host: "localhost", port: 12345, } # => 'psql --host=localhost --port=12345 --username=bingo\ bob blah < /where\ ever/it/is.psql' ``` this might be useful if you have a simple positional command like ``` Cmds "blah <%= arg %>", ["value"] ``` and you want to quickly add in some optional value ``` Cmds "blah <%= maybe? %> <%= arg %>", ["value"] Cmds "blah <%= maybe? %> <%= arg %>", ["value"], maybe: "yes!" ``` ### shortcuts there are support for `sprintf`-style shortcuts. **positional** `%s` is replaced with `<%= arg %>`. so ``` Cmds.sub "./test/echo_cmd.rb %s", ["hello world!"] ``` is the same as ``` Cmds "./test/echo_cmd.rb <%= arg %>", ["hello world!"] ``` **keyword** `%{key}` and `%s` are replaced with `<%= key %>`, and `%{key?}` and `%s` are replaced with `<%= key? %>` for optional keywords. so ``` Cmds "./test/echo_cmd.rb %{key}", key: "hello world!" ``` and ``` Cmds "./test/echo_cmd.rb %s", key: "hello world!" ``` are the same is ``` Cmds "./test/echo_cmd.rb <%= key %>", key: "hello world!" ``` **escaping** strings that would be replaced as shortcuts can be escaped by adding one more `%` to the front of them: ``` Cmds.sub "%%s" # => "%s" Cmds.sub "%%%s" # => "%%s" ``` note that unlike `sprintf`, which has a much more general syntax, this is only necessary for patterns that exactly match a shortcut, not `%` in general: ``` Cmds.sub "50%" # => "50%" ``` ## reuse commands ``` playbook = Cmds.new "ansible-playbook -i %{inventory} %{playbook}" playbook.call inventory: "./hosts", playbook: "blah.yml" ``` currying ``` dev_playbook = playbook.curry inventory: "inventory/dev" prod_playbook = playbook.curry inventory: "inventory/prod" # run setup.yml on the development hosts dev_playbook.call playbook: "setup.yml" # run setup.yml on the production hosts prod_playbook.call playbook: "setup.yml" ``` ## defaults NEEDS TEST can be accomplished with reuse and currying stuff ``` playbook = Cmds.new "ansible-playbook -i %{inventory} %{playbook}", inventory: "inventory/dev" # run setup.yml on the development hosts playbook.call playbook: "setup.yml" # run setup.yml on the production hosts prod_playbook.call playbook: "setup.yml", inventory: "inventory/prod" ``` ## future..? ### formatters kinda like `sprintf` formatters or string escape helpers in Rails, they would be exposed as functions in ERB and as format characters in the shorthand versions: ``` Cmds "blah <%= j obj %>", obj: {x: 1} # => blah \{\"x\":1\} Cmds "blah %j", [{x: 1}] # => blah \{\"x\":1\} Cmds "blah %j", obj: {x: 1} # => blah \{\"x\":1\} ``` the `s` formatter would just format as an escaped string (no different from `<%= %>`). other formatters could include * `j` for JSON (as shown above) * `r` for raw (unescaped) * `l` or `,` for comma-separated list (which some commands like as input) * `y` for YAML * `p` for path, joining with `File.join`