Cheatsheet on bash scripting
1 Bash shell basics for file zintscript.sh
1.1 #!/bin/bash
This is always the first line of the script
1.2 Execute it using bash:
bash zintscript.sh
1.3 Execute it independently using chmod
Make the script executable with
chmod 740 zintscript.sh ./zintscript.sh
1.4 One command per line, flush left
touch mynewfile.org chmod 740 mynewfile.org
1.5 Splitting one long command over multiple lines
You can escape the newline character, with a \
which will
let you continue a long command on the next line.
awk '/^\#\+DATE/{print; \ !/^\#\+DATE/{print} \ END {print "this is the end"}' junk.org
1.6 Variables $x
# Create a variable: mynewvar='gold' # Read a variable if $mynewvar = 'gold' : # notice the colon : NETIP=172.17.17. LOCALIP="$(command)" LOCALIP="$(IP ADDR | egrep '\''([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})'\'')"
1.7 Looping with "for"
This loop converts some markdown
(*.md) files to org-mode
files using pandoc.
I have not actually tried pandoc, but it looks promising.
for f in 'ls *.md'; do pandoc -f markdown -t org -o ${f}.org ${f}; done
1.8 Giving bash scripts options
The standard format is command --options -a arguments
from the command line.
From opensource.com article on this:
The strategy for parsing options in Bash is to cycle through all arguments passed to your shell script, determine whether they are an option or not, and then shift to the next argument. Repeat this process until no options remain.
#!/bin/bash while [ True ]; do if [ "$1" = "--euler" -o "$1" = "-e" ]; then EULER=2718281828459 shift 1 else break fi done echo $EULER
In this code, I create a while loop
which serves as an infinite loop until
there are no further arguments to process. An if statement attempts to match
whatever argument is found in the first position ($1)
to either –euler or
-e. The Euler number here is just used as an example.
The shift
keyword causes all arguments to shift by 1
, such that an argument
in position 2 ($2) is moved into position 1 ($1). The else
statement is
triggered when there are no further arguments to process
, which breaks the
while loop.
At the end of the script, the value of $EULER is printed to the terminal.
1.9 Giving bash scripts more options
From the above section, we would only catch the single –alpha or -a option.
To get the rest, we can store the remaining options in an array.
#!/bin/bash while [ True ]; do if [ "$1" = "--euler" -o "$1" = "-e" ]; then EULER=2718281828459 shift 1 else break fi done echo $EULER ARG=( "${@}" ) for i in ${ARG[@]}; do echo $i done
Test
1.10 From github:
#!/bin/bash # viewing environment variables echo "The value of the home variable is: " echo $HOME # issue a command echo "The output of the pwd command is: " pwd # grab output and make it readable using $(command) echo "The value of the pwd command is $(pwd)" # assign command output to a variable ${variable} output=$(pwd) echo "The value of the output variable is ${output}" # ${} reminds you that inside the curlies is a variable # view data on the command line $@ echo "I saw $@ on the command line" # read data from the user echo "Enter a value: " read userInput echo "You just entered $userInput" # concatenate userinput with command output echo "Enter a file extension: " read ext touch "yourfile.${ext}" # check to see if a file exists if [ -d /etc/sysconfig ]; then echo "That file is there and a directory" else echo "Not there or not a directory" fi for i in {10..30} ; do echo $i ; done | xargs -I_ touch deletme-file-_.txt for f in ./deleteme*.*; do cat add-a-line >> $f; done
1.11 Bash shell expansion {..}
Related to the the above example, is the bash shell expansion of ranges of characters using the {..} syntax.
touch files-{A..G}.data
will result in these files
files-A files-B files-C files-D files-E files-F files-G
2 Bash script built-in and environement variables
To read more about these built-in variables, see man bash
as always.
2.1 $1
$1
is the first argument
included when running this script. Likewise $2
and $3
etc are the second and third arguments.
Which shows you that $*
expands ALL space delimited arguments as ONE argument
while $@
expands ALL space delimited arguments each as their own SEPARATE
arguments.
2.2 $@ (all args)
bash will expand $@ to all the arguments
attached to the calling shell command,
with each space delimited arguement as its own SEPARATE argument. For example:
for FILE in "$@"; do mv "$FILE" ~/Trash/; done
IF you run this with an argument *.org
then each file ending in .org
will be
passed to the for
loop ONE AT A TIME
Really the same as expanding to $1 $2 $3 ... $n
2.3 $* is very similar to $@ (all args as ONE string)
But they are not interchangeable, because $*
takes ALL the arguments as one long
string arguement.
2.4 bash script to display $@ and $*
This website, the geek stuff, had this sample script:
#!/bin/bash # show me how bash special args expand out. Script is called expan.sh export IFS='-' cnt=1 # Printing the data available in $* echo "Values of \"\$*\":" for arg in "$*" do echo "Arg #$cnt= $arg" let "cnt+=1" done cnt=1 # Printing the data available in $@ echo "Values of \"\$@\":" for arg in "$@" do echo "Arg #$cnt= $arg" let "cnt+=1" done
When executed with you get
$ ./expan.sh "This is" 2 3 Values of "$*": Arg #1= This is-2-3 Values of "$@": Arg #1= This is Arg #2= 2 Arg #3= 3
Which shows you that $*
expands ALL space delimited arguments as ONE argument
while $@
expands ALL space delimited arguments each as their own SEPARATE
arguments.
2.5 $# (count)
Returns a count
of how many positional arguments
exist. If you run a script
like this: myscript.sh 55 two three
then $# will return the value 3
because
there were three positional arguments to the bash script.
2.6 $$ and $! (process id)
Whenever bash runs a script, it assigns a process id
, which can be recalled with
$$
At the same time, $!
will return the process id
of the last background
process that was executed.
2.7 $? (exit status)
Returns the exit status
of the most recently executed script, or command.
2.8 $- (options)
Returns the options
set using the built-in set
command.
2.9 $_ (last arg)
Returns the last argument
to the previous command. At the shell startup, it
gives the absolute filename
of the shell script being executed.
2.10 Environment variables
You can check what variables you have with the command printenv
Or you can expand that and check ALL system environment variables with
the command set
Then use the in scripts for instance:
3 Wrapper script for running pyenv python in crontab
Let's say you want to run covid.py every morning, but it needs python 3.7.5 which is set up in your pyenv setting.
In your crontab run 59 7 * * * /Users/zintis/wrapper.sh
And wrapper.sh looks like:
#!/bin/bash -l // this picks up your ~/.bash_profile environment variables cd /Users/zintis/bin/python/bin // needed only if this dir is not in your python path source /Users/zintis/.virtualenv/dslab/bin/activate // activate your pyenv python /Users/home/covid.py // actually run your script.
3.1 alternatively,
which python
> Users/zintis.pyenv/shims/python
then but that as the shebang in your covid program.
i.e.
#!/Users/zintis/.pyenv/shims/python
3.2 alternatively, set env right in crontab
SHELL=/usr/local/bin PATH=/usr/local/sbin:/usr/local/bin/ <continues your PATH> 30 12,15,18 * * 1,2,3,4,5 root export VAR1=value1 && export VAR2=value2 && export VAR3=value3 && export VAR4=value4 /usr/bin/python3 /path_to_script/Kwanty_bez_eng.py
4 Standard File Descriptors
There are two standard output file descriptors
- 1 is
stdout
- 2 is
stderr
By default both are the terminal
where the command was entered.
When running a script from a cron job, i.e NOT from the standard terminal
you can redirect &1
and &2
to some other location, typically a file.
batchjob.py > ~/batchoutput 2>&1
This means the standard output, stdout
, is the file ~/batchjoboutput
and to
also redirect stderr
to the same place. i.e. redirect 2 to what &1 is.
4.1 Common errors
2>1
it would simply rediredt standard errors to a file called '1'
job &2>&1
it would run "job
" in the background, &
There does not have to be
a space between the first &
and the 2
, so that simply takes "2"
which is
stderr
and redirects it to, in this case &1
, which is stdout.
5 Logical Operators, &&, ||
x && y
==> =y only if x true
i.e x had to complete successfully before you do yx || y
==> y only if x false
i.e only do y if x failed
These can often let you avoid using cubmersome if statements in your bash scripts.
Below are details for each type. Remember that $? is a shell variable that holds the error condition from the last executed command. 0 if it worked, 1 if it failed.
5.1 and operator && only execute the 2nd part if the first PASSED
ansible@c8host ~[1174] $ true && echo "ok it worked" ok it worked ansible@c8host ~[1175] $ false && echo "ok it worked" <nothing was echoed here, but that is hard to show, hence this explanation> ansible@c8host ~[1176] $
That was taken from the MIT missing lecture series on youtube
This is the AND
operator (&&
):
The second command will ONLY run if the first command succeeded. So, here is a better example, good for handling ugly errors:
cat ".bashrc" ; echo "this is my profile" cat ".bashrc" && echo "this is my profile"
- The
first
choice above will fail with an ugly error if there is no file called.bashrc.
- The
second
choice will print "this is my profile" only if there really was a profile. Much better.
5.1.1 Summarizing x && y ==> y only if x true
5.2 or operator || only execute the 2nd part if the first FAILED
The second choice will print
the message ONLY if the first command failed
.
If the first command succeeded, the second command will NOT run.
The above still might have an ugly error, which you can supress by redirecting
to null, stderr
is &2
, i.e. &2 > /dev/null
ansible@c8host ~[1172] $ false || echo "logical OR says false was false" logical OR says false was false ansible@c8host ~[1173] $ true || echo "logical OR says false was false" ansible@c8host ~[1174] $
In bash shell scripts you can do 'cd .. || exit'
as a safe way to run
a command and if that fails, simply exit the script altogether.
5.2.1 Summarizing x || y ==> y only if x false
5.3 always do the second no matter what, … just concatentate two commands
echo "first" ;
echo "second will always print too"
The first command completes, before the second command runs.
5.3.1 Summarizing x ; y ==> do x, do y
5.4 &
This is the same as the shell &,
i.e. "put this in the background
"
sleep 5 ; echo "all done." sleep 5 & echo "all done."
The first
command will print "all done" only after waiting 5 seconds.
the second
commnd will print "all done" right away, but the script will
still take 5 more seconds to finish (less the time it took to echo all done)
5.5 Example:
5.6 Good code fragment example
add) asktype && askinfo && tryconnect && finalize || delete ;;
This will run each of these commands in order, and continue as long as each of them runs successfully. As soon as one fails, all the others will fail, until you get to the || which will run if any of the previous failed. If they all succeeded, then the delete will NOT run.
Slick and effective.
6 if elif else fi
In bash scripting the conditional statement syntax is:
if <condition>; then<commands>fi
if [conditional]; then statements elif statements else statements fi
The conditionals in the square brackets can be:
! EXPRESSION The EXPRESSION is false. -n STRING The length of STRING is greater than zero. -z STRING The lengh of STRING is zero (ie it is empty). STRING1 = STRING2 STRING1 is equal to STRING2 STRING1 != STRING2 STRING1 is not equal to STRING2 INTEGER1 -eq INTEGER2 INTEGER1 is numerically equal to INTEGER2 INTEGER1 -gt INTEGER2 INTEGER1 is numerically greater than INTEGER2 INTEGER1 -lt INTEGER2 INTEGER1 is numerically less than INTEGER2 -d FILE FILE exists and is a directory. -e FILE FILE exists. -r FILE FILE exists and the read permission is granted. -s FILE FILE exists and it's size is greater than zero (ie. it is not empty). -w FILE FILE exists and the write permission is granted. -x FILE FILE exists and the execute permission is granted.
6.1 conditionals
If they are in [ ]
then the statement in the [ ]
is evaluated
, and if
true
, the if statement can proceed. This [ ]
evaluation is actually a
shortcut to the test statement
, so the following behave the same:
if [ $foo -ge 3 ]; then if test $foo -ge 3; then
'=
' does not equal 'eq
'
=
does a string comparison
, while eq
is a numeric comparison
.
7 special characters
These will obviously impact your comparisons greatly. You must escape them
if the actual character is needed, using a \
Common special characters used in Bash
” ”
or‘ ‘
Denotes whitespace. Single quotes preserve literal meaning; double quotes allow substitutions.$
Denotes an expansion (for use with variables, command substitution, arithmetic substitution, etc.)\
Escape character. Used to remove “specialness” from a special character.#
Comments. Anything after this character isn’t interpreted.=
Assignment[ ]
or <double square brackets> Test; evaluates for either true or false!
Negation- >>, >, < Input/output redirection
|
Sends the output of one command to the input of another.*
or?
Globs (aka wildcards).?
is a wildcard for a single character.
8 case statements
Case statements take the form:
case <some expression> in pattern1) echo "since pattern1 is met do these statements" ;; pattern2) echo "since pattern2 is met do these statements" ;; pattern3) echo "since pattern3 is met do these statements" ;; pattern4) echo "since pattern4 is met do these statements" ;; *) echo "all other patterns will match so this always gets done" ;; esac
From link
Each case statement starts with the case keyword followed by the case expression and the in keyword. The statement ends with the esac keyword.
- You can use multiple patterns separated by the | operator.
- The
)
operator terminates a pattern list. - A pattern can have special characters.
- A pattern and its associated commands are known as a clause.
- Each clause must be terminated with
;;
. - The commands corresponding to the first pattern that matches the expression are executed.
- It is a common practice to use the wildcard asterisk symbol (*) as a final pattern to define the default case. This pattern will always match.
- If no pattern is matched the return status is zero. Otherwise, the return status is the exit status of the executed commands
9 Some misc tools to show current state
show memory used vs total
free
free -h
combining with awk to show 3rd field followed by 2nd field when line starts with "Mem"
free -h | awk '/^Mem:/ {print $3 "/" $2}'
breaking down the awk:
- search for lines starting with Mem:
- then print the 3rd field,
- followed by a slash
- followed by the 2nd field.
show CPU temperature
lm_sensors
sensors sensors | awk '/^temp1/ {print $2}'
- search for lines beginning with temp1
- print the 2nd field on that line.
For mac it is "powermetrics'
show cpu processes ps ps axc (no minus) ps axch -o cmd:15,%mem --sort=%mem | head ps axch -o cmd:15,%cpu --sort=%cpu | head
10 Functions in bash scripts
Bash scripts have several different syntaxes for writing functions, shown here
10.1 functionname() { …. }
For example, putting the following into a file called zintfunc, allows you to
source zintfunc
which will add functions to your session called, ipaddr, update,
clean, helpme, and leave.
# Functions ipaddr() { ip addr | egrep '([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})' | grep -v 127.0.0.1 | awk '{printf "%-18s%10s\n", $2, $NF}' } update() { echo "Starting update" sudo dnf update -y sudo dnf upgrade -y } clean() { echo "Cleaning up" sudo dnf clean all echo "This is what you've got installed" sudo dnf list installed } helpme() { cat << _MYEOF_ So, you need help. Ok, very simply, this program shows you how to use functions. shown above is the who to write the functions, then to call, i.e. use, the functions simply use the fucntion name in your script. update clean helpme _MYEOF_ } leave() { echo "==================" echo "Ok, see you around" echo "Ok, see you around" echo "==================" exit } # Main program echo "Zintis maintenance script, - Oct 2020 \n" if [ "$1" == "--clean"]; then update clean leave fi if [ "$1" == "--help"]; then helpme leave fi if [ -n "$1" ]; then echo " Error: needs an argument. Please run this script with one of:" echo " --help, --clean, --update" fi update leave
11 from Shell Tools and Scripting lecture 2020
Works:
foo=bar
echo $foo
Doesn't work:
foo = bar
because is bash looking for the command 'foo' with two args the first being an '=' the second being 'bar' i.e. don't put the space around the equals sign.
11.1 single vs double quotes:
echo "Value is $foo" Value is bar
echo 'Value is $foo' Value is $foo
11.2 functions, and $1 is the first arg in bash, $2 is 2nd etc…
Create a file called mcd.sh, and save it with the following:
mcd () { mkdir -p "$1" cd "$1" }
running this with source mcd.sh
seems to do nothing, but afterwards you can
type mcd test
and it will create the directory test, as well as cd into it.
11.3 variable names bash assigns to command line arguments
Bash takes command line arguments and assigns them to special variables as follows:
$0
is thename
of the script$1
is thefirst arg
in bash$2
through to$9
are thesecond
throughninth
argument$?
is theerror
from the command$_
is thelast
arguments from the command
11.4 standard i/o
<
to read stdin from a file>
to write stdout to a file- stderr
- error code
echo "Hello" Hello echo $? 0
$?
is0
when things run well,$?
is1
when things fail
grep zintis /etc/fstab
- if zintis is not found, then nothing will appear at the command prompt,
- but
echo $?
will be1
- thus indicating that
grep zintis /etc/fstab
failed to find. - true will always set
$?
to0
- false will always set
$?
to1
11.5 Save output of command into a variable
foo=$(pwd) echo "We are in $(pwd)"
11.6 process substitution: <(some-unix-command)
cat <(ls) <(ls ..)
diff <(ls foo) <(ls bar)
will show which files are in one and not the other directory.
This <(somecommand)
construct will execute the command and redirect it into stdin
So the cat command above will show a listing of the current directory AND the parent directory.
- diff -u file1 file2
While we are on the topic of diff
diff can be run in context mode,
-c
or in unified mode,-u
Here is a sample of
context mode
:diff -c file1.txt file2.txt *** file1.txt Thu Jan 11 08:52:37 2018 --- file2.txt Thu Jan 11 08:53:01 2018 *************** *** 1,4 **** cat - mv - comm cp --- 1,4 ---- cat cp + diff + comm
+
indicates a line in the second file that needs to be added to the first file to make them identical-
indicates a line in the first file that needs to be deleted to make them identicalUnified mode does NOT display any redundant information. i.e. it is more concise. And here is a sample of unified mode:
$ cat file1 one two three four five $ cat file2 one two two-1 two-2 two-3 three Four five $ diff -u file1 file2 --- file1 2021-06-19 13:26:26.000000000 -0400 +++ file2 2021-06-19 13:27:06.000000000 -0400 @@ -1,5 +1,8 @@ one two +two-1 +two-2 +two-3 three -four +Four five
The first file is indicated by a
-
and the 2nd by a+
@@ - is the line range from the first file, and @@ + is the line range from the 2nd file.
- unified diff mode
When run in
unified diff mode
with the-u
option you get:First two lines are
--- from-file from-file-modification-time +++++ to-file to-file-modification-time
--- from-file from-file-modification-time +++ to-file to-file-modification-time
Next come one or more hunks of differences with this syntax:
@@ from-file-line-numbers to-file-line-numbers @@ line-from-either-file line-from-either-file…
If a hunk and its context contain two or more lines, its line numbers look like ‘start,count’. Otherwise only its end line number appears.
The lines common to both files begin with a space character. The lines that actually differ between the two files have one of the following indicator characters in the left print column:
‘+’ A line was added here to the first file. ‘-’ A line was removed here from the first file.
Here is a larger example:
$ diff -u 1stfile 2ndfile --- 1stfile 2020-05-22 11:32:48.000000000 -0400 +++ 2ndfile 2021-02-02 16:20:15.000000000 -0500 @@ -1,12 +1,10 @@ # -# macOS Notice +# macOS Notice This line was changed on the 2nd file. # # This file is not consulted for DNS hostname resolution, address # resolution, or the DNS query routing mechanism used by most # processes on this system. # -# This line is in the first file, but will be removed in 2nd file -# This line also will be removed in 2nd file. # # SEE ALSO # dns-sd(1), scutil(8) @@ -17,3 +15,5 @@ nameserver 208.67.220.220 You can get so confused that you'll start in to race down long wiggled roads at a break-necking pace +And # new line added +On and on. # second new line added, hence now we are looking at a block of 5
11.7 globbing *.jpg
ls *.jpg is obvious
but convert image.{png,jpg}
is very useful too as it expands out to:
covert image.png impage.jpg
mkdir foo bar touch {foo,bar}/{a..d}
will expand to: foo/a foo/b foo/c foo/d bar/a bar/b bar/c bar/d
touch bar{,1,2,a,b}
expands to: =bar, bar1, bar2, bara, bar
touch home{1,2}/bin/python/test{1,2,3}.py
expands to:
home1/bin/python/test1.py home1/bin/python/test2.py home1/bin/python/test3.py home2/bin/python/test1.py home2/bin/python/test3.py home2/bin/python/test4.py
12 man test
gives you a full listing of all the bash scripting tests available to you. cool cool
13 tldr <command>
will give you a quick example of how to use command
14 Sample bash script
#!/bin/bash # a useless program in anything other than showing shell variables echo "Starting program on $(date)" echo "Running $0 script with $# arguments with pid $$" for file in "$@"; do grep senecacollege "$file" . /dev/null 2> /dev/null # when pattern is NOT found, grep has an exit sttus 1 # we redirect stdout and stderr to a null retster since we don't care to see if [ [ "$?" eq 0 ] ]; then echo "senecacollege found in $file" fi done
15 echo vs printf
Taken from stackexchange.com running:
echo "blah" >/dev/udp/localhost/8125
will not send the "blah" command to the server listening on 8125, whereas
printf "blah" >/dev/udp/localhost/8125
will.
echo "blah\n" >/dev/udp/localhost/8125
will as well. (need to confirm)
The difference is that echo sends a newline at the end of its output. There is no way to "send" an EOF.
Both echo
and printf
are built-in commands (printf is Bash built-in since
v2.0.2, 1998). echo always exits with a 0 status, and simply prints arguments
followed by an end of line character on the standard output, while printf
allows for definition of a formatting string and gives a non-zero exit status
code upon failure.
printf has more control over the output format. It can be used to specify the field width to use for item, as well as various formatting choices for numbers (such as what output base to use, whether to print an exponent, whether to print a sign, and how many digits to print after the decimal point). This is done by supplying the format string, that controls how and where to print the other arguments and has the same syntax as C language (%03d, %e, %+d,…). This means that you must escape the percent symbol when not talking about format (%%).
Probably, if you add the newline character that echo uses by default (except when using -n option) you'll get the same effect. so
16 bash debugging
try shellcheck myscript.sh
on a bash script for some tips and syntax checking.
my CentOS8 Linux system did not have shellcheck, nor did dnf install find it.
(not found on my CentOS system, nor in dnf repos that I have)
16.1 Trick to echo lines and line numbers as script is running
A neat little trick while debugging:
export PS4="\$LINENO: " set -xv
The set -xv
will print out each line before it is executed, and then the line
once the shell interpolates variables, etc. The $PS4
is the prompt used by
set -xv
. This will print the line number of the shell script as it
executes. You'll be able to follow what is going on and where you may have
problems.
17 sampling profilers and tracing profilers
sampling profilers take a bigger hit in performanf
every 10ms the program will halt, and look at the stack trace the continue. After enough time, the profiler will give you some statistics to see where most of the time was spent. i.e. which function or method, or line of code.
17.1 tracing profiler
18 tac is a reverse cat
18.1 lsof
list of open files.
19 benchmarking
20 Bash script here documents and here strings
A block of code, that is a form of i/o redirect.
It could be part of a shell script, and it feeds a command list to an interactive program or command line. The here document can be treated as a separate file, or also as multiple line input redirected to a shell script
20.1 here syntax
command << HERESTRING text1 text2 ... textn HERESTRING
The HERESTRING is often EOF or >>EOF or just EOF
The command can be any bash command, for example wc -l
which would just
show you who many lines are in the HEREDOCUMENT.
For a more useful example, you could cat a here document that lists the arguments of the calling command:
#!/usr/bin/env bash cat << EOF 0th argument is: $0 1st argument is: $1 2nd argument is: $2 EOF
The run that script with Apples oranges peaches you should get
0th argument is: Apples 1st argument is: oranges 2nd argument is: peaches
An even better example for ftp:
ftp -n << MYFTP 2> /dev/null open ftp.acme.com user anonymous zintis@cisco.com ascii prompt cd folderofgoodstuff mget file1 file2 file3 bye MYFTP
20.2 here strings
used for input redirection from text or a variable. The input is included in the same line with single quoatation marks.
wc -w <<< 'Hello World!'
I have also seen here docs starting with the HERESTRING in single quoatation marks, such as
#!/usr/bin/env bash cat << 'EOF' 0th argument is: $0 1st argument is: $1 2nd argument is: $2 EOF
20.3 grep string -A3
This will show lines that contain 'string' as well as 3 lines after that line.
Try this: grep "if " -A3 ~/bin/python/bin/*.py
Kind of useful, I think.
20.4 grep string -B1
Similarily this shows the line and 1 line before that line as well.
grep "elif" -B2 ~/bin/python/bin/*.py
20.5 Input File Seperator, $IFS
By default the $IFS is set to a <space>, i.e. ' ' You can change that in a script, but good idea is to change it back to what it was when you started. We need to save that as $OLDIFS. Like this example, that has input that are comma separated i.e. csv
#!/bin/env bash OLDIFS=$IFS IFS="," ... do you thing with inputs that are separated with single tics IFS=$OLDIFS
#!/bin/env bash OLDIFS=$IFS IFS="," while read user job uid location # first four fields in a csv file do echo -e "\e[1;33m$user \ ==================\e[0m\n\ Role : \t $job\n\ ID : \t $uid\n\ Site : \t $location\n" done < $1 IFS=$OLDIFS
note that the echo command above could all have been written on one line but the "\" character simply spans the echo statement across to the next line in the code. It has nothting to do with the newline character, which is \n apart from often coming right after it. You could have written
do echo -e "\e[1;33m$user ==================\e[0m\n Role : \t $job\n ID : \t $uid\n Site : \t $location\n" done
And gotten the exact same output from the script.
21 bash script to move files to .Trash
This from a youtube video from Joe Collins :
#/bin/env bash if [ ! -d "$HOME/.Trash" ] ; then mkdir "$HOME/.Trash" mkdir "$HOME/.Trash" || exit 1 # a bit more robust fi # or even better, replace the above if block with: [ -d $HOME/.Trash ] || mkdir ~/.Trash || exit 1 # that might not be "better" as it is more obtuse. i.e. strive for readability over efficiency for FILE in "$@" do mv "$FILE"" ~/.Trash/ done
22 Seneca checking scripts
These are good examples of what you can do with a bash script, even on remote servers.
The following is actually a script that runs in three parts.
- the main part of the script
- a separate script designated with delimiters that runs on remote host 1
- a second separate script that runs on remote host 2
#!/bin/bash # check-assn2.bash # Author: Murray Saul # Created: October 7, 2017 Updated: October 20, 2018 # Edited by: Peter Callaghan November 10, 2019 # Purpose: To generate data to be mailed to OPS335 instructor in order # to mark OPS335 assignment #2 # Error-checking prior to running shell script # Check that user is logged on as root if [ `id -u` -ne 0 ] then echo "You must run this shell script as root" >&2 exit 1 fi # Check that mailx application has been installed if ! which mailx > /dev/null 2> /dev/null then echo "You need to run \"yum install mailx\" prior to running this shell script" >&2 exit 1 fi nameserver=australinea nameserveraddress=172.28.105.2 mailtransferagent=asia mailtransferaddress=172.28.105.5 mailsubmissionagent=europe mailsubmissionaddress=172.28.105.6 sambaserver=southamerica sambaaddress=172.28.105.8 tld=ops sld=earth bld=continents if ! virsh list | egrep -iqs "($nameserver|$mailtransferagent|$mailsubmissionagent|$sambaserver)" then echo "You need to run your \"$nameserver\", \"$mailtransferagent\", \"$mailsubmissionagent\", and \"$sambaserver\" VMs" >&2 exit 2 fi # Prompt for username and Full name read -p "Please enter YOUR FULL NAME: " fullName read -p "Please enter YOUR SENECA LOGIN USERNAME: " userID # Generate Evaluation Report clear echo "Your Assignment #2 is being evaluated..." echo "This make take a few minutes to complete..." cat <<'PPC' > /tmp/check$mailtransferagent.bash #!/bin/bash #Ensure the host name has been set correctly echo "Hostname:"`hostname` echo echo "SELinux status:"`getenforce` echo echo "DOMAIN:"`grep -E "^[[:space:]]*DOMAIN=" /etc/sysconfig/network-scripts/ifcfg-*` echo "DOMAINNAME:"`grep -E "^^[[:space:]]*DOMAINNAME=" /etc/sysconfig/network` echo "SEARCH:"`grep -E "^^[[:space:]]*search" /etc/resolv.conf` echo echo "IP ADDRESS" ip addr show echo echo INTERFACES for interface in `ls /etc/sysconfig/network-scripts/ifcfg-*` do echo $interface cat $interface echo done echo SECAFRENTI echo "sshd:"`systemctl is-active sshd.service` echo "sshd:"`systemctl is-enabled sshd.service` echo echo "firewalld:"`systemctl is-active firewalld.service` echo "firewalld:"`systemctl is-enabled firewalld.service` echo echo "iptables:"`systemctl is-active iptables.service` echo "iptables:"`systemctl is-enabled iptables.service` echo echo "postfix:"`systemctl is-active postfix.service` echo "postfix:"`systemctl is-enabled postfix.service` echo echo "RELEASE:"`uname -r` echo echo "LAST CHANGE" last | sed -rne '/still running$/ d' -e '/^reboot[[:space:]]+system boot/ p' echo echo UUIDS for links in `ip link show | grep -E "([a-fA-F0-9]{2}:){5}([a-fA-F0-9]{2})" | sed -rne "s/^.*(([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}) brd.*/\1/" -e "/ff:ff:ff:ff:ff:ff/ d" -e "/00:00:00:00:00:00/ d" -e "p"`; do echo $links; done echo echo BLKID blkid | sed -r 's/^.*UUID=\"([a-zA-Z0-9\-]+)\".*$/\1/' echo echo IFCFG sed -n -r -e '/^#?UUID=/ p' -e '/^#?HWADDR=/ p' /etc/sysconfig/network-scripts/ifcfg-* | cut -d'=' -f2 echo echo IPTABLES iptables -L -v -n echo echo POSTFIX CONFIG sed -re '/^$/ d' -e '/^[[:space:]]*#/ d' /etc/postfix/main.cf echo GIFNOC XIFTSOP echo yum install -y bind-utils &> /dev/null echo MAILXCHANGE dig @172.28.105.2 MX continents.earth.ops. | grep -E "^[^;].*MX" echo EGNAHCXLIAM PPC cat <<'PPC' > /tmp/check$mailsubmissionagent.bash #!/bin/bash #Ensure the host name has been set correctly echo "Hostname:"`hostname` echo echo "SELinux status:"`getenforce` echo echo "DOMAIN:"`grep -E "^[[:space:]]*DOMAIN=" /etc/sysconfig/network-scripts/ifcfg-*` echo "DOMAINNAME:"`grep -E "^^[[:space:]]*DOMAINNAME=" /etc/sysconfig/network` echo "SEARCH:"`grep -E "^^[[:space:]]*search" /etc/resolv.conf` echo echo "IP ADDRESS" ip addr show echo echo INTERFACES for interface in `ls /etc/sysconfig/network-scripts/ifcfg-*` do echo $interface cat $interface echo done echo SECAFRENTI echo "sshd:"`systemctl is-active sshd.service` echo "sshd:"`systemctl is-enabled sshd.service` echo echo "firewalld:"`systemctl is-active firewalld.service` echo "firewalld:"`systemctl is-enabled firewalld.service` echo echo "iptables:"`systemctl is-active iptables.service` echo "iptables:"`systemctl is-enabled iptables.service` echo echo "postfix:"`systemctl is-active postfix.service` echo "postfix:"`systemctl is-enabled postfix.service` echo echo "dovecot:"`systemctl is-active dovecot.service 2> /dev/null` echo "dovecot:"`systemctl is-enabled dovecot.service 2> /dev/null` echo echo "RELEASE:"`uname -r` echo echo "LAST CHANGE" last | sed -rne '/still running$/ d' -e '/^reboot[[:space:]]+system boot/ p' echo echo UUIDS for links in `ip link show | grep -E "([a-fA-F0-9]{2}:){5}([a-fA-F0-9]{2})" | sed -rne "s/^.*(([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}) brd.*/\1/" -e "/ff:ff:ff:ff:ff:ff/ d" -e "/00:00:00:00:00:00/ d" -e "p"`; do echo $links; done echo echo BLKID blkid | sed -r 's/^.*UUID=\"([a-zA-Z0-9\-]+)\".*$/\1/' echo echo IFCFG sed -n -r -e '/^#?UUID=/ p' -e '/^#?HWADDR=/ p' /etc/sysconfig/network-scripts/ifcfg-* | cut -d'=' -f2 echo echo IPTABLES iptables -L -v -n echo echo POSTFIX CONFIG sed -re '/^$/ d' -e '/^[[:space:]]*#/ d' /etc/postfix/main.cf echo GIFNOC XIFTSOP echo echo DOVECOT CONFIG sed -re '/^$/ d' -e '/^[[:space:]]*#/ d' /etc/dovecot/dovecot.conf sed -re '/^$/ d' -e '/^[[:space:]]*#/ d' /etc/dovecot/conf.d/10-{auth,mail,ssl}.conf echo GIFNOC TOCEVOD echo echo ALIASES echo root:`postalias -q root hash:/etc/aliases` PPC cat <<'PPC' > /tmp/check$sambaserver.bash #!/bin/bash #Ensure the host name has been set correctly echo "Hostname:"`hostname` echo echo "SELinux status:"`getenforce` echo echo "DOMAIN:"`grep -E "^[[:space:]]*DOMAIN=" /etc/sysconfig/network-scripts/ifcfg-*` echo "DOMAINNAME:"`grep -E "^^[[:space:]]*DOMAINNAME=" /etc/sysconfig/network` echo "SEARCH:"`grep -E "^^[[:space:]]*search" /etc/resolv.conf` echo echo "IP ADDRESS" ip addr show echo echo INTERFACES for interface in `ls /etc/sysconfig/network-scripts/ifcfg-*` do echo $interface cat $interface echo done echo "sshd:"`systemctl is-active sshd.service` echo "sshd:"`systemctl is-enabled sshd.service` echo echo "smb:"`systemctl is-active smb.service` echo "smb:"`systemctl is-enabled smb.service` echo echo "firewalld:"`systemctl is-active firewalld.service` echo "firewalld:"`systemctl is-enabled firewalld.service` echo echo "iptables:"`systemctl is-active iptables.service` echo "iptables:"`systemctl is-enabled iptables.service` echo echo "RELEASE:"`uname -r` echo echo "LAST CHANGE" last | sed -rne '/still running$/ d' -e '/^reboot[[:space:]]+system boot/ p' echo echo UUIDS for links in `ip link show | grep -E "([a-fA-F0-9]{2}:){5}([a-fA-F0-9]{2})" | sed -rne "s/^.*(([a-fA-F0-9]{2}:){5}[a-fA-F0-9]{2}) brd.*/\1/" -e "/ff:ff:ff:ff:ff:ff/ d" -e "/00:00:00:00:00:00/ d" -e "p"`; do echo $links; done echo echo BLKID blkid | sed -r 's/^.*UUID=\"([a-zA-Z0-9\-]+)\".*$/\1/' echo echo IFCFG sed -n -r -e '/^#?UUID=/ p' -e '/^#?HWADDR=/ p' /etc/sysconfig/network-scripts/ifcfg-* | cut -d'=' -f2 echo echo IPTABLES iptables -L -v -n echo SELBALTI echo echo USERS pdbedit -L echo SRUSE echo echo SAMBA-CONFIG testparm -s 2>&1 echo GIFNOC-ABMAS echo echo SELinuxSEttings getsebool -a | grep samba echo sgnittESxuniLES echo echo SELinuxContexts ls -LZR /supercontinents echo stxetonCxuniLES PPC ssh $mailtransferaddress 'bash ' < /tmp/check$mailtransferagent.bash > /tmp/output-$mailtransferagent.txt 2>&1 ssh $mailsubmissionaddress 'bash ' < /tmp/check$mailsubmissionagent.bash > /tmp/output-$mailsubmissionagent.txt 2>&1 ssh $sambaaddress 'bash ' < /tmp/check$sambaserver.bash > /tmp/output-$sambaserver.txt 2>&1 tar -czf a2.$userID.tgz /tmp/output-$mailtransferagent.txt /tmp/output-$mailsubmissionagent.txt /tmp/output-$sambaserver.txt rm -f /tmp/check$mailtransferagent.bash /tmp/check$mailsubmissionagent.bash /tmp/check$sambaserver.bash /tmp/output-$mailtransferagent.txt /tmp/output-$mailsubmissionagent.txt /tmp/output-$sambaserver.txt 2> /dev/null echo "The script created a file called a2.$userID.tgz in the current directory. Upload that to blackboard for Assignment 2."
23 Paths and basenames in bash scripts
It is often best to work in full path names in scripts, so as to not rely on the current directory where you run your script (i.e. relative paths).
But then you would likely need to get just the filename of a file, without
the full path. Bash offers basename
and dirname
for just this purpose.
for orgfile in /Users/zintis/eg/**/*.org do base_name=$(basename ${orgfile}) path_name=${orgfile%/*} echo "${orgfile} is the full name" echo "${path_name} is just the path name" echo " ----> ${base_name} is the basename" done
24 loop through file contents in a while loop
24.1 method 1
#!/bin/bash # with input redirection < hello.txt file="/usr/home/user1/somegoodfile.txt" while read line do echo "${line}" echo $line done < "${file}"
24.2 method 2
#!/bin/bash # with cat into a pipe (read into a single variable and print that) cat hello.txt | while read p do echo $p done
24.3 method 3
#!/bin/bash # Use IFF the input field seperator while IFS=' ' read -r line do echo $line done < hello.txt
-r prefents the backslash from being interpretted. but method 3 to me should be avoided, because it is obtuse. wtf!??
24.4 method 4
Using file descriptors.
#!/bin/bash filename='peptides.txt' exec 4<"$filename" echo Start while read -u4 p ; do echo "$p" done
or
cut -d$'\n' -f1 file.cut
"Words of the form $'string'
are treated specially. The word expands to
string, with backslash-escaped characters replaced as specified by the ANSI C
standard."
24.5 same on one line
for word in $(cat peptides.txt); do echo $word; done
24.6 more elaborate using IFS
This is all on one line, but split over three using \ The idea here is to temporarily change the current IFS, input field separator, to a newline, \n, and split the file by new lines. Default would split words I believe.
OLDIFS=$IFS; IFS=$'\n';\ for line in $(cat peptides.txt); \ do cmd_a.sh $line; cmd_b.py $line; \ done > outfile.txt; IFS=$OLDIFS
25 Sample bash sripts
25.1 Backup script from the web:
Check out Jason's script: I got this from jasonmcarman
#!/bin/bash # Jason Carman; Jasonmcarman@gmail.com # Created: February 2012 # Updated: August 2013 # Purpose: To automate virtual machine back up and restoration # Version: 3.1 # script should be run as ./vs [options] # # possible options: # -b - Backup # -r - Restore # -f - Restore on Fresh Install of Host OS # -o - Run on one vm, not all # NOTE: This script should be run as root # In order to work properly this script requires some initial configuration # for the variables dpath and spath. Read further, as all the configuration # occurs at the top of this script immediately following the comment block. # Error Codes: # 1 - User provided invalid option # 2 - User selected both -b and -r # 3 - User selected neither -b or -r # 4 - User didn't provide an option # 5 - Destination path not found # 6 - Source path not found # 7 - User provided a vm name but no action to perform (-b or -r) # 8 - User selected -o but didn't provide a vm name ######################################################################### ## Variable Configuration - change these values to reflect your system ## ######################################################################### # these path names should reflect your own directory structure and backup storage location ###### IMPORTANT! ###### # For this to work properly the directories contained in the variable must exist. If you're doing a restore on a fresh install of the host, create the directory contained in dpath and copy the files there before you begin the process. # destination path: dpath='/home/jmcarman/Backups' # source path: spath='/var/lib/libvirt/images' # Array to store virtual machine names, to add more virtual machines to this copy the same format. To change these names to reflect your system, keep the single quotes and change the content inbetween. # # format vms=(nameofmachine1 nameofmachine2 nameofmachine3 nameofmachine4) ect.... vms=(centos1 centos2 centos3) # Variable for log text file for easy reference logfile=virtualsafety-log.txt ######################################################################################### ## Changes beyond this point not recommended unless you really know what you're doing, ## ## as results may be unpredictable. ## ######################################################################################### function backup() { # change directory to where the virtual machines are stored as files cd $spath # use virsh dumpxml to create back ups of the virtual machines virsh dumpxml $vm >$dpath/$vm.xml # gzip images and store them in back up directory, run in the background echo "Creating backup of $vm in $dpath" touch $dpath/$vm.backup.gz $(gzip <$spath/$vm >$dpath/$vm.backup.gz)& progress logMsg="$logMsg $vm," } function restore() { cd $spath echo "Restoring $vm" $(gunzip <$dpath/$vm.backup.gz >$spath/$vm)& progress logMsg="$logMsg $vm," cp $spath/$vm.xml $dpath/$vm.xml virsh define $vm.xml } function logfile () { # check to see if the destination and sourch path exist if [ ! -e "$dpath" ]; then echo "Destination path: "$dpath" does not exist, please reconfigure your script and run again" exit 5 fi if [ ! -e "$spath" ]; then echo "Source path: "$dpath" does not exist, please reconfigure your script and run again" exit 6 fi logcheck=$(ls $dpath | grep $logfile) if [[ "$logcheck" == "" ]]; then # log file doesn't exist, create it echo "VIRTUAL SAFETY LOG" > $dpath/$logfile echo "FOR: $HOSTNAME" >> $dpath/$logfile echo "Backup Source Location: $spath" >> $dpath/$logfile echo "Backup Destination Location: $dpath" >> $dpath/$logifle echo "Start: " $(date | cut -d" " -f2,3,6) >> $dpath/$logfile echo "End: " >> $dpath/$logfile echo "--------------------------------------" >> $dpath/$logfile else # update the end date sed -i "s/End\:.*/End\:\ $(date | cut -d' ' -f2,3,6)/" $dpath/$logfile fi } function progress() { # $! is most recent running process ID, check running processes to see if the backround gzip has completed. While it's running, print a "#" on the screen every 3 seconds. while [[ $(ps | grep $!) != "" ]]; do echo -n '#' sleep 3 done echo "" # To keep messages and prompts aligned properly } function completion() { if [[ $b == 1 ]]; then action=Backup elif [[ $r == 1 ]]; then action=Restoration fi # tell the user the backup process has finished echo "Automated $action Complete, process logged to $dpath/$logfile" # append log time and date to a file for use in future documentation, add a blank line after for easier readability logMsg="$logMsg created with virtualsafety script on" echo "$logMsg" $(date) >> $dpath/$logfile echo "" >> $dpath/$logfile } while getopts brfo: options do case $options in b) b=1 ;; r) r=1 ;; f) f=1 ;; o) o=1;vm=$OPTARG ;; \?) echo "You have selected an invalid option, please use one of the following: -b for backup, -r for restore and if using -r please use -f if this restore is being done on a fresh install of the host" exit 1 ;; esac done if [[ $# == 0 ]]; then echo "./vs must be called with at least one argument, -b to back up -r to restore, -f with -r if doing the restoration on a fresh install of the host" exit 4 fi if [[ "$b" == 1 && "$r" == 1 ]]; then echo "You must select either backup (-b) or restore (-r), not both" exit 2 elif [[ "$b" == 0 && "$r" == 0 ]]; then echo "You must select either backup (-b) or restore (-r)" exit 3 elif [[ "$b" == 0 || "$r" == 0 && "$o" == 1 ]]; then echo "You must select either backup (-b) or restore (-r) with -o" exit 7 elif [[ "$o" == 1 && $vm == "" ]]; then echo "You must provide the name of a virtual machine with -o" exit 8 fi logfile $dpath $logfile if [[ "$b" == 1 ]]; then # tell the user the back up is in progress echo "Begining automated back up..." logMsg="Backups of" # call the backup function in a for loop refferencing the array set above, passing it the parameters stored in the array $vms and variables $dpath, $spath. if [[ "$o" == 1 ]]; then backup $vm $dpath $spath else for vm in "${vms[@]}"; do backup $vm $dpath $spath done # Call the completion function to log the message to the appropriate file completion $b $dpath $logfile $logMsg fi fi if [[ "$r" == 1 ]]; then logMsg="Restoration of" if [[ "$f" == 1 ]]; then echo "Installing virtual machine manager and updating the host" # Install virtualization software and update the host yum -y groupinstall "Virtualization" yum -y update # Start the virtualization service and enable it to start on boot up systemctl start libvirtd systemctl enable libvirtd else echo "Begining automated restoration..." if [[ "$o" == 1 ]]; then # restore only one machine restore $vm $dpath $spath else # call the restore function in a for loop refferencing the array set above, passing it the parameters stored in the array $vms and variables $dpath, $spath. for vm in "${vms[@]}"; do restore $vm $dpath $spath done # Call the completion function to log the message to the appropriate file completion $r $dpath $logfile $logMsg fi fi fi vs Displaying vs.
25.2 Allow an ip address to ssh through iptables script
#!/bin/bash # ask for an ip address, then add a temporary rule to iptables to allow that # address to ssh to this host # temporary because after a reboot, these new addtions will no longer be there # i.e. they are not permanent. echo "Current MYSSH iptables are:" sudo iptables -L MYSSH -n --line-numbers echo " " echo " -----------------------------" echo " Where are you trying to access me from? " echo " Enter the ip address in the form a.b.c.d or a.b.c.0/24 or any other CIDR block form: " read myip echo " " echo " Do you want to add $myip to your iptables. (y/n) ? " read proceedornot if [ $proceedornot = "y" ]; then sudo iptables -I MYSSH 4 -s $myip -p tcp --dport 1963 -j ACCEPT # this iptables command inserts (I) into the MYSSH list, position 4 # the ip address $myip to be accepted on port 1963 echo " -----------------------------" sudo iptables -L MYSSH -n --line-numbers fi echo $proceedornot