├── .ci ├── Dockerfile.ci ├── Dockerfile.ci.test ├── build-checksums.sh ├── cirrus-release.sh ├── install.sh.manifest.txt ├── libsh.full-minified.sh.manifest.txt ├── libsh.full.sh.manifest.txt ├── libsh.minimal-minified.sh.manifest.txt ├── libsh.minimal.sh.manifest.txt ├── libsh.sh.manifest.txt ├── update-changelog.sh └── update-gh-pages-install.sh ├── .cirrus.yml ├── .github └── bors.toml ├── .gitignore ├── .prettierrc.yml ├── API.md ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE-APACHE ├── LICENSE-MIT ├── Makefile ├── README.md ├── VERSION.txt ├── contrib ├── install.sh └── libsh-vendor.mk ├── distrib ├── libsh.full.sh └── libsh.minimal.sh ├── lib ├── _header ├── _ksh_local.sh ├── _shebang.sh ├── check_cmd.sh ├── cleanup_directory.sh ├── cleanup_file.sh ├── die.sh ├── download.sh ├── indent.sh ├── info.sh ├── info_end.sh ├── info_start.sh ├── mktemp_directory.sh ├── mktemp_file.sh ├── need_cmd.sh ├── print_version.sh ├── section.sh ├── setup_cleanup_directories.sh ├── setup_cleanup_files.sh ├── setup_cleanups.sh ├── setup_traps.sh ├── trap_cleanup_directories.sh ├── trap_cleanup_files.sh ├── trap_cleanups.sh └── warn.sh ├── support ├── compile-install.awk ├── compile.awk ├── minify.awk └── sources.awk ├── tests ├── check_cmd_test.sh ├── cleanup_directory_test.sh ├── cleanup_file_test.sh ├── die_test.sh ├── download_test.sh ├── indent_test.sh ├── info_end_test.sh ├── info_start_test.sh ├── info_test.sh ├── mktemp_directory_test.sh ├── mktemp_file_test.sh ├── need_cmd_test.sh ├── print_version_test.sh ├── section_test.sh ├── setup_cleanup_directories_test.sh ├── setup_cleanup_files_test.sh ├── setup_cleanups_test.sh ├── setup_traps_test.sh ├── test_helpers.sh ├── trap_cleanup_directories_test.sh ├── trap_cleanup_files_test.sh ├── trap_cleanups_test.sh └── warn_test.sh └── vendor └── mk ├── base.mk ├── release.mk └── shell.mk /.ci/Dockerfile.ci: -------------------------------------------------------------------------------- 1 | FROM alpine:3 2 | 3 | RUN apk add --no-cache \ 4 | curl \ 5 | git \ 6 | jo \ 7 | jq \ 8 | make 9 | -------------------------------------------------------------------------------- /.ci/Dockerfile.ci.test: -------------------------------------------------------------------------------- 1 | ARG VERSION=20190328 2 | 3 | FROM busybox:1.30.1 as bb30 4 | FROM busybox:1.23.2 as bb23 5 | 6 | FROM fidian/multishell:$VERSION 7 | 8 | ARG VERSION 9 | 10 | COPY --from=bb30 /bin/busybox /usr/local/bin/busybox-1.30.1 11 | COPY --from=bb23 /bin/busybox /usr/local/bin/busybox-1.23.2 12 | 13 | # hadolint ignore=DL3008 14 | RUN apt-get update -y \ 15 | && apt-get install --no-install-recommends -y \ 16 | busybox \ 17 | ca-certificates \ 18 | curl \ 19 | git \ 20 | make \ 21 | tar \ 22 | && apt-get clean \ 23 | && rm -rf /var/lib/lists/* \ 24 | && for v in 1.30.1 1.23.2; do \ 25 | for s in ash hush; do \ 26 | echo '#!/usr/bin/env sh' >"/usr/local/bin/busybox-${v}-${s}"; \ 27 | echo "exec /usr/local/bin/busybox-$v $s \"\$@\"" \ 28 | >>"/usr/local/bin/busybox-${v}-${s}"; \ 29 | chmod 0755 "/usr/local/bin/busybox-${v}-${s}"; \ 30 | done; \ 31 | done 32 | -------------------------------------------------------------------------------- /.ci/build-checksums.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | print_usage() { 5 | local program="$1" 6 | 7 | echo "$program 8 | 9 | Generates a checksum digests for a file 10 | 11 | USAGE: 12 | $program [FLAGS] [--] 13 | 14 | FLAGS: 15 | -h, --help Prints help information 16 | 17 | ARGS: 18 | An input file 19 | " | sed 's/^ \{1,4\}//g' 20 | } 21 | 22 | main() { 23 | set -eu 24 | if [ -n "${DEBUG:-}" ]; then set -v; fi 25 | if [ -n "${TRACE:-}" ]; then set -xv; fi 26 | 27 | local program 28 | program="$(basename "$0")" 29 | 30 | OPTIND=1 31 | while getopts "h-:" arg; do 32 | case "$arg" in 33 | h) 34 | print_usage "$program" 35 | return 0 36 | ;; 37 | -) 38 | case "$OPTARG" in 39 | help) 40 | print_usage "$program" 41 | return 0 42 | ;; 43 | '') 44 | # "--" terminates argument processing 45 | break 46 | ;; 47 | *) 48 | print_usage "$program" >&2 49 | die "invalid argument --$OPTARG" 50 | ;; 51 | esac 52 | ;; 53 | \?) 54 | print_usage "$program" >&2 55 | die "invalid argument; arg=-$OPTARG" 56 | ;; 57 | esac 58 | done 59 | shift "$((OPTIND - 1))" 60 | 61 | if [ -z "${1:-}" ]; then 62 | print_usage "$program" >&2 63 | die "missing argument" 64 | fi 65 | local file="$1" 66 | shift 67 | if [ ! -f "$file" ]; then 68 | print_usage "$program" >&2 69 | die "file '$file' not found" 70 | fi 71 | 72 | need_cmd basename 73 | need_cmd dirname 74 | 75 | local basename 76 | basename="$(basename "$file")" 77 | 78 | echo "--- Generating checksums for '$file'" 79 | cd "$(dirname "$file")" 80 | build_md5 "$basename" 81 | build_sha256 "$basename" 82 | } 83 | 84 | build_sha256() { 85 | local file="$1" 86 | 87 | need_cmd uname 88 | 89 | echo " - Generating SHA256 checksum digest" 90 | { 91 | case "$(uname -s)" in 92 | FreeBSD) 93 | need_cmd sed 94 | need_cmd sha256 95 | sha256 "$file" | sed -E 's/^.*\(([^)]+)\) = (.+)$/\2 \1/' 96 | ;; 97 | Linux) 98 | need_cmd sha256sum 99 | sha256sum "$file" 100 | ;; 101 | Darwin) 102 | need_cmd shasum 103 | shasum -a 256 "$file" 104 | ;; 105 | *) 106 | die "unsupported platform '$(uname -s)'" 107 | ;; 108 | esac 109 | } >"$file.sha256" 110 | } 111 | 112 | build_md5() { 113 | local file="$1" 114 | 115 | need_cmd uname 116 | 117 | echo " - Generating MD5 checksum digest" 118 | { 119 | case "$(uname -s)" in 120 | FreeBSD) 121 | need_cmd md5 122 | need_cmd sed 123 | md5 "$file" | sed -E 's/^.*\(([^)]+)\) = (.+)$/\2 \1/' 124 | ;; 125 | Linux) 126 | need_cmd md5sum 127 | md5sum "$file" 128 | ;; 129 | Darwin) 130 | need_cmd md5 131 | need_cmd sed 132 | md5 "$file" | sed -E 's/^.*\(([^)]+)\) = (.+)$/\2 \1/' 133 | ;; 134 | *) 135 | die "unsupported platform '$(uname -s)'" 136 | ;; 137 | esac 138 | } >"$file.md5" 139 | } 140 | 141 | die() { 142 | echo "" >&2 143 | echo "xxx $1" >&2 144 | echo "" >&2 145 | return 1 146 | } 147 | 148 | need_cmd() { 149 | if ! command -v "$1" >/dev/null 2>&1; then 150 | die "Required command '$1' not found on PATH" 151 | fi 152 | } 153 | 154 | main "$@" 155 | -------------------------------------------------------------------------------- /.ci/cirrus-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | main() { 5 | set -eu 6 | if [ -n "${DEBUG:-}" ]; then set -v; fi 7 | if [ -n "${TRACE:-}" ]; then set -xv; fi 8 | 9 | local subcmd 10 | subcmd="$1" 11 | shift 12 | 13 | "$subcmd" "$@" 14 | } 15 | 16 | changelog_section() { 17 | local changelog_file="$1" 18 | local tag="$2" 19 | local version="${tag#v}" 20 | 21 | need_cmd awk 22 | 23 | awk -v version_header_pat="^## \\\[$version\\\] - " ' 24 | BEGIN { 25 | version_section = 0 26 | urls_section = 0 27 | } 28 | 29 | # Start printing when the version section is found including the section 30 | # header 31 | $0 ~ version_header_pat { 32 | version_section = 1 33 | print 34 | next 35 | } 36 | # Stop printing when the next version section is found or when the urls 37 | # section is found 38 | version_section == 1 && (/^## \[/ || /^$/) { 39 | version_section = 0 40 | } 41 | # Print lines while in the version section 42 | version_section == 1 { 43 | print 44 | } 45 | # Start printing when the urls section is found, including the section 46 | # comment 47 | /^$/ { 48 | urls_section = 1 49 | print 50 | next 51 | } 52 | # Print lines while in the urls section 53 | urls_section == 1 { 54 | print 55 | } 56 | ' "$changelog_file" 57 | 58 | } 59 | 60 | ci_download() { 61 | local artifact="$1" 62 | shift 63 | 64 | need_cmd basename 65 | need_cmd curl 66 | 67 | if [ -z "${CIRRUS_BUILD_ID:-}" ]; then 68 | die "missing required environment variable: CIRRUS_BUILD_ID" 69 | fi 70 | 71 | local dest 72 | dest="$(basename "$artifact")" 73 | 74 | echo "--- Downlading Cirrus artifact '$artifact' to '$dest'" >&2 75 | 76 | curl \ 77 | --fail \ 78 | -X GET \ 79 | --output "$dest" \ 80 | "https://api.cirrus-ci.com/v1/artifact/build/$CIRRUS_BUILD_ID/$artifact" \ 81 | "${@:---}" 82 | } 83 | 84 | gh_create_release() { 85 | local repo="$1" 86 | local tag="$2" 87 | local name="$3" 88 | local body="$4" 89 | local draft="$5" 90 | local prerelease="$6" 91 | 92 | need_cmd jo 93 | need_cmd jq 94 | need_cmd sed 95 | 96 | local payload 97 | payload="$( 98 | jo \ 99 | tag_name="$tag" \ 100 | name="$name" \ 101 | body="$body" \ 102 | draft="$draft" \ 103 | prerelease="$prerelease" 104 | )" 105 | 106 | local response 107 | if ! response="$( 108 | gh_rest POST "/repos/$repo/releases" --data "$payload" 109 | )"; then 110 | echo "!!! Failed to create a release for tag $tag" >&2 111 | return 1 112 | fi 113 | 114 | echo "$response" | jq -r .upload_url | sed -E 's,\{.+\}$,,' 115 | } 116 | 117 | gh_create_version_release() { 118 | local repo="$1" 119 | local tag="$2" 120 | 121 | gh_delete_release "$repo" "$tag" 122 | 123 | local prerelease 124 | if echo "${tag#v}" | grep -q -E '^\d+\.\d+.\d+$'; then 125 | prerelease=false 126 | else 127 | prerelease=true 128 | fi 129 | 130 | echo "--- Creating GitHub *draft* release '$tag' for '$repo'" >&2 131 | 132 | gh_create_release \ 133 | "$repo" \ 134 | "$tag" \ 135 | "$tag" \ 136 | "Release ${tag#v}" \ 137 | true \ 138 | "$prerelease" 139 | } 140 | 141 | gh_delete_release() { 142 | local repo="$1" 143 | local tag="$2" 144 | 145 | local release_ids rid 146 | release_ids="$(gh_release_id_for_tag "$repo" "$tag" 2>/dev/null)" 147 | 148 | if [ -n "$release_ids" ]; then 149 | for rid in $release_ids; do 150 | echo "--- Deleting GitHub pre-existing release '$tag' ($rid)" >&2 151 | if ! gh_rest DELETE "/repos/$repo/releases/$rid" >/dev/null; then 152 | echo "!!! Failed to delete a pre-existing release '$tag' ($rid)" >&2 153 | return 1 154 | fi 155 | done 156 | fi 157 | } 158 | 159 | gh_download() { 160 | local repo="$1" 161 | shift 162 | local tag="$1" 163 | shift 164 | local asset="$1" 165 | shift 166 | 167 | need_cmd curl 168 | need_cmd jq 169 | 170 | if ! gh_rest GET "/repos/$repo/releases/tags/$tag" >/tmp/response; then 171 | echo "!!! Failed to find a release for tag $tag" >&2 172 | return 1 173 | fi 174 | 175 | local dl_url 176 | dl_url="$( 177 | jq -r ".assets[] | select(.name == \"$asset\") | .browser_download_url" \ 178 | &2 182 | 183 | curl \ 184 | --fail \ 185 | -X GET \ 186 | --location \ 187 | --output "$asset" \ 188 | "$dl_url" \ 189 | "${@:---}" 190 | } 191 | 192 | gh_publish_release() { 193 | local repo="$1" 194 | local tag="$2" 195 | local changelog_file="$3" 196 | 197 | need_cmd jo 198 | need_cmd jq 199 | 200 | local release_id 201 | release_id="$(gh_release_id_for_tag "$repo" "$tag")" 202 | 203 | local body 204 | if [ "$tag" = "nightly" ]; then 205 | body="" 206 | else 207 | local changelog_section 208 | changelog_section="$(changelog_section "$changelog_file" "$tag")" 209 | 210 | body="$changelog_section" 211 | fi 212 | 213 | local payload 214 | payload="$( 215 | jo \ 216 | draft=false \ 217 | name="Release ${tag#v}" \ 218 | body="$body" 219 | )" 220 | 221 | echo "--- Publishing GitHub release '$tag' for '$repo'" >&2 222 | 223 | local response 224 | if ! response="$( 225 | gh_rest POST "/repos/$repo/releases/$release_id" --data "$payload" 226 | )"; then 227 | echo "!!! Failed to update a release for tag $tag" >&2 228 | return 1 229 | fi 230 | } 231 | 232 | gh_release_id_for_tag() { 233 | local repo="$1" 234 | local tag="$2" 235 | 236 | need_cmd jq 237 | 238 | if ! gh_rest GET "/repos/$repo/releases" >/tmp/response; then 239 | echo "!!! Failed to find a release for tag $tag" >&2 240 | return 1 241 | fi 242 | 243 | jq ".[] | select(.tag_name == \"$tag\") | .id" /tmp/response; then 254 | echo "!!! Failed to find a release for tag $tag" >&2 255 | return 1 256 | fi 257 | 258 | jq -r .upload_url /dev/null 2>&1; then 302 | echo "--- Updating Git tag reference for '$tag'" >&2 303 | local payload 304 | payload="$( 305 | jo \ 306 | sha="$sha" \ 307 | force=true 308 | )" 309 | if ! gh_rest PATCH "/repos/$repo/git/refs/tags/$tag" --data "$payload" >/dev/null; then 310 | echo "!!! Failed to update Git tag reference for '$tag'" >&2 311 | return 1 312 | fi 313 | else 314 | echo "--- Creating Git tag reference for '$tag'" >&2 315 | local payload 316 | payload="$( 317 | jo \ 318 | sha="$sha" \ 319 | ref="refs/tags/$tag" 320 | )" 321 | if ! gh_rest POST "/repos/$repo/git/refs" --data "$payload" >/dev/null; then 322 | echo "!!! Failed to create Git tag reference for '$tag'" >&2 323 | return 1 324 | fi 325 | fi 326 | } 327 | 328 | gh_upload() { 329 | local url="$1" 330 | local artifact_file="$2" 331 | 332 | need_cmd basename 333 | 334 | if [ ! -f "$artifact_file" ]; then 335 | echo "!!! Artifact file '$artifact_file' not found, cannot upload" >&2 336 | return 1 337 | fi 338 | 339 | local artifact content_type 340 | artifact="$(basename "$artifact_file")" 341 | content_type="application/octet-stream" 342 | 343 | echo "--- Publishing artifact '$artifact' to $url" >&2 344 | 345 | gh_rest_raw POST "$url?name=$artifact" \ 346 | --header "Content-Type: $content_type" \ 347 | --data-binary "@$artifact_file" 348 | } 349 | 350 | gh_upload_all() { 351 | local url="$1" 352 | local dir="$2" 353 | 354 | find "$dir" -type f | while read -r artifact_file; do 355 | if ! gh_upload "$url" "$artifact_file"; then 356 | echo "!!! Failed to upload '$artifact_file'" >&2 357 | return 1 358 | fi 359 | done 360 | } 361 | 362 | die() { 363 | echo "" >&2 364 | echo "xxx $1" >&2 365 | echo "" >&2 366 | exit 1 367 | } 368 | 369 | need_cmd() { 370 | if ! command -v "$1" >/dev/null 2>&1; then 371 | die "Required command '$1' not found on PATH" 372 | fi 373 | } 374 | 375 | main "$@" 376 | -------------------------------------------------------------------------------- /.ci/install.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 install.sh 2 | freebsd-x86_64 install.sh 3 | linux-aarch64 install.sh 4 | linux-arm install.sh 5 | linux-i686 install.sh 6 | linux-x86_64 install.sh 7 | linuxgnu-i686 install.sh 8 | linuxgnu-x86_64 install.sh 9 | -------------------------------------------------------------------------------- /.ci/libsh.full-minified.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 libsh.full-minified.sh 2 | freebsd-x86_64 libsh.full-minified.sh 3 | linux-aarch64 libsh.full-minified.sh 4 | linux-arm libsh.full-minified.sh 5 | linux-i686 libsh.full-minified.sh 6 | linux-x86_64 libsh.full-minified.sh 7 | linuxgnu-i686 libsh.full-minified.sh 8 | linuxgnu-x86_64 libsh.full-minified.sh 9 | -------------------------------------------------------------------------------- /.ci/libsh.full.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 libsh.full.sh 2 | freebsd-x86_64 libsh.full.sh 3 | linux-aarch64 libsh.full.sh 4 | linux-arm libsh.full.sh 5 | linux-i686 libsh.full.sh 6 | linux-x86_64 libsh.full.sh 7 | linuxgnu-i686 libsh.full.sh 8 | linuxgnu-x86_64 libsh.full.sh 9 | -------------------------------------------------------------------------------- /.ci/libsh.minimal-minified.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 libsh.minimal-minified.sh 2 | freebsd-x86_64 libsh.minimal-minified.sh 3 | linux-aarch64 libsh.minimal-minified.sh 4 | linux-arm libsh.minimal-minified.sh 5 | linux-i686 libsh.minimal-minified.sh 6 | linux-x86_64 libsh.minimal-minified.sh 7 | linuxgnu-i686 libsh.minimal-minified.sh 8 | linuxgnu-x86_64 libsh.minimal-minified.sh 9 | -------------------------------------------------------------------------------- /.ci/libsh.minimal.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 libsh.minimal.sh 2 | freebsd-x86_64 libsh.minimal.sh 3 | linux-aarch64 libsh.minimal.sh 4 | linux-arm libsh.minimal.sh 5 | linux-i686 libsh.minimal.sh 6 | linux-x86_64 libsh.minimal.sh 7 | linuxgnu-i686 libsh.minimal.sh 8 | linuxgnu-x86_64 libsh.minimal.sh 9 | -------------------------------------------------------------------------------- /.ci/libsh.sh.manifest.txt: -------------------------------------------------------------------------------- 1 | darwin-x86_64 libsh.sh 2 | freebsd-x86_64 libsh.sh 3 | linux-aarch64 libsh.sh 4 | linux-arm libsh.sh 5 | linux-i686 libsh.sh 6 | linux-x86_64 libsh.sh 7 | linuxgnu-i686 libsh.sh 8 | linuxgnu-x86_64 libsh.sh 9 | -------------------------------------------------------------------------------- /.ci/update-changelog.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | print_usage() { 5 | local program="$1" 6 | 7 | echo "$program 8 | 9 | Updates a CHANGELOG.md for a new release 10 | 11 | USAGE: 12 | $program [FLAGS] [--] 13 | 14 | FLAGS: 15 | -h, --help Prints help information 16 | 17 | ARGS: 18 | GitHub repository url [ex: https://github.com/fnichol/libsh] 19 | Version for the new release [ex: 1.2.0] 20 | Tag name for the new release [ex: v1.2.0] 21 | Path to a Markdown CHANGELOG [default: CHANGELOG.md] 22 | " | sed 's/^ \{1,4\}//g' 23 | } 24 | 25 | main() { 26 | set -eu 27 | if [ -n "${DEBUG:-}" ]; then set -v; fi 28 | if [ -n "${TRACE:-}" ]; then set -xv; fi 29 | 30 | local program 31 | program="$(basename "$0")" 32 | 33 | local changelog=CHANGELOG.md 34 | 35 | OPTIND=1 36 | while getopts "h-:" arg; do 37 | case "$arg" in 38 | h) 39 | print_usage "$program" 40 | return 0 41 | ;; 42 | -) 43 | case "$OPTARG" in 44 | help) 45 | print_usage "$program" 46 | return 0 47 | ;; 48 | '') 49 | # "--" terminates argument processing 50 | break 51 | ;; 52 | *) 53 | print_usage "$program" >&2 54 | die "invalid argument --$OPTARG" 55 | ;; 56 | esac 57 | ;; 58 | \?) 59 | print_usage "$program" >&2 60 | die "invalid argument; arg=-$OPTARG" 61 | ;; 62 | esac 63 | done 64 | shift "$((OPTIND - 1))" 65 | 66 | if [ -z "${1:-}" ]; then 67 | print_usage "$program" >&2 68 | die "missing argument" 69 | fi 70 | local repo="$1" 71 | shift 72 | if [ -z "${1:-}" ]; then 73 | print_usage "$program" >&2 74 | die "missing argument" 75 | fi 76 | local version="$1" 77 | shift 78 | if [ -z "${1:-}" ]; then 79 | print_usage "$program" >&2 80 | die "missing argument" 81 | fi 82 | local tag_name="$1" 83 | shift 84 | if [ -n "${1:-}" ]; then 85 | changelog="$1" 86 | shift 87 | fi 88 | if [ ! -f "$changelog" ]; then 89 | print_usage "$program" >&2 90 | die "changelog '$changelog' not found" 91 | fi 92 | 93 | update_changelog "$repo" "$version" "$tag_name" "$changelog" 94 | } 95 | 96 | update_changelog() { 97 | local repo="$1" 98 | local version="$2" 99 | local tag_name="$3" 100 | local changelog="$4" 101 | 102 | need_cmd date 103 | need_cmd rm 104 | need_cmd sed 105 | 106 | local date 107 | date="$(date -u +%F)" 108 | local nl=' 109 | ' 110 | 111 | sed -i.bak -E \ 112 | -e 's,[Uu]nreleased,'"${version}"',g' \ 113 | -e 's,\.\.\.HEAD,...'"${tag_name}"',g' \ 114 | -e 's,ReleaseDate,'"${date}"',g' \ 115 | -e 's,(),\1'"\\${nl}\\${nl}"'## [Unreleased] - ReleaseDate,g' \ 116 | -e 's,(),\1'"\\${nl}\\${nl}"'[unreleased]: '"${repo}"'/compare/'"${tag_name}"'...HEAD,g' \ 117 | "$changelog" 118 | rm -f "$changelog.bak" 119 | } 120 | 121 | die() { 122 | echo "" >&2 123 | echo "xxx $1" >&2 124 | echo "" >&2 125 | return 1 126 | } 127 | 128 | need_cmd() { 129 | if ! command -v "$1" >/dev/null 2>&1; then 130 | die "Required command '$1' not found on PATH" 131 | fi 132 | } 133 | 134 | main "$@" 135 | -------------------------------------------------------------------------------- /.ci/update-gh-pages-install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | print_usage() { 5 | local program="$1" 6 | 7 | echo "$program 8 | 9 | Updates a vendored install.sh in a repo's GitHub pages Git branch 10 | 11 | USAGE: 12 | $program [FLAGS] [--] [] 13 | 14 | FLAGS: 15 | -h, --help Prints help information 16 | 17 | ARGS: 18 | Path to install script 19 | GitHub repository [default: fnichol/libsh] 20 | " | sed 's/^ \{1,4\}//g' 21 | } 22 | 23 | main() { 24 | set -eu 25 | if [ -n "${DEBUG:-}" ]; then set -v; fi 26 | if [ -n "${TRACE:-}" ]; then set -xv; fi 27 | 28 | local program 29 | program="$(basename "$0")" 30 | 31 | . "lib/cleanup_directory.sh" 32 | . "lib/die.sh" 33 | . "lib/mktemp_directory.sh" 34 | . "lib/need_cmd.sh" 35 | 36 | local repo git_name git_email 37 | repo=fnichol/libsh 38 | git_name="Fletcher Nichol" 39 | git_email="fnichol@nichol.ca" 40 | 41 | OPTIND=1 42 | while getopts "h-:" arg; do 43 | case "$arg" in 44 | h) 45 | print_usage "$program" 46 | return 0 47 | ;; 48 | -) 49 | case "$OPTARG" in 50 | help) 51 | print_usage "$program" 52 | return 0 53 | ;; 54 | '') 55 | # "--" terminates argument processing 56 | break 57 | ;; 58 | *) 59 | print_usage "$program" >&2 60 | die "invalid argument --$OPTARG" 61 | ;; 62 | esac 63 | ;; 64 | \?) 65 | print_usage "$program" >&2 66 | die "invalid argument; arg=-$OPTARG" 67 | ;; 68 | esac 69 | done 70 | shift "$((OPTIND - 1))" 71 | 72 | if [ -z "${1:-}" ]; then 73 | print_usage "$program" >&2 74 | die "missing argument" 75 | fi 76 | local install_script="$1" 77 | shift 78 | if [ ! -f "$install_script" ]; then 79 | print_usage "$program" >&2 80 | die "install script '$install_script' not found" 81 | fi 82 | if [ -n "${1:-}" ]; then 83 | repo="$1" 84 | shift 85 | fi 86 | 87 | if [ -z "${GITHUB_TOKEN:-}" ]; then 88 | die "missing required environment variable: GITHUB_TOKEN" 89 | fi 90 | 91 | need_cmd chmod 92 | need_cmd cp 93 | need_cmd git 94 | 95 | local workdir 96 | workdir="$(mktemp_directory)" 97 | cleanup_directory "$workdir" 98 | 99 | cp "$install_script" "$workdir/install.sh" 100 | chmod 755 "$workdir/install.sh" 101 | 102 | git clone \ 103 | "https://${GITHUB_TOKEN}:x-oauth-basic@github.com/$repo.git" "$workdir/repo" 104 | cd "$workdir/repo" 105 | git config --local user.name "$git_name" 106 | git config --local user.email "$git_email" 107 | git checkout gh-pages 108 | mv ../install.sh install.sh 109 | git add install.sh 110 | git commit --message="chore: update released install.sh [ci skip]" 111 | git push origin gh-pages 112 | } 113 | 114 | main "$@" 115 | -------------------------------------------------------------------------------- /.cirrus.yml: -------------------------------------------------------------------------------- 1 | --- 2 | check_task: 3 | name: check 4 | only_if: 5 | $CIRRUS_BRANCH !=~ ".*\.tmp" && $CIRRUS_BRANCH != $CIRRUS_DEFAULT_BRANCH 6 | container: 7 | image: fnichol/check-shell:latest 8 | check_script: make check 9 | 10 | test_task: 11 | name: test-${PLATFORM}-${SHELL_BIN} 12 | alias: tests 13 | only_if: 14 | $CIRRUS_BRANCH !=~ ".*\.tmp" && $CIRRUS_BRANCH != $CIRRUS_DEFAULT_BRANCH 15 | matrix: 16 | - env: 17 | PLATFORM: linux 18 | matrix: 19 | - SHELL_BIN: apple-bash-23 20 | - SHELL_BIN: apple-bash-44 21 | - SHELL_BIN: apple-bash-76 22 | - SHELL_BIN: apple-bash-94 23 | - SHELL_BIN: apple-bash-106.220 24 | - SHELL_BIN: bash-2 25 | - SHELL_BIN: bash-3 26 | - SHELL_BIN: bash-3.00 27 | - SHELL_BIN: bash-4 28 | - SHELL_BIN: bash-5 29 | - SHELL_BIN: busybox-1.23.2-ash 30 | - SHELL_BIN: busybox-1.30.1-ash 31 | - SHELL_BIN: dash-0 32 | - SHELL_BIN: dash-0.5.10.2 33 | - SHELL_BIN: ksh93-2008-07-25 34 | LANG: C 35 | - SHELL_BIN: ksh93-2012-08-01 36 | # - SHELL_BIN: zsh-3 37 | # - SHELL_BIN: zsh-4 38 | - SHELL_BIN: zsh-5 39 | container: 40 | dockerfile: .ci/Dockerfile.ci.test 41 | test_script: make test 42 | - env: 43 | PLATFORM: macos 44 | matrix: 45 | - SHELL_BIN: bash-3 46 | - SHELL_BIN: bash-5 47 | PKG: bash 48 | - SHELL_BIN: dash 49 | PKG: dash 50 | - SHELL_BIN: sh 51 | - SHELL_BIN: ksh 52 | - SHELL_BIN: zsh 53 | macos_instance: 54 | image: catalina-base 55 | setup_script: | 56 | if [ -n "${PKG:-}" ]; then brew install "$PKG"; fi 57 | if [ "$SHELL_BIN" = "bash-3" ]; then 58 | ln -snvf /bin/bash /usr/local/bin/bash-3 59 | fi 60 | if [ "$SHELL_BIN" = "bash-5" ]; then 61 | ln -snvf /usr/local/bin/bash /usr/local/bin/bash-5 62 | fi 63 | test_script: make test 64 | - env: 65 | PLATFORM: freebsd 66 | matrix: 67 | - SHELL_BIN: bash 68 | PKG: shells/bash 69 | - SHELL_BIN: dash 70 | PKG: shells/dash 71 | # - SHELL_BIN: jsh 72 | # PKG: shells/heirloom-sh 73 | - SHELL_BIN: ksh 74 | PKG: shells/pdksh 75 | - SHELL_BIN: ksh93 76 | PKG: shells/ksh93 77 | # - SHELL_BIN: mksh 78 | # PKG: shells/mksh 79 | - SHELL_BIN: oksh 80 | PKG: shells/oksh 81 | - SHELL_BIN: sh 82 | - SHELL_BIN: zsh 83 | PKG: shells/zsh 84 | freebsd_instance: 85 | image_family: freebsd-12-2 86 | setup_script: | 87 | pkg install --yes devel/git devel/gmake textproc/gsed 88 | if [ -n "${PKG:-}" ]; then pkg install --yes "$PKG"; fi 89 | test_script: gmake test 90 | 91 | build_libs_task: 92 | name: build-libs 93 | only_if: 94 | $CIRRUS_TAG != '' || $CIRRUS_BRANCH == 'staging' || $CIRRUS_BRANCH == 95 | 'trying' 96 | depends_on: 97 | - check 98 | - tests 99 | container: 100 | dockerfile: .ci/Dockerfile.ci 101 | build_script: | 102 | if [ "${CIRRUS_TAG:-}" = "nightly" ]; then 103 | export NIGHTLY_BUILD="$(date -u +%F)" 104 | fi 105 | make build 106 | checksums_script: | 107 | cd build 108 | distribs="$(ls *.sh)" 109 | for d in $distribs; do ../.ci/build-checksums.sh "$d"; done 110 | libraries_artifacts: 111 | path: "build/*" 112 | 113 | ci_finished_task: 114 | name: ci-finished 115 | depends_on: 116 | - check 117 | - tests 118 | - build-libs 119 | container: 120 | dockerfile: .ci/Dockerfile.ci 121 | clone_script: mkdir -p "$CIRRUS_WORKING_DIR" 122 | success_script: /bin/true 123 | 124 | create_github_release_task: 125 | name: create-github-release 126 | only_if: $CIRRUS_TAG != '' 127 | depends_on: 128 | - build-libs 129 | container: 130 | dockerfile: .ci/Dockerfile.ci 131 | env: 132 | GITHUB_TOKEN: ENCRYPTED[87d044b1e0dfc41f65334518692837ee920a6bcf614f7845103ce64158da8126be6410a002396029705a2a5cf46b2e19] 133 | create_github_release_script: | 134 | if ! upload_url="$( 135 | ./.ci/cirrus-release.sh gh_create_version_release \ 136 | "$CIRRUS_REPO_FULL_NAME" \ 137 | "$CIRRUS_TAG" 138 | )"; then 139 | echo "xxx Failed to create release" >&2 140 | exit 1 141 | fi 142 | echo "$upload_url" > /tmp/upload_url 143 | download_cirrus_artifacts_script: | 144 | cr="$(readlink -f ./.ci/cirrus-release.sh)" 145 | manifests="$(cd .ci && ls -1 *.manifest.txt)" 146 | for m in $manifests; do 147 | manifest="$(readlink -f ".ci/$m")" 148 | mkdir -p /tmp/release 149 | cd /tmp/release 150 | awk '{ print $2 }' "$manifest" | sort | uniq | while read -r a; do 151 | "$cr" ci_download "build-libs/libraries/build/$a" 152 | "$cr" ci_download "build-libs/libraries/build/$a.md5" 153 | "$cr" ci_download "build-libs/libraries/build/$a.sha256" 154 | done 155 | cp "$manifest" . 156 | cd - 157 | done 158 | ls -l /tmp/release/* 159 | upload_github_release_artifacts_script: | 160 | url="$(cat /tmp/upload_url)" 161 | ./.ci/cirrus-release.sh gh_upload_all "$url" /tmp/release 162 | 163 | publish_github_release_task: 164 | name: publish-github-release 165 | only_if: $CIRRUS_TAG != '' 166 | depends_on: 167 | - create-github-release 168 | container: 169 | dockerfile: .ci/Dockerfile.ci 170 | env: 171 | GITHUB_TOKEN: ENCRYPTED[87d044b1e0dfc41f65334518692837ee920a6bcf614f7845103ce64158da8126be6410a002396029705a2a5cf46b2e19] 172 | publish_release_script: | 173 | ./.ci/cirrus-release.sh gh_publish_release \ 174 | "$CIRRUS_REPO_FULL_NAME" "$CIRRUS_TAG" CHANGELOG.md 175 | 176 | publish_install_sh_task: 177 | name: publish-install-sh 178 | only_if: $CIRRUS_TAG =~ 'v.*' 179 | depends_on: 180 | - publish-github-release 181 | container: 182 | dockerfile: .ci/Dockerfile.ci 183 | env: 184 | GITHUB_TOKEN: ENCRYPTED[87d044b1e0dfc41f65334518692837ee920a6bcf614f7845103ce64158da8126be6410a002396029705a2a5cf46b2e19] 185 | publish_install_sh_script: | 186 | ./.ci/cirrus-release.sh gh_download \ 187 | "$CIRRUS_REPO_FULL_NAME" "$CIRRUS_TAG" install.sh 188 | ./.ci/update-gh-pages-install.sh install.sh 189 | 190 | release_finished_task: 191 | name: release-finished 192 | only_if: $CIRRUS_TAG != '' 193 | depends_on: 194 | - create-github-release 195 | - publish-github-release 196 | - publish-install-sh 197 | container: 198 | dockerfile: .ci/Dockerfile.ci 199 | clone_script: mkdir -p "$CIRRUS_WORKING_DIR" 200 | success_script: /bin/true 201 | 202 | trigger_nightly_release_task: 203 | name: trigger-nightly-release 204 | only_if: $CIRRUS_CRON == 'nightly' 205 | container: 206 | image: alpine:3 207 | env: 208 | GITHUB_TOKEN: ENCRYPTED[87d044b1e0dfc41f65334518692837ee920a6bcf614f7845103ce64158da8126be6410a002396029705a2a5cf46b2e19] 209 | install_dependencies_script: apk add curl git jo jq 210 | trigger_release_script: 211 | ./.ci/cirrus-release.sh gh_update_tag "$CIRRUS_REPO_FULL_NAME" nightly 212 | -------------------------------------------------------------------------------- /.github/bors.toml: -------------------------------------------------------------------------------- 1 | status = ["ci-finished"] 2 | commit_title = "merge: ${PR_REFS}" 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /tmp/ 2 | /build/ 3 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | proseWrap: always 2 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | # libsh Full API 2 | 3 |
4 | Table of Contents 5 | 6 | 7 | 8 | - [check_cmd](#check_cmd) 9 | - [Environment Variables](#environment-variables) 10 | - [Examples](#examples) 11 | - [cleanup_directory](#cleanup_directory) 12 | - [Global Variables](#global-variables) 13 | - [Examples](#examples-1) 14 | - [cleanup_file](#cleanup_file) 15 | - [Global Variables](#global-variables-1) 16 | - [Examples](#examples-2) 17 | - [die](#die) 18 | - [Environment Variables](#environment-variables-1) 19 | - [Notes](#notes) 20 | - [Examples](#examples-3) 21 | - [download](#download) 22 | - [Notes](#notes-1) 23 | - [Examples](#examples-4) 24 | - [indent](#indent) 25 | - [Notes](#notes-2) 26 | - [Examples](#examples-5) 27 | - [info_end](#info_end) 28 | - [Environment Variables](#environment-variables-2) 29 | - [Examples](#examples-6) 30 | - [info_start](#info_start) 31 | - [Environment Variables](#environment-variables-3) 32 | - [Examples](#examples-7) 33 | - [info](#info) 34 | - [Environment Variables](#environment-variables-4) 35 | - [Examples](#examples-8) 36 | - [mktemp_directory](#mktemp_directory) 37 | - [Examples](#examples-9) 38 | - [mktemp_file](#mktemp_file) 39 | - [Examples](#examples-10) 40 | - [need_cmd](#need_cmd) 41 | - [Environment Variables](#environment-variables-5) 42 | - [Notes](#notes-3) 43 | - [Examples](#examples-11) 44 | - [print_version](#print_version) 45 | - [Examples](#examples-12) 46 | - [section](#section) 47 | - [Environment Variables](#environment-variables-6) 48 | - [Examples](#examples-13) 49 | - [setup_cleanup_directories](#setup_cleanup_directories) 50 | - [Global Variables](#global-variables-2) 51 | - [Examples](#examples-14) 52 | - [setup_cleanup_files](#setup_cleanup_files) 53 | - [Global Variables](#global-variables-3) 54 | - [Examples](#examples-15) 55 | - [setup_cleanups](#setup_cleanups) 56 | - [Examples](#examples-16) 57 | - [setup_traps](#setup_traps) 58 | - [Examples](#examples-17) 59 | - [trap_cleanup_directories](#trap_cleanup_directories) 60 | - [Global Variables](#global-variables-4) 61 | - [Examples](#examples-18) 62 | - [trap_cleanup_files](#trap_cleanup_files) 63 | - [Global Variables](#global-variables-5) 64 | - [Examples](#examples-19) 65 | - [trap_cleanups](#trap_cleanups) 66 | - [Examples](#examples-20) 67 | - [warn](#warn) 68 | - [Environment Variables](#environment-variables-7) 69 | - [Examples](#examples-21) 70 | 71 | 72 | 73 |
74 | 75 | ## check_cmd 76 | 77 | Determines whether or not a program is available on the system PATH. 78 | 79 | - `@param [String]` program name 80 | - `@return 0` if program is found on system PATH 81 | - `@return 1` if program is not found 82 | 83 | ### Environment Variables 84 | 85 | - `PATH` indirectly used to search for the program 86 | 87 | ### Examples 88 | 89 | Basic usage, when used as a conditional check: 90 | 91 | ```sh 92 | if check_cmd git; then 93 | echo "Found Git" 94 | fi 95 | ``` 96 | 97 | ## cleanup_directory 98 | 99 | Tracks a directory for later cleanup in a trap handler. 100 | 101 | This function can be called immediately after a temp directory is created, 102 | before a directory is created, or long after a directory exists. When used in 103 | combination with [`trap_cleanup_directories`], all directories registered by 104 | calling `cleanup_directory` will be removed if they exist when 105 | `trap_cleanup_directories` is invoked. 106 | 107 | - `@param [String]` path to directory 108 | - `@return 0` if successful 109 | - `@return 1` if a temp file could not be created 110 | 111 | [`trap_cleanup_directories`]: #trap_cleanup_directories 112 | 113 | ### Global Variables 114 | 115 | - `__CLEANUP_DIRECTORIES__` used to track the collection of directories to clean 116 | up whose value is a file. If not declared or set, this function will set it 117 | up. 118 | 119 | ### Examples 120 | 121 | Basic usage: 122 | 123 | ```sh 124 | dir="$(mktemp_directory)" 125 | cleanup_directory "$dir" 126 | # do work on directory, etc. 127 | ``` 128 | 129 | ## cleanup_file 130 | 131 | Tracks a file for later cleanup in a trap handler. 132 | 133 | This function can be called immediately after a temp file is created, before a 134 | file is created, or long after a file exists. When used in combination with 135 | [`trap_cleanup_files`], all files registered by calling `cleanup_file` will be 136 | removed if they exist when `trap_cleanup_files` is invoked. 137 | 138 | - `@param [String]` path to file 139 | - `@return 0` if successful 140 | - `@return 1` if a temp file could not be created 141 | 142 | [`trap_cleanup_files`]: #trap_cleanup_files 143 | 144 | ### Global Variables 145 | 146 | - `__CLEANUP_FILES__` used to track the collection of files to clean up whose 147 | value is a file. If not declared or set, this function will set it up. 148 | 149 | ### Examples 150 | 151 | Basic usage: 152 | 153 | ```sh 154 | file="$(mktemp_file)" 155 | cleanup_file "$file" 156 | # do work on file, etc. 157 | ``` 158 | 159 | ## die 160 | 161 | Prints an error message to standard error and exits with a non-zero exit code. 162 | 163 | - `@param [String]` warning text 164 | - `@stderr` warning text message 165 | 166 | ### Environment Variables 167 | 168 | - `TERM` used to determine whether or not the terminal is capable of printing 169 | colored output. 170 | 171 | ### Notes 172 | 173 | This function calls `exit` and will **not** return. 174 | 175 | ### Examples 176 | 177 | Basic usage: 178 | 179 | ```sh 180 | die "No program to download tarball" 181 | ``` 182 | 183 | ## download 184 | 185 | Downloads the contents at the given URL to the given local file. 186 | 187 | This implementation attempts to use the `curl` program with a fallback to the 188 | `wget` program and a final fallback to the `ftp` program. The first download 189 | program to succeed is used and if all fail, this function returns a non-zero 190 | code. 191 | 192 | - `@param [String]` download URL 193 | - `@param [String]` destination file 194 | - `@return 0` if a download was successful 195 | - `@return 1` if a download was not successful 196 | 197 | ### Notes 198 | 199 | At least one of `curl`, `wget`, or `ftp` must be compiled with SSL/TLS support 200 | to be able to download from `https` sources. 201 | 202 | ### Examples 203 | 204 | Basic usage: 205 | 206 | ```sh 207 | download http://example.com/file.txt /tmp/file.txt 208 | ``` 209 | 210 | ## indent 211 | 212 | Indents the output from a command while preserving the command's exit code. 213 | 214 | In minimal/POSIX shells there is no support for `set -o pipefail` which means 215 | that the exit code of the first command in a shell pipeline won't be addressable 216 | in an easy way. This implementation uses a temp file to ferry the original 217 | command's exit code from a subshell back into the main function. The output can 218 | be aligned with a pipe to `sed` as before but now we have an implementation 219 | which mimics a `set -o pipefail` which should work on all Bourne shells. Note 220 | that the `set -o errexit` is disabled during the command's invocation so that 221 | its exit code can be captured. 222 | 223 | Based on implementation from 224 | [Stack Overflow](https://stackoverflow.com/a/54931544) 225 | 226 | - `@param [String[]]` command and arguments 227 | - `@return` the exit code of the command which was executed 228 | 229 | ### Notes 230 | 231 | In order to preserve the output order of the command, the `stdout` and `stderr` 232 | streams are combined, so the command will not emit its `stderr` output to the 233 | caller's `stderr` stream. 234 | 235 | ### Examples 236 | 237 | Basic usage: 238 | 239 | ```sh 240 | indent cat /my/file 241 | ``` 242 | 243 | ## info_end 244 | 245 | Completes printing an informational, detailed step to standard out which has no 246 | output, started with [`info_start`]. 247 | 248 | - `@stdout` informational heading text 249 | - `@return 0` if successful 250 | 251 | [`info_start`]: #info_start 252 | 253 | ### Environment Variables 254 | 255 | - `TERM` used to determine whether or not the terminal is capable of printing 256 | colored output. 257 | 258 | ### Examples 259 | 260 | Basic usage: 261 | 262 | ```sh 263 | info_end 264 | ``` 265 | 266 | ## info_start 267 | 268 | Prints an informational, detailed step to standard out which has no output. 269 | 270 | - `@param [String]` informational text 271 | - `@stdout` informational heading text 272 | - `@return 0` if successful 273 | 274 | ### Environment Variables 275 | 276 | - `TERM` used to determine whether or not the terminal is capable of printing 277 | colored output. 278 | 279 | ### Examples 280 | 281 | Basic usage: 282 | 283 | ```sh 284 | info_start "Copying file" 285 | ``` 286 | 287 | ## info 288 | 289 | Prints an informational, detailed step to standard out. 290 | 291 | - `@param [String]` informational text 292 | - `@stdout` informational heading text 293 | - `@return 0` if successful 294 | 295 | ### Environment Variables 296 | 297 | - `TERM` used to determine whether or not the terminal is capable of printing 298 | colored output. 299 | 300 | ### Examples 301 | 302 | Basic usage: 303 | 304 | ```sh 305 | info "Downloading tarball" 306 | ``` 307 | 308 | ## mktemp_directory 309 | 310 | Creates a temporary directory and prints the name to standard output. 311 | 312 | Most system use the first no-argument version, however Mac OS X 10.10 (Yosemite) 313 | and older don't allow the no-argument version, hence the second fallback 314 | version. 315 | 316 | All tested invocations will create a file in each platform's suitable temporary 317 | directory. 318 | 319 | - `@param [optional, String]` parent directory 320 | - `@stdout` path to temporary directory 321 | - `@return 0` if successful 322 | 323 | ### Examples 324 | 325 | Basic usage: 326 | 327 | ```sh 328 | dir="$(mktemp_directory)" 329 | # use directory 330 | ``` 331 | 332 | With a custom parent directory: 333 | 334 | ```sh 335 | dir="$(mktemp_directory $HOME)" 336 | # use directory 337 | ``` 338 | 339 | ## mktemp_file 340 | 341 | Creates a temporary file and prints the name to standard output. 342 | 343 | Most systems use the first no-argument version, however Mac OS X 10.10 344 | (Yosemite) and older don't allow the no-argument version, hence the second 345 | fallback version. 346 | 347 | All tested invocations will create a file in each platform's suitable temporary 348 | directory. 349 | 350 | - `@param [optional, String]` parent directory 351 | - `@stdout` path to temporary file 352 | - `@return 0` if successful 353 | 354 | ### Examples 355 | 356 | Basic usage: 357 | 358 | ```sh 359 | file="$(mktemp_file)" 360 | # use file 361 | ``` 362 | 363 | With a custom parent directory: 364 | 365 | ```sh 366 | dir="$(mktemp_file "$HOME")" 367 | # use file 368 | ``` 369 | 370 | ## need_cmd 371 | 372 | Prints an error message and exits with a non-zero code if the program is not 373 | available on the system PATH. 374 | 375 | - `@param [String]` program name 376 | - `@stderr` a warning message is printed if program cannot be found 377 | 378 | ### Environment Variables 379 | 380 | - `PATH` indirectly used to search for the program 381 | 382 | ### Notes 383 | 384 | If the program is not found, this function calls `exit` and will **not** return. 385 | 386 | ### Examples 387 | 388 | Basic usage, when used as a guard or prerequisite in a function: 389 | 390 | ```sh 391 | need_cmd git 392 | ``` 393 | 394 | ## print_version 395 | 396 | Prints program version information to standard out. 397 | 398 | The minimal implementation will output the program name and version, separated 399 | with a space, such as `my-program 1.2.3`. However, if the Git program is 400 | detected and the current working directory is under a Git repository, then more 401 | information will be displayed. Namely, the short Git SHA and author commit date 402 | will be appended in parenthesis at end of the line. For example, 403 | `my-program 1.2.3 (abc123 2000-01-02)`. Alternatively, if the Git commit 404 | information is known ahead of time, it can be provided via optional arguments. 405 | 406 | If verbose mode is enable by setting the optional third argument to a `true`, 407 | then a detailed version report will be appended to the single line "simple 408 | mode". Assuming that the Git program is available and the current working 409 | directory is under a Git repository, then three extra lines will be emitted: 410 | 411 | 1. `release: 1.2.3` the version string 412 | 2. `commit-hash: abc...` the full Git SHA of the current commit 413 | 3. `commit-date: 2000-01-02` the author commit date of the current commit 414 | 415 | If Git is not found and no additional optional arguments are provided, then only 416 | the `release: 1.2.3` line will be emitted for verbose mode. 417 | 418 | Finally, if the Git repository is not "clean", that is if it contains 419 | uncommitted or modified files, a `-dirty` suffix will be added to the short and 420 | long Git SHA refs to signal that the implementation may not perfectly correspond 421 | to a SHA commit. 422 | 423 | - `@param [String]` program name 424 | - `@param [String]` version string 425 | - `@param [optional, String]` verbose mode set if value if `"true"` 426 | - `@param [optional, String]` short Git SHA 427 | - `@param [optional, String]` long Git SHA 428 | - `@param [optional, String]` commit/version date 429 | - `@stdout` version information 430 | - `@return 0` if successful 431 | 432 | Note that the implementation for this function was inspired by Rust's 433 | [`cargo version`](https://git.io/fjsOh). 434 | 435 | ### Examples 436 | 437 | Basic usage: 438 | 439 | ```sh 440 | print_version "my-program" "1.2.3" 441 | ``` 442 | 443 | An optional third argument puts the function in verbose mode and more detail is 444 | output to standard out: 445 | 446 | ```sh 447 | print_version "my-program" "1.2.3" "true" 448 | ``` 449 | 450 | An empty third argument is the same as only providing two arguments (i.e. 451 | non-verbose): 452 | 453 | ```sh 454 | print_version "my-program" "1.2.3" "" 455 | ``` 456 | 457 | ## section 458 | 459 | Prints a section-delimiting header to standard out. 460 | 461 | - `@param [String]` section heading text 462 | - `@stdout` section heading text 463 | - `@return 0` if successful 464 | 465 | ### Environment Variables 466 | 467 | - `TERM` used to determine whether or not the terminal is capable of printing 468 | colored output. 469 | 470 | ### Examples 471 | 472 | Basic usage: 473 | 474 | ```sh 475 | section "Building project" 476 | ``` 477 | 478 | ## setup_cleanup_directories 479 | 480 | Sets up state to track directories for later cleanup in a trap handler. 481 | 482 | This function is typically used in combination with [`cleanup_directory`] and 483 | [`trap_cleanup_directories`]. 484 | 485 | - `@return 0` if successful 486 | - `@return 1` if a temp file could not be created 487 | 488 | ### Global Variables 489 | 490 | - `__CLEANUP_DIRECTORIES__` used to track the collection of directories to clean 491 | up whose value is a file. If not declared or set, this function will set it 492 | up. 493 | - `__CLEANUP_DIRECTORIES_SETUP__` used to track if the `__CLEANUP_DIRECTORIES__` 494 | variable has been set up for the current process 495 | 496 | ### Examples 497 | 498 | Basic usage: 499 | 500 | ```sh 501 | setup_cleanup_directories 502 | ``` 503 | 504 | Used with [`cleanup_directory`], [`setup_traps`], and 505 | [`trap_cleanup_directories`]: 506 | 507 | ```sh 508 | setup_cleanup_directories 509 | setup_traps trap_cleanup_directories 510 | 511 | dir="$(mktemp_directory)" 512 | cleanup_directory "$dir" 513 | # do work on directory, etc. 514 | ``` 515 | 516 | [`cleanup_file`]: #cleanup_file 517 | [`setup_traps`]: #setup_traps 518 | [`trap_cleanup_directories`]: #trap_cleanup_directories 519 | 520 | ## setup_cleanup_files 521 | 522 | Sets up state to track files for later cleanup in a trap handler. 523 | 524 | This function is typically used in combination with [`cleanup_file`] and 525 | [`trap_cleanup_files`]. 526 | 527 | - `@return 0` if successful 528 | - `@return 1` if a temp file could not be created 529 | 530 | ### Global Variables 531 | 532 | - `__CLEANUP_FILES__` used to track the collection of files to clean up whose 533 | value is a file. If not declared or set, this function will set it up. 534 | - `__CLEANUP_FILES_SETUP__` used to track if the `__CLEANUP_FILES__` variable 535 | has been set up for the current process 536 | 537 | ### Examples 538 | 539 | Basic usage: 540 | 541 | ```sh 542 | setup_cleanup_files 543 | ``` 544 | 545 | Used with [`cleanup_file`], [`setup_traps`], and [`trap_cleanup_files`]: 546 | 547 | ```sh 548 | setup_cleanup_files 549 | setup_traps trap_cleanup_files 550 | 551 | file="$(mktemp_file)" 552 | cleanup_file "$file" 553 | # do work on file, etc. 554 | ``` 555 | 556 | [`cleanup_file`]: #cleanup_file 557 | [`setup_traps`]: #setup_traps 558 | [`trap_cleanup_files`]: #trap_cleanup_files 559 | 560 | ## setup_cleanups 561 | 562 | Sets up state to track files and directories for later cleanup in a trap 563 | handler. 564 | 565 | This function is typically used in combination with [`cleanup_file`] and 566 | [`cleanup_directory`] as well as [`trap_cleanups`]. 567 | 568 | - `@return 0` if successful 569 | - `@return 1` if the setup was not successful 570 | 571 | ### Examples 572 | 573 | Basic usage: 574 | 575 | ```sh 576 | setup_cleanups 577 | ``` 578 | 579 | Used with [`cleanup_directory`], [`cleanup_file`], [`setup_traps`], and 580 | [`trap_cleanups`]: 581 | 582 | ```sh 583 | setup_cleanups 584 | setup_traps trap_cleanups 585 | 586 | file="$(mktemp_file)" 587 | cleanup_file "$file" 588 | # do work on file, etc. 589 | 590 | dir="$(mktemp_directory)" 591 | cleanup_directory "$dir" 592 | # do work on directory, etc. 593 | ``` 594 | 595 | [`cleanup_directory`]: #cleanup_directory 596 | [`cleanup_file`]: #cleanup_file 597 | [`setup_traps`]: #setup_traps 598 | [`trap_cleanups`]: #trap_cleanups 599 | 600 | ## setup_traps 601 | 602 | Sets up traps for `EXIT` and common signals with the given cleanup function. 603 | 604 | In addition to `EXIT`, the `HUP`, `INT`, `QUIT`, `ALRM`, and `TERM` signals are 605 | also covered. 606 | 607 | This implementation was based on a very nice, portable signal handling thread 608 | thanks to an implementation on 609 | [Stack Overflow](https://unix.stackexchange.com/a/240736). 610 | 611 | - `@param [String]` name of function to run with traps 612 | 613 | ### Examples 614 | 615 | Basic usage with a simple "hello world" cleanup function: 616 | 617 | ```sh 618 | hello_trap() { 619 | echo "Hello, trap!" 620 | } 621 | 622 | setup_traps hello_trap 623 | ``` 624 | 625 | If the cleanup is simple enough to be a one-liner, you can provide the command 626 | as the single argument: 627 | 628 | ```sh 629 | setup_traps "echo 'Hello, World!'" 630 | ``` 631 | 632 | ## trap_cleanup_directories 633 | 634 | Removes any tracked directories registered via [`cleanup_directory`]. 635 | 636 | - `@return 0` whether or not an error has occurred 637 | 638 | [`cleanup_directory`]: #cleanup_directory 639 | 640 | ### Global Variables 641 | 642 | - `__CLEANUP_DIRECTORIES__` used to track the collection of files to clean up 643 | whose value is a file. If not declared or set, this function will assume there 644 | is no work to do. 645 | 646 | ### Examples 647 | 648 | Basic usage: 649 | 650 | ```sh 651 | trap trap_cleanup_directories 1 2 3 15 ERR EXIT 652 | 653 | dir="$(mktemp_directory)" 654 | cleanup_directory "$dir" 655 | # do work on directory, etc. 656 | ``` 657 | 658 | ## trap_cleanup_files 659 | 660 | Removes any tracked files registered via [`cleanup_file`]. 661 | 662 | - `@return 0` whether or not an error has occurred 663 | 664 | [`cleanup_file`]: #cleanup_file 665 | 666 | ### Global Variables 667 | 668 | - `__CLEANUP_FILES__` used to track the collection of files to clean up whose 669 | value is a file. If not declared or set, this function will assume there is no 670 | work to do. 671 | 672 | ### Examples 673 | 674 | Basic usage: 675 | 676 | ```sh 677 | trap trap_cleanup_files 1 2 3 15 ERR EXIT 678 | 679 | file="$(mktemp_file)" 680 | cleanup_file "$file" 681 | # do work on file, etc. 682 | ``` 683 | 684 | ## trap_cleanups 685 | 686 | Removes any tracked files and directories registered via [`cleanup_file`] and 687 | [`cleanup_directory`] respectively. 688 | 689 | - `@return 0` whether or not an error has occurred 690 | 691 | [`cleanup_directory`]: #cleanup_directory 692 | [`cleanup_file`]: #cleanup_file 693 | 694 | ### Examples 695 | 696 | Basic usage: 697 | 698 | ```sh 699 | trap trap_cleanups 1 2 3 15 ERR EXIT 700 | ``` 701 | 702 | Used with [`setup_traps`]: 703 | 704 | ```sh 705 | setup_traps trap_cleanups 706 | ``` 707 | 708 | [`setup_traps`]: #setup_traps 709 | 710 | ## warn 711 | 712 | Prints a warning message to standard out. 713 | 714 | - `@param [String]` warning text 715 | - `@stdout` warning heading text 716 | - `@return 0` if successful 717 | 718 | ### Environment Variables 719 | 720 | - `TERM` used to determine whether or not the terminal is capable of printing 721 | colored output. 722 | 723 | ### Examples 724 | 725 | Basic usage: 726 | 727 | ```sh 728 | warn "Could not connect to service" 729 | ``` 730 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## [Unreleased] - ReleaseDate 6 | 7 | ## [0.10.1] - 2021-05-09 8 | 9 | ### Fixed 10 | 11 | - ensure `setup_cleanup_directories` & `setup_cleanup_files` are isolated to 12 | their own process 13 | 14 | ## [0.10.0] - 2021-04-22 15 | 16 | ### Added 17 | 18 | - add support for nightly releases 19 | 20 | ### Changed 21 | 22 | - add `local` undefined exception in shell code 23 | 24 | ## [0.9.0] - 2021-04-14 25 | 26 | ### Added 27 | 28 | - add `setup_cleanup_directories` function 29 | - add `setup_cleanup_files` function 30 | - add `setup_cleanups` function 31 | - add `trap_cleanups` function 32 | - add `contrib/libsh-vendor.mk` Make task for use in projects 33 | 34 | ## [0.8.0] - 2021-04-11 35 | 36 | ### Added 37 | 38 | - add an API page, fully documenting all functions in one place 39 | - complete sections in Readme 40 | - add optional support for provided Git info in `print_version` function 41 | - add `install.sh` support for installing distributions 42 | - release a new `install.sh` script on each release 43 | 44 | ### Fixed 45 | 46 | - update errant local variable `code` in `download` function 47 | 48 | ## [0.7.0] - 2021-03-23 49 | 50 | ### Added 51 | 52 | - add support for multiple distributions of libsh 53 | 54 | ### Changed 55 | 56 | - remove locals in functions taking 1 argument 57 | - update default branch to `main` 58 | - upgrade CI workflow & introduce bors for merging 59 | 60 | ## [0.6.0] - 2020-12-30 61 | 62 | ### Added 63 | 64 | - add `info_start` function 65 | - add `info_end` function 66 | 67 | ## [0.5.0] - 2020-05-27 68 | 69 | ### Added 70 | 71 | - add `mktemp_directory` function 72 | - add `cleanup_directory` function 73 | - add `trap_cleanup_directories` function 74 | 75 | ## [0.4.0] - 2020-05-26 76 | 77 | ### Added 78 | 79 | - add coloring for `alacritty`, `tmux`, & `tmux-*` terminals 80 | 81 | ### Fixed 82 | 83 | - split all `local` declarations onto their own lines in `install.sh` 84 | 85 | ## [0.3.0] - 2020-05-13 86 | 87 | ### Added 88 | 89 | - add `indent` function 90 | - add `ftp` program support to `download`, primarily for OpenBSD 91 | 92 | ## [0.2.0] - 2019-11-26 93 | 94 | ### Added 95 | 96 | - add `install.sh` to remotely install libsh from GitHub releases 97 | - add `download` function 98 | - add `check_cmd` function 99 | - add `warn` function 100 | - add `setup_traps` function 101 | - add support for macOS 10.10 and older 102 | 103 | ### Changed 104 | 105 | - **breaking:** update `die` function to exit on failure 106 | - **breaking:** update `need_cmd` function to exit on failure 107 | 108 | ### Removed 109 | 110 | - remove `libbash.sh` 111 | 112 | ### Fixed 113 | 114 | - remove errant `s` from `set` if it exists 115 | 116 | ## [0.1.0] - 2019-11-25 117 | 118 | - the initial release 119 | 120 | 121 | 122 | [unreleased]: https://github.com/fnichol/libsh/compare/v0.10.1...HEAD 123 | 124 | [0.10.1]: https://github.com/fnichol/libsh/compare/v0.10.0...v0.10.1 125 | [0.10.0]: https://github.com/fnichol/libsh/compare/v0.9.0...v0.10.0 126 | [0.9.0]: https://github.com/fnichol/libsh/compare/v0.8.0...v0.9.0 127 | [0.8.0]: https://github.com/fnichol/libsh/compare/v0.7.0...v0.8.0 128 | [0.7.0]: https://github.com/fnichol/libsh/compare/v0.6.0...v0.7.0 129 | [0.6.0]: https://github.com/fnichol/libsh/compare/v0.5.0...v0.6.0 130 | [0.5.0]: https://github.com/fnichol/libsh/compare/v0.4.0...v0.5.0 131 | [0.4.0]: https://github.com/fnichol/libsh/compare/v0.3.0...v0.4.0 132 | [0.3.0]: https://github.com/fnichol/libsh/compare/v0.2.0...v0.3.0 133 | [0.2.0]: https://github.com/fnichol/libsh/compare/v0.1.0...v0.2.0 134 | [0.1.0]: https://github.com/fnichol/libsh/compare/636e5de...v0.1.0 135 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at fnichol@nichol.ca. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 Fletcher Nichol 2 | 3 | Permission is hereby granted, free of charge, to any 4 | person obtaining a copy of this software and associated 5 | documentation files (the "Software"), to deal in the 6 | Software without restriction, including without 7 | limitation the rights to use, copy, modify, merge, 8 | publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software 10 | is furnished to do so, subject to the following 11 | conditions: 12 | 13 | The above copyright notice and this permission notice 14 | shall be included in all copies or substantial portions 15 | of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 18 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 19 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 20 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 21 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 22 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 23 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 24 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 25 | DEALINGS IN THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SH_TESTS := $(shell find tests -type f -name '*_test.sh') 2 | 3 | TEST_TOOLS += curl git tar 4 | ifeq ($(shell uname -s),FreeBSD) 5 | TEST_TOOLS += gsed 6 | endif 7 | 8 | NAME := libsh 9 | REPO := https://github.com/fnichol/libsh 10 | 11 | include vendor/mk/base.mk 12 | include vendor/mk/shell.mk 13 | include vendor/mk/release.mk 14 | 15 | distribs = full minimal 16 | distribs_builds = $(patsubst %,build/libsh.%.sh,$(distribs)) \ 17 | $(patsubst %,build/libsh.%-minified.sh,$(distribs)) 18 | distribs_tests = $(patsubst %,test-libsh.%.sh,$(distribs)) \ 19 | $(patsubst %,test-libsh.%-minified.sh,$(distribs)) 20 | 21 | build: $(distribs_builds) build/libsh.sh build/install.sh ## Builds the sources 22 | .PHONY: build 23 | 24 | test: test-shell $(distribs_tests)## Runs all tests 25 | .PHONY: test 26 | 27 | check: check-shell ## Checks all linting, styling, & other rules 28 | .PHONY: check 29 | 30 | clean: clean-shell ## Cleans up project 31 | rm -rf build 32 | .PHONY: clean 33 | 34 | build/libsh.%-minified.sh: build/libsh.%.sh 35 | @echo "--- $@" 36 | awk -f support/minify.awk $< > $@ 37 | 38 | build/libsh.%.sh: lib/*.sh 39 | @echo "--- $@" 40 | mkdir -p build 41 | awk \ 42 | -v NIGHTLY_BUILD="$${NIGHTLY_BUILD:-}" \ 43 | -f support/compile.awk distrib/$(@F) > $@ 44 | 45 | build/libsh.sh: build/libsh.full.sh 46 | @echo "--- $@" 47 | cp $< $@ 48 | 49 | build/install.sh: contrib/install.sh build/libsh.full-minified.sh 50 | @echo "--- $@" 51 | awk \ 52 | -v NIGHTLY_BUILD="$${NIGHTLY_BUILD:-}" \ 53 | -f support/compile-install.awk $< $(word 2,$^) > $@ 54 | chmod 755 $@ 55 | 56 | test-libsh.%-minified.sh: build/libsh.%-minified.sh 57 | @echo "--- $@" 58 | @tests=$$(awk -f support/sources.awk distrib/libsh.$*.sh | awk '{ \ 59 | gsub(/lib/, "tests"); gsub(/\.sh/, "_test.sh"); print \ 60 | }' ) && \ 61 | for test in $$tests; do \ 62 | export SHELL_BIN=$(SHELL_BIN); \ 63 | export SRC=$<; \ 64 | echo " - Running $$test (SHELL_BIN=$$SHELL_BIN, SRC=$$SRC)"; \ 65 | $(SHELL_BIN) $$test || exit $$?; \ 66 | done 67 | 68 | test-libsh.%.sh: build/libsh.%.sh 69 | @echo "--- $@" 70 | @tests=$$(awk -f support/sources.awk distrib/libsh.$*.sh | awk '{ \ 71 | gsub(/lib/, "tests"); gsub(/\.sh/, "_test.sh"); print \ 72 | }' ) && \ 73 | for test in $$tests; do \ 74 | export SHELL_BIN=$(SHELL_BIN); \ 75 | export SRC=$<; \ 76 | echo " - Running $$test (SHELL_BIN=$$SHELL_BIN, SRC=$$SRC)"; \ 77 | $(SHELL_BIN) $$test || exit $$?; \ 78 | done 79 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 |
3 | libsh 4 |
5 |

6 | 7 |

8 | A library of common, reusable, and portable 9 | POSIX shell 10 | functions. 11 |

12 | 13 | | | | 14 | | ---------------: | ------------------------------------------------------------------------------- | 15 | | CI | [![CI Status][badge-overall]][ci] [![Bors enabled][badge-bors]][bors-dashboard] | 16 | | Latest Version | [![Latest version][badge-version]][github] | 17 | | GitHub Downloads | [![GitHub downloads][badge-github-dl]][github-releases] | 18 | | License | [![License][badge-license]][license] | 19 | 20 |
21 | Table of Contents 22 | 23 | 24 | 25 | - [What is libsh?](#what-is-libsh) 26 | - [Motivation](#motivation) 27 | - [Usage](#usage) 28 | - [Installation](#installation) 29 | - [install.sh](#installsh) 30 | - [GitHub Releases](#github-releases) 31 | - [Bespoke](#bespoke) 32 | - [Code of Conduct](#code-of-conduct) 33 | - [Issues](#issues) 34 | - [Contributing](#contributing) 35 | - [Release History](#release-history) 36 | - [Authors](#authors) 37 | - [License](#license) 38 | - [Contribution](#contribution) 39 | 40 | 41 | 42 |
43 | 44 | ## What is libsh? 45 | 46 | libsh is a collection is small, single purpose shell functions that help 47 | developers write consistent, well-formatted, portable scripts and programs 48 | without having to re-implement common tasks such as section printing, file 49 | downloading, trap handling, etc. 50 | 51 | The project is actively run and tested against several shell implementations, 52 | including but not limited to: 53 | 54 | - [Bash] 55 | - [BusyBox ash] 56 | - [DASH] 57 | - [KornShell] 58 | - [Zsh] 59 | - [sh] 60 | 61 | Additionally, several operating systems and distributions are tested and 62 | targeting including: 63 | 64 | - [FreeBSD] 65 | - [Linux distributions] 66 | - [OpenBSD] 67 | - [macOS] 68 | 69 | To help increase its use cases, several alternative bundles are provided called 70 | "distributions" which allow a user to consume the full library or a smaller 71 | subset. Currently, there are 2 main distributions, each with a comments included 72 | and a minified version: 73 | 74 | - full 75 | - full-minified 76 | - minimal 77 | - minimal-minified 78 | 79 | The details for each distribution can be found in the [`distrib/`] directory. 80 | 81 | [sh]: https://en.wikipedia.org/wiki/Almquist_shell 82 | [bash]: https://www.gnu.org/software/bash/ 83 | [zsh]: https://www.zsh.org/ 84 | [dash]: http://gondor.apana.org.au/~herbert/dash/ 85 | [kornshell]: http://www.kornshell.org/ 86 | [busybox ash]: https://www.busybox.net/ 87 | [linux distributions]: https://en.wikipedia.org/wiki/List_of_Linux_distributions 88 | [macos]: https://www.apple.com/macos/ 89 | [freebsd]: https://www.freebsd.org/ 90 | [openbsd]: https://www.openbsd.org/ 91 | [`distrib/`]: https://github.com/fnichol/libsh/tree/main/distrib 92 | 93 | ## Motivation 94 | 95 | Writing scripts and programs in shell code can be both rapid and responsive 96 | while at the same time being arcane and intensely error-prone. Add to that there 97 | is little ability to re-use ideas and implementations short of copy/pasting 98 | bodies of code around. libsh was born out of a desire to write some of these 99 | common solutions for the **last time**. 100 | 101 | As time moves forward, these snippets of code are used in a new environment, 102 | whether that is unintentionally a new shell implementation (for example, running 103 | a script for the first time on Ubuntu which uses DASH as its `/bin/sh` or in an 104 | Alpine Linux container which uses BusyBox's `ash`) or on a new system (for 105 | example, on macOS which now defaults to Zsh vs. a BSD variant which may default 106 | to KornShell). Likely something breaks and the resulting lesson is "this code is 107 | not portable" or "I need to install a full Bash to make this work properly". 108 | libsh also has a goal here to run with the same behavior in as many places as 109 | possible to ensure that a solution to a problem can truly be re-used. 110 | 111 | In an attempt to solve for the "copy/paste" and version drift issues, an 112 | installer is provided that will install releases of this library into your 113 | project as a standalone file or in-lined into a script with an insertion 114 | directive comment. This allows a user to update their codebases when new 115 | versions of libsh are released in as painless a way as possible. If the full 116 | library is more than is needed or if the file size becomes an issue, several 117 | "distributions" are provided as a way to consume no more than you need. 118 | 119 | If you have been nodding along so far, we hope you can use some of our 120 | collective knowledge rolled into libsh in your shell-based projects! 121 | 122 | ## Usage 123 | 124 | There are multiple ways to consume libsh, but the provided `install.sh` is the 125 | quickest to get started. Here's how to download the latest release of the full 126 | and minified version of libsh, which will be written to 127 | `./vendor/lib/libsh.full-minified.sh`: 128 | 129 | ```sh 130 | curl -sSf https://fnichol.github.io/libsh/install.sh | sh -s -- -d full-minified 131 | ``` 132 | 133 | More installations options are described in the [Installation](#installation) 134 | section. 135 | 136 | What follows is an example of a program installer script which downloads a 137 | tarball from a website, extracts, and installs it. As is often the case, what 138 | starts out seemingly an easy task, quickly becomes complex when dealing with 139 | error conditions such as required programs not being found, temporary files and 140 | directories not getting cleaned up, etc. Here's how libsh's library can help: 141 | 142 | ```sh 143 | #!/usr/bin/env sh 144 | set -eu 145 | 146 | # source/import library functions into script or alternatively insert a 147 | # distribution directly into the script with the `install.sh` program and a 148 | # line in the script containing only `# INSERT: libsh.sh` 149 | . "vendor/lib/libsh.full-minified.sh" 150 | 151 | # add traps to automatically cleanup an directories on exit/abort/etc 152 | setup_traps trap_cleanup_directories 153 | 154 | # write a heading style section banner to start off the script with color if 155 | # the terminal supports it 156 | section "Downloading program" 157 | 158 | # create a temporary directory in the system's appropriate TEMPDIR 159 | tmpdir="$(mktemp_directory)" 160 | # defer cleaning up this directory until the end of the program, on success or 161 | # failure, making use of the traps set above 162 | cleanup_directory "$tmpdir" 163 | 164 | # download a file use curl, wget, of ftp (on OpenBSD), whichever is found and 165 | # terminate the program if a suitable download program cannot be found 166 | download https://example.com/program.tar.gz "$tmpdir/program.tar.gz" \ 167 | || die "no download program found" 168 | 169 | # write a progress, sub task of the above section, again with color if supported 170 | info "Extracting program" 171 | # check and ensure that the `tar` program is found and terminate the program if 172 | # it is not found 173 | need_cmd tar 174 | tar xvzf $tmpdir/program.tar.gz -C "$tmpdir" 175 | 176 | # write an info style line that will do some work without writing any output 177 | info_start "Installing program" 178 | install "$tmpdir/program" "$HOME/bin/program" 179 | # write an ending to the above info line with "done." 180 | info_end 181 | 182 | info "Program installed" 183 | # indent the output of the program's version output at a level to fall 184 | # "inside" the info banner 185 | indent "$HOME/bin/program --version" 186 | ``` 187 | 188 | The full documented set of functions can be found on the [API] page. 189 | 190 | ## Installation 191 | 192 | There are various ways of consuming libsh, depending on needs, automation, etc. 193 | 194 | ### install.sh 195 | 196 | An installer is provided at which 197 | can help install an initial version libsh or to upgrade a preexisting version. 198 | It can be downloaded and run locally or piped into a shell interpreter in the 199 | "curl-bash" style as shown below. Note that if you're opposed to this idea, no 200 | problem, download it, read it and use it (or not). Otherwise check out some of 201 | the alternatives below. 202 | 203 | Vendor the latest full release into `./vendor/lib/libsh.full.sh`: 204 | 205 | ```sh 206 | curl -sSf https://fnichol.github.io/libsh/install.sh | sh 207 | ``` 208 | 209 | Vendor a specific minimal release into `/tmp/common-functions.sh`: 210 | 211 | ```sh 212 | curl -sSf https://fnichol.github.io/libsh/install.sh | sh -s -- \ 213 | --release=0.0.1 --distribution=minimal --target=/tmp/common-functions.sh 214 | ``` 215 | 216 | Insert the latest full release into `myprog.sh` at a line that contains only 217 | `# INSERT: libsh.sh`: 218 | 219 | ```sh 220 | curl -sSf https://fnichol.github.io/libsh/install.sh | sh -s -- \ 221 | --mode=insert --target=myprog.sh 222 | ``` 223 | 224 | Update the inserted version with a specific minimal/minified release in 225 | `cli.sh`: 226 | 227 | ```sh 228 | curl -sSf https://fnichol.github.io/libsh/install.sh | sh -s -- \ 229 | --mode=insert --release=0.0.1 --distribution=minimal-minified --target=cli.sh 230 | ``` 231 | 232 | ### GitHub Releases 233 | 234 | Each release of libsh comes with release artifacts published in [GitHub 235 | Releases][github-releases]. The `install.sh` program downloads its artifacts 236 | from this location so, this amounts to a manual/alternative way to consume 237 | libsh. Each artifact is also provided with MD5 and SHA256 checksums to help 238 | verify the artifact on a target system. 239 | 240 | ### Bespoke 241 | 242 | While a full distribution of libsh does not live in a single file in [source 243 | control][github], each function lives in its own source file and imports its own 244 | direct function dependencies (example: 245 | [download.sh](https://github.com/fnichol/libsh/blob/main/lib/download.sh)). It 246 | is very doable to import/vendor/combine various functions for use in other 247 | programs without having to consume the entire library nor even a slimmer 248 | distribution, however this remains an exercise for the reader. 249 | 250 | ## Code of Conduct 251 | 252 | This project adheres to the Contributor Covenant [code of 253 | conduct][code-of-conduct]. By participating, you are expected to uphold this 254 | code. Please report unacceptable behavior to fnichol@nichol.ca. 255 | 256 | ## Issues 257 | 258 | If you have any problems with or questions about this image, please contact us 259 | through a [GitHub issue][issues]. 260 | 261 | ## Contributing 262 | 263 | You are invited to contribute to new features, fixes, or updates, large or 264 | small; we are always thrilled to receive pull requests, and do our best to 265 | process them as fast as we can. 266 | 267 | Before you start to code, we recommend discussing your plans through a [GitHub 268 | issue][issues], especially for more ambitious contributions. This gives other 269 | contributors a chance to point you in the right direction, give you feedback on 270 | your design, and help you find out if someone else is working on the same thing. 271 | 272 | ## Release History 273 | 274 | See the [changelog] for a full release history. 275 | 276 | ## Authors 277 | 278 | Created and maintained by [Fletcher Nichol][fnichol] (). 279 | 280 | ## License 281 | 282 | Licensed under either of 283 | 284 | - The Apache License, Version 2.0 ([LICENSE-APACHE][license-apachev2] or 285 | ) 286 | - The MIT license ([LICENSE-MIT][license-mit] or 287 | ) 288 | 289 | at your option. 290 | 291 | ### Contribution 292 | 293 | Unless you explicitly state otherwise, any contribution intentionally submitted 294 | for inclusion in the work by you, as defined in the Apache-2.0 license, shall be 295 | dual licensed as above, without any additional terms or conditions. 296 | 297 | [api]: https://github.com/fnichol/libsh/blob/main/API.md 298 | [badge-bors]: https://bors.tech/images/badge_small.svg 299 | [badge-github-dl]: 300 | https://img.shields.io/github/downloads/fnichol/libsh/total.svg 301 | [badge-license]: 302 | https://img.shields.io/badge/License-Apache%202.0%20%2F%20MIT-blue.svg 303 | [badge-overall]: https://api.cirrus-ci.com/github/fnichol/libsh.svg 304 | [badge-version]: https://img.shields.io/github/tag/fnichol/libsh.svg 305 | [bors-dashboard]: https://app.bors.tech/repositories/32312 306 | [changelog]: https://github.com/fnichol/libsh/blob/main/CHANGELOG.md 307 | [ci]: https://cirrus-ci.com/github/fnichol/libsh 308 | [code-of-conduct]: https://github.com/fnichol/libsh/blob/main/CODE_OF_CONDUCT.md 309 | [fnichol]: https://github.com/fnichol 310 | [github-releases]: https://github.com/fnichol/libsh/releases 311 | [github]: https://github.com/fnichol/libsh 312 | [issues]: https://github.com/fnichol/libsh/issues 313 | [license]: #license 314 | [license-apachev2]: https://github.com/fnichol/libsh/blob/main/LICENSE-APACHE 315 | [license-mit]: https://github.com/fnichol/libsh/blob/main/LICENSE-MIT 316 | -------------------------------------------------------------------------------- /VERSION.txt: -------------------------------------------------------------------------------- 1 | 0.10.2-dev 2 | -------------------------------------------------------------------------------- /contrib/install.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | print_usage() { 5 | local program version author 6 | program="$1" 7 | version="$2" 8 | author="$3" 9 | 10 | need_cmd sed 11 | 12 | echo "$program $version 13 | 14 | Installs libsh into a dedicated vendored file or inserted into an existing 15 | source file 16 | 17 | USAGE: 18 | $program [FLAGS] [OPTIONS] [--] 19 | 20 | FLAGS: 21 | -h, --help Prints help information 22 | -V, --version Prints version information 23 | 24 | OPTIONS: 25 | -d, --distribution= Distribution format 26 | [values: full, full-minified, 27 | minimal, minimal-minified] 28 | [default: full] 29 | -m, --mode= Install mode 30 | [values: vendor, insert] 31 | [default: vendor] 32 | -r, --release= Release version 33 | [examples: latest, 1.2.3, nightly] 34 | [default: latest] 35 | -t, --target= Target directory or file for installation 36 | [examples: /tmp/libsh.sh, file.txt] 37 | [default: ./vendor/lib/libsh..sh] 38 | 39 | EXAMPLES: 40 | # Vendor the latest full release into ./vendor/lib/libsh.sh 41 | $program 42 | 43 | # Vendor a specific minimal release into /tmp/libsh-0.0.1.sh 44 | $program --release=0.0.1 --distribution=minimal \\ 45 | --target=/tmp/libsh-0.0.1.sh 46 | 47 | # Insert the latest full release into myprog.sh at a line: 48 | # \`# INSERT: libsh.sh\` 49 | $program --mode=insert --target=myprog.sh 50 | 51 | # Update the inserted version with a specific minimal/minified 52 | # release in cli.sh: 53 | $program --mode=insert --release=0.0.1 \\ 54 | --distribution=minimal-minified --target=cli.sh 55 | 56 | AUTHOR: 57 | $author 58 | " | sed 's/^ \{1,4\}//g' 59 | } 60 | 61 | main() { 62 | set -eu 63 | if [ -n "${DEBUG:-}" ]; then set -v; fi 64 | if [ -n "${TRACE:-}" ]; then set -xv; fi 65 | 66 | local program version author sha sha_long date 67 | program="install.sh" 68 | version="@@version@@" 69 | author="Fletcher Nichol " 70 | sha="@@commit_hash_short@@" 71 | sha_long="@@commit_hash@@" 72 | date="@@commit_date@@" 73 | 74 | # Parse CLI arguments and set local variables 75 | parse_cli_args "$program" "$version" "$author" "$sha" "$sha_long" "$date" "$@" 76 | local distrib mode release target 77 | distrib="$DISTRIB" 78 | mode="$MODE" 79 | release="$RELEASE" 80 | target="$TARGET" 81 | unset DISTRIB MODE RELEASE TARGET 82 | 83 | setup_traps trap_cleanup_files 84 | 85 | if [ "$release" = "latest" ]; then 86 | release="$(latest_release fnichol/libsh)" 87 | fi 88 | 89 | case "$mode" in 90 | insert) 91 | insert_libsh "$release" "$distrib" "$target" 92 | ;; 93 | vendor) 94 | vendor_libsh "$release" "$distrib" "$target" 95 | ;; 96 | *) 97 | die "invalid mode value; mode=$mode" 98 | ;; 99 | esac 100 | } 101 | 102 | parse_cli_args() { 103 | local program version author sha sha_long date mode release target 104 | program="$1" 105 | shift 106 | version="$1" 107 | shift 108 | author="$1" 109 | shift 110 | sha="$1" 111 | shift 112 | sha_long="$1" 113 | shift 114 | date="$1" 115 | shift 116 | 117 | DISTRIB="full" 118 | MODE="vendor" 119 | RELEASE="latest" 120 | TARGET= 121 | 122 | OPTIND=1 123 | while getopts "hd:m:r:t:V-:" arg; do 124 | case "$arg" in 125 | h) 126 | print_usage "$program" "$version" "$author" 127 | exit 0 128 | ;; 129 | d) 130 | if is_distrib_valid "$OPTARG"; then 131 | DISTRIB="$OPTARG" 132 | else 133 | print_usage "$program" "$version" "$author" >&2 134 | die "invalid distribution name $OPTARG" 135 | fi 136 | ;; 137 | m) 138 | if is_mode_valid "$OPTARG"; then 139 | MODE="$OPTARG" 140 | else 141 | print_usage "$program" "$version" "$author" >&2 142 | die "invalid mode name $OPTARG" 143 | fi 144 | ;; 145 | r) 146 | RELEASE="$OPTARG" 147 | ;; 148 | t) 149 | TARGET="$OPTARG" 150 | ;; 151 | V) 152 | print_version "$program" "$version" "true" "$sha" "$sha_long" "$date" 153 | exit 0 154 | ;; 155 | -) 156 | long_optarg="${OPTARG#*=}" 157 | case "$OPTARG" in 158 | distribution=?*) 159 | if is_distrib_valid "$long_optarg"; then 160 | DISTRIB="$long_optarg" 161 | else 162 | print_usage "$program" "$version" "$author" >&2 163 | die "invalid distribution name '$long_optarg'" 164 | fi 165 | ;; 166 | distribution*) 167 | print_usage "$program" "$version" "$author" >&2 168 | die "missing required argument for --$OPTARG option" 169 | ;; 170 | help) 171 | print_usage "$program" "$version" "$author" 172 | exit 0 173 | ;; 174 | mode=?*) 175 | if is_mode_valid "$long_optarg"; then 176 | MODE="$long_optarg" 177 | else 178 | print_usage "$program" "$version" "$author" >&2 179 | die "invalid mode name '$long_optarg'" 180 | fi 181 | ;; 182 | mode*) 183 | print_usage "$program" "$version" "$author" >&2 184 | die "missing required argument for --$OPTARG option" 185 | ;; 186 | release=?*) 187 | RELEASE="$long_optarg" 188 | ;; 189 | release*) 190 | print_usage "$program" "$version" "$author" >&2 191 | die "missing required argument for --$OPTARG option" 192 | ;; 193 | target=?*) 194 | TARGET="$long_optarg" 195 | ;; 196 | target*) 197 | print_usage "$program" "$version" "$author" >&2 198 | die "missing required argument for --$OPTARG option" 199 | ;; 200 | version) 201 | print_version "$program" "$version" "true" \ 202 | "$sha" "$sha_long" "$date" 203 | exit 0 204 | ;; 205 | '') 206 | # "--" terminates argument processing 207 | break 208 | ;; 209 | *) 210 | print_usage "$program" "$version" "$author" >&2 211 | die "invalid argument --$OPTARG" 212 | ;; 213 | esac 214 | ;; 215 | \?) 216 | print_usage "$program" "$version" "$author" >&2 217 | die "invalid argument; arg=-$OPTARG" 218 | ;; 219 | esac 220 | done 221 | shift "$((OPTIND - 1))" 222 | 223 | if [ -z "$TARGET" ]; then 224 | TARGET="./vendor/lib/libsh.${DISTRIB}.sh" 225 | fi 226 | } 227 | 228 | download_libsh() { 229 | local release distrib target repo 230 | release="$1" 231 | distrib="$2" 232 | target="$3" 233 | repo="https://github.com/fnichol/libsh" 234 | if [ "$release" != "nightly" ]; then 235 | release="v$release" 236 | fi 237 | 238 | download \ 239 | "$repo/releases/download/$release/libsh.${distrib}.sh" \ 240 | "$target" 241 | } 242 | 243 | insert_libsh() { 244 | local release distrib target 245 | release="$1" 246 | distrib="$2" 247 | target="$3" 248 | 249 | need_cmd awk 250 | need_cmd cat 251 | 252 | local libsh rendered 253 | libsh="$(mktemp_file)" 254 | cleanup_file "$libsh" 255 | rendered="$(mktemp_file)" 256 | cleanup_file "$rendered" 257 | 258 | section "Inserting libsh.sh '$release' ($distrib) into $target" 259 | 260 | download_libsh "$release" "$distrib" "$libsh" 261 | 262 | info "Inlining libsh into $target" 263 | awk -v libsh="$libsh" ' 264 | BEGIN { 265 | tgtprint = 1 266 | libprint = 0 267 | } 268 | 269 | # Stop printing from BEGIN line 270 | /^# BEGIN: libsh.sh$/ { 271 | tgtprint = 0 272 | } 273 | # Insert libsh at BEGIN or INSERT lines 274 | /^# BEGIN: libsh.sh$/ || /^# INSERT: libsh.sh$/ { 275 | while ((getline libshln < libsh) > 0) { 276 | if (libshln == "# BEGIN: libsh.sh") { 277 | # Start printing libsh contents starting with BEGIN line 278 | libprint = 1 279 | print libshln 280 | } else if (libshln == "# END: libsh.sh") { 281 | # Stop printing libsh contents after printing END line 282 | libprint = 0 283 | print libshln 284 | } else if (libprint == 1) { 285 | # If libshprint mode is enabled, print line 286 | print libshln 287 | } 288 | } 289 | close(libsh) 290 | } 291 | # Do not print INSERT line 292 | /^# INSERT: libsh.sh$/ { 293 | next 294 | } 295 | # Resume printing after END line 296 | /^# END: libsh.sh$/ { 297 | tgtprint = 1 298 | next 299 | } 300 | # If target print mode is enabled, print line 301 | tgtprint == 1 { 302 | print 303 | } 304 | ' "$target" >"$rendered" 305 | cat "$rendered" >"$target" 306 | } 307 | 308 | is_distrib_valid() { 309 | local distrib 310 | distrib="$1" 311 | 312 | case "$distrib" in 313 | full | full-minified | minimal | minimal-minified) 314 | return 0 315 | ;; 316 | *) 317 | return 1 318 | ;; 319 | esac 320 | } 321 | 322 | is_mode_valid() { 323 | local mode 324 | mode="$1" 325 | 326 | case "$mode" in 327 | vendor | insert) 328 | return 0 329 | ;; 330 | *) 331 | return 1 332 | ;; 333 | esac 334 | } 335 | 336 | latest_release() { 337 | local gh_repo 338 | gh_repo="$1" 339 | 340 | need_cmd awk 341 | 342 | local tmpfile 343 | tmpfile="$(mktemp_file)" 344 | cleanup_file "$tmpfile" 345 | 346 | download \ 347 | "https://api.github.com/repos/$gh_repo/releases/latest" \ 348 | "$tmpfile" \ 349 | >/dev/null 350 | awk ' 351 | BEGIN { FS="\""; RS="," } 352 | $2 == "tag_name" { sub(/^v/, "", $4); print $4 } 353 | ' "$tmpfile" 354 | } 355 | 356 | vendor_libsh() { 357 | local release distrib target 358 | release="$1" 359 | distrib="$2" 360 | target="$3" 361 | 362 | need_cmd cat 363 | need_cmd touch 364 | need_cmd dirname 365 | need_cmd mkdir 366 | 367 | local tmpfile 368 | tmpfile="$(mktemp_file)" 369 | cleanup_file "$tmpfile" 370 | 371 | section "Vendoring libsh.sh '$release' ($distrib) to $target" 372 | 373 | download_libsh "$release" "$distrib" "$tmpfile" 374 | 375 | info "Copying libsh to $target" 376 | mkdir -p "$(dirname "$target")" 377 | touch "$target" 378 | cat "$tmpfile" >"$target" 379 | } 380 | 381 | version_ge() { 382 | local version maj min 383 | version="$1" 384 | maj="$2" 385 | min="$3" 386 | 387 | need_cmd awk 388 | 389 | [ "$(echo "$version" | awk -F'.' '{ print $1 }')" -ge "$maj" ] \ 390 | && [ "$(echo "$version" | awk -F'.' '{ print $2 }')" -ge "$min" ] 391 | } 392 | 393 | # INSERT: libsh.sh 394 | 395 | main "$@" 396 | -------------------------------------------------------------------------------- /contrib/libsh-vendor.mk: -------------------------------------------------------------------------------- 1 | vendor-libsh: ## Vendors updated version of libsh 2 | @echo "--- $@" 3 | curl --proto '=https' --tlsv1.2 -sSf \ 4 | https://fnichol.github.io/libsh/install.sh \ 5 | | sh -s -- --mode=vendor --release=latest 6 | .PHONY: vendor-libsh 7 | -------------------------------------------------------------------------------- /distrib/libsh.full.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/check_cmd.sh" 4 | . "lib/cleanup_directory.sh" 5 | . "lib/cleanup_file.sh" 6 | . "lib/die.sh" 7 | . "lib/download.sh" 8 | . "lib/indent.sh" 9 | . "lib/info.sh" 10 | . "lib/info_end.sh" 11 | . "lib/info_start.sh" 12 | . "lib/mktemp_directory.sh" 13 | . "lib/mktemp_file.sh" 14 | . "lib/need_cmd.sh" 15 | . "lib/print_version.sh" 16 | . "lib/section.sh" 17 | . "lib/setup_cleanup_directories.sh" 18 | . "lib/setup_cleanup_files.sh" 19 | . "lib/setup_cleanups.sh" 20 | . "lib/setup_traps.sh" 21 | . "lib/trap_cleanup_directories.sh" 22 | . "lib/trap_cleanup_files.sh" 23 | . "lib/trap_cleanups.sh" 24 | . "lib/warn.sh" 25 | -------------------------------------------------------------------------------- /distrib/libsh.minimal.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/die.sh" 4 | . "lib/indent.sh" 5 | . "lib/info.sh" 6 | . "lib/need_cmd.sh" 7 | . "lib/section.sh" 8 | -------------------------------------------------------------------------------- /lib/_header: -------------------------------------------------------------------------------- 1 | # 2 | # Copyright 2019 Fletcher Nichol and/or applicable contributors. 3 | # 4 | # Licensed under the Apache License, Version 2.0 or the MIT license (see 6 | # , at your option. This 7 | # file may not be copied, modified, or distributed except according to those 8 | # terms. 9 | # 10 | # libsh.sh 11 | # -------- 12 | # project: https://github.com/fnichol/libsh 13 | # author: Fletcher Nichol 14 | # version: @@version@@ 15 | # distribution: @@distrib@@ 16 | # commit-hash: @@commit_hash@@ 17 | # commit-date: @@commit_date@@ 18 | # artifact: https://github.com/fnichol/libsh/releases/download/v@@version@@/@@distrib@@ 19 | # source: https://github.com/fnichol/libsh/tree/v@@version@@ 20 | # archive: https://github.com/fnichol/libsh/archive/v@@version@@.tar.gz 21 | # 22 | -------------------------------------------------------------------------------- /lib/_ksh_local.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | if [ -n "${KSH_VERSION:-}" ]; then 4 | # Evil, nasty, wicked hack to ignore calls to `local `, on the strict 5 | # assumption that no initialization will take place, i.e. `local 6 | # =`. If this assumption holds, this implementation fakes a 7 | # `local` keyword for ksh. The `eval` is used as some versions of dash will 8 | # error with "Syntax error: Bad function name" whether or not it's in a 9 | # conditional (likely in the parser/ast phase) (src: 10 | # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=619786). Also, `shfmt` 11 | # does *not* like a function called `local` so...another dodge here. TBD on 12 | # this one, folks... 13 | eval "local() { return 0; }" 14 | fi 15 | -------------------------------------------------------------------------------- /lib/_shebang.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | -------------------------------------------------------------------------------- /lib/check_cmd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Determines whether or not a program is available on the system PATH. 4 | # 5 | # * `@param [String]` program name 6 | # * `@return 0` if program is found on system PATH 7 | # * `@return 1` if program is not found 8 | # 9 | # # Environment Variables 10 | # 11 | # * `PATH` indirectly used to search for the program 12 | # 13 | # # Examples 14 | # 15 | # Basic usage, when used as a conditional check: 16 | # 17 | # ```sh 18 | # if check_cmd git; then 19 | # echo "Found Git" 20 | # fi 21 | # ``` 22 | check_cmd() { 23 | if ! command -v "$1" >/dev/null 2>&1; then 24 | return 1 25 | fi 26 | } 27 | -------------------------------------------------------------------------------- /lib/cleanup_directory.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/setup_cleanup_directories.sh" 4 | 5 | # Tracks a directory for later cleanup in a trap handler. 6 | # 7 | # This function can be called immediately after a temp directory is created, 8 | # before a directory is created, or long after a directory exists. When used in 9 | # combination with [`trap_cleanup_directories`], all directories registered by 10 | # calling `cleanup_directory` will be removed if they exist when 11 | # `trap_cleanup_directories` is invoked. 12 | # 13 | # * `@param [String]` path to directory 14 | # * `@return 0` if successful 15 | # * `@return 1` if a temp file could not be created 16 | # 17 | # [`trap_cleanup_directories`]: #trap_cleanup_directories 18 | # 19 | # # Global Variables 20 | # 21 | # * `__CLEANUP_DIRECTORIES__` used to track the collection of directories to 22 | # clean up whose value is a file. If not declared or set, this function will 23 | # set it up. 24 | # 25 | # # Examples 26 | # 27 | # Basic usage: 28 | # 29 | # ```sh 30 | # dir="$(mktemp_directory)" 31 | # cleanup_directory "$dir" 32 | # # do work on directory, etc. 33 | # ``` 34 | cleanup_directory() { 35 | setup_cleanup_directories 36 | echo "$1" >>"$__CLEANUP_DIRECTORIES__" 37 | } 38 | -------------------------------------------------------------------------------- /lib/cleanup_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/setup_cleanup_files.sh" 4 | 5 | # Tracks a file for later cleanup in a trap handler. 6 | # 7 | # This function can be called immediately after a temp file is created, before 8 | # a file is created, or long after a file exists. When used in combination with 9 | # [`trap_cleanup_files`], all files registered by calling `cleanup_file` will 10 | # be removed if they exist when `trap_cleanup_files` is invoked. 11 | # 12 | # * `@param [String]` path to file 13 | # * `@return 0` if successful 14 | # * `@return 1` if a temp file could not be created 15 | # 16 | # [`trap_cleanup_files`]: #trap_cleanup_files 17 | # 18 | # # Global Variables 19 | # 20 | # * `__CLEANUP_FILES__` used to track the collection of files to clean up whose 21 | # value is a file. If not declared or set, this function will set it up. 22 | # 23 | # # Examples 24 | # 25 | # Basic usage: 26 | # 27 | # ```sh 28 | # file="$(mktemp_file)" 29 | # cleanup_file "$file" 30 | # # do work on file, etc. 31 | # ``` 32 | cleanup_file() { 33 | setup_cleanup_files 34 | echo "$1" >>"$__CLEANUP_FILES__" 35 | } 36 | -------------------------------------------------------------------------------- /lib/die.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Prints an error message to standard error and exits with a non-zero exit 4 | # code. 5 | # 6 | # * `@param [String]` warning text 7 | # * `@stderr` warning text message 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Notes 15 | # 16 | # This function calls `exit` and will **not** return. 17 | # 18 | # # Examples 19 | # 20 | # Basic usage: 21 | # 22 | # ```sh 23 | # die "No program to download tarball" 24 | # ``` 25 | die() { 26 | case "${TERM:-}" in 27 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 28 | printf -- "\n\033[1;31;40mxxx \033[1;37;40m%s\033[0m\n\n" "$1" >&2 29 | ;; 30 | *) 31 | printf -- "\nxxx %s\n\n" "$1" >&2 32 | ;; 33 | esac 34 | 35 | exit 1 36 | } 37 | -------------------------------------------------------------------------------- /lib/download.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | . "lib/check_cmd.sh" 5 | . "lib/info.sh" 6 | . "lib/need_cmd.sh" 7 | . "lib/warn.sh" 8 | 9 | # Downloads the contents at the given URL to the given local file. 10 | # 11 | # This implementation attempts to use the `curl` program with a fallback to the 12 | # `wget` program and a final fallback to the `ftp` program. The first download 13 | # program to succeed is used and if all fail, this function returns a non-zero 14 | # code. 15 | # 16 | # * `@param [String]` download URL 17 | # * `@param [String]` destination file 18 | # * `@return 0` if a download was successful 19 | # * `@return 1` if a download was not successful 20 | # 21 | # # Notes 22 | # 23 | # At least one of `curl`, `wget`, or `ftp` must be compiled with SSL/TLS 24 | # support to be able to download from `https` sources. 25 | # 26 | # # Examples 27 | # 28 | # Basic usage: 29 | # 30 | # ```sh 31 | # download http://example.com/file.txt /tmp/file.txt 32 | # ``` 33 | download() { 34 | local _url _dst _code _orig_flags 35 | _url="$1" 36 | _dst="$2" 37 | 38 | need_cmd sed 39 | 40 | # Attempt to download with curl, if found. If successful, quick return 41 | if check_cmd curl; then 42 | info "Downloading $_url to $_dst (curl)" 43 | _orig_flags="$-" 44 | set +e 45 | curl -sSfL "$_url" -o "$_dst" 46 | _code="$?" 47 | set "-$(echo "$_orig_flags" | sed s/s//g)" 48 | if [ $_code -eq 0 ]; then 49 | unset _url _dst _code _orig_flags 50 | return 0 51 | else 52 | local _e 53 | _e="curl failed to download file, perhaps curl doesn't have" 54 | _e="$_e SSL support and/or no CA certificates are present?" 55 | warn "$_e" 56 | unset _e 57 | fi 58 | fi 59 | 60 | # Attempt to download with wget, if found. If successful, quick return 61 | if check_cmd wget; then 62 | info "Downloading $_url to $_dst (wget)" 63 | _orig_flags="$-" 64 | set +e 65 | wget -q -O "$_dst" "$_url" 66 | _code="$?" 67 | set "-$(echo "$_orig_flags" | sed s/s//g)" 68 | if [ $_code -eq 0 ]; then 69 | unset _url _dst _code _orig_flags 70 | return 0 71 | else 72 | local _e 73 | _e="wget failed to download file, perhaps wget doesn't have" 74 | _e="$_e SSL support and/or no CA certificates are present?" 75 | warn "$_e" 76 | unset _e 77 | fi 78 | fi 79 | 80 | # Attempt to download with ftp, if found. If successful, quick return 81 | if check_cmd ftp; then 82 | info "Downloading $_url to $_dst (ftp)" 83 | _orig_flags="$-" 84 | set +e 85 | ftp -o "$_dst" "$_url" 86 | _code="$?" 87 | set "-$(echo "$_orig_flags" | sed s/s//g)" 88 | if [ $_code -eq 0 ]; then 89 | unset _url _dst _code _orig_flags 90 | return 0 91 | else 92 | local _e 93 | _e="ftp failed to download file, perhaps ftp doesn't have" 94 | _e="$_e SSL support and/or no CA certificates are present?" 95 | warn "$_e" 96 | unset _e 97 | fi 98 | fi 99 | 100 | unset _url _dst _code _orig_flags 101 | # If we reach this point, curl, wget and ftp have failed and we're out of 102 | # options 103 | warn "Downloading requires SSL-enabled 'curl', 'wget', or 'ftp' on PATH" 104 | return 1 105 | } 106 | -------------------------------------------------------------------------------- /lib/indent.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | . "lib/mktemp_file.sh" 5 | . "lib/need_cmd.sh" 6 | 7 | # Indents the output from a command while preserving the command's exit code. 8 | # 9 | # In minimal/POSIX shells there is no support for `set -o pipefail` which means 10 | # that the exit code of the first command in a shell pipeline won't be 11 | # addressable in an easy way. This implementation uses a temp file to ferry the 12 | # original command's exit code from a subshell back into the main function. The 13 | # output can be aligned with a pipe to `sed` as before but now we have an 14 | # implementation which mimics a `set -o pipefail` which should work on all 15 | # Bourne shells. Note that the `set -o errexit` is disabled during the 16 | # command's invocation so that its exit code can be captured. 17 | # 18 | # Based on implementation from [Stack 19 | # Overflow](https://stackoverflow.com/a/54931544) 20 | # 21 | # * `@param [String[]]` command and arguments 22 | # * `@return` the exit code of the command which was executed 23 | # 24 | # # Notes 25 | # 26 | # In order to preserve the output order of the command, the `stdout` and 27 | # `stderr` streams are combined, so the command will not emit its `stderr` 28 | # output to the caller's `stderr` stream. 29 | # 30 | # # Examples 31 | # 32 | # Basic usage: 33 | # 34 | # ```sh 35 | # indent cat /my/file 36 | # ``` 37 | indent() { 38 | local _ecfile _ec _orig_flags 39 | 40 | need_cmd cat 41 | need_cmd rm 42 | need_cmd sed 43 | 44 | _ecfile="$(mktemp_file)" 45 | 46 | _orig_flags="$-" 47 | set +e 48 | { 49 | "$@" 2>&1 50 | echo "$?" >"$_ecfile" 51 | } | sed 's/^/ /' 52 | set "-$(echo "$_orig_flags" | sed s/s//g)" 53 | _ec="$(cat "$_ecfile")" 54 | rm -f "$_ecfile" 55 | 56 | unset _ecfile _orig_flags 57 | return "${_ec:-5}" 58 | } 59 | -------------------------------------------------------------------------------- /lib/info.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Prints an informational, detailed step to standard out. 4 | # 5 | # * `@param [String]` informational text 6 | # * `@stdout` informational heading text 7 | # * `@return 0` if successful 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # info "Downloading tarball" 20 | # ``` 21 | info() { 22 | case "${TERM:-}" in 23 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 24 | printf -- "\033[1;36;40m - \033[1;37;40m%s\033[0m\n" "$1" 25 | ;; 26 | *) 27 | printf -- " - %s\n" "$1" 28 | ;; 29 | esac 30 | } 31 | -------------------------------------------------------------------------------- /lib/info_end.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Completes printing an informational, detailed step to standard out which has 4 | # no output, started with `info_start` 5 | # 6 | # * `@stdout` informational heading text 7 | # * `@return 0` if successful 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # info_end 20 | # ``` 21 | info_end() { 22 | case "${TERM:-}" in 23 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 24 | printf -- "\033[1;37;40m%s\033[0m\n" "done." 25 | ;; 26 | *) 27 | printf -- "%s\n" "done." 28 | ;; 29 | esac 30 | } 31 | -------------------------------------------------------------------------------- /lib/info_start.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Prints an informational, detailed step to standard out which has no output. 4 | # 5 | # * `@param [String]` informational text 6 | # * `@stdout` informational heading text 7 | # * `@return 0` if successful 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # info_start "Copying file" 20 | # ``` 21 | info_start() { 22 | case "${TERM:-}" in 23 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 24 | printf -- "\033[1;36;40m - \033[1;37;40m%s ... \033[0m" "$1" 25 | ;; 26 | *) 27 | printf -- " - %s ... " "$1" 28 | ;; 29 | esac 30 | } 31 | -------------------------------------------------------------------------------- /lib/mktemp_directory.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/need_cmd.sh" 4 | 5 | # Creates a temporary directory and prints the name to standard output. 6 | # 7 | # Most system use the first no-argument version, however Mac OS X 10.10 8 | # (Yosemite) and older don't allow the no-argument version, hence the second 9 | # fallback version. 10 | # 11 | # All tested invocations will create a file in each platform's suitable 12 | # temporary directory. 13 | # 14 | # * `@param [optional, String]` parent directory 15 | # * `@stdout` path to temporary directory 16 | # * `@return 0` if successful 17 | # 18 | # # Examples 19 | # 20 | # Basic usage: 21 | # 22 | # ```sh 23 | # dir="$(mktemp_directory)" 24 | # # use directory 25 | # ``` 26 | # 27 | # With a custom parent directory: 28 | # 29 | # ```sh 30 | # dir="$(mktemp_directory "$HOME")" 31 | # # use directory 32 | # ``` 33 | 34 | # shellcheck disable=SC2120 35 | mktemp_directory() { 36 | need_cmd mktemp 37 | 38 | if [ -n "${1:-}" ]; then 39 | mktemp -d "$1/tmp.XXXXXX" 40 | else 41 | mktemp -d 2>/dev/null || mktemp -d -t tmp 42 | fi 43 | } 44 | -------------------------------------------------------------------------------- /lib/mktemp_file.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/need_cmd.sh" 4 | 5 | # Creates a temporary file and prints the name to standard output. 6 | # 7 | # Most systems use the first no-argument version, however Mac OS X 10.10 8 | # (Yosemite) and older don't allow the no-argument version, hence the second 9 | # fallback version. 10 | 11 | # All tested invocations will create a file in each platform's suitable 12 | # temporary directory. 13 | # 14 | # * `@param [optional, String]` parent directory 15 | # * `@stdout` path to temporary file 16 | # * `@return 0` if successful 17 | # 18 | # # Examples 19 | # 20 | # Basic usage: 21 | # 22 | # ```sh 23 | # file="$(mktemp_file)" 24 | # # use file 25 | # ``` 26 | # 27 | # With a custom parent directory: 28 | # 29 | # ```sh 30 | # dir="$(mktemp_file $HOME)" 31 | # # use file 32 | # ``` 33 | 34 | # shellcheck disable=SC2120 35 | mktemp_file() { 36 | need_cmd mktemp 37 | 38 | if [ -n "${1:-}" ]; then 39 | mktemp "$1/tmp.XXXXXX" 40 | else 41 | mktemp 2>/dev/null || mktemp -t tmp 42 | fi 43 | } 44 | -------------------------------------------------------------------------------- /lib/need_cmd.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/check_cmd.sh" 4 | . "lib/die.sh" 5 | 6 | # Prints an error message and exits with a non-zero code if the program is not 7 | # available on the system PATH. 8 | # 9 | # * `@param [String]` program name 10 | # * `@stderr` a warning message is printed if program cannot be found 11 | # 12 | # # Environment Variables 13 | # 14 | # * `PATH` indirectly used to search for the program 15 | # 16 | # # Notes 17 | # 18 | # If the program is not found, this function calls `exit` and will **not** 19 | # return. 20 | # 21 | # # Examples 22 | # 23 | # Basic usage, when used as a guard or prerequisite in a function: 24 | # 25 | # ```sh 26 | # need_cmd git 27 | # ``` 28 | need_cmd() { 29 | if ! check_cmd "$1"; then 30 | die "Required command '$1' not found on PATH" 31 | fi 32 | } 33 | -------------------------------------------------------------------------------- /lib/print_version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | . "lib/check_cmd.sh" 5 | 6 | # Prints program version information to standard out. 7 | # 8 | # The minimal implementation will output the program name and version, 9 | # separated with a space, such as `my-program 1.2.3`. However, if the Git 10 | # program is detected and the current working directory is under a Git 11 | # repository, then more information will be displayed. Namely, the short Git 12 | # SHA and author commit date will be appended in parenthesis at end of the 13 | # line. For example, `my-program 1.2.3 (abc123 2000-01-02)`. Alternatively, if 14 | # the Git commit information is known ahead of time, it can be provided via 15 | # optional arguments. 16 | # 17 | # If verbose mode is enable by setting the optional third argument to a 18 | # `true`, then a detailed version report will be appended to the 19 | # single line "simple mode". Assuming that the Git program is available and the 20 | # current working directory is under a Git repository, then three extra lines 21 | # will be emitted: 22 | # 23 | # 1. `release: 1.2.3` the version string 24 | # 2. `commit-hash: abc...` the full Git SHA of the current commit 25 | # 3. `commit-date: 2000-01-02` the author commit date of the current commit 26 | # 27 | # If Git is not found and no additional optional arguments are provided, then 28 | # only the `release: 1.2.3` line will be emitted for verbose mode. 29 | # 30 | # Finally, if the Git repository is not "clean", that is if it contains 31 | # uncommitted or modified files, a `-dirty` suffix will be added to the short 32 | # and long Git SHA refs to signal that the implementation may not perfectly 33 | # correspond to a SHA commit. 34 | # 35 | # * `@param [String]` program name 36 | # * `@param [String]` version string 37 | # * `@param [optional, String]` verbose mode set if value if `"true"` 38 | # * `@param [optional, String]` short Git SHA 39 | # * `@param [optional, String]` long Git SHA 40 | # * `@param [optional, String]` commit/version date 41 | # * `@stdout` version information 42 | # * `@return 0` if successful 43 | # 44 | # Note that the implementation for this function was inspired by Rust's [`cargo 45 | # version`](https://git.io/fjsOh). 46 | # 47 | # # Examples 48 | # 49 | # Basic usage: 50 | # 51 | # ```sh 52 | # print_version "my-program" "1.2.3" 53 | # ``` 54 | # 55 | # An optional third argument puts the function in verbose mode and more detail 56 | # is output to standard out: 57 | # 58 | # ```sh 59 | # print_version "my-program" "1.2.3" "true" 60 | # ``` 61 | # 62 | # An empty third argument is the same as only providing two arguments (i.e. 63 | # non-verbose): 64 | # 65 | # ```sh 66 | # print_version "my-program" "1.2.3" "" 67 | # ``` 68 | print_version() { 69 | local _program _version _verbose _sha _long_sha _date 70 | _program="$1" 71 | _version="$2" 72 | _verbose="${3:-false}" 73 | _sha="${4:-}" 74 | _long_sha="${5:-}" 75 | _date="${6:-}" 76 | 77 | if [ -z "$_sha" ] || [ -z "$_long_sha" ] || [ -z "$_date" ]; then 78 | if check_cmd git \ 79 | && git rev-parse --is-inside-work-tree >/dev/null 2>&1; then 80 | if [ -z "$_sha" ]; then 81 | _sha="$(git show -s --format=%h)" 82 | if ! git diff-index --quiet HEAD --; then 83 | _sha="${_sha}-dirty" 84 | fi 85 | fi 86 | if [ -z "$_long_sha" ]; then 87 | _long_sha="$(git show -s --format=%H)" 88 | case "$_sha" in 89 | *-dirty) _long_sha="${_long_sha}-dirty" ;; 90 | esac 91 | fi 92 | if [ -z "$_date" ]; then 93 | _date="$(git show -s --format=%ad --date=short)" 94 | fi 95 | fi 96 | fi 97 | 98 | if [ -n "$_sha" ] && [ -n "$_date" ]; then 99 | echo "$_program $_version ($_sha $_date)" 100 | else 101 | echo "$_program $_version" 102 | fi 103 | 104 | if [ "$_verbose" = "true" ]; then 105 | echo "release: $_version" 106 | if [ -n "$_long_sha" ]; then 107 | echo "commit-hash: $_long_sha" 108 | fi 109 | if [ -n "$_date" ]; then 110 | echo "commit-date: $_date" 111 | fi 112 | fi 113 | 114 | unset _program _version _verbose _sha _long_sha _date 115 | } 116 | -------------------------------------------------------------------------------- /lib/section.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Prints a section-delimiting header to standard out. 4 | # 5 | # * `@param [String]` section heading text 6 | # * `@stdout` section heading text 7 | # * `@return 0` if successful 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # section "Building project" 20 | # ``` 21 | section() { 22 | case "${TERM:-}" in 23 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 24 | printf -- "\033[1;36;40m--- \033[1;37;40m%s\033[0m\n" "$1" 25 | ;; 26 | *) 27 | printf -- "--- %s\n" "$1" 28 | ;; 29 | esac 30 | } 31 | -------------------------------------------------------------------------------- /lib/setup_cleanup_directories.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/mktemp_file.sh" 4 | 5 | # Sets up state to track directories for later cleanup in a trap handler. 6 | # 7 | # This function is typically used in combination with [`cleanup_directory`] and 8 | # [`trap_cleanup_directories`]. 9 | # 10 | # * `@return 0` if successful 11 | # * `@return 1` if a temp file could not be created 12 | # 13 | # # Global Variables 14 | # 15 | # * `__CLEANUP_DIRECTORIES__` used to track the collection of directories to 16 | # clean up whose value is a file. If not declared or set, this function will 17 | # set it up. 18 | # * `__CLEANUP_DIRECTORIES_SETUP__` used to track if the 19 | # `__CLEANUP_DIRECTORIES__` variable has been set up for the current process 20 | # 21 | # # Examples 22 | # 23 | # Basic usage: 24 | # 25 | # ```sh 26 | # setup_cleanup_directories 27 | # ``` 28 | # 29 | # Used with [`cleanup_directory`], [`setup_traps`], and 30 | # [`trap_cleanup_directories`]: 31 | # 32 | # ```sh 33 | # setup_cleanup_directories 34 | # setup_traps trap_cleanup_directories 35 | # 36 | # dir="$(mktemp_directory)" 37 | # cleanup_directory "$dir" 38 | # # do work on directory, etc. 39 | # ``` 40 | # 41 | # [`cleanup_file`]: #cleanup_file 42 | # [`setup_traps`]: #setup_traps 43 | # [`trap_cleanup_directories`]: #trap_cleanup_directories 44 | setup_cleanup_directories() { 45 | if [ "${__CLEANUP_DIRECTORIES_SETUP__:-}" != "$$" ]; then 46 | unset __CLEANUP_DIRECTORIES__ 47 | __CLEANUP_DIRECTORIES_SETUP__="$$" 48 | export __CLEANUP_DIRECTORIES_SETUP__ 49 | fi 50 | 51 | # If a tempfile hasn't been setup yet, create it 52 | if [ -z "${__CLEANUP_DIRECTORIES__:-}" ]; then 53 | __CLEANUP_DIRECTORIES__="$(mktemp_file)" 54 | 55 | # If the result string is empty, tempfile wasn't created so report failure 56 | if [ -z "$__CLEANUP_DIRECTORIES__" ]; then 57 | return 1 58 | fi 59 | 60 | export __CLEANUP_DIRECTORIES__ 61 | fi 62 | } 63 | -------------------------------------------------------------------------------- /lib/setup_cleanup_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/mktemp_file.sh" 4 | 5 | # Sets up state to track files for later cleanup in a trap handler. 6 | # 7 | # This function is typically used in combination with [`cleanup_file`] and 8 | # [`trap_cleanup_files`]. 9 | # 10 | # * `@return 0` if successful 11 | # * `@return 1` if a temp file could not be created 12 | # 13 | # # Global Variables 14 | # 15 | # * `__CLEANUP_FILES__` used to track the collection of files to clean up whose 16 | # value is a file. If not declared or set, this function will set it up. 17 | # * `__CLEANUP_FILES_SETUP__` used to track if the `__CLEANUP_FILES__` 18 | # variable has been set up for the current process 19 | # 20 | # # Examples 21 | # 22 | # Basic usage: 23 | # 24 | # ```sh 25 | # setup_cleanup_files 26 | # ``` 27 | # 28 | # Used with [`cleanup_file`], [`setup_traps`], and [`trap_cleanup_files`]: 29 | # 30 | # ```sh 31 | # setup_cleanup_files 32 | # setup_traps trap_cleanup_files 33 | # 34 | # file="$(mktemp_file)" 35 | # cleanup_file "$file" 36 | # # do work on file, etc. 37 | # ``` 38 | # 39 | # [`cleanup_file`]: #cleanup_file 40 | # [`setup_traps`]: #setup_traps 41 | # [`trap_cleanup_files`]: #trap_cleanup_files 42 | setup_cleanup_files() { 43 | if [ "${__CLEANUP_FILES_SETUP__:-}" != "$$" ]; then 44 | unset __CLEANUP_FILES__ 45 | __CLEANUP_FILES_SETUP__="$$" 46 | export __CLEANUP_FILES_SETUP__ 47 | fi 48 | 49 | # If a tempfile hasn't been setup yet, create it 50 | if [ -z "${__CLEANUP_FILES__:-}" ]; then 51 | __CLEANUP_FILES__="$(mktemp_file)" 52 | 53 | # If the result string is empty, tempfile wasn't created so report failure 54 | if [ -z "$__CLEANUP_FILES__" ]; then 55 | return 1 56 | fi 57 | 58 | export __CLEANUP_FILES__ 59 | fi 60 | } 61 | -------------------------------------------------------------------------------- /lib/setup_cleanups.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/setup_cleanup_directories.sh" 4 | . "lib/setup_cleanup_files.sh" 5 | 6 | # Sets up state to track files and directories for later cleanup in a trap 7 | # handler. 8 | # 9 | # This function is typically used in combination with [`cleanup_file`] and 10 | # [`cleanup_directory`] as well as [`trap_cleanups`]. 11 | # 12 | # * `@return 0` if successful 13 | # * `@return 1` if the setup was not successful 14 | # 15 | # # Examples 16 | # 17 | # Basic usage: 18 | # 19 | # ```sh 20 | # setup_cleanups 21 | # ``` 22 | # 23 | # Used with [`cleanup_directory`], [`cleanup_file`], [`setup_traps`], and 24 | # [`trap_cleanups`]: 25 | # 26 | # ```sh 27 | # setup_cleanups 28 | # setup_traps trap_cleanups 29 | # 30 | # file="$(mktemp_file)" 31 | # cleanup_file "$file" 32 | # # do work on file, etc. 33 | # 34 | # dir="$(mktemp_directory)" 35 | # cleanup_directory "$dir" 36 | # # do work on directory, etc. 37 | # ``` 38 | # 39 | # [`cleanup_directory`]: #cleanup_directory 40 | # [`cleanup_file`]: #cleanup_file 41 | # [`setup_traps`]: #setup_traps 42 | # [`trap_cleanups`]: #trap_cleanups 43 | setup_cleanups() { 44 | setup_cleanup_directories 45 | setup_cleanup_files 46 | } 47 | -------------------------------------------------------------------------------- /lib/setup_traps.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # Sets up traps for `EXIT` and common signals with the given cleanup function. 5 | # 6 | # In addition to `EXIT`, the `HUP`, `INT`, `QUIT`, `ALRM`, and `TERM` signals 7 | # are also covered. 8 | # 9 | # This implementation was based on a very nice, portable signal handling thread 10 | # thanks to an implementation on 11 | # [Stack Overflow](https://unix.stackexchange.com/a/240736). 12 | # 13 | # * `@param [String]` name of function to run with traps 14 | # 15 | # # Examples 16 | # 17 | # Basic usage with a simple "hello world" cleanup function: 18 | # 19 | # ```sh 20 | # hello_trap() { 21 | # echo "Hello, trap!" 22 | # } 23 | # 24 | # setup_traps hello_trap 25 | # ``` 26 | # 27 | # If the cleanup is simple enough to be a one-liner, you can provide the 28 | # command as the single argument: 29 | # 30 | # ```sh 31 | # setup_traps "echo 'Hello, World!'" 32 | # ``` 33 | setup_traps() { 34 | local _sig 35 | for _sig in HUP INT QUIT ALRM TERM; do 36 | trap " 37 | $1 38 | trap - $_sig EXIT 39 | kill -s $_sig "'"$$"' "$_sig" 40 | done 41 | 42 | if [ -n "${ZSH_VERSION:-}" ]; then 43 | # Zsh uses the `EXIT` trap for a function if declared in a function. 44 | # Instead, use the `zshexit()` hook function which targets the exiting of a 45 | # shell interpreter. Additionally, a function in Zsh is not a closure over 46 | # outer variables, so we'll use `eval` to construct the function body 47 | # containing the cleanup function to invoke. 48 | # 49 | # See: 50 | # * https://stackoverflow.com/a/22794374 51 | # * http://zsh.sourceforge.net/Doc/Release/Functions.html#Hook-Functions 52 | eval "zshexit() { eval '$1'; }" 53 | else 54 | # shellcheck disable=SC2064 55 | trap "$1" EXIT 56 | fi 57 | 58 | unset _sig 59 | } 60 | -------------------------------------------------------------------------------- /lib/trap_cleanup_directories.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # Removes any tracked directories registered via [`cleanup_directory`]. 5 | # 6 | # * `@return 0` whether or not an error has occurred 7 | # 8 | # [`cleanup_directory`]: #cleanup_directory 9 | # 10 | # # Global Variables 11 | # 12 | # * `__CLEANUP_DIRECTORIES__` used to track the collection of files to clean up 13 | # whose value is a file. If not declared or set, this function will assume 14 | # there is no work to do. 15 | # 16 | # # Examples 17 | # 18 | # Basic usage: 19 | # 20 | # ```sh 21 | # trap trap_cleanup_directories 1 2 3 15 ERR EXIT 22 | # 23 | # dir="$(mktemp_directory)" 24 | # cleanup_directory "$dir" 25 | # # do work on directory, etc. 26 | # ``` 27 | trap_cleanup_directories() { 28 | set +e 29 | 30 | if [ -n "${__CLEANUP_DIRECTORIES__:-}" ] \ 31 | && [ -f "$__CLEANUP_DIRECTORIES__" ]; then 32 | local _dir 33 | while read -r _dir; do 34 | rm -rf "$_dir" 35 | done <"$__CLEANUP_DIRECTORIES__" 36 | unset _dir 37 | rm -f "$__CLEANUP_DIRECTORIES__" 38 | fi 39 | } 40 | -------------------------------------------------------------------------------- /lib/trap_cleanup_files.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # Removes any tracked files registered via [`cleanup_file`]. 5 | # 6 | # * `@return 0` whether or not an error has occurred 7 | # 8 | # [`cleanup_file`]: #cleanup_file 9 | # 10 | # # Global Variables 11 | # 12 | # * `__CLEANUP_FILES__` used to track the collection of files to clean up whose 13 | # value is a file. If not declared or set, this function will assume there is 14 | # no work to do. 15 | # 16 | # # Examples 17 | # 18 | # Basic usage: 19 | # 20 | # ```sh 21 | # trap trap_cleanup_files 1 2 3 15 ERR EXIT 22 | # 23 | # file="$(mktemp_file)" 24 | # cleanup_file "$file" 25 | # # do work on file, etc. 26 | # ``` 27 | trap_cleanup_files() { 28 | set +e 29 | 30 | if [ -n "${__CLEANUP_FILES__:-}" ] && [ -f "$__CLEANUP_FILES__" ]; then 31 | local _file 32 | while read -r _file; do 33 | rm -f "$_file" 34 | done <"$__CLEANUP_FILES__" 35 | unset _file 36 | rm -f "$__CLEANUP_FILES__" 37 | fi 38 | } 39 | -------------------------------------------------------------------------------- /lib/trap_cleanups.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | . "lib/trap_cleanup_directories.sh" 4 | . "lib/trap_cleanup_files.sh" 5 | 6 | # Removes any tracked files and directories registered via [`cleanup_file`] 7 | # and [`cleanup_directory`] respectively. 8 | # 9 | # * `@return 0` whether or not an error has occurred 10 | # 11 | # [`cleanup_directory`]: #cleanup_directory 12 | # [`cleanup_file`]: #cleanup_file 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # trap trap_cleanups 1 2 3 15 ERR EXIT 20 | # ``` 21 | # 22 | # Used with [`setup_traps`]: 23 | # 24 | # ```sh 25 | # setup_traps trap_cleanups 26 | # ``` 27 | # 28 | # [`setup_traps`]: #setup_traps 29 | trap_cleanups() { 30 | set +e 31 | 32 | trap_cleanup_directories 33 | trap_cleanup_files 34 | } 35 | -------------------------------------------------------------------------------- /lib/warn.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # Prints a warning message to standard out. 4 | # 5 | # * `@param [String]` warning text 6 | # * `@stdout` warning heading text 7 | # * `@return 0` if successful 8 | # 9 | # # Environment Variables 10 | # 11 | # * `TERM` used to determine whether or not the terminal is capable of printing 12 | # colored output. 13 | # 14 | # # Examples 15 | # 16 | # Basic usage: 17 | # 18 | # ```sh 19 | # warn "Could not connect to service" 20 | # ``` 21 | warn() { 22 | case "${TERM:-}" in 23 | *term | alacritty | rxvt | screen | screen-* | tmux | tmux-* | xterm-*) 24 | printf -- "\033[1;31;40m!!! \033[1;37;40m%s\033[0m\n" "$1" 25 | ;; 26 | *) 27 | printf -- "!!! %s\n" "$1" 28 | ;; 29 | esac 30 | } 31 | -------------------------------------------------------------------------------- /support/compile-install.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env awk 2 | 3 | BEGIN { 4 | input = ARGV[1] 5 | distrib = ARGV[2] 6 | 7 | setup_vars(input) 8 | compile_source(input, distrib) 9 | } 10 | 11 | function compile_source(src, distrib, _skip) { 12 | while (getline 0) { 13 | if (/^# (BEGIN|INSERT): libsh.sh$/) { 14 | insert_libsh(distrib) 15 | if (/^# BEGIN: libsh.sh$/) { 16 | _skip = 1 17 | } 18 | } else if (_skip == 1 && /^# END: libsh.sh$/) { 19 | _skip = 0 20 | } else if (_skip == 0) { 21 | for (token in vars) { 22 | gsub(token, vars[token]) 23 | } 24 | print 25 | } 26 | } 27 | } 28 | 29 | function insert_libsh(distrib, _print) { 30 | while (getline 0) { 31 | if (/^# BEGIN: libsh.sh$/) { 32 | _print = 1 33 | print 34 | } else if (/^# END: libsh.sh$/) { 35 | _print = 0 36 | print 37 | } else if (_print == 1) { 38 | print 39 | } 40 | } 41 | } 42 | 43 | function setup_vars(distrib, _arr, _size) { 44 | if (getline <"VERSION.txt" > 0) { 45 | vars["@@version@@"] = $1 46 | } 47 | if (NIGHTLY_BUILD) { 48 | _size = split(vars["@@version@@"], _arr, "-") 49 | vars["@@version@@"] = _arr[1] "-nightly." NIGHTLY_BUILD 50 | } 51 | if ((("git " "show -s --format=%H") | getline) > 0) { 52 | vars["@@commit_hash@@"] = $1 53 | } 54 | if ((("git " "show -s --format=%h") | getline) > 0) { 55 | vars["@@commit_hash_short@@"] = $1 56 | } 57 | if ((("git " "show -s --format=%ad --date=short") | getline) > 0) { 58 | vars["@@commit_date@@"] = $1 59 | } 60 | _size = split(distrib, _arr, "/") 61 | vars["@@distrib@@"] = _arr[_size] 62 | } 63 | -------------------------------------------------------------------------------- /support/compile.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env awk 2 | 3 | BEGIN { 4 | distrib = ARGV[1] 5 | shebang = "#!/usr/bin/env sh" 6 | 7 | setup_vars(distrib) 8 | compile_source(distrib) 9 | } 10 | 11 | function compile_source(src) { 12 | find_sources(distrib) 13 | 14 | print_source("lib/_shebang.sh") 15 | print "" 16 | print "# BEGIN: libsh.sh" 17 | print "" 18 | inline_source("lib/_header") 19 | print "" 20 | inline_source(src) 21 | inline_source("lib/_ksh_local.sh") 22 | inline_sources() 23 | print "" 24 | print "# END: libsh.sh" 25 | } 26 | 27 | function find_sources(src) { 28 | while (getline 0) { 29 | if ($1 == ".") { 30 | gsub(/("|')/, "", $2) 31 | srcs[$2] = 1 32 | find_sources($2) 33 | } 34 | } 35 | close(src) 36 | } 37 | 38 | function inline_sources(_str, _arr, _size, _i) { 39 | for (_i in srcs) { 40 | _str = _str " " _i 41 | } 42 | _size = split(_str, _arr, " ") 43 | heapsort(_arr, _size) 44 | for (_i in _arr) { 45 | print "" 46 | inline_source(_arr[_i]) 47 | } 48 | } 49 | 50 | function inline_source(src, _skip, _src_line) { 51 | while (getline 0) { 52 | # Remove extra empty lines after source lines 53 | if (_src_line == 1) { 54 | if (/^$/) { 55 | continue 56 | } else { 57 | _src_line = 0 58 | } 59 | } 60 | # Remove multiple contiguous empty lines 61 | if (_empty_line == 1) { 62 | if (/^$/) { 63 | continue 64 | } else { 65 | _empty_line = 0 66 | } 67 | } 68 | if (/^$/) { 69 | _empty_line = 1 70 | } 71 | 72 | if (_skip == 1) { 73 | # Skip lines until the next empty line is found 74 | if (/^$/) { 75 | _skip = 0 76 | } 77 | continue 78 | } else if ($0 == shebang) { 79 | # Start skipping lines once the shebang line is found 80 | _skip = 1 81 | continue 82 | } else if ($1 == ".") { 83 | # Start skipping source lines and any trailing empty lines 84 | _src_line = 1 85 | continue 86 | } else { 87 | # Otherwise print the line 88 | for (token in vars) { 89 | gsub(token, vars[token]) 90 | } 91 | print 92 | } 93 | } 94 | close(src) 95 | } 96 | 97 | function print_source(src) { 98 | while (getline 0) { 99 | for (token in vars) { 100 | gsub(token, vars[token]) 101 | } 102 | print 103 | } 104 | close(src) 105 | } 106 | 107 | function setup_vars(distrib, _arr, _size) { 108 | if (getline <"VERSION.txt" > 0) { 109 | vars["@@version@@"] = $1 110 | } 111 | if (NIGHTLY_BUILD) { 112 | _size = split(vars["@@version@@"], _arr, "-") 113 | vars["@@version@@"] = _arr[1] "-nightly." NIGHTLY_BUILD 114 | } 115 | if ((("git " "show -s --format=%H") | getline) > 0) { 116 | vars["@@commit_hash@@"] = $1 117 | } 118 | if ((("git " "show -s --format=%ad --date=short") | getline) > 0) { 119 | vars["@@commit_date@@"] = $1 120 | } 121 | _size = split(distrib, _arr, "/") 122 | vars["@@distrib@@"] = _arr[_size] 123 | } 124 | 125 | function heapsort(arr, size, _i) { 126 | for (_i = int(size / 2); _i >= 1; _i--) { 127 | heapify(arr, _i, size) 128 | } 129 | for (_i = size; _i > 1; _i--) { 130 | { swap(arr, 1, _i) } 131 | { heapify(arr, 1, _i - 1) } 132 | } 133 | } 134 | 135 | function heapify(arr, left, right, _p, _c) { 136 | for (_p = left; (_c = 2 * _p) <= right; _p = _c) { 137 | if (_c < right && arr[_c + 1] > arr[_c]) { 138 | _c++ 139 | } 140 | 141 | if (arr[_p] < arr[_c]) { 142 | swap(arr, _c, _p) 143 | } 144 | } 145 | } 146 | 147 | function swap(arr, i, j, _t) { 148 | _t = arr[i] 149 | arr[i] = arr[j] 150 | arr[j] = _t 151 | } 152 | -------------------------------------------------------------------------------- /support/minify.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env awk 2 | 3 | $0 == "# BEGIN: libsh.sh" { 4 | begin = 1 5 | } 6 | /^# distribution: / { 7 | gsub(/\.sh/, "-minified.sh") 8 | } 9 | begin == 1 && /^$/ { 10 | begin = 0 11 | notice = 1 12 | print 13 | next 14 | } 15 | notice == 1 && /^$/ { 16 | notice = 0 17 | stripcomment = 1 18 | next 19 | } 20 | stripcomment == 1 && /^\s*#\s+shellcheck\s+/ { 21 | print 22 | next 23 | } 24 | stripcomment == 1 && /^# END: libsh.sh$/ { 25 | print "" 26 | print 27 | next 28 | } 29 | stripcomment == 1 && (/^\s*#/ || /^$/) { 30 | next 31 | } 32 | { 33 | print 34 | } 35 | -------------------------------------------------------------------------------- /support/sources.awk: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env awk 2 | 3 | BEGIN { 4 | distrib = ARGV[1] 5 | 6 | find_sources(distrib) 7 | for (src in srcs) { 8 | print src 9 | } 10 | } 11 | 12 | function find_sources(src) { 13 | while (getline 0) { 14 | if ($1 == ".") { 15 | gsub(/("|')/, "", $2) 16 | srcs[$2] = 1 17 | find_sources($2) 18 | } 19 | } 20 | close(src) 21 | } 22 | -------------------------------------------------------------------------------- /tests/check_cmd_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # shellcheck source=tests/test_helpers.sh 4 | . "${0%/*}/test_helpers.sh" 5 | 6 | . "${0%/*}/../lib/_ksh_local.sh" 7 | 8 | . "${SRC:=lib/check_cmd.sh}" 9 | 10 | oneTimeSetUp() { 11 | commonOneTimeSetUp 12 | } 13 | 14 | setUp() { 15 | commonSetUp 16 | } 17 | 18 | testCheckCmdPresent() { 19 | # The `ls` command should almost always be in `$PATH` as a program 20 | run check_cmd ls 21 | 22 | assertTrue 'check_cmd failed' "$return_status" 23 | assertStdoutNull 24 | assertStderrNull 25 | } 26 | 27 | testCheckCmdMissing() { 28 | run check_cmd __not_a_great_chance_this_will_exist__ 29 | 30 | assertFalse 'check_cmd succeeded' "$return_status" 31 | assertStdoutNull 32 | assertStderrNull 33 | } 34 | 35 | shell_compat "$0" 36 | 37 | . "$shunit2" 38 | -------------------------------------------------------------------------------- /tests/cleanup_directory_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/cleanup_directory.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testCleanupDirectory() { 20 | local directory 21 | __CLEANUP_DIRECTORIES__="$(mktemp_file)" 22 | directory="$tmppath/testCleanupDirectory" 23 | mkdir -p "$directory" 24 | run cleanup_directory "$directory" 25 | 26 | assertTrue 'cleanup_directory failed' "$return_status" 27 | assertEquals "$(cat "$__CLEANUP_DIRECTORIES__")" "$directory" 28 | assertTrue 'directories could not be removed' \ 29 | "rm -rf '$__CLEANUP_DIRECTORIES__' '$directory'" 30 | 31 | assertStdoutNull 32 | assertStderrNull 33 | 34 | unset directory 35 | } 36 | 37 | testCleanupDirectoryNoVar() { 38 | local directory 39 | unset __CLEANUP_DIRECTORIES__ 40 | directory="$tmppath/testCleanupDirectoryNoVar" 41 | mkdir -p "$directory" 42 | run cleanup_directory "$directory" 43 | 44 | assertTrue 'cleanup_directory failed' "$return_status" 45 | assertEquals "$(cat "$__CLEANUP_DIRECTORIES__")" "$directory" 46 | assertTrue 'directories could not be removed' \ 47 | "rm -rf '$__CLEANUP_DIRECTORIES__' '$directory'" 48 | 49 | assertStdoutNull 50 | assertStderrNull 51 | 52 | unset directory 53 | } 54 | 55 | shell_compat "$0" 56 | 57 | . "$shunit2" 58 | -------------------------------------------------------------------------------- /tests/cleanup_file_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/cleanup_file.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testCleanupFile() { 20 | local file 21 | __CLEANUP_FILES__="$(mktemp_file)" 22 | file="$(mktemp_file)" 23 | run cleanup_file "$file" 24 | 25 | assertTrue 'cleanup_file failed' "$return_status" 26 | assertEquals "$(cat "$__CLEANUP_FILES__")" "$file" 27 | assertTrue 'files could not be removed' "rm '$__CLEANUP_FILES__' '$file'" 28 | 29 | assertStdoutNull 30 | assertStderrNull 31 | 32 | unset file 33 | } 34 | 35 | testCleanupFileNoVar() { 36 | local file 37 | unset __CLEANUP_FILES__ 38 | file="$(mktemp_file)" 39 | run cleanup_file "$file" 40 | 41 | assertTrue 'cleanup_file failed' "$return_status" 42 | assertEquals "$(cat "$__CLEANUP_FILES__")" "$file" 43 | assertTrue 'files could not be removed' "rm '$__CLEANUP_FILES__' '$file'" 44 | 45 | assertStdoutNull 46 | assertStderrNull 47 | 48 | unset file 49 | } 50 | 51 | shell_compat "$0" 52 | 53 | . "$shunit2" 54 | -------------------------------------------------------------------------------- /tests/die_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/die.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testDieStripAnsi() { 20 | printf -- '\nxxx I give up\n\n' >"$expected" 21 | run_with_sh_script die 'I give up' 22 | 23 | stripAnsi <"$stderr" >"$actual" 24 | 25 | assertFalse 'fail did not fail' "$return_status" 26 | # Shell string equals has issues trailing newlines, so let's use `cmp` to 27 | # compare byte by byte 28 | assertTrue 'ANSI stderr not equal' "cmp '$expected' '$actual'" 29 | assertStdoutNull 30 | } 31 | 32 | testDieAnsi() { 33 | printf -- '\n\033[1;31;40mxxx \033[1;37;40mI give up\033[0m\n\n' >"$expected" 34 | export TERM=xterm 35 | run_with_sh_script die 'I give up' 36 | 37 | assertFalse 'fail did not fail' "$return_status" 38 | # Shell string equals has issues with ANSI escapes, so let's use `cmp` to 39 | # compare byte by byte 40 | assertTrue 'ANSI stderr not equal' "cmp '$expected' '$stderr'" 41 | assertStdoutNull 42 | } 43 | 44 | shell_compat "$0" 45 | 46 | . "$shunit2" 47 | -------------------------------------------------------------------------------- /tests/download_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/download.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testDownloadReal() { 20 | local url 21 | # a stable, generally available, consistent, and small file to download 22 | url="https://raw.githubusercontent.com/fnichol/libsh/636e5de/.gitignore" 23 | printf -- "tmp/\n" >"$expected" 24 | 25 | run download "$url" "$actual" 26 | 27 | assertTrue 'download failed' "$return_status" 28 | assertStderrNull 29 | assertStdoutStripAnsiContains "Downloading $url to $actual" 30 | assertEquals "content isn't equal" "$(cat "$expected")" "$(cat "$actual")" 31 | } 32 | 33 | testDownloadFakeSucceedingCurl() { 34 | local url file 35 | url="http://example.com" 36 | file="/not/important" 37 | 38 | isolatedPathFor sh grep sed printf 39 | touch "$isolated_path/curl" 40 | chmod 0755 "$isolated_path/curl" 41 | { 42 | echo '#!/usr/bin/env sh' 43 | echo 'echo "curl $@" >&2' 44 | echo 'exit 0' 45 | } >>"$isolated_path/curl" 46 | 47 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 48 | export PATH="$isolated_path" 49 | run download "$url" "$file" 50 | # Restore `PATH` 51 | export PATH="$__ORIG_PATH" 52 | 53 | assertTrue 'download failed' "$return_status" 54 | assertStdoutStripAnsiContains "Downloading $url to $file" 55 | assertStdoutStripAnsiContains "curl" 56 | assertStderrEquals "curl -sSfL $url -o $file" 57 | } 58 | 59 | testDownloadFakeFailingCurlWithFakeSucceedingWget() { 60 | local url file 61 | url="http://example.com" 62 | file="/not/important" 63 | 64 | isolatedPathFor sh grep sed printf 65 | touch "$isolated_path/curl" 66 | chmod 0755 "$isolated_path/curl" 67 | { 68 | echo '#!/usr/bin/env sh' 69 | echo 'exit 1' 70 | } >>"$isolated_path/curl" 71 | touch "$isolated_path/wget" 72 | chmod 0755 "$isolated_path/wget" 73 | { 74 | echo '#!/usr/bin/env sh' 75 | echo 'echo "wget $@" >&2' 76 | echo 'exit 0' 77 | } >>"$isolated_path/wget" 78 | 79 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 80 | export PATH="$isolated_path" 81 | run download "$url" "$file" 82 | # Restore `PATH` 83 | export PATH="$__ORIG_PATH" 84 | 85 | assertTrue 'download failed' "$return_status" 86 | assertStdoutStripAnsiContains "Downloading $url to $file" 87 | assertStdoutStripAnsiContains "curl failed to download file" 88 | assertStdoutStripAnsiContains "wget" 89 | assertStderrEquals "wget -q -O $file $url" 90 | } 91 | 92 | testDownloadFakeSucceedingWget() { 93 | local url file 94 | url="http://example.com" 95 | file="/not/important" 96 | 97 | isolatedPathFor sh grep sed printf 98 | touch "$isolated_path/wget" 99 | chmod 0755 "$isolated_path/wget" 100 | { 101 | echo '#!/usr/bin/env sh' 102 | echo 'echo "wget $@" >&2' 103 | echo 'exit 0' 104 | } >>"$isolated_path/wget" 105 | 106 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 107 | export PATH="$isolated_path" 108 | run download "$url" "$file" 109 | # Restore `PATH` 110 | export PATH="$__ORIG_PATH" 111 | 112 | assertTrue 'download failed' "$return_status" 113 | assertStdoutStripAnsiContains "Downloading $url to $file" 114 | assertStdoutStripAnsiContains "wget" 115 | assertStderrEquals "wget -q -O $file $url" 116 | } 117 | 118 | testDownloadFakeFailingCurlWithFakeFailingWgetWithFakeSucceedingFtp() { 119 | local url file 120 | url="http://example.com" 121 | file="/not/important" 122 | 123 | isolatedPathFor sh grep sed printf 124 | touch "$isolated_path/curl" 125 | chmod 0755 "$isolated_path/curl" 126 | { 127 | echo '#!/usr/bin/env sh' 128 | echo 'exit 1' 129 | } >>"$isolated_path/curl" 130 | touch "$isolated_path/wget" 131 | chmod 0755 "$isolated_path/wget" 132 | { 133 | echo '#!/usr/bin/env sh' 134 | echo 'exit 1' 135 | } >>"$isolated_path/wget" 136 | touch "$isolated_path/ftp" 137 | chmod 0755 "$isolated_path/ftp" 138 | { 139 | echo '#!/usr/bin/env sh' 140 | echo 'echo "ftp $@" >&2' 141 | echo 'exit 0' 142 | } >>"$isolated_path/ftp" 143 | 144 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 145 | export PATH="$isolated_path" 146 | run download "$url" "$file" 147 | # Restore `PATH` 148 | export PATH="$__ORIG_PATH" 149 | 150 | assertTrue 'download failed' "$return_status" 151 | assertStdoutStripAnsiContains "Downloading $url to $file" 152 | assertStdoutStripAnsiContains "curl failed to download file" 153 | assertStdoutStripAnsiContains "wget failed to download file" 154 | assertStdoutStripAnsiContains "ftp" 155 | assertStderrEquals "ftp -o $file $url" 156 | } 157 | 158 | testDownloadFakeFailingCurlWithFakeFailingWgetWithFakeFailingFtp() { 159 | local url file 160 | url="http://example.com" 161 | file="/not/important" 162 | 163 | isolatedPathFor sh grep sed printf 164 | touch "$isolated_path/curl" 165 | chmod 0755 "$isolated_path/curl" 166 | { 167 | echo '#!/usr/bin/env sh' 168 | echo 'exit 1' 169 | } >>"$isolated_path/curl" 170 | touch "$isolated_path/wget" 171 | chmod 0755 "$isolated_path/wget" 172 | { 173 | echo '#!/usr/bin/env sh' 174 | echo 'exit 1' 175 | } >>"$isolated_path/wget" 176 | touch "$isolated_path/ftp" 177 | chmod 0755 "$isolated_path/ftp" 178 | { 179 | echo '#!/usr/bin/env sh' 180 | echo 'exit 1' 181 | } >>"$isolated_path/ftp" 182 | 183 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 184 | export PATH="$isolated_path" 185 | run download "$url" "$file" 186 | # Restore `PATH` 187 | export PATH="$__ORIG_PATH" 188 | 189 | assertFalse 'download succeeded' "$return_status" 190 | assertStdoutStripAnsiContains "Downloading $url to $file" 191 | assertStdoutStripAnsiContains "curl failed to download file" 192 | assertStdoutStripAnsiContains "wget failed to download file" 193 | assertStdoutStripAnsiContains "ftp failed to download file" 194 | assertStdoutStripAnsiContains "Downloading requires SSL-enabled" 195 | assertStderrNull 196 | } 197 | 198 | testDownloadFakeSucceedingFtp() { 199 | local url file 200 | url="http://example.com" 201 | file="/not/important" 202 | 203 | isolatedPathFor sh grep sed printf 204 | touch "$isolated_path/ftp" 205 | chmod 0755 "$isolated_path/ftp" 206 | { 207 | echo '#!/usr/bin/env sh' 208 | echo 'echo "ftp $@" >&2' 209 | echo 'exit 0' 210 | } >>"$isolated_path/ftp" 211 | 212 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 213 | export PATH="$isolated_path" 214 | run download "$url" "$file" 215 | # Restore `PATH` 216 | export PATH="$__ORIG_PATH" 217 | 218 | assertTrue 'download failed' "$return_status" 219 | assertStdoutStripAnsiContains "Downloading $url to $file" 220 | assertStdoutStripAnsiContains "ftp" 221 | assertStderrEquals "ftp -o $file $url" 222 | } 223 | 224 | testDownloadNoWgetOrCurlOrFtp() { 225 | isolatedPathFor grep sed printf 226 | # Clear `PATH` so the real `curl`, `wget`, and `ftp` programs cannot be found 227 | export PATH="$isolated_path" 228 | run download "http://example.com" "/not/important" 229 | # Restore `PATH` 230 | export PATH="$__ORIG_PATH" 231 | 232 | assertFalse 'download succeeded' "$return_status" 233 | assertStdoutStripAnsiContains "Downloading requires SSL-enabled" 234 | assertStderrNull 235 | } 236 | 237 | shell_compat "$0" 238 | 239 | . "$shunit2" 240 | -------------------------------------------------------------------------------- /tests/indent_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/indent.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testIndent() { 20 | run indent echo 'hello, world' 21 | 22 | assertTrue 'indent failed' "$return_status" 23 | assertStdoutStripAnsiEquals ' hello, world' 24 | assertStderrNull 25 | } 26 | 27 | testIndentPropagatesCommandExitCode() { 28 | touch "$tmppath/exiter" 29 | chmod 0755 "$tmppath/exiter" 30 | { 31 | echo '#!/usr/bin/env sh' 32 | echo 'exit 19' 33 | } >>"$tmppath/exiter" 34 | 35 | run indent "$tmppath/exiter" 36 | 37 | assertEquals "exit code isn't equal" "19" "$return_status" 38 | } 39 | 40 | testIndentCombinesOutputStreams() { 41 | printf -- ' stdout1\n' >"$expected" 42 | printf -- ' stderr1\n' >>"$expected" 43 | printf -- ' stdout2\n' >>"$expected" 44 | touch "$tmppath/streamer" 45 | chmod 0755 "$tmppath/streamer" 46 | { 47 | echo '#!/usr/bin/env sh' 48 | echo 'echo "stdout1"' 49 | echo 'echo "stderr1" >&2' 50 | echo 'echo "stdout2"' 51 | echo 'exit 0' 52 | } >>"$tmppath/streamer" 53 | 54 | run indent "$tmppath/streamer" 55 | 56 | assertTrue 'indent failed' "$return_status" 57 | # Shell string equals has issues trailing newlines, so let's use `cmp` to 58 | # compare byte by byte 59 | assertTrue 'stdout not equal' "cmp '$expected' '$stdout'" 60 | assertStderrNull 61 | } 62 | 63 | shell_compat "$0" 64 | 65 | . "$shunit2" 66 | -------------------------------------------------------------------------------- /tests/info_end_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/info_end.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testInfoEndStripAnsi() { 20 | run info_end 21 | 22 | assertTrue 'info_end failed' "$return_status" 23 | assertStdoutStripAnsiEquals 'done.' 24 | assertStderrNull 25 | } 26 | 27 | testInfoEndAnsi() { 28 | printf -- '\033[1;37;40mdone.\033[0m\n' \ 29 | >"$expected" 30 | export TERM=xterm 31 | run info_end 32 | 33 | assertTrue 'info_end failed' "$return_status" 34 | # Shell string equals has issues with ANSI escapes, so let's use $(cmp) to 35 | # compare byte by byte 36 | assertTrue 'ANSI stdout not equal' "cmp '$expected' '$stdout'" 37 | assertStderrNull 38 | } 39 | 40 | shell_compat "$0" 41 | 42 | . "$shunit2" 43 | -------------------------------------------------------------------------------- /tests/info_start_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/info_start.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testInfoStartStripAnsi() { 20 | run info_start 'something is happening' 21 | 22 | assertTrue 'info_start failed' "$return_status" 23 | assertStdoutStripAnsiEquals ' - something is happening ... ' 24 | assertStderrNull 25 | } 26 | 27 | testInfoStartAnsi() { 28 | printf -- '\033[1;36;40m - \033[1;37;40msomething ... \033[0m' \ 29 | >"$expected" 30 | export TERM=xterm 31 | run info_start 'something' 32 | 33 | assertTrue 'info_start failed' "$return_status" 34 | # Shell string equals has issues with ANSI escapes, so let's use $(cmp) to 35 | # compare byte by byte 36 | assertTrue 'ANSI stdout not equal' "cmp '$expected' '$stdout'" 37 | assertStderrNull 38 | } 39 | 40 | shell_compat "$0" 41 | 42 | . "$shunit2" 43 | -------------------------------------------------------------------------------- /tests/info_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/info.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testInfoStripAnsi() { 20 | run info 'something is happening' 21 | 22 | assertTrue 'info failed' "$return_status" 23 | assertStdoutStripAnsiEquals ' - something is happening' 24 | assertStderrNull 25 | } 26 | 27 | testInfoAnsi() { 28 | printf -- '\033[1;36;40m - \033[1;37;40msomething is happening\033[0m\n' \ 29 | >"$expected" 30 | export TERM=xterm 31 | run info 'something is happening' 32 | 33 | assertTrue 'info failed' "$return_status" 34 | # Shell string equals has issues with ANSI escapes, so let's use $(cmp) to 35 | # compare byte by byte 36 | assertTrue 'ANSI stdout not equal' "cmp '$expected' '$stdout'" 37 | assertStderrNull 38 | } 39 | 40 | shell_compat "$0" 41 | 42 | . "$shunit2" 43 | -------------------------------------------------------------------------------- /tests/mktemp_directory_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/mktemp_directory.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testMktempDirectory() { 20 | run mktemp_directory 21 | 22 | assertTrue 'mktemp_directory failed' "$return_status" 23 | assertTrue 'result is not a directory' "[ -d '$(cat "$stdout")' ]" 24 | assertStderrNull 25 | assertTrue 'temp directory cannot be removed' "rmdir $(cat "$stdout")" 26 | } 27 | 28 | testMktempDirectoryParentDir() { 29 | run mktemp_directory "$tmppath" 30 | 31 | assertTrue 'mktemp_directory failed' "$return_status" 32 | assertTrue 'result is not a directory' "[ -d '$(cat "$stdout")' ]" 33 | assertTrue 'parent dir not is tmppath' \ 34 | "[ '$(dirname "$stdout")' = '$tmppath' ]" 35 | assertStderrNull 36 | assertTrue 'temp directory cannot be removed' "rmdir $(cat "$stdout")" 37 | } 38 | 39 | shell_compat "$0" 40 | 41 | . "$shunit2" 42 | -------------------------------------------------------------------------------- /tests/mktemp_file_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/mktemp_file.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testMktempFile() { 20 | run mktemp_file 21 | 22 | assertTrue 'mktemp_file failed' "$return_status" 23 | assertTrue 'result is not a file' "[ -f '$(cat "$stdout")' ]" 24 | assertStderrNull 25 | assertTrue 'temp file cannot be removed' "rm $(cat "$stdout")" 26 | } 27 | 28 | testMktempFileParentDir() { 29 | run mktemp_file "$tmppath" 30 | 31 | assertTrue 'mktemp_file failed' "$return_status" 32 | assertTrue 'result is not a file' "[ -f '$(cat "$stdout")' ]" 33 | assertTrue 'parent dir not is tmppath' \ 34 | "[ '$(dirname "$stdout")' = '$tmppath' ]" 35 | assertStderrNull 36 | assertTrue 'temp file cannot be removed' "rm $(cat "$stdout")" 37 | } 38 | 39 | shell_compat "$0" 40 | 41 | . "$shunit2" 42 | -------------------------------------------------------------------------------- /tests/need_cmd_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/need_cmd.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testNeedCmdPresent() { 20 | # The `ls` command should almost always be in `$PATH` as a program 21 | run need_cmd ls 22 | 23 | assertTrue 'need_cmd failed' "$return_status" 24 | assertStdoutNull 25 | assertStderrNull 26 | } 27 | 28 | testNeedCmdMissing() { 29 | run_with_sh_script need_cmd __not_a_great_chance_this_will_exist__ 30 | 31 | assertFalse 'need_cmd succeeded' "$return_status" 32 | assertStderrStripAnsiContains 'xxx Required command' 33 | assertStderrStripAnsiContains 'not found on PATH' 34 | assertStdoutNull 35 | } 36 | 37 | shell_compat "$0" 38 | 39 | . "$shunit2" 40 | -------------------------------------------------------------------------------- /tests/print_version_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/print_version.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testPrintVersionDefaultWithGit() { 20 | createGitRepo repo 21 | run print_version "cool" "1.2.3" 22 | 23 | assertStdoutEquals "cool 1.2.3 ($short_sha 2000-01-02)" 24 | assertStderrNull 25 | } 26 | 27 | testPrintVersionVerboseWithGit() { 28 | createGitRepo repo 29 | run print_version "cool" "1.2.3" "true" 30 | 31 | assertStdoutEquals "cool 1.2.3 ($short_sha 2000-01-02) 32 | release: 1.2.3 33 | commit-hash: $long_sha 34 | commit-date: 2000-01-02" 35 | assertStderrNull 36 | } 37 | 38 | testPrintVersionExplicitNonverboseWithGit() { 39 | createGitRepo repo 40 | run print_version "cool" "1.2.3" "" # setting non-verbose with empty arg 41 | 42 | assertStdoutEquals "cool 1.2.3 ($short_sha 2000-01-02)" 43 | assertStderrNull 44 | } 45 | 46 | testPrintVersionNoGitRepoWithGit() { 47 | local dir 48 | dir="$tmppath/testPrintVersionNoGitRepo" 49 | rm -f "$dir" 50 | mkdir -p "$dir" 51 | cd "$dir" || return 1 52 | run print_version "cool" "1.2.3" 53 | 54 | assertStdoutEquals "cool 1.2.3" 55 | assertStderrNull 56 | cd - >/dev/null || return 1 57 | rm -rf "$dir" 58 | 59 | unset dir 60 | } 61 | 62 | testPrintVersionDirtyWithGit() { 63 | createGitRepo repo 64 | echo 'Uh oh' >README.md 65 | run print_version "cool" "1.2.3" 66 | 67 | assertStdoutEquals "cool 1.2.3 (${short_sha}-dirty 2000-01-02)" 68 | assertStderrNull 69 | } 70 | 71 | testPrintVersionDirtyVerboseWithGit() { 72 | createGitRepo repo 73 | echo 'Uh oh' >README.md 74 | run print_version "cool" "1.2.3" "true" 75 | 76 | assertStdoutEquals "cool 1.2.3 (${short_sha}-dirty 2000-01-02) 77 | release: 1.2.3 78 | commit-hash: ${long_sha}-dirty 79 | commit-date: 2000-01-02" 80 | assertStderrNull 81 | } 82 | 83 | testPrintVersionNoGit() { 84 | createGitRepo repo 85 | # Save full path to `grep` 86 | GREP="$(command -v grep)" 87 | export GREP 88 | # Temporarily clear PATH so the `git` program cannot be found 89 | export PATH="" 90 | run print_version "cool" "1.2.3" 91 | # Restore PATH and unset GREP 92 | export PATH="$__ORIG_PATH" 93 | unset GREP 94 | 95 | assertStdoutEquals "cool 1.2.3" 96 | assertStderrNull 97 | } 98 | 99 | testPrintVersionVerboseNoGit() { 100 | createGitRepo repo 101 | # Save full path to `grep` 102 | GREP="$(command -v grep)" 103 | export GREP 104 | # Temporarily clear PATH so the `git` program cannot be found 105 | export PATH="" 106 | run print_version "cool" "1.2.3" "true" 107 | # Restore PATH and unset GREP 108 | export PATH="$__ORIG_PATH" 109 | unset GREP 110 | 111 | assertStdoutEquals "cool 1.2.3 112 | release: 1.2.3" 113 | assertStderrNull 114 | } 115 | 116 | testPrintVersionNoGitWithShaAndDate() { 117 | createGitRepo repo 118 | # Save full path to `grep` 119 | GREP="$(command -v grep)" 120 | export GREP 121 | # Temporarily clear PATH so the `git` program cannot be found 122 | export PATH="" 123 | run print_version "cool" "1.2.3" \ 124 | "false" "abcshort" "abclong" "2000-01-02" 125 | # Restore PATH and unset GREP 126 | export PATH="$__ORIG_PATH" 127 | unset GREP 128 | 129 | assertStdoutEquals "cool 1.2.3 (abcshort 2000-01-02)" 130 | assertStderrNull 131 | } 132 | 133 | testPrintVersionVerboseNoGitWithShaAndDate() { 134 | createGitRepo repo 135 | # Save full path to `grep` 136 | GREP="$(command -v grep)" 137 | export GREP 138 | # Temporarily clear PATH so the `git` program cannot be found 139 | export PATH="" 140 | run print_version "cool" "1.2.3" \ 141 | "true" "abcshort" "abclong" "2000-01-02" 142 | # Restore PATH and unset GREP 143 | export PATH="$__ORIG_PATH" 144 | unset GREP 145 | 146 | assertStdoutEquals "cool 1.2.3 (abcshort 2000-01-02) 147 | release: 1.2.3 148 | commit-hash: abclong 149 | commit-date: 2000-01-02" 150 | assertStderrNull 151 | } 152 | 153 | testPrintVersionWithGitAndShaAndDate() { 154 | createGitRepo repo 155 | run print_version "cool" "1.2.3" \ 156 | "false" "abcshort" "abclong" "2000-01-02" 157 | 158 | assertStdoutEquals "cool 1.2.3 (abcshort 2000-01-02)" 159 | assertStderrNull 160 | } 161 | 162 | testPrintVersionVerboseWithGiAndShaAndDate() { 163 | createGitRepo repo 164 | run print_version "cool" "1.2.3" \ 165 | "true" "abcshort" "abclong" "2000-01-02" 166 | 167 | assertStdoutEquals "cool 1.2.3 (abcshort 2000-01-02) 168 | release: 1.2.3 169 | commit-hash: abclong 170 | commit-date: 2000-01-02" 171 | assertStderrNull 172 | } 173 | 174 | createGitRepo() { 175 | rm -rf "tmppath/$1" 176 | git init --quiet "$tmppath/$1" 177 | cd "$tmppath/$1" || return 1 178 | touch README.md 179 | git config user.name "Nobody" 180 | git config user.email "nobody@example.com" 181 | git add README.md 182 | git commit --quiet --message='First commit' --date=2000-01-02T03:04:05 183 | 184 | short_sha="$(git show -s --format=%h)" 185 | long_sha="$(git show -s --format=%H)" 186 | } 187 | 188 | shell_compat "$0" 189 | 190 | . "$shunit2" 191 | -------------------------------------------------------------------------------- /tests/section_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/section.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testSectionStripAnsi() { 20 | run section 'hello there' 21 | 22 | assertTrue 'section failed' "$return_status" 23 | assertStdoutStripAnsiEquals '--- hello there' 24 | assertStderrNull 25 | } 26 | 27 | testSectionAnsi() { 28 | printf -- '\033[1;36;40m--- \033[1;37;40mhello there\033[0m\n' >"$expected" 29 | export TERM=xterm 30 | run section 'hello there' 31 | 32 | assertTrue 'section failed' "$return_status" 33 | # Shell string equals has issues with ANSI escapes, so let's use $(cmp) to 34 | # compare byte by byte 35 | assertTrue 'ANSI stdout not equal' "cmp '$expected' '$stdout'" 36 | assertStderrNull 37 | } 38 | 39 | shell_compat "$0" 40 | 41 | . "$shunit2" 42 | -------------------------------------------------------------------------------- /tests/setup_cleanup_directories_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/setup_cleanup_directories.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testSetupCleanupDirectories() { 20 | unset __CLEANUP_DIRECTORIES__ 21 | run setup_cleanup_directories 22 | 23 | assertTrue 'setup_cleanup_directories failed' "$return_status" 24 | assertStdoutNull 25 | assertStderrNull 26 | assertTrue 'cleanup directories does not exist' \ 27 | "[ -f '$__CLEANUP_DIRECTORIES__' ]" 28 | } 29 | 30 | testSetupCleanupFilesWithVarSet() { 31 | local original_value 32 | original_value="$tmppath/testSetupCleanupDirectories.cleanup" 33 | __CLEANUP_DIRECTORIES__="$original_value" 34 | run setup_cleanup_directories 35 | 36 | assertTrue 'setup_cleanup_directories failed' "$return_status" 37 | assertStdoutNull 38 | assertStderrNull 39 | assertEquals 'clean directories variable changed' \ 40 | "$original_value" "$__CLEANUP_DIRECTORIES__" 41 | } 42 | 43 | shell_compat "$0" 44 | 45 | . "$shunit2" 46 | -------------------------------------------------------------------------------- /tests/setup_cleanup_files_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/setup_cleanup_files.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testSetupCleanupFiles() { 20 | unset __CLEANUP_FILES__ 21 | run setup_cleanup_files 22 | 23 | assertTrue 'setup_cleanup_files failed' "$return_status" 24 | assertStdoutNull 25 | assertStderrNull 26 | assertTrue 'cleanup files does not exist' "[ -f '$__CLEANUP_FILES__' ]" 27 | } 28 | 29 | testSetupCleanupFilesWithVarSet() { 30 | local original_value 31 | original_value="$tmppath/testSetupCleanupFiles.cleanup" 32 | __CLEANUP_FILES__="$original_value" 33 | run setup_cleanup_files 34 | 35 | assertTrue 'setup_cleanup_files failed' "$return_status" 36 | assertStdoutNull 37 | assertStderrNull 38 | assertEquals 'clean files variable changed' \ 39 | "$original_value" "$__CLEANUP_FILES__" 40 | } 41 | 42 | shell_compat "$0" 43 | 44 | . "$shunit2" 45 | -------------------------------------------------------------------------------- /tests/setup_cleanups_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/setup_cleanups.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testSetupCleanups() { 20 | unset __CLEANUP_FILES__ 21 | unset __CLEANUP_DIRECTORIES__ 22 | run setup_cleanups 23 | 24 | assertTrue 'setup_cleanups failed' "$return_status" 25 | assertStdoutNull 26 | assertStderrNull 27 | assertTrue 'cleanup files does not exist' "[ -f '$__CLEANUP_FILES__' ]" 28 | assertTrue 'cleanup directories does not exist' \ 29 | "[ -f '$__CLEANUP_DIRECTORIES__' ]" 30 | } 31 | 32 | testSetupCleanupFilesWithVarSet() { 33 | local original_value_f 34 | local original_value_d 35 | original_value_f="$tmppath/testSetupCleanupFiles.cleanup" 36 | __CLEANUP_FILES__="$original_value_f" 37 | original_value_d="$tmppath/testSetupCleanupDirectories.cleanup" 38 | __CLEANUP_DIRECTORIES__="$original_value_d" 39 | run setup_cleanup_files 40 | 41 | assertTrue 'setup_cleanup_files failed' "$return_status" 42 | assertStdoutNull 43 | assertStderrNull 44 | assertEquals 'clean files variable changed' \ 45 | "$original_value_f" "$__CLEANUP_FILES__" 46 | assertEquals 'clean directories variable changed' \ 47 | "$original_value_d" "$__CLEANUP_DIRECTORIES__" 48 | } 49 | 50 | shell_compat "$0" 51 | 52 | . "$shunit2" 53 | -------------------------------------------------------------------------------- /tests/setup_traps_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/setup_traps.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testSetupTrapsSimple() { 20 | run_in_sh_script <<'EOF' 21 | echo "start" 22 | setup_traps "echo 'trap fired'" 23 | echo "end" 24 | EOF 25 | 26 | assertTrue 'setup_traps failed' "$return_status" 27 | assertStdoutEquals "start 28 | end 29 | trap fired" 30 | assertStderrNull 31 | } 32 | 33 | testSetupTrapsHUP() { 34 | # TODO: Skip for pdksh--the setup script isn't handling the signal correctly 35 | # despite the trap implementation working with the sh_script. 36 | if echo "${KSH_VERSION:-}" | grep -q "PD KSH"; then 37 | return 38 | fi 39 | 40 | signalScript HUP 41 | 42 | assertTrue 'setup_traps failed' "$return_status" 43 | if [ -n "${ZSH_VERSION:-}" ]; then 44 | # Zsh will invoke the trap on `HUP` and again when the process exits via 45 | # the `zshexit()` hook 46 | assertStdoutEquals "start 47 | trap fired 48 | trap fired" 49 | else 50 | assertStdoutEquals "start 51 | trap fired" 52 | fi 53 | # stderr may contain a message about the terminated process 54 | } 55 | 56 | testSetupTrapsALRM() { 57 | # TODO: Skip for pdksh--the setup script isn't handling the signal correctly 58 | # despite the trap implementation working with the sh_script. 59 | if echo "${KSH_VERSION:-}" | grep -q "PD KSH"; then 60 | return 61 | fi 62 | 63 | signalScript ALRM 64 | 65 | assertTrue 'setup_traps failed' "$return_status" 66 | assertStdoutEquals "start 67 | trap fired" 68 | # stderr may contain a message about the terminated process 69 | } 70 | 71 | testSetupTrapsTERM() { 72 | # TODO: Skip for pdksh--the setup script isn't handling the signal correctly 73 | # despite the trap implementation working with the sh_script. 74 | if echo "${KSH_VERSION:-}" | grep -q "PD KSH"; then 75 | return 76 | fi 77 | 78 | signalScript TERM 79 | 80 | assertTrue 'setup_traps failed' "$return_status" 81 | assertStdoutEquals "start 82 | trap fired" 83 | # stderr may contain a message about the terminated process 84 | } 85 | 86 | # testSetupTrapsINT - does not terminate script with test suite so is skipped 87 | 88 | # testSetupTrapsQUIT - does not terminate script with test suite so is skipped 89 | 90 | signalScript() { 91 | run_in_sh_script_and_signal "$1" <<'EOF' 92 | echo "start" 93 | setup_traps "echo 'trap fired'" 94 | sleep 10 & 95 | wait $! 96 | echo "end" 97 | EOF 98 | } 99 | 100 | shell_compat "$0" 101 | 102 | . "$shunit2" 103 | -------------------------------------------------------------------------------- /tests/test_helpers.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | # shellcheck disable=SC2034 4 | commonOneTimeSetUp() { 5 | set -u 6 | 7 | __ORIG_FLAGS="$-" 8 | __ORIG_PATH="$PATH" 9 | __ORIG_TERM="$TERM" 10 | __ORIG_PWD="$(pwd)" 11 | 12 | tmppath="$SHUNIT_TMPDIR/tmp" 13 | 14 | stdout="$tmppath/stdout" 15 | stderr="$tmppath/stderr" 16 | expected="$tmppath/expected" 17 | actual="$tmppath/actual" 18 | template="$tmppath/template" 19 | sh_script="$tmppath/sh_script.sh" 20 | isolated_path="$tmppath/isolated_path" 21 | 22 | fakebinpath="$SHUNIT_TMPDIR/fakebin" 23 | } 24 | 25 | commonSetUp() { 26 | # Reset set flags to its original value 27 | set "-$__ORIG_FLAGS" 28 | # Reset the value of `$PATH` to its original value 29 | PATH="$__ORIG_PATH" 30 | # Reset the value of `$TERM` to its original value 31 | TERM="$__ORIG_TERM" 32 | # Restore the original working directory 33 | cd "$__ORIG_PWD" || return 1 34 | # Clean any prior test file/directory state 35 | rm -rf "$tmppath" "$fakebinpath" 36 | # Unset any prior test variable state 37 | unset return_status 38 | # Unset any prior cleanup file variable 39 | unset __CLEANUP_FILES__ 40 | # Create a scratch directory that will be removed on every test 41 | mkdir -p "$tmppath" 42 | } 43 | 44 | assertStdoutEquals() { 45 | if [ "$#" -eq 2 ]; then 46 | assertEquals "$1" "$2" "$(cat "$stdout")" 47 | else 48 | assertEquals 'stdout not equal' "$1" "$(cat "$stdout")" 49 | fi 50 | } 51 | 52 | assertStdoutStripAnsiEquals() { 53 | if [ "$#" -eq 2 ]; then 54 | assertEquals "$1" "$2" "$(stripAnsi <"$stdout")" 55 | else 56 | assertEquals 'stdout (strip ANSI) not equal' "$1" "$(stripAnsi <"$stdout")" 57 | fi 58 | } 59 | 60 | assertStdoutContains() { 61 | if [ "$#" -eq 2 ]; then 62 | assertTrue "$1" "grep -E '$2' <'$stdout'" 63 | else 64 | assertTrue 'stdout does not contain' "grep -E '$1' <'$stdout'" 65 | fi 66 | } 67 | 68 | assertStdoutStripAnsiContains() { 69 | stripAnsi <"$stdout" >"$tmppath/stdout_no_ansi" 70 | 71 | if [ "$#" -eq 2 ]; then 72 | assertTrue "$1" "grep -E '$2' <'$tmppath/stdout_no_ansi'" 73 | else 74 | assertTrue 'stdout does not contain' "grep -E '$1' <'$tmppath/stdout_no_ansi'" 75 | fi 76 | } 77 | 78 | assertStdoutNull() { 79 | assertTrue 'stdout is not empty' "[ ! -s '$stdout' ]" 80 | } 81 | 82 | assertStderrEquals() { 83 | if [ "$#" -eq 2 ]; then 84 | assertEquals "$1" "$2" "$(cat "$stderr")" 85 | else 86 | assertEquals 'stderr not equal' "$1" "$(cat "$stderr")" 87 | fi 88 | } 89 | 90 | assertStderrStripAnsiEquals() { 91 | if [ "$#" -eq 2 ]; then 92 | assertEquals "$1" "$2" "$(stripAnsi <"$stderr")" 93 | else 94 | assertEquals 'stderr (strip ANSI) not equal' "$1" "$(stripAnsi <"$stderr")" 95 | fi 96 | } 97 | 98 | assertStderrContains() { 99 | if [ "$#" -eq 2 ]; then 100 | assertTrue "$1" "grep -E '$2' <'$stderr'" 101 | else 102 | assertTrue 'stderr does not contain' "grep -E '$1' <'$stderr'" 103 | fi 104 | } 105 | 106 | assertStderrStripAnsiContains() { 107 | stripAnsi <"$stderr" >"$tmppath/stderr_no_ansi" 108 | 109 | if [ "$#" -eq 2 ]; then 110 | assertTrue "$1" "grep -E '$2' <'$tmppath/stderr_no_ansi'" 111 | else 112 | assertTrue 'stderr does not contain' "grep -E '$1' <'$tmppath/stderr_no_ansi'" 113 | fi 114 | } 115 | 116 | assertStderrNull() { 117 | assertTrue 'stderr is not empty' "[ ! -s '$stderr' ]" 118 | } 119 | 120 | run() { 121 | # Implementation inspired by `run` in bats 122 | # See: https://git.io/fjCcr 123 | _origFlags="$-" 124 | set +e 125 | # functrace is not supported by all shells, eg: dash 126 | if set -o | "${GREP:-grep}" -q '^functrace'; then 127 | # shellcheck disable=SC3041 128 | set +T 129 | fi 130 | # errtrace is not supported by all shells, eg: ksh 131 | if set -o | "${GREP:-grep}" -q '^errtrace'; then 132 | # shellcheck disable=SC3041 133 | set +E 134 | fi 135 | "$@" >"$stdout" 2>"$stderr" 136 | return_status=$? 137 | set "-$_origFlags" 138 | unset _origFlags 139 | 140 | return "$return_status" 141 | } 142 | 143 | __setup_sh_script() { 144 | cat "${SRC:?SRC not set}" >"$sh_script" 145 | cat "${0%/*}/../lib/_ksh_local.sh" >>"$sh_script" 146 | echo >>"$sh_script" 147 | } 148 | 149 | run_in_sh_script() { 150 | __setup_sh_script 151 | while read -r line; do 152 | echo "$line" >>"$sh_script" 153 | done 154 | 155 | run "${SHELL_BIN:-sh}" "$sh_script" 156 | } 157 | 158 | run_in_sh_script_and_signal() { 159 | __setup_sh_script 160 | while read -r line; do 161 | echo "$line" >>"$sh_script" 162 | done 163 | 164 | echo " 165 | # Run the script with the shell interpreter in the background 166 | ${SHELL_BIN:-sh} $sh_script & 167 | # Capture the pid of the script 168 | bgps=\$! 169 | # Sleep to wait for script to start running and to start writing to output 170 | # streams 171 | sleep 0.05 172 | # Send the given signal to the script process 173 | kill -s '$1' \$bgps 174 | # Wait for the script process to terminate 175 | wait \$bgps 176 | # Return the exit code from the script process 177 | exit $? 178 | " >"$tmppath/run_in_bg.sh" 179 | 180 | run "${SHELL_BIN:-sh}" "$tmppath/run_in_bg.sh" 181 | } 182 | 183 | run_with_sh_script() { 184 | __setup_sh_script 185 | echo '"$@"' >>"$sh_script" 186 | 187 | run "${SHELL_BIN:-sh}" "$sh_script" "$@" 188 | } 189 | 190 | debugLastRun() { 191 | echo "======================" 192 | echo "Last 'run' invocation:" 193 | echo "----------------------" 194 | echo 195 | echo "return_status=$return_status" 196 | echo 197 | echo "stdout:" 198 | echo "---" 199 | cat "$stdout" 200 | echo "---" 201 | echo 202 | echo "stderr:" 203 | echo "---" 204 | cat "$stderr" 205 | echo "---" 206 | echo "======================" 207 | } 208 | 209 | stripAnsi() { 210 | case "$(uname -s)" in 211 | FreeBSD) 212 | gsed -r 's,\x1B\[[0-9;]*[a-zA-Z],,g' 213 | ;; 214 | *) 215 | # The `sed` implementation on macOS does not support either `\x1b` nor 216 | # the `-r` flag, and dash has a bug 217 | # (https://bugs.launchpad.net/ubuntu/+source/dash/+bug/1499473) where 218 | # `\xNN` hex bytes can't be printed, therefore we'll emit the correct 219 | # byte in octal with a `printf` subshell. 220 | sed 's,'"$(printf "\033")"'\[[0-9;]*[a-zA-Z],,g' 221 | ;; 222 | esac 223 | } 224 | 225 | isolatedPathFor() { 226 | mkdir -p "$isolated_path" 227 | 228 | for _bin in "$@"; do 229 | if command -v "$_bin" >/dev/null; then 230 | ln -snf "$(command -v "$_bin")" "$isolated_path/$_bin" 231 | fi 232 | done 233 | } 234 | 235 | shell_compat() { 236 | if [ -n "${ZSH_VERSION:-}" ]; then 237 | # shellcheck disable=SC3040 238 | set -o shwordsplit 239 | SHUNIT_PARENT="$1" 240 | fi 241 | } 242 | 243 | # shellcheck disable=SC2034 244 | shunit2="${0%/*}/../tmp/shunit2/shunit2" 245 | -------------------------------------------------------------------------------- /tests/trap_cleanup_directories_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/trap_cleanup_directories.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testTrapCleanupDirectories() { 20 | local alpha bravo charlie 21 | __CLEANUP_DIRECTORIES__="$tmppath/testTrapCleanupDirectories.cleanup" 22 | alpha="$tmppath/testTrapCleanupDirectories/alpha" 23 | mkdir -p "$alpha" 24 | echo "$alpha" >>"$__CLEANUP_DIRECTORIES__" 25 | bravo="$tmppath/testTrapCleanupDirectories/bravo" 26 | mkdir -p "$bravo" 27 | echo "$bravo" >>"$__CLEANUP_DIRECTORIES__" 28 | charlie="$tmppath/testTrapCleanupDirectories/charlie" 29 | mkdir -p "$charlie" 30 | echo "$charlie" >>"$__CLEANUP_DIRECTORIES__" 31 | 32 | assertTrue 'cleanup directories does not exist' \ 33 | "[ -f '$__CLEANUP_DIRECTORIES__' ]" 34 | assertTrue 'alpha does not exist' "[ -d '$alpha' ]" 35 | assertTrue 'bravo does not exist' "[ -d '$bravo' ]" 36 | assertTrue 'charlie does not exist' "[ -d '$charlie' ]" 37 | 38 | assertTrue 'trap_cleanup_directories does not fail' trap_cleanup_directories 39 | 40 | assertTrue 'cleanup files exists' "[ ! -f '$__CLEANUP_DIRECTORIES__' ]" 41 | assertTrue 'alpha exists' "[ ! -d '$alpha' ]" 42 | assertTrue 'bravo exists' "[ ! -d '$bravo' ]" 43 | assertTrue 'charlie exists' "[ ! -d '$charlie' ]" 44 | 45 | assertStdoutNull 46 | assertStderrNull 47 | 48 | unset alpha bravo charlie 49 | } 50 | 51 | testTrapCleanupDirectoriesNoVar() { 52 | unset __CLEANUP_DIRECTORIES__ 53 | 54 | assertTrue 'trap_cleanup_directories does not fail' trap_cleanup_directories 55 | 56 | assertStdoutNull 57 | assertStderrNull 58 | } 59 | 60 | testTrapCleanupDirectoriesNoFile() { 61 | __CLEANUP_DIRECTORIES__="$tmppath/testTrapCleanupDirectoriesNoFile.cleanup" 62 | rm -f "$__CLEANUP_DIRECTORIES__" 63 | 64 | assertTrue 'trap_cleanup_directories does not fail' trap_cleanup_directories 65 | 66 | assertStdoutNull 67 | assertStderrNull 68 | } 69 | 70 | shell_compat "$0" 71 | 72 | . "$shunit2" 73 | -------------------------------------------------------------------------------- /tests/trap_cleanup_files_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/trap_cleanup_files.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testTrapCleanupFiles() { 20 | local alpha bravo charlie 21 | __CLEANUP_FILES__="$tmppath/testTrapCleanupFiles.cleanup" 22 | mkdir "$tmppath/testTrapCleanupDirectories" 23 | alpha="$tmppath/testTrapCleanupDirectories/alpha" 24 | touch "$alpha" 25 | echo "$alpha" >>"$__CLEANUP_FILES__" 26 | bravo="$tmppath/testTrapCleanupDirectories/bravo" 27 | touch "$bravo" 28 | echo "$bravo" >>"$__CLEANUP_FILES__" 29 | charlie="$tmppath/testTrapCleanupDirectories/charlie" 30 | touch "$charlie" 31 | echo "$charlie" >>"$__CLEANUP_FILES__" 32 | 33 | assertTrue 'cleanup files does not exist' "[ -f '$__CLEANUP_FILES__' ]" 34 | assertTrue 'alpha does not exist' "[ -f '$alpha' ]" 35 | assertTrue 'bravo does not exist' "[ -f '$bravo' ]" 36 | assertTrue 'charlie does not exist' "[ -f '$charlie' ]" 37 | 38 | assertTrue 'trap_cleanup_files does not fail' trap_cleanup_files 39 | 40 | assertTrue 'cleanup files exists' "[ ! -f '$__CLEANUP_FILES__' ]" 41 | assertTrue 'alpha exists' "[ ! -f '$alpha' ]" 42 | assertTrue 'bravo exists' "[ ! -f '$bravo' ]" 43 | assertTrue 'charlie exists' "[ ! -f '$charlie' ]" 44 | 45 | assertStdoutNull 46 | assertStderrNull 47 | 48 | unset alpha bravo charlie 49 | } 50 | 51 | testTrapCleanupFilesNoVar() { 52 | unset __CLEANUP_FILES__ 53 | 54 | assertTrue 'trap_cleanup_files does not fail' trap_cleanup_files 55 | 56 | assertStdoutNull 57 | assertStderrNull 58 | } 59 | 60 | testTrapCleanupFilesNoFile() { 61 | __CLEANUP_FILES__="$tmppath/testTrapCleanupFilesNoFile.cleanup" 62 | rm -f "$__CLEANUP_FILES__" 63 | 64 | assertTrue 'trap_cleanup_files does not fail' trap_cleanup_files 65 | 66 | assertStdoutNull 67 | assertStderrNull 68 | } 69 | 70 | shell_compat "$0" 71 | 72 | . "$shunit2" 73 | -------------------------------------------------------------------------------- /tests/trap_cleanups_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/trap_cleanups.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testTrapCleanups() { 20 | local f_alpha f_bravo f_charlie 21 | local d_alpha d_bravo d_charlie 22 | __CLEANUP_FILES__="$tmppath/testTrapCleanupFiles.cleanup" 23 | __CLEANUP_DIRECTORIES__="$tmppath/testTrapCleanupDirectories.cleanup" 24 | 25 | mkdir "$tmppath/testTrapCleanupDirectories" 26 | f_alpha="$tmppath/testTrapCleanupDirectories/f_alpha" 27 | touch "$f_alpha" 28 | echo "$f_alpha" >>"$__CLEANUP_FILES__" 29 | f_bravo="$tmppath/testTrapCleanupDirectories/f_bravo" 30 | touch "$f_bravo" 31 | echo "$f_bravo" >>"$__CLEANUP_FILES__" 32 | f_charlie="$tmppath/testTrapCleanupDirectories/f_charlie" 33 | touch "$f_charlie" 34 | echo "$f_charlie" >>"$__CLEANUP_FILES__" 35 | 36 | d_alpha="$tmppath/testTrapCleanupDirectories/d_alpha" 37 | mkdir -p "$d_alpha" 38 | echo "$d_alpha" >>"$__CLEANUP_DIRECTORIES__" 39 | d_bravo="$tmppath/testTrapCleanupDirectories/d_bravo" 40 | mkdir -p "$d_bravo" 41 | echo "$d_bravo" >>"$__CLEANUP_DIRECTORIES__" 42 | d_charlie="$tmppath/testTrapCleanupDirectories/d_charlie" 43 | mkdir -p "$d_charlie" 44 | echo "$d_charlie" >>"$__CLEANUP_DIRECTORIES__" 45 | 46 | assertTrue 'cleanup files does not exist' "[ -f '$__CLEANUP_FILES__' ]" 47 | assertTrue 'f_alpha does not exist' "[ -f '$f_alpha' ]" 48 | assertTrue 'f_bravo does not exist' "[ -f '$f_bravo' ]" 49 | assertTrue 'f_charlie does not exist' "[ -f '$f_charlie' ]" 50 | 51 | assertTrue 'cleanup directories does not exist' \ 52 | "[ -f '$__CLEANUP_DIRECTORIES__' ]" 53 | assertTrue 'd_alpha does not exist' "[ -d '$d_alpha' ]" 54 | assertTrue 'd_bravo does not exist' "[ -d '$d_bravo' ]" 55 | assertTrue 'd_charlie does not exist' "[ -d '$d_charlie' ]" 56 | 57 | assertTrue 'trap_cleanups does not fail' trap_cleanups 58 | 59 | assertTrue 'cleanup files exists' "[ ! -f '$__CLEANUP_FILES__' ]" 60 | assertTrue 'f_alpha exists' "[ ! -f '$f_alpha' ]" 61 | assertTrue 'f_bravo exists' "[ ! -f '$f_bravo' ]" 62 | assertTrue 'f_charlie exists' "[ ! -f '$f_charlie' ]" 63 | 64 | assertTrue 'cleanup files exists' "[ ! -f '$__CLEANUP_DIRECTORIES__' ]" 65 | assertTrue 'd_alpha exists' "[ ! -d '$d_alpha' ]" 66 | assertTrue 'd_bravo exists' "[ ! -d '$d_bravo' ]" 67 | assertTrue 'd_charlie exists' "[ ! -d '$d_charlie' ]" 68 | 69 | assertStdoutNull 70 | assertStderrNull 71 | 72 | unset f_alpha f_bravo f_charlie 73 | unset d_alpha d_bravo d_charlie 74 | } 75 | 76 | testTrapCleanupsNoVar() { 77 | unset __CLEANUP_FILES__ 78 | unset __CLEANUP_DIRECTORIES__ 79 | 80 | assertTrue 'trap_cleanups does not fail' trap_cleanups 81 | 82 | assertStdoutNull 83 | assertStderrNull 84 | } 85 | 86 | testTrapCleanupFilesNoFile() { 87 | __CLEANUP_FILES__="$tmppath/testTrapCleanupFilesNoFile.cleanup" 88 | rm -f "$__CLEANUP_FILES__" 89 | __CLEANUP_DIRECTORIES__="$tmppath/testTrapCleanupDirectoriesNoFile.cleanup" 90 | rm -f "$__CLEANUP_DIRECTORIES__" 91 | 92 | assertTrue 'trap_cleanups does not fail' trap_cleanups 93 | 94 | assertStdoutNull 95 | assertStderrNull 96 | } 97 | 98 | shell_compat "$0" 99 | 100 | . "$shunit2" 101 | -------------------------------------------------------------------------------- /tests/warn_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # shellcheck disable=SC3043 3 | 4 | # shellcheck source=tests/test_helpers.sh 5 | . "${0%/*}/test_helpers.sh" 6 | 7 | . "${0%/*}/../lib/_ksh_local.sh" 8 | 9 | . "${SRC:=lib/warn.sh}" 10 | 11 | oneTimeSetUp() { 12 | commonOneTimeSetUp 13 | } 14 | 15 | setUp() { 16 | commonSetUp 17 | } 18 | 19 | testWarnStripAnsi() { 20 | run warn 'something is questionable' 21 | 22 | assertTrue 'warn failed' "$return_status" 23 | assertStdoutStripAnsiEquals '!!! something is questionable' 24 | assertStderrNull 25 | } 26 | 27 | testWarnAnsi() { 28 | printf -- '\033[1;31;40m!!! \033[1;37;40msomething is questionable\033[0m\n' \ 29 | >"$expected" 30 | export TERM=xterm 31 | run warn 'something is questionable' 32 | 33 | assertTrue 'warn failed' "$return_status" 34 | # Shell string equals has issues with ANSI escapes, so let's use $(cmp) to 35 | # compare byte by byte 36 | assertTrue 'ANSI stdout not equal' "cmp '$expected' '$stdout'" 37 | assertStderrNull 38 | } 39 | 40 | shell_compat "$0" 41 | 42 | . "$shunit2" 43 | -------------------------------------------------------------------------------- /vendor/mk/base.mk: -------------------------------------------------------------------------------- 1 | CHECK_TOOLS ?= 2 | TEST_TOOLS ?= 3 | 4 | all: clean build test check ## Runs clean, build, test, check 5 | .PHONY: all 6 | 7 | prepush: check test ## Runs all checks/test required before pushing 8 | @echo "--- $@" 9 | @echo "all prepush targets passed, okay to push." 10 | .PHONY: prepush 11 | 12 | checktools: ## Checks that required check tools are found on PATH 13 | @echo "--- $@" 14 | $(foreach tool, $(CHECK_TOOLS), $(if $(shell which $(tool)),, \ 15 | $(error "Required tool '$(tool)' not found on PATH"))) 16 | .PHONY: checktools 17 | 18 | testtools: ## Checks that required test tools are found on PATH 19 | @echo "--- $@" 20 | $(foreach tool, $(TEST_TOOLS), $(if $(shell which $(tool)),, \ 21 | $(error "Required tool '$(tool)' not found on PATH"))) 22 | .PHONY: testtools 23 | 24 | help: ## Prints help information 25 | @printf -- "\033[1;36;40mmake %s\033[0m\n" "$@" 26 | @echo 27 | @echo "USAGE:" 28 | @echo " make [TARGET]" 29 | @echo 30 | @echo "TARGETS:" 31 | @grep -hE '^[a-zA-Z0-9_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk '\ 32 | BEGIN { FS = ":.*?## " }; \ 33 | { printf " \033[1;36;40m%-20s\033[0m %s\n", $$1, $$2 }' 34 | .PHONY: help 35 | -------------------------------------------------------------------------------- /vendor/mk/release.mk: -------------------------------------------------------------------------------- 1 | BUMP_MODE ?= 2 | BUMP_PRE ?= 3 | BUMP_SET ?= 4 | 5 | ifndef NAME 6 | $(error NAME is not set in Makefile and is required in release.mk) 7 | endif 8 | 9 | ifndef REPO 10 | $(error REPO is not set in Makefile and is required in release.mk) 11 | endif 12 | 13 | release-prepare: clean prepush release-bump-version release-update-changelog \ 14 | release-create-branch release-create-release-commit \ 15 | release-tag release-bump-version-dev release-create-dev-commit \ 16 | release-push-head 17 | @echo "--- $@" 18 | @echo "Final release steps:" 19 | @echo "" 20 | @echo "1. Create a pull request for the release branch by visiting:" 21 | @echo " $(REPO)/pull/new/release-$$(cat tmp/LAST_VERSION.txt)" 22 | @echo "2. Wait for CI to turn green" 23 | @echo "3. Locally run \`git push origin v$$(cat tmp/LAST_VERSION.txt)\`" 24 | @echo "4. Comment on the pull request with \`bors merge\`" 25 | @echo "5. Wait for bors to integrate, test, and merge the pull request" 26 | @echo "" 27 | .PHONY: release-prepare 28 | 29 | release-bump-version: ## Set a new version for the project. 30 | @echo "--- $@" 31 | @echo " - Bumping version" 32 | @mkdir -p tmp 33 | @cp VERSION.txt tmp/LAST_VERSION.txt 34 | @if [ -n "$(BUMP_SET)" ]; then \ 35 | echo "$(BUMP_SET)" >VERSION.txt; \ 36 | elif [ -n "$(BUMP_MODE)" ] && [ -n "$(BUMP_PRE)" ]; then \ 37 | versio bump file $(BUMP_MODE) --pre-release $(BUMP_PRE); \ 38 | elif [ -n "$(BUMP_MODE)" ]; then \ 39 | versio bump file $(BUMP_MODE); \ 40 | else \ 41 | versio bump file set --no-pre-release; \ 42 | fi 43 | @echo " VERSION.txt now set to: $$(cat VERSION.txt)" 44 | .PHONY: release-bump-version 45 | 46 | release-bump-version-dev: 47 | @echo "--- $@" 48 | @echo " - Bumping version for next iteration" 49 | @mkdir -p tmp 50 | @cp VERSION.txt tmp/LAST_VERSION.txt 51 | @if grep -q -E '^\d+\.\d+.\d+-.+' tmp/LAST_VERSION.txt; then \ 52 | versio bump file set --pre-release dev; \ 53 | else \ 54 | versio bump file patch --pre-release dev; \ 55 | fi 56 | @echo " VERSION.txt now set to: $$(cat VERSION.txt)" 57 | .PHONY: release-bump-version-dev 58 | 59 | release-create-branch: 60 | @echo "--- $@" 61 | git checkout -b "release-$$(cat VERSION.txt)" 62 | .PHONY: release-create-branch 63 | 64 | release-create-dev-commit: 65 | @echo "--- $@" 66 | git add . 67 | git commit --signoff \ 68 | --message "chore: start next iteration $$(cat VERSION.txt)" 69 | .PHONY: release-create-dev-commit 70 | 71 | release-create-release-commit: 72 | @echo "--- $@" 73 | git add . 74 | git commit --signoff \ 75 | --message "release: $(NAME) $$(cat VERSION.txt)" 76 | .PHONY: release-create-release-commit 77 | 78 | release-push-head: 79 | @echo "--- $@" 80 | git push origin HEAD 81 | .PHONY: release-push-head 82 | 83 | release-tag: ## Create a new release Git tag 84 | @echo "--- $@" 85 | version="$$(cat VERSION.txt)" && git tag \ 86 | --annotate "v$$version" --message "release: $(NAME) $$version" 87 | .PHONY: release-tag 88 | 89 | release-update-changelog: 90 | @echo "--- $@" 91 | version="$$(cat VERSION.txt)" && tag_name="v$$version" \ 92 | && ./.ci/update-changelog.sh "$(REPO)" "$$version" "$$tag_name" 93 | .PHONY: release-update-changelog 94 | -------------------------------------------------------------------------------- /vendor/mk/shell.mk: -------------------------------------------------------------------------------- 1 | SH_SOURCES ?= $(shell find . -type f -name '*.sh' -not -path './tmp/*' -and -not -path './vendor/*') 2 | SHELL_BIN ?= $(SHELL) 3 | SHUNIT2_VERSION := 2.1.7 4 | CHECK_TOOLS += shellcheck shfmt 5 | 6 | test-shell: testtools dl-shunit2 ## Runs all shell code tests 7 | @echo "--- $@" 8 | @for test in $(SH_TESTS); do \ 9 | export SHELL_BIN=$(SHELL_BIN); \ 10 | echo " - Running: $$test (SHELL_BIN=$$SHELL_BIN)"; \ 11 | $(SHELL_BIN) $$test || exit $$?; \ 12 | done 13 | .PHONY: test-shell 14 | 15 | check-shell: shellcheck shfmt ## Checks linting & styling rules for shell code 16 | .PHONY: check-shell 17 | 18 | shellcheck: checktools ## Checks shell code for linting rules 19 | @echo "--- $@" 20 | shellcheck --external-sources $(SH_SOURCES) 21 | .PHONY: shellcheck 22 | 23 | shfmt: checktools ## Checks shell code for consistent formatting 24 | @echo "--- $@" 25 | shfmt -i 2 -ci -bn -d -l $(SH_SOURCES) 26 | .PHONY: shfmt 27 | 28 | clean-shell: ## Cleans up shell project 29 | rm -rf tmp 30 | .PHONY: clean-shell 31 | 32 | dl-shunit2: tmp/shunit2 ## Downloads shUnit2 33 | .PHOHY: dl-shunit2 34 | 35 | tmp/shunit2: tmp/shunit2-$(SHUNIT2_VERSION) 36 | @echo "--- $@" 37 | ln -snf ./shunit2-$(SHUNIT2_VERSION) tmp/shunit2 38 | 39 | tmp/shunit2-$(SHUNIT2_VERSION): 40 | @echo "--- $@" 41 | mkdir -p $@ 42 | curl -sSfL https://github.com/kward/shunit2/archive/v$(SHUNIT2_VERSION).tar.gz \ 43 | | tar xzf - -C tmp/ 44 | --------------------------------------------------------------------------------