My first exposure to the UNIX shell was when I was being told that I have to share access to the Internet with my younger siblings. Not keen on turning my computer into a time-sharing station, I had to build my first router. My dad worked in telecommunications and made sure every kid had an own landline phone and a PC in the room. But he wanted us to share a single Internet dial-up to save money. Thanks, Dad, you paved the road! ❤️
Growing up with the Intel 8086, MS-DOS and later Windows I knew nothing about UNIX except that everyone on the still small Internet thought it was the superior operating system.
It is always advised to trust people on the Internet 😉 and so I started installing FreeBSD on a spare machine. And then I built a router. I have absolutely no idea how I made it work. At that time my knowledge about computer networks was practically zero. Internet was still delivered via dial-up lines. On top of that, I had not a clue how the UNIX shell worked. But I made it work. Somehow.
Since that early exposure to the UNIX shell, I occasionally uncover little wonders and
surprises. In this article, I’d like to share a few of these “uhm… what?” moments that I
had when I used
echo and the shell. I’ll be using the Bourne Again Shell on
Ubuntu Bionic for the demos. So it is not really a UNIX shell but close enough.
Let’s first look at
echo and how it is invoked by the shell:
$ echo $
If we don’t provide an argument, it prints a newline. We can suppress the newline by adding
$ echo -n $
That is a feature of
echo, not the shell, but we will come back to this later.
Let’s look at a popular shell feature now: Comments. We can add a comment to a command by
# symbol. Comments will not be interpreted and are not part of the
arguments that a binary is called with.
$ echo foo # bar foo $ echo foo #bar foo
The string bar is never echoed because it is part of a comment. There is one important thing to notice: A comment must be a word. It cannot appear in the middle of another word. If it does, it is not a comment anymore:
$ echo foo#bar foo#bar $ echo foo# bar foo# bar
Besides comments, there is more pre-processing the shell can do for us. We all know about the glob patterns, right? The patterns are applied to all files in a directory and save us a lot of time typing file names.
Let’s try that in an empty directory:
$ echo * *
Uhm… wait? Isn’t the asterisk supposed to be replaced with the file names in that
directory? Well, it is. But if there are no files to match, the shell keeps the asterisk
and hands it over to
echo. I find this surprising. This could be a problem in
Ok, let’s add a file named
* to that directory and see the difference:
$ touch '*' $ echo * *
🧐 I can’t tell the difference. Can you? So how do we know if this is an actual filename or
just the asterisk? Let’s check with
ls if that file really exists:
$ ls '*'
It does. And the filename is…? Is the file named
find out using
$ stat '*' File: * Size: 0 Blocks: 0 IO Block: 4096 regular empty file Device: 801h/2049d Inode: 3147147 Links: 1 ✂️
The filename is
ls is just friendly enough to quote the name,
that is why it appears as
'*'. So if we call
echo * we should see
* and not the asterisk
*. We can prove that the
shell does the globbing by adding another file to the directory and see if we get both
$ touch hello $ echo * * hello
Yeah, all the files are there. All the files? Well, not really. There is a convention that
the shell ignores files that start with a
. when matching the glob pattern. In
every directory, there is a self-link named
. and a link to the parent
... A special case is the root directory
/ in which
.., are self-links. So how do we get all the files
now? If we match for files that start with a dot, we do not get the other files. If we
* the shell will hide the dot-files from us. The trick is to use
$ echo .* * . .. * hello
The shell expands both patterns for us. The first one matched the dot-files only, the
second one all files but the dot files. All results are passed to
But enough about glob patterns. Remember the
-n option from earlier? Let’s say
we want to echo -n. How would we do that?
$ echo -n $
😕 Clearly, that does not work. How about this?
$ echo "-n" $
🙁 Nope. And this?
$ echo '-n' $
😣 Nada. Nein. Njet. But why? The shell is processing the words and handling them to
echo afterward. It does not make a difference if we quote them.
echo always sees -n and thinks it is a command line option. So, how
about using some force?
$ echo -n -n $
😫 Impossible! Now
echo thinks we are a bit out of sync by passing the same
option twice. Forgivable as it is, it ignores one of the options. But hey, I remember
something about the double dash in bash! We can use it to mark the end of a
parameter list. Let’s give that a shot:
$ echo -- -n -- -n
😤 So close. But still not there. The shell won’t help us here. Luckily, the authors of
echo have built something in that we can leverage. Using the
option we can treat the input as an expression. Let me quickly show you why using an
expression alone will not save us:
$ echo -e "-n" $
echo still thinks we are passing an argument. However, if we make this
argument not look like an option,
echo will think of it as a string.
$ echo -e "-n\n" -n $
🤨 Almost there! Now we have the output we want. And an extra newline. By treating the
string as expression, we unlocked the special control character
\c. It can be
used to indicate that we want to stop processing at the point where it appears in a string.
Let’s combine this:
$ echo -e '-n\n\c' -n $
🤩 Hooray! We made it. We can even apply our new knowledge to make
-n but without a newline:
$ echo -e '-n\c' -n$
Given what we just learned, what do you think this command does?
$ >>'>' echo **'*'