Intermediate command line topics
At the start, I mentioned that “text is the universal interface”, but we’ve not truly seen how the command line handles text. First, let’s download this example files of names and then do a bunch of operations on it:
Input
$ curl -o names.txt -L phette23.github.io/c4l21-learn-to-love-the-command-line/names.txt
Output
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 162 100 162 0 0 3857 0 --:--:-- --:--:-- --:--:-- 3951
100 137 100 137 0 0 856 0 --:--:-- --:--:-- --:--:-- 1412
You should be able to copy-paste that URL onto your prompt. Note that, on Windows, the Control + C and Control + V keyboard shortcuts conflict with special terminal functions. You may need to right-click with your mouse to paste. If curl
isn’t working for you, you can always download the file into our workshop folder.
Text processing greatest hits
We have a lot to cover, so let’s quickly learn the basics of several commands before diving into making them work together.
The cat
😺 command stands for concatenate. It can be used to combine several files but it’s often used to print the contents of a text file to your terminal:
Input
$ cat names.txt
Output
names
ephetteplace
katharina
ariadne
phette23
ephetteplace
testname
madeupname
nameyMcNameName
names with spaces in it
testname
testname
sort
sorts text lexically:
Input
$ sort names.txt
Output
ariadne
ephetteplace
ephetteplace
katharina
madeupname
names
names with spaces in it
nameyMcNameName
phette23
testname
testname
testname
uniq
removes duplicate lines, leaving only unique values:
Input
$ uniq names.txt
Output
names
ephetteplace
katharina
ariadne
phette23
ephetteplace
testname
madeupname
nameyMcNameName
names with spaces in it
testname
Wait a minute, why didn’t that work? Can you see the difference between passing names.txt to uniq
and cat
? Any guesses as to why this did not remove all the duplicates?
I find it counterintuitive but what uniq
actually does is eliminate duplicate adjacent lines. So to find a unique list of values we want to first sort
and then uniq
our names. How do we do this? Using output redirection to “pipe” the output of sort
to the input of uniq
. Output redirection is one of the most powerful features of the command line:
Input
$ sort names.txt | uniq
Output
ariadne
ephetteplace
katharina
madeupname
names
names with spaces in it
nameyMcNameName
phette23
testname
The vertical bar |
performs this piping of output to input. Notice how our uniq
has no arguments that follow it; since piping data between commands is so powerful, most commands are designed to work with it. We might see the terms “standard output”, abbreviated “stdout”, and “standard input” or “stdin” mentioned in documentation for commands. Those terms refer to these streams of data, coming from and going into a command. So while our uniq
command did not have a text file to for input it checked stdin and found the names text there.
As far as I know, there is no non-standard input or output or anything else. So why do we use “standard output” and not simply “output”? I don’t know ¯\(ツ)/¯
To demonstrate pipes, let’s look at the mirrored commands head
and tail
which both take a numeric -n
argument and return either the first or last N lines:
Input
$ sort names.txt | head -n 4
Output
ariadne
ephetteplace
ephetteplace
katharina
To get the very last line:
Input
$ sort names.txt | tail -n 1
Output
testname
You can have any number of commands strung together in what’s referred to as a pipeline:
Input
$ cat names.txt | wc -l
$ uniq names.txt | wc -l
$ sort names.txt | uniq | wc -l
Output
12
11
9
The wc
command stands for word count and the -l
flag tells it to count the lines of its input. So this triad of commands demonstrates the differences in length between the complete names.txt file, the file without repeated lines, and the file without duplicate non-adjacent lines.
We can also pass head
or tail
a number prefixed with a plus +
sign to print everything but N minus 1 lines. Why N minus 1 and not simply N? Again, I cannot explain, that’s just how it works 🤷🏻. So, combining a few things, here is a deduplicated set of names with no header row:
Input
$ tail -n +2 names.txt | sort | uniq
Output
ariadne
ephetteplace
katharina
madeupname
names with spaces in it
nameyMcNameName
phette23
testname
One last handy tool before we look at files: tr
stands for translate. It converts a given character to another but we need to know output redirection because it operates only on standard input, not on files.
Input
$ cat names.txt | tr 'n' 'N'
Output
Names
ephetteplace
kathariNa
ariadNe
phette23
ephetteplace
testName
madeupName
NameyMcNameName
Names with spaces iN it
testName
testName
tr
has a useful -d
flag which, instead of translating a given character, deletes it. I use this frequently to remove newline characters from input but here we can remove spaces:
Input
$ tail -n 5 names.txt | tr -d ' '
Output
madeupname
nameyMcNameName
nameswithspacesinit
testname
testname
tr
translates characters one-by-one but we can use a more all-purpose, but complex, command to perform larger substitutions:
Input
$ tail -n 5 names.txt | sed 's|name|NAME|'
Output
madeupNAME
NAMEyMcNameName
NAMEs with spaces in it
testNAME
testNAME
sed
is a stream editor with enough choices that it is practically a programming language unto itself. For our purposes, we will only learn the substitute command. sed
’s first argument may look strange because of it’s special format. The first letter actually stands for the editing operation, in this case s
for substitution, then we can use any character as a separator to divide up our original string and what we want to replace it with. So sed 's|name|NAME|'
and sed 's/name/NAME/'
are identical commands: the first uses a vertical bar |
as separator while the second uses a forward slash /
.
The first piece of a sed
substitution is actually be a regular expression. We won’t learn more about these today—and it’s truly not necessary to know much for simple search-and-replace operations—but naturally segue into another popular command, grep
which searches for regular expressions.
Input
$ grep 'names' names.txt
Output
names
names with spaces in it
grep
returned only the lines that matched the phrase “names”. As with all the commands we’re seeing, we can pipe data to grep instead of passing it a file:
Input
$ head -n 6 names.txt | grep '[0-9]'
Output
phette23
This uses a less familiar regular expression, it’s not search for the literal phrase “[0-9]” but instead for “any character in the range 0 to 9” e.g. any number. So taken altogether our command prints any name that is 1) in the first six lines of names.txt (the head
part) and 2) has a number in it (the grep
part).
If we know regular expressions, sed
and grep
unlock an enormous amount of power. It’s worth noting that many versions of grep
only known a simplified subset of regular expressions but we can use egrep
or grep -E
to access extended, and more complex, regular expressions.
Writing output to a file
All this manipulating text output is well and good but what if we want to write results to a file? It’s relatively rare that we want merely to view transformed text, we usually want to save the changes in a file. Luckily, much like the vertical bar |
pipes one command’s output to another’s input, we can use the greater than symbol >
to write output to a file:
$ sort names.txt | uniq > sorted-names.txt
If we write to a file that does not yet exist, it is created. If we write to a pre-existing file, its contents are overwritten.
It’s often useful to not overwrite but to append onto the end of a file. For instance, if we are continually monitoring the output of a repeated process, we might use two greater than symbols >>
to append output to a log file:
Input
$ echo "first name" > more-names.txt
$ tail -n +2 names.txt >> more-names.txt
$ echo "last name" >> more-names.txt
$ cat more-names.txt
Output
first name
ephetteplace
katharina
ariadne
phette23
ephetteplace
testname
madeupname
nameyMcNameName
names with spaces in it
testname
testname
last name
echo
is a new command to us—it prints the input string(s) we provide to stdout. Why is that useful? Mostly so Bash scripts can print information as they execute. Here, we used echo
to create a new file “more-names.txt” with a single entry, then added all but the first line of our names.txt file to it, and then added one final entry.
Two important warnings: >
erases the file’s contents before our chain of commands even begins. So often if we write to the same file we’re using as input, it simply wipes out the file’s contents. Notice how sort more-names.txt > more-names.txt
empties the file.
Secondly, an analogous problem occurs when appending output to the input file: it creates an infinite loop that builds an enormous text file until we out of disk space. Try to think of data as being processed one line at a time through the entire command pipeline, as opposed to the whole input file passed through step-by-step. If we keep adding to the input before processing is complete, the cycle never ends. Therefore, if we want to modify an existing file, we write to a new file and then mv
the new one to overwrite the input file e.g. sort names.txt > sorted-names.txt; mv sorted-names.txt names.txt
.
There are some commands that have flags which let us edit files in place but know that this is not typically the way things work in the shell, but has to be specially supported.
Control flow: variables, subshells, conditions
As we probably anticipated, Bash also has features typical of all programming languages that let us substitute “variable” names for literal data, conditions that execute commands only based on certain criteria, and loops that repeat operations a configurable number of times.
Bash variable assignment is straightforward: write a name, then equals =
, then its value. If our data has a space in it, we need to either escape it or wrap it in quotes. To obtain the variable’s value later, prefix the name with a dollar sign $
.
Input
$ NAME=Roger
$ echo $NAME
$ SURNAME=Rabbit
$ echo $SURNAME
$ NAME="Adriadne Vegas"
$ echo $NAME
Output
Roger
Rabbit
Adriadne Vegas
It is not necessary to make variable names all-caps but it is a convention for the sake of clarity. You can also use underscores _
and numbers in variable names, but the first character cannot be a number. Variable names are case sensitive (“name” is not the same as “NAME”).
We see several notable things in this small example: again because spaces are meaningful characters we have to be cognizant of their usage. What happens if you write LASTNAME = Phetteplace
with spaces? Also, we can overwrite existing variables by writing to them again (we redefined NAME
in the example).
What if we wanted to store the value of a command in a variable? The syntax also involves a $
though the two operations are not similar:
Input
$ NAME=$(tail -n 1 names.txt)
$ ODDNAME=$(tail -n 1 names.txt | tr 'n' 'N' | tr 'e' '%')
$ echo $NAME
$ echo $ODDNAME
Output
testname
t%stNam%
The $( ... )
construction executes a subshell i.e. think of it as automating the process of us opening a new terminal window running the Bash shell, executing the command elided here as ...
, and then copy-pasting the stdout (if any) of that command back into our variable. You can also wrap a command in backticks “` … `” to execute a subshell. This is technically called command substitution as there are a few other ways subshells are done, but for our purposes the terms are interchangeable. Subshells are immensely useful in conjunction with the two of our other topics, variables and loops, as we shall see in the next section.
Conditions
Up until now, most of the commands we’ve written executed successfully. What does this mean? We’ve seen that successful commands return output in the form of “stdout” which we can redirect to other commands and files. It turns out commands analogously write error messages to standard error or stderr. All commands are considered either successful or failed based on their exit status. I bring up these terms as inchoate definitions for now because we do not have time to dig deeper into them..
Let’s learn what happens with failed commands by learning a special boolean syntax: two adjacent ampersands &&
are a “logical and” which, when inserted in between two commands, cause the second command to run only if the first was successful.
$ tail -n 2 names.txt && echo "Success"
testname
testname
Success
$ tail -n 2 "this file does not exist" && echo "Success"
tail: this file does not exist: No such file or directory
Our first command has its usual output (the last two lines of names.txt) but adds a “Success” line at the end. The second command shows error output but no “Success”; the echo
command was never even executed because of the logical and &&
.
Two adjacent vertical bars ||
is a “logical or” and does the opposite: the second commands runs only if the first failed. Let’s repeat our two tail
commands:
$ tail -n 2 names.txt || echo "Failure"
testname
testname
$ tail -n 2 "this file does not exist" || echo "Failure"
tail: this file does not exist: No such file or directory
Failure
||
is incredibly useful for working around one common problem we’ve already encountered: cross-platform inconsistency. Given that available commands or their syntax might differ across operating systems, logical or ||
lets us try one command and, if it doesn’t work, do something else. Most Mac and Linux systems will come with a simple wget
command for downloading web content, while Git Bash does not. We could have downloaded the names.txt file at the top of this page like so:
$ wget $URL || curl -o names.txt -L $URL
This uses wget
if it’s available and falls back to curl
if not. Isn’t it bad to be running failed commands and referencing software that’s not installed? The command line somewhat conflates command success/failure with true/false conditions. In general, it is safe to run broken commands. It is much more dangerous to run a successful command that does something terrible to our file system than to run an unsuccessful one that simply prints an error message.
We may be thinking that it will be hard to execute complex conditions with only &&
and ||
, plus produce many extraneous error messages, and we’re right. Bash has an entire “if-then” syntax for executing commands only if very specific conditions are met. A warning: I find the Bash “if” syntax to be even more clunky than other command line constructions. Especially if you know a major programming language, these “if” statements may seem somewhat arcane or primitive.
Input
$ FILE=names.txt
$ if [[ -s $FILE ]]
> then echo "$FILE exists and has content"
> elif [[ -e $FILE ]]
> then echo "$FILE exists but is empty"
> else echo "$FILE does not exist"
> fi
Output
names.txt exists and has content
We can try redefining the $FILE
variable then using up-arrow ⬆️ to get our long if statement again to test this. Try running the loop using both an empty file created with touch
and a reference to a non-existent file.
Here are the elements of an if condition in order:
if
starts the statement- double or single square brackets
[[ ... ]]
wrap a condition, there are a few differences between[[
and[
but we only use[[
as it is less error-prone (we don’t need to escape as many special characters, for instance). - there are many flags and comparisons that can be done inside the brackets, we won’t cover them all here, but running
man [
(note it’s a single bracket) orhelp test
on Git Bash shows many of the options then
prefixes the command to be conditionally run, it can be on its own line or beside the command- (optionally) any number of “else if”
elif
conditions and their correspondingthen
commands - (optionally) an
else
fallback which is executed if no other conditions proved true fi
finishes the statement
Here is one more “if” condition example showing some string comparisons.
$ NAME=testname
$ if [[ -z $NAME ]]
> then echo "Please provide a name."
> elif [[ $NAME = 'testname' ]]
> then echo "This is just a test..."
> elif [[ $NAME ~= 'test' ]]
> then echo "This doesn't look like a test!"
> fi
As above, try running a few names through this phrase. The -z
flag checks if a string is empty (it can be used to check if a variable hasn’t been defined yet), =
compares if two strings are identical, and ~=
is a pattern matching comparison (true if ‘test’ is in $NAME
).
There is much more to “if” statements in terms of what conditions we can use, but the statements are hard to write out line-by-line onto the prompt. They will be more useful after our next section when we learn more about combining many commands into a script.
Let’s revisit our table of special characters now. The meanings of hashmark #
and single ampersand &
are new to us. We won’t have time to go into background jobs but we will see #
when we look at Bash scripting.
Character | Meaning |
---|---|
[space] | separates commands, arguments, and options |
\ | “escape” (don’t obey any special meaning of) the following character |
~ | User’s home folder e.g. /Users/username |
. | The current location, the folder you’re in |
.. | The parent folder of your current location |
* | wildcard, used to match patterns of file names |
| | instead of printing to stdout, pipe output to the next command |
> | instead of printing to stdout, write output to a file |
>> | instead of printing to stdout, append output to a file |
$ | used to reference variables and run subshells |
&& | logical “and”, run the following command if the preceding one was successful |
|| | logical “or”, run the following command if the preceding was unsuccessful |
& | run the preceding command as a job in the background |
# | comment (shell ignores the rest of the line that follows it) |
Bash scripting
Other than a few extra bits, Bash scripting is wonderful because of its simplicity: if we put a series of commands into a file, we can then execute them all at once by feeding the file to the Bash shell. Let’s write a short script that sorts the contents of all the text files in the current directory.
We can edit scripts using any text editor like NotePad++, Sublime Text, Atom, Visual Studio Code, etc. but not a rich text editor like MS Word or Pages. I recommend you use whatever you feel most comfortable with. There are also command-line editors like vim
, nano
, and emacs
. These are nice in that they keep us in the context of the CLI but are hard to learn. It is very useful if our GUI editor has an associated command that opens files. Atom, for instance, let’s us run atom script.sh
to create or open a “script.sh” file in our current location.
echo $(date) "sorting all text files in this folder"
for TEXTFILE in $(ls *.txt); do
echo "Sorting $TEXTFILE..."
# remember we want to write to a temporary file, not to our input file
sort $TEXTFILE > tmp
mv tmp $TEXTFILE && echo "Done."
done
echo "Done sorting all files."
This script concisely combines almost everything we’ve learned so far: subshells, variables, globbing, writing to a file, and comments. Let’s explain the few pieces that we don’t understand yet. We can name it “script.sh” and run it by passing it to the Bash shell itself: bash script.sh
.
First of all, we haven’t talked about single versus double quotes yet. Unfortunately, they are semantically different. Single quotes treat absolutely everything contained in them as regular text. Double quotes allow the shell to interpolate variable names into their values. Here’s a brief demonstration of the difference:
Input
$ NAME=Orestes
$ echo '$NAME is a variable' && echo "its value is $NAME"
Output
$NAME is a variable
its value is Orestes
What if we don’t want to type bash
before our script’s name every time we run it? To turn a script into something more like a real command, one virtually indistinguishable from tools like cat
and echo
, we need to do two things: make the script tell the shell how to interpret its code and make it executable.
The shell does not assume, for instance, that our code is Bash and not Python, even if we give it a hint using a “.sh” or “.py” file extension. File extensions, for the most part, are arbitrary and ignored on the command line. To tell the shell to always interpret our code as a Bash script, we add #!/usr/bin/env bash
as the very first line. What does this enigmatic phrase mean? If a script starts with the #!
then the shell assumes what follows it is the interpreter to use.
We could write the precise path to our Bash shell here, like #!/bin/bash
. What is the problem with that? Depending on our machine, Bash might be installed in a different location and our script is now less portable. The env
command helps to execute commands with a consistent environment. In this instance, it will find a Bash interpreter for us—whether it’s installed at /bin/bash, /usr/bin/bash, /usr/local/bin/bash, or elsewhere—and use it. Almost every script, whether it’s a shell script or not, should begin with #!/usr/bin/env $INTERPRETER
e.g. #!/usr/bin/env python
for a Python script.
For the second step, we need to understand more about file permissions. Let’s review the “long” version of ls
output:
Input
$ ls -l
Output
total 4232
-rw-r--r-- 1 ephetteplace staff 2152700 Mar 16 12:56 more-names.txt
-rw-r--r-- 1 ephetteplace staff 137 Mar 16 16:11 names.txt
-rw-r--r-- 1 ephetteplace staff 223 Mar 16 12:56 script.sh
-rw-r--r-- 1 ephetteplace staff 106 Mar 16 12:56 sorted-names.txt
The very first segment of the listing is a representation of the files’ permissions. We don’t have time to fully understand it but suffice to say that “r” stands for read permissions, the ability to view the file, “w” stands for write permissions, the ability to modify the file, and what none of our files evince is an “x” permission for the ability to execute the file as a program. These permissions are referred to as a file’s “mode” and the chmod
command changes the mode of the file:
Input
$ chmod +x script.sh
$ ls -l
Output
total 4232
-rw-r--r-- 1 ephetteplace staff 2152700 Mar 16 12:56 more-names.txt
-rw-r--r-- 1 ephetteplace staff 137 Mar 16 16:11 names.txt
-rwxr-xr-x 1 ephetteplace staff 223 Mar 16 12:56 script.sh
-rw-r--r-- 1 ephetteplace staff 106 Mar 16 12:56 sorted-names.txt
The +x
flag essentially means “give everyone executable permissions for this file”. Now we can reference our script without specifying an interpreter: ./script.sh
(recall that period .
references our current location so this is the full path to the script).
Whew, we’ve finally covered the hashbang and making something executable. But what about the for...do...done
phrase in our script? That was a for
loop which iterates over the output of the $(ls *.txt)
subshell. For-loops can be used to iterate over A) all the words in a sentence, or B) all the lines in a file.
$ # These two loops are equivalent
$ NAMES="Eric Adriadne Katharina Dixon"
$ for NAME in $NAMES
> do echo $NAME
> done
$ # translate space-separated text into newline-separated text, "\n" is a newline
$ echo $NAMES | tr ' ' '\n' > namesfile.txt
$ for NAME in $(cat namesfile.txt)
> do echo $NAME
> done
As with “if-then-fi”, Bash is loose about how these statements are arranged, we can write “do” on its own line or alongside the first line of the looped operations. What is nice is that Bash lets us type out “if” statements and loops over multiple lines: normally, when we insert a newline character by pressing Enter or Return the command line executes whatever text is the on prompt. But because Bash sees the “for” statement it changes the prompt to >
and waits until it sees a corresponding done
before executing our code.
Bash also has case
conditions, where you check a series of conditions and can specify a fallback if not are met, and while
loops that execute as long as a condition is true. These are less useful, in my opinion, and we won’t cover them.
What about passing parameters to scripts? Let’s write a script named “listnames.sh” that is like our demonstrative loops above but it accepts the names as parameter.
#!/usr/bin/env bash
NAMES=$1
for NAME in $NAMES
do echo $NAME
done
There are several special $
variables and $1
refers to the first argument passed to our script. Analogously, $2
will be the second argument, etc. Let’s finish our example by making our script executable and running it:
$ chmod +x listnames.sh
$ ./listnames.sh "John Jacob Jingleheimer Schmidt"
What do you think would happen if we didn’t quote the first argument here?
One final note about executing programs: we will frequently run into commands that our user is not allowed to execute, or which attempt to modify portions of the file or operating system we are not allowed to modify. Common examples include updating software with softwareupdate
on Mac or apt-get update
on Linux. In these instances, we can use the sudo
command as a prefix, which stands for superuser do.
If we run sudo softwareupdate
and then type in our password when prompted, sudo
runs softwareupdate
as the root, administrative user. sudo
is one of the first interactive commands we’ve seen, where the prompt pauses and awaits our input (another example is rm -i
). sudo
also lets us run commands as other, non-root, users. It is common to need to run commands on a web server as the same user as the web server software e.g. sudo -u apache ./clear_cache.php
might run a “clear_cache.php” PHP script as the “apache” user.
It should go without saying that allowing programs to run as the root user is inherently dangerous, as the elevated permissions allow almost anything to happen. Only run trusted programs with sudo
.
Customizing our shell
We’ve written a script, specified its interpreter, and made it executable. But it’s still not really like builtin commands like ls
or sort
because we have to reference it’s full path every time we use it, whether by typing ./script.sh
or /Users/username/Code4Lib-CLI-Workshop/script.sh
. How come we don’t need to use the full path to other commands? Let’s use this problem as an impetus to learn how to customize the shell and truly make it our own.
The special $PATH
variable is a nuanced feature of most shells and the cause of a great deal of errors. PATH
is a colon-separated list of folders where the shell looks for executable programs. Since it knows about these programs, it doesn’t require you to type out their full path and they are available for tab-completion. Very often, when we install software like node.js, python, or ruby programs we will need to modify our PATH to search in the folders where these languages install executables.
What are the contents of your $PATH
? Try running echo $PATH
to find out. Note that the folder paths are separated by colons :
. Because our “Code4Lib-CLI-Workshop” folder is not listed, executables in it cannot be referenced directly by their name. To modify our PATH
we append a new folder’s path to the variable itself like so:
Input
$ # add our current location to our PATH
$ PATH="$PATH:/Users/ephetteplace/Code4Lib-CLI-Workshop"
$ echo $PATH
Output
/usr/local/bin:/usr/local/sbin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/ephetteplace/Code4Lib-CLI-Workshop
A better way to do this is to use a subshell: PATH="$PATH:"$(pwd)
appends our current location to the PATH
.
Isn’t appending something to itself what caused an infinite loop that crashed our hard drive earlier? Yeah, it is, but this problem doesn’t apply to variables, go figure.
Your PATH
may look different but should contain a bunch of similar-looking paths ending in “bin” for binary, which I think is an artifact of the days when the only programs were compiled binaries. Now what happens if we run script.sh
without a full path? What will happen if we move to a different location and run it? Now, try closing and restarting your terminal then running script.sh
again. The $PATH
resets with each session so it will not work.
Modifying the PATH
was helpful, but now every time we open our terminal, if we have a folder of scripts we want to use, we have to first add them to the PATH
again. It would be better if Bash knew that we wanted to perform certain PATH
modifications every time we ran the shell. Luckily, every time we run Bash it looks for a special “.bash_profile” file in our user’s home folder and executes it like a script. In fact, we may already have a “.bash_profile” file without even knowing it.
Let’s do more than permanently modify our PATH
, let’s set up some convenient shortcuts. I run some commands so frequently that reducing them from four characters to one is a meaningful improvement. The alias
command lets us map longer commands to short aliases which, when entered into the CLI, are expanded as if Bash did a search-and-replace operation just before execution. So let’s open .bash_profile and add a few lines to it:
# .bash_profile
export PATH="$PATH:/Users/ephetteplace/Code4Lib-CLI-Workshop"
# a better, more-informative list
alias l="ls -al"
# a "count lines" command
alias cl="wc -l"
export
is another new command, it means that the environmental variable we’re changing (in this instance, PATH
) will be exported to any subshells. In general, anytime we change a variable in .bash_profile, we will want to export
it or else the scripts we write or subshells we run may not work as expected.
We can also modify our prompt, the text that surrounds each command. This is done by modifying the PS1
variable. You can insert the return values of various commands into this prompt but there are also a series of special escaped character codes that PS1
interprets. Here is a more complete list but let’s look at just a few notable ones:
Code | Meaning |
---|---|
\d | the current date in “Weekday Month Date” format (e.g., “Fri Mar 26”). |
\h | the hostname i.e. your computer’s name |
\s | the name of the shell e.g. “bash” |
\t | the time in 24-hour HH:MM:SS format |
\u | your username |
\w | your current location |
Let’s continue editing our .bash_profile to have a nicer prompt that cuts across two lines.
# .bash_profile
export PATH="$PATH:/Users/ephetteplace/Code4Lib-CLI-Workshop"
# a better, more-informative list
alias l="ls -al"
# a "count lines" command
alias cl="wc -l"
# looks like "ephetteplace @ computerName in /Some/path"
# then a newline with a "$" prompt
export PS1="\u @ \h in \w\n$ "
What about nice colors? Changing the color of text on the command line involves a bunch of wild-looking codes that differ depending on environment. You can see my .bash_profile as an example. Fish shell, incidentally, has a much nicer coloring system that uses a simple set_color
command.
A Conclusion of sorts
It makes sense that many people, even those in information professions, do not use the command line. It is archaic, pedantic, error-prone, and full of weird conventions dating back to a bygone era of computing. But I hope that these final sections pointed out two of its immense powers: interoperability and customizability. No doubt, as you have been working through these materials, certain things have seemed senseless or frustrating. But setting up a few thoughtful aliases can alleviate much of the CLI’s problems. Writing out a long script to perform a series of data munging operations can save hours of labor each time it’s used. The command line is from an older era of computing, yes, but also an era largely devoid of opacity, of black boxes. There is an incredible freedom in being able to specify the most subtle nuances of the environment you work within.
To become familiar with the command line, I forced myself to perform basic tasks—managing files, editing simple text, renaming things—there even though it was less convenient than using a GUI file system. Eventually, I got to the point where I could identify tasks suitable for automation, so I applied the same tenant: even if it meant taking up much more time initially, I wrote scripts to perform repeated tasks. Now, I spend a lot of time on the CLI not only because it’s efficient but because it’s enjoyable. It welcomes you to find creative solutions that involve piecing together complex operations from the tiny building blocks of focused but deceptively powerful tools like sort
and tail
. Hopefully. the tools and examples listed on the further learning page inspire you to find similar solutions in your own life.