June 23rd, 2024

Ruby: A great language for shell scripts

Ruby is praised for shell scripting due to its features like calling external commands, handling status codes, using types, functional constructs, regex matching, threading, and file operations. It's recommended for complex scripts alongside Bash.

Read original articleLink Icon
Ruby: A great language for shell scripts

Ruby is highlighted as a powerful language for writing shell scripts, offering features that make it a compelling alternative to Bash and Python. The article emphasizes Ruby's ability to call external commands easily using backticks, handle status codes effectively, utilize types for safety, implement functional constructions like map and reduce, perform regex matching seamlessly, manage threads effortlessly, and conduct file and directory operations intuitively. The author suggests considering Ruby for complex scripts while acknowledging the continued relevance of Bash. The article concludes by advocating for Ruby as a comprehensive and user-friendly option for shell scripting tasks.

Related

Smalltalk syntax in 7 minutes [video]

Smalltalk syntax in 7 minutes [video]

The YouTube video explains Smalltalk syntax, emphasizing readability and message-based object interaction. It covers keywords, arrays, closures, and method execution in Pharo Smalltalk, providing a practical example and additional learning resources.

Lessons Learned from Scaling to Multi-Terabyte Datasets

Lessons Learned from Scaling to Multi-Terabyte Datasets

Insights on scaling to multi-terabyte datasets, emphasizing algorithm evaluation before scaling. Tools like Joblib and GNU Parallel for single machine scaling, transitioning to multiple machines, and comparing performance/cost implications. Recommendations for parallel workloads and analytical tasks using AWS Batch, Dask, and Spark. Considerations for tool selection based on team size and workload.

As you learn Forth, it learns from you (1981)

As you learn Forth, it learns from you (1981)

The Forth programming language is highlighted for its unique features like extensibility, speed, and efficiency. Contrasted with Basic, Forth's threaded code system and data handling methods make it versatile.

Elixir Gotchas

Elixir Gotchas

The article highlights common pitfalls in Elixir programming, including confusion between charlists and strings, differences in pattern matching, struct behavior, accessing struct fields, handling keyword lists, and unique data type comparisons.

Start all of your commands with a comma (2009)

Start all of your commands with a comma (2009)

The article discusses creating a ~/bin/ directory in Unix to store custom commands, avoiding name collisions with system commands by prefixing custom commands with a comma. This technique ensures unique, easily accessible commands.

Link Icon 44 comments
By @codesnik - 5 months
I sometimes wonder why we don't see ruby used for shell stuff more often. It inherited most of the good stuff for shell scripting from Perl, and Perl took a lot of it's syntax from sh and sed and awk, so almost anything you can do in shell script you can do in ruby, but with an option of making it gradually less terse and more readable, while having sane variables and data handling from the start.

Also ruby is great in allowing complexity to grow smoothly, no sudden hiccups. You start with just one line (everything goes into module main implicitly), extend it to a single-file script, require some built-in libraries, then add a module or helper class in the same file, and only then maybe extract those files to required files, add gems, whatever. No boilerplate whatsoever, no jumps, no big rewrites.

meanwhile, a lot of tooling nowadays is written in Go, and I have no idea why, it's not friendly for os manipulation at all, and number crunching power is not needed in many, many tasks of that sort.

By @cyclotron3k - 5 months
Totally agree! Other tricks I rely on:

a) put a `binding.irb` (or `binding.pry`) in any rescue block you may have in your script - it'll allow you to jump in and see what went wrong in an interactive way. (You'll need a `require 'irb'` in your script too, ofc)

b) I always use `Pathname` instead of `File` - it's part of the standard library, is a drop in replacement for `File` (and `Dir`) and generally has a much more natural API.

c) Often backticks are all you need, but when you need something a little stronger (e.g. when handling filenames with spaces in them, or potentially hostile user input, etc), Ruby has a plethora of tools in its stdlib to handle any scenario. First step would be `system` which escapes inputs for you (but doesn't return stdout).

d) Threads in Ruby are super easy, but using `Parallel` (not part of the stdlib) can make it even easier! A contrived example: `Parallel.map(url_list) { |url| Benchmark.measure { system('wget', url) }.real }.sum` to download a bunch of files in parallel and get the total time.

MacOS has Ruby 2.6 installed by default which is perfectly serviceable, but it's EOL and there are plenty of features in 3+ that make the jump more than worthwhile.

By @xavdid - 5 months
Ruby's a great language- I've always enjoyed its ergonomics and clarity. But its editor tooling hasn't kept up with its one-time competitor, Python.

I've mostly been in the Python ecosystem for the past few years and the LSP investment from Microsoft has really shown. Rich Python support in VSCode is seamless and simple. Coming back to Ruby after that caught me off guard - it feels like I'm writing syntax-highlighted plain text. There's an LSP extension from Shopify, but it's temperamental and I have trouble getting it working.

Editor support isn't everything (the actual language design is still the most important), but it definitely affects how eager I am to use it. I basically never choose Ruby over Python given the option, which is too bad. Ruby's a cool language!

By @kazinator - 5 months
It seems like a waste of precious syntax to dedicate backticks to running shell commands.

What's between the backticks is not even portable; the commands rely on an operating-system-specific command interpreter.

> puts `ls`.lines.map { |name| name.strip.length } # prints the lengths of the filenames

Fantastic example, except for the commandment violation: "thou shalt not parse the output of 'ls'"!

You really want to read the directory and map over the stat function (or equivalent) to get the length.

  2> (flow "."
       open-directory
       get-lines
       (mapcar [chain stat .size]))
  (727 2466 21 4096 643 16612 5724 163 707 319 352135 140 51 0 4096
   114898 1172428 1328258 4096 4096 4096 29953 4096 4096 0 27 4096
   4096 35585 8450 968 40960 14610 4096 14 755128 1325258 4096 17283
   218 471 104 4096 99707 1255 4096 129 4096 721 9625 401 15658
   4096 235 98 1861 664 23944 4286 4096 1024 0)
By @lambdaba - 5 months
The greatest feature for this is inline deps, something very rare to have built-in (I've only found Deno to have a similar feature): https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
By @dcchambers - 5 months
I work for a company that has a large Rails monolith. Although we use many more languages than just ruby these days, we still have a ton of scripting, config, and tooling that is all in Ruby. It's a joy to work with IMO.

Another common pattern I see is people using embedded ruby in a shell script. It does make it a little harder to read/understand at a glance, but it's nice for being able to do most simple things in sh but drop into ruby for things that sh/bash suck at.

That said, I get a feeling that the people that joined once we'd added stuff outside of the Rails monolith and don't know/use Ruby are...not big fans.

By @drusepth - 5 months
Ruby is an amazing language. I've seen some systems it's not already installed on and hop over to something like perl/python in those cases, but Ruby is by far my preferred hammer for small scripts. The code is beautiful.

Small nit: your note in Feature 4 is actually supposed to be in Feature 5, I assume.

By @nightpool - 5 months
Ruby is my favorite shell scripting language, I used it last year for a complex ffmpeg automation script (use blackdetect to detect long stretches of power-off and split a video file into much smaller components), and Ruby made it a breeze when I know it would have been a real struggle to get working in bash or powershell
By @hiAndrewQuinn - 5 months
I have what probably sounds like a niche use case, where most of the boxes I work on don't have access to the Internet.

So for me, "is it installed in the base distribution" is the difference between being able to start immediately no matter which box I'm concerned with, and spending months trying to upstream a new program to be installed with our OS image team.

I took a look around a vanilla Debian 12 box, and didn't see Ruby in there [1]. So, sadly, although I really like the way Ruby looks, I'm going to have to stick with Bash and Python 3 for the hard stuff.

[1]: https://hiandrewquinn.github.io/til-site/posts/what-programm...

By @djbusby - 5 months
These are great points, if you already have Ruby in your stack.

For me, when Bash ain't enough I upgrade to the Project language (PHP, Python, JS, etc). For compiled project I reach for LSB language (Perl, Python) before introducing a new dependency.

By @phendrenad2 - 5 months
Sadly, 9 out of 10 environments lack a Ruby interpreter out of the box. Are you going to add 5 minutes to your docker build to compile Ruby? Probably not.

Luckily, I've found that Perl has most of the best features of Ruby, and it's installed everywhere. It's time to Make Perl Great Again.

By @corytheboyd - 5 months
Overall a nice lite write up! Bash is great, but it occasionally becomes untenable, usually around the time where HTTP and whatnot becomes involved. Same goes for shell exec exit codes, you can use an API like popen3 for this: https://ruby-doc.org/stdlib-2.4.1/libdoc/open3/rdoc/Open3.ht...

You mention using threads and regex match global variables in the same write up. Please use the regex match method response instead of the $1 variables to save yourself the potential awful debugging session. It even lets you access named capture groups in the match response using the already familiar Hash access API. Example: https://stackoverflow.com/a/18825787

In general, just don’t use global variables in Ruby. It’s already SO easy to move “but it has to be global” functionality to static class methods or constants that I’ve encountered exactly zero cases where I have NEEDED global variables. Even if you need a “stateful constant” Ruby had a very convenient Singleton mixin that provides for a quick and easy solution.

Besides, if you actually WERE to take advantage of them being global VARIABLES (reassigning the value) I would confidently bet that your downstream code would break, because I’m guessing said downstream code assumes the global value is constant. Just avoid them, there’s no point, use constants. This applies to any language TBH, but here we’re talking about Ruby :)

By @samatman - 5 months
The things which make Ruby good for shell scripts are, to a large degree, things it inherited from Perl. Which was, and is, a great language for scripting.

People use it a lot less these days, for a lot of reasons, some better than others. I myself do simple stuff in bash, and pull out some Python for more complex scripting. My Perl chops have withered, last time I was paid to use it was 21 years ago, but it really is a great scripting language, and you'll find it installed on a great deal more systems than Ruby.

One of these days I'll give Raku a spin, just for old time's sake.

By @bingemaker - 5 months
My first application of Ruby was to use it for shell auto-completions. I'm so grateful that I learnt Ruby first, and then Rails. Ruby is a great language to get some utility working out real fast. Rails is great for MVP. I fail to understand why people bitch about Ruby/Rails by comparing them to other languages/frameworks.
By @derefr - 5 months
I love using Ruby for shell scripting, but there are also a ton of little nits I have to fix whenever I'm doing it.

For example: Ruby has no built-in for "call a subprocess and convert a nonzero exit status into an exception", ala bash `set -e`. So in many of my Ruby scripts there lives this little helper:

    def system!(*args, **kwargs)
      r = system(*args, **kwargs)
      fail "subprocess failed" unless $?.success?
      r
    end
And I can't ask "is this command installed" in an efficient built-in way, so I end up throwing this one in frequently too (in this instance, whimsically attached to the metaclass of the ENV object):

    class << ENV
      def path
        @path ||= self['PATH'].split(':').map{ |d| Pathname.new(d) }
      end

      def which(cmd)
        cmd = cmd.to_s
        self.path.lazy.map{ |d| d + cmd }.find{ |e| e.file? && e.executable? }
      end
    end
I have other little snippets like this for:

• implicitly logging process-spawns (ala bash `set -x`)

• High-level wrapper methods like `Pathname#readable_when_elevated?` that run elevated through IO.popen(['sudo', ...]) — the same way you'd use `sudo` in a bash script for least-privilege purposes

• Recursive path helpers, e.g. a `Pathname#collapse_tree` method that recursively deletes empty subdirectories, with the option to consider directories that only contain OS errata files like `.DS_Store` "empty" (in other words, what you'd get back from of a git checkout, if you checked in the directory with a sensible .gitignore in play)

...and so forth. It really does end up adding up, to the point that I feel like what I really want is a Ruby-like language or a Ruby-based standalone DSL processor that's been optimized for sysadmin tasks.

By @nunez - 5 months
I agree. Ruby is a _fantastic_ language for getting things done quickly whose credibility was unfairly maligned by Rails.

Unbelievably easy to read, and, with rspec, it is stupid easy to write tests for. No need to fuss with interfaces like you do with Golang; yes, that is the right thing to do, but when you need to ship _now_, it becomes a pain and generates serious boilerplate quickly.

I've switched to Golang for most things these days, as it is a much safer language overall, but when shell scripts get too hard, Ruby's a great language to turn to.

By @nomilk - 5 months
If you never coded in ruby before, but use macOS, you already have ruby installed. Just open terminal and type

  irb
to bring up the ruby interpreter and try out the code in the article.
By @pipeline_peak - 5 months
Being able to call external commands with backticks alone makes it better suited than Python for shell scripting.
By @bdcravens - 5 months
I spend most of my time writing Rails or other backend Ruby, and I prefer my system-level scripts in bash. Philosophically I don't want to have to manage dependencies or broken gems (though inline deps obviate that, and it's not like I've never had to wrestle with apt)
By @blahgeek - 5 months
Perl also satisfies all listed features
By @Alifatisk - 5 months
I wonder why this post blew up so much
By @aorth - 5 months
Side note: Firefox is offering to translate this blog post from Portuguese for me though the content is clearly in English. I noticed the `<html>` element has a `lang="pt"` attribute. The site is generated by Jekyll, which I have not used in years, so I'm wondering if this is a site-level setting or could be overridden in frontmatter...
By @0mwh - 5 months
I've been working (intermittently) on a project to do exactly this: https://github.com/0mWh/ruby-linux-initrd
By @lr4444lr - 5 months
Not denying Ruby is good, but other than process forking, why should I prefer it to Python?
By @JohnMakin - 5 months
To simulate “types” in complex shell scripting I typically involve a lot of json objects acting as my data structures and the file system for a rudimentary database. They’re horrifying ugly, but they tend to work pretty reliably.

This is probably easier.

By @xgdgsc - 5 months
By @fire_lake - 5 months
Nothing about dependencies!

A scripting language needs a way to declare dependencies in a locked-down way, inside of the script that requires them. They must be portable across platforms.

By @kajika91 - 5 months
No pipe (and I mean in parallel too) no love for me.

Nice calling syntax though.

By @corytheboyd - 5 months
Hell yeah! I’ll never forget being fooled by HN that you have to use Perl if you want portable scripts that aren’t bash, writing a whole script with it, and having a coworker politely, yet firmly, tell me that I am dumb and it should just be Ruby… and it was a script I was checking into a Rails app! It’s even trivial to include dependencies with bundler inline https://bundler.io/guides/bundler_in_a_single_file_ruby_scri...
By @greenthrow - 5 months
By definition a shell script is one that is run by the shell. The term should not be overloaded to include scripts that require another interpreter.
By @JohnMakin - 5 months
As a self proclaimed shell advocate I am… intrigued. As a self proclaimed shell advocate, I also have a revulsion to back ticks.
By @floppy-disk - 5 months
While I write most of my scripts in Ruby and enjoy doing so, there is one gripe I have with it: its slow start-up time. On my machine, running an empty Ruby script takes about 100ms, compared to <10ms for Python, Perl, Lua, Bash.

One can mitigate the problem somewhat using the `--disable-gems` flag, but that's not a good general solution.

By @oliviergg - 5 months
For me, the problem with shell scripting is that I do it only occasionally. I’ve always been tempted by higher-level languages. The arrival of tools like ChatGPT has made me much more comfortable writing scripts of intermediate complexity. I find shell scripting more interesting now.
By @globular-toast - 5 months
Cool, I'm sold, added Ruby to my to learn list! I use Python a lot but I don't think it's as good for writing scripts as bash or Perl. Ruby looks like it fits that "better but not much harder" category much better.
By @softwaredoug - 5 months
Shoutout to Hannes Moser at Shopify and our regular pairing on the Disco CLI tool that orchestrated a lot of Elasticsearch config stuff. :)

As a Python guy I found the setup for this sort of CLI too really refreshing!

By @adamnemecek - 5 months
I’ve been enjoying nushell
By @didip - 5 months
No it’s not a great language.

Too many ways to do the same thing.

Not packaged by default on most Linux.

Monkey patching makes things even harder to debug.

By @Demiurge - 5 months
If Rails could be written in Python, I would probably prefer that. Ruby has too much of Perl. If I didn't choose to write a critical piece of software in Perl, which caused me numerous sleepless nights hunting for a missing quote or other weird character, I might have thought it was cool. By my experience with PHP and Perl is why I prefer Python. Clever is not what I ever want a language to be.

Python > Lua > PHP > Ruby > JavaScript > Perl > Bash

By @rednafi - 5 months
Ruby is slow and encourages an esoteric convention over configuration style of OO code.

If you enjoy it, more power to you. However, Python is everyone's second favorite or least favorite language, and it runs laps around Ruby any day. Then there's Go if you need some extra oomph!

By @easylion - 5 months
Java for the win. Have used java for any kind of programming or running shell commands or literally anything since last 5 years professionally and personally.

Language is just a tool, anything and everything works if you love it enough