Bash Basics

basename & DIRNAME

basename "$FILE"
filename="$(basename $FILE)"
path="$(dirname $FILE)"

BOOLEAN

Bash doesn’t have built-in true and false boolean values that can be used to test for validity.  It’s often just as clear to use 0 or 1 for false and true, respectively.
“For interactive use, like one-liners, make sure to leave a space after !, or it will do history expansion. ((! foo)) works, so does ! ((foo)). ((foo || bar)) works as expected”[1].  Also, remember that bash considers an exit code of 0 success while an exit code of 1 is failure.  If you are working with true being equal to 1 and false being equal to 0 your may have to adjust some bash-y expectations.

#!/bin/bash

flag=0
if (( flag )); then
  echo "Condition is true";
fi

false=0
true=1

((false)) && echo false
((true)) && echo true
((!false)) && echo not false
((!true)) && echo not true

(Source: stackoverflow.com)


conditionals

if...then...elif...else...fi

if TEST-COMMAND1; then
  STATEMENTS1
elif TEST-COMMAND2; then
  STATEMENTS2
elif [ $test_var -ge 3 -a $test_var -lt 11 ]; then
  STATEMENTS3
else 
  STATEMENTS4
fi

FIND

Find files that have been changed in the last 24 hours.

find /var/lib/ -mtime -1 -ls

-1 means anything changed one day or less ago.  +1 means anything that has changed at least one day ago.  Having only 1 means exactly one day ago.

(Source: stackoverflow.com)


LOOPS

Loop through a list of files.

while read patch; do
  patch -p1 -f < ${patch} &>> $PATCH_LOG
done < <(ls -l ${PATCH_SUBDIR}/*.patch)

parameter expansions (substitutions)

For even more reference, see the holy grail of bash PEs.

Replace a character in a string with another character.

$ VER=4.19.87
$ NEWVER="${VER//./_}"
$ echo "$NEWVER"
4_19_87

PATHS

When you experience odd behavior with finding or not finding an installed or uninstalled program, you may need to refresh your remembered locations.

hash -r

See the following references: man hash | pip3 is looking for a wrong path | what does hash -r command do? | what is the purpose of the hash command


PRINTING

printf an exclamation mark.

Option 1: use single-quoted strings.

printf 'Unable to find config file.  Exiting!'

Option 2: add to the end of the string, outside of the quotes

printf "Unable to find config file.  Exiting" !

Option 3: print ASCII representation of ! character

printf "\041"

Option 4: Mix double quotes with single quotes (extension to Option 1)

printf "Unable to find config file.  Exiting"'!'" (that prints an exclamation mark just fine.\n"

Option 5: use a format string

printf "This (%s) is an exclamation mark%s\n" ! !

printf Two Variables
printf "%s %s" % "${MYSTRING1}" "${MYSTRING2}"

 


sequence

for i in $(seq 1 $END); do printf "$i\n"; done

 


set

The Set Built-in

Unless specifically required to not be, the second line of all Bash scripts ought to be: set -euf -o pipefail

Echo on for single command
set -x
ls $mydir
set +x

NOTE: In Jenkins, set -x is the default.  This echoes all commands.  Override by putting the following at the top of the “Execute Shell” build step:

#!/bin/bash +x

Strings

Change Case – Lower/upper

(I do not use “downcase” – it’s “lower case – that word needs to be exonerated from its duty of describing a letter case.)

Entire Word

I’m not even going to try to out-do this answer.

Though I will drop it here for quick reference:

$ myvar="True"
$ echo "$myvar" | tr '[:upper:]' '[:lower:]'
true

First Letter

foo="$(tr '[:lower:]' '[:upper:]' <<< ${foo:0:1})${foo:1}"
String Substitution

Again, great post on StackOverflow.  Dropping it here for quick reference.

#!/bin/bash
firstString="I love Suzi and Marry"
secondString="Sara"
echo "${firstString/Suzi/$secondString}" 
# prints 'I love Sara and Marry'

Style guide

Formatting

shfmt

$ shfmt -w -i 4 -sr <file(s) or path...>

How-to

man page
TLDP BASH Programming How-To
TLDP Advanced Bash-Scripting Guide
Google’s Shell Style Guide

Linter

shellcheck

Tests

-f Check if a file exists

FILE=/tmp/myfile.txt
if [[ -f "$FILE" ]]; then
  echo "$FILE exists"
fi

-z Check if a variable is empty

if [ -z "$var" ]; then
  printf '$var is empty\n';
else
  printf '$var is not empty\n';
fi

SUDO

One-liner including password

NOTE: Do not use this if you do not want others to see your script or bash history, which includes your user’s sudo password.

Use case: SSH to a Clonezilla Server that is already running and already has a specific user created on it.  This user can SSH but the files on the Clonezilla live system are read-only so you can not edit /etc/sudoers file to specify NOPASSWD for your user.

$ echo <password> | sudo -S <command>

Trickster: to avoid exposing the password on the command history, start the command with a SPACE character.

Check for sudo in your shell scripts
if [ $(id -u) -eq 0 ]; then
  printf "You are root\n"
else
  printf "You are not root\n"
fi

trap

For easier cleanup, use trap

 


Variables

Default Value
MyVar="${DEPLOY_ENV:-default_value}"

Git Stuff

Storing Screts

“Why should I worry? Our GitHub repository is private, and only our team can access the secrets!”

— Safelyl Storing Secrets in Git
EmbeddedArtistry.com

Even if you don’t want to concern yourself with it, consider it a challenge to keep your secrets secret.


Tips & Tricks

Tell git which key to use

Same way you tell any SSH session which key to use.

# vi ~/.ssh/config

Host github.com
    Hostname %h
    User git
    IdentityFile ~/.ssh/github_id_rsa

Workflow

Update Local with Remote

Use git pull --rebase

Rebase Local Master with Remote Master
git stash                               # optional
git checkout master
git fetch origin
git rebase origin/master

IPTables – Log Everything

First, this allows all traffic in and logs those requests.

Secondly, the final rules could probably be tweaked.  I may do another post to examine this set very, very closely.

NOTE: if you’re making changes from a remote location, make sure you tell your firewall to let you in.  If you already have rules in place, this will do no harm.  If you have no rules in place, this will hopefully make it clear to you and your system that SSH needs to be let through.  The -s <start-ip> <end-ip> is optional.

iptables -A INPUT -s <start-ip> <end-ip> -p tcp --dport 22 -j ACCEPT

View current distro info (if you’re not sure):

root@aptly:~# lsb_release -a
No LSB modules are available.
Distributor ID: Debian
Description: Debian GNU/Linux 7.8 (wheezy)
Release: 7.8
Codename: wheezy

Based on the distro info, do we know where iptables configs are being stored?  From experience, I would suspect /etc/sysconfig (RHEL flavors) or /etc/iptables (Debian flavors) but neither directory exists in my case.  Taking a look at iptables -L -n, there are no rules loaded.  So, maybe I’ll just create my own and load them at boot.  Also, take a look at netstat -tlnp to see what is currently listening and be sure not to block anything that you know you need.

# netstat -tlnp
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address Foreign Address State      PID/Program name
tcp        0      0 0.0.0.0:111   0.0.0.0:*       LISTEN     1642/rpcbind
tcp        0      0 0.0.0.0:22    0.0.0.0:*       LISTEN     2519/sshd
tcp        0      0 127.0.0.1:25  0.0.0.0:*       LISTEN     2471/exim4
tcp        0      0 0.0.0.0:58397 0.0.0.0:*       LISTEN     1673/rpc.statd
tcp        0      0 0.0.0.0:10050 0.0.0.0:*       LISTEN     2357/zabbix_agentd
tcp6       0      0 :::111        :::*            LISTEN     1642/rpcbind
tcp6       0      0 :::22         :::*            LISTEN     2519/sshd
tcp6       0      0 ::1:25        :::*            LISTEN     2471/exim4
tcp6       0      0 :::36063      :::*            LISTEN     1673/rpc.statd
tcp6       0      0 :::10050      :::*            LISTEN     2357/zabbix_agentd

Now, let’s begin…

Start by making a place for your rules.

mkdir /etc/iptables
vi /etc/iptables/iptables.ipv4

Add the following, or something like it:

# INPUT
iptables -N LOGGING

# Allows all loopback (lo0) traffic and drop all traffic to 127/8 that doesn't use lo0
iptables -A INPUT -i lo -j ACCEPT
iptables -A INPUT ! -i lo -d 127.0.0.0/8 -j REJECT

# Accepts all established inbound connections
iptables -A INPUT -m state --state ESTABLISHED,RELATED -j LOGGING

# Allows SSH connections
# The --dport number is the same as in /etc/ssh/sshd_config
iptables -A INPUT -p tcp -m state --state NEW --dport 22 -j LOGGING

# Allow ping
# note that blocking other types of icmp packets is considered a bad idea by some
# remove -m icmp --icmp-type 8 from this line to allow all kinds of icmp:
# https://security.stackexchange.com/questions/22711
iptables -A INPUT -p icmp -m icmp --icmp-type 8 -j LOGGING

# log iptables denied calls (access via 'dmesg' command)
iptables -A INPUT -j LOGGING

iptables -A LOGGING -m limit --limit 4/min -j LOG --log-prefix "iptables-accepted: " --log-level 4
iptables -A LOGGING -j ACCEPT

# OUTPUT
# Allows all outbound traffic
iptables -A OUTPUT -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
iptables -A OUTPUT -j ACCEPT

# Reject all other inbound - default deny unless explicitly allowed policy:
iptables -A INPUT -j REJECT

# iptables -A FORWARD -i eth0 -m state --state RELATED,ESTABLISHED -j ACCEPT
iptables -A FORWARD -j REJECT

Save your changes. Note, the commands in this file are to be *run* on the command line, NOT *loaded* by iptables. So…load this series of iptables commands:

bash /etc/iptables/iptables.ipv4

Assuming you did not lose contact with your host, check the state of your firewall, now.

# iptables -L -n
Chain INPUT (policy ACCEPT)
target prot opt source destination
ACCEPT tcp -- 0.0.0.0/0 0.0.0.0/0 tcp dpt:22
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0
REJECT all -- 0.0.0.0/0 127.0.0.0/8 reject-with icmp-port-unreachable
LOGGING all -- 0.0.0.0/0 0.0.0.0/0 state RELATED,ESTABLISHED
LOGGING tcp -- 0.0.0.0/0 0.0.0.0/0 state NEW tcp dpt:22
LOGGING icmp -- 0.0.0.0/0 0.0.0.0/0 icmptype 8
LOGGING all -- 0.0.0.0/0 0.0.0.0/0
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable

Chain FORWARD (policy ACCEPT)
target prot opt source destination
REJECT all -- 0.0.0.0/0 0.0.0.0/0 reject-with icmp-port-unreachable

Chain OUTPUT (policy ACCEPT)
target prot opt source destination
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0 state NEW,RELATED,ESTABLISHED
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

Chain LOGGING (5 references)
target prot opt source destination
LOG all -- 0.0.0.0/0 0.0.0.0/0 limit: avg 4/min burst 5 LOG flags 0 level 4 prefix "iptables-accepted: "
ACCEPT all -- 0.0.0.0/0 0.0.0.0/0

I will note here first that as it stands now, this may not be the most efficient use of the rules.  I doesn’t seem to be the most succinct BUT for now, it’s okay.

Save the current rules to an IPTables config.

iptables-save > /etc/iptables/rules.v4

This is what the rules.v4 file contains:

# Generated by iptables-save v1.4.14 on Thu Mar 12 10:04:00 2020
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:LOGGING - [0:0]
-A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -d 127.0.0.0/8 ! -i lo -j REJECT --reject-with icmp-port-unreachable
-A INPUT -m state --state RELATED,ESTABLISHED -j LOGGING
-A INPUT -p tcp -m state --state NEW -m tcp --dport 22 -j LOGGING
-A INPUT -p icmp -m icmp --icmp-type 8 -j LOGGING
-A INPUT -j LOGGING
-A INPUT -j REJECT --reject-with icmp-port-unreachable
-A FORWARD -j REJECT --reject-with icmp-port-unreachable
-A OUTPUT -m state --state NEW,RELATED,ESTABLISHED -j ACCEPT
-A OUTPUT -j ACCEPT
-A LOGGING -m limit --limit 4/min -j LOG --log-prefix "iptables-accepted: "
-A LOGGING -j ACCEPT
COMMIT
# Completed on Thu Mar 12 10:04:00 2020

Now…normally, I would follow methods that are more generally acceptable for making sure these rules are loaded each reboot (see iptables-persistent).  However, the system on which I’m setting this up is a system that is being retired.  The system is providing legacy services and cannot be updated under any circumstances.  This exercise was begun to simply determine what services or servers are accessing this server so I don’t want to do anything too intrusive, like updating packages or even installing packages, for that matter.  Therefore, I will load these rules at boot time via cron.

@reboot /sbin/iptables-restore < /etc/iptables/rules.v4

One more test:

# iptables -F
# iptables-restore /etc/iptables/rules.v4
# iptables -L -n

And now reboot to test it all.

I’m seeing some ntp that I didn’t account for:

Mar 12 10:19:25 aptly kernel: [ 6.316149] iptables-accepted: IN=eth0 OUT= MAC=9a:6c:12:75:a3:20:00:1c:73:8d:05:d7:08:00 SRC=88.99.76.254 DST=10.61.75.72 LEN=76 TOS=0x18 PREC=0x00 TTL=50 ID=28740 DF PROTO=UDP SPT=123 DPT=123 LEN=56

What the heck is 88.99.76.254?  Seems legit, according to this post, but it did remind me that I ought to update my NTP servers to point to our internal ones.

 

Python 3 on CentOS 7

Without impacting your native Python installation, you can install Python 3 along side of it using the following method.

Installing Python 3

Step 1: If you can, take a snapshot of your VM before proceeding.

Step 2: Run the following commands:

$ sudo yum update
$ sudo yum install @development
$ sudo yum install -y zlib-devel openssl-devel sqlite-devel bzip2-devel xz-libs
$ VER=3.6.5
$ PYVER="Python-${VER}"
$ wget http://www.python.org/ftp/python/${VER}/${PYVER}.tar.xz
$ xz -d ./${PYVER}.tar.xz
$ tar -xvf ./${PYVER}.tar
$ cd ${PYVER}
$ ./configure
$ make
$ sudo make altinstall

To see what you’ve done:

$ which python3
/usr/local/bin/python3

$ which python
/usr/bin/python

$ which python3.6
/usr/local/bin/python3.6

You can create symlinks to safely reference items of interest:

$ ls -al /usr/local/bin/
total 24836
drwxr-xr-x.  2 root root      196 May 17 17:38 .
drwxr-xr-x. 12 root root      131 Apr 11 00:59 ..
-rwxr-xr-x.  1 root root      101 May 17 16:38 2to3-3.6
-rwxr-xr-x.  1 root root      242 May 17 16:38 easy_install-3.6
-rwxr-xr-x.  1 root root       99 May 17 16:38 idle3.6
lrwxrwxrwx.  1 root root       21 May 17 17:38 pip3 -> /usr/local/bin/pip3.6
-rwxr-xr-x.  1 root root      214 May 17 16:38 pip3.6
-rwxr-xr-x.  1 root root       84 May 17 16:38 pydoc3.6
lrwxrwxrwx.  1 root root       24 May 17 17:31 python3 -> /usr/local/bin/python3.6
-rwxr-xr-x.  2 root root 12699000 May 17 16:38 python3.6
-rwxr-xr-x.  2 root root 12699000 May 17 16:38 python3.6m
-rwxr-xr-x.  1 root root     3117 May 17 16:38 python3.6m-config
-rwxr-xr-x.  1 root root      441 May 17 16:38 pyvenv-3.6

Python 3 and pip

Issues like missing modules may arise.  They’ll look something like the following:

$ python3 ./main.py 
Traceback (most recent call last):
File "./main.py", line 8, in <module>
from lxml.etree import ET
ModuleNotFoundError: No module named 'lxml'

Can pip help us?

$ which pip3
/usr/local/bin/pip3

Looks like I have a pip3.  Let’s try…

$ pip3 install lxml
-bash: /usr/local/bin/pip3: /usr/local/bin/python3.6: bad interpreter: No such file or directory

Hmmm…that does NOT look good.  Do I have python3.6?

$ which python3.6
/usr/bin/python3.6

Sure do, but it’s in a location that pip3 is paying no attention to.  As you can see from the output, pip3 is unusable.  Just running pip3 results in bad interpreter: No such file or directory.  We need to fix pip or fix the part where it’s expecting python3.6 to be in /usr/local/bin/python3.6

Easiest thing to try is to update the operating system’s hash table where it stores its mappings of programs to their locations so it doesn’t have to search every time.  Sometimes those mappings can get out of sync, especially if a location of a program changes.

$ hash
hits command
1 /usr/bin/sudo
2 /usr/local/bin/pip3
2 /usr/bin/python3
2 /usr/bin/ls
$ hash -r
$ hash
hash: hash table empty

Maybe that will help pip3 find what it needs?  Um, no.  Not one bit.

$ pip3 install lxml
-bash: /usr/local/bin/pip3: /usr/local/bin/python3.6: bad interpreter: No such file or directory

Why is pip3 expecting python3.6 to be in that location when it clearly isn’t?

directory listing showing broken symlink
When symlinks are broken they blink.

After fixing my symlink (not because it would help with my current issue but because it’s good to fix things), I returned my focus to pip3.  I would like to uninstall and reinstall it.  If it is being managed by yum, it can easily be uninstalled by calling yum remove.

$ sudo yum remove python3-pip
Loaded plugins: fastestmirror
No Match for argument: python3-pip
No Packages marked for removal

But, it isn’t.  How did pip3 get on this machine?  Can python3 run pip as a module?

$ python3 -m pip
/usr/local/bin/python3: No module named pip
 -m module-name
       Searches sys.path for the named module and runs the corresponding .py file as a script.

Nope.  Okay, enough spinning my wheels.  The setup I have isn’t making much sense.  How is this supposed to happen?  How is pip supposed to be installed?  Maybe I can do what’s supposed to be done (or by trying something else somebody else claims to have worked for them) and things will just work?

$ python3 -m ensurepip --user
Looking in links: /tmp/tmp41dea8fo
Requirement already satisfied: setuptools in /usr/lib/python3.6/site-packages (39.2.0)
Collecting pip
Installing collected packages: pip
Successfully installed pip-18.1

$ python3 -m pip

Usage: 
 /usr/local/bin/python3 -m pip <command> [options]

Commands:
 install                     Install packages.
 download                    Download packages.
 uninstall                   Uninstall packages.
 freeze                      Output installed packages in requirements format.
 list                        List installed packages.
 show                        Show information about installed packages.
 check                       Verify installed packages have compatible dependencies.
 config                      Manage local and global configuration.
 search                      Search PyPI for packages.
 wheel                       Build wheels from your requirements.
 hash                        Compute hashes of package archives.
 completion                  A helper command used for command completion.
 help                        Show help for commands.

General Options:
 -h, --help                  Show help.
 --isolated                  Run pip in an isolated mode, ignoring environment variables and user configuration.
 -v, --verbose               Give more output. Option is additive, and can be used up to 3 times.
 -V, --version               Show version and exit.
 -q, --quiet                 Give less output. Option is additive, and can be used up to 3 times (corresponding to
                             WARNING, ERROR, and CRITICAL logging levels).
--log <path>                 Path to a verbose appending log.
--proxy <proxy>              Specify a proxy in the form [user:passwd@]proxy.server:port.
--retries <retries>          Maximum number of retries each connection should attempt (default 5 times).
--timeout <sec>              Set the socket timeout (default 15 seconds).
--exists-action <action>     Default action when a path already exists: (s)witch, (i)gnore, (w)ipe, (b)ackup, (a)bort).
--trusted-host <hostname>    Mark this host as trusted, even though it does not have valid or any HTTPS.
--cert <path>                Path to alternate CA bundle.
--client-cert <path>         Path to SSL client certificate, a single file containing the private key and the
                             certificate in PEM format.
--cache-dir <dir>            Store the cache data in <dir>.
--no-cache-dir               Disable the cache.
--disable-pip-version-check
                             Don't periodically check PyPI to determine whether a new version of pip is available for
                             download. Implied with --no-index.
--no-color                   Suppress colored output

Yep!

Can I do what I had originally set out to do?

$ python3 -m pip install --user lxml
Collecting lxml
Using cached https://files.pythonhosted.org/packages/dd/ba/a0e6866057fc0bbd17192925c1d63a3b85cf522965de9bc02364d08e5b84/lxml-4.5.0-cp36-cp36m-manylinux1_x86_64.whl
Installing collected packages: lxml
Successfully installed lxml-4.5.0

Yep!