Modular Computers the manual GitHub

sh

sh: the shell.

Synopsis

sh [-l]
sh -c command [name [arg…]]
sh script [arg…]

Description

sh is mcos’s command interpreter, after POSIX sh. It reads commands, expands their words and runs them. Only shell behavior lives in it: commands are programs (in /bin, found through PATH, or named by a path), and the builtins are only what has to change the shell itself.

With no arguments it reads commands from standard input. When that is a terminal it is interactive: it shows a prompt and reads each line with its line editor (see LINE EDITING). Otherwise it reads lines as they come, with no prompt.

With -c, it runs command and exits; name becomes $0 (else sh) and the args $1, $2, and so on. With a script, it runs the file’s commands (a first line starting with #! is skipped), with $0 the script’s name and the args the positional parameters.

With -l it is a login shell (the console’s, and a remote login’s): before anything else it runs /etc/profile, then goes to $HOME (as login puts a user there). An interactive shell then runs the file the ENV variable names, if there is one (mcos’s /etc/profile sets it to $HOME/.shrc). Each runs as shell commands in the shell itself, as . runs a file, so the variables and aliases they set stay.

There are no if, for, while, case or functions yet: a command line is simple commands joined by the operators below.

Words and quoting

A line is split into words at spaces and tabs. A # at the start of a word starts a comment, to the end of the line.

\c
The character c taken literally. A \ at the end of a line joins the next line to it.
’text’
Everything inside taken literally; no expansions.
”text”
Taken literally but for $ expansions and ` substitutions. Inside, \ quotes only \, ”, $, ` and a new line; before anything else it stays a backslash.
~
A word that is just ~, or starts with ~/, begins with HOME (or / if it isn’t set).

Quoted text is never split into fields or globbed.

Expansion

Each word is expanded in this order: parameters and command substitutions; then field splitting of what they gave, where unquoted; then globs, where unquoted.

Parameters

$NAME, ${NAME}
The variable’s value; nothing if it isn’t set. A name is letters, digits and underscores, not starting with a digit.
$?
The status of the last command.
$$
The shell’s process id.
$!
The process id of the last command started in the background.
$#
How many positional parameters there are.
$@, $*
The positional parameters. Inside double quotes, ”$@” gives each parameter as a field of its own, and ”$*” gives them all as one, joined by the first character of IFS (a space when it isn’t set, nothing when it’s empty).
$0, $1 … $9
The shell’s (or script’s) name, and the positional parameters. Only one digit is read: $10 is $1 and a 0.
${NAME:-word}
word if NAME is unset or empty, else its value. ${NAME-word} does the same only when it’s unset.
${NAME:=word}
As :-, and word is also assigned to NAME. ${NAME=word} only when it’s unset.
${NAME:+word}
word if NAME is set and not empty, else nothing. ${NAME+word} whenever it’s set.
${NAME:?word}
The value; if it is unset or empty, an error instead: sh: NAME: word (or parameter not set), and the command doesn’t run (status 1). A shell that isn’t interactive (a script, or sh -c) exits then, with status 1, as POSIX has it; at the prompt the shell goes on. ${NAME?word} only when it’s unset.
${#NAME}
The length of the value, in bytes.

Anything else in ${…} is a bad substitution: a syntax error.

Command substitution

$(command) and `command` run command in a child shell (sh -c) and are replaced by its output, the new lines at its end dropped. They nest, and set $?.

Field splitting

The result of an unquoted expansion is split into fields at runs of the characters in IFS (space, tab and new line when it isn’t set). An empty IFS splits nothing. Unlike POSIX, every run of IFS characters is one separator, so splitting never makes empty fields.

Globs

After splitting, a field with an unquoted *, ? or [ is a pattern, matched against file names, a path part at a time, and replaced by the names that match, sorted:

*
Any string, including the empty one.
?
Any one character.
[…]
Any one character in the set: characters, ranges (a-z) and classes ([:digit:], [:alpha:], and so on). [!…] or [^…] is any character not in it.

A name starting with a dot matches only a pattern part that starts with a dot. A pattern that matches nothing is kept as it is.

Commands

A simple command is words, with assignments before them and redirections anywhere among them. The first word names the command: an alias, a builtin, or a program (a name with a / is a path; else the first file of that name in the directories of PATH, separated by :).

An alias is replaced as the line is read: when a command’s first word (after any assignments) is an unquoted alias, the alias’s text takes its place in the line and is read there, so it may hold operators (alias l=’ls | head’). An alias isn’t replaced again inside its own text (so it may name the command it stands for), and one whose text ends in a blank makes the next word a command’s first word too. A quoted name (‘ll’) is never an alias. As in POSIX, an alias defined on a line counts from the next line read.

NAME=value
Before a command, sets NAME in that command’s environment only; before a builtin, only while the builtin runs (IFS=: read a b), but for the special builtins (:, ., source, exit, export, set, unset), after which it stays set. Alone, sets the shell’s variable. The value is expanded but not split or globbed. Every shell variable is in the environment of the commands it runs.
command1 | command2
A pipeline: the first’s standard output is the second’s input. Its status is the last command’s.
! pipeline
The pipeline’s status negated: 0 becomes 1, anything else 0. The ! must be a word of its own.
pipeline1 && pipeline2
Runs the second only if the first’s status was 0.
pipeline1 || pipeline2
Runs the second only if the first’s status wasn’t 0.
list ; list
One after the other. A new line does the same; after |, && or || a new line just goes on to the next line.
list &
Runs in the background, without waiting for it, ignoring SIGINT (^C). A list of more than one pipeline (a && b &) runs as a whole in a child shell. At a terminal each process id started is printed as [pid], and before the next prompt each one that has ended is reported as [pid] Done, [pid] Exit status or the signal that ended it. $! is the last one’s id.

A builtin in a pipeline or in the background runs in a child shell, so it can’t change this one (a cd there has no effect here).

Redirection

Redirections are done before the command runs, left to right. A number n right before the operator (no space) names the file descriptor; else it is 0 for <</strong> and 1 for >.

<</strong>file
Reads from file.
>file
Writes to file, made or emptied first.
>>file
Appends to file.
n>&m, n<&m
Makes n a copy of descriptor m (2>&1).
n>&-
Closes n.

The file’s word is expanded, and must give exactly one field (else “ambiguous redirect”). If a redirection fails, the command doesn’t run and its status is 1. A redirection alone (> file) makes or empties the file.

Builtins

cd [- | dir]
Changes the working directory to dir, or to HOME with none. With -, to OLDPWD (printing it). Sets PWD and OLDPWD.
pwd
Prints the working directory.
exit [n]
Exits the shell with status n, or that of the last command. A non-numeric n is an error, and the shell exits with 2.
export [NAME[=value]…]
Sets each NAME given a value. Every variable is exported already, so a bare NAME changes nothing. With no arguments, lists every variable as export NAME=’value’.
unset NAME…
Removes the variables.
set
Lists every variable as NAME=’value’. It takes no options.
. file, source file
Runs the commands in file in this shell (the file is named by its path; PATH isn’t searched). Its status is the last command’s; 1 if the file can’t be read, 2 if none is named.
alias [name[=value]…]
Defines name as standing for value, or with no =, prints its definition. With no arguments, lists every alias. How aliases are replaced is under COMMANDS.
unalias name…
Removes the aliases.
read [-r] [NAME…]
Reads a line from standard input (or its redirection) into the variables, split as POSIX has it: runs of the blanks in IFS separate fields, each other character of IFS ends one (blanks about it and all), and blanks at the line’s ends are dropped; each NAME gets a field, the last the rest of the line. With none, the line goes into REPLY. A backslash keeps the character after it from splitting and is dropped, and one at the line’s end joins the next line to it; with -r, backslashes are just characters. Status 1 at end of file.
type name…
Says what each name is: an alias (and what it stands for), a shell builtin, or a program’s path.
command [-p] name [arg…]
Runs name as a builtin or a program, ignoring aliases. -p changes nothing (the default PATH is the shell’s).
command -v name…
Prints how each name would be run: an alias’s definition, a builtin’s name, or a program’s path; status 1 if any is not found.
command -V name…
Says it in words, as type does.
true, :
Does nothing, with status 0.
false
Does nothing, with status 1.

Prompts

An interactive shell prompts with PS1 (default \w \$ ), and with PS2 (default > ) while a line goes on: an open quote, $( or ${, a trailing |, && or ||, or a trailing \. In the prompts:

\w
The working directory, with ~ for HOME (unless HOME is /).
\W
Its last part.
\$
A $.
\n
A new line.
\\
A backslash.

Line editing

At a terminal the shell reads its lines with readline, which puts the tty in raw mode while a line is typed and back before the command runs. The line can be edited anywhere, across the rows it wraps onto:

Left, Right
Move a character.
Ctrl+Left, Ctrl+Right
Move a word: to the start of the word before the cursor, or the end of the word after it, as bash does (Alt+Left and Alt+Right, and ESC b and ESC f, too).
Home, ^A, End, ^E
Move to the line’s start or end.
Backspace, ^H
Erase the character before the cursor.
Delete, ^D
Erase the character under the cursor. ^D on an empty line is end of input: the shell prints exit and exits.
^U, ^K
Cut the line before or after the cursor.
^W
Cut the word before the cursor.
Up, Down
Go through the history (the last 100 lines).
^L
Clear the screen and draw the line again.
^C
Drop the line, and prompt again.
Tab
Complete the word before the cursor. In a command’s first word, to the builtins, aliases and programs in PATH; in any other word, or one with a /, to file names (directories end in /). One match is put in, followed by a space; several put in what they all start with, and a second Tab lists them.

Keys typed ahead while a command starts are handed to that command. While a command runs, ^C sends SIGINT to it (the shell itself carries on).

Exit status

A command’s status is its program’s exit status, and:

127
The command wasn’t found.
126
It was found but can’t be run (a directory).
2
A syntax error, such as syntax error near unexpected token `|’, or a bad substitution: nothing on that line runs, and a script stops there. Also a builtin’s usage error.
1
A redirection failed, or a builtin failed.
128+n
Signal n ended the command (130 for ^C).

Errors are written to standard error as sh: what: why. A command not found is reported on the command’s own standard error (its redirections are made first), so nosuch 2> /dev/null says nothing.

The shell exits with the status exit gave, or the last command’s; 127 if script can’t be opened.

Environment

PATH
Where commands are looked for.
HOME
For cd, ~ and the prompt.
ENV
A file an interactive shell runs at its start.
IFS
Field separators.
PS1, PS2
The prompts.
PWD, OLDPWD
Set by cd.

Files

/etc/profile
What a login shell runs first: mcos’s sets HOME to /home, ENV to $HOME/.shrc and a few aliases (ll, la, l).
/home/.shrc
What an interactive shell runs next (by ENV): the user’s, empty at first. An update of mcos leaves /home as it is.

Examples

ls /bin | grep s > names
echo "$HOME" ${USER:-nobody}
n=$(ls | head -n 1); echo $n
cat *.lua 2> /dev/null || echo none
sleep 5 &
sh -c 'echo $0 $1' me hi

Notes

No if, for, while, case, functions, here-documents or job control (jobs, fg). set takes no options.

See also

lua(1), stty(1), kill(1), ps(1), time(1)