So what is the shell? The shell is just a general name for any user
space program that allows access to resources in the system, via
some kind of interface.
Shells come in many different flavours but are generally provided to
aid a human operator in accessing the system. This could be
interactively, by typing at a terminal, or via scripts, which are
files that contain a sequence of commands.
For example, to see all of the files in a folder, the human operator
could write a program in a language such as C, making system calls to
do what they want. But for day-to-day tasks, this would be
repetitive. A shell will normally offer us a quick way to do that
exact task, without having to manually write a program to do it.
We're not directly interacting with the 'shell' in this
diagram. We're actually using a terminal. When a user wants to work
with a shell interactively, using a keyboard to provide input and a
display to see the output on the screen, the user uses a terminal.
The shell is the program that is going to take input from somewhere
and run a series of commands. When the shell is running in a
terminal, it is normally taking input interactively from the
user. As the user types in commands, the terminal feeds the input to
the shell and presents the output of the shell on the screen.
Executables are programs your system can use; your shell just calls
out to them.
Executables are just files with the 'executable' bit set. If I
execute the cat command, the shell will search for an executable named
cat in my $PATH. If it finds it, it will run the program.
$PATH is the standard environment variable used to define where the
shell should search for programs.
The shell will start with the earlier locations and move to the later
ones. This allows local flavours of tools to be installed for users,
which will take precedence over general versions of tools.
Executables don't have to be compiled program code, they can be
scripts. If a file starts with #! (the 'shebang'), then the system
will try to run the contents of the file with the program specified in
the shebang.
"Built-Ins"
Builtins are very shell-specific and usually control the shell itself
Some commands are a builtin so that they can function in a sensible
manner. For example, cd command changes the current directory - if we
executed it as a process, it would change only the directory for the
cd process itself, not the shell, making it much less useful.
Echo is builtin because the shell can run much more quickly by not
actually running a program if it has its own built in implementation.
Functions
Functions are powerful ways to write logic but will normally be shell-specific.
g/re/p , This command ran on all lines (g, for global),
applied a regular expression (re, for regular expression) and then
printed (p for print) the results.
The xargs [build and execute commands] command takes input,
uses the input to create commands, then executes the commands. I
tend to remember it as "Execute with Arguments" as the name xargs
sounds a little odd!
By default xargs take the input, joins each line together with a
space and then passes it to the echo command.
My general advice for regular expressions is start simple and add complexity only if you need it.
We can build regular expressions using an 'iterative' process,
starting with the basics, then adding more features as we need them.
Let's take validating an email address as an example. The way I would
build a regular expression to validate an email address would be to
use the following steps:
Create a small list of valid email address
Add some items to the list which look 'kind of' valid but are not quite right
Build a regular expression which matches the correct email address
Refine the expression to eliminate the invalid addresses
In most cases this will be sufficient.
I would advise that you keep expressions simple if possible - if they
are getting too complex then break up your input or break up the
processing into smaller chunks of work!
Remember that a regular expression does not have to be the only way
you validate input. You might use a regular expression to do a quick
check on a form on a website to make sure that an email address has at
least the correct structure, but you might then use a more
sophisticated check later on (such as sending the user an activation
email) to actually confirm that the address actually belongs to the
user.
A shell script is just a text file which contains a set of commands.
当你发现总是重复敲一系列命令的时候,就可以考虑将这些重复的序列写脚本,这样有几个好处:
节省时间,不用每次敲一些重复的命令
可以使用你喜欢的编辑器编辑脚本,添加注释描述你想实现的事情,可以利用 git 管理版本
作为脚本文件,便于机器之间的分享,与人之间的分享
实现一个 'common' 命令
Read a large number of commands from the history
Sort the commands, then count the number of duplicates
Sort this list showing the most commonly run commands first
Print the results to the screen.
# Write the title of our command.
echo "common commands:"
# Show the most commonly used commands.
tail ~/.bash_history -n 1000 | sort | uniq -c | sed 's/^ *//' | sort -n -r | head -n 10
# Show the most commonly used commands.
tail ~/.bash_history -n 1000 \
| sort \
| uniq -c \
| sed 's/^ *//' \
| sort -n -r \
| head -n 10
Be careful when you split lines up - the continuation character must
be the last character on the line. If you add something after it (such
as a comment) then the command will fail.
A shebang is a special set of symbols at the beginning of a file that
tells the system what program should be used to run the file.
The shebang is the two characters - #!. The name 'shebang' comes from
the names of the symbols. The first symbol is a 'sharp' symbol
(sometimes it is called a hash, it depends a little on context). The
second symbol is an exclamation point. In programming the exclamation
point is sometimes called the 'bang' symbol. When we put the two
together, we get 'sharp bang', which is shortened to 'shebang'.
之前的脚本,可以加上 shebangs:
#!/usr/bin/sh# Write the title of our command.echo"common commands:"# Show the most commonly used commands.
tail ~/.bash_history -n 1000 | sort | uniq -c | sed 's/^ *//' | sort -n -r | head -n 10
此时就可以利用 env (set environment and execute command) ,它会去执行命令,并从 $PATH 上找到命令所在的路径。
#!/usr/bin/env bashecho"Hello from Bash"
Using a shebang to specify the exact command to run, and then using
the env command to allow the $PATH to be searched is generally the
safest and most portable way to specify how a shell script should run.
You can also use the source (execute commands from a file) command to
load the contents of a file into the current shell.
Remember that when we run a shell script, a new shell is created as a
child process of the current shell. This means that if you change
something in the environment, such as a variable, it will not affect
the environment of the shell that ran the script.
This works because when the shell sees a command, it searches through
the folders in the $PATH environment variable to find out where the
command is. And the /usr/local/bin folder is in this list of paths.
Why do we use the /usr/local/bin folder rather than the /usr/bin
folder? This is just a convention. In general, the /usr/bin folder is
for commands which are install ed with package manager tools like apt
or Homebrew (on MacOS). The /usr/local/bin folder is used for commands
which you create for yourself on your local machine and manage
yourself.
Variables are places where the system, the shell, or shell users like
ourselves can store data.
By convention, if a variable is in uppercase then it is an environment
variable or a built in variable that comes from the shell.
An environment variable is a variable that is set by the system. They
often contain useful values to help configure your system.
Variables that you define yourself should be lowercase.
This helps to distinguish between environment variables and your own
variables.
It is a good habit to use lowercase for variable names. Using
uppercase will work, but when you use uppercase you run the risk of
'overwriting' the value of an environment variable and causing
unexpected results later.
The variables we create in the Shell are called Shell Variables. They
are accessible in the current shell session that we are running.
Shell variables are isolated to the current process.
If we run another process from our shell, such as another shell script
or program, our shell variables are not inherited by this
process.
This is by design - these shell variables are expected to be used for
our local session only.
If you want to ensure that a variable is available to all child
processes, you can use the export (set export attribute) builtin to
tell the shell to export the variable as an Environment Variable.
Environment Variables are always inherited by child processes - so if
you need to provide some kind of configuration or context to a child
process, you will likely want to export your variable.
赋值和引用
# 通过 `=` 赋值变量,注意没有空格password="somethingsecret"# $(...) execute a set of commands in a 'sub shell'masked_password=$(echo"$password" | sed 's/./*/g')echo"Setting password '${masked_password}'..."# 显示引用变量 ${variable}# wrong,会找 USER_backup 变量,但找不到echo"Creating backup folder at: '$USER_backup'"
mkdir $USER_backup# correctecho"Creating backup folder at: '${USER}_backup'"
mkdir "${USER}_backup"
Arrays in Bash start at index zero. Arrays in the Z-Shell start at
index one - this can cause confusion and mistakes in scripts so it is
something you might have to consider if you are writing scripts that
can be used by either shell.
It's important to use curly braces around your array expressions.
days=("Monday""Tuesday""Wednesday""Thursday""Friday""Saturday""Sunday")echo"The first day is: ${days[0]}"echo"The last day is: ${days[6]}"
# Create an associative array called 'book'.declare -A book
# Set some values on the array.book[title]="Effective Shell"book[author]="Dave Kerr"# Show one of the values.echo"Book details: ${book[title]} - ${book[author]}"
There is often a lot of confusion about a specific topic in the
shell - when should you surround a variable in quotes?
This might sound like a purely stylistic question, but surrounding a
variable in quotes can dramatically change how your script works.
Quoting Tips:
Use double quotes most of the time - they will handle variables and
sub-shells for you and not do weird things like word splitting
Use single quotes for literal values
Use no quotes if you want to expand wildcards
Single Quotes - Literal Values
Single quotes should be used when you want to put special characters
into a variable, or call a command that includes whitespace or special
characters.
message=' ~~ Save $$$ on with ** "this deal" ** ! ~~ 'echo"$message"
Double Quotes - Parameter Expansion
Double quotes work in a very similar way to single quotes except that
they allow you to use parameter expansion with the $ dollar symbol and
escaping with the \ symbol.
deal="Buy one get one free"message="Deal is '$deal' - save \$"echo"$message"# `` 内的也在一个 sub-shell 执行,但应该避免使用,统一使用 $() 的形式echo"The date is `date`"
Shell Parameter Expansion is the process by which the shell evaluates
a variable that follows the $ dollar symbol.
But there are a number of special features we can use when expanding
parameters. There are many options available and you can find them all
by running man bash and searching for the text EXPANSION.
I would avoid these techniques if possible as they are fairly
specific to Bash and likely will be confusing to readers.
It is generally enough to know that if you see special symbols inside
a ${variable} expression then the writer is performing some kind of
string manipulation.
The read (read from standard input) command can be used to read a line
of text from standard input. When the text is read it is put into a
variable, allowing it to be used in our scripts.
The read command reads a line of text from standard input and stores
the result in a variable called REPLY. We can then use this variable
to use the text that was read.
In general you should provide a variable name for read - it will make
your script a little easier to understand. Not every user will know
that the $REPLY variable is the default location, so they might find
it confusing if you don't provide a variable name. By specifying a
variable name explicitly we make our script easier to follow.
# 默认存在 $REPLYecho"What is your name?"readecho"Hello, $REPLY"# 指定存值的变量echo"What is your name?"read name
echo"Hello, ${name}"# prompt (bash)read -p "Please enter your name: " name
echo"Hello, $name"# prompt (zsh)read"?Please enter your name: "echo"Hello, $REPLY"# The -s (silent) flag can be used to hide the input as it is being written.read -s -p "Enter a new password: " password
masked_password=$(echo"$password" | sed 's/./*/g')echo""echo"Your password is: $masked_password"# Limiting the Input# Use the -n flag with the value 1 to specify that we want to read a single character only.read -n 1 -p "Continue? (y/n): " yesorno
echo""echo"You typed: ${yesorno}"
if <test-commands>
then
<conditional-command 1>
<conditional-command 2>
<conditional-command n>
fi# 写在一行, 用 `;` 分隔if <test-commands>; then <conditional-command 1> <conditional-command 2> <conditional-command n>; fi
The if statement will run the 'test commands'. If the result of the
commands are all zero (which means 'success'), then each of the
'conditional' commands will be run. We 'close' the if statement with
the fi keyword, which is if written backwards.
if! test -d ~/backups
thenecho"Creating backups folder"
mkdir ~/backups
fi# 简写if![ -d ~/backups ]thenecho"Creating backups folder"
mkdir ~/backups
fi
if[ -x /usr/local/bin/common ]; thenecho"The 'common' command has been installed and is executable."elif[ -e /usr/local/bin/common ]; thenecho"The 'common' command has been installed and is not executable."elseecho"The 'common' command has not been installed."fi
# && 'and' || 'or'if[ $year -ge 1980 ] && [ $year -lt 1990 ]; thenecho"$year is in the 1980s"fi# -a 'and' -o 'or'if[ $year -ge 1980 ] && [ $year -lt 1990 ]; thenecho"$year is in the 1980s"fi# Chaining# Run command1, if it succeeds run command2.
command1 && command2
# Run command1, if it does not succeed run command2.
command1 || command2
Operator
Usage
-n
True if the length of a string is non-zero.
-z
True if the length of a string is zero.
-d
True if the file exists and is a folder.
-e
True if the file exists, regardless of the file type.
-f
True if the file exists and is a regular file.
-L
True if the file exists and is a symbolic link.
-r
True if the file exists and is readable.
-s
True if the file exists and has a size greater than zero.
-w
True if the file exists and is writable.
-x
True if the file exists and is executable - if it is a directory this checks if it can be searched.
file1 -nt file2
True if file1 exists and is newer than file2.
file1 -ot file2
True if file1 exists and is older than file2.
file1 -ef file2
True if file1 and file2 exist and are the same file.
var
True if the variable var is set and is not empty.
s1 = s2
True if the strings s1 and s2 are identical.
s1 != s2
True if the strings s1 and s2 are not identical.
n1 -eq n2
True if the numbers n1 and n2 are equal.
n1 -ne n2
True if the numbers n1 and n2 are not equal.
n1 -lt n2
True if the number n1 is less than n2.
n1 -le n2
True if the number n1 is less than or equal to n2.
n1 -gt n2
True if the number n1 is greater than n2.
n1 -ge n2
True if the number n1 is greater than or equal to n2.
case <expression> in
pattern1)
<pattern1-commands>
;;
pattern2 | pattern3)
<pattern2and3-commands>
;;
*)
<default-commands>
;;
esac
以 case 开头,以 esac 结束(反转了词序)。
read -p "Yes or no: " response
case"${response}" in
y | Y | yes | ok)
echo"You have confirmed"
;;
n | N | no)
echo"You have denied"
;;
*)
echo"'${response}' is not a valid response"
;;
esacread -p "Yes or no: " response
case"${response}" in[yY]*)
echo"You have (probably) confirmed"
;;
[nN]*)
echo"You have (probably) denied"
;;
*)
echo"'${response}' is not a valid response"
;;
esac
sentence="What can the harvest hope for, if not for the care of the Reaper Man?"for word in $sentencedoecho"$word"done
The reason is that the shell is a text based environment and the
designers have taken this into account. Most of the time when we are
running shell commands in a terminal we are running commands that
simply output text. If we want to be able to use the output of these
commands in constructs like loops, the shell has to decide how to
split the output up.
If the files that you are trying to loop through are too complex to
match with a shell pattern, you can use the find command to search for
files, then loop through the results.
# Create a symlink to 'effective-shell' that has a space in it...
ln -s ~/effective shell ~/effective\ shell
# Find all symlinks and print each one.links=$(find ~ -type l)for link in $linksdoecho"Found Link: $link"done
# Save the current value of IFS - so we can restore it later. Split on newlines.old_ifs=$IFS# We have to use the complex looking 'ANSI C Quoting' syntax to set $IFS to a newlineIFS=$'\n'# Find all symlinks and print each one.links=$(find ~ -type l)for link in $linksdoecho"Found Link: $link"done# Restore the original value of IFS.IFS=$old_ifs
The $IFS variable is the 'internal field separator' variable. It is
what the shell uses to decide what characters should be used to split
up text into words. By default, this variable includes the space
character, the tab character and the newline character.
I believe that in this case it is probably best to not use a shell
script. There is no solution that is particularly clean or simple. In
this case I think you might be better off using a programming
language.
Another common way to use a for loop is with brace expansion. Brace
expansion we have already seen a number of times so far - we can use
it to generate a sequence of values.
touch {coffee,tea,milkshake}-menu.txt
# loop through a sequence of values or a range of numbers with 'increment'for i in{0..25..5}doecho"Loop ${i}"done
The while loop is a loop that executes commands until a certain
condition is met.
基本结构:
while <test-commands>
do
<conditional-command 1>
<conditional-command 2>
<conditional-command n>
done
例子:
# Create an empty array of random numbers.random_numbers=()# As long as the length of the array is less than five, continue to loop.while[ ${#random_numbers[@]} -lt 5 ]do# Get a random number, ask the user if they want to add it to the array.random_number=$RANDOMread -p "Add $random_number to the list? (y/n): " choice
# If the user chose 'y' add the random number to the array.if["$choice" = "y"]; thenrandom_numbers+=($random_number); fidone# Show the contents of the array.echo"Random Numbers: ${random_numbers[@]}"
There are times that you may want to loop forever. For example you
might be writing a script that reads an option from the user,
processes it, and then starts again.
while truedoecho"1) Move forwards"echo"2) Move backwards"echo"3) Turn Left"echo"4) Turn Right"echo"5) Explore"echo"0) Quit"read -p "What will you do: " choice
if[ $choice -eq 0 ]; thenexitfi# The rest of the game logic would go here!# ...done
The until loop operates just like the while loop, except that it runs
until the test commands return success.
As long as the test commands do not return success, the loop will run
the conditional commands. After the conditional commands have been
run, the loop goes 'back to the start' and evaluates the test commands
again.
In general I would recommend using while loops rather than until
loops. While loops are going to be more familiar to readers as they
exist in many programming languages - until loops are a little more
rare. And you can easily turn any until loop into a while loop by
simply inverting the test commands you are running.
until <test-commands>
do
<conditional-command 1>
<conditional-command 2>
<conditional-command n>
done
# until loop# Create an empty random number string - we're going to build it up in the loop.random_number=""# Keep on looping until the random number is at least 15 characters long.until["${#random_number}" -ge 15 ]dorandom_number+=$RANDOMdoneecho"Random Number: ${random_number}"# while looprandom_number=""while["${#random_number}" -lt 15 ]dorandom_number+=$RANDOMdoneecho"Random Number: ${random_number}"
echo"For each folder, choose y/n to show contents, or c to cancel."for file in ~/*
do# If the file is not a directory, or it cannot be searched, skip it.if![ -d "$file"] || ![ -x "$file"]; then continue; fi# Ask the user if they want to see the contents.read -p "Show: $file? [y/n/c]: " choice
# If the user chose 'c' for cancel, break.if["$choice" = "c"]; then break; fi# If the user choice 'y' to show contents, list them.if["$choice" = "y"]; then ls "$file"; fidone
# Set some variables.title="My Cool Script"version="1.2"succeeded=0
# Create a function that writes a message and changes a variable.title(){# Note that we can read variables...title_message="${title} - version ${version}"echo"${title_message}"# ...and set them as well.succeeded=1
}# Show the value of 'succeeded' before and after the function call.echo"Succeeded: ${succeeded}"
title
echo"Succeeded: ${succeeded}"echo"Title Message: ${title_message}"
If you come from a programming background you might find it odd that
you can create a variable in a function and use it outside of the
function. This is a feature known as dynamic scoping. Many common
programming languages like Python, JavaScript, C, Java and others use
an alternative mechanism called lexical scoping.
Lexical scoping is a feature that ensures that you can only use a
variable from within the 'scope' that it is defined. This can reduce
errors - because it means that if you define a variable in a function
you don't accidentally 'overwrite' the value of another variable that
is used elsewhere.
You can use the local keyword to define a variable that is only
available in the 'local' scope, i.e. the function that it is defined
in. This allows you to use lexical scoping and can reduce the risk of
errors.
run_loop(){localcount=0
for i in{1..10}; do# Update our counter.count=$((count + 1))doneecho"Count is: ${count}"}
In general, you should use 'local' variables inside functions. This
can help to avoid problems where calling a function can have an
unintended side effects:
# 比较用 local 和不用的区别# Set a count variable somewhere in our script...count=3
# Call our 'run_loop' function.
run_loop
# Write out the value of 'count'.echo"The 'count' variable is: ${count}"
sum(){localvalue1=$1localvalue2=$2localresult=$((value1 + value2))echo"The sum of ${value1} and ${value2} is ${result}"}# Create a function that calculates the sum of two numbers.sum(){echo"The sum of $1 and $2 is $(($1 + $2))"}# usage# sum 3 6# sum 10 33
Variable
Description
$0
path that called the script (使用 curl cht.sh/'bash parameter $0' 查阅用法)
$1
The first parameter
$2
The second parameter
${11}
The 11th parameter - if the parameter is more than one digit you must surround it with braces
$#
The number of parameters
$@
The full set of parameters as an array
$*
The full set of parameters as a string separated by the first value in the $IFS variable
${@:start:count}
A subset of 'count' parameters starting at parameter number 'start'
# Show the top 'n' values of a set.show_top(){# Grab the number of values to show, then shift.localn=$1shift# Get the set of values to show. Notice that we start in position 1 now.localvalues=${@:1:n}echo"Top ${n} values: ${values}"}
is_even(){localnumber=$1# A number is even if when we divide it by 2 there is no remainder.# Set 'result' to 1 if the parameter is even and 0 otherwise.if[ $((number % 2)) -eq 0 ]; thenresult=1
elseresult=0
fi}
$ number=33
$ is_even $number
$ echo "Result is: $result"
Result is: 0
In general, this method of returning values from a function should be
avoided. It overwrites the value of a global variable and that can be
confusing for the operator.
A more common way to return a value from a function is to write its
result to stdout
输出到 stdout
lowercase(){localparams="$@"# Translate all uppercase characters to lowercase characters.echo"$params" | tr '[:upper:]''[:lower:]'}
If you have a programming background it might seem very strange that
you write results in a function by writing to stdout. Remember - the
shell is a text based interface to the computer system. The majority
of commands that we have seen so far that provide output write their
output to the screen. This is what ls does, what find does, what cat
does and so on. When we echo a result from a function, we are really
just following the Unix standard of writing the results of a program
to the screen.
Remember - shell functions are designed to behave in a similar way to
shell commands. They write their output to stdout.
Although it might feel a bit clunky, writing the results of a command
to stdout is a tried and tested method of returning results.
但是,如果脚本中有很多次输出,最终的结果可能不是我们期待的。
command_exists(){if type"$1"; thenecho"1"elseecho"0"fi}# result=$(command_exists "touch")# echo "Result is: ${result}"
The return (return from shell function) command causes a function to
exit with a given status code.
This is something that often causes confusion in shell scripts. The
reason is that in most programming languages, you would use a 'return'
statement to return the result of a function. But in the shell, when
we return, we set the status code of the function.
What is a status code? When a command runs, we expect it to return a
status code of 'zero' to indicate success. Any non-zero status code is
used to specify an error code.
Remember - only use the 'return' command to set a status code. Many
shells will only allow values from 0-255 to be set, and most users
will expect that a command should return zero for success and that any
non-zero value is an error code. If you need to provide output for a
command that is not just a status code, you should write it to stdout
or if you must, set the value of a global variable.
When you run a shell script, if a command in the script fails, the
script will continue to run. Like many other points in this chapter
this might seem unintuitive if you come from a programming background,
but this makes sense in the shell - if the shell was to terminate
whenever a command fails it would be very difficult to use
interactively.
In general in your shell scripts if a command fails you probably want
the entire script to stop executing. Otherwise you can get this
cascading effect as commands continue to return even after there was a
failure, which can lead to all sorts of unexpected behaviour.
如果先创建一个文件, 在执行脚本:
touch "/tmp/$(date +"%Y-%m-%d")"
#!/usr/bin/env sh# Get today's date in the format YYYY-MM-DD.today=$(date +"%Y-%m-%d")# Create the path to today's temp folder and then make sure the folder exists.temp_path="/tmp/${today}"
mkdir -p "${temp_path}"# Now that we've created the folder, make a symlink to it in our homedir.
ln -sf "${temp_path}""${HOME}/today"# Write out the path we created.echo"${temp_path}"
You can use the set (set option) command to set the trace option (set -x). This
option is incredibly useful for debugging shell scripts. When the
trace option is set, the shell will write out each statement before it
is evaluated.
# today.sh - creates a 'today' symlink in the home directory folder to a fresh# temporary folder each day.# Enable tracing in the script.set -x
# Get today's date in the format YYYY-MM-DD.today=$(date +"%Y-%m-%d")# Create the path to today's temp folder and then make sure the folder exists.temp_path="/tmp/${today}"
mkdir -p "${temp_path}"# Now that we've created the folder, make a symlink to it in our homedir.
ln -sf "${temp_path}""${HOME}/today"# Disable tracing now that we are done with the work.set +x
# Write out the path we created.echo"${temp_path}"
Each command that the shell executes is written to stdout before it is
executed. The parameters are expanded, which can make it far easier to
see what is going on and troubleshoot issues.
The + symbol is written at the start of each trace line, so that you
can differentiate it from normal output that you write in your
script1. The final line of output in the example above does not have
a + in front of it - because it is actual output from an echo command,
rather than a trace line.
The number of + symbols indicates the 'level of indirection'
set -x
echo"Name of home folder is $(basename $(echo ~) )"
推荐的设置:
# Fail on errors in commands or in pipelines.set -e
set -o pipefail
# Uncomment the below if you want to enable tracing to debug the script.# set -x
You can use the trap (trap signals and events) command to specify a
set of commands to run when the shell receives signals, or at certain
points such as when the script exits or a function returns.
# Create a temporary folder for the effective shell download.source="https://effective-shell.com/downloads/effective-shell-samples.tar.gz"tmp_dir=$(mktemp -d 2>/dev/null || mktemp -d -t 'effective-shell')tmp_tar="${tmp_dir}/effective-shell.tar.gz"# Define a cleanup function that we will call when the script exits or if# it is aborted.cleanup(){if[ -e "${tmp_tar}"]; then rm "$tmp_tar}"; fiif[ -d "${tmp_dir}"]; then rm -rf "${tmp_dir}"; fi}# Cleanup on interrupt or terminate signals and on exit.trap"cleanup" INT TERM EXIT
# Download the samples.
curl --fail --compressed -q -s "${source}" -o "${tmp_tar}"# Extract the samples.
tar -xzf "${tmp_tar}" -C "${tmp_dir}"
The select compound command prints a menu and allows the user to make
a selection. It is not part of the Posix standard, but is available in
Bash and most Bash-like shells.
select fruit in Apple Banana Cherry Durian
doecho"You chose: $fruit"echo"This is item number: $REPLY"done
You will often see a nice little trick that allows you to change the
current directory for a specific command, without affecting the
current directory for the shell.
The brackets around the statements mean that these commands are run in
a sub-shell. Because they run in a sub-shell, they change the
directory in the sub-shell only, not the current shell. This means we
don't need to change back to the previous directory after the commands
have completed.
(mkdir -p ~/new-project; cd ~/new-project; touch README.md)