Python, Ruby, and Golang: A Command-Line Application Comparison

Python, Ruby, and Golang: A Command-Line Application Comparison

Python, Ruby, and Golang: A Command-Line Application Comparison

In late 2014 I built a tool called 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.

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:

$ 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...

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 package.

@pymr.command()
@click.option('--directory', '-d', default='./')
@click.option('--tag', '-t', multiple=True)
@click.option('--append', is_flag=True)
def register(directory, tag, append):
    ...

Ruby (rumr)

To build the command line interface in ruby I chose to use the thor gem.

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
  ...

Golang (gomr)

To build the command line interface in Golang I chose to use the cli.go package.

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",
            },
        },
    },
}

Registration

The registration logic is as follows:

  1. If the user asks to --append read the .[py|ru|go]mr file if it exists.
  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 involves the os module.

pymr_file = os.path.join(directory, '.pymr')
if os.path.exists(pymr_file):
    # ...

Ruby (rumr)

For ruby this involves the File class.

rumr_file = File.join(directory, '.rumr')
if File.exist?(rumr_file)
    # ...

Golang (gomr)

For golang this involves the path package.

fn := path.Join(directory, ".gomr")
if _, err := os.Stat(fn); err == nil {
    // ...
}

Unique Merge

Python (pymr)

For python this involves the use of a set.

# new_tags and cur_tags are tuples
new_tags = tuple(set(new_tags + cur_tags))

Ruby (rumr)

For ruby this involves the use of the .uniq array method.

# Edited (5/31)
# old method:
#  new_tags = (new_tags + cur_tags).uniq

# new_tags and cur_tags are arrays
new_tags |= cur_tags

Golang (gomr)

For golang this involves the use of custom function.

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)
}

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 module.

# read
cur_tags = pickle.load(open(pymr_file))

# write
pickle.dump(new_tags, open(pymr_file, 'wb'))

Ruby (rumr)

For ruby this involves the use of the YAML module.

# 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)

Golang (gomr)

For golang this involves the use of the config package.

// read
cfg, _ := config.ReadDefault(".gomr")

// write
outCfg.WriteFile(fn, 0644, "gomr configuration file")

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 module and fnmatch module.

for root, _, fns in os.walk(basepath):
    for fn in fnmatch.filter(fns, '.pymr'):
        # ...

Ruby (rumr)

For ruby this involves the Find and File classes.

# 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|
    # ...

Golang (gomr)

For golang this requires the filepath package and a custom callback function.

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))

String Comparison

Python (pymr)

Nothing additional is needed in python for this task.

if tag in cur_tags:
    # ...

Ruby (rumr)

Nothing additional is needed in ruby for this task.

if cur_tags.include? tag
    # ...

Golang (gomr)

For golang this requires the strings package.

if strings.Contains(cur_tags, tag) {
    // ...
}

Calling a Shell Command

Python (pymr)

For python this requires the os module and the subprocess module.

os.chdir(root)
subprocess.call(command, shell=True)

Ruby (rumr)

For ruby this involves the Kernel module and the Backticks syntax.

# Edited (5/31)
# old method
#  puts `bash -c "cd #{base_path} && #{command}"`
Dir.chdir(File.dirname(path)) { puts `#{command}` }

Golang (gomr)

For golang this involves the os package and the os/exec package.

os.Chdir(filepath.Dir(path))
cmd := exec.Command("bash", "-c", command)
stdout, err := cmd.Output()

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 don’t 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.

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='[email protected]',
    url='https://github.com/kpurdon/pymr',
    download_url='https://github.com/kpurdon/pymr',
    version='2.0.1',
    packages=find_packages(),
    classifiers=classifiers,
    **setuptools_kwargs
)

Ruby (rumr)

For ruby a rumr.gemspec is required. Once the gem is created and uploaded is can be installed with gem install rumr.

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       = '[email protected]'
  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

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 don’t 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 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.

Thanks for reading ❤

Complete Python Course Go from zero to hero in Python

Complete Python Course Go from zero to hero in Python

Complete Python Course Go from zero to hero in Python

Description
Welcome to the Complete Python Course!

This course will take you from Zero to Hero in Python, easily and smartly. We've crafted every piece of content to be concise and straightforward, while never leaving you confused. This course will dive right into Python and get you productive from the very beginning. This is the most comprehensive, yet straight-forward, course for the Python programming language on Simpliv! Whether you have never programmed before, already know basic syntax, or want to learn about the advanced features of Python, this course is for you! In this course we will teach you Python 3. (Note, we also provide older Python 2 notes in case of you need them)

Why Learn Python?

Python is a general-purpose language, which means it can be used to build just about anything,which will be made easy with the right tools/libraries. Professionally, Python is great for backend web development, data analysis, artificial intelligence, and scientific computing. Many developers have also used Python to build productivity tools, games, and desktop apps, so there are plenty of resources to help you learn how to do those as well.

To read more:

Moving from Python to Go

Moving from Python to Go

In this post, I’m going to tell you about my journey from Python to Go, and provide you with some tips and expose you to some of the resources that helped me succeed on this journey and live to tell the tale.

I love Python. It has been my go-to language for the past five years. Python is very friendly and easy to learn while still remaining super effective.

You can use it for almost anything — from creating a simple script and web development through data visualizations, and machine learning.

But the growth in the maturity of Go, the strong user base, and the fact that more and more companies have decided to move to Go after successful benchmarking, made me read more extensively about Go, and think about how I can add it into my tool set and apply its benefits to my work.

But this post is not going to talk about which programming language is better — Python or Go, there are plenty of posts and comparisons about this topic online, and it really depends on the use case in my opinion.

In this post, I’m going to tell you about my journey from Python to Go, and provide you with some tips and expose you to some of the resources that helped me succeed on this journey and live to tell the tale.

Key differences that I encountered

Of course, as first step I went over the amazing official “Tour Of Go”, which definitely gave me a strong basic knowledge about the Go syntax.

In order to strengthen that knowledge, I read the Go for Python Programmers ebook that allowed me to continue on to the next step — which I believe to be the most educational — trying and failing.

I took common functions that I had previously been using in Python like JSON serialization or working with HTTP calls, and tried to write them in Go.

By applying similar concepts from Python in Go, and still embracing the static nature of the language, I encountered some of the key differences between Go and Python.

Project Layout

First and foremost, Python typically does not require a specific directory hierarchy, whereas , Go on the other hand does.

Go uses a “standard” layout which is a bit more complex and creates slightly more work, but the upside is a well-structured code base that encourages modular code and keeps things orderly as a project grows in size.

The official “How to Write Go Code” has a section that explains exactly how to build your workspace.

Statically and strongly typed

Go is a statically typed language, which can make you feel uncomfortable at first because of your habits from a dynamically typed languages like Python or Ruby.

There is no doubt that dynamic languages are more error prone, and it takes more effort in terms of input validation to prevent common syntax or parsing errors. Think about a function that calculates the sum of two integers, there is really no guarantee that the user who uses this function won’t pass a string into the function — which will cause a TypeError.
This scenario can’t happen in Go, as you need to declare the type for each variable — which type of variable your function can get, and which type of variable your function will return.

At first it was a bit annoying, and it felt like it makes my coding speed much slower, but after a short time reading and writing in Go, you really get used to it, and it actually can save time, and make your code much more robust.

Native concurrency

Go has native concurrency support using goroutines and channels, which can be really handy nowadays.

At first, the concept of channels can be a bit tricky, and it’s easy to look at it as some kind of data structure or queuing implementation. After some reading though, they become more straightforward, and you can really enjoy the value they bring, and take full advantage of them.

A simple visualization of goroutines and channels by Ivan Daniluk

package main
func main() {
    // create new channel of type int
    ch := make(chan int)
// start new anonymous goroutine
    go func() {
        // send 42 to channel
        ch <- 42
    }()
    // read from channel
    <-ch
}

For more examples, take a look at the Hootsuite real life implementation of goroutines, channels and the select statement, or this great explanation from ArdanLabs.

Working with JSON

Well, no more json.loads() for you.
In Python deserializing JSON objects is super simple — just use json.loads and that’s it!
But in Go, as a statically typed language, this simple operation might be more tricky.

In Go, you parse the JSON into a struct you’ve defined before. Any field which won’t fit in the struct will be ignored, which is a good thing. Think of it as a predefined protocol between the two sides. You really don’t need to be “surprised” by the data you received in the JSON, and the JSON fields and types need to be “agreed” upon by both sides.

{
  “first”: “Elad”,
  “last”: “Leev”,
  “location”:”IL”,
  “id”: “93”
}

type AccountData struct {
 First    string `json:"first"`
 Last     string `json:"last"`
 Location string `json:"location"`
 ID       string `json:"id"`
}

Of course you can still deserialize JSONs without structs but it should be avoided if possible, and it’s always better to embrace the static nature of the language.

To better understand JSON encoding in Go, you can take a look at this post , or use “Go By Example” — which is the ultimate cheat sheet resource you will ever have.
Too lazy to convert your JSON into Go struct? No problem — this tool will do it for you.

Clean code

The Go compiler will always try to keep your code as clean as possible.
Go compiler treats unused variables as a compilation error, and moreover, Go took the unique approach of having the machine take care of most formatting issues.
Go will run the gofmt program while saving or compiling, and it will take care of the large majority of formatting issues.

You don’t care about one of the variables? Again — no problem! Just use the _ (underscore) and assign it to an empty identifier.

A must read doc that contains info about Go formatting is “Effective Go”.

Finding the Right Library and Frameworks

I really got used to my Python frameworks and libraries like Flask, Jinja2, Requests and even Kazoo, and I really worried that I won’t find the right ones for Go.
But as you can guess, the great community of Go has it’s own unique libraries that can even make you completely forget about the old ones.
Here are some of my preferences —

Python Requests => **net/http**The built in [net/http](https://golang.org/pkg/net/http/)provides HTTP client and server implementations that is really great, and super easy to use.
Flask + Jinja2=> Gin
Gin is an HTTP web framework with a really simple API — parameters in path, upload files, grouping routes (/api/v1 , /api/v2), custom log formats, serving static files, HTML rendering and really powerful custom middleware.
Take a look at this benchmark.
CLI Creation=> Cobra Cobra is both a library for creating powerful CLI applications as well as a program to generate applications and command files.
Many of the most widely used Go projects are built using Cobra including Kubernetes, etcd and OpenShift.

Some other libraries that I highly recommend are: Viper, Gonfig, and this awesome list — Awsome-Go.

Summary

As an avid Python user for five years, I was concerned that the transition to Go would be a painful one.

But I was really psyched to see that there is a truly strong community, contributing and maintaining excellent resources to help you succeed with your transition to Go.

Go is one of the fastest growing programming languages today, and I expect that Google will manage to make Go the go-to language for writing cloud applications and infrastructure.

This is an exciting time for Go and I encourage you all to check it out and become Gophers!