One of the most powerful features of Unix and Linux is that using traditional command line tools, everything is a stream of bytes. Granted, modern software has blurred this a bit, but at the command line, everything is text with certain loose conventions about what separates fields and records. This lets you do things like take a directory listing, sort it, remove the duplicates, and compare it to another directory listing. But what if the shell understood more data types other than streams? You might argue it would make some things better and some things worse, but you don’t have to guess, you can install cosh, a shell that provides tools to produce and work with structured data types.

The system is written with Rust, so you will need Rust setup to compile it. For most distributions, that’s just a package install (rust-all in Ubuntu-like distros, for example). Once you have it running, you’ll have a few new things to learn compared to other shells you’ve used.

Examples

A good way to get a quick flavor of the shell’s idiosyncracies is to contrast it with the usual shell syntax. The Github page has several good examples:

  • Find files matching a path, and search them for data:

sh: find . -iname ‘*test*’ -print0 | xargs -0 grep data
cosh: lsr; [test m] grep; [f<; [data m] grep] map

  • Find the total size of all files in the current directory:

sh: ls | xargs stat -c %s | awk ‘{s+=$1} END {print s}’ –
cosh: ls; [is-dir; not] grep; [stat; size get] map; sum

  • Get the second and third columns from each row of a CSV file:

sh: cut -d, -f2,3 test-data/csv
cosh: test-data/csv f<; [chomp; , split; (1 2) get] map

  • Sort files by modification time:

sh: ls -tr
cosh: ls; [[stat; mtime get] 2 apply; <=>] sortp

As you can see, sometimes commands are a little longer, but presumably, there is less to remember, and it is a bit more self-documenting.

But Why?

The key idea is that this shell understands multiple data types. In particular, it can deal with hash maps, sets, and lists. Basic items include booleans, integers (32-bit or of arbitrary size), floats, and strings.

The input prompt is more like a command prompt for a programming language. In fact, Forth programmers will appreciate the RPN capabilities:

/tmp/cosh$ 5 3 /
1
/tmp/cosh$ 5.0 3 /
1.6666666666666667
/tmp/cosh$

Storing into variables is similar to Forth, too, using ! and @ with the RPN-style notation. In fact, it all looks like Forth from swap and drop to the way if controls conditionals. However, the stack doesn’t exist between lines. So the above examples do not leave the result on the stack.

The documentation on Github is good, but there are a few things you’ll have to work out. For example, the string function ++ is documented, but the example uses the word append, which doesn’t seem to work.

/tmp/cosh$ hacka day ++
hackaday
/tmp/cosh$ hacka day append
hacka
day
append

Commands and Regular Expressions

Most shell commands exist in cosh, too, but not necessarily as external tools. Some have aliases, too. For example, you can use mv, but you can also use rename. Everything is, of course, using the RPN format for arguments.

If you want an external command, you need to prefix it with a dollar sign or, in an interactive shell, you can use a space if you prefer. For example, if you run ls, you’ll get the cosh version of ls. If you run $ls, that’s the actual ls command you expect.  If you put the external name in braces, what is returned is a generator that allows you to walk through the output.

What’s a shell without regular expressions? With cosh, you have an “m” expression that tells you if you have a match or a “c” condition that returns captures from the expression. There are also “s” expressions for search and replace. You can also add flags to allow different options like case insensitivity.

I found the capture part confusing. You’d think it would provide a list of things matched in parenthesis, but either it doesn’t or I couldn’t find the right syntax to make it do so. For example:

/tmp/cosh$ name=al "name=(.*)$" c
(
   0: name=al
)
/tmp/cosh$ name=al,name=jim "name=([a-zA-z]+)/g" c
(
   0: name=al
   1: name=jim
)

The documentation shows some examples of this that don’t work exactly right, too. For example, try this from the documentation:

/tmp/cosh$ asdf "(..)" c

To get the result the document shows, you need the /g flag on the regular expression. Otherwise, only the first match appears.

Parsing

One big feature of cosh is that it can parse json and XML. It can also write out files in that format. We’d love to see a proper CSV parser, although that’s a little easier to handle directly with cosh primitives than an XML file.

For example, if you want the 3rd and 4th fields from a CSV file, you can read it and use the split and get functions in a map:

/tmp/cosh$ test.csv f<; [chomp; , split; (3 4) get] map

Of course, that’s not going to handle things like quoted values, but that’s typically a problem in other simple shell scripts, too.

Working with json is easy. For example, if you want to find the fields that match a regular expression, you can do that:

file.json f<; from-json; keys; [.{4} m] grep;
v[gen (
0: asdf
1: qwer
2: tyui
3: zxcv
)]  # from the official examples

Winner?

Will we start using cosh every day? Honestly, probably not. But it is interesting. We might keep it in our back pocket for writing scripts that need to process json or XML.

We like the Forth syntax, but not everyone will. We also like the data typing. But as a general-purpose shell, it seems to leave something to be desired.

Of course, what we really like is Linux gives you choices. If you like cosh, knock yourself out. Don’t like it? Pick another shell or — if you are feeling brave — write you own. The world is your oyster.

We couldn’t help but think of the database-like Crush shell while playing with cosh. Then here’s cat9, which is a strange shell, indeed. There are, too, some more mainstream alternative shells like zsh and fish.