Portable shell script to audit Gradle (a build tool for the JVM)

Script monkeys ahoy!

Verifying the integrity of the Gradle Wrapper JAR, for version control repositories that allow users to include a binary payload: gradle/wrapper/gradle-wrapper.jar

It does other stuff too.

In the browser viewing: auditgradle.sh or
git clone https://bitbucket.org/snippets/KJ_Duncan/LpLedA/portable-shell-script-to-audit-the-gradle.git

For more Gradle adventures drop in and see @jdugan6240 efforts locally at discuss kakoune or knock on the door at his repository on github.

For the click fearing here’s the copy and paste job:

auditgradle.sh
#! /usr/bin/env sh
# ------------------------------------------------------------------------------------------------------------ #
# Table of Contents
#   1) Introduction
#   2) Debug
#   3) Security
#   4) Standard out
#   5) Short circuit
#   6) Domain
#   7) Call stack
#   8) Helper functions
#   9) Entry point
#  10) Appendices
# ------------------------------------------------------------------------------------------------------------ #
# INTRODUCTION  ---------------------------------------------------------------------------------------------- #
# ------------------------------------------------------------------------------------------------------------ #
#
# Version control repositories that allow users to include a binary payload:
#     gradle/wrapper/gradle-wrapper.jar
#
#   Gradle 2021, Verifying the integrity of the Gradle Wrapper JAR, The Gradle Wrapper, viewed 04 February 2021,
#                <https://docs.gradle.org/current/userguide/gradle_wrapper.html#wrapper_checksum_verification>
#
# Enjoy Gradle? Meet Hugh, he programs the sht out of it! Thanks Hugh.
#
#   Greene, H 2021, The Holy Gradle, holygradle-plugins, viewed 25 March 2021,
#                   https://bitbucket.org/HughG/holygradle-plugins/src/master/
#
# Life's better with music.
#
#   Jackson, E 2009, Bulletproof, La Roux, Spotify, Youtube,
#                    https://open.spotify.com/track/3kMrazSvILsgcwtidZd1Qd?si=afef67527d5f4e4f
#                    https://youtu.be/Kk8eJh4i8Lo
#
# ------------------------------------------------------------------------------------------------------------ #
# DEBUG ------------------------------------------------------------------------------------------------------ #
# ------------------------------------------------------------------------------------------------------------ #
# Prints trace report to file and the current options available for this shell to the terminal,
# invoke via; -debug <command>, --debug <command>. Refer to Appendix 1
_print_set_debug() { exec 2>auditgradle.debug; set -xo; }
# ------------------------------------------------------------------------------------------------------------ #
# SECURITY --------------------------------------------------------------------------------------------------- #
# ------------------------------------------------------------------------------------------------------------ #
# TEST: as a parent function 'cd() { command cd "$@" >/dev/null; pwd; }'  is not invoked.
# IFS is set to null, field splitting is set to the default value. Refer to Appendix 2.
IFS=
# ------------------------------------------------------------------------------------------------------------ #
# Unset all possible aliases.
\unalias -a
# ------------------------------------------------------------------------------------------------------------ #
# Ensure env, getconf, and command are not a user function. Refer to Appendix 2.
unset -f env getconf command
# ------------------------------------------------------------------------------------------------------------ #
# Put on a reliable PATH prefix, as order matters.
# before: /usr/bin/*
# after: /bin/*
PATH="$(command -p getconf PATH):$PATH"
# ------------------------------------------------------------------------------------------------------------ #
# STANDARD OUT ----------------------------------------------------------------------------------------------- #
# ------------------------------------------------------------------------------------------------------------ #
# Prints information to the users terminal. Refer to Appendix 3.
_print_info() { env printf "%s \n" "$*" 1>&2; }
# ------------------------------------------------------------------------------------------------------------ #
# Invoke via; -h, -help, --help, -\?.
_print_help() {
cat 0<<_EOF_

Usage: ${0##*/} [-h|-help|--help] |
                      [[-g|-get|--get] <version> <checksum>] |
                      [[-c|-check|--check] <version>] |
                      [[-u|-upgrade|--upgrade] <version> <checksum>]

  -g, -get, --get

      VERSION="\$2" CHECKSUM="\$3"

      $ gradle wrapper \\
          --gradle-version "\$VERSION" \\
          --distribution-type all \\
          --gradle-distribution-sha256-sum "\$CHECKSUM"

      # -------------------------------------------------------------------- #
        Gradle 2021, Gradle distribution and wrapper JAR checksum reference,
          viewed 04 February 2021, <https://gradle.org/release-checksums/>
      # -------------------------------------------------------------------- #
        Integrated Development Environment (IDE) pulls:
        Complete (-all|-bin) ZIP Checksum
          gradle/wrapper/gradle.wrapper.properties
      # -------------------------------------------------------------------- #
        Command Line Interface (CLI) pulls:
        Wrapper JAR Checksum
          $ ./gradlew tasks
      # -------------------------------------------------------------------- #

  -c, -check, --check

      VERSION="\$2"

      $ cd \$PWD/gradle/wrapper

      $ wget --tries=1 \\
             --no-cookies \\
             --max-redirect=0 \\
             --secure-protocol=TLSv1_2 \\
             --append-output=auditgradle.log \\
             --output-document=gradle-wrapper.jar.sha256 \\
             --header="Accept: application/gzip, application/octet-stream" \\
             https://downloads.gradle-dn.com/distributions/gradle-"\$VERSION"-wrapper.jar.sha256

      $ echo "  gradle-wrapper.jar" >> gradle-wrapper.jar.sha256
      $ sha256sum --check gradle-wrapper.jar.sha256

      # -------------------------------------------------------------------- #

   -u, -upgrade, --upgrade

      VERSION="\$2" CHECKSUM="\$3"

      $ ./gradlew wrapper \\
                  --gradle-version "\$VERSION" \\
                  --distribution-type all \\
                  --gradle-distribution-sha256-sum "\$CHECKSUM"

      Run the following commands:
      $ jcmd -l
      > org.gradle.launcher.daemon.bootstrap.GradleDaemon \$VERSION

      $ ./gradlew --stop
      > Stopping Daemon(s)
      > 1 Daemon stopped

_EOF_
}
# -------------------------------------------------------------------------------------------------- #
# SHORT CIRCUIT ------------------------------------------------------------------------------------ #
# -------------------------------------------------------------------------------------------------- #
# Zero positional arguments upon initialisation prints the help message to the users terminal.
if [ $# -eq 0 ]; then _print_help 2>/dev/null; exit 1; fi
# -------------------------------------------------------------------------------------------------- #
# An adjunct check for system utilities to process the calls made by this script.
# Refer to Appendix 4.
_check_dependency() {
  if ! (type "$1" >/dev/null 2>&1); then
    _print_info "ERROR: missing dependency can't find $1"
    exit 1
  fi
}
# -------------------------------------------------------------------------------------------------- #
# DOMAIN ------------------------------------------------------------------------------------------- #
# -------------------------------------------------------------------------------------------------- #
# From the present working directory check the user's path includes gradle/wrapper/ and neither
# directory is a symbolic representation. Then call the Gradle host with wget and output a https
# auditgradle.log and the gradle-wrapper.jar.sha256 file. Refer to Appendix 5.
_check_gradle_wrapper() {
  version="$1"
  root=$(env pwd -P)
  location="$root/gradle/wrapper"
  # ------------------------------------------------------------------------------------------------ #
  if [ -d "$location" ];
    then if [ -h "$root"/gradle ] || [ -h "$root"/gradle/wrapper ];
           then _print_info "Either directory is a symbolic link: \$PWD/gradle/wrapper"; exit 1;
           else cd -P "$location" 2>/dev/null || { _print_info "cd action failed"; exit 1; }
         fi
    else _print_info "No such directory: \$PWD/gradle/wrapper"; exit 1;
  fi
  # ------------------------------------------------------------------------------------------------ #
  _print_info "downloading $version of gradle-wrapper.jar.sha256..."
  # ------------------------------------------------------------------------------------------------ #
  wget --tries=1 \
       --no-cookies \
       --max-redirect=0 \
       --secure-protocol=TLSv1_2 \
       --output-document=gradle-wrapper.jar.sha256 \
       --append-output="$root/auditgradle.log" \
       --header="Accept: application/gzip, application/octet-stream" \
       https://downloads.gradle-dn.com/distributions/gradle-"$version"-wrapper.jar.sha256
  # ------------------------------------------------------------------------------------------------ #
  _print_info "wget's auditgradle.log in $root..."
}
# -------------------------------------------------------------------------------------------------- #
# From the present working directory use the system's Gradle distribution to pull a gradle
# wrapper version with the associated checksum. Distribution type all enables both IDE
# code-completion and Gradle documentation.
_get_gradle_wrapper() {
  version="$1"
  checksum="$2"
  # ------------------------------------------------------------------------------------------------ #
  _print_info "Wrapper JAR Checksum: $checksum"
  _print_info "Downloading $version of gradle-wrapper.jar..."
  # ------------------------------------------------------------------------------------------------ #
  gradle wrapper \
         --gradle-version "$version" \
         --distribution-type all \
         --gradle-distribution-sha256-sum "$checksum"
}
# -------------------------------------------------------------------------------------------------- #
# In the present working directory find the projects gradlew executable. Then update to the
# gradle wrapper version with the associated checksum. Distribution type all enables both IDE
# code-completion and Gradle documentation. Refer to Appendix 6.
_upgrade_gradle_wrapper() {
  version="$1"
  checksum="$2"
  # ------------------------------------------------------------------------------------------------ #
  gw=$(find -P . -maxdepth 1 -mindepth 1 -mount -executable -name 'gradlew' -readable -type f)

  if [ -z "$gw" ]; then _print_info "ERROR: gradlew not found in current working directory"; exit 1; fi
  # ------------------------------------------------------------------------------------------------ #
  _print_info "Wrapper JAR Checksum: $checksum"
  _print_info "Downloading $version of gradle-wrapper.jar..."
  # ------------------------------------------------------------------------------------------------ #
  $gw wrapper \
      --gradle-version "$version" \
      --distribution-type all \
      --gradle-distribution-sha256-sum "$checksum"
}
# -------------------------------------------------------------------------------------------------- #
# From the projects gradle/wrapper directory check the local gradle-wrapper.jar checksum.
# Refer to Appendix 7.
_shasum_gradle_wrapper() {
  _print_info "checking sha256 of gradle-wrapper.jar..."
  # ------------------------------------------------------------------------------------------------ #
  echo "  gradle-wrapper.jar" 1>> gradle-wrapper.jar.sha256
  # ------------------------------------------------------------------------------------------------ #
  sha256sum --check gradle-wrapper.jar.sha256
  # ------------------------------------------------------------------------------------------------ #
  SHA256=$(cat gradle-wrapper.jar.sha256)
  SHA256SUM=$(sha256sum gradle-wrapper.jar)
  # ------------------------------------------------------------------------------------------------ #
  _print_info "1) sha256:    $SHA256"
  _print_info "2) sha256sum: $SHA256SUM"
  # ------------------------------------------------------------------------------------------------ #
  rm --interactive=once --verbose  gradle-wrapper.jar.sha256
}
# -------------------------------------------------------------------------------------------------- #
# CALL STACK --------------------------------------------------------------------------------------- #
# -------------------------------------------------------------------------------------------------- #
# Check the parent system has the required utilities installed; wget, sha256sum,
# and make a call to two script functions.
_run_check() {
  _print_info "Running: check gradle wrapper"
  # ------------------------------------------------------------------------------------------------ #
  _check_dependency wget
  _check_dependency sha256sum
  # ------------------------------------------------------------------------------------------------ #
  _check_gradle_wrapper "$1"
  _shasum_gradle_wrapper
}
# -------------------------------------------------------------------------------------------------- #
# Check the parent system has the required utility installed; gradle, and call a script function.
_run_get() {
  _print_info "Running: get gradle wrapper"
  # ------------------------------------------------------------------------------------------------ #
  _check_dependency gradle
  # ------------------------------------------------------------------------------------------------ #
  _get_gradle_wrapper "$1" "$2"
}
# -------------------------------------------------------------------------------------------------- #
# Run the scripts function to upgrade the gradle-wrapper.jar.
_run_upgrade() {
  _print_info "Running: upgrade gradle wrapper"
  # ------------------------------------------------------------------------------------------------ #
  _upgrade_gradle_wrapper "$1" "$2"
}
# -------------------------------------------------------------------------------------------------- #
# HELPER FUNCTIONS --------------------------------------------------------------------------------- #
# -------------------------------------------------------------------------------------------------- #
# Checks regular expression for natural numbers:
#   <digit>.<digit> or <digit>.<digit>.<digit>
#   <digit><digit>.<digit><digit> or <digit><digit>.<digit><digit>.<digit><digit>
#
# Pattern conforms to the Gradle continuum policy. Refer to Appendix 8.
_confirm_version() {
  VERSION=$( expr "$1" : '\(^\([0-9]\{1,2\}\.[0-9]\{1,2\}\.[0-9]\{1,2\}\)$\|^\([0-9]\{1,2\}\.[0-9]\{1,2\}\)$\)' 2>/dev/null )
}
# -------------------------------------------------------------------------------------------------- #
# Checksum must conform to a length of 64 containing only Hindu-Arabic Numerals and
# the English language lowercase letters. Refer to Appendix 8.
_confirm_checksum() {
  CHECKSUM=$( expr "$1" : '^\([0-9a-z]\{64\}\)$' 2>/dev/null )
}
# -------------------------------------------------------------------------------------------------- #
# The user's input checksum conforms to a length of 64. Refer to Appendix 8.
_confirm_length() {
  return $( expr length "$1" = 64 >/dev/null 2>&1 );
}
# -------------------------------------------------------------------------------------------------- #
VERSION_MSG="Version format is numerical (i.e., 5.2 also 6.8.3): <digit>.<digit>.?<digit>?"
# These are global variables, duplication at the function level is intentional for flexibility.
VERSION=
CHECKSUM=
# -------------------------------------------------------------------------------------------------- #
# ENTRY POINT -------------------------------------------------------------------------------------- #
# -------------------------------------------------------------------------------------------------- #
if [ "$1" = "-debug" ] || [ "$1" = "--debug" ]; then shift; _print_set_debug; fi
# -------------------------------------------------------------------------------------------------- #
case "$1" in
  ( -c | -check | --check )
      if [ "$2" ];
        then maybe="$2"
             _confirm_version "$maybe"
          if [ "$VERSION" = "$maybe" ];
            then _run_check "$VERSION"
            else _print_info "$VERSION_MSG"
          fi
        else _print_info "Option requires an argument: $1 <version>"
      fi
      ;;
  ( -g | -get | --get )
      if [ "$2" ] && [ "$3" ];
        then maybe="$2"
             _confirm_version "$maybe"
          if [ "$VERSION" = "$maybe" ];
            then kinda="$3"
              if ( _confirm_length "$kinda" );
                then _confirm_checksum "$kinda"
                  if [ "$CHECKSUM" = "$kinda" ];
                    then _run_get "$VERSION" "$CHECKSUM"
                    else _print_info "Invalid gradle checksum: $3"
                  fi
                else _print_info "Invalid checksum length: $3"
              fi
            else _print_info "$VERSION_MSG"
          fi
        else _print_info "Option requires two arguments: $1 <version> <checksum>"
      fi
      ;;
  ( -h | -help | --help | -\? )
      _print_help 1>&2
      ;;
  ( -u | -upgrade | --upgrade )
      if [ "$2" ] && [ "$3" ];
        then maybe="$2"
             _confirm_version "$maybe"
          if [ "$VERSION" = "$maybe" ];
            then kinda="$3"
              if ( _confirm_length "$kinda" );
                then _confirm_checksum "$kinda"
                  if [ "$CHECKSUM" = "$kinda" ];
                    then _run_upgrade "$VERSION" "$CHECKSUM"
                    else _print_info "Invalid gradle checksum: $3"
                  fi
                else _print_info "Invalid checksum length: $3"
              fi
            else _print_info "$VERSION_MSG"
          fi
        else _print_info "Option requires two arguments: $1 <version> <checksum>"
      fi
      ;;
  ( * )
      _print_info "Option $1 invalid argument."
      ;;
esac
exit $?
# -------------------------------------------------------------------------------------------------- #
# APPENDICES --------------------------------------------------------------------------------------- #
# -------------------------------------------------------------------------------------------------- #
# Appendix 1 - _print_set_debug
#
#              set -o
#                     Write the current settings of the options to standard output in an unspecified format.
#
#              set -x
#                     The shell shall write to standard error a trace for each command after it expands the command and before it executes it.
#
#              example:
#                       $ ./auditgradle.sh -debug -check 6.8.3
#                       $ less auditgradle.debug
#
#              The Open Group 2018, set - set or unset options and positional parameters, Base Specifications Issue 7, 2018 edt,
#                      viewed 24 March 2021, https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_25
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 2 - IFS='<space><tab><newline>'
#
#              If IFS is not set, it shall behave as normal for an unset variable, except that
#              field splitting by the shell and line splitting by the read utility shall be
#              performed as if the value of IFS is <space> <tab> <newline> (2.5.3 Shell Variables).
#
#              NOTE: Text editors are subject to user configurations (kakoune/editorconfig), also
#                    repositories or transit may introduce anomalies. Double check IFS environment
#                    variable prior to running scripts.
#
#              IEEE and The Open Group 2018, 8. Environment Variables, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap08.html
#
#              IEEE and The Open Group 2018, IFS, 2.5.3 Shell Variables, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
#
#              IEEE and The Open Group 2018, env - set the environment for command invocation, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/env.html
#
#              IEEE and The Open Group 2018, getconf - get configuration values, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/getconf.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 3 - _print_info
#
#              printf: Due to shell aliases and built-in printf functions, using an unadorned printf
#                      interactively or in a script may get you different functionality [...]. Invoke
#                      it via env (i.e., env printf …) to avoid interference from the shell.
#
#              GNU 2020, 15.2 printf: Format and print data, GNU Coreutils, viewed 24 March 2021,
#                        https://www.gnu.org/software/coreutils/manual/html_node/printf-invocation.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 4 - _check_dependency
#
#              The conditional statement: if ! $(builtin command -V "$1" >/dev/null 2>&1) ; then
#
#              Path traversal provides an inconsequential location for the
#              utility. Therefore, command substitution jargon and disposable.
#              In addition to, the -V flag is dispensable as the output is not
#              read by the user.
#
#
#              Command Search and Execution
#              --------------------------------------------------------------
#               A) 2.14. Special Built-In Utilities
#                  break     exec      set
#                  colon     exit      shift
#                  continue  export    times
#                  dot       readonly  trap
#                  eval      return    unset
#              --------------------------------------------------------------
#               B)
#                 alloc     comparguments  comptry     history  pushd
#                 autoload  compcall       compvalues  hist     readarray
#                 bind      compctl        declare     let      repeat
#                 bindkey   compdescribe   dirs        local    savehistory
#                 builtin   compfiles      disable     login    source
#                 bye       compgen        disown      logout   shopt
#                 caller    compgroups     dosh        map      stop
#                 cap       complete       echotc      mapfile  suspend
#                 chdir     compquote      echoti      popd     typeset
#                 clone     comptags       help        print    whence
#              ---------------------------------------------------------------
#               C) 2.9.5 Function Definition Command
#              ---------------------------------------------------------------
#               D)
#                 alias    false    hash    pwd   unalias
#                 bg       fc       jobs    read  wait
#                 cd       fg       kill    true
#                 command  getopts  newgrp  umask
#              ---------------------------------------------------------------
#               E)
#                 path
#              ---------------------------------------------------------------
#
#              IEEE and The Open Group 2018, Command Search and Execution, viewed 28 March 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_01_01
#
#              IEEE and The Open Group 2018, command - execute a simple command, viewed 28 March 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/command.html
#
#              IEEE and The Open Group 2018, sh - shell, the standard command language interpreter, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/sh.html
#
#              IEEE and The Open Group 2018, type - write a description of command type, viewed 12 April 2021,
#                                            https://pubs.opengroup.org/onlinepubs/9699919799/utilities/type.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 5 - _check_gradle_wrapper
#
#              env --chdir=GUARANTEED_PATH, path is unsubstantiated: `pwd`/gradle/wrapper
#
#              The default behaviour is --physical; pwd, realpath, cd.
#              realpath --no-symlinks does not --canonicalize-existing path for symlinks.
#              cd -P into a physical directory not it's symbolic representation.
#
#              If either directories; gradle, gradle/wrapper is a symbolic then a base directory
#                 is not honoured given the below utility function:
#              realpath --canonicalize-existing --physical --relative-base="$root" gradle/wrapper
#
#              Redirects +1
#              Referer: https://services.gradle.org/distributions/
#              Host: downloads.gradle-dn.com
#              As at 30 March 2021, wget --secure-protocol=TLSv1_3 is unsupported at host
#
#              wget --input-file=If there are URLs both on the command line and in an input file, those on the
#                                command lines will be the first ones to be retrieved.
#
#              pwd: Due to shell aliases and built-in pwd functions, using an unadorned pwd
#                   interactively or in a script may get you different functionality [...].
#                   Invoke it via env (i.e., env pwd …) to avoid interference from the shell.
#
#              $ env --ignore-environment --debug pwd -P
#              > cleaning environ
#              > executing: pwd
#              >   arg[0]= ‘pwd’
#              >   arg[1]= ‘-P’
#
#              GNU 2021, 2.4 Logging and Input File Options, GNU Wget 1.21.1-dirty Manual, viewed 23 March 2021,
#                        https://www.gnu.org/software/wget/manual/html_node/Logging-and-Input-File-Options.html
#
#              GNU 2020, 19.1 pwd: Print working directory, Coreutils - GNU core utilities, viewed 23 March 2021,
#                        https://www.gnu.org/software/coreutils/manual/html_node/pwd-invocation.html
#
#              GNU 2020, 23.2 env: Run a command in a modified environment, Coreutils - GNU core utilities, viewed 23 March 2021,
#                        https://www.gnu.org/software/coreutils/manual/html_node/env-invocation.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 6 - _upgrade_gradle_wrapper
#
#              find (Options Globals Tests)
#                 where
#                      Options = -P
#                      Globals = -maxdepth 1 -mindepth 1 -mount
#                      Tests   = -executable -name 'gradlew' -readable -type f
#
#              GNU 2021, 8.1 Invoking find, GNU Findutils 4.8.0, viewed 29 March 2021,
#                        https://www.gnu.org/software/findutils/manual/html_node/find_html/Invoking-find.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 7 - _shasum_gradle_wrapper
#
#              rm --interactive=once prompts the user for confirmation if three or more files require deletion
#
#              GNU 2020, 11.5 rm: Remove files or directories, GNU Coreutils Manual, viewed 29 March 2021,
#                        https://www.gnu.org/software/coreutils/manual/html_node/rm-invocation.html
#
# -------------------------------------------------------------------------------------------------- #
# Appendix 8 - _confirm_version, _confirm_checksum, _confirm_length
#
#              version has;  three groups, separated by '.' , start [0-9], middle [0-9], end [0-9]
#              version regex: (expr '6.8.3' : '^\([0-9]\{1,2\}\.[0-9]\{1,2\}\.[0-9]\{1,2\}\)$') returns match or null
#
#              checksum has; a fixed length, no separation characters, contains [0-9a-z]
#              checksum regex: (expr '7faa7198769f872826c8ef4f1450f839ec27f0b4d5d1e51bade63667cbccd205' : '^\([0-9a-z]\{64\}\)$') return match or null
#              checksum length: (expr length '7faa7198769f872826c8ef4f1450f839ec27f0b4d5d1e51bade63667cbccd205' = 64) returns 1 or 0
#
#              length redirection: 2>/dev/null; returns 0 or 1 or null
#              length redirection: >/dev/null 2>&1; true or returns 1 or 2
#
#              GNU 2020, 16.4 expr: Evaluate expressions, GNU Coreutils Manual, viewed 31 March 2021,
#                        https://www.gnu.org/software/coreutils/manual/html_node/expr-invocation.html
#
#              gradle-completion 2017, Security risk (?): Backticks in descriptions are attempted to eval!, Issue #91, gradle/gradle-completion,
#                                      viewed 10 March 2021, https://github.com/gradle/gradle-completion/issues/91
#
# -------------------------------------------------------------------------------------------------- #

Whats Gradle? :thinking: