diff --git a/README.md b/README.md index e69de29..3e6aff0 100644 --- a/README.md +++ b/README.md @@ -0,0 +1,12 @@ +python-ruby-golang +---------- + +A comparison of click (Python), thor (Ruby), and cli.go (Golang) for building a very simple command-line tool. + +## QuickStart + +See the README.md in each of the subdirectories for more information. + +## Blog Post + +[Coming Soon!]() diff --git a/golang/README.md b/golang/README.md new file mode 100644 index 0000000..faff740 --- /dev/null +++ b/golang/README.md @@ -0,0 +1,21 @@ +gomr: go [m]utli-[r]epository manager +----- + +gomr is a golang command-line tool used to execute arbitrary commands on a set of tagged directories. + +## Features + +* Configuration is stored in the registered directory (allows for version-controlled configuration) +* Any arbitrary command can be run, no ties to version-control specific commands. + + +## Installation + +I'm not distributing binaries yet (TODO) for now checkout and `go install` to build the binary. + +## Command Help + +For help run: +* `gomr --help` +* `gomr register --help` +* `gomr run --help` diff --git a/golang/gomr.go b/golang/gomr.go new file mode 100644 index 0000000..003f91f --- /dev/null +++ b/golang/gomr.go @@ -0,0 +1,60 @@ +package main + +import ( + "github.com/codegangsta/cli" + "os" +) + +func main() { + app := cli.NewApp() + app.Name = "gomr" + app.Author = "Kyle W. Purdon" + app.Version = "0.0.1" + app.Usage = "multi-repository manager in go" + app.Commands = []cli.Command{ + { + Name: "register", + Usage: "register a directory", + Action: register, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "directory, d", + Value: "./", + Usage: "directory to tag", + }, + cli.StringFlag{ + Name: "tag, t", + Value: "default", + Usage: "tag to add for directory", + }, + cli.BoolFlag{ + Name: "append", + Usage: "append the tag to an existing registered directory", + }, + }, + }, + { + Name: "run", + Usage: "run a command", + Action: run, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "basepath, b", + Value: "./", + Usage: "path to begin the recursive search", + }, + cli.StringFlag{ + Name: "tag, t", + Value: "default", + Usage: "tag to add for directory", + }, + cli.BoolFlag{ + Name: "dryrun, d", + Usage: "print (dont execute) the commands that will be run", + }, + }, + }, + } + + app.Run(os.Args) +} diff --git a/golang/register.go b/golang/register.go new file mode 100644 index 0000000..221f305 --- /dev/null +++ b/golang/register.go @@ -0,0 +1,50 @@ +package main + +import ( + "fmt" + "github.com/codegangsta/cli" + "github.com/robfig/config" + "os" + "path" + "strings" +) + +func AppendIfMissing(slice []string, i string) []string { + for _, ele := range slice { + if ele == i { + return slice + } + } + return append(slice, i) +} + +func register(ctx *cli.Context) { + + fn := path.Join(ctx.String("directory"), ".gomr") + + newTags := strings.Split(ctx.String("tag"), ",") + + if ctx.Bool("append") { + if _, err := os.Stat(fn); err == nil { + cfg, _ := config.ReadDefault(".gomr") + curTags, _ := cfg.String("gomr", "tags") + + for _, tag := range strings.Split(curTags, ",") { + newTags = AppendIfMissing(newTags, tag) + } + } else { + err := "append used, existing file not found." + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } + } + + outTags := strings.Join(newTags, ",") + + outCfg := config.NewDefault() + outCfg.AddSection("gomr") + outCfg.AddOption("gomr", "tags", outTags) + outCfg.WriteFile(fn, 0644, "gomr configuration file") + + return +} diff --git a/golang/run.go b/golang/run.go new file mode 100644 index 0000000..263ecfe --- /dev/null +++ b/golang/run.go @@ -0,0 +1,57 @@ +package main + +import ( + "fmt" + "github.com/codegangsta/cli" + "github.com/robfig/config" + "os" + "os/exec" + "path/filepath" + "strings" +) + +func RunGomr(ctx *cli.Context) filepath.WalkFunc { + return func(path string, f os.FileInfo, err error) error { + + tag := ctx.String("tag") + dryrun := ctx.Bool("dryrun") + + if len(ctx.Args()) == 0 { + panic("no command given") + } + + command := ctx.Args()[0] + + if strings.Contains(path, ".gomr") { + + cfg, _ := config.ReadDefault(path) + gomrTags, _ := cfg.String("gomr", "tags") + + if strings.Contains(gomrTags, tag) { + if dryrun { + fmt.Printf("Would run %s in %s\n", command, filepath.Dir(path)) + } else { + os.Chdir(filepath.Dir(path)) + cmd := exec.Command("sh", "-c", command) + stdout, err := cmd.Output() + + if err != nil { + panic(err.Error()) + } + + println(string(stdout)) + } + } + + } + return nil + } +} + +func run(ctx *cli.Context) { + + root := ctx.String("basepath") + filepath.Walk(root, RunGomr(ctx)) + + return +} diff --git a/post.md b/post.md new file mode 100644 index 0000000..2585712 --- /dev/null +++ b/post.md @@ -0,0 +1,465 @@ +--- +layout: post +title: "Python, Ruby, and Golang: A Command-Line Application Comparison" +author: Kyle W. Purdon +categories: [python, ruby, golang, development] +--- + +**Edit (5/31):** There has been lots of great discussion on my [reddit post](http://www.reddit.com/r/programming/comments/37x1o0/python_ruby_and_golang_a_commandline_application/). I have made some edits (with notes) on many of the code examples you will see throughout this post. + +----- + +In late 2014 I built a tool called [pymr](https://github.com/kpurdon/pymr). I recently felt the need to learn golang and refresh my ruby knowledge so I decided to revisit the idea of pymr and build it in multiple languages. In this post I will break down the "mr" (merr) application (pymr, gomr, rumr) and present the implementation of specific pieces in each language. I will provide an overall personal preference at the end but will leave the comparison of individual pieces up to you. + +For those that want to skip directly to the code here are the repos: + +* [Python - pymr](https://github.com/kpurdon/pymr) +* [Ruby - rumr](https://github.com/kpurdon/rumr) +* [Golang - gomr](https://github.com/kpurdon/gomr) + + + +Application Structure +===== + +The basic idea of this application is that you have some set of related directories that you want to execute a single command on. The "mr" tool provides a method for registering directories, and a method for running commands on groups of registered directories. The application has the following components: + +* A command-line interface +* A registration command (writes a file with given tags) +* A run command (runs a given command on registered directories) + +Command-Line Interface +========== +----- + +The command line interface for the "mr" tools is: + +{% highlight bash %} +$ pymr --help +Usage: pymr [OPTIONS] COMMAND [ARGS]... + +Options: + --help Show this message and exit. + +Commands: + register register a directory + run run a given command in matching... +{% endhighlight %} + +To compare building the command-line interface let's take a look at the register command in each language. + +#### Python (pymr) + +To build the command line interface in python I chose to use the [click](http://click.pocoo.org/4/) package. + +{% highlight python %} +@pymr.command() +@click.option('--directory', '-d', default='./') +@click.option('--tag', '-t', multiple=True) +@click.option('--append', is_flag=True) +def register(directory, tag, append): + ... +{% endhighlight %} + +#### Ruby (rumr) + +To build the command line interface in ruby I chose to use the [thor](http://whatisthor.com/) gem. + +{% highlight ruby %} +desc 'register', 'Register a directory' +method_option :directory, + aliases: '-d', + type: :string, + default: './', + desc: 'Directory to register' +method_option :tag, + aliases: '-t', + type: :array, + default: 'default', + desc: 'Tag/s to register' +method_option :append, + type: :boolean, + desc: 'Append given tags to any existing tags?' +def register + ... +{% endhighlight %} + +#### Golang (gomr) + +To build the command line interface in Golang I chose to use the [cli.go](https://github.com/codegangsta/cli) package. + +{% highlight go %} +app.Commands = []cli.Command{ + { + Name: "register", + Usage: "register a directory", + Action: register, + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "directory, d", + Value: "./", + Usage: "directory to tag", + }, + cli.StringFlag{ + Name: "tag, t", + Value: "default", + Usage: "tag to add for directory", + }, + cli.BoolFlag{ + Name: "append", + Usage: "append the tag to an existing registered directory", + }, + }, + }, +{% endhighlight %} + + +Registration +========== +----- + +The registration logic is as follows: + +1. If the user asks to `--append` read the `.[py|ru|go]mr` file if it exits. +2. Merge the existing tags with the given tags. +3. Write a new `.[...]mr` file with the new tags. + +This breaks down into a few small tasks we can compare in each language: + +* Searching for and reading a file. +* Merging two items (only keeping the unique set) +* Writing a file + +### File Search + +#### Python (pymr) + +For python this invloves the [os](https://docs.python.org/2/library/os.html) module. + +{% highlight python %} +pymr_file = os.path.join(directory, '.pymr') +if os.path.exists(pymr_file): + ... +{% endhighlight %} + +#### Ruby (rumr) + +For ruby this involves the [File](http://ruby-doc.org/core-1.9.3/File.html) class. + +{% highlight ruby %} +rumr_file = File.join(directory, '.rumr') +if File.exist?(rumr_file) + ... +{% endhighlight %} + + +#### Golang (gomr) + +For golang this involves the [path](http://golang.org/pkg/path/) package. + +{% highlight go %} +fn := path.Join(directory, ".gomr") +if _, err := os.Stat(fn); err == nil { + ... +{% endhighlight %} + + +### Unique Merge + +#### Python (pymr) + +For python this involves the use of a [set](https://docs.python.org/2/library/sets.html). + +{% highlight python %} +# new_tags and cur_tags are tuples +new_tags = tuple(set(new_tags + cur_tags)) +{% endhighlight %} + +#### Ruby (rumr) + +For ruby this involves the use of the [.uniq](http://ruby-doc.org/core-2.2.0/Array.html#method-i-uniq) array method. + +{% highlight ruby %} +# Edited (5/31) +# old method: +# new_tags = (new_tags + cur_tags).uniq + +# new_tags and cur_tags are arrays +new_tags |= cur_tags +{% endhighlight %} + +#### Golang (gomr) + +For golang this involves the use of custom function. + +{% highlight go %} +func AppendIfMissing(slice []string, i string) []string { + for _, ele := range slice { + if ele == i { + return slice + } + } + return append(slice, i) +} + +for _, tag := range strings.Split(curTags, ",") { + newTags = AppendIfMissing(newTags, tag) + } +{% endhighlight %} + + +### File Read/Write + +I tried to choose the simplest possible file format to use in each language. + +#### Python (pymr) + +For python this involves the use of the [pickle](https://docs.python.org/2/library/pickle.html) module. + +{% highlight python %} +# read +cur_tags = pickle.load(open(pymr_file)) + +# write +pickle.dump(new_tags, open(pymr_file, 'wb')) +{% endhighlight %} + +#### Ruby (rumr) + +For ruby this involves the use of the [YAML](http://ruby-doc.org/stdlib-2.2.1/libdoc/yaml/rdoc/YAML.html) module. + +{% highlight ruby %} +# read +cur_tags = YAML.load_file(rumr_file) + +# write +# Edited (5/31) +# old method: +# File.open(rumr_file, 'w') { |f| f.write new_tags.to_yaml } +IO.write(rumr_file, new_tags.to_yaml) +{% endhighlight %} + +#### Golang (gomr) + +For golang this involves the use of the [config](https://github.com/robfig/config) package. + +{% highlight go %} +// read +cfg, _ := config.ReadDefault(".gomr") + +// write +outCfg.WriteFile(fn, 0644, "gomr configuration file") +{% endhighlight %} + + +Run (Command Execution) +========== +----- + +The run logic is as follows: + +1. Recursively walk from the given basepath searching for `.[...]mr` files +2. Load a found file, and see if the given tag is in it +3. Call the given command in the directory of a matching file. + +This breaks down into a few small tasks we can compare in each language: + +* Recursive Directory Search +* String Comparison +* Calling a Shell Command + +### Recursive Directory Search + +#### Python (pymr) + +For python this involves the [os](https://docs.python.org/2/library/os.html) module and [fnmatch](https://docs.python.org/2/library/fnmatch.html) module. + +{% highlight python %} +for root, _, fns in os.walk(basepath): + for fn in fnmatch.filter(fns, '.pymr'): + ... +{% endhighlight %} + +#### Ruby (rumr) + +For ruby this involves the [Find](http://ruby-doc.org/stdlib-2.2.0/libdoc/find/rdoc/Find.html) and [File](http://ruby-doc.org/core-2.2.0/File.html) classes. + +{% highlight ruby %} +# Edited (5/31) +# old method: +# Find.find(basepath) do |path| +# next unless File.basename(path) == '.rumr' +Dir[File.join(options[:basepath], '**/.rumr')].each do |path| + ... +{% endhighlight %} + +#### Golang (gomr) + +For golang this requires the [filepath](http://golang.org/pkg/path/filepath/) package and a custom callback function. + +{% highlight go %} +func RunGomr(ctx *cli.Context) filepath.WalkFunc { + return func(path string, f os.FileInfo, err error) error { + ... + if strings.Contains(path, ".gomr") { + ... + +filepath.Walk(root, RunGomr(ctx)) +{% endhighlight %} + +### String Comparison + +#### Python (pymr) + +Nothing additional is needed in python for this task. + +{% highlight python %} +if tag in cur_tags: + ... +{% endhighlight %} + +#### Ruby (rumr) + +Nothing additional is needed in ruby for this task. + +{% highlight ruby %} +if cur_tags.include? tag + ... +{% endhighlight %} + +#### Golang (gomr) + +For golang this requires the [strings](http://golang.org/pkg/strings/) package. + +{% highlight go %} +if strings.Contains(cur_tags, tag) { + ... +{% endhighlight %} + +### Calling a Shell Command + +#### Python (pymr) + +For python this requires the [os](https://docs.python.org/2/library/os.html) module and the [subprocess](https://docs.python.org/2/library/subprocess.html) module. + +{% highlight python %} +os.chdir(root) +subprocess.call(command, shell=True) +{% endhighlight %} + +#### Ruby (rumr) + +For ruby this involves the [Kernel](http://ruby-doc.org/core-2.2.0/Kernel.html#method-i-system) module and the Backticks syntax. + +{% highlight ruby %} +# Edited (5/31) +# old method +# puts `bash -c "cd #{base_path} && #{command}"` +Dir.chdir(File.dirname(path)) { puts `#{command}` } +{% endhighlight %} + +#### Golang (gomr) + +For golang this involves the [os](https://golang.org/pkg/os/) package and the [os/exec](https://golang.org/pkg/os/exec/) package. + +{% highlight go %} +os.Chdir(filepath.Dir(path)) +cmd := exec.Command("bash", "-c", command) +stdout, err := cmd.Output() +{% endhighlight %} + +Packaging +========== +----- + +The ideal mode of distribution for this tool is via a package. A user could then install it `tool install [pymr,rumr,gomr]` and have a new command on there systems path to execute. I dont want to go into packaging systems here, rather I will just show the basic configuration file needed in each language. + +#### Python (pymr) + +For python a `setup.py` is required. Once the package is created and uploaded it can be installed with `pip install pymr`. + +{% highlight python %} +from setuptools import setup, find_packages + +classifiers = [ + 'Environment :: Console', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7' +] + +setuptools_kwargs = { + 'install_requires': [ + 'click>=4,<5' + ], + 'entry_points': { + 'console_scripts': [ + 'pymr = pymr.pymr:pymr', + ] + } +} + + +setup( + name='pymr', + description='A tool for executing ANY command in a set of tagged directories.', + author='Kyle W Purdon', + author_email='kylepurdon@gmail.com', + url='https://github.com/kpurdon/pymr', + download_url='https://github.com/kpurdon/pymr', + version='2.0.1', + packages=find_packages(), + classifiers=classifiers, + **setuptools_kwargs +) +{% endhighlight %} + +#### Ruby (rumr) + +For ruby a `rumr.gemspec` is required. Once the gem is created and uploaded is can be installed with `gem install rumr`. + +{% highlight ruby %} +Gem::Specification.new do |s| + s.name = 'rumr' + s.version = '1.0.0' + s.summary = 'Run system commands in sets' \ + ' of registered and tagged directories.' + s.description = '[Ru]by [m]ulti-[r]epository Tool' + s.authors = ['Kyle W. Purdon'] + s.email = 'kylepurdon@gmail.com' + s.files = ['lib/rumr.rb'] + s.homepage = 'https://github.com/kpurdon/rumr' + s.license = 'GPLv3' + s.executables << 'rumr' + s.add_dependency('thor', ['~>0.19.1']) +end +{% endhighlight %} + +#### Golang (gomr) + +For golang the source is simply compiled into a binary that can be redistributed. There is no additional file needed and currently no package repository to push to. + +Conclusion +========== +----- + +For this tool Golang feels like the wrong choice. I dont need it to be very performant and I'm not utilizing the native concurrency Golang has to offer. This leaves me with Ruby and Python. For about 80% of the logic my personal preference is a toss-up between the two. Here are the pieces I find better in one language: + +### Command-Line Interface Declaration + +Python is the winner here. The [click]() libraries decorator style declaration is clean and simple. Keep in mind I have only tried the Ruby [thor]() gem so there may be better solutions in Ruby. This is also not a commentary on either language, rather that the CLI library I used in python is my preference. + +### Recursive Directory Search + +Ruby is the winner here. I found that this entire section of code was much cleaner and more readable using ruby's `Find.find()` and especially the `next unless` syntax. + +### Packaging + +Ruby is the winner here. The `rumr.gemspec` file is much simpler and the process of building and pushing a gem was much simpler as well. The [bundler](http://bundler.io/) tool also makes installing in semi-isolated environments a snap. + +## Final Determination + +Because of packaging and the recursive directory search preference I would choose Ruby as the tool for this application. However the differences in preference were so minor that Python would be more than fitting as well. Golang however, is not the correct tool here. diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..9453d90 --- /dev/null +++ b/python/README.md @@ -0,0 +1,27 @@ +pymr: [Py]thon [m]utli-[r]epository Tool +----- + +PyMR is a python command-line tool used to execute arbitrary commands on a set of tagged directories. + +## Features + +* Configuration is stored in the registered directory (allows for version-controlled configuration) +* Any arbitrary command can be run, no ties to version-control specific commands. + +## Where is PyMR + +PyMR source is available on [Github](https://github.com/kpurdon/pymr) and is released to [PyPI](https://pypi.python.org/pypi/pymr). + +## Installation + +From pypi: `pip install pymr` + +From source: +* `python setup.py install` + +## Command Help + +For help run: +* `pymr --help` +* `pymr register --help` +* `pymr run --help` diff --git a/python/pymr/__init__.py b/python/pymr/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/pymr/__init__.pyc b/python/pymr/__init__.pyc new file mode 100644 index 0000000..bd94b2f Binary files /dev/null and b/python/pymr/__init__.pyc differ diff --git a/python/pymr/pymr.py b/python/pymr/pymr.py new file mode 100644 index 0000000..08744f7 --- /dev/null +++ b/python/pymr/pymr.py @@ -0,0 +1,61 @@ +import os +import pickle +import fnmatch +from subprocess import call + +import pathlib +import click + + +@click.group() +def pymr(): + pass + + +@pymr.command() +@click.option('--directory', '-d', default='./') +@click.option('--tag', '-t', multiple=True) +@click.option('--append', is_flag=True) +def register(directory, tag, append): + ''' + register a directory + ''' + + pymr_file = os.path.join(directory, '.pymr') + new_tags = tag + + if append: + if os.path.exists(pymr_file): + cur_tags = pickle.load(open(pymr_file)) + new_tags = tuple(set(new_tags + cur_tags)) + + pickle.dump(new_tags, open(pymr_file, 'wb')) + + +@pymr.command() +@click.argument('command') +@click.option('--basepath', '-b', default='./') +@click.option('--tag', '-t') +@click.option('--dryrun', is_flag=True) +def run(command, basepath, tag, dryrun): + ''' + run a given command in matching sub-directories + ''' + + for fn in pathlib.Path(basepath).glob('**/.pymr'): + + with fn.open(mode='rb') as f: + cur_tags = pickle.load(f) + + parent_dir = str(fn.parent) + + if tag in cur_tags: + if dryrun: + print('Would run {0} in {1}'.format(command, parent_dir)) + else: + os.chdir(parent_dir) + call(command, shell=True) + + +if __name__ == '__main__': + pymr() diff --git a/python/pymr/pymr.pyc b/python/pymr/pymr.pyc new file mode 100644 index 0000000..b139c12 Binary files /dev/null and b/python/pymr/pymr.pyc differ diff --git a/python/setup.py b/python/setup.py new file mode 100644 index 0000000..e632ae3 --- /dev/null +++ b/python/setup.py @@ -0,0 +1,37 @@ +from setuptools import setup, find_packages + +classifiers = [ + 'Environment :: Console', + 'Operating System :: OS Independent', + 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', + 'Intended Audience :: Developers', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7' +] + +setuptools_kwargs = { + 'install_requires': [ + 'click>=4,<5', + 'pathlib>=1,<2' + ], + 'entry_points': { + 'console_scripts': [ + 'pymr = pymr.pymr:pymr', + ] + } +} + + +setup( + name='pymr', + description='A tool for executing ANY command in a set of tagged directories.', + author='Kyle W Purdon', + author_email='kylepurdon@gmail.com', + url='https://github.com/kpurdon/pymr', + download_url='https://github.com/kpurdon/pymr', + version='2.0.1', + packages=find_packages(), + classifiers=classifiers, + **setuptools_kwargs +) diff --git a/ruby/Gemfile b/ruby/Gemfile new file mode 100644 index 0000000..98c294d --- /dev/null +++ b/ruby/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'thor', '~>0.19.1' diff --git a/ruby/README.md b/ruby/README.md new file mode 100644 index 0000000..0dbe795 --- /dev/null +++ b/ruby/README.md @@ -0,0 +1,25 @@ +rumr: [Ru]by [m]utli-[r]epository Tool +----- + +Rumr is a ruby command-line tool used to execute arbitrary commands on a set of tagged directories. + +## Features + +* Configuration is stored in the registered directory (allows for version-controlled configuration) +* Any arbitrary command can be run, no ties to version-control specific commands. + + +## Installation + +From rubygems: `gem install rumr` + +From source: +* `gem build rumr.gemspec` +* `gem install rumr-[version].gem` + +## Command Help + +For help run: +* `rumr help` +* `rumr help register` +* `rumr help exec` diff --git a/ruby/bin/rumr b/ruby/bin/rumr new file mode 100755 index 0000000..29e84ec --- /dev/null +++ b/ruby/bin/rumr @@ -0,0 +1,5 @@ +#!/usr/bin/env ruby + +require 'rumr' + +Rumr.start(ARGV) diff --git a/ruby/lib/rumr.rb b/ruby/lib/rumr.rb new file mode 100755 index 0000000..8fd5bf2 --- /dev/null +++ b/ruby/lib/rumr.rb @@ -0,0 +1,54 @@ +require 'find' +require 'yaml' +require 'thor' + +# [Ru]by [m]ulti-[r]epository command line tool +class Rumr < Thor + desc 'register', 'Register a directory' + method_option :directory, + aliases: '-d', + type: :string, + default: './', + desc: 'Directory to register' + method_option :tag, + aliases: '-t', + type: :array, + default: 'default', + desc: 'Tag/s to register' + method_option :append, + type: :boolean, + desc: 'Append given tags to any existing tags?' + def register + new_tags = options[:tag] + rumr_file = File.join(options[:directory], '.rumr') + if options[:append] && File.exist?(rumr_file) + new_tags |= YAML.load_file(rumr_file) + end + IO.write(rumr_file, new_tags.to_yaml) + end + + desc 'exec COMMAND', 'Execute (run) a given command on registered directories' + method_option :basepath, + aliases: '-b', + type: :string, + default: './', + desc: 'Directory to begin search for rumr files.' + method_option :tag, + aliases: '-t', + type: :string, + default: 'default', + desc: 'Tag to match against' + method_option :dryrun, + type: :boolean, + desc: 'Display (do not execute) the commands to run.' + def exec(command) + Dir[File.join(options[:basepath], '**/.rumr')].each do |path| + next unless YAML.load_file(path).include? options[:tag] + if options['dryrun'] + puts "Would execute #{command} in #{path}" + else + Dir.chdir(File.dirname(path)) { puts `#{command}` } + end + end + end +end diff --git a/ruby/rumr.gemspec b/ruby/rumr.gemspec new file mode 100644 index 0000000..7ac7bd9 --- /dev/null +++ b/ruby/rumr.gemspec @@ -0,0 +1,14 @@ +Gem::Specification.new do |s| + s.name = 'rumr' + s.version = '1.0.2' + s.summary = 'Run system commands in sets' \ + ' of registered and tagged directories.' + s.description = '[Ru]by [m]ulti-[r]epository Tool' + s.authors = ['Kyle W. Purdon'] + s.email = 'kylepurdon@gmail.com' + s.files = ['lib/rumr.rb'] + s.homepage = 'https://github.com/kpurdon/rumr' + s.license = 'GPLv3' + s.executables << 'rumr' + s.add_dependency('thor', ['~>0.19.1']) +end