About Shell And Echo

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 -n:

$ 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 using the # 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 scripts maybe.

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 * or '*'? Let’s find out using stat:

$ 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 the filename * 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 files:

$ 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 directory ... A special case is the root directory / in which both, . and .., 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 match for * the shell will hide the dot-files from us. The trick is to use both:

$ 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 echo.

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 -e 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 echo print -n but without a newline:

$ echo -e '-n\c'
-n$

Given what we just learned, what do you think this command does?

$ >>'>' echo **'*'