Yesterday at the excellent Devopsdays in Gent, Belgium, I proposed an open session to flesh out an idea I had a few weeks ago - to use Cucumber as a general scripting language.

Cucumber's Given/When/Then steps are well suited to procedural tasks like shell script, and you would be writing your "scripts" in straightforward language that non-technical users such as managers and clients could understand. Also, as writing a scenario without a Then to close it feels unbalanced, you'd get in the mindset of testing the actions of your "scripts" fairly quickly.

With little more than the hypothesis above, a group of us found a room and started modeling some scenarios. Our focus was on file manipulation, as it was a low hanging fruit and something most scripts do.

We came up with this:

Feature: Copy files around

  Scenario: A single file
    Given I am in "/tmp"
    And the file "spoons" exists
    When I copy the file "spoons" to "forks"
    Then the file "forks" should exist
    And the file "forks" should be readable

  Scenario: Multiple files
    Given I am in "/tmp"
    Given the following table of tasty fruit:
      | filename |
      | apples   |
      | oranges  |
      | bananas  |
      | ananas   |
      | file with lots o spaces |
      | spoons of : doom |
    When I create the directory "/tmp/some_other_dir"
    When I copy the tasty fruit in the table to "/tmp/some_other_dir"
    Then the tasty fruit in the table should exist in "/tmp/some_other_dir"

The first scenario is fairly self explanatory, but the second one is where the interesting stuff starts happening.

In the implementation of the "following table" step, we create an instance variable that persists the list of files between steps. This way, we can reference the "tasty fruit" throughout our other steps:

Given /^the following table of (.+):$/ do |name, table|
  @tables = {}
  @tables[name] = table.hashes

We use the (.+) regex to capture the name of the table so we can poke at it later on. This design lets you easily use multiple tables throughout your steps that won't conflict with one another:

  Scenario: Multiple files from multiple tables
    Given the following table of tasty fruit:
      | filename |
      | apples   |
      | oranges  |
    And the following table of baggy baggage:
      | filename |
      | suitcase |
      | backpack |
    When I copy the baggy baggage in the table to "/tmp/some_other_dir"
    And I copy the tasty fruit in the table to "/tmp/some_other_dir"
    Then the tasty fruit in the table should exist in "/tmp/some_other_dir"
    And the baggy baggage in the table should exist in "/tmp/some_other_dir"

Other steps can reference data in the table by accepting a name and looking it up in the hash of tables:

Then /^the (.+) in the table should exist in "([^\"]*)"$/ do |name, destination|
  @tables[name].each do |file|
    File.exists?(File.join(destination, file["filename"])).should be_true

We also looked at handling permission problems:

  Scenario: Do things i'm not allowed to
    When I create the directory "/usr/bin/wtf"

Here the step will raise an Errno::EACCES exception, and as Cucumber uses a pretty formatter by default, the failed step will appear in red.

Finally we tried copying files with a glob. The initial implementation I banged out was very Unix focused (it used *, which is a very explicit globbing syntax), so we scrapped that idea and wrote our intentions in plain English:

  Scenario: Copy based on a pattern
    Given I am in "/tmp"
    When I create the directory "/tmp/pattern_dir"
    And I copy files beginning with the letters z,y,x to "/tmp/pattern_dir"
    Then they should exist there

The implementation is obvious, and is very understandable (and seemingly powerful) to someone with no knowledge of globbing.

People who have used Cucumber in web development will likely note that the above implementation is an example of tightly coupled steps, which is sometimes regarded as an anti-pattern. I'm of the opinion that this is a lot more painful in a web development context than in a procedural/scripting tool one.

From my recollection of Euruko earlier this year, when Aslak was asked whether he considers it an antipattern, he said it can be ok to use depending on the problem you're trying to solve, so I take that as tacit permission that it is ok this context. :-)

I posted the results of the session to a Gist yesterday, and I have also published a repo with a bundler-ready install process, so people can hack on it more.

After the session I remembered that the feature file doesn't actually have to start with Feature, so it's possible to write standalone scenarios one after another.

When wrapping up, someone in the room pointed out that our implementation actually went one better than being readable by non-technical users - they could probably write the scripts themselves.

This is pretty powerful, and coupled with Cucumber's very cool step generation when running scenarios with undefined steps, makes it very easy to start prototyping a standard library of human readable scripting commands.

There was chatter on the Cucumber mailing list a few weeks ago about providing alternate interfaces for writing and executing Cucumber features, and it could be cool to see a drag-and-drop interface with a library of common tasks that calls out to Cucumber to execute them. You could even build something quite beautiful with HotCocoa.

Anyhow, if you think anything mentioned above is a cool idea, check out the code and start hacking!