"When you type to UNIX, a gnome deep in the system
is gathering your characters and saving them in a secret place.
The characters will not be given to a program until you
type return (or new-line), as described above in Logging in."
Though foreign to most users, the text-terminal is the original
interface for interactive computing.
As Dr. Brian Kerninghan put it, this interface forms a language unto itself,
with files serving as nouns and commands
as verbs.[1]
Here we explore the theory behind this interface, and offer a glimpse
of its power and utility.
For a tutorial introduction, see
Effective Shell by Dave Kerr or The Linux Command Line for Beginners by Canonical.
Terminals
and Terminal Emulators
A terminal is a computer peripheral similar to a typewriter.
The earliest examples printed output onto paper.
These terminals were produced well into the 1970's, and later
electronic typewriters often supported terminal mode,
allowing them to interface with computers via serial cable.
Ken Thompson (sitting) and Dennis Ritchie at PDP-11, in Bell Labs' Unix Room[1]
As technology developed and interactive computing became more popular,
hard-copy terminals were phased out in favor of
screen-based entities.
The resulting devices consisted of a monitor and keyboard;
unlike modern monitors, they could only display text.
Within Unix-like operating systems, terminals are represented by
tty device files.
Terminal Emulators
Terminals persist today in the form of
terminal emulators.
A terminal emulator is a program that runs within the context of a desktop
environment and provides the functionality of a physical terminal.
On-screen, it looks sorta like:
Extraterm - The Swiss Army Chainsaw of Terminal Emulators
These emulators are "dumb" in the sense that they do nothing except
display text and return text.
Per tradition, their input and output is handled through device files.
tty(1) - Print the file name of the terminal connected to standard input
Shells
A shell is a program which interprets and executes our (arbitrary) commands.
It is a "shell" in the sense that it is the outermost layer of the
operating system, with which the user has direct
contact.[2]
The Unix architecture entertains the shell as an ordinary user-space process.
This corresponds directly to, Find and execute command,
and pass it arguments [arg ...].
There are a few places the shell looks for the command being executed.
In order:
Aside:
Two tools to identify commands are
type and which.
User-defined functions - A way to group commands
Shell builtin commands - Commands hard-coded into the shell
External programs - Commands that exist as separate programs
Nearly all commands fall into the third category,
external programs.
For example, ls is an external program, and
can be found in /usr/bin.
Arguments
Commands are the first part of a "typical line of input";
the other is arguments.
Like commands, arguments are supplied by the user.
They are passed to each function, builtin, or program as it begins
execution, and it is up to that command to interpret them.
For example, echo responds to arguments
very simply— It prints them back out:
$ echo one two three four
one two three four
Common arguments include:
$ command --help
which prints a terse help message; and,
$ command --version
which prints the version number of the software.
See a command's manual page for
more documentation.
Recall that each process has a
current working directory.
Since the shell is a process, it too has a current working directory.
At the command line, this is of particular importance:
It represents where you are in the directory tree.
Aside:
Relative file names are resolved with respect to
the current working directory.
The shell's current working directory may be printed with
the print working directory command,
pwd:
$ pwd
/home/josh
And we can change its value with the change directories command, cd:
$ cd Public
$ pwd
/home/josh/Public
Since each directory refers to its parent directory as ..,
we can always move up the directory tree with,
$ cd ..
$ pwd
/home/josh
Standard File Descriptors
The shell is connected to its terminal by three open files,
known as the standard files:
STDIN the terminal keyboard, at file descriptor 0
STDOUT the terminal screen, buffered, at file descriptor 1
STDERR the terminal screen, unbuffered, at file descriptor 2
Since open files are preserved across both fork()
and exec(),
each command inherits our keyboard and terminal screen
at these file descriptors.
For example, the program /usr/bin/ls,
which lists directory contents,
prints to STDOUT, and our command
$ ls
runs that program in the context of the terminal.
Redirection
and Pipelines
One of the features that makes the Unix command-line interface
exceptionally powerful is the ability to direct output away from
the terminal.
Since the terminal is represented as a collection of files,
the operations on it and any other file are identical.
Thus, we can replace the TTY file a command is going to write to
with a regular file, and capture the command's output in it:
$ lscpu > cpu_stats
This is called redirection,
and here we are redirecting STDOUT to the file
cpu_stats (the file is created if it does not exist).
STDERR has not been changed, and therefore the terminal
remains the destination for any error messages.
Redirection operators <, >, and
2> redirect STDIN,
STDOUT, and STDERR, respectively.
Redirections are interpreted by the shell before the command
executes, and are not passed as arguments to the command.
Pipelines
We have a few other tricks up our sleeve:
The pipe operator, |, allows us to direct
the output (STDOUT) of one command to the
input (STDIN) of another.
The result is called a pipeline, and allows us to
manipulate program output as a stream of text.
For example, Section 7 of the manual pages contains many
interesting articles, and we can get a single line summary
of each using apropos:
$ apropos -s 7 '.*'
bpf-helpers (7) - list of eBPF helper functions
gnupg (7) - The GNU Privacy Guard suite of programs
RAND (7ssl) - the OpenSSL random generator
Standards (7) - X Window System Standards and Specifications
UPower (7) - System-wide Power Management
utf-8 (7) - an ASCII compatible multibyte Unicode encoding
address_families (7) - socket address families (domains)
aio (7) - POSIX asynchronous I/O overview
armscii-8 (7) - Armenian character set encoded in octal, decimal, and ...
arp (7) - Linux ARP kernel module.
[...]
By piping this output into the sort command,
we can alphabatize the results, forming something closer to an index:
$ apropos -s 7 '.*' | sort
address_families (7) - socket address families (domains)
aio (7) - POSIX asynchronous I/O overview
armscii-8 (7) - Armenian character set encoded in octal, decimal, and ...
arp (7) - Linux ARP kernel module.
ascii (7) - ASCII character set encoded in octal, decimal, and hex...
asymmetric-key (7) - Kernel key type for holding asymmetric keys
attributes (7) - POSIX safety concepts
audit.rules (7) - a set of rules loaded in the kernel audit system
backend (7) - cups backend transmission interfaces
bio (7ssl) - Basic I/O abstraction
[...]
We can continue in this fashion:
If we only want the first few lines, we can pipe the results into head:
$ apropos -s 7 '.*' | sort | head
If we want only the last few lines, we can use tail:
$ apropos -s 7 '.*' | sort | tail
And if we want to page through the results, we can pipe into less:
Scripts must be stored as executable files.
One can change a file's mode bits using
chmod:
$ chmod +x file
The last trick up our sleeve is saving a sequence of commands
into a file, then executing the file as a program.
Such programs are called scripts, and lend the
command-line a great deal of power: Rather than repeating
ourselves, we can simply save commands into a file, then
execute the file.
The first line of a script must begin with the characters
#!.
This line is referred to as shebang,
and designates the program that will interpret the script.
For example, if we had a file named hello
with the following contents,
#!/usr/bin/sh
echo Hello World!
we could execute it as an external program with,
Aside:
Here we are using ./ to execute a program in the
current working directory.
The earliest versions of UNIX entertained exactly two processes:
One for each of two terminals connected to the machine.[3]
The shell,
sh,
existed as a user-space process, but fork() had not been introduced.
To execute a command, the shell replaced itself with
the requested program;
the program, on calling exit(), replaced itself with the
shell once again.[3]
In particular, change directories was implemented
as a separate command.
When invoked, it inherited the shell's current working directory,
changed it, and then the shell inherited the resulting working directory.
With the inclusion of the fork() system call, though,
the chdir command broke:
"There was much reading of code and anxious introspection about how
the addition of fork could have broken the chdir call. Finally the
truth dawned: in the old system chdir was an ordinary command; it
adjusted the current directory of the (unique) process attached to
the terminal. Under the new system, the chdir command correctly
changed the current directory of the process created to execute it,
but this process promptly terminated and had no effect whatsoever on
its parent shell! It was necessary to make chdir a special command,
executed internally within the shell. It turns out that several
command-like functions have the same property, for example login."[3]