├── .gitignore ├── README.md ├── golang ├── README.md ├── gomr.go ├── register.go └── run.go ├── python ├── README.md ├── pymr │ ├── __init__.py │ └── pymr.py └── setup.py └── ruby ├── Gemfile ├── README.md ├── bin └── rumr ├── lib └── rumr.rb └── rumr.gemspec /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realpython/python-ruby-golang/f104762099870708f340f323acf1d7b881ac1f2e/.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | python-ruby-golang 2 | ---------- 3 | 4 | A comparison of click (Python), thor (Ruby), and cli.go (Golang) for building a very simple command-line tool. 5 | 6 | ## QuickStart 7 | 8 | See the README.md in each of the subdirectories for more information. 9 | 10 | ## Blog Post 11 | 12 | [Python, Ruby, and Golang: A Command-Line Application Comparison](https://realpython.com/blog/python/python-ruby-and-golang-a-command-line-application-comparison/) 13 | -------------------------------------------------------------------------------- /golang/README.md: -------------------------------------------------------------------------------- 1 | gomr: go [m]utli-[r]epository manager 2 | ----- 3 | 4 | gomr is a golang command-line tool used to execute arbitrary commands on a set of tagged directories. 5 | 6 | ## Features 7 | 8 | * Configuration is stored in the registered directory (allows for version-controlled configuration) 9 | * Any arbitrary command can be run, no ties to version-control specific commands. 10 | 11 | 12 | ## Installation 13 | 14 | I'm not distributing binaries yet (TODO) for now checkout and `go install` to build the binary. 15 | 16 | ## Command Help 17 | 18 | For help run: 19 | * `gomr --help` 20 | * `gomr register --help` 21 | * `gomr run --help` 22 | -------------------------------------------------------------------------------- /golang/gomr.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/codegangsta/cli" 5 | "os" 6 | ) 7 | 8 | func main() { 9 | app := cli.NewApp() 10 | app.Name = "gomr" 11 | app.Author = "Kyle W. Purdon" 12 | app.Version = "0.0.1" 13 | app.Usage = "multi-repository manager in go" 14 | app.Commands = []cli.Command{ 15 | { 16 | Name: "register", 17 | Usage: "register a directory", 18 | Action: register, 19 | Flags: []cli.Flag{ 20 | cli.StringFlag{ 21 | Name: "directory, d", 22 | Value: "./", 23 | Usage: "directory to tag", 24 | }, 25 | cli.StringFlag{ 26 | Name: "tag, t", 27 | Value: "default", 28 | Usage: "tag to add for directory", 29 | }, 30 | cli.BoolFlag{ 31 | Name: "append", 32 | Usage: "append the tag to an existing registered directory", 33 | }, 34 | }, 35 | }, 36 | { 37 | Name: "run", 38 | Usage: "run a command", 39 | Action: run, 40 | Flags: []cli.Flag{ 41 | cli.StringFlag{ 42 | Name: "basepath, b", 43 | Value: "./", 44 | Usage: "path to begin the recursive search", 45 | }, 46 | cli.StringFlag{ 47 | Name: "tag, t", 48 | Value: "default", 49 | Usage: "tag to add for directory", 50 | }, 51 | cli.BoolFlag{ 52 | Name: "dryrun, d", 53 | Usage: "print (dont execute) the commands that will be run", 54 | }, 55 | }, 56 | }, 57 | } 58 | 59 | app.Run(os.Args) 60 | } 61 | -------------------------------------------------------------------------------- /golang/register.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codegangsta/cli" 6 | "github.com/robfig/config" 7 | "os" 8 | "path" 9 | "strings" 10 | ) 11 | 12 | func AppendIfMissing(slice []string, i string) []string { 13 | for _, ele := range slice { 14 | if ele == i { 15 | return slice 16 | } 17 | } 18 | return append(slice, i) 19 | } 20 | 21 | func register(ctx *cli.Context) { 22 | 23 | fn := path.Join(ctx.String("directory"), ".gomr") 24 | 25 | newTags := strings.Split(ctx.String("tag"), ",") 26 | 27 | if ctx.Bool("append") { 28 | if _, err := os.Stat(fn); err == nil { 29 | cfg, _ := config.ReadDefault(".gomr") 30 | curTags, _ := cfg.String("gomr", "tags") 31 | 32 | for _, tag := range strings.Split(curTags, ",") { 33 | newTags = AppendIfMissing(newTags, tag) 34 | } 35 | } else { 36 | err := "append used, existing file not found." 37 | fmt.Fprintf(os.Stderr, "error: %v\n", err) 38 | os.Exit(1) 39 | } 40 | } 41 | 42 | outTags := strings.Join(newTags, ",") 43 | 44 | outCfg := config.NewDefault() 45 | outCfg.AddSection("gomr") 46 | outCfg.AddOption("gomr", "tags", outTags) 47 | outCfg.WriteFile(fn, 0644, "gomr configuration file") 48 | 49 | return 50 | } 51 | -------------------------------------------------------------------------------- /golang/run.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/codegangsta/cli" 6 | "github.com/robfig/config" 7 | "os" 8 | "os/exec" 9 | "path/filepath" 10 | "strings" 11 | ) 12 | 13 | func RunGomr(ctx *cli.Context) filepath.WalkFunc { 14 | return func(path string, f os.FileInfo, err error) error { 15 | 16 | tag := ctx.String("tag") 17 | dryrun := ctx.Bool("dryrun") 18 | 19 | if len(ctx.Args()) == 0 { 20 | panic("no command given") 21 | } 22 | 23 | command := ctx.Args()[0] 24 | 25 | if strings.Contains(path, ".gomr") { 26 | 27 | cfg, _ := config.ReadDefault(path) 28 | gomrTags, _ := cfg.String("gomr", "tags") 29 | 30 | if strings.Contains(gomrTags, tag) { 31 | if dryrun { 32 | fmt.Printf("Would run %s in %s\n", command, filepath.Dir(path)) 33 | } else { 34 | os.Chdir(filepath.Dir(path)) 35 | cmd := exec.Command("sh", "-c", command) 36 | stdout, err := cmd.Output() 37 | 38 | if err != nil { 39 | panic(err.Error()) 40 | } 41 | 42 | println(string(stdout)) 43 | } 44 | } 45 | 46 | } 47 | return nil 48 | } 49 | } 50 | 51 | func run(ctx *cli.Context) { 52 | 53 | root := ctx.String("basepath") 54 | filepath.Walk(root, RunGomr(ctx)) 55 | 56 | return 57 | } 58 | -------------------------------------------------------------------------------- /python/README.md: -------------------------------------------------------------------------------- 1 | pymr: [Py]thon [m]utli-[r]epository Tool 2 | ----- 3 | 4 | PyMR is a python command-line tool used to execute arbitrary commands on a set of tagged directories. 5 | 6 | ## Features 7 | 8 | * Configuration is stored in the registered directory (allows for version-controlled configuration) 9 | * Any arbitrary command can be run, no ties to version-control specific commands. 10 | 11 | ## Where is PyMR 12 | 13 | PyMR source is available on [Github](https://github.com/kpurdon/pymr) and is released to [PyPI](https://pypi.python.org/pypi/pymr). 14 | 15 | ## Installation 16 | 17 | From pypi: `pip install pymr` 18 | 19 | From source: 20 | * `python setup.py install` 21 | 22 | ## Command Help 23 | 24 | For help run: 25 | * `pymr --help` 26 | * `pymr register --help` 27 | * `pymr run --help` 28 | -------------------------------------------------------------------------------- /python/pymr/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realpython/python-ruby-golang/f104762099870708f340f323acf1d7b881ac1f2e/python/pymr/__init__.py -------------------------------------------------------------------------------- /python/pymr/pymr.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import fnmatch 4 | from subprocess import call 5 | 6 | import click 7 | 8 | 9 | @click.group() 10 | def pymr(): 11 | pass 12 | 13 | 14 | @pymr.command() 15 | @click.option('--directory', '-d', default='./') 16 | @click.option('--tag', '-t', multiple=True) 17 | @click.option('--append', is_flag=True) 18 | def register(directory, tag, append): 19 | ''' 20 | register a directory 21 | ''' 22 | 23 | pymr_file = os.path.join(directory, '.pymr') 24 | new_tags = tag 25 | 26 | if append: 27 | if os.path.exists(pymr_file): 28 | cur_tags = pickle.load(open(pymr_file)) 29 | new_tags = tuple(set(new_tags + cur_tags)) 30 | 31 | pickle.dump(new_tags, open(pymr_file, 'wb')) 32 | 33 | 34 | @pymr.command() 35 | @click.argument('command') 36 | @click.option('--basepath', '-b', default='./') 37 | @click.option('--tag', '-t') 38 | @click.option('--dryrun', is_flag=True) 39 | def run(command, basepath, tag, dryrun): 40 | ''' 41 | run a given command in matching sub-directories 42 | ''' 43 | 44 | for root, _, fns in os.walk(basepath): 45 | for fn in fnmatch.filter(fns, '.pymr'): 46 | cur_tags = pickle.load(open(os.path.join(root, fn))) 47 | if tag in cur_tags: 48 | if dryrun: 49 | print('Would run {0} in {1}'.format(command, root)) 50 | else: 51 | os.chdir(root) 52 | call(command, shell=True) 53 | 54 | 55 | if __name__ == '__main__': 56 | pymr() 57 | -------------------------------------------------------------------------------- /python/setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | classifiers = [ 4 | 'Environment :: Console', 5 | 'Operating System :: OS Independent', 6 | 'License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)', 7 | 'Intended Audience :: Developers', 8 | 'Programming Language :: Python', 9 | 'Programming Language :: Python :: 2', 10 | 'Programming Language :: Python :: 2.7' 11 | ] 12 | 13 | setuptools_kwargs = { 14 | 'install_requires': [ 15 | 'click>=4,<5', 16 | 'pathlib>=1,<2' 17 | ], 18 | 'entry_points': { 19 | 'console_scripts': [ 20 | 'pymr = pymr.pymr:pymr', 21 | ] 22 | } 23 | } 24 | 25 | 26 | setup( 27 | name='pymr', 28 | description='A tool for executing ANY command in a set of tagged directories.', 29 | author='Kyle W Purdon', 30 | author_email='kylepurdon@gmail.com', 31 | url='https://github.com/kpurdon/pymr', 32 | download_url='https://github.com/kpurdon/pymr', 33 | version='2.0.1', 34 | packages=find_packages(), 35 | classifiers=classifiers, 36 | **setuptools_kwargs 37 | ) 38 | -------------------------------------------------------------------------------- /ruby/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gem 'thor', '~>0.19.1' 4 | -------------------------------------------------------------------------------- /ruby/README.md: -------------------------------------------------------------------------------- 1 | rumr: [Ru]by [m]utli-[r]epository Tool 2 | ----- 3 | 4 | Rumr is a ruby command-line tool used to execute arbitrary commands on a set of tagged directories. 5 | 6 | ## Features 7 | 8 | * Configuration is stored in the registered directory (allows for version-controlled configuration) 9 | * Any arbitrary command can be run, no ties to version-control specific commands. 10 | 11 | 12 | ## Installation 13 | 14 | From rubygems: `gem install rumr` 15 | 16 | From source: 17 | * `gem build rumr.gemspec` 18 | * `gem install rumr-[version].gem` 19 | 20 | ## Command Help 21 | 22 | For help run: 23 | * `rumr help` 24 | * `rumr help register` 25 | * `rumr help exec` 26 | -------------------------------------------------------------------------------- /ruby/bin/rumr: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require 'rumr' 4 | 5 | Rumr.start(ARGV) 6 | -------------------------------------------------------------------------------- /ruby/lib/rumr.rb: -------------------------------------------------------------------------------- 1 | require 'find' 2 | require 'yaml' 3 | require 'thor' 4 | 5 | # [Ru]by [m]ulti-[r]epository command line tool 6 | class Rumr < Thor 7 | desc 'register', 'Register a directory' 8 | method_option :directory, 9 | aliases: '-d', 10 | type: :string, 11 | default: './', 12 | desc: 'Directory to register' 13 | method_option :tag, 14 | aliases: '-t', 15 | type: :array, 16 | default: 'default', 17 | desc: 'Tag/s to register' 18 | method_option :append, 19 | type: :boolean, 20 | desc: 'Append given tags to any existing tags?' 21 | def register 22 | new_tags = options[:tag] 23 | rumr_file = File.join(options[:directory], '.rumr') 24 | if options[:append] && File.exist?(rumr_file) 25 | new_tags |= YAML.load_file(rumr_file) 26 | end 27 | IO.write(rumr_file, new_tags.to_yaml) 28 | end 29 | 30 | desc 'exec COMMAND', 'Execute (run) a given command on registered directories' 31 | method_option :basepath, 32 | aliases: '-b', 33 | type: :string, 34 | default: './', 35 | desc: 'Directory to begin search for rumr files.' 36 | method_option :tag, 37 | aliases: '-t', 38 | type: :string, 39 | default: 'default', 40 | desc: 'Tag to match against' 41 | method_option :dryrun, 42 | type: :boolean, 43 | desc: 'Display (do not execute) the commands to run.' 44 | def exec(command) 45 | Dir[File.join(options[:basepath], '**/.rumr')].each do |path| 46 | next unless YAML.load_file(path).include? options[:tag] 47 | if options['dryrun'] 48 | puts "Would execute #{command} in #{path}" 49 | else 50 | Dir.chdir(File.dirname(path)) { puts `#{command}` } 51 | end 52 | end 53 | end 54 | end 55 | -------------------------------------------------------------------------------- /ruby/rumr.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |s| 2 | s.name = 'rumr' 3 | s.version = '1.0.2' 4 | s.summary = 'Run system commands in sets' \ 5 | ' of registered and tagged directories.' 6 | s.description = '[Ru]by [m]ulti-[r]epository Tool' 7 | s.authors = ['Kyle W. Purdon'] 8 | s.email = 'kylepurdon@gmail.com' 9 | s.files = ['lib/rumr.rb'] 10 | s.homepage = 'https://github.com/kpurdon/rumr' 11 | s.license = 'GPLv3' 12 | s.executables << 'rumr' 13 | s.add_dependency('thor', ['~>0.19.1']) 14 | end 15 | --------------------------------------------------------------------------------