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:
- 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 callbreak
when they're done with it. - 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?
seq 1 …
- theseq
command outputs all the numbers between and including the two we specify. We start at1
and continue to the result of the tricky bit of this line, which will essentially spit out a random number between 1 and 15. By piping this towhile
, we'll be executing the contents ofwhile
a random number of times between 1 and 15.$((…))
- the double parentheses let us evaluate a mathematical expression as our argument. For example,$((5 + 3))
would put8
in its place.$RANDOM % 15 + 1
-$RANDOM
is a special variable that'll return a random number.%
is an operator that returns the remainder of the first number when divided by the second. This'll in effect get us a random integer from 0 to 14; we then add 1 to get a random number from 1 to 15 (which doesn't matter in this case, but I wanted to illustrate the principle).
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:
- Establish the interpreter as
bash
, using/usr/bin/env
to find the path tobash
- Enter an infinite loop
- Take user input, with
$
as a prompt - Output a random number of "o"s—half capital and half lowercase—between
Mo
and…
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…
Comment via Fediverse