add shellspec tests to the install.sh file

This commit is contained in:
Scott Motte
2024-06-14 17:23:59 -07:00
parent 7e0885b0a9
commit 16c234855f
7 changed files with 960 additions and 1 deletions

1
.gitignore vendored
View File

@@ -16,3 +16,4 @@ tests/tmp
!tests/.env.multiline
!tests/.env.vault
!tests/monorepo/**/.env*
spec/tmp/*

View File

@@ -7,3 +7,5 @@ dist/*
.env*
!.env.vault
tests/
spec/
.shellspec

12
.shellspec Normal file
View File

@@ -0,0 +1,12 @@
--require spec_helper
## Default kcov (coverage) options
# --kcov-options "--include-path=. --path-strip-level=1"
# --kcov-options "--include-pattern=.sh"
# --kcov-options "--exclude-pattern=/.shellspec,/spec/,/coverage/,/report/"
## Example: Include script "myprog" with no extension
# --kcov-options "--include-pattern=.sh,myprog"
## Example: Only specified files/directories
# --kcov-options "--include-pattern=myprog,/lib/"

446
install.sh Normal file
View File

@@ -0,0 +1,446 @@
#!/bin/sh
set -e
VERSION=""
DIRECTORY="/usr/local/bin"
REGISTRY_URL="https://registry.npmjs.org"
INSTALL_SCRIPT_URL="https://dotenvx.sh/install.sh"
# ./install.sh
# ___________________________________________________________________________________________________
# | _ |
# | | | | | |
# | __| | ___ | |_ ___ _ ____ ____ __ |
# | / _` |/ _ \| __/ _ \ '_ \ \ / /\ \/ / |
# | | (_| | (_) | || __/ | | \ V / > < |
# | \__,_|\___/ \__\___|_| |_|\_/ /_/\_\ |
# | |
# | |
# | *a better dotenv*from the creator of [`dotenv`](https://github.com/motdotla/dotenv). |
# | |
# | * run anywhere (cross-platform) |
# | * multi-environment |
# | * encrypted envs |
# | |
# | ## Install |
# | |
# | ```sh |
# | curl -sfS https://dotenvx.sh/install.sh | sh |
# | ``` |
# | |
# | or self-execute this file: |
# | |
# | ```sh |
# | curl -sfS https://dotenvx.sh/install.sh > install.sh |
# | chmod +x install.sh |
# | ./install.sh |
# | ``` |
# | |
# | more install examples: |
# | |
# | ```sh |
# | # curl examples |
# | curl -sfS "https://dotenvx.sh/install.sh" | sudo sh |
# | curl -sfS "https://dotenvx.sh/install.sh?directory=." | sh |
# | curl -sfS "https://dotenvx.sh/install.sh?version=0.44.0" | sh |
# | curl -sfS "https://dotenvx.sh/install.sh?directory=/custom/path&version=0.44.0" | sh |
# | |
# | # self-executing examples |
# | ./install.sh --directory=. |
# | ./install.sh --version=0.44.0 |
# | ./install.sh --directory=/custom/path --version=0.44.0 |
# | ./install.sh --help |
# | ``` |
# | |
# | ## Usage |
# | |
# | ```sh |
# | $ echo "HELLO=World" > .env |
# | $ echo "console.log('Hello ' + process.env.HELLO)" > index.js |
# | |
# | $ node index.js |
# | Hello undefined # without dotenvx |
# | |
# | $ dotenvx run -- node index.js |
# | Hello World # with dotenvx |
# | ``` |
# | |
# | see [`dotenvx`](https://github.com/dotenvx/dotenvx) for extended usage guides. |
# | |
# |_________________________________________________________________________________________________|
# usage ---------------------------------
usage() {
echo "Usage: $0 [options] [command]"
echo ""
echo "install dotenvx a better dotenv"
echo ""
echo "Options:"
echo " --directory directory to install dotenvx to (default: \"/usr/local/bin\")"
echo " --version version of dotenvx to install (default: \"$VERSION\")"
echo ""
echo "Commands:"
echo " install install dotenvx"
echo " help display help"
}
# machine checks ------------------------
is_version_valid() {
if [ -z "$VERSION" ]; then
echo "[INSTALLATION_FAILED] VERSION is blank in install.sh script"
echo "? set VERSION to valid semantic semver version and try again"
return 1
fi
local semver_regex="^([0-9]+)\.([0-9]+)\.([0-9]+)$"
if [[ "$VERSION" =~ $semver_regex ]]; then
return 0
else
echo "[INSTALLATION_FAILED] VERSION is not a valid semantic version in install.sh script"
echo "? set VERSION to valid semantic semver version and try again"
return 1
fi
}
is_directory_writable() {
# check installation directory is writable
if [ ! -w "$(directory)" ]; then
echo "[INSTALLATION_FAILED] the installation directory [$(directory)] is not writable by the current user"
echo "? run as root [$(help_sudo_install_command "$0")] or choose a writable directory like your current directory [$(help_customize_directory_command "$0")]"
return 1
fi
return 0
}
is_curl_installed() {
local curl_path="$(which_curl)"
if [ -z "$curl_path" ]; then
echo "[INSTALLATION_FAILED] curl is required and is not installed"
echo "? install curl [$(help_install_curl_command)] and try again"
return 1
fi
return 0
}
is_os_supported() {
local os="$(os)"
case "$os" in
linux) os="linux" ;;
darwin) os="darwin" ;;
*)
echo "[INSTALLATION_FAILED] your operating system ${os} is currently unsupported"
echo "? request support by opening an issue at [https://github.com/dotenvx/dotenvx/issues]"
return 1
;;
esac
return 0
}
is_arch_supported() {
local arch="$(arch)"
case "$arch" in
x86_64) arch="x86_64" ;;
amd64) arch="amd64" ;;
arm64) arch="arm64" ;;
aarch64) arch="aarch64" ;;
*)
echo "[INSTALLATION_FAILED] your architecture ${arch} is currently unsupported - must be x86_64, amd64, arm64, or aarch64"
echo "? request support by opening an issue at [https://github.com/dotenvx/dotenvx/issues]"
return 1
;;
esac
return 0
}
# is_* checks ---------------------------
is_piped() {
[ "$0" = "sh" ] || [ "$0" = "bash" ]
}
is_ci() {
[ -n "$CI" ] && [ $CI != 0 ]
}
is_test_mode() {
[ -n "$TEST_MODE" ] && [ $TEST_MODE != 0 ]
}
is_windows() {
[ "$(os)" = "windows" ]
}
is_installed() {
local flagged_version="$1"
local current_version=$("$(directory)/$(binary_name)" --version 2>/dev/null || echo "0")
# if --version flag passed
if [ -n "$flagged_version" ]; then
if [ "$current_version" = "$flagged_version" ]; then
# return true since version already installed
return 0
else
# return false since version not installed
return 1
fi
fi
# if no version flag passed
if [ "$current_version" != "$VERSION" ]; then
# return false since latest is not installed
return 1
fi
echo "[dotenvx@$current_version] already installed ($(directory)/$(binary_name))"
# return true since version already installed
return 0
}
# helpers -------------------------------
directory() {
local dir=$DIRECTORY
case "$dir" in
~*/*)
dir="$HOME/${dir#\~/}"
;;
~*)
dir="$HOME/${dir#\~}"
;;
esac
echo "${dir}"
return 0
}
os() {
echo "$(uname -s | tr '[:upper:]' '[:lower:]')"
return 0
}
arch() {
echo "$(uname -m | tr '[:upper:]' '[:lower:]')"
return 0
}
os_arch() {
echo "$(os)-$(arch)"
return 0
}
filename() {
echo "dotenvx-$VERSION-$(os_arch).tar.gz"
return 0
}
download_url() {
echo "$REGISTRY_URL/@dotenvx/dotenvx-$(os_arch)/-/dotenvx-$(os_arch)-$VERSION.tgz"
return 0
}
progress_bar() {
if $(is_ci); then
echo "--no-progress-meter"
else
echo "--progress-bar"
fi
return 0
}
binary_name() {
if $(is_windows); then
echo "dotenvx.exe"
else
echo "dotenvx"
fi
return 0
}
# which_* -------------------------------
which_curl() {
local result
result=$(command -v curl 2>/dev/null) # capture the output without displaying it on the screen
echo "$result"
return 0
}
which_dotenvx() {
local result
result=$(command -v dotenvx 2>/dev/null) # capture the output without displaying it on the screen
echo "$result"
return 0
}
# warnings* -----------------------------
warn_of_any_conflict() {
local dotenvx_path="$(which_dotenvx)"
if [ "$dotenvx_path" != "" ] && [ "$dotenvx_path" != "$(directory)/$(binary_name)" ]; then
echo "[DOTENVX_CONFLICT] conflicting dotenvx found at $dotenvx_path" >&2
echo "? we recommend updating your path to include $(directory)" >&2
fi
return 0
}
# help text -----------------------------
help_sudo_install_command() {
if is_piped; then
echo "curl -sfS $INSTALL_SCRIPT_URL | sudo $0"
else
echo "sudo $0"
fi
return 0
}
help_customize_directory_command() {
if is_piped; then
echo "curl -sfS $INSTALL_SCRIPT_URL | $0 -s -- --directory=."
else
echo "$0 --directory=."
fi
return 0
}
help_install_curl_command() {
if command -v apt-get >/dev/null 2>&1; then
echo "sudo apt-get update && sudo apt-get install -y curl"
elif command -v yum >/dev/null 2>&1; then
echo "sudo yum install -y curl"
elif command -v brew >/dev/null 2>&1; then
echo "brew install curl"
elif command -v pkg >/dev/null 2>&1; then
echo "sudo pkg install curl"
else
echo "install curl manually"
fi
return 0
}
# install/run ---------------------------
install_dotenvx() {
# 0. override version
VERSION="${1:-$VERSION}"
# 1. setup tmpdir
local tmpdir=$(command mktemp -d)
local pipe="$tmpdir/pipe"
mkfifo "$pipe"
install_failed_cleanup() {
echo "[INSTALLATION_FAILED] failed to download from registry [$(download_url)]"
echo "? verify the download url and try downloading manually"
rm -r "$tmpdir"
}
# TODO: handle dotenvx.exe when on a windows machine? binary is not package/dotenvx, it's package/dotenvx.exe
# Start curl in the background and redirect output to the pipe
curl $(progress_bar) --fail -L --proto '=https' "$(download_url)" > "$pipe" &
curl_pid=$!
# Start tar in the background to read from the pipe
sh -c "tar xz --directory $(directory) --strip-components=1 -f '$pipe' 'package/$(binary_name)'" &
tar_pid=$!
if ! wait $curl_pid || ! wait $tar_pid; then
install_failed_cleanup
return 1
fi
# 3. clean up
rm -r "$tmpdir"
# warn of any conflict
warn_of_any_conflict
# let user know
echo "[dotenvx@$VERSION] installed successfully ($(directory)/$(binary_name))"
return 0
}
run() {
# parse arguments
for arg in "$@"; do
case $arg in
version=* | --version=*)
VERSION="${arg#*=}"
;;
directory=* | --directory=*)
DIRECTORY="${arg#*=}"
;;
help | --help)
usage
return 0
;;
*)
# Unknown option
echo "Unknown option: $arg"
usage
return 1
;;
esac
done
# machine checks
is_version_valid
is_directory_writable
is_curl_installed
is_os_supported
is_arch_supported
# install logic
if [ -n "$VERSION" ]; then
# Check if the specified version is already installed
if is_installed "$VERSION"; then
echo "[dotenvx@$VERSION] already installed ($(directory)/$(binary_name))"
return 0
else
install_dotenvx "$VERSION"
fi
else
if is_installed; then
echo "[dotenvx@$VERSION] already installed ($(directory)/$(binary_name))"
return 0
else
install_dotenvx
fi
fi
}
if ! is_test_mode; then
run "$@"
exit $?
fi
# "thanks for using dotenvx!" - mot

View File

@@ -22,7 +22,8 @@
"scripts": {
"standard": "standard",
"standard:fix": "standard --fix",
"test": "tap run --show-full-coverage",
"test": "tap run --show-full-coverage && shellspec",
"shellspec": "bash shellspec",
"prerelease": "npm test",
"release": "standard-version"
},

473
spec/install_spec.sh Normal file
View File

@@ -0,0 +1,473 @@
export CI=1 # because we are running this on github actions and makes testing output easier
export TEST_MODE=1 # set to test mode in order to not run run() for test purposes
Describe 'install.sh'
Include install.sh
setup() {
VERSION="0.44.2"
DIRECTORY="./spec/tmp"
}
# remove the dotenvx binary before each test
cleanup() {
rm -f ./spec/tmp/dotenvx
}
mock_home() {
HOME="/home/testuser"
DIRECTORY="~/testdir"
}
mock_unwritable_directory() {
DIRECTORY="/usr/local/testing-installer" # requires root/sudo
}
mock_which_curl_empty() {
echo ""
return 0
}
mock_which_dotenvx_empty() {
echo ""
return 0
}
mock_which_dotenvx_path_different() {
echo "/different/path"
return 0
}
preinstall_dotenvx() {
# Run the actual install_dotenvx function to install the binary
install_dotenvx > /dev/null
}
BeforeEach 'setup'
BeforeEach 'cleanup'
AfterEach 'cleanup'
Describe 'default values'
It 'checks default VERSION'
When call echo "$VERSION"
The output should equal "0.44.2"
End
It 'checks default DIRECTORY'
When call echo "$DIRECTORY"
The output should equal "./spec/tmp"
End
End
Describe 'is_piped()'
It 'returns false'
When call is_piped
The status should equal 1
End
End
Describe 'help_sudo_install_command'
It 'returns sudo in front of command'
When call help_sudo_install_command
The status should equal 0
The output should equal "sudo $0"
End
Describe 'when is_piped is true'
is_piped() {
return 0
}
It "returns with curl example"
When call help_sudo_install_command
The status should equal 0
The output should equal "curl -sfS https://dotenvx.sh/install.sh | sudo $0"
End
End
End
Describe 'help_customize_directory_command'
It 'returns with --directory flag'
When call help_customize_directory_command
The status should equal 0
The output should equal "$0 --directory=."
End
Describe 'when is_piped is true'
is_piped() {
return 0
}
It "returns with curl example"
When call help_customize_directory_command
The status should equal 0
The output should equal "curl -sfS https://dotenvx.sh/install.sh | $0 -s -- --directory=."
End
End
End
Describe 'is_ci()'
It 'returns true'
When call is_ci
The status should equal 0
End
End
Describe 'progress_bar()'
It 'returns --no-progress-meter (because is_ci is true)'
When call progress_bar
The status should equal 0
The output should equal "--no-progress-meter"
End
Describe 'when is_ci is true'
mock_is_ci_false() {
CI=0
}
Before 'mock_is_ci_false'
It 'returns --progress-bar'
When call progress_bar
The status should equal 0
The output should equal "--progress-bar"
End
End
End
Describe 'is_test_mode()'
It 'returns true'
When call is_test_mode
The status should equal 0
End
Describe 'typical case when TEST_MODE is false'
mock_is_test_mode_false() {
TEST_MODE=0
}
Before 'mock_is_test_mode_false'
It 'returns false'
When call is_test_mode
The status should equal 1
End
End
End
Describe 'usage()'
It 'displays usage'
When call usage
The output should equal "Usage: $0 [options] [command]
install dotenvx a better dotenv
Options:
--directory directory to install dotenvx to (default: \"/usr/local/bin\")
--version version of dotenvx to install (default: \"0.44.2\")
Commands:
install install dotenvx
help display help"
End
End
Describe 'directory()'
It 'smartly returns directory as default INSTALL_DIR'
When call directory
The output should equal "./spec/tmp"
End
Describe 'when home directory'
Before 'mock_home'
It 'expands ~ to home directory'
When call directory
The output should equal "/home/testuser/testdir"
End
End
End
Describe 'is_version_valid()'
It 'is true (0)'
When call is_version_valid
The status should equal 0
End
Describe 'when VERSION blank'
mock_version_blank() {
VERSION=""
}
Before mock_version_blank
It 'is false'
When call is_version_valid
The status should equal 1
The output should equal "[INSTALLATION_FAILED] VERSION is blank in install.sh script
? set VERSION to valid semantic semver version and try again"
End
End
Describe 'when VERSION invalid'
mock_version_invalid() {
VERSION="22"
}
Before mock_version_invalid
It 'is false'
When call is_version_valid
The status should equal 1
The output should equal "[INSTALLATION_FAILED] VERSION is not a valid semantic version in install.sh script
? set VERSION to valid semantic semver version and try again"
End
End
End
Describe 'is_directory_writable()'
It 'is true (0)'
When call is_directory_writable
The status should equal 0
End
Describe 'when unwritable directory'
Before 'mock_unwritable_directory'
It 'is false (1) to /usr/local/testing-installer (typical case that /usr/local/testing-installer is not writable)'
When call is_directory_writable
The status should equal 1
The output should equal "[INSTALLATION_FAILED] the installation directory [/usr/local/testing-installer] is not writable by the current user
? run as root [sudo $0] or choose a writable directory like your current directory [$0 --directory=.]"
End
End
End
Describe 'is_curl_installed()'
It 'is true (0) (typical case that /usr/bin/curl is installed)'
When call is_curl_installed
The status should equal 0
End
Describe 'no curl'
which_curl() {
mock_which_curl_empty
}
It 'is false (1)'
When call is_curl_installed
The status should equal 1
The output should equal "[INSTALLATION_FAILED] curl is required and is not installed
? install curl [$(help_install_curl_command)] and try again"
End
End
End
Describe 'os()'
It 'returns current os lowercased'
When call os
The status should equal 0
The output should equal "$(uname -s | tr '[:upper:]' '[:lower:]')"
End
End
Describe 'arch()'
It 'returns current arch lowercased'
When call arch
The status should equal 0
The output should equal "$(uname -m | tr '[:upper:]' '[:lower:]')"
End
End
Describe 'is_os_supported()'
It 'returns true'
When call is_os_supported
The status should equal 0
End
End
Describe 'is_arch_supported()'
It 'returns true'
When call is_arch_supported
The status should equal 0
End
End
Describe 'os_arch()'
It 'returns the combined values'
When call os_arch
The status should equal 0
The output should equal "$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | tr '[:upper:]' '[:lower:]')"
End
End
Describe 'filename()'
It 'returns the combined values'
When call filename
The status should equal 0
The output should equal "dotenvx-0.44.2-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | tr '[:upper:]' '[:lower:]').tar.gz"
End
End
Describe 'download_url()'
It 'returns the combined values'
When call download_url
The status should equal 0
The output should equal "https://registry.npmjs.org/@dotenvx/dotenvx-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | tr '[:upper:]' '[:lower:]')/-/dotenvx-$(uname -s | tr '[:upper:]' '[:lower:]')-$(uname -m | tr '[:upper:]' '[:lower:]')-0.44.2.tgz"
End
End
Describe 'is_windows()'
It 'returns false'
When call is_windows
The status should equal 1
End
Describe 'when on windows os'
os() {
echo "windows"
return 0
}
It 'returns true'
When call is_windows
The status should equal 0
End
End
End
Describe 'is_installed()'
which_dotenvx() {
mock_which_dotenvx_empty
}
It 'returns false'
When call is_installed
The status should equal 1
End
Describe 'when already installed'
Before 'preinstall_dotenvx'
It 'returns true and outputs a message'
When call is_installed
The status should equal 0
The output should equal "[dotenvx@0.44.2] already installed (./spec/tmp/dotenvx)"
End
End
End
Describe 'which_dotenvx()'
which_dotenvx() {
mock_which_dotenvx_empty
}
It 'returns empty space'
When call which_dotenvx
The output should equal ""
End
Describe 'when a different path'
which_dotenvx() {
mock_which_dotenvx_path_different
}
It 'returns the different path'
When call which_dotenvx
The output should equal "/different/path"
End
End
End
Describe 'warn_of_any_conflict()'
which_dotenvx() {
mock_which_dotenvx_empty
}
It 'does not warn since which dotenvx is empty'
When call warn_of_any_conflict
The status should equal 0
The stderr should equal ""
The output should equal ""
End
Describe 'when a different path'
which_dotenvx() {
mock_which_dotenvx_path_different
}
It 'warns'
When call warn_of_any_conflict
The status should equal 0
The stderr should equal "[DOTENVX_CONFLICT] conflicting dotenvx found at /different/path
? we recommend updating your path to include ./spec/tmp"
End
End
End
Describe 'install_dotenvx()'
which_dotenvx() {
mock_which_dotenvx_empty
}
It 'installs it'
When call install_dotenvx
The status should equal 0
The output should equal "[dotenvx@0.44.2] installed successfully (./spec/tmp/dotenvx)"
End
Describe 'when a different path'
which_dotenvx() {
mock_which_dotenvx_path_different
}
It 'installs it but warns'
When call install_dotenvx
The status should equal 0
The output should equal "[dotenvx@0.44.2] installed successfully (./spec/tmp/dotenvx)"
The stderr should equal "[DOTENVX_CONFLICT] conflicting dotenvx found at /different/path
? we recommend updating your path to include ./spec/tmp"
End
End
End
Describe 'run()'
which_dotenvx() {
mock_which_dotenvx_empty
}
It 'installs dotenvx'
When call run
The status should equal 0
The output should equal "[dotenvx@0.44.2] installed successfully (./spec/tmp/dotenvx)"
End
Describe 'when a different path'
which_dotenvx() {
mock_which_dotenvx_path_different
}
It 'installs it but warns'
When call run
The status should equal 0
The output should equal "[dotenvx@0.44.2] installed successfully (./spec/tmp/dotenvx)"
The stderr should equal "[DOTENVX_CONFLICT] conflicting dotenvx found at /different/path
? we recommend updating your path to include ./spec/tmp"
End
End
Describe 'when already installed at same location'
Before 'preinstall_dotenvx'
It 'says already installed'
When call run
The status should equal 0
The output should equal "[dotenvx@0.44.2] already installed (./spec/tmp/dotenvx)"
End
End
End
End

24
spec/spec_helper.sh Normal file
View File

@@ -0,0 +1,24 @@
# shellcheck shell=sh
# Defining variables and functions here will affect all specfiles.
# Change shell options inside a function may cause different behavior,
# so it is better to set them here.
# set -eu
# This callback function will be invoked only once before loading specfiles.
spec_helper_precheck() {
# Available functions: info, warn, error, abort, setenv, unsetenv
# Available variables: VERSION, SHELL_TYPE, SHELL_VERSION
: minimum_version "0.28.1"
}
# This callback function will be invoked after a specfile has been loaded.
spec_helper_loaded() {
:
}
# This callback function will be invoked after core modules has been loaded.
spec_helper_configure() {
# Available functions: import, before_each, after_each, before_all, after_all
: import 'support/custom_matcher'
}