|
1 | 1 | # Bash Commons
|
2 | 2 |
|
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). |
0 commit comments