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.
Best read at
where the API doc links should work and you got a table and contents.
-----------------------------------------------------------------------------
Status
-----------------------------------------------------------------------------
Ya know, before you get too excited...
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.
-----------------------------------------------------------------------------
License
-----------------------------------------------------------------------------
MIT
-----------------------------------------------------------------------------
Real-World Examples
-----------------------------------------------------------------------------
Or, "what's it look like?"...
- Instead of
```Ruby
`psql \
--username=#{ (db_config['username'] || ENV['USER']).shellescape } \
#{ db_config['database'].shellescape } \
< #{ filepath.shellescape }`
```
write
```Ruby
Cmds 'psql %{opts} %{db} < %{dump}',
db: db_config['database'],
dump: filepath,
opts: {
username: db_config['username'] || ENV['USER']
}
```
to run a command like
```bash
psql --username=nrser that_db < ./some/file/path
```
Cmds takes care of shell escaping for you.
- Instead of
```Ruby
`PGPASSWORD=#{ config[:password].shellescape } \
pg_dump \
--username=#{ config[:username].shellescape } \
--host=#{ config[:host].shellescape } \
--port=#{ config[:port].shellescape } \
#{ config[:database].shellescape } \
> #{ filepath.shellescape }`
```
which can be really hard to pick out what's going on from a quick glance, write
```Ruby
Cmds.new(
'pg_dump %{opts} %{database}',
kwds: {
opts: {
username: config[:username],
host: config[:host],
port: config[:port],
},
database: config[:database],
},
env: {
PGPASSWORD: config[:password],
},
).stream! { |io| io.out = filename }
```
I find it much easier to see what's going on their quickly.
Again, with some additional comments and examples:
```Ruby
# We're going to instantiate a new {Cmds} object this time, because we're
# not just providing values for the string template, we're specifying an
# environment variable for the child process too.
#
cmd = Cmds.new(
# The string template to use.
'pg_dump %{opts} %{database}',
kwds: {
# Hashes will automatically be expanded to CLI options. By default,
# we use `--name=VALUE` format for long ones and `-n VALUE` for short,
# but it's configurable.
opts: {
username: config[:username],
host: config[:host],
port: config[:port],
},
# As mentioned above, everything is shell escaped automatically
database: config[:database],
},
# Pass environment as it's own Hash. There are options for how it is
# provided to the child process as well.
env: {
# Symbol keys are fine, we'll take care of that for you
PGPASSWORD: config[:password],
},
)
# Take a look!
cmd.prepare
# => "PGPASSWORD=shhh\\! pg_dump --host=localhost --port=5432 --username=nrser blah"
# Now stream it. the `!` means raise if the exit code is not 0
exit_code = cmd.stream! { |io|
# We use the block to configure I/O. Here we send the standard output to
# a file, which can be a String, Pathname or IO object
io.out = filename
}
```
-----------------------------------------------------------------------------
Installation
-----------------------------------------------------------------------------
Add this line to your application's `Gemfile`:
gem 'cmds'
And then execute:
bundle install
Or install it globally with:
gem install cmds
-----------------------------------------------------------------------------
Overview
-----------------------------------------------------------------------------
Cmds is based around a central {Cmds} class that takes a template for the command and a few options and operates by either wrapping the results in a {Cmds::Result} instance or streaming the results to `IO` objects or handler blocks.
-----------------------------------------------------------------------------
Features
-----------------------------------------------------------------------------
### Templates ###
#### ERB ####
Templates are processed with "[Embedded Ruby][]" (eRuby/ERB) using the [Erubis][] gem.
[Embedded Ruby]: https://en.wikipedia.org/wiki/ERuby
[Erubis]: http://www.kuwata-lab.com/erubis/
For how it works check out
1. {Cmds::ERBContext}
2. {Cmds::ShellERuby}
3. {Cmds#render}
******************************************************************************
##### Positional Values from `args` #####
1. Use the `args` array made available in the templates with entry indexes.
Example when constructing:
```ruby
Cmds.new(
'cp <%= args[0] %> <%= args[1] %>',
args: [
'source.txt',
'dest.txt',
],
).prepare
# => "cp source.txt dest.txt"
```
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.
2. Use the `arg` method made available in the templates to get the next positional arg.
Example when using "sugar" methods that take `args` as the single-splat (`*args`):
```ruby
Cmds.prepare 'cp <%= arg %> <%= arg %>',
'source.txt',
'dest.txt'
# => "cp source.txt dest.txt"
```
******************************************************************************
##### Keyword Values from `kwds` #####
Just use the key as the method name.
When constructing:
```ruby
Cmds.new(
'cp <%= src %> <%= dest %>',
kwds: {
src: 'source.txt',
dest: 'dest.txt',
},
).prepare
# => "cp source.txt dest.txt"
```
When using "sugar" methods that take `kwds` as the double-splat (`**kwds`):
```ruby
Cmds.prepare 'cp <%= src %> <%= dest %>',
src: 'source.txt',
dest: 'dest.txt'
# => "cp source.txt dest.txt"
```
###### Key Names to Avoid ######
If possible, avoid naming your keys:
- `arg`
- `args`
- `initialize`
- `get_binding`
- `method_missing`
If you must name them those things, don't expect to be able to access them as shown above; use `<%= @kwds[key] %>`.
###### Keys That Might Not Be There ######
Normally, if you try to interpolate a key that doesn't exist you will get a `KeyError`:
```ruby
Cmds.prepare "blah <%= maybe %> <%= arg %>", "value"
# KeyError: couldn't find keys :maybe or "maybe" in keywords {}
```
I like a lot this better than just silently omitting the value, but sometimes you know that they key might not be set and want to receive `nil` if it's not.
In this case, append `?` to the key name (which is a method call in this case) and you will get `nil` if it's not set:
```ruby
Cmds.prepare "blah <%= maybe? %> <%= arg %>", "value"
# => "blah value"
```
```ruby
Cmds.prepare "blah <%= maybe? %> <%= arg %>", "value", maybe: "yes"
# => "blah yes value"
```
******************************************************************************
##### Shell Escaping #####
Cmds automatically shell-escapes values it interpolates into templates by passing them through the Ruby standard libray's [Shellwords.escape][].
[Shellwords.escape]: http://ruby-doc.org/stdlib/libdoc/shellwords/rdoc/Shellwords.html#method-c-escape
```ruby
Cmds.prepare "cp <%= src %> <%= dest %>",
src: "source.txt",
dest: "path with spaces.txt"
=> "cp source.txt path\\ with\\ spaces.txt"
```
It doesn't always do the prettiest job, but it's part of the standard library and seems to work pretty well... shell escaping is a messy and complicated topic (escaping for *which* shell?!), so going with the built-in solution seems reasonable for the moment, though I do hate all those backslashes... they're a pain to read.
###### Raw Interpolation ######
You can render a raw string with `<%== %>`.
To see the difference with regard to the previous example (which would break the `cp` command in question):
```ruby
Cmds.prepare "cp <%= src %> <%== dest %>",
src: "source.txt",
dest: "path with spaces.txt"
=> "cp source.txt path with spaces.txt"
```
And a way it make a little more sense:
```ruby
Cmds.prepare "<%== bin %> <%= *args %>",
'blah',
'boo!',
bin: '/usr/bin/env echo'
=> "/usr/bin/env echo blah boo\\!"
```
******************************************************************************
##### Splatting (`*`) To Render Multiple Shell Tokens #####
Render multiple shell tokens (individual strings the shell picks up - basically, each one is an entry in `ARGV` for the child process) in one expression tag by prefixing the value with `*`:
```ruby
Cmds.prepare '<%= *exe %> <%= cmd %> <%= opts %> <%= *args %>',
'x', 'y', # <= these are the `args`
exe: ['/usr/bin/env', 'blah'],
cmd: 'do-stuff',
opts: {
really: true,
'some-setting': 'dat-value',
}
# => "/usr/bin/env blah do-stuff --really --some-setting=dat-value x y"
```
`ARGV` tokenization by the shell would look like:
```ruby
[
'/usr/bin/env',
'blah',
'do-stuff',
'--really',
'--some-setting=dat-value',
'x',
'y',
]
```
- Compare to *without* splats:
```ruby
Cmds.prepare '<%= exe %> <%= cmd %> <%= opts %> <%= args %>',
'x', 'y', # <= these are the `args`
exe: ['/usr/bin/env', 'blah'],
cmd: 'do-stuff',
opts: {
really: true,
'some-setting': 'dat-value',
}
# => "/usr/bin/env,blah do-stuff --really --some-setting=dat-value x,y"
```
Which is probably *not* what you were going for... it would produce an `ARGV` something like:
```ruby
[
'/usr/bin/env,blah',
'do-stuff',
'--really',
'--some-setting=dat-value',
'x,y',
]
```
You can of course use "splatting" together with slicing or mapping or whatever.
******************************************************************************
##### Logic #####
All of ERB is available to you. I've tried to put in features and options that make it largely unnecessary, but if you've got a weird or complicated case, or if you just like the HTML/Rails-esque templating style, it's there for you:
```ruby
cmd = Cmds.new <<-END
<% if use_bin_env %>
/usr/bin/env
<% end %>
docker build .
-t <%= tag %>
<% if file %>
--file <%= file %>
<% end %>
<% build_args.each do |key, value| %>
--build-arg <%= key %>=<%= value %>
<% end %>
<% if yarn_cache %>
--build-arg yarn_cache_file=<%= yarn_cache_file %>
<% end %>
END
cmd.prepare(
use_bin_env: true,
tag: 'nrser/blah:latest',
file: './prod.Dockerfile',
build_args: {
yarn_version: '1.3.2',
},
yarn_cache: true,
yarn_cache_file: './yarn-cache.tgz',
)
# => "/usr/bin/env docker build . -t nrser/blah:latest
# --file ./prod.Dockerfile --build-arg yarn_version=1.3.2
# --build-arg yarn_cache_file=./yarn-cache.tgz"
# (Line-breaks added for readability; output is one line)
```
******************************************************************************
#### `printf`-Style Short-Hand (`%s`, `%{key}`, `%s`)
Cmds also supports a [printf][]-style short-hand. Sort-of.
[printf]: https://en.wikipedia.org/wiki/Printf_format_string
It's a clumsy hack from when I was first writing this library, and I've pretty moved to using the ERB-style, but there are still some examples that use it, and I guess it still works (to whatever extent it ever really did), so it's probably good to mention it.
It pretty much just replaces some special patterns with their ERB-equivalent via the {Cmds.replace_shortcuts} method before moving on to ERB processing:
1. `%s` => `<%= arg %>`
2. `%{key}` => `<%= key %>`
3. `%{key?}` => `<%= key? %>`
4. `%s` => `<%= key %>`
5. `%s` => `<%= key? %>`
And the escaping versions, where you can put anothe `%` in front to get the literal intead of the subsitution:
1. `%%s` => `%s`
2. `%%{key}` => `%{key}`
3. `%%{key?}` => `%{key?}`
4. `%%s` => `%s`
5. `%%s` => `%s`
That's it. No `printf` formatting beyond besides `s` (string).
-----------------------------------------------------------------------------
Old docs I haven't cleaned up yet...
-----------------------------------------------------------------------------
### execution
you can provide three types of arguments when executing a command:
1. positional arguments for substitution
2. keyword arguments for substitution
3. input to stdin
all `Cmds` instance execution methods have the same form for accepting these:
1. positional arguments are provided in an optional array that must be the first argument:
`Cmds "cp <%= arg %> <%= arg %>", [src_path, dest_path]`
note that the arguments need to be enclosed in square braces. Cmds does **NOT** use \*splat for positional arguments because it would make a `Hash` final parameter ambiguous.
2. keyword arguments are provided as optional hash that must be the last argument:
`Cmds "cp <%= src %> <%= dest %>", src: src_path, dest: dest_path`
in this case, curly braces are not required since Ruby turns the trailing keywords into a `Hash` provided as the last argument (or second-to-last argument in the case of a block included in the method signature).
3. input and output is handled with blocks:
`Cmds(“wc -l”){ “one\ntwo\nthree\n” }
Cmds.stream './test/tick.rb <%= times %>', times: times do |io|
io.on_out do |line|
# do something with the output line
end
io.on_err do |line|
# do something with the error line
end
end`
### 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"
```
### input
```
c = Cmds.new("wc", input: "blah blah blah).call
```
### future..?
#### exec
want to be able to use to exec commands
#### 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`