What is a shell

According to wikipedia, a Unix shell is a command-line interpreter or shell that provides a traditional user interface for the Unix operating system and for Unix-like systems.

Basically it means that there are executables and user should ask them to do everything. For example, to calculate 2 + 2, you could use a tool named calc:

s@yoga:~$ calc 2 + 2 4

Or ruby:

s@yoga:~$ ruby -e 'puts 2 + 2' 4

Or whatever.

But shell is also a programming language. Same 2 + 2 calculation in bash:

s@yoga:~$ echo $((2 + 2)) 4

And it’s important, because significant part of any system is based on bash, perl, python and other scripts.

Why bash is bad schell

First of all,

s@yoga:~$ 2 + 2 2: command not found

Let’s struct what people do in the shell.

First, obvious thing is running programs. cd here, ls there, xdg-open this, vim that.

Unix is designed so that everything is file and it can be somehow interpreted. Good. In fact, things are a bit more complex: file can be read, changed (create/update/delete), or interpreted. And it has access rights for different groups of users.

Second, file system is hierarchical, and you need to access file before doing anything with it.

Ok, so shell should be able to find file in filesystem and operate on it.

Next, people often need to do several commands with same file, and see the result of each of them. I mean

cd some/git/repo vim git pull git add . git commit git push

Shell should allow it easily. Bash handles it with cd and relative paths, but for me it sucks. There must be a better way.

Other people do scripting, where result of one program is passed to another one. They call it pipes, but it’s no more than methods chaining or functions compositon. Plus we have standard output and standard error channels.

Bash is the most ugly programming language I’ve even seen, except some esoteric stuff. But people use it for serious! I never understood that.

Next problem is user-interface. It sucks. In fish shell it’s much better, btw. Ugly completions, antiintuitive behaviour. You can look at fish main page to see where bash fails (and fish doesn’t).

Automating things is crap too. Yes, bash has aliases, and bash has functions, but

why the hell they are not the same?

I should use bash as programming language writing that functions. This is a concentrated pain.

I guess second item is the answer for first.

Why not to use fish / zsh?

You can say yes, bash is crap, but there are a bunch of better shells, for example fish. Why not to use them?

Because they are thinking about backward compatibility with bash. In general this is good, but in this case it prevents people to build really usefull thing. They sacrifice convenience in large to convenience in writing command line flags without quotes. Plus they don’t want to shock people who just moved from bash by alien syntax.

And they are somewhat right. I saw people criticize rush for incompatibility with bash and it affects on their popularity. But it means that I (or anyone else) can build more convenient shell.

And since I know how to do it, I’ll do.

The Plan

ZSH and Fish build their own languages where main goal is to execute executables. Let’s look on it from the other side – let’s take a programming language and create a dsl for standard shell functions.

DSL (or domain-specific language) is a computer language specialized to a particular application domain (c) wikipedia. In another words, it’s a set of commands that high-level enough to solve particular problems easy.

DSL’s can be divided into several types:

standalone (like makefile, grep, sed)

internal (implemented as libraries for programming languages)

embedded (e.g. macro language within spreadsheets)

implementing one specific function, called in runtime (printf of regexp processing)

macro systems (expanded to PL commands in compile time)

As mentioned before, shell essence is a tool that allows:

run executable files

find and open files and folders

work with access rights of files

get output of programs (stdout and stderr)

scripting

Everything but last item can be extracted into some internal DSL. Internal DSLs are most convenient in this very situation, because we need more or less consistent way to both run programs (for example) and do scripting.

Choosing Programming Language

I’m not an intrigue master – the language is Ruby and here is why.

First of all, it’s the best object-oriented language I’ve even knew. Seriously, read some internets (and find that it’s not and smalltalk rules). It’s pure object-oriented, it’s very consistent, it’s simply convenient to write on it.

Second, ruby has the great metaprogramming possibilities. You can mutate everything, choose different bindings, define methods and classes in runtime. You can even extend one class with different modules in runtime! I did it once, and it’s still a shame for me. But when you’re building a REPL it’s extremely convenient.

Third, ruby is an interpreted language, which is almost necessary to build a REPl. Ruby is not unique there, though.

Brackets! Ruby allows you to avoid brackets and call functions like that:

puts "Ruby is awesome!"

It’s not a killer feature, but it’s again very convenient, when you use a shell.

Meet rush

Adam Wiggins created rush in 2008, but then he became too busy to support and develop it and from 2012 it was nearly dead.

I forked it in 2014, cleaned up and added many other things. Good for me. All the changed are in a pull request. Since it’s still not merged, I released my own gem (rush2), you can see details in README.

Let me demonstrate what it can do now.

First of all, it’s just ruby:

λ → Math::sin 3.14159 = 2.65358979335273e-06

Second, it can run executables:

λ → echo "hello" hello = true

Third, it can run executables in directories (better, than bash, IMHO):

λ → opensource['rush/VERSION'].cat 0.8.0

Did you see that opensource? It’s kind of environment variable. Rush allows you to set any functions and constants in config file. For example, my variables look like this:

opensource = home/'opensource' blog = opensource/'s-mage.github.io' config = home/'.rush' documents = home/'Documents/' downloads = home/'Downloads/' pictures = home/'Pictures' # Environmental variables. custom_env = { gopath: home/'go', ruby_bin: home/'.rbenv/shims', matlab: home/'MatLab/bin', toggl: home/'toggldesktop' } ENV['PATH'] = custom_env.values.map(&:full_path).join(':') << ':' << ENV['PATH'] ENV['RAILS_ENV'] = 'development' ENV['RACK_ENV'] = 'development'

And couple of other sort of aliases.

Nice prompt! Again, you can set in in config file:

Rush::Shell.prompt = ' λ → '

Again about config file. You can create plain old ruby methods to extend rush functionality:

class Rush::Dir < Rush::Entry def git(command, *args) case when command == :std && args.size == 1 result = git :pull result << git(:add) result << git(:commit, args.first) result << git(:push) return result when command == :add && args.empty? args << '.' when command == :commit && args.size == 1 args[0] = "'#{args[0]}'" # commit message should be in quotes. args.unshift '-m' when command == :log bash 'git log --graph --pretty=format:"%Cred%h%Creset -%C(yellow)%d%Creset %s %Cgreen(%cr) %C(bold blue)<%an>%Creset" --abbrev-commit' end bash "git #{command} #{args.join(' ')}" end end class Rush::File < Rush::Entry # image to flickr, get link to it. def flickr Rush::Dir.new(ENV['HOME'])['opensource/8398506/2flickr.rb'].open_with :ruby, self.full_path end end

You also can add custom commands to files and dirs. And they are just ruby methods too.

def vim(*args) if self.class == Rush::Dir system "cd #{full_path}; vim +CtrlPMixed" else super end end alias_method :v, :vim

And you can require any gems and use them:

# termit translation commands require 'termit' def termit(from, to, text, opts = {}) opts = { source_lang: from, target_lang: to, text: text }.merge(opts) Termit::Main.new(opts).translate end def desay(text, opts = {}) opts = { source_lang: :de, target_lang: :de, text: text, talk: true }.merge(opts) Termit::Main.new(opts).translate end def deru(text, opts = {}) opts = { source_lang: :de, target_lang: :ru, text: text }.merge(opts) desay text Termit::Main.new(opts).translate end

In short, you can just use ruby like you want! And it’s awesome.

Plus it has autocompletion, nice output, you can chain methods, you can use everything from ruby. In has even source code highlighting, though it’s unnecessary (I’ll write about it in some other post).

This is how it approximately looks:

Summary

Rush is still not perfect, but it’s convenient and it’s for sure much more convenient than bash. I use it every day, develop it and add functions I need. If you want any functions, please write about it in Github or if you fixed anything yourself, make a pull-request. For example, it would be nice to have completion like in fish (with completion from history).

So, as guys from rubinius say, use ruby!