2.7. Advanced shell commands#
Now that youâve learned how to run basic commands, itâs time to go a bit deeper into one of the basic elements of Unix : redirection and pipes.
You will have noticed that most Unix programs, such as date, cal, find etc. have ASCII text as output. Normally this output, called the standard output, is written to the screen.
Standard output is one of the three standard streams that most Unix commands work with. These three are:
Standard input (a.k.a.
stdin): used by programs to read input from.Standard output (
stdout): used by programs to write the regular output to.Standard error (
stderr): used by programs to write error messages to.
By default, when you run a program in the shell, the input and output streams are connected to the terminal.
So, any output or errors generated by the program will be shown in the terminal, as you have seen in all the previous exercises of this manual.
Many text processing programs that youâve seen, such as grep, cat, wc, etc., can also read input from an input stream if they are not given a file path as argument to work on.
To illustrate the most basic setup of a program taking input from stdin, doing some processing, and producing output on stdout, we can take a look at cat.
In its simplest form this command reads all input from stdin and does no processing, but simply copies the input to stdout.
Exercise 2.146
Run plain cat, with no filename argument at all. Type a line of text, then press Ctrl-D to signal the end of your input.
Without a filename, cat reads from stdin instead â in this case, whatever you type at the terminal. Notice that cat still writes to stdout exactly as it would with a file argument: since nothing here is redirected, stdout is still connected to the terminal screen, so cat simply repeats back the text you typed.
(In this browser-based terminal, what you have typed will only appear after you press Ctrl-D. In a real terminal, cat processes the input line by line, and it would repeat each of your lines immediately after you pressed Enter. More on that later.)
{
"checks": [
{ "type": "commandSucceeded", "pattern": "^cat\\s*$",
"desc": "should run cat with no filename argument at all" },
{ "type": "commandEvent", "eventType": "coreutil.cat",
"desc": "should use cat" }
]
}
Youâve probably already seen commands generate error messages before throughout this manual and its exercises.
Nevertheless, for completeness, letâs also use cat to illustrate how messages appear on the stderr error stream.
Exercise 2.147
First run ls to verify your home directory is empty. Then run cat nofile.txt, a file that doesnât exist.
cat generates an error message that the file doesnât exist, written to the stderr stream. Notice that this error is still shown on the terminal, even though it is not the content of nofile.txt that we would expect from cat on stdout.
Of course, stderr is by default also connected to the terminal.
{
"filesystem": { },
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.ls",
"desc": "should first run ls to check the directory is empty" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/nofile.txt"] },
"desc": "should try to cat nofile.txt" },
{ "type": "stderrContains", "text": "No such file or directory", "pattern": "^cat\\s+nofile\\.txt",
"desc": "cat nofile.txt should print an error to stderr, since the file doesn't exist" }
]
}
2.7.1. Redirecting standard output#
Sometimes you donât want to see the output of a command in the terminal, but you want to save it in a file.
You could try to select the terminal output with the mouse, copy it, and then paste it into an opened text editor.
However, the bash shell provides a much easier method using the > redirection symbol after a command, combined with a target file name.
This instructs bash to save the text from the commandâs standard output stream in the given file path.
For example:
$ date > the_date.txt
We will say the standard output of date has been redirected to the file the_date.txt.
Exercise 2.148
Try the to redirect the output of date into a file called the_date.txt,
and verify with cat that this file was indeed created and contains the output of the date command.
{
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.date",
"desc": "should run date" },
{ "type": "commandEvent", "eventType": "redirect.write", "match": { "absPath": "/home/student/the_date.txt" },
"desc": "should redirect the output of date into the_date.txt with >" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/the_date.txt"] },
"desc": "should inspect the_date.txt with cat" }
]
}
In the example above with the > redirection, if file the_date.txt already exists, its content would be completely replaced by the output of the date command.
Sometimes, you instead want to append text to a pre-existing file, extending the fileâs existing content.
In that case, use the >> redirection symbol.
For example, the following would ensure the file journal.txt has two lines:
$ echo The current date is: > journal.txt
$ date >> journal.txt
Exercise 2.149
Run date >> date_list.txt. Then repeat the exact same command two more times, but instead of retyping it, use !! to re-run the previous command. Afterwards, date_list.txt should contain three lines, one per invocation.
Afterwards, inspect date_list.txt with cat to confirm it contains the content of multiple lines.
{
"checks": [
{ "type": "commandEvent", "eventType": "history.expansion",
"desc": "should use !! to re-run the previous command, instead of retyping it" },
{ "type": "commandEvent", "eventType": "redirect.append", "match": { "absPath": "/home/student/date_list.txt" },
"desc": "should append into date_list.txt with >>, not overwrite it with >" },
{ "type": "fileMatches", "path": "/home/student/date_list.txt", "pattern": "^(.+\\n){3}",
"desc": "date_list.txt should end up with (at least) three lines, one per date invocation" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/date_list.txt"] },
"desc": "should inspect date_list.txt with cat" }
]
}
Exercise 2.150
Suppose date_list.txt does not exist yet. What happens if you run date >> date_list.txt?
It fails, since
>>requires the file to already exist.It creates
date_list.txtand writes the date into it. For a brand new file,>>behaves exactly like>.Nothing happens, since there is nothing to append to yet.
It creates an empty
date_list.txt, but the date output is discarded.
Exercise 2.151
Run the following three commands, one after another:
$ echo "first line" > README.txt
$ echo "second line" >> README.txt
$ echo "third line" > README.txt
Before you run the last one, guess what README.txt will contain afterwards.
Then check it with less or cat.
{
"checks": [
{ "type": "commandEvent", "eventType": "redirect.write", "match": { "absPath": "/home/student/README.txt" },
"desc": "should use > at least once (creating, then later overwriting, README.txt)" },
{ "type": "commandEvent", "eventType": "redirect.append", "match": { "absPath": "/home/student/README.txt" },
"desc": "should use >> at least once (appending to README.txt)" },
{ "type": "fileContains", "path": "/home/student/README.txt", "text": "third line",
"desc": "README.txt should contain the last line written with >" },
{ "type": "fileContains", "path": "/home/student/README.txt", "text": "first line", "negate": true,
"desc": "the final > should have erased the earlier content, including \"first line\"" },
{ "type": "fileContains", "path": "/home/student/README.txt", "text": "second line", "negate": true,
"desc": "the final > should have erased the earlier content, including \"second line\"" }
]
}
Exercise 2.152
The file notes.txt already contains some old text. Using only echo and redirection (no mv, rm, or nano) overwrite its content so it ends up containing exactly three lines, in this order: line one, line two, line three. None of the original content should remain. Youâll need to combine > (to start fresh) and >> (to add the rest).
{
"filesystem": { "/home/student/notes.txt": "This is some old junk text.\nDelete me please.\n" },
"checks": [
{ "type": "commandEvent", "eventType": "redirect.write", "match": { "absPath": "/home/student/notes.txt" },
"desc": "should use > at least once, to overwrite the old content" },
{ "type": "commandEvent", "eventType": "redirect.append", "match": { "absPath": "/home/student/notes.txt" },
"desc": "should use >> at least once, to add the remaining lines" },
{ "type": "commandRan", "pattern": "^mv\\b", "negate": true,
"desc": "should not use mv" },
{ "type": "commandRan", "pattern": "^rm\\b", "negate": true,
"desc": "should not use rm" },
{ "type": "commandRan", "pattern": "^nano\\b", "negate": true,
"desc": "should not use nano" },
{ "type": "fileMatches", "path": "/home/student/notes.txt", "pattern": "^line one\\nline two\\nline three\\n$",
"desc": "notes.txt should contain exactly the three given lines, in order, with none of the original content left" }
]
}
2.7.2. Redirecting standard error#
If you donât specify anything, any error messages on the standard output stream will also be written to the screen, even if you redirect the standard output somewhere else. This is intentional: if you want to capture the output of a program in a file, then you often do not want any error or warning messages intermixed with the output.
However, there are also cases where you do want to redirect the standard error to a file, for example when you want to errors into a separate error log file.
In such cases, you can also instruct bash to redirect the standard error to a file using 2> (the â2â here refers to the 2nd output stream, i.e. the error stream.
For example, to capture any errors generated by the date command:
$ date -d "ysterday" 2> error_log.txt
(the misspelling of âyesterdayâ in this simple example is intentional to make date generate an error. As it doesnât recognize the word ysterday, it prints an error message to stderr instead of the actual date to stdout. This way, you can actually observe whether that error message ends up in error_log.txt or not.)
Exercise 2.153
Try date -d "ysterday" > date.txt (yes, still misspelled on purpose) and inspect date.txt.
Notice it is empty, since the error message went to the screen, not to the file.
Now try again, redirecting standard error into the same file using 2> instead (remember you can press Up to easily adjust the command without retyping everything).
This time date.txt should actually contain the error message, verify this by inspect it with cat.
{
"filesystem": { "/home/student/stuff/todo.txt": "buy milk\n" },
"checks": [
{ "type": "commandEvent", "eventType": "redirect.write",
"match": { "absPath": "/home/student/date.txt", "mergeStderr": false },
"desc": "the first attempt (plain >, no 2>) should leave date.txt without the error message" },
{ "type": "commandRan", "pattern": "2>\\s*date\\.txt",
"desc": "should also try the stderr redirection with 2>" },
{ "type": "commandEvent", "eventType": "redirect.write",
"match": { "absPath": "/home/student/date.txt", "stream": "stderr" },
"desc": "the 2> attempt should redirect the error message into date.txt" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/date.txt"] },
"desc": "should inspect date.txt with cat" }
]
}
Sometimes a command is expected to produce a lot of warnings and errors on stderr which can be safely ignored.
To avoid all these messages cluttering the output, it is common to redirect the error stream to
a special file of the Linux operating system, called /dev/null. You can write data to it as in any other file,
but any data written to it will be completely discarded by the operating system.
For example, searching for a file with find in the filesystem root directory for directories with the same name as the user can produce a lot of warnings on stderr that some directories can not be accessed. An easy way to ignore the stderr messages, and still see the regular matches on stdout, would then be:
$ find / -name $USER 2> /dev/null
Exercise 2.154
Try this in a terminal on your real Ubuntu Linux computer.
Search the filesystem for files or directories called opt, starting in the filesystem root.
Try to see the differnce with and without appending 2> /dev/null to ignore errors from stderr.
Which of the following opt directories (among others) do exist in your Ubuntu Linux system?
/etc/opt
/home/opt
/opt
/root/opt
/share/opt
/usr/opt
/usr/share/opt
/var/opt
Finally, perhaps you want to capture errors together with the standard output into the same file, just like they would appear in your terminal without redirecting.
The portable way to do this, which works the same in every shell, is to redirect standard output as usual with >,
and afterwards instruct bash to redirect standard error to âwherever standard output currently goesâ using 2>&1:
$ date -d "ysterday" > date.txt 2>&1
Bash also offers the shorthand >& for the same thing, but this only works in bash itself, not in every shell.
Note: in this manualâs browser-based terminal emulator,
the 2>&1 and >& notation do not work, but you can try it yourself on a real terminal.
2.7.3. Redirecting standard input#
Standard input can also be redirected to get its content from a file.
For example, you used the notation less myfile.txt to view the file myfile.txt, providing less with the filename as an argument.
However, to feed myfile.txt to less as standard input, you can use the < redirection sign:
$ less < myfile.txt
This makes the shell read the content of the file after <, and provide that text as input on stdin for the program before the <.
So, the command above will send the file content of myfile.txt directly to less, whereas less myfile.txt relies on the less command to interpret
its argument as a file path that it should read itself.
For this simple example, both variations will have basically the same result.
Exercise 2.155
Of course, you can use both standard input and output at the same time, e.g.:
$ wc < myfile.txt > myfile.stats
Here we use the nifty wc utility that you encountered before.
Verify the results in myfile.stats with cat.
{
"filesystem": { "/home/student/myfile.txt": "hello world\nthis is myfile\n" },
"checks": [
{ "type": "commandEvent", "eventType": "redirect.read", "match": { "absPath": "/home/student/myfile.txt" },
"desc": "should feed myfile.txt into wc using <" },
{ "type": "commandEvent", "eventType": "coreutil.wc",
"desc": "should use wc" },
{ "type": "commandEvent", "eventType": "redirect.write", "match": { "absPath": "/home/student/myfile.stats" },
"desc": "should redirect the wc output into myfile.stats with >" },
{ "type": "fileMatches", "path": "/home/student/myfile.stats", "pattern": "2\\s+5\\s+27",
"desc": "myfile.stats should contain the line/word/byte counts of myfile.txt" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/myfile.stats"] },
"desc": "should cat myfile.stats" },
{ "type": "tabCompletionHint" }
]
}
Exercise 2.156
Running wc myfile.txt and wc < myfile.txt produce nearly the same output, except for a subtle difference.
What is that difference?
wc myfile.txtreports the filename in the output;wc < myfile.txtdoes not as it does not see the filename.wc myfile.txtby default reports the number of lines, unlikewc < myfile.txt.wc < myfile.txtprints its output tostderr, whilewc myfile.txtusestdout.wc < myfile.txtwill not produce an error message ifmyfile.txtdoes not exist, unlikewc myfile.txt.
2.7.4. Pipes#
Fig. 2.9 Connecting programs with redirection, letting each program write its stdandard output to a temporary file, and the next program read from that file.#
Redirection to files has various uses, including logging results of commands, append text files with new information, and make programs that only read from stdin process files on your harddisk.
Redirection can also be used to combine programs following the ref(sec:unix-philosophy), letting the output of one program be the input for the next, to compose more complex behavior, as illustrated in Fig. 2.9.
For example, take the command wc -l, which counts the number of lines in a file. To find out how many people are logged on at the moment, you might do this:
$ who > tmp.txt
$ wc -l tmp.txt
$ rm tmp.txt
It works, but itâs a bit clumsy. Combining program inputs and outputs via file redirects has a few serious limitations:
If you want to apply a sequence of operations to a text file, you would have to create a number of intermediary files. Now you have to manage additional file management (delete them, check if they didnât exist already, etc.) just to combine a few programs.
Each program has to terminate completely before the next program can start.
To solve this problem, you can connect the standard output of one command directly to the standard input of another command using pipes (symbol |).
Pipes store program standard output in memory, and provide it as standard input to the next program without creating any intermediate files.
Fig. 2.10 illustrates this.
Fig. 2.10 Connecting programs with pipes, which avoids creating temporary files altogether.#
It even allows programs to run in parallel, such that each next step can process on the output from the previous step once it becomes available.
With pies, the previous example becomes:
$ who | wc -l
You can pipe multiple commands together, and even combine it with redirects. For example, if you would like to store the number of people logged on, you could use:
$ who | wc -l > count.txt
Some more examples:
ls -t ~/documents | lessallows you to view the output oflsone screen at a time.ls /opt/ros | wc -lcounts the number of files and directories in/opt/ros.cat /etc/group | cut -d":" -f4 | grep "wcaarls" | wc -ldisplays the number of groups the userwcaarlsis a member of (here we use thecutcommand youâve encountered before; on a real Linux system, the file/etc/groupcontains information on which user belongs to what user group).
Exercise 2.157
Count how many entries are in /usr/bin by piping ls into wc -l.
{
"checks": [
{ "type": "commandEvent", "eventType": "syntax.pipeline",
"desc": "should use a pipe (|)" },
{ "type": "commandEvent", "eventType": "coreutil.ls", "match": { "absPaths": ["/usr/bin"] },
"desc": "should use ls -l /usr/bin" },
{ "type": "commandEvent", "eventType": "coreutil.wc",
"desc": "should use wc -l" },
{ "type": "compareOutput", "referenceCommand": "ls -l /usr/bin | wc -l",
"studentPattern": "^ls\\s.*/usr/bin.*\\|\\s*wc\\s+-l",
"desc": "the printed count should match ls -l /usr/bin | wc -l" }
]
}
Exercise 2.158
Why does a pipe like ls -l /usr/bin | wc -l avoid the need for an intermediate file, unlike redirection alone?
It doesnât; pipes still create a hidden temporary file.
The standard output of
lsis connected directly to the standard input ofwcin memory, so no file is ever written to disk.wcreads/usr/binitself, solsâs output is discarded.Pipes only work for commands that produce no output.
Exercise 2.159
Print just the usernames from /etc/passwd by piping cat into the cut command.
Hint: if you inspect the content of /etc/passwd, you see its fields are delimited with :,
so you must configure cut to separate lines on this character, and only keep the first field.
(Note that the same thing can be achieved with cut alone, without first calling cat; this is just a simple exercise to practice using pipes)
{
"filesystem": {
"/etc/passwd": "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nsshd:x:100:65534::/run/sshd:/usr/sbin/nologin\nsyslog:x:101:104::/home/syslog:/usr/sbin/nologin\n"
},
"checks": [
{ "type": "commandEvent", "eventType": "syntax.pipeline",
"desc": "should use a pipe (|)" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/etc/passwd"] },
"desc": "should cat /etc/passwd" },
{ "type": "commandEvent", "eventType": "coreutil.cut", "match": { "delimiter": ":" },
"desc": "should cut out the first field delimiter \":\"" },
{ "type": "stdoutContains", "text": "root" },
{ "type": "stdoutContains", "text": "daemon" },
{ "type": "stdoutContains", "text": "sshd" },
{ "type": "stdoutContains", "text": "syslog" }
]
}
Somtimes, you might want to âsiphon offâ the data to see intermediate results, and store the text at some point in the pipe in a file
while also passing that text on to stdout for the next command to consume.
Redirecting stdout to a file with > does not work, because it does produce a file but the stdout itself becomes empty.
Instead, you can use tee command (for T-connection).
If tee is called with the -a option, it will append rather than overwrite a file.
For example:
$ cut -d":" -f1 /etc/passwd | tee -a result.txt | grep "d$"
This line will append the stdout result of the first command to an intermediate file called result.txt,
but also pass it on as stdin to the last grep command.
What do you think this file will contain? Verify.
Exercise 2.160
Try the tee pipeline above, and inspect result.txt afterwards with cat to check what you think it will contain.
{
"filesystem": {
"/etc/passwd": "root:x:0:0:root:/root:/bin/bash\ndaemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\nsshd:x:100:65534::/run/sshd:/usr/sbin/nologin\nsyslog:x:101:104::/home/syslog:/usr/sbin/nologin\n"
},
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.cut", "match": { "delimiter": ":" },
"desc": "should cut out the first field (username) of /etc/passwd" },
{ "type": "commandEvent", "eventType": "coreutil.tee", "match": { "absPaths": ["/home/student/result.txt"], "append": true },
"desc": "should tee -a the cut output into result.txt" },
{ "type": "commandEvent", "eventType": "coreutil.grep", "match": { "pattern": "d$" },
"desc": "should grep for usernames ending in d" },
{ "type": "fileContains", "path": "/home/student/result.txt", "text": "root",
"desc": "result.txt should contain every username tee saw, not just the ones grep later matched" },
{ "type": "fileContains", "path": "/home/student/result.txt", "text": "sshd" },
{ "type": "fileContains", "path": "/home/student/result.txt", "text": "syslog" },
{ "type": "commandEvent", "eventType": "coreutil.cat", "match": { "absPaths": ["/home/student/result.txt"] },
"desc": "should inspect result.txt afterwards with cat" },
{ "type": "stdoutContains", "text": "root", "pattern": "result\\.txt",
"desc": "that inspection should actually show result.txt's contents on screen" }
]
}
Exercise 2.161
In cat /etc/passwd | cut -d":" -f1 | tee -a result.txt | grep "d$", which usernames end up in result.txt?
Only the ones that also end in âdâ (i.e. only the ones
grepmatches).All of them:
teesits beforegrepin the pipeline, so it siphons off every username, regardless of whatgreplater filters out.None:
tee -aonly appends if the file already exists.Only the first username, since
teecloses the file after one line.
Pipes are what makes the Unix shell very versatile and flexible. We already discussed the Unix philosophy: make lots of tools, each of which does one thing and does it well. Pipes form the glue between these tools.
Finally, when discussing pipes, itâs useful to recall the cat command that you encountered before.
Itâs a bit more versatile than you may have thought:
you can supply a number of file names, which
catwill show, one after another. For example, try:$ cat myfile*txt
if you donât specify a file,
catwill use standard input. This is useful for quickly creating small text files. Try:$ cat > short.txt
You will notice that you donât get a prompt. Instead, everything you type is stored in
short.txt. To stop entering text, press Ctrl-D.the
catcommand is often used as the beginning of a pipe, for example:$ cat /etc/passwd | grep "false" | sort | less
Try this line. What do you think it does?
Warning
The interactive terminal in this manual is a JavaScript-based emulator, not a real shell, and a plain cat with no file argument is one place where that shows.
In a real bash terminal, cat echoes each line straight back to the screen right after you press Enter, one line at a time, for as long as it keeps reading. In this emulator, nothing appears until you press Ctrl-D: only then does everything you typed appear at once (or, if redirected, get written to a file all at once).
For the exercise below this doesnât matter, since the output is redirected into a file either way. But if you try a bare cat with no > afterwards, donât be surprised that it behaves differently from a real terminal.
Exercise 2.162
Try cat > short.txt yourself: type a line or two of text, then press Ctrl-D to stop and save.
{
"checks": [
{ "type": "commandRan", "pattern": "^cat\\s*>\\s*short\\.txt",
"desc": "should run cat > short.txt (no file argument, since input comes from what you type)" },
{ "type": "commandEvent", "eventType": "coreutil.cat",
"desc": "should use cat" },
{ "type": "commandEvent", "eventType": "redirect.write", "match": { "absPath": "/home/student/short.txt" },
"desc": "should redirect what you type into short.txt with >" },
{ "type": "fileExists", "path": "/home/student/short.txt" }
]
}
Exercise 2.163
What does cat /etc/passwd | grep "false" | sort | less do?
It prints
/etc/passwdsorted alphabetically, then searches for the word âfalseâ inless.It filters the lines of
/etc/passwdthat contain âfalseâ (typically accounts with a disabled login shell), sorts those lines alphabetically, and shows the result one screen at a time.It counts how many lines in
/etc/passwdcontain the word âfalseâ.It fails, because
catcannot be the first command in a pipe.
Exercise 2.164
Assuming myfile.txt exists in the current working directory,
which of these four commands does not produce the same output as the other three commands?
Try to predict what each of the commands does, or try them out yourself.
less myfile.txtless < myfile.txtcat myfile.txt | lessecho myfile.txt | less
2.7.5. Command substitution#
As we have seen above, with a | pipe you can take the standard output of one program and provide it as standard input of another program.
Sometimes, however, you want to take the output of a command, and use the result as arguments for a next command, as if you would type it in the shell.
Taking the output of one command, and use it as part of the next command is called command substitution, and Bash has a special syntax for this: $([COMMAND]).
Here is a simple example:
$ echo "It was $(date) when I was learning about command substitution"
From the output, you should see that bash has substituted the $(date) part by the output of the date command command.
The command within the brackets can be more complex than just running a single program,
for example:
$ echo "The weekday tomorrow be $(date -d "tomorrow" | cut -d ' ' -f1)"
It is even possible to nest substitutions $( ), such as
$ echo My terminal is $(grep $(whoami) /etc/passwd | cut -d":" -f7)
When you study such a command to understand what it does, it may make sense to test the commands in order of execution, inspecting their output at each step. In this case, you can construct the full command from the individual commands as follows:
$ whoami
$ grep $(whoami) /etc/passwd
$ grep $(whoami) /etc/passwd | cut -d":" -f7
$ echo My terminal is $(grep $(whoami) /etc/passwd | cut -d":" -f7)
Exercise 2.165
The goal of this exercise is to run the word count command wc on all .txt files that exist in the home directory, or any of its subdirectories.
To do this, first run find as a standalone command to see in the terminal the relative paths of all .txt files:
$ find . -name "*.txt"
Now, let wc count the number of lines -l of the files, by using command substitution with this find command to list all file paths as arguments for wc.
{
"filesystem": {
"/home/student/a.txt": "one\ntwo\n",
"/home/student/notes/b.txt": "uno\ndos\ntres\n",
"/home/student/projects/ideas/project.md": "# My new project\nSee README.txt\n",
"/home/student/projects/ideas/README.txt": "still planning\n",
"/home/student/readme.md": "not a txt file\n"
},
"checks": [
{ "type": "commandEvent", "eventType": "coreutil.find", "match": { "namePattern": "*.txt" },
"desc": "use find to identify all .txt files" },
{ "type": "commandEvent", "eventType": "coreutil.wc", "match": {"flags": ["l"]},
"desc": "should use wc -l" },
{ "type": "commandEvent", "eventType": "syntax.cmdsubst",
"desc": "should use $(...) command substitution" },
{ "type": "compareOutput", "referenceCommand": "wc -l $(find . -name \"*.txt\")",
"studentPattern": "^wc\\s+-l\\s+\\$\\(find",
"desc": "the printed line counts should list the number of lines all txt files" }
]
}
There is also an alternative syntax for command substitution using backticks to demarcate the substitution. Here are some examples that do the same as what you saw before:
$ echo "It was `date` when I was learning about command substitution"
$ wc -l `find . -name "*.txt"`
Note
You still sometimes find this backtick notation in older scripts and documentation, but it is considered deprecated.
Exercise 2.166
Why is $( ) generally preferred over the backtick notation for command substitution?
Backticks are not supported in
bash, only insh.$( )can be nested directly (e.g.$(cmd1 $(cmd2))), while nesting backticks requires escaping the inner ones, and$( )is visually easier to spot in a command than backticks.$( )runs faster, because it avoids starting a subshell.Backticks can only be used with
echo, not any other command.People like to type more.
2.7.6. Multiple commands on one line#
You can use a semi-colon to provide the shell with multiple commands that should be executed one after the other, without intervention.
Unlike using pipes, with ; the shell does not execute the commands in parallel but one after the other, and no output is redirected.
For example, instead of this:
$ sleep 3
$ echo ready
You can achieve the same as a one-liner as follows:
$ sleep 3; echo done
The difference is that with the one-liner you donât have to wait for the sleep 3 command to finish (after 3 seconds) to enter the next echo done command.
The shell will just immediately start the next command itself once the sleep 3 command terminated.
You can also group multiple commands together using parentheses. The parentheses act like a temporary subshell, where you can change directories without affecting the working directory afterwards.
For example, try the difference between:
$ cd ~
$ (cd /bin; pwd); pwd
and
$ cd ~
$ cd /bin; pwd; pwd
Note that the final pwd still shows your working directory is your home directory in the first case,
because the cd /bin was executed within the parentheses, and thus only changed the working directory for the first pwd command.
Exercise 2.167
There is a directory stuff/ in your home directory containing a file todo.txt. Then run (cd stuff; pwd); ls.
Does the final ls show the contents of your home directory, or the stuff/ subdirectory?
{
"filesystem": { "/home/student/stuff/todo.txt": "buy milk\n" },
"checks": [
{ "type": "commandEvent", "eventType": "syntax.subshell",
"desc": "should run (cd stuff; pwd) as a subshell" },
{ "type": "commandEvent", "eventType": "builtin.cd", "match": { "absPath": "/home/student/stuff" },
"desc": "should cd into stuff/ inside the subshell" },
{ "type": "stdoutContains", "text": "/home/student/stuff", "pattern": "^\\(cd\\s+stuff",
"desc": "pwd inside the subshell should report /home/student/stuff" },
{ "type": "stdoutContains", "text": "stuff", "pattern": "^\\(cd\\s+stuff.*\\)\\s*;\\s*ls",
"desc": "the ls after the subshell should still list your home directory (showing the stuff/ folder itself), since cd inside () didn't affect the parent shell" }
]
}
Exercise 2.168
After running (cd stuff; pwd); ls, what is the current working directory of your shell?
stuff/, sincecdinside the parentheses changed it.Still your home directory, since
(...)runs thecdinside a subshell which doesnât affect the parent shell.It depends on whether
stuff/exists./, since parentheses reset the shell to the root directory.
2.7.7. Aliases#
Another useful property of the shell is aliasing. This means that you can create your own âshort handâ notation for commands you often use, saving you more typing work. An alias is setup in bash using a syntax as follows:
$ alias name='command'
in which name is the short hand you want to give the command.
For example, ll is a common alias to quickly run ls with additional switches for detailed listings of directory content:
$ alias ll='ls -alF'
The alias can be executed without with additional arguments:
$ ll # like running ls -alF
$ ll *.txt # like running ls -alF *.txt
$ ll /usr/bin # like running ls -alF /usr/bin
To get an overview of all active aliases, you can just enter alias without arguments.
Exercise 2.169
Create an alias for find . -name; call it f and try it using f "*.txt".
{
"filesystem": {
"/home/student/notes.txt": "todo\n",
"/home/student/readme.md": "not txt\n"
},
"checks": [
{ "type": "commandRan", "pattern": "^alias\\s+f=",
"desc": "should create an alias called f for find . -name" },
{ "type": "commandEvent", "eventType": "coreutil.find", "match": { "namePattern": "*.txt" },
"desc": "using f \"*.txt\" should actually run find . -name \"*.txt\"" },
{ "type": "stdoutContains", "text": "notes.txt",
"desc": "f \"*.txt\" should find notes.txt" }
]
}
Exercise 2.170
Try this on your real Ubuntu Linux computer. Open two terminals.
In one of the terminals, define an alias, and confirm that it works in that terminal.
Now check in the other terminal, which is running a separate bash process,
if your custom alias is also available there.
Now close all terminals, and open a new one.
Again check if the alias is available.
Aliases only exist for the shell where they were created in. They do not persist and are not shared between sessions.
Aliases only exist as long as the shell where they were created in is active. While active, the alias is available in other terminals too.
Setting an alias permanently changes your account settings; it is immediately available in all other existing and later created terminals.
Setting an alias permanently changes your account settings, but it will only be available in new shell processes started after setting the alias.
As you may notice, if you want an aliases to reliably persist,
it will need to be defined in each bash processes you start separately.
Rather than retyping the alias every time,
a more convenient approach is to define your aliases in a startup file like ~/.bashrc,
which will be executed automatically every time you start a bash process in a new terminal.
This will be discussed later in Section 2.10.7.