Skip to content

Commit 4dfbca0

Browse files
authored
Merge pull request #1 from gruntwork-io/v0.0.1
bash-commons v0.0.1
2 parents ed6a23c + 4c2d1c2 commit 4dfbca0

21 files changed

+1758
-1
lines changed

.circleci/Dockerfile

+13
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
FROM ubuntu:16.04
2+
MAINTAINER Gruntwork <[email protected]>
3+
4+
# Install Bats
5+
RUN DEBIAN_FRONTEND=noninteractive apt-get update && \
6+
apt-get install -y software-properties-common && \
7+
add-apt-repository ppa:duggan/bats && \
8+
DEBIAN_FRONTEND=noninteractive apt-get update && \
9+
apt-get install -y bats
10+
11+
# Install other basic dependencies
12+
RUN apt-get install -y python-pip jq sudo && \
13+
pip install awscli --upgrade --user

.circleci/config.yml

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version: 2
2+
jobs:
3+
build:
4+
docker:
5+
- image: gruntwork/bash-commons-circleci-tests
6+
steps:
7+
- checkout
8+
- run: bats test

README.md

+219-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,221 @@
11
# Bash Commons
22

3-
[WIP]
3+
This repo contains a collection of reusable Bash functions for handling common tasks such as logging, assertions,
4+
string manipulation, and more. It is our attempt to bring a little more sanity, predictability, and coding reuse to our
5+
Bash scripts. All the code has thorough automated tests and is packaged into functions, so you can safely import it
6+
into your bash scripts using `source`.
7+
8+
9+
10+
11+
## Examples
12+
13+
Once you have `bash-commons` installed (see the [install instructions](#install)), you use `source` to import the
14+
modules and start calling the functions within them:
15+
16+
```bash
17+
source /opt/gruntwork/bash-commons/log.sh
18+
source /opt/gruntwork/bash-commons/assert.sh
19+
source /opt/gruntwork/bash-commons/os.sh
20+
21+
log_info "Hello, World!"
22+
23+
assert_not_empty "--foo" "$foo" "You must provide a value for the --foo parameter."
24+
25+
if os_is_ubuntu "16.04"; then
26+
log_info "This script is running on Ubuntu 16.04!"
27+
elif os_is_centos; then
28+
log_info "This script is running on CentOS!"
29+
fi
30+
```
31+
32+
33+
34+
35+
## Install
36+
37+
The first step is to download the code onto your computer.
38+
39+
The easiest way to do this is with the [Gruntwork Installer](https://github.com/gruntwork-io/gruntwork-installer)
40+
(note, you'll need to replace `<VERSION>` below with a version number from the [releases
41+
page](https://github.com/gruntwork-io/bash-commons/releases)):
42+
43+
```bash
44+
gruntwork-install \
45+
--repo https://github.com/gruntwork-io/bash-commons \
46+
--module-name bash-commons \
47+
--tag <VERSION>
48+
```
49+
50+
The default install location is `/opt/gruntwork/bash-commons`, but you can override that using the `dir` param, and
51+
override the owner of the install dir using the `owner` and `group` params:
52+
53+
```bash
54+
gruntwork-install \
55+
--repo https://github.com/gruntwork-io/bash-commons \
56+
--module-name bash-commons \
57+
--tag <VERSION> \
58+
--module-param dir=/foo/bar \
59+
--module-param owner=my-os-username \
60+
--module-param group=my-os-group
61+
```
62+
63+
If you don't want to use the Gruntwork Installer, you can use `git clone` to get the code onto your computer and then
64+
copy it to it's final destination manually:
65+
66+
```bash
67+
git clone --branch <VERSION> https://github.com/gruntwork-io/bash-commons.git
68+
69+
sudo mkdir -p /opt/gruntwork
70+
cp -r bash-commons/modules/bash-commons/src /opt/gruntwork/bash-commons
71+
sudo chown -R "my-os-username:my-os-group" /opt/gruntwork/bash-commons
72+
```
73+
74+
75+
76+
## Importing modules
77+
78+
You can use the `source` command to "import" the modules you need and use them in your code:
79+
80+
```bash
81+
source /opt/gruntwork/bash-commons/log.sh
82+
```
83+
84+
This will make all the functions within that module available in your code:
85+
86+
```bash
87+
log_info "Hello, World!"
88+
```
89+
90+
91+
92+
93+
## Available modules
94+
95+
Here's an overview of the modules available in `bash-commons`:
96+
97+
* `array.sh`: Helpers for working with Bash arrays, such as checking if an array contains an element, or joining an
98+
array into a string with a delimiter between elements.
99+
100+
* `assert.sh`: Assertions that check a condition and exit if the condition is not met, such as asserting a variable is
101+
not empty or that an expected app is installed. Useful for defensive programming.
102+
103+
* `aws.sh`: A collection of thin wrappers for direct calls to the [AWS CLI](https://aws.amazon.com/cli/) and [EC2
104+
Instance Metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html). These thin
105+
wrappers give you a shorthand way to fetch certain information (e.g., information about an EC2 Instance, such as its
106+
private IP, public IP, Instance ID, and region). Moreover, you can swap out `aws.sh` with a version that returns mock
107+
data to make it easy to run your code locally (e.g., in Docker) and to run unit tests.
108+
109+
* `aws-wrapper.sh`: A collection of "high level" wrappers for the [AWS CLI](https://aws.amazon.com/cli/) and [EC2
110+
Instance Metadata](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html) to simplify common
111+
tasks such as looking up tags or IPs for EC2 Instances. Note that these wrappers handle all the data processing and
112+
logic, whereas all the direct calls to the AWS CLI and EC2 metadata endpoints are delegated to `aws.sh` to make unit
113+
testing easier.
114+
115+
* `file.sh`: A collection of helpers for working with files, such as checking if a file exists or contains certain text.
116+
117+
* `log.sh`: A collection of logging helpers that write logs to `stderr` with log levels (INFO, WARN, ERROR) and
118+
timestamps.
119+
120+
* `os.sh`: A collection of Operating System helpers, such as checking which flavor of Linux (e.g., Ubuntu, CentOS) is
121+
running and validating checksums.
122+
123+
* `string.sh`: A collection of string manipulation functions, such as checking if a string contains specific text,
124+
stripping prefixes, and stripping suffixes.
125+
126+
127+
128+
129+
## Coding principles
130+
131+
The code in `bash-commons` follows the following principles:
132+
133+
1. [Compatibility](#compatibility)
134+
1. [Code style](#code-style)
135+
1. [Everything is a function](#everything-is-a-function)
136+
1. [Namespacing](#namespacing)
137+
1. [Testing](#testing)
138+
139+
140+
### Compatibility
141+
142+
The code in this repo aims to be compatible with:
143+
144+
* Bash 3
145+
* Most major Linux distributions (e.g., Ubuntu, CentOS)
146+
147+
148+
### Code style
149+
150+
All the code should mainly follow the [Google Shell Style Guide](https://google.github.io/styleguide/shell.xml).
151+
In particular:
152+
153+
* The first line of every script should be `#!/bin/bash`.
154+
* All code should be defined in functions.
155+
* Functions should exit or return 0 on success and non-zero on error.
156+
* Functions should return output by writing it to `stdout`.
157+
* Functions should log to `stderr`.
158+
* All variables should be `local`. No global variables are allowed at all.
159+
* Make as many variables `readonly` as possible.
160+
* If calling to a subshell and storing the output in a variable (foo=`$( ... )`), do NOT use `local` and `readonly`
161+
in the same statement or the [exit code will be
162+
lost](https://blog.gruntwork.io/yak-shaving-series-1-all-i-need-is-a-little-bit-of-disk-space-6e5ef1644f67). Instead,
163+
declare the variable as `local` on one line and then call the subshell on the next line.
164+
* Quote all strings.
165+
* Use `[[ ... ]]` instead of `[ ... ]`.
166+
* Use snake_case for function and variable names. Use UPPER_SNAKE_CASE for constants.
167+
168+
169+
### Everything in a function
170+
171+
It's essential that ALL code is defined in a function. That allows you to use `source` to "import" that code without
172+
anything actually being executed.
173+
174+
175+
### Namespacing
176+
177+
Bash does not support namespacing, so we fake it using a convention on the function names: if you create a file
178+
`<foo.sh>`, all functions in it should start with `foo_`. For example, all the functions in `log.sh` start with `log_`
179+
(`log_info`, `log_error`) and all the functions in `string.sh` start with `string_` (`string_contains`,
180+
`string_strip_prefix`). That makes it easier to tell which functions came from which modules.
181+
182+
For readability, that means you should typically give files a name that is a singular noun. For example, `log.sh`
183+
instead of `logging.sh` and `string.sh` instead of `strings.sh`.
184+
185+
186+
### Testing
187+
188+
Every function should be tested:
189+
190+
* Automated tests are in the [test](/test) folder.
191+
192+
* We use [Bats](https://github.com/sstephenson/bats) as our unit test framework for Bash code. Note: Bats has not been
193+
maintained the last couple years, so we may need to change to the [bats-core](https://github.com/bats-core/bats-core)
194+
fork at some point (see [#150](https://github.com/sstephenson/bats/issues/150)).
195+
196+
* We run all tests in the [gruntwork/bash-commons-circleci-tests Docker
197+
image](https://hub.docker.com/r/gruntwork/bash-commons-circleci-tests/) so that (a) it's consistent with how the CI
198+
server runs them, (b) the tests always run on Linux, (c) any changes the tests make, such as writing files or
199+
creating OS users, won't affect the host OS, (d) we can replace some of the modules, such as `aws.sh`, with mocks at
200+
test time. There is a `docker-compose.yml` file in the `test` folder to make it easy to run the tests.
201+
202+
* To run all the tests: `docker-compose up`.
203+
204+
* To run one test file: `docker-compose run tests bats test/array.bats`.
205+
206+
* To leave the Docker container running so you can debug, explore, and interactively run bats: `docker-compose run tests bash`.
207+
208+
* If you ever need to build a new Docker image, the `Dockerfile` is in the [.circleci folder](/.circleci):
209+
210+
```bash
211+
cd .circleci
212+
docker build -t gruntwork/bash-commons-circleci-tests .
213+
docker push gruntwork/bash-commons-circleci-tests
214+
```
215+
216+
217+
218+
## TODO
219+
220+
1. Add automated tests for `aws.sh` and `aws-wrapper.sh`. We have not tested these as they require either running an
221+
EC2 Instance or run something like [LocalStack](https://github.com/localstack/localstack).

modules/bash-commons/install.sh

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
#!/bin/bash
2+
# This script is used by the Gruntwork Installer to install the bash-commons library.
3+
4+
set -e
5+
6+
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
readonly BASH_COMMONS_SRC_DIR="$SCRIPT_DIR/src"
8+
9+
source "$BASH_COMMONS_SRC_DIR/log.sh"
10+
source "$BASH_COMMONS_SRC_DIR/assert.sh"
11+
source "$BASH_COMMONS_SRC_DIR/os.sh"
12+
13+
readonly DEFAULT_INSTALL_DIR="/opt/gruntwork/bash-commons"
14+
readonly DEFAULT_USER_NAME="$(os_get_current_users_name)"
15+
readonly DEFAULT_USER_GROUP_NAME="$(os_get_current_users_group)"
16+
17+
function print_usage {
18+
echo
19+
echo "Usage: install.sh [options]"
20+
echo
21+
echo "This script is used by the Gruntwork Installter to install the bash-commons library."
22+
echo
23+
echo "Options:"
24+
echo
25+
echo -e " --dir\t\tInstall the bash-commons library into this folder. Default: $DEFAULT_INSTALL_DIR"
26+
echo -e " --owner\tMake this user the owner of the folder in --dir. Default: $DEFAULT_USER_NAME."
27+
echo -e " --group\tMake this group the owner of the folder in --dir. Default: $DEFAULT_USER_GROUP_NAME."
28+
echo -e " --help\tShow this help text and exit."
29+
echo
30+
echo "Example:"
31+
echo
32+
echo " gruntwork-install --repo https://github.com/gruntwork-io/bash-commons --module-name bash-commons --tag v0.0.1 --module-param dir=/opt/gruntwork/bash-commons"
33+
}
34+
35+
function install {
36+
local install_dir="$DEFAULT_INSTALL_DIR"
37+
local install_dir_owner="$DEFAULT_USER_NAME"
38+
local install_dir_group="$DEFAULT_USER_GROUP_NAME"
39+
40+
while [[ $# > 0 ]]; do
41+
local key="$1"
42+
43+
case "$key" in
44+
--dir)
45+
assert_not_empty "$key" "$2"
46+
install_dir="$2"
47+
shift
48+
;;
49+
--owner)
50+
assert_not_empty "$key" "$2"
51+
install_dir_owner="$2"
52+
shift
53+
;;
54+
--group)
55+
assert_not_empty "$key" "$2"
56+
install_dir_group="$2"
57+
shift
58+
;;
59+
--help)
60+
print_usage
61+
exit
62+
;;
63+
*)
64+
log_error "Unrecognized argument: $key"
65+
print_usage
66+
exit 1
67+
;;
68+
esac
69+
70+
shift
71+
done
72+
73+
log_info "Starting install of bash-commons..."
74+
75+
sudo mkdir -p "$install_dir"
76+
sudo cp -R "$BASH_COMMONS_SRC_DIR/." "$install_dir"
77+
sudo chown -R "$install_dir_owner:$install_dir_group" "$install_dir"
78+
79+
log_info "Successfully installed bash-commons!"
80+
}
81+
82+
install "$@"

modules/bash-commons/src/array.sh

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
#!/bin/bash
2+
3+
set -e
4+
5+
# Returns 0 if the given item (needle) is in the given array (haystack); returns 1 otherwise.
6+
function array_contains {
7+
local readonly needle="$1"
8+
shift
9+
local readonly haystack=("$@")
10+
11+
local item
12+
for item in "${haystack[@]}"; do
13+
if [[ "$item" == "$needle" ]]; then
14+
return 0
15+
fi
16+
done
17+
18+
return 1
19+
}
20+
21+
# Joins the elements of the given array into a string with the given separator between each element.
22+
#
23+
# Examples:
24+
#
25+
# array_join "," ("A" "B" "C")
26+
# Returns: "A,B,C"
27+
#
28+
function array_join {
29+
local readonly separator="$1"
30+
shift
31+
local readonly values=("$@")
32+
33+
local out=""
34+
for (( i=0; i<"${#values[@]}"; i++ )); do
35+
if [[ "$i" -gt 0 ]]; then
36+
out="${out}${separator}"
37+
fi
38+
out="${out}${values[i]}"
39+
done
40+
41+
echo -n "$out"
42+
}

0 commit comments

Comments
 (0)