1#!/bin/sh
2#
3# Copyright 2020, Data61, CSIRO (ABN 41 687 119 230)
4#
5# SPDX-License-Identifier: BSD-2-Clause
6#
7
8# Exit on unhandled error exit statuses; turn off filename expansion
9# ("globbing"); warn on expansion of unset parameters.
10set -efu
11
12PROGNAME=${0##*/}
13DIRNAME=${0%/*}
14
15die () {
16    echo "$PROGNAME: fatal error: $*" >&2
17    exit 3
18}
19
20is_shell_script () {
21    FILE=$1
22    # Use parameter expansion tricks as a poor man's pattern matcher; if the
23    # expansion does not mutate the parameter, then it _didn't_ match the
24    # pattern.
25    if [ "${FILE%.bash}" != "$FILE" ] \
26        || [ "${FILE%.ksh}" != "$FILE" ] \
27        || [ "${FILE%.sh}" != "$FILE" ]
28    then
29        # It's claiming to be a Bourne-based script; believe it.
30        return 0
31    fi
32
33    # If it's executable, let file(1) do the hard work of figuring out what kind
34    # of program it is.
35    if [ -x "$FILE" ]
36    then
37        DESCRIPTION=$(file -b "$FILE")
38
39        case "$DESCRIPTION" in
40            # Catch POSIX, Korn, Bourne-Again, and Z shell scripts.
41            ("* shell script*"|"* zsh script*")
42                return 0
43                ;;
44            # Catch indirections through env.  Mind ksh88 and ksh93.
45            ("*env *sh script*"|"*env ksh88 script*"|"*env ksh93 script*")
46                return 0
47                ;;
48        esac
49    fi
50
51    # All checks failed; report that the argument is not a shell script.
52    return 1
53}
54
55# Amusingly, this script won't validate itself with "command -v"; the
56# "-v" flag was part of the "User Portability Utilities option" in POSIX
57# Issue 6 (2004), but only promoted to full mandatory status in Issue 7
58# (2017).  So instead use "which" (a Debianism, but widely available)
59# until Debian's checkbashisms catches up.
60#
61#if ! command -v checkbashisms > /dev/null
62if ! which checkbashisms > /dev/null
63then
64    die "\"checkbashisms\" command not found; is the \"devscripts\" package" \
65        "installed?"
66fi
67
68if ! which file > /dev/null
69then
70    die "\"file\" command not found; is the \"file\" package installed?"
71fi
72
73if ! which python3 > /dev/null
74then
75    die "\"python3\" command not found; is the \"python3\" package installed?"
76fi
77
78if [ -f .stylefilter ]
79then
80    set -- $(python3 "$DIRNAME"/filter.py -f .stylefilter "$@")
81fi
82
83for FILE
84do
85    # Is the current file a shell script?  If not, skip it.
86    is_shell_script "$FILE" || continue
87
88    # --force checks for non-POSIX constructs even in scripts that declare bash
89    # as the interpreter (useful because we don't want any bash scripts).
90    #
91    # --posix forces full-POSIX checking, ignoring the couple of exceptions to
92    # POSIX-compliance permitted by Debian policy.
93    #
94    # --extra prints out the offending line after the diagnostic.
95    if ! checkbashisms --force --posix --extra "$FILE"
96    then
97        die "script \"$FILE\" has non-POSIX features"
98    fi
99
100    # Now use the shell's own internal parser to find more subtle problems.
101    # Even this is not perfect because in this mode, the shell fails to perform
102    # many expansions due to possible side effects.  Also, the shell language is
103    # not decidable.  :-|  (See Trienen, Jennerod, "Mining Debian Maintainer
104    # Scripts",
105    # https://debconf18.debconf.org/talks/90-mining-debian-maintainer-scripts/ )
106    #
107    # Here's an example of a construct sh -n doesn't catch that fails when run
108    # for real:
109    #
110    # [[ a =~ a* ]] || echo a does not equal a
111    #
112    # ...and more forgivably, stupid eval tricks like this:
113    #
114    # STRING='${ARRAY_DEREF[1]}'; eval echo $STRING
115    if ! sh -n "$FILE"
116    then
117        die "script \"$FILE\" failed syntax check"
118    fi
119done
120