Skip to content
Lesson 14 of 16

Step 1 of 5 · Reading · ~3 min

Read

Production Concerns

History Expansion

After this lesson your shell keeps a numbered list of the commands you ran and lets you reuse them with !!, !N, !-N, !$, !*, !string and ^old^new. It shows the line it actually ran, prints the list when asked, and tells you when an event does not exist. The one mechanism behind all of them is a rewrite of the typed line before the tokenizer sees it.

History runs before everything else

History expansion is the very first step of the pipeline, ahead of tokenizing. The shell scans the raw line for an unquoted !, replaces each designator with text from an earlier command, and then treats the rewritten line as if you had typed it. That is why sudo !! works: the !! is replaced by the whole previous command and the result is sudo followed by it. A rewritten line is also what goes into the history, so a later !! repeats the real command, not the shorthand.

The designators

TypedReplaced by
!!the previous command
!Ncommand number N, counting from 1 as history shows them
!-Nthe command N back; !-1 is the same as !!
!$the last word of the previous command
!*all words of the previous command except the first, separated by single spaces
!stringthe most recent command that begins with string
^old^newthe previous command with the first old replaced by new (the closing caret is optional; text after it is appended)

A name ends at a space, a tab or one of ; & | ( ) < > ' ". A ! followed by a space, a tab, =, ( or the end of the line is an ordinary character, so echo wow! and [ a != b ] work.

Quoting

Single quotes switch history expansion off completely: echo 'hi!!' prints the two bangs. A backslash protects the next character. Double quotes do not protect a !, which surprises people: echo "hello!!" expands. Expansion applies to what you typed, so the quote marks stay in the rewritten line.

Failure

If the event does not exist (a number out of range, nothing starts with the string, or the history is empty) the shell prints bash: <the designator>: event not found, runs nothing and records nothing. A ^old^new whose old text is not in the previous command prints bash: <the line>: substitution failed.

The history list and the history command

Every command that is executed is recorded after expansion, in order, numbered from 1. The history command prints the list as the number right-aligned in five columns, two spaces, then the command. It is recorded before it runs, so the listing ends with itself. history -c clears the list, and numbering starts again at 1. A line that fails expansion is not recorded; blank lines are ignored.

A worked trace

History is make test, make install. Typing !ma finds the most recent command starting with "ma", which is make install, and runs it as entry 3. Typing !make t is not what you would expect: the string ends at the space, so it looks for a command starting with "make", finds the newest, and the t is appended. The result is make install t.

Common mistakes

  • Treating ! as special inside single quotes.
  • Searching !string oldest-first instead of newest-first.
  • Recording the line as typed instead of the expanded line.
  • Counting !N from zero, or counting !-N from the wrong end.

Your exercise

The starter reads lines, calls your expander, records and runs commands, and prints the history listing. You write the expansion function. The most likely mistake is expanding inside single quotes.

Up nextCapstone: The Whole ShellProduction Concerns

Discussion

Ask a question, share an insight, or help someone who’s stuck.

Sign in to post a comment or reply.

Loading…