I really don't want to write this tutorial, but all the existing Bash tutorials are so horrible that I have no choice. I looked at books, websites, and YouTube - all horrible. They don't start with the basics. They include all kinds of useless crap. And they don't explain core concepts. So I have no choice but to write this for my Learn Reactionary Programming Bash lesson.
Bash is a shell, one of many, but the one I prefer. I will focus on Mac and Windows. I don't have Linux, and I hate Linux, so I won't discuss it. Most of Bash is the same on Mac and Windows, but where they differ, I will discuss both.
How you access Bash depends on your operating system. If you are on a Mac then you access Bash through the Mac Terminal which is found in "Applications > Utilities > Terminal.app". Be sure to set the default shell to Bash. If you are on Windows then install MSYS2. The default terminal isn't so good, so I suggest using the Windows Terminal.
When I start Bash on my Mac I see:
Last login: Thu Jan 4 23:25:35 on ttys004
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.
~ $
On Windows - MSYS2 I just see:
~ $
The line with the $
is the command prompt. The cursor is at the end of it, and if I type, my text will go there. You may have different text before the $
which is okay, but the line should end with $
. If it doesn't, something is wrong.
Now type "qqq". When I say type "whatever", you should type return/enter at the end. Only when you type return/enter will Bash process what you typed. Now you should see:
~ $ qqq
-bash: qqq: command not found
~ $
Bash doesn't know what "qqq" means and says so. Now try the following... Note that you type what is after the $
and Bash should respond as shown.
~ $ echo hi
hi
~ $ echo how are you
how are you
~ $ echo bye
bye
The echo
command just echoes what comes after. Now press the up-arrow on your keyboard. This should put the previous command where your cursor is. Up-arrow again brings the command before that. Try down-arrow and left-arrow and right-arrow. You can use this to navigate through your command history. The delete key also works for editing lines. And of course you can type. When you press return/enter then Bash will get your edited command and process it.
When you enter echo how are you
, echo
is the command. This command has 3 arguments: how
, are
, and you
. Commands and arguments are separated with spaces. It doesn't matter how many spaces, so:
~ $ echo how are you
how are you
echo
just returns the arguments separated by one space.
~ $ echo one; echo two
one
two
You can put multiple commands on one line separated by a ;
.
Enter:
~ $ man echo
You should get something like:
ECHO(1) BSD General Commands Manual ECHO(1)
NAME
echo -- write arguments to the standard output
SYNOPSIS
echo [-n] [string ...]
DESCRIPTION
The echo utility writes any specified operands, separated by single blank
(` ') characters and followed by a newline (`\n') character, to the stan-
dard output.
The following option is available:
-n Do not print the trailing newline character. This may also be
achieved by appending `\c' to the end of the string, as is done by
iBCS2 compatible systems. Note that this option as well as the
effect of `\c' are implementation-defined in IEEE Std 1003.1-2001
(``POSIX.1'') as amended by Cor. 1-2002. Applications aiming for
maximum portability are strongly encouraged to use printf(1) to
suppress the newline character.
:
But if you are on Windows, you may not have man
installed. In that case, do:
~ $ pacman -S man-db
to install man
as described here and then try man echo
again.
The man
command shows documentation of commands. Unfortunately it has a silly user interface based on memorizing keys, so I will just tell you the few keys you need. Down-arrow and up-arrow move down and up by one line. The space key moves down by one page. And most importantly, typing "q" quits and takes you back to Bash. You just have to memorize this.
Now try entering man man
. You don't need all the stuff shown, but you can see what a complicated man page looks like. You can use man
to get the documentation of other commands as I discuss them.
You should be familiar with Mac Finder or Windows File Explorer, and you should know from this that directories (also called "folders") are organized into a tree.
On Mac:
~ $ pwd
/Users/fschmidt
~ $ open .
~ $
On Windows:
~ $ pwd
/home/fschmidt
~ $ explorer .
~ $
When using Bash, you are always in some directory, called the current directory or the working directory. pwd
shows you the full path to this directory. Do man pwd
for details. open .
should open the Mac Finder for the current directory, and explorer .
should open the Windows File Explorer for the current directory.
Continuing on my Mac:
~ $ mkdir learn
~ $ cd learn
~/learn $ pwd
/Users/fschmidt/learn
mkdir
makes a directory in the current directory. You should be able to see the created directory in Mac Finder or Windows File Explorer. cd
stands for "change directory". This changes the current directory. cd
is a built-in command (built into Bash), and man
isn't useful with built-in commands, so instead of man cd
, try help cd
. Continuing...
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ ls
~/learn $ touch file1
~/learn $ ls
file1
~/learn $ touch file2
~/learn $ touch file3
~/learn $ ls
file1 file2 file3
~/learn $ mkdir dir1
~/learn $ ls
dir1 file1 file2 file3
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ ls -a
. .. dir1 file1 file2 file3
~/learn $ ls -a -F
./ ../ dir1/ file1 file2 file3
~/learn $ ls -aF
./ ../ dir1/ file1 file2 file3
ls
lists files and touch
creates an empty file. Arguments that start with "-" are options. Do man ls
to see what the options I used do. -F
appends a "/" to directories, and -a
shows files starting with "." which are usually hidden. Options can be combined.
~/learn $ ls file1
file1
~/learn $ ls qqq
ls: qqq: No such file or directory
~/learn $ ls file1 qqq file2
ls: qqq: No such file or directory
file1 file2
~/learn $ ls dir1
~/learn $ touch dir1/d1file
~/learn $ ls dir1
d1file
~/learn $ ls -d dir1
dir1
~/learn $ ls file1 file2 dir1
file1 file2
dir1:
d1file
~/learn $ ls -d file1 file2 dir1
dir1 file1 file2
~/learn $ ls -dF file1 file2 dir1
dir1/ file1 file2
Without file arguments, ls
lists files in the current directory. With file arguments, it lists those files if they exist. If the file is a directory, it will list what is in the directory unless the -d
option is used.
~/learn $ ls
dir1 file1 file2 file3
~/learn $ ls .
dir1 file1 file2 file3
~/learn $ ls -d .
.
~/learn $ ls -dF .
./
~/learn $ ls ./file1
./file1
~/learn $ ls dir1
d1file
~/learn $ ls ./dir1
d1file
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ cd .
~/learn $ pwd
/Users/fschmidt/learn
.
is the current directory.
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ cd dir1
~/learn/dir1 $ pwd
/Users/fschmidt/learn/dir1
~/learn/dir1 $ ls .
d1file
~/learn/dir1 $ ls ..
dir1 file1 file2 file3
~/learn/dir1 $ cd ..
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ cd dir1
~/learn/dir1 $ pwd
/Users/fschmidt/learn/dir1
~/learn/dir1 $ cd ../..
~ $ pwd
/Users/fschmidt
~ $ cd learn
~/learn $ pwd
/Users/fschmidt/learn
..
is the parent directory.
~/learn $ echo *
dir1 file1 file2 file3
~/learn $ echo d*
dir1
~/learn $ echo f*
file1 file2 file3
~/learn $ echo *1
dir1 file1
~/learn $ echo dir1/*
dir1/d1file
~/learn $ echo */*
dir1/d1file
~/learn $ echo qqq*
qqq*
*
does wildcard matching of files. It is important to understand that Bash does the wildcard matching and then passes the resulting arguments to the command. echo
never sees the "*" unless there is no match.
~/learn $ ls *
file1 file2 file3
dir1:
d1file
~/learn $ ls -dF *
dir1/ file1 file2 file3
~/learn $ ls -dF d*
dir1/
~/learn $ ls -dF f*
file1 file2 file3
~/learn $ ls -dF *1
dir1/ file1
~/learn $ ls dir1/*
dir1/d1file
~/learn $ ls */*
dir1/d1file
~/learn $ ls -dF qqq*
ls: qqq*: No such file or directory
Should be self-explanatory.
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ cd ~
~ $ pwd
/Users/fschmidt
~ $ cd learn/dir1
~/learn/dir1 $ pwd
/Users/fschmidt/learn/dir1
~/learn/dir1 $ cd
~ $ pwd
/Users/fschmidt
~ $ cd ~/learn
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ echo ~
/Users/fschmidt
~/learn $ echo .
.
~/learn $ echo ..
..
~
means your home directory. cd
without arguments is the same as cd ~
. ~
is expanded into your home directory by Bash.
~/learn $ ls -ltF
total 0
drwxr-xr-x 3 fschmidt staff 96 Jan 5 02:33 dir1/
-rw-r--r-- 1 fschmidt staff 0 Jan 5 02:21 file3
-rw-r--r-- 1 fschmidt staff 0 Jan 5 02:21 file2
-rw-r--r-- 1 fschmidt staff 0 Jan 5 02:21 file1
-l
gives you this ugly techy format. You get the date that the file was last modified. Before the date is the file size. -t
sorts by date descending.
Lastly I will describe autocompletion. I type echo d
without enter/return but instead then press the tab key. It autocompletes to echo dir1/
. I press tab again and it autocompletes to echo dir1/d1file
. Pressing tab while entering a file or directory makes Bash try to autocomplete using matching file names. If I enter echo f
and press tab, I get echo file
. It doesn't know which to choose next. Another tab just beeps. And another tab shows me the options like this:
~/learn $ echo file
file1 file2 file3
~/learn $ echo file
In general, you can press tab anytime while entering a file name and see what happens. Autocompletion saves a lot of typing.
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ cp file1 copied
~/learn $ ls -F
copied dir1/ file1 file2 file3
~/learn $ mv copied moved
~/learn $ ls -F
dir1/ file1 file2 file3 moved
~/learn $ rm moved
~/learn $ ls -F
dir1/ file1 file2 file3
cp
copies files or directories. mv
moves files or directories. rm
removes files or directories. See the man
pages of these commands for details.
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ mkdir dir2
~/learn $ touch dir2/d2file
~/learn $ ls -F
dir1/ dir2/ file1 file2 file3
~/learn $ ls dir2
d2file
~/learn $ rm dir2
rm: dir2: is a directory
~/learn $ rm -d dir2
rm: dir2: Directory not empty
~/learn $ rm dir2/d2file
~/learn $ rm -d dir2
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ mkdir dir2
~/learn $ touch dir2/d2file
~/learn $ ls -F
dir1/ dir2/ file1 file2 file3
~/learn $ rm -r dir2
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ cp dir1 dir2
cp: dir1 is a directory (not copied).
~/learn $ cp -r dir1 dir2
~/learn $ ls -F
dir1/ dir2/ file1 file2 file3
~/learn $ ls dir2
d1file
~/learn $ cp f* dir2
~/learn $ ls dir2
d1file file1 file2 file3
~/learn $ rm -r dir2
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ ls -F
dir1/ file1 file2 file3
~/learn $ mkdir dir2
~/learn $ cp -r dir1 dir2
~/learn $ ls -F dir2
dir1/
~/learn $ ls -F dir2/dir1
d1file
~/learn $ rm -r dir2
~/learn $ ls -F
dir1/ file1 file2 file3
I could explain all this, but I won't. You should learn to understand commands and their options using man
and by playing with them. Don't continue until you completely understand the above.
~/learn $ echo a b
a b
~/learn $ echo "a b"
a b
~/learn $ echo 'a b'
a b
~/learn $ echo "a b" c
a b c
Bash treats text in quotes as one argument. So in echo a b
, echo
has two arguments: "a" and "b". In echo "a b"
, echo
has one argument: "a b". In echo 'a b'
, echo
has one argument: "a b". In echo "a b" c
, echo
has two arguments: "a b" and "c".
~/learn $ echo a\ \ \ b
a b
Outside of quotes, \
is not treated as a separator, but rather is treated as a space character that is part of the argument.
~/learn $ echo $X
~/learn $ X="some text"
~/learn $ echo $X
some text
~/learn $ echo "X is: $X"
X is: some text
~/learn $ echo 'X is: $X'
X is: $X
~/learn $ X="$X and more"
~/learn $ echo $X
some text and more
Here X
is a variable. You get its value with $X
. This also works inside double-quotes but not inside single-quotes.
There are special variables called environment variables that are used by Bash.
~/learn $ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/fschmidt/Dropbox/bin:/Users/fschmidt/hg/luan/scripts:/usr/local/opt/postgresql@9.5/bin
~/learn $ which ls
/bin/ls
~/learn $ cd /bin
/bin $ pwd
/bin
/bin $ ls
[ dd launchctl pwd test
bash df link rm unlink
cat echo ln rmdir wait4path
chmod ed ls sh zsh
cp expr mkdir sleep
csh hostname mv stty
dash kill pax sync
date ksh ps tcsh
/bin $ ls -F
[* dd* launchctl* pwd* test*
bash* df* link* rm* unlink*
cat* echo* ln* rmdir* wait4path*
chmod* ed* ls* sh* zsh*
cp* expr* mkdir* sleep*
csh* hostname* mv* stty*
dash* kill* pax* sync*
date* ksh* ps* tcsh*
/bin $ cd ~/learn
~/learn $
PATH
is an environment variable containing a list of directories separated by :
that are searched for commands by Bash. The which
command shows the full path to a command. ls -F
appends a *
to executable files.
~/learn $ subl file1
-bash: subl: command not found
~/learn $ "/Applications/Sublime Text.app/Contents/SharedSupport/bin/subl" file1
~/learn $ PATH="$PATH:/Applications/Sublime Text.app/Contents/SharedSupport/bin"
~/learn $ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/fschmidt/Dropbox/bin:/Users/fschmidt/hg/luan/scripts:/usr/local/opt/postgresql@9.5/bin:/Applications/Sublime Text.app/Contents/SharedSupport/bin
~/learn $ subl file1
~/learn $
Here I edit the file file1
with Sublime Text, first by using the full path, and then by adding the directory to PATH
so that Bash can find subl
.
I have Microsoft Word on Windows. From the Windows Command Prompt (not Bash):
C:\Users\fschmidt>winword
C:\Users\fschmidt>where winword
C:\Program Files\Microsoft Office\root\Office16\WINWORD.EXE
winword
runs Microsoft Word. The Command Prompt where
command is like the Bash which
command. So now on MSYS2:
~ $ winword
bash: winword: command not found
~ $ echo $PATH
/usr/local/bin:/usr/bin:/bin:/opt/bin:/c/Windows/System32:/c/Windows:/c/Windows/System32/Wbem:/c/Windows/System32/WindowsPowerShell/v1.0/:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/c/Program Files/TortoiseHg:/c/Program Files/Java/jdk1.8.0_202/bin
~ $ PATH="$PATH:/c/Program Files/Microsoft Office/root/Office16"
~ $ echo $PATH
/usr/local/bin:/usr/bin:/bin:/opt/bin:/c/Windows/System32:/c/Windows:/c/Windows/System32/Wbem:/c/Windows/System32/WindowsPowerShell/v1.0/:/usr/bin/site_perl:/usr/bin/vendor_perl:/usr/bin/core_perl:/c/Program Files/TortoiseHg:/c/Program Files/Java/jdk1.8.0_202/bin:/c/Program Files/Microsoft Office/root/Office16
~ $ winword
~ $
Returning to the Mac, there is another way to run applications found in Finder's "Applications" simply as applications instead of as commands.
~/learn $ open -a 'Sublime Text' file1
Another useful environment variable is PS1
which controls the command prompt. I already have this set up, but if I didn't:
Franklins-MacBook-Pro:learn fschmidt$ echo $PS1
\h:\W \u\$
Franklins-MacBook-Pro:learn fschmidt$ PS1="\w $ "
~/learn $ echo $PS1
\w $
~/learn $
Google "bash PS1" for more info.
~/learn $ cd
~ $ ls .bash_profile
.bash_profile
If .bash_profile
isn't found then do touch .bash_profile
to create it. This file contains Bash commands that are run when Bash starts. If you already have this file, it is likely to contain comments that start with #
. Comments are ignored like this:
~ $ # comment line, does nothing
~ $ echo whatever # end of line comment
whatever
~ $
To edit .bash_profile
on a Mac, you can do:
~ $ open -a 'Sublime Text' .bash_profile
To edit .bash_profile
on Windows, you can do:
~ $ notepad .bash_profile
Now try adding this line to .bash_profile
:
echo hello there
Now when you open a new Bash terminal, you should see "hello there". .bash_profile
runs when Bash is started by opening a new Bash terminal.
I set PS1
and PATH
in .bash_profile
to have the command prompt I want, and access to the commands that I want. I suggest that you make the Sublime Text command subl
available in PATH
.
~/learn $ find .
.
./file3
./file2
./file1
./dir1
./dir1/d1file
~/learn $ find . -name 'file*'
./file3
./file2
./file1
~/learn $ find . -name '*file'
./dir1/d1file
~/learn $ find . -name 'd*'
./dir1
./dir1/d1file
~/learn $ find . -name '*1' -or -name '*2'
./file2
./file1
./dir1
find
recursively searches for files in a directory tree. Note that in this case the *
wildcard matching is not being done by Bash, it is being done by find
. find
has many options for searching for files and acting on them, see man find
.
~/learn $ echo 'this is a test' >test.txt
~/learn $ ls -F
dir1/ file1 file2 file3 test.txt
~/learn $ cat test.txt
this is a test
~/learn $ echo 'this is another test' >test.txt
~/learn $ cat test.txt
this is another test
~/learn $ echo 'another line' >>test.txt
~/learn $ cat test.txt
this is another test
another line
~/learn $ cat <test.txt
this is another test
another line
~/learn $ cat <<End >test.txt
> I am typing this
> and this
> End
~/learn $ cat test.txt
I am typing this
and this
~/learn $ (echo one; echo two) >test.txt
~/learn $ cat test.txt
one
two
All programs have standard input, standard output, and standard error. Programs write normal output to standard output and error messages to standard error. By default, standard input comes from the terminal, and standard output and standard error go to the terminal, but this can be changed. >file
sends standard output to file
. >>file
appends standard output to file
. <file
reads standard input from file
. <<whatever
reads standard input from the text that follows until a line with just whatever
. Commands can be combined between (
and )
. Be sure to man cat
to understand how cat
works.
~/learn $ ls >ls.txt
~/learn $ cat ls.txt
dir1
file1
file2
file3
ls.txt
test.txt
~/learn $ ls -d f* q* >ls.txt
ls: q*: No such file or directory
~/learn $ cat ls.txt
file1
file2
file3
~/learn $ ls -d f* q* 2>ls.txt
file1 file2 file3
~/learn $ cat ls.txt
ls: q*: No such file or directory
~/learn $ ls -d f* q* | tee ls.txt
ls: q*: No such file or directory
file1
file2
file3
~/learn $ cat ls.txt
file1
file2
file3
~/learn $ ls -d f* q* 2>&1 | tee ls.txt
ls: q*: No such file or directory
file1
file2
file3
~/learn $ cat ls.txt
ls: q*: No such file or directory
file1
file2
file3
2>file
sends standard error to file
. |
sends standard output of the previous command to standard input of the following command. 2>&1
sends standard error to standard output. tee file
reads standard input and then writes it to both standard output and to file
.
~/learn $ find . -type f | wc -l
6
There are 6 files in learn
. Use man
to figure out how this works.
~/learn $ sleep 3
~/learn $ sleep 30
^C
~/learn $
sleep 3
sleeps for 3 seconds, meaning it does nothing for 3 seconds. I waited 3 seconds for this command to finish. Then I ran sleep 30
which would sleep for 30 seconds, but I lost my patience and pressed control+c which interrupts the program and breaks out of it. You can try control+c if you ever get stuck waiting for a command to finish.
~/learn $ wc
I am typing this
and this
now I will end my input with control+d
3 14 65
~/learn $ wc
this time I will use control+c to break out
^C
~/learn $
Control+d means end of input.
~/learn $ echo I am in $(pwd)
I am in /Users/fschmidt/learn
~/learn $ echo this directory contains: $(ls)
this directory contains: dir1 file1 file2 file3 ls.txt test.txt
~/learn $ echo this directory contains $(ls | wc -l) files
this directory contains 6 files
cmd $(commands)
will use the output of commands
as argument text for cmd
.
~/learn $ cat $(find . -type f) | wc -c
86
The files in learn
contain a total of 86 bytes. Use man
to figure out how this works.
~/learn $ (sleep 5; echo done) &
[1] 10080
~/learn $ echo waiting
waiting
~/learn $ done
[1]+ Done ( sleep 5; echo done )
~/learn $
Normally Bash waits for a command to complete before showing the command prompt and allowing input. But ending a command line with &
tells bash not to wait, but instead to run the command in a separate process. Above in ~/learn $ echo waiting
, I typed in echo waiting
. But in ~/learn $ done
, I did not type done
. Instead this was produced by echo done
after 5 seconds. [1] 10080
tells me that a process was started and [1]+ Done ( sleep 5; echo done )
tells me that the process finished.
This is useful where you do not want to wait for a command to finish. Consider this on Windows:
~ $ notepad
Here you will not get a command prompt again until you quit Notepad because Bash is waiting for this command to finish. So instead do:
~ $ notepad &
[1] 2010
~ $
Now Notepad will run and you can continue using Bash.
Make a file called test.sh
containing the following:
echo this is a shell script
Now from Bash:
~/learn $ cat test.sh
echo this is a shell script
~/learn $ ./test.sh
-bash: ./test.sh: Permission denied
~/learn $ ls -F test.sh
test.sh
~/learn $ chmod +x test.sh
~/learn $ ls -F test.sh
test.sh*
~/learn $ ./test.sh
this is a shell script
~/learn $
chmod +x file
makes file
into an executable that can be run. Now I will edit test.sh
~/learn $ # edit test.sh
~/learn $ cat test.sh
nonsense
echo this is a shell script
~/learn $ ./test.sh
./test.sh: line 1: nonsense: command not found
this is a shell script
~/learn $ # edit test.sh
~/learn $ cat test.sh
set -e
nonsense
echo this is a shell script
~/learn $ ./test.sh
./test.sh: line 2: nonsense: command not found
~/learn $
By default, scripts continue running after an error. In longer scripts, we want the script to exit after an error. set -e
does this, see help set
.
~/learn $ X=some
~/learn $ echo $X
some
~/learn $ echo $Xthing
~/learn $ echo ${X}thing
something
~/learn $ # edit test.sh
~/learn $ cat test.sh
echo "\$* = $*"
echo "\$# = $#"
echo "\$0 = $0"
echo "\$1 = $1"
echo "\$2 = $2"
echo "\$3 = $3"
echo "\$4 = $4"
echo "\$14 = $14"
echo "\${14} = ${14}"
echo "\$@ = $@"
./count.sh "$*"
./count.sh "$@"
~/learn $ ./test.sh a b "c d"
$* = a b c d
$# = 3
$0 = ./test.sh
$1 = a
$2 = b
$3 = c d
$4 =
$14 = a4
${14} =
$@ = a b c d
1
3
~/learn $ cat count.sh
echo $#
~/learn $
Bash scripts have special defined variables. The difference between $*
and $@
is subtle, and you will usually just use $*
. $*
returns all arguments as one string while $@
returns the arguments separately, but this distinction rarely makes any difference.
~/learn $ X=value
~/learn $ echo $X
value
~/learn $ # edit test.sh
~/learn $ cat test.sh
echo "\$X = $X"
~/learn $ ./test.sh
$X =
~/learn $ export X
~/learn $ ./test.sh
$X = value
Variables are defined in the current shell. Shell scripts are run in their own shell. So by default, they don't see variables defined in the terminal/parent shell. export var
makes var
available in descendant processes, meaning available in shell scripts. It is a good idea to do export PATH
in .bash_profile
so that your PATH is available to your scripts.
~/learn $ X=terminal
~/learn $ echo $X
terminal
~/learn $ # edit test.sh
~/learn $ cat test.sh
X=script
export X
~/learn $ ./test.sh
~/learn $ echo $X
terminal
~/learn $ . test.sh
~/learn $ echo $X
script
You can export a variable from parent to children but not from children to parent. . script
includes the text in the file script
in the current shell. In this case, it is not run in a separate shell. This is the only way to have a script set variables in your terminal shell.
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ # edit test.sh
~/learn $ cat test.sh
cd ~
~/learn $ ./test.sh
~/learn $ pwd
/Users/fschmidt/learn
~/learn $ . test.sh
~ $ pwd
/Users/fschmidt
~ $ cd learn
~/learn $
This illustrates the difference between ./script
and . script
.
~/learn $ echo $PATH
/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin:/Users/fschmidt/Dropbox/bin:/Users/fschmidt/hg/luan/scripts:/usr/local/opt/postgresql@9.5/bin:/Applications/Sublime Text.app/Contents/SharedSupport/bin
~/learn $ echo ~/Dropbox/bin
/Users/fschmidt/Dropbox/bin
~/learn $ ls -F ~/Dropbox/bin/e
/Users/fschmidt/Dropbox/bin/e*
~/learn $ cat ~/Dropbox/bin/e
open -a 'Sublime Text' $*
~/learn $ e test.sh
~/learn $
When you write useful scripts, put them in a directory and add that directory to your PATH. I use ~/Dropbox/bin
and I have a script named e
in that directory for editing files. So e test.sh
lets me edit test.sh
from the command line.
Here is a more advanced script called undocx.sh
that unpacks a Word DOCX file.
#!/bin/bash
set -e
if [ $# -ne 1 ]; then
echo "usage: $0 filename"
exit 1
fi
FILE="$1"
NEWDIR=$(basename $FILE .docx)
mkdir $NEWDIR
unzip $FILE -d $NEWDIR
export XMLLINT_INDENT=$'\t'
for file in $(find $NEWDIR -name "*.xml" -o -name "*.rels"); do
mv "$file" temp.xml
xmllint --format temp.xml >"$file"
done
rm temp.xml
Bash is a full programming language containing all the usual features. Some commands in my script are well explained by man
, but some are not. In particular, the documentation for if
and for
are poor. In cases like this, I suggest asking ChatGPT like this:
Please explain the Bash "if" statement.
Please explain the Bash "for" statement.
ChatGPT knows Bash well. I trust ChatGPT to explain details but not to explain core concepts. You can also try Google, but ChatGPT is better than modern programmers.
At least 90% of your usage of Bash will be simple commands that you enter in the terminal. Try to use Bash as much as possible instead of using the GUI so that you get practice using it. Unless you become system administrator, you won't use advanced scripting much. But with a solid understanding of the core basics, you should be able to figure out how to read or write advanced scripts when needed.