From f5cb74d85984493b32a241824cd17cda160760c0 Mon Sep 17 00:00:00 2001 From: Andrew Countryman Date: Thu, 28 Mar 2024 11:14:42 -0700 Subject: [PATCH] Overhaul shell scripts (#144) Resolves #143 (Overhaul shell scripts). --- .github/workflows/ci.yml | 2 +- README.md | 12 +- ci/{analyze => analyze.sh} | 87 ++++++++++---- git/hooks/install | 152 ------------------------ git/hooks/install.sh | 123 +++++++++++++++++++ git/hooks/{pre-commit => pre-commit.sh} | 126 ++++++++++++-------- 6 files changed, 270 insertions(+), 232 deletions(-) rename ci/{analyze => analyze.sh} (62%) delete mode 100755 git/hooks/install create mode 100755 git/hooks/install.sh rename git/hooks/{pre-commit => pre-commit.sh} (68%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dc30b54..ba283af 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,4 +33,4 @@ jobs: - uses: actions/checkout@v4 - name: Analyze shell: bash - run: ./ci/analyze --analyzer shellcheck + run: ./ci/analyze.sh --analyzer shellcheck diff --git a/README.md b/README.md index 4e69aa9..f00fefd 100644 --- a/README.md +++ b/README.md @@ -45,11 +45,11 @@ If the toolchain fails to locate tools, consult the documentation for CMake's ## Usage (Development) -This repository's Git `pre-commit` hook script is the simplest way to configure, build, +This repository's Git `pre-commit.sh` hook script is the simplest way to configure, build, and test this project during development. -See the `pre-commit` script's help text for usage details. +See the `pre-commit.sh` script's help text for usage details. ```shell -./git/hooks/pre-commit --help +./git/hooks/pre-commit.sh --help ``` Additional checks, such as static analysis, are performed by this project's GitHub Actions @@ -67,11 +67,11 @@ workflow. ## Git Hooks -To install this repository's Git hooks, run the `install` script located in the +To install this repository's Git hooks, run the `install.sh` script located in the `git/hooks` directory. -See the `install` script's help text for usage details. +See the `install.sh` script's help text for usage details. ``` -$ ./git/hooks/install --help +$ ./git/hooks/install.sh --help ``` ## Code of Conduct diff --git a/ci/analyze b/ci/analyze.sh similarity index 62% rename from ci/analyze rename to ci/analyze.sh index 6b5e213..be1f242 100755 --- a/ci/analyze +++ b/ci/analyze.sh @@ -35,27 +35,50 @@ function abort() exit 1 } +function validate_script() +{ + if ! shellcheck "$script"; then + abort + fi +} + function display_help_text() { - echo "NAME" - echo " $mnemonic - Run a static analyzer against toolchain-avr-gcc." - echo "SYNOPSIS" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic --analyzer " - echo "OPTIONS" - echo " --analyzer " - echo " Specify the analyzer to run against toolchain-avr-gcc. The following" - echo " analyzers are supported:" - echo " shellcheck" - echo " --help" - echo " Display this help text." - echo " --version" - echo " Display the version of this script." - echo "EXAMPLES" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic --analyzer shellcheck" + local analyzer + + printf "%b" \ + "NAME\n" \ + " $mnemonic - Ensure no static analysis errors are present.\n" \ + "SYNOPSIS\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + " $mnemonic --analyzer \n" \ + "OPTIONS\n" \ + " --analyzer \n" \ + " Specify the analyzer to run. The following analyzers are supported:\n" \ + "" + + for analyzer in "${analyzers[@]}"; do + printf "%b" \ + " $analyzer\n" \ + "" + done + + printf "%b" \ + " --help\n" \ + " Display this help text.\n" \ + " --version\n" \ + " Display the version of this script.\n" \ + "EXAMPLES\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + "" + + for analyzer in "${analyzers[@]}"; do + printf "%b" \ + " $mnemonic --analyzer $analyzer\n" \ + "" + done } function display_version() @@ -63,9 +86,24 @@ function display_version() echo "$mnemonic, version $version" } +function value_is_in_array() +{ + local -r target_value="$1"; shift + local -r array=( "$@" ) + + local value + for value in "${array[@]}"; do + if [[ "$target_value" == "$value" ]]; then + return 0; + fi + done + + return 1 +} + function run_shellcheck() { - local scripts; mapfile -t scripts < <( git -C "$repository" ls-files ':!:*.py' | xargs -r -d '\n' -I '{}' find "$repository/{}" -executable ); readonly scripts + local scripts; mapfile -t scripts < <( git -C "$repository" ls-files '*.sh' | xargs -r -d '\n' -I '{}' find "$repository/{}" ); readonly scripts if ! shellcheck "${scripts[@]}"; then abort @@ -81,9 +119,16 @@ function main() { local -r script=$( readlink -f "$0" ) local -r mnemonic=$( basename "$script" ) + + validate_script + local -r repository=$( readlink -f "$( dirname "$script" )/.." ) local -r version=$( git -C "$repository" describe --match=none --always --dirty --broken ) + local -r analyzers=( + shellcheck + ) + while [[ "$#" -gt 0 ]]; do local argument="$1"; shift @@ -99,7 +144,7 @@ function main() local -r analyzer="$1"; shift - if [[ "$analyzer" != "shellcheck" ]]; then + if ! value_is_in_array "$analyzer" "${analyzers[@]}"; then abort "'$analyzer' is not a supported analyzer" fi ;; diff --git a/git/hooks/install b/git/hooks/install deleted file mode 100755 index 5568701..0000000 --- a/git/hooks/install +++ /dev/null @@ -1,152 +0,0 @@ -#!/usr/bin/env bash - -# toolchain-avr-gcc -# -# Copyright 2019-2024, Andrew Countryman and the -# toolchain-avr-gcc contributors -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this -# file except in compliance with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software distributed under -# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the specific language governing -# permissions and limitations under the License. - -# Description: Git hooks install script. - -function error() -{ - local -r message="$1" - - ( >&2 echo "$mnemonic: $message" ) -} - -function abort() -{ - if [[ "$#" -gt 0 ]]; then - local -r message="$1" - - error "$message, aborting" - fi - - exit 1 -} - -function display_help_text() -{ - echo "NAME" - echo " $mnemonic - Install the toolchain-avr-gcc repository's Git hooks." - echo "SYNOPSIS" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic --all" - echo " $mnemonic []" - echo "OPTIONS" - echo " --all" - echo " Install all supported hooks." - echo " --help" - echo " Display this help text." - echo " --version" - echo " Display the version of this script." - echo " " - echo " The whitespace separated list of hooks to install. The following hooks" - echo " are supported:" - - for hook in "${supported_hooks[@]}"; do - echo " $hook" - done - - echo "EXAMPLES" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic --all" - echo " $mnemonic ${supported_hooks[*]}" -} - -function display_version() -{ - echo "$mnemonic, version $version" -} - -function valid_hook() -{ - local -r hook="$1" - - for supported_hook in "${supported_hooks[@]}"; do - if [[ "$hook" == "$supported_hook" ]]; then - return 0 - fi - done - - return 1 -} - -function validate_hooks() -{ - for hook in "${selected_hooks[@]}"; do - if ! valid_hook "$hook"; then - abort "'$hook' is not a supported hook" - fi - done -} - -function install_hooks() -{ - for hook in "${selected_hooks[@]}"; do - rm -f "$repository/.git/hooks/$hook" - - if ! ln -s "$hooks/$hook" "$repository/.git/hooks/$hook" > "/dev/null"; then - abort "'$hook' installation failure" - fi - done -} - -function main() -{ - local -r script=$( readlink -f "$0" ) - local -r mnemonic=$( basename "$script" ) - local -r hooks=$( dirname "$script" ) - local -r repository=$( readlink -f "$hooks/../.." ) - local -r version=$( git -C "$repository" describe --match=none --always --dirty --broken ) - - local supported_hooks; mapfile -t supported_hooks < <( git -C "$repository" ls-files 'git/hooks/' ':!:git/hooks/install' | xargs -r -d '\n' -I '{}' find "$repository/{}" -executable -printf '%f\n' ); readonly supported_hooks - - local selected_hooks=() - - while [[ "$#" -gt 0 ]]; do - local argument="$1"; shift - - case "$argument" in - --all) - selected_hooks=( "${supported_hooks[@]}" ) - break - ;; - --help) - display_help_text - exit - ;; - --version) - display_version - exit - ;; - --*) - ;& - -*) - abort "'$argument' is not a supported option" - ;; - *) - selected_hooks+=( "$argument" ) - ;; - esac - done - - readonly selected_hooks - - validate_hooks - install_hooks -} - -main "$@" diff --git a/git/hooks/install.sh b/git/hooks/install.sh new file mode 100755 index 0000000..5b69b80 --- /dev/null +++ b/git/hooks/install.sh @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +# toolchain-avr-gcc +# +# Copyright 2019-2024, Andrew Countryman and the +# toolchain-avr-gcc contributors +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not use this +# file except in compliance with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software distributed under +# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +# Description: Git hooks install script. + +function error() +{ + local -r message="$1" + + ( >&2 echo "$mnemonic: $message" ) +} + +function abort() +{ + if [[ "$#" -gt 0 ]]; then + local -r message="$1" + + error "$message, aborting" + fi + + exit 1 +} + +function validate_script() +{ + if ! shellcheck "$script"; then + abort + fi +} + +function display_help_text() +{ + printf "%b" \ + "NAME\n" \ + " $mnemonic - Install Git hooks.\n" \ + "SYNOPSIS\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + " $mnemonic\n" \ + "OPTIONS\n" \ + " --help\n" \ + " Display this help text.\n" \ + " --version\n" \ + " Display the version of this script.\n" \ + "EXAMPLES\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + " $mnemonic\n" \ + "" +} + +function display_version() +{ + echo "$mnemonic, version $version" +} + +function install_git_hooks() +{ + local hook_scripts; mapfile -t hook_scripts < <( git -C "$repository" ls-files 'git/hooks/' ':!:git/hooks/install.sh' | xargs -r -d '\n' -I '{}' find "$repository/{}" ); readonly hook_scripts + + local hook_script + for hook_script in "${hook_scripts[@]}"; do + local hook; hook=$( basename "$hook_script" | cut -f 1 -d '.' ) + + rm -f "$repository/.git/hooks/$hook" + + if ! ln -s "$hook_script" "$repository/.git/hooks/$hook"; then + abort "'$hook' installation failure" + fi + done +} + +function main() +{ + local -r script=$( readlink -f "$0" ) + local -r mnemonic=$( basename "$script" ) + + validate_script + + local -r repository=$( readlink -f "$( dirname "$script" )/../.." ) + local -r version=$( git -C "$repository" describe --match=none --always --dirty --broken ) + + while [[ "$#" -gt 0 ]]; do + local argument="$1"; shift + + case "$argument" in + --help) + display_help_text + exit + ;; + --version) + display_version + exit + ;; + --*) + ;& + -*) + abort "'$argument' is not a supported option" + ;; + *) + abort "'$argument' is not a valid argument" + ;; + esac + done + + install_git_hooks +} + +main "$@" diff --git a/git/hooks/pre-commit b/git/hooks/pre-commit.sh similarity index 68% rename from git/hooks/pre-commit rename to git/hooks/pre-commit.sh index 99423ab..f0ca426 100755 --- a/git/hooks/pre-commit +++ b/git/hooks/pre-commit.sh @@ -17,18 +17,6 @@ # Description: Git pre-commit hook script. -function message() -{ - local -r content="$1" - - echo -n "$mnemonic: $content" -} - -function errors_found() -{ - echo "error(s) found" -} - function error() { local -r message="$1" @@ -47,31 +35,36 @@ function abort() exit 1 } +function validate_script() +{ + if ! shellcheck "$script"; then + abort + fi +} + function display_help_text() { - echo "NAME" - echo " $mnemonic - Ensure:" - echo " - Filenames are portable" - echo " - No whitespace errors are present" - echo " - No script errors are present" - echo " - No build errors are present" - echo "SYNOPSIS" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic [--jobs ]" - echo "OPTIONS" - echo " --help" - echo " Display this help text." - echo " --jobs " - echo " Specify the number of build jobs to use when building. If the number of" - echo " jobs is not specified, 'nproc - 1' jobs will be used." - echo " --version" - echo " Display the version of this script." - echo "EXAMPLES" - echo " $mnemonic --help" - echo " $mnemonic --version" - echo " $mnemonic" - echo " $mnemonic --jobs 1" + printf "%b" \ + "NAME\n" \ + " $mnemonic - Ensure commit preconditions are met.\n" \ + "SYNOPSIS\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + " $mnemonic [--jobs ]\n" \ + "OPTIONS\n" \ + " --help\n" \ + " Display this help text.\n" \ + " --jobs \n" \ + " Specify the number of build jobs to use when building. If the number of\n" \ + " jobs is not specified, 'nproc - 1' jobs will be used.\n" \ + " --version\n" \ + " Display the version of this script.\n" \ + "EXAMPLES\n" \ + " $mnemonic --help\n" \ + " $mnemonic --version\n" \ + " $mnemonic\n" \ + " $mnemonic --jobs 1\n" \ + "" } function display_version() @@ -79,59 +72,86 @@ function display_version() echo "$mnemonic, version $version" } +function message() +{ + local -r content="$1" + local -r content_length=${#content} + local -r content_length_max=47 + local -r ellipsis_count_min=3 + local -r ellipsis_count=$(( content_length_max - content_length + ellipsis_count_min )) + + if [[ "$ellipsis_count" -lt "$ellipsis_count_min" ]]; then + abort "increase content_length_max (ellipsis_count=$ellipsis_count)" + fi + + local -r ellipsis=$( head -c "$ellipsis_count" < /dev/zero | tr '\0' '.' ) + + echo -n "$mnemonic: $content $ellipsis " +} + +function message_status_no_errors_found() +{ + echo "none" +} + +function message_status_errors_found() +{ + echo "error(s) found" +} + function ensure_filenames_are_portable() { - message "checking for non-portable (non-ASCII) filenames ... " + message "checking for non-portable (non-ASCII) filenames" if [[ $( git -C "$repository" diff --cached --name-only --diff-filter=A -z "$against" | LC_ALL=C tr -d '[ -~]\0' | wc -c ) != 0 ]]; then - errors_found + message_status_errors_found error "aborting commit due to non-portable (non-ASCII) filename(s)" abort fi - echo "none" + message_status_no_errors_found } function ensure_no_whitespace_errors_are_present() { - message "checking for whitespace errors .................... " + message "checking for whitespace errors" if ! git -C "$repository" diff-index --check --cached "$against" -- > "/dev/null" 2>&1; then - errors_found + message_status_errors_found error "aborting commit due to whitespace error(s), listed below" git -C "$repository" diff-index --check --cached "$against" -- abort fi - echo "none" + message_status_no_errors_found } function ensure_no_script_errors_are_present() { - message "checking for script errors ........................ " + message "checking for script errors" - local scripts; mapfile -t scripts < <( git -C "$repository" ls-files ':!:*.py' | xargs -r -d '\n' -I '{}' find "$repository/{}" -executable ); readonly scripts + local scripts; mapfile -t scripts < <( git -C "$repository" ls-files '*.sh' | xargs -r -d '\n' -I '{}' find "$repository/{}" ); readonly scripts if ! shellcheck "${scripts[@]}" > "/dev/null" 2>&1; then - errors_found + message_status_errors_found error "aborting commit due to script error(s), listed below" shellcheck "${scripts[@]}" abort fi - echo "none" + message_status_no_errors_found } function ensure_no_build_errors_are_present() { - message "checking for build errors ......................... " + message "checking for build errors" local -r toolchain_file="$repository/toolchain.cmake" local -r build_directory="$repository/build" if [[ ! -d "$build_directory" ]]; then if ! cmake -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" -S "$repository" -B "$build_directory" > "/dev/null" 2>&1; then - errors_found + message_status_errors_found error "aborting commit due to CMake initialization error(s), listed below" rm -rf "$build_directory" cmake -DCMAKE_TOOLCHAIN_FILE="$toolchain_file" -S "$repository" -B "$build_directory" @@ -140,28 +160,30 @@ function ensure_no_build_errors_are_present() fi if ! cmake "$build_directory" > "/dev/null" 2>&1; then - errors_found + message_status_errors_found error "aborting commit due to CMake configuration error(s), listed below" cmake "$build_directory" abort fi if ! cmake --build "$build_directory" -j "$build_jobs" > "/dev/null" 2>&1; then - errors_found + message_status_errors_found error "aborting commit due to CMake build error(s), listed below" cmake --build "$build_directory" -j "$build_jobs" abort fi - echo "none" + message_status_no_errors_found } function main() { local -r script=$( readlink -f "$0" ) local -r mnemonic=$( basename "$script" ) - local -r hooks=$( dirname "$script" ) - local -r repository=$( readlink -f "$hooks/../.." ) + + validate_script + + local -r repository=$( readlink -f "$( dirname "$script" )/../.." ) local -r version=$( git -C "$repository" describe --match=none --always --dirty --broken ) while [[ "$#" -gt 0 ]]; do