moosh: the Moo Shell

by benjamin@tty1.blog at 2023-09-13T00:58:02.000Z

As a joke, this week I coded a Linux shell: moosh. The whole idea behind moosh is that it responds to every command with a random-length "MooOooOOo…"

So, yes, it's a joke. But let's take a look at it and see if there are any ideas about scripting we can learn along the way.

The Code

The code is pretty short, written in bash, so here is the full code:

#!/usr/bin/env bash

while true; do
	printf "$ "
	read input
	if [[ "$input" ]]; then
		echo -n "Mo"
		seq 1 $(($RANDOM % 15 + 1)) | while read n; do
			if [ "$(($RANDOM % 2))" = "0" ]; then
				echo -n "o"
			else
				echo -n "O"
			fi
		done
		echo "…"
	fi
done

We'll break this down, piece by piece.

Part 1: The Shebang

#!/usr/bin/env bash

The first line of all shell scripts follows this form. Called the shebang, it tells your system what program to use to run the file. In this case we want to use bash, but we don't have a guarantee that the bash executable will be in the same location, so pointing to a location like /bin/bash might not work on all systems.

To solve this dilemma, we can use /usr/bin/env to find the location of any executable. Another common use is to create a python script; to do so, you could use this shebang:

#!/usr/bin/env python

Before we move on, I should also cover /bin/sh.

#!/bin/sh

On a Linux (or similar) system, /bin/sh will point to a POSIX-compatible shell. On many systems, this is bash, but not all, so for greater compatibility you shouldn't use /bin/sh if you're using bash-specific features. For example, on my system I use dash as my /bin/sh, which is more minimal, but more performant.

I would usually use /bin/sh for scripting, but in this case I wanted to use the $RANDOM variable, which isn't present in all shells, so I did /usr/bin/env bash instead.

Part 2: The Infinite Loop

while true; do
    # …
done

Pretty self-explanatory what this does: while the true command succeeds (which will always be the case), the action inside the loop will be executed, effectively executing repeatedly. We're using this to keep prompting for new commands. Now, there are a couple ways, still, to break this loop:

  1. Run the break command from the script; this exits the loop. Often people don't know how long a loop will run or exactly what condition will end it, so they'll create an infinite loop and just call break when they're done with it.
  2. Hit ^C (ctrl + c) while the script is running. This is the approach we're expecting users to take; when they're done with moosh, they can just hit ^C and exit.

Now, let's look at what will happen endlessly inside this loop.

Part 3: Taking Input

printf "$ "
read input
if [[ "$input" ]]; then
    # …
fi

First, we want a shell prompt. We don't need anything fancy (since people can't actually do anything in this shell), so we'll just use $ as our prompt.

printf "$ "

By using printf with no arguments, we're printing out the string without a line break, so user input will happen on the same line.

Now, we want to take the user's command into a variable, input. We can use the read command for that.

read input

We don't actually care what the input is, since we're going to do the same thing regardless. But we do want to make sure there is input; we can use an if statement to check that the user did input something, and if not skip responding.

if [[ "$input" ]]; then
    # …
fi

Part 4: The Response

Now that we know the user has inputted something, we can respond!

echo -n "Mo"
seq 1 $(($RANDOM % 15 + 1)) | while read n; do
    if [ "$(($RANDOM % 2))" = "0" ]; then
        echo -n "o"
    else
        echo -n "O"
    fi
done
echo "…"

Let's start with the beginning and ending echo statements.

echo -n "Mo"
# …
echo "…"

Here, we start the line with "Mo" and end it with "…". Between these two segments, we'll be outputting a random number of "o"s.

seq 1 $(($RANDOM % 15 + 1)) | while read n; do
    # …
done

Above, we see one of my favorite tricks: piping a command's output to a while loop. That lets us execute an action for each line; in this case the line will be stored in the n variable. Now, what's going on with the command we're passing to the loop?

And, finally, we get to output the "o"s. We're inside the loop, now, which means whatever commands we put will be executed from 1 to 15 times. We could simply just do this:

echo -n "o"

But, to have a little more fun, let's randomly use either an o or an O to add more character to our moos.

if [ "$(($RANDOM % 2))" = "0" ]; then
    echo -n "o"
else
    echo -n "O"
fi

Most of this is pretty straightforward. If $(($RANDOM % 2)) is 0, output an "o", if not, a "O".

Now, what is $(($RANDOM % 2))? Well, let's think back to our last use of $RANDOM. $RANDOM % 2 represents the remainder of some random integer divided by two. The only possible outputs are 0 (it divides evenly by 2) and 1 (it's an odd number, with one remainder), effectively running the first command half the time and the second command for the other half.

Wow, that's everything.

Putting it All Together

Let's take another look at the completed script, shall we?

#!/usr/bin/env bash

while true; do
	printf "$ "
	read input
	if [[ "$input" ]]; then
		echo -n "Mo"
		seq 1 $(($RANDOM % 15 + 1)) | while read n; do
			if [ "$(($RANDOM % 2))" = "0" ]; then
				echo -n "o"
			else
				echo -n "O"
			fi
		done
		echo "…"
	fi
done

In this script we:

Let's try it! Put the code into a file (I use one named moosh), and run it:

$ ls
moosh
$ ./moosh
bash: ./moosh: Permission denied

Uh oh, what happened?

The answer is simple: we haven't told your OS that ./moosh is a file you can execute. Now, you could work around this by passing the file to bash directly (bash ./moosh), but there's a better way.

$ chmod +x ./moosh

This gives us permission to execute the file.

benjamin@tty1.blog ~] $ ./moosh
$ █

Hooray! moosh has launched! Let's try some commands:

$ ping tty1.blog
MooooooooOOoOooOo…
$ echo "This is the Moo Shell"
MooOOO…
$ cat ./moosh
MoOoOooOoO…

It works! Give yourself a pat on the back. 🎉

Conclusion

I hope that, as silly as this project was, walking through it step by step helped you get a picture of how bash scripting works. I'll likely point to this as an example in the future so that I can go in a little less detail if we walk our way through scripts again.

I had fun, I hope you did too. I'm always learning myself and am open to feedback on moosh and how I could have made it better or feedback on my explanation in this article; if you have comments or concerns, I'd be gratified if you'd leave a reply down below.

$ say-goodbye
MoooOoooOoO…

Benjamin Hollon

benjamin@tty1.blog

Benjamin Hollon is a writer and citizen of the world with far too many hobbies. Besides writing for his blogs, he codes, plays and composes music, writes poetry and fiction, and more. He is currently studying Communications and Professional Writing at Texas A&M.

He is active on Mastodon.

Email Public Key


Comments

Comment via Fediverse



Liked what you read?

What's next?