8 Commits

Author SHA1 Message Date
Vidar Holen
21462b11b3 Merge branch 'doctest-new-build' of https://github.com/phadej/shellcheck into phadej-doctest-new-build 2018-12-16 18:33:21 -08:00
Oleg Grenrus
75949fe51e Change prop> to >>> prop $
As doctest doesn't make QuickCheck related magic, but only
evaluates expressions: We are fast.
2018-10-17 19:29:10 +03:00
Oleg Grenrus
d510a3ef6c Guard quickcheck.1 regeneration
Without this guard it will be regenerate on each run,
and cabal new-build considers whole package dirty.
2018-09-10 19:44:06 +03:00
Oleg Grenrus
5516596b26 Fix quicktest, add note to it and quickrun 2018-09-10 19:41:15 +03:00
Oleg Grenrus
9e7539c10b Use native codegen in docker build, results in smaller binary 2018-09-10 19:09:28 +03:00
Oleg Grenrus
a5a7b332f1 Use LLVM + split-sections 2018-09-10 19:09:28 +03:00
Oleg Grenrus
a68e3aeb26 Striptests is not necessary anymore 2018-09-10 19:09:28 +03:00
Oleg Grenrus
259b1a5dc6 Run tests as doctests 2018-09-10 19:09:28 +03:00
43 changed files with 2405 additions and 4614 deletions

View File

@@ -1,82 +0,0 @@
#!/bin/bash
_cleanup(){
rm -rf dist shellcheck || true
}
build_linux() {
# Linux Docker image
name="$DOCKER_BASE"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
docker build -t "$name:current" .
docker run "$name:current" --version
printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
id=$(docker create "$name:current")
docker cp "$id:/bin/shellcheck" "shellcheck"
docker rm "$id"
ls -l shellcheck
./shellcheck myscript
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64";
done
# Linux Alpine based Docker image
name="$DOCKER_BASE-alpine"
DOCKER_BUILDS="$DOCKER_BUILDS $name"
sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
docker build -f Dockerfile.alpine -t "$name:current" .
docker run "$name:current" sh -c 'shellcheck --version'
_cleanup
}
build_aarch64() {
# Linux aarch64 static executable
docker run -v "$PWD:/mnt" koalaman/aarch64-builder 'buildsc'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-aarch64"
done
}
build_armv6hf() {
# Linux armv6hf static executable
docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
for tag in $TAGS
do
cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf";
done
_cleanup
}
build_windows() {
# Windows .exe
docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
for tag in $TAGS
do
cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe";
done
_cleanup
}
build_osx() {
# Darwin x86_64 static executable
brew install cabal-install pandoc gnu-tar
sudo ln -s /usr/local/bin/gsha512sum /usr/local/bin/sha512sum
sudo ln -s /usr/local/bin/gtar /usr/local/bin/tar
export PATH="/usr/local/bin:$PATH"
cabal update
cabal install --dependencies-only
cabal build shellcheck
for tag in $TAGS
do
cp "dist/build/shellcheck/shellcheck" "deploy/shellcheck-$tag.darwin-x86_64";
done
_cleanup
}

View File

@@ -1,6 +0,0 @@
*
!LICENSE
!Setup.hs
!ShellCheck.cabal
!shellcheck.hs
!src

3
.gitignore vendored
View File

@@ -13,6 +13,9 @@ cabal-dev
cabal.sandbox.config cabal.sandbox.config
cabal.config cabal.config
.stack-work .stack-work
dist-newstyle/
.ghc.environment.*
cabal.project.local
### Snap ### ### Snap ###
/snap/.snapcraft/ /snap/.snapcraft/

View File

@@ -35,14 +35,6 @@ do
rm "shellcheck" rm "shellcheck"
done done
for file in *.linux-aarch64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.linux.aarch64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in *.linux-armv6hf for file in *.linux-armv6hf
do do
base="${file%.*}" base="${file%.*}"
@@ -51,15 +43,8 @@ do
rm "shellcheck" rm "shellcheck"
done done
for file in *.darwin-x86_64
do
base="${file%.*}"
cp "$file" "shellcheck"
tar -cJf "$base.darwin.x86_64.tar.xz" --transform="s:^:$base/:" README.txt LICENSE.txt shellcheck
rm "shellcheck"
done
for file in ./* for file in ./*
do do
sha512sum "$file" > "$file.sha512sum" sha512sum "$file" > "$file.sha512sum"
done done

View File

@@ -1,4 +1,3 @@
sudo: required sudo: required
language: sh language: sh
@@ -6,52 +5,67 @@ language: sh
services: services:
- docker - docker
matrix: before_install:
include: - DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
- os: linux - DOCKER_BUILDS=""
env: BUILD=linux - TAGS=""
- os: linux - test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
env: BUILD=windows - test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
- os: linux - echo "Tags are $TAGS"
env: BUILD=armv6hf
- os: linux
env: BUILD=aarch64
- os: osx
env: BUILD=osx
before_install: |
DOCKER_BASE="$DOCKER_USERNAME/shellcheck"
DOCKER_BUILDS=""
TAGS=""
test "$TRAVIS_BRANCH" = master && TAGS="$TAGS latest" || true
test -n "$TRAVIS_TAG" && TAGS="$TAGS stable $TRAVIS_TAG" || true
echo "Tags are $TAGS"
script: script:
- mkdir -p deploy - mkdir deploy
- source ./.compile_binaries # Remove all tests to reduce binary size
- ./striptests - ./striptests
- set -x; build_"$BUILD"; set +x; # Linux Docker image
- name="$DOCKER_BASE"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- docker build -t "$name:current" .
- docker run "$name:current" --version
- printf '%s\n' "#!/bin/sh" "echo 'hello world'" > myscript
- docker run -v "$PWD:/mnt" "$name:current" myscript
# Copy static executable from docker image
- id=$(docker create "$name:current")
- docker cp "$id:/bin/shellcheck" "shellcheck"
- docker rm "$id"
- ls -l shellcheck
- ./shellcheck myscript
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-x86_64"; done
# Linux Alpine based Docker image
- name="$DOCKER_BASE-alpine"
- DOCKER_BUILDS="$DOCKER_BUILDS $name"
- sed -e '/DELETE-MARKER/,$d' Dockerfile > Dockerfile.alpine
- docker build -f Dockerfile.alpine -t "$name:current" .
- docker run "$name:current" sh -c 'shellcheck --version'
# Linux armv6hf static executable
- docker run -v "$PWD:/mnt" koalaman/armv6hf-builder -c 'compile-shellcheck'
- for tag in $TAGS; do cp "shellcheck" "deploy/shellcheck-$tag.linux-armv6hf"; done
- rm -f shellcheck || true
# Windows .exe
- docker run --user="$UID" -v "$PWD:/appdata" koalaman/winghc cuib
- for tag in $TAGS; do cp "dist/build/ShellCheck/shellcheck.exe" "deploy/shellcheck-$tag.exe"; done
- rm -rf dist shellcheck || true
# Misc packaging
- ./.prepare_deploy - ./.prepare_deploy
after_success: | after_success:
if [ "$BUILD" = "linux" ]; then - docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD"
docker login -u="$DOCKER_USERNAME" -p="$DOCKER_PASSWORD" - for repo in $DOCKER_BUILDS;
for repo in $DOCKER_BUILDS; do do
for tag in $TAGS; do for tag in $TAGS;
do
echo "Deploying $repo:current as $repo:$tag..."; echo "Deploying $repo:current as $repo:$tag...";
docker tag "$repo:current" "$repo:$tag" || exit 1; docker tag "$repo:current" "$repo:$tag" || exit 1;
docker push "$repo:$tag" || exit 1; docker push "$repo:$tag" || exit 1;
done; done;
done; done;
fi
after_failure: | after_failure:
id - id
pwd - pwd
df -h - df -h
find . -name '*.log' -type f -exec grep "" /dev/null {} + - find . -name '*.log' -type f -exec grep "" /dev/null {} +
find . -ls - find . -ls
deploy: deploy:
provider: gcs provider: gcs

View File

@@ -1,36 +1,6 @@
## v0.7.0 - 2019-07-28 ## Since previous release
### Added ### Added
- Precompiled binaries for macOS and Linux aarch64
- Preliminary support for fix suggestions - Preliminary support for fix suggestions
- New `-f diff` unified diff format for auto-fixes
- Files containing Bats tests can now be checked
- Directory wide directives can now be placed in a `.shellcheckrc`
- Optional checks: Use `--list-optional` to show a list of tests,
Enable with `-o` flags or `enable=name` directives
- Source paths: Use `-P dir1:dir2` or a `source-path=dir1` directive
to specify search paths for sourced files.
- json1 format like --format=json but treats tabs as single characters
- Recognize FLAGS variables created by the shflags library.
- Site-specific changes can now be made in Custom.hs for ease of patching
- SC2154: Also warn about unassigned uppercase variables (optional)
- SC2252: Warn about `[ $a != x ] || [ $a != y ]`, similar to SC2055
- SC2251: Inform about ineffectual ! in front of commands
- SC2250: Warn about variable references without braces (optional)
- SC2249: Warn about `case` with missing default case (optional)
- SC2248: Warn about unquoted variables without special chars (optional)
- SC2247: Warn about $"(cmd)" and $"{var}"
- SC2246: Warn if a shebang's interpreter ends with /
- SC2245: Warn that Ksh ignores all but the first glob result in `[`
- SC2243/SC2244: Suggest using explicit -n for `[ $foo ]` (optional)
- SC1135: Suggest not ending double quotes just to make $ literal
### Changed
- If a directive or shebang is not specified, a `.bash/.bats/.dash/.ksh`
extension will be used to infer the shell type when present.
- Disabling SC2120 on a function now disables SC2119 on call sites
### Fixed
- SC2183 no longer warns about missing printf args for `%()T`
## v0.6.0 - 2018-12-02 ## v0.6.0 - 2018-12-02
### Added ### Added

View File

@@ -3,20 +3,53 @@ FROM ubuntu:18.04 AS build
USER root USER root
WORKDIR /opt/shellCheck WORKDIR /opt/shellCheck
# Install OS deps # Install OS deps, including GHC from HVR-PPA
RUN apt-get update && apt-get install -y ghc cabal-install # https://launchpad.net/~hvr/+archive/ubuntu/ghc
RUN apt-get -yq update \
&& apt-get -yq install software-properties-common \
&& apt-add-repository -y "ppa:hvr/ghc" \
&& apt-get -yq update \
&& apt-get -yq install cabal-install-2.4 ghc-8.4.3 pandoc \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/opt/ghc/bin:${PATH}"
# Use gold linker and check tools versions
RUN ln -s $(which ld.gold) /usr/local/bin/ld && \
cabal --version \
&& ghc --version \
&& ld --version
# Install Haskell deps # Install Haskell deps
# (This is a separate copy/run so that source changes don't require rebuilding) # (This is a separate copy/run so that source changes don't require rebuilding)
#
# We also patch regex-tdfa and aeson removing hard-coded -O2 flag.
# This makes compilation faster and binary smaller.
# Performance loss is unnoticeable for ShellCheck
#
# Remember to update versions, once in a while.
COPY ShellCheck.cabal ./ COPY ShellCheck.cabal ./
RUN cabal update && cabal install --dependencies-only --ghc-options="-optlo-Os -split-sections" RUN cabal update && \
cabal get regex-tdfa-1.2.3.1 && sed -i 's/-O2//' regex-tdfa-1.2.3.1/regex-tdfa.cabal && \
cabal get aeson-1.4.0.0 && sed -i 's/-O2//' aeson-1.4.0.0/aeson.cabal && \
echo 'packages: . regex-tdfa-1.2.3.1 aeson-1.4.0.0 > cabal.project' && \
cabal new-build --dependencies-only \
--disable-executable-dynamic --enable-split-sections --disable-tests
# Copy source and build it # Copy source and build it
COPY LICENSE Setup.hs shellcheck.hs ./ COPY LICENSE Setup.hs shellcheck.hs shellcheck.1.md ./
COPY src src COPY src src
RUN cabal build Paths_ShellCheck && \ COPY test test
ghc -optl-static -optl-pthread -isrc -idist/build/autogen --make shellcheck -split-sections -optc-Wl,--gc-sections -optlo-Os && \ # This SED is the only "nastyness" we have to do
strip --strip-all shellcheck # Hopefully soon we could add per-component ld-options to cabal.project
RUN sed -i 's/-- STATIC/ld-options: -static -pthread -Wl,--gc-sections/' ShellCheck.cabal && \
cat ShellCheck.cabal && \
cabal new-build \
--disable-executable-dynamic --enable-split-sections --disable-tests && \
cp $(find dist-newstyle -type f -name shellcheck) . && \
strip --strip-all shellcheck && \
file shellcheck && \
ls -l shellcheck
RUN mkdir -p /out/bin && \ RUN mkdir -p /out/bin && \
cp shellcheck /out/bin/ cp shellcheck /out/bin/

172
README.md
View File

@@ -8,45 +8,45 @@ ShellCheck is a GPLv3 tool that gives warnings and suggestions for bash/sh shell
The goals of ShellCheck are The goals of ShellCheck are
* To point out and clarify typical beginner's syntax issues that cause a shell - To point out and clarify typical beginner's syntax issues that cause a shell
to give cryptic error messages. to give cryptic error messages.
* To point out and clarify typical intermediate level semantic problems that - To point out and clarify typical intermediate level semantic problems that
cause a shell to behave strangely and counter-intuitively. cause a shell to behave strangely and counter-intuitively.
* To point out subtle caveats, corner cases and pitfalls that may cause an - To point out subtle caveats, corner cases and pitfalls that may cause an
advanced user's otherwise working script to fail under future circumstances. advanced user's otherwise working script to fail under future circumstances.
See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify! See [the gallery of bad code](README.md#user-content-gallery-of-bad-code) for examples of what ShellCheck can help you identify!
## Table of Contents ## Table of Contents
* [How to use](#how-to-use) - [How to use](#how-to-use)
* [On the web](#on-the-web) - [On the web](#on-the-web)
* [From your terminal](#from-your-terminal) - [From your terminal](#from-your-terminal)
* [In your editor](#in-your-editor) - [In your editor](#in-your-editor)
* [In your build or test suites](#in-your-build-or-test-suites) - [In your build or test suites](#in-your-build-or-test-suites)
* [Installing](#installing) - [Installing](#installing)
* [Compiling from source](#compiling-from-source) - [Travis CI](#travis-ci)
* [Installing Cabal](#installing-cabal) - [Compiling from source](#compiling-from-source)
* [Compiling ShellCheck](#compiling-shellcheck) - [Installing Cabal](#installing-cabal)
* [Running tests](#running-tests) - [Compiling ShellCheck](#compiling-shellcheck)
* [Gallery of bad code](#gallery-of-bad-code) - [Running tests](#running-tests)
* [Quoting](#quoting) - [Gallery of bad code](#gallery-of-bad-code)
* [Conditionals](#conditionals) - [Quoting](#quoting)
* [Frequently misused commands](#frequently-misused-commands) - [Conditionals](#conditionals)
* [Common beginner's mistakes](#common-beginners-mistakes) - [Frequently misused commands](#frequently-misused-commands)
* [Style](#style) - [Common beginner's mistakes](#common-beginners-mistakes)
* [Data and typing errors](#data-and-typing-errors) - [Style](#style)
* [Robustness](#robustness) - [Data and typing errors](#data-and-typing-errors)
* [Portability](#portability) - [Robustness](#robustness)
* [Miscellaneous](#miscellaneous) - [Portability](#portability)
* [Testimonials](#testimonials) - [Miscellaneous](#miscellaneous)
* [Ignoring issues](#ignoring-issues) - [Testimonials](#testimonials)
* [Reporting bugs](#reporting-bugs) - [Ignoring issues](#ignoring-issues)
* [Contributing](#contributing) - [Reporting bugs](#reporting-bugs)
* [Copyright](#copyright) - [Contributing](#contributing)
* [Other Resources](#other-resources) - [Copyright](#copyright)
## How to use ## How to use
@@ -54,7 +54,7 @@ There are a number of ways to use ShellCheck!
### On the web ### On the web
Paste a shell script on <https://www.shellcheck.net> for instant feedback. Paste a shell script on https://www.shellcheck.net for instant feedback.
[ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends! [ShellCheck.net](https://www.shellcheck.net) is always synchronized to the latest git commit, and is the easiest way to give ShellCheck a go. Tell your friends!
@@ -85,45 +85,8 @@ You can see ShellCheck suggestions directly in a variety of editors.
### In your build or test suites ### In your build or test suites
While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites. While ShellCheck is mostly intended for interactive use, it can easily be added to builds or test suites.
It makes canonical use of exit codes, so you can just add a `shellcheck` command as part of the process.
For example, in a Makefile: ShellCheck makes canonical use of exit codes, and can output simple JSON, CheckStyle compatible XML, GCC compatible warnings as well as human readable text (with or without ANSI colors). See the [Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
```Makefile
check-scripts:
# Fail if any of these files have warnings
shellcheck myscripts/*.sh
```
or in a Travis CI `.travis.yml` file:
```yaml
script:
# Fail if any of these files have warnings
- shellcheck myscripts/*.sh
```
Services and platforms that have ShellCheck pre-installed and ready to use:
* [Travis CI](https://travis-ci.org/)
* [Codacy](https://www.codacy.com/)
* [Code Climate](https://codeclimate.com/)
* [Code Factor](https://www.codefactor.io/)
Services and platforms with third party plugins:
* [SonarQube](https://www.sonarqube.org/) through [sonar-shellcheck-plugin](https://github.com/emerald-squad/sonar-shellcheck-plugin)
Most other services, including [GitLab](https://about.gitlab.com/), let you install
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
or by downloading and unpacking a [binary release](#installing-the-shellcheck-binary).
It's a good idea to manually install a specific ShellCheck version regardless. This avoids
any surprise build breaks when a new version with new warnings is published.
For customized filtering or reporting, ShellCheck can output simple JSON, CheckStyle compatible XML,
GCC compatible warnings as well as human readable text (with or without ANSI colors). See the
[Integration](https://github.com/koalaman/shellcheck/wiki/Integration) wiki page for more documentation.
## Installing ## Installing
@@ -178,23 +141,15 @@ On openSUSE
zypper in ShellCheck zypper in ShellCheck
Or use OneClickInstall - <https://software.opensuse.org/package/ShellCheck> Or use OneClickInstall - https://software.opensuse.org/package/ShellCheck
On Solus: On Solus:
eopkg install shellcheck eopkg install shellcheck
On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)): On Windows (via [scoop](http://scoop.sh)):
```cmd scoop install shellcheck
C:\> choco install shellcheck
```
Or Windows (via [scoop](http://scoop.sh)):
```cmd
C:\> scoop install shellcheck
```
From Snap Store: From Snap Store:
@@ -203,8 +158,8 @@ From Snap Store:
From Docker Hub: From Docker Hub:
```sh ```sh
docker run --rm -v "$PWD:/mnt" koalaman/shellcheck:stable myscript docker pull koalaman/shellcheck:stable # Or :v0.4.7 for that version, or :latest for daily builds
# Or :v0.4.7 for that version, or :latest for daily builds docker run -v "$PWD:/mnt" koalaman/shellcheck myscript
``` ```
or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled. or use `koalaman/shellcheck-alpine` if you want a larger Alpine Linux based image to extend. It works exactly like a regular Alpine image, but has shellcheck preinstalled.
@@ -213,39 +168,32 @@ Alternatively, you can download pre-compiled binaries for the latest release her
* [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked) * [Linux, x86_64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
* [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked) * [Linux, armv6hf](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
* [Linux, aarch64](https://storage.googleapis.com/shellcheck/shellcheck-stable.linux.armv6hf.tar.xz) aka ARM64 (statically linked)
* [MacOS, x86_64](https://shellcheck.storage.googleapis.com/shellcheck-stable.darwin.x86_64.tar.xz)
* [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip) * [Windows, x86](https://storage.googleapis.com/shellcheck/shellcheck-stable.zip)
or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds. or see the [storage bucket listing](https://shellcheck.storage.googleapis.com/index.html) for checksums, older versions and the latest daily builds.
Distro packages already come with a `man` page. If you are building from source, it can be installed with: Distro packages already come with a `man` page. If you are building from source, it can be installed with:
```console pandoc -s -t man shellcheck.1.md -o shellcheck.1
pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1 sudo mv shellcheck.1 /usr/share/man/man1
sudo mv shellcheck.1 /usr/share/man/man1
```
### Travis CI ## Travis CI
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it. Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
If you still want to do so in order to upgrade at your leisure or ensure you're If you still want to do so in order to upgrade at your leisure or ensure the latest release, follow the steps to install the shellcheck binary, bellow.
using the latest release, follow the steps below to install a binary version.
### Installing a pre-compiled binary ## Installing the shellcheck binary
The pre-compiled binaries come in `tar.xz` files. To decompress them, make sure *Pre-requisite*: the program 'xz' needs to be installed on the system.
`xz` is installed. To install it on debian/ubuntu/linux mint, run `apt install xz-utils`.
On Debian/Ubuntu/Mint, you can `apt install xz-utils`. To install it on Redhat/Fedora/CentOS, run `yum -y install xz`.
On Redhat/Fedora/CentOS, `yum -y install xz`.
A simple installer may do something like:
```bash ```bash
scversion="stable" # or "v0.4.7", or "latest" export scversion="stable" # or "v0.4.7", or "latest"
wget -qO- "https://storage.googleapis.com/shellcheck/shellcheck-${scversion?}.linux.x86_64.tar.xz" | tar -xJv wget "https://storage.googleapis.com/shellcheck/shellcheck-${scversion}.linux.x86_64.tar.xz"
cp "shellcheck-${scversion}/shellcheck" /usr/bin/ tar --xz -xvf shellcheck-"${scversion}".linux.x86_64.tar.xz
cp shellcheck-"${scversion}"/shellcheck /usr/bin/
shellcheck --version shellcheck --version
``` ```
@@ -259,9 +207,11 @@ ShellCheck is built and packaged using Cabal. Install the package `cabal-install
On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source. On MacOS (OS X), you can do a fast install of Cabal using brew, which takes a couple of minutes instead of more than 30 minutes if you try to compile it from source.
$ brew install cabal-install brew install cask
brew cask install haskell-platform
cabal install cabal-install
On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from <https://www.haskell.org/platform/> On MacPorts, the package is instead called `hs-cabal-install`, while native Windows users should install the latest version of the Haskell platform from https://www.haskell.org/platform/
Verify that `cabal` is installed and update its dependency list with Verify that `cabal` is installed and update its dependency list with
@@ -297,15 +247,12 @@ may use a legacy codepage. In `cmd.exe`, `powershell.exe` and Powershell ISE,
make sure to use a TrueType font, not a Raster font, and set the active make sure to use a TrueType font, not a Raster font, and set the active
codepage to UTF-8 (65001) with `chcp`: codepage to UTF-8 (65001) with `chcp`:
```cmd > chcp 65001
chcp 65001 Active code page: 65001
```
In Powershell ISE, you may need to additionally update the output encoding: In Powershell ISE, you may need to additionally update the output encoding:
```powershell > [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
```
### Running tests ### Running tests
@@ -483,13 +430,13 @@ Alexander Tarasikov,
Issues can be ignored via environmental variable, command line, individually or globally within a file: Issues can be ignored via environmental variable, command line, individually or globally within a file:
<https://github.com/koalaman/shellcheck/wiki/Ignore> https://github.com/koalaman/shellcheck/wiki/Ignore
## Reporting bugs ## Reporting bugs
Please use the GitHub issue tracker for any bugs or feature suggestions: Please use the GitHub issue tracker for any bugs or feature suggestions:
<https://github.com/koalaman/shellcheck/issues> https://github.com/koalaman/shellcheck/issues
## Contributing ## Contributing
@@ -504,12 +451,11 @@ The contributor retains the copyright.
ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE). ShellCheck is licensed under the GNU General Public License, v3. A copy of this license is included in the file [LICENSE](LICENSE).
Copyright 2012-2019, [Vidar 'koala_man' Holen](https://github.com/koalaman/) and contributors. Copyright 2012-2018, Vidar 'koala_man' Holen and contributors.
Happy ShellChecking! Happy ShellChecking!
## Other Resources
## Other Resources
* The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221). * The wiki has [long form descriptions](https://github.com/koalaman/shellcheck/wiki/Checks) for each warning, e.g. [SC2221](https://github.com/koalaman/shellcheck/wiki/SC2221).
* ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)! * ShellCheck does not attempt to enforce any kind of formatting or indenting style, so also check out [shfmt](https://github.com/mvdan/sh)!

View File

@@ -1,3 +1,8 @@
{-# LANGUAGE CPP #-}
{-# OPTIONS_GHC -Wall #-}
module Main (main) where
import Distribution.PackageDescription ( import Distribution.PackageDescription (
HookedBuildInfo, HookedBuildInfo,
emptyHookedBuildInfo ) emptyHookedBuildInfo )
@@ -9,12 +14,42 @@ import Distribution.Simple (
import Distribution.Simple.Setup ( SDistFlags ) import Distribution.Simple.Setup ( SDistFlags )
import System.Process ( system ) import System.Process ( system )
import System.Directory ( doesFileExist, getModificationTime )
#ifndef MIN_VERSION_cabal_doctest
#define MIN_VERSION_cabal_doctest(x,y,z) 0
#endif
#if MIN_VERSION_cabal_doctest(1,0,0)
import Distribution.Extra.Doctest ( addDoctestsUserHook )
main :: IO ()
main = defaultMainWithHooks $ addDoctestsUserHook "doctests" myHooks
where
myHooks = simpleUserHooks { preSDist = myPreSDist }
#else
#ifdef MIN_VERSION_Cabal
-- If the macro is defined, we have new cabal-install,
-- but for some reason we don't have cabal-doctest in package-db
--
-- Probably we are running cabal sdist, when otherwise using new-build
-- workflow
#warning You are configuring this package without cabal-doctest installed. \
The doctests test-suite will not work as a result. \
To fix this, install cabal-doctest before configuring.
#endif
main :: IO ()
main = defaultMainWithHooks myHooks main = defaultMainWithHooks myHooks
where where
myHooks = simpleUserHooks { preSDist = myPreSDist } myHooks = simpleUserHooks { preSDist = myPreSDist }
#endif
-- | This hook will be executed before e.g. @cabal sdist@. It runs -- | This hook will be executed before e.g. @cabal sdist@. It runs
-- pandoc to create the man page from shellcheck.1.md. If the pandoc -- pandoc to create the man page from shellcheck.1.md. If the pandoc
-- command is not found, this will fail with an error message: -- command is not found, this will fail with an error message:
@@ -27,10 +62,20 @@ main = defaultMainWithHooks myHooks
-- --
myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo myPreSDist :: Args -> SDistFlags -> IO HookedBuildInfo
myPreSDist _ _ = do myPreSDist _ _ = do
putStrLn "Building the man page (shellcheck.1) with pandoc..." exists <- doesFileExist "shellcheck.1"
putStrLn pandoc_cmd if exists
result <- system pandoc_cmd then do
putStrLn $ "pandoc exited with " ++ show result source <- getModificationTime "shellcheck.1.md"
target <- getModificationTime "shellcheck.1"
if target < source
then makeManPage
else putStrLn "shellcheck.1 is more recent than shellcheck.1.md"
else makeManPage
return emptyHookedBuildInfo return emptyHookedBuildInfo
where where
pandoc_cmd = "pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1" makeManPage = do
putStrLn "Building the man page (shellcheck.1) with pandoc..."
putStrLn pandoc_cmd
result <- system pandoc_cmd
putStrLn $ "pandoc exited with " ++ show result
pandoc_cmd = "pandoc -s -t man shellcheck.1.md -o shellcheck.1"

View File

@@ -1,5 +1,5 @@
Name: ShellCheck Name: ShellCheck
Version: 0.7.0 Version: 0.6.0
Synopsis: Shell script analysis tool Synopsis: Shell script analysis tool
License: GPL-3 License: GPL-3
License-file: LICENSE License-file: LICENSE
@@ -28,16 +28,14 @@ Extra-Source-Files:
shellcheck.1.md shellcheck.1.md
-- built with a cabal sdist hook -- built with a cabal sdist hook
shellcheck.1 shellcheck.1
-- convenience script for stripping tests
striptests
-- tests
test/shellcheck.hs
custom-setup custom-setup
setup-depends: setup-depends:
base >= 4 && <5, base >= 4 && <5,
process >= 1.0 && <1.7, directory >= 1.2 && <1.4,
Cabal >= 1.10 && <2.5 process >= 1.0 && <1.7,
cabal-doctest >= 1.0.6 && <1.1,
Cabal >= 1.10 && <2.5
source-repository head source-repository head
type: git type: git
@@ -49,21 +47,17 @@ library
build-depends: build-depends:
semigroups semigroups
build-depends: build-depends:
aeson,
array,
-- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode. -- GHC 7.6.3 (base 4.6.0.1) is buggy (#1131, #1119) in optimized mode.
-- Just disable that version entirely to fail fast. -- Just disable that version entirely to fail fast.
aeson,
base > 4.6.0.1 && < 5, base > 4.6.0.1 && < 5,
bytestring, bytestring,
containers >= 0.5, containers >= 0.5,
deepseq >= 1.4.0.0, deepseq >= 1.4.0.0,
Diff >= 0.2.0, directory,
directory >= 1.2.3.0,
mtl >= 2.2.1, mtl >= 2.2.1,
filepath,
parsec, parsec,
regex-tdfa, regex-tdfa,
QuickCheck >= 2.7.4,
-- When cabal supports it, move this to setup-depends: -- When cabal supports it, move this to setup-depends:
process process
exposed-modules: exposed-modules:
@@ -74,18 +68,13 @@ library
ShellCheck.AnalyzerLib ShellCheck.AnalyzerLib
ShellCheck.Checker ShellCheck.Checker
ShellCheck.Checks.Commands ShellCheck.Checks.Commands
ShellCheck.Checks.Custom
ShellCheck.Checks.ShellSupport ShellCheck.Checks.ShellSupport
ShellCheck.Data ShellCheck.Data
ShellCheck.Fixer
ShellCheck.Formatter.Format ShellCheck.Formatter.Format
ShellCheck.Formatter.CheckStyle ShellCheck.Formatter.CheckStyle
ShellCheck.Formatter.Diff
ShellCheck.Formatter.GCC ShellCheck.Formatter.GCC
ShellCheck.Formatter.JSON ShellCheck.Formatter.JSON
ShellCheck.Formatter.JSON1
ShellCheck.Formatter.TTY ShellCheck.Formatter.TTY
ShellCheck.Formatter.Quiet
ShellCheck.Interface ShellCheck.Interface
ShellCheck.Parser ShellCheck.Parser
ShellCheck.Regex ShellCheck.Regex
@@ -98,37 +87,31 @@ executable shellcheck
semigroups semigroups
build-depends: build-depends:
aeson, aeson,
array,
base >= 4 && < 5, base >= 4 && < 5,
bytestring, bytestring,
containers,
deepseq >= 1.4.0.0, deepseq >= 1.4.0.0,
Diff >= 0.2.0, ShellCheck,
directory >= 1.2.3.0, containers,
directory,
mtl >= 2.2.1, mtl >= 2.2.1,
filepath,
parsec >= 3.0, parsec >= 3.0,
QuickCheck >= 2.7.4, regex-tdfa
regex-tdfa,
ShellCheck
main-is: shellcheck.hs main-is: shellcheck.hs
test-suite test-shellcheck -- Marker to add flags for static linking
type: exitcode-stdio-1.0 -- STATIC
build-depends:
aeson,
array,
base >= 4 && < 5,
bytestring,
containers,
deepseq >= 1.4.0.0,
Diff >= 0.2.0,
directory >= 1.2.3.0,
mtl >= 2.2.1,
filepath,
parsec,
QuickCheck >= 2.7.4,
regex-tdfa,
ShellCheck
main-is: test/shellcheck.hs
test-suite doctests
type: exitcode-stdio-1.0
main-is: doctests.hs
build-depends:
base,
doctest >= 0.16.0 && <0.17,
QuickCheck >=2.11 && <2.13,
ShellCheck,
template-haskell
x-doctest-options: --fast
ghc-options: -Wall -threaded
hs-source-dirs: test

View File

@@ -3,3 +3,10 @@
# This allows testing changes without recompiling. # This allows testing changes without recompiling.
runghc -isrc -idist/build/autogen shellcheck.hs "$@" runghc -isrc -idist/build/autogen shellcheck.hs "$@"
# Note: with new-build you can
#
# % cabal new-run --disable-optimization -- shellcheck "$@"
#
# This does build the executable, but as the optimisation is disabled,
# the build is quite fast.

View File

@@ -1,15 +1,21 @@
#!/usr/bin/env bash #!/bin/bash
# quicktest runs the ShellCheck unit tests in an interpreted mode. # shellcheck disable=SC2091
# This allows running tests without compiling, which can be faster.
# quicktest runs the ShellCheck unit tests.
# Once `doctests` test executable is build, we can just run it
# This allows running tests without compiling library, which is faster.
# 'cabal test' remains the source of truth. # 'cabal test' remains the source of truth.
( $(find dist -type f -name doctests)
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
if [[ $var == *ExitSuccess* ]] # Note: if you have build the project with new-build
then #
exit 0 # % cabal new-build -w ghc-8.4.3 --enable-tests
else #
grep -C 3 -e "Fail" -e "Tracing" <<< "$var" # and have cabal-plan installed (e.g. with cabal new-install cabal-plan),
exit 1 # then you can quicktest with
fi #
) 2>&1 # % $(cabal-plan list-bin doctests)
#
# Once the test executable exists, we can simply run it to perform doctests
# which use GHCi under the hood.

View File

@@ -29,13 +29,14 @@ will warn that decimals are not supported.
+ For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will + For scripts starting with `#!/bin/ksh` (or using `-s ksh`), ShellCheck will
not warn at all, as `ksh` supports decimals in arithmetic contexts. not warn at all, as `ksh` supports decimals in arithmetic contexts.
# OPTIONS # OPTIONS
**-a**,\ **--check-sourced** **-a**,\ **--check-sourced**
: Emit warnings in sourced files. Normally, `shellcheck` will only warn : Emit warnings in sourced files. Normally, `shellcheck` will only warn
about issues in the specified files. With this option, any issues in about issues in the specified files. With this option, any issues in
sourced files will also be reported. sourced files files will also be reported.
**-C**[*WHEN*],\ **--color**[=*WHEN*] **-C**[*WHEN*],\ **--color**[=*WHEN*]
@@ -43,13 +44,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
is *auto*. **--color** without an argument is equivalent to is *auto*. **--color** without an argument is equivalent to
**--color=always**. **--color=always**.
**-i**\ *CODE1*[,*CODE2*...],\ **--include=***CODE1*[,*CODE2*...]
: Explicitly include only the specified codes in the report. Subsequent **-i**
options are cumulative, but all the codes can be specified at once,
comma-separated as a single argument. Include options override any provided
exclude options.
**-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...] **-e**\ *CODE1*[,*CODE2*...],\ **--exclude=***CODE1*[,*CODE2*...]
: Explicitly exclude the specified codes from the report. Subsequent **-e** : Explicitly exclude the specified codes from the report. Subsequent **-e**
@@ -62,39 +56,16 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
standard output. Subsequent **-f** options are ignored, see **FORMATS** standard output. Subsequent **-f** options are ignored, see **FORMATS**
below for more information. below for more information.
**--list-optional** **-S**\ *SEVERITY*,\ **--severity=***severity*
: Output a list of known optional checks. These can be enabled with **-o** : Specify minimum severity of errors to consider. Valid values are *error*,
flags or **enable** directives. *warning*, *info* and *style*. The default is *style*.
**--norc**
: Don't try to look for .shellcheckrc configuration files.
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
: Enable optional checks. The special name *all* enables all of them.
Subsequent **-o** options accumulate. This is equivalent to specifying
**enable** directives.
**-P**\ *SOURCEPATH*,\ **--source-path=***SOURCEPATH*
: Specify paths to search for sourced files, separated by `:` on Unix and
`;` on Windows. This is equivalent to specifying `search-path`
directives.
**-s**\ *shell*,\ **--shell=***shell* **-s**\ *shell*,\ **--shell=***shell*
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*. : Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
The default is to deduce the shell from the file's `shell` directive, The default is to use the file's shebang, or *bash* if the target shell
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to can't be determined.
POSIX `sh` (not the system's), and will warn of portability issues.
**-S**\ *SEVERITY*,\ **--severity=***severity*
: Specify minimum severity of errors to consider. Valid values in order of
severity are *error*, *warning*, *info* and *style*.
The default is *style*.
**-V**,\ **--version** **-V**,\ **--version**
@@ -112,10 +83,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
line (plus `/dev/null`). This option allows following any file the script line (plus `/dev/null`). This option allows following any file the script
may `source`. may `source`.
**FILES...**
: One or more script files to check, or "-" for standard input.
# FORMATS # FORMATS
@@ -152,59 +119,27 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
... ...
</checkstyle> </checkstyle>
**diff** **json**
: Auto-fixes in unified diff format. Can be piped to `git apply` or `patch -p1`
to automatically apply fixes.
--- a/test.sh
+++ b/test.sh
@@ -2,6 +2,6 @@
## Example of a broken script.
for f in $(ls *.m3u)
do
- grep -qi hq.*mp3 $f \
+ grep -qi hq.*mp3 "$f" \
&& echo -e 'Playlist $f contains a HQ file in mp3 format'
done
**json1**
: Json is a popular serialization format that is more suitable for web : Json is a popular serialization format that is more suitable for web
applications. ShellCheck's json is compact and contains only the bare applications. ShellCheck's json is compact and contains only the bare
minimum. Tabs are counted as 1 character. minimum.
{
comments: [
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
}
**json**
: This is a legacy version of the **json1** format. It's a raw array of
comments, and all offsets have a tab stop of 8.
**quiet**
: Suppress all normal output. Exit with zero if no issues are found,
otherwise exit with one. Stops processing after the first issue.
[
{
"file": "filename",
"line": lineNumber,
"column": columnNumber,
"level": "severitylevel",
"code": errorCode,
"message": "warning message"
},
...
]
# DIRECTIVES # DIRECTIVES
ShellCheck directives can be specified as comments in the shell script
ShellCheck directives can be specified as comments in the shell script. before a command or block:
If they appear before the first command, they are considered file-wide.
Otherwise, they apply to the immediately following command or block:
# shellcheck key=value key=value # shellcheck key=value key=value
command-or-structure command-or-structure
@@ -234,64 +169,17 @@ Valid keys are:
The command can be a simple command like `echo foo`, or a compound command The command can be a simple command like `echo foo`, or a compound command
like a function definition, subshell block or loop. like a function definition, subshell block or loop.
**enable**
: Enable an optional check by name, as listed with **--list-optional**.
Only file-wide `enable` directives are considered.
**source** **source**
: Overrides the filename included by a `source`/`.` statement. This can be : Overrides the filename included by a `source`/`.` statement. This can be
used to tell shellcheck where to look for a file whose name is determined used to tell shellcheck where to look for a file whose name is determined
at runtime, or to skip a source by telling it to use `/dev/null`. at runtime, or to skip a source by telling it to use `/dev/null`.
**source-path**
: Add a directory to the search path for `source`/`.` statements (by default,
only ShellCheck's working directory is included). Absolute paths will also
be rooted in these paths. The special path `SCRIPTDIR` can be used to
specify the currently checked script's directory, as in
`source-path=SCRIPTDIR` or `source-path=SCRIPTDIR/../libs`. Multiple
paths accumulate, and `-P` takes precedence over them.
**shell** **shell**
: Overrides the shell detected from the shebang. This is useful for : Overrides the shell detected from the shebang. This is useful for
files meant to be included (and thus lacking a shebang), or possibly files meant to be included (and thus lacking a shebang), or possibly
as a more targeted alternative to 'disable=2039'. as a more targeted alternative to 'disable=2039'.
# RC FILES
Unless `--norc` is used, ShellCheck will look for a file `.shellcheckrc` or
`shellcheckrc` in the script's directory and each parent directory. If found,
it will read `key=value` pairs from it and treat them as file-wide directives.
Here is an example `.shellcheckrc`:
# Look for 'source'd files relative to the checked script,
# and also look for absolute paths in /mnt/chroot
source-path=SCRIPTDIR
source-path=/mnt/chroot
# Turn on warnings for unquoted variables with safe values
enable=quote-safe-variables
# Turn on warnings for unassigned uppercase variables
enable=check-unassigned-uppercase
# Allow using `which` since it gives full paths and is common enough
disable=SC2230
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
will look in `~/.shellcheckrc` followed by the XDG config directory
(usually `~/.config/shellcheckrc`) on Unix, or `%APPDATA%/shellcheckrc` on
Windows. Only the first file found will be used.
Note for Snap users: the Snap sandbox disallows access to hidden files.
Use `shellcheckrc` without the dot instead.
Note for Docker users: ShellCheck will only be able to look for files that
are mounted in the container, so `~/.shellcheckrc` will not be read.
# ENVIRONMENT VARIABLES # ENVIRONMENT VARIABLES
The environment variable `SHELLCHECK_OPTS` can be set with default flags: The environment variable `SHELLCHECK_OPTS` can be set with default flags:
export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016' export SHELLCHECK_OPTS='--shell=bash --exclude=SC2016'
@@ -310,7 +198,6 @@ ShellCheck uses the follow exit codes:
+ 4: ShellCheck was invoked with bad options (e.g. unknown formatter). + 4: ShellCheck was invoked with bad options (e.g. unknown formatter).
# LOCALE # LOCALE
This version of ShellCheck is only available in English. All files are This version of ShellCheck is only available in English. All files are
leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid leniently decoded as UTF-8, with a fallback of ISO-8859-1 for invalid
sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for sequences. `LC_CTYPE` is respected for output, and defaults to UTF-8 for
@@ -319,23 +206,20 @@ locales where encoding is unspecified (such as the `C` locale).
Windows users seeing `commitBuffer: invalid argument (invalid character)` Windows users seeing `commitBuffer: invalid argument (invalid character)`
should set their terminal to use UTF-8 with `chcp 65001`. should set their terminal to use UTF-8 with `chcp 65001`.
# AUTHORS # AUTHOR
ShellCheck is written and maintained by Vidar Holen.
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
long list of wonderful contributors.
# REPORTING BUGS # REPORTING BUGS
Bugs and issues can be reported on GitHub: Bugs and issues can be reported on GitHub:
https://github.com/koalaman/shellcheck/issues https://github.com/koalaman/shellcheck/issues
# COPYRIGHT # COPYRIGHT
Copyright 2012-2015, Vidar Holen.
Copyright 2012-2019, Vidar Holen and contributors.
Licensed under the GNU General Public License version 3 or later, Licensed under the GNU General Public License version 3 or later,
see https://gnu.org/licenses/gpl.html see https://gnu.org/licenses/gpl.html
# SEE ALSO # SEE ALSO
sh(1) bash(1) sh(1) bash(1)

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,7 +17,6 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
import qualified ShellCheck.Analyzer
import ShellCheck.Checker import ShellCheck.Checker
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
@@ -25,12 +24,9 @@ import ShellCheck.Regex
import qualified ShellCheck.Formatter.CheckStyle import qualified ShellCheck.Formatter.CheckStyle
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Formatter.GCC import qualified ShellCheck.Formatter.GCC
import qualified ShellCheck.Formatter.JSON import qualified ShellCheck.Formatter.JSON
import qualified ShellCheck.Formatter.JSON1
import qualified ShellCheck.Formatter.TTY import qualified ShellCheck.Formatter.TTY
import qualified ShellCheck.Formatter.Quiet
import Control.Exception import Control.Exception
import Control.Monad import Control.Monad
@@ -50,7 +46,6 @@ import System.Console.GetOpt
import System.Directory import System.Directory
import System.Environment import System.Environment
import System.Exit import System.Exit
import System.FilePath
import System.IO import System.IO
data Flag = Flag String String data Flag = Flag String String
@@ -72,7 +67,6 @@ instance Monoid Status where
data Options = Options { data Options = Options {
checkSpec :: CheckSpec, checkSpec :: CheckSpec,
externalSources :: Bool, externalSources :: Bool,
sourcePaths :: [FilePath],
formatterOptions :: FormatterOptions, formatterOptions :: FormatterOptions,
minSeverity :: Severity minSeverity :: Severity
} }
@@ -80,7 +74,6 @@ data Options = Options {
defaultOptions = Options { defaultOptions = Options {
checkSpec = emptyCheckSpec, checkSpec = emptyCheckSpec,
externalSources = False, externalSources = False,
sourcePaths = [],
formatterOptions = newFormatterOptions { formatterOptions = newFormatterOptions {
foColorOption = ColorAuto foColorOption = ColorAuto
}, },
@@ -94,23 +87,11 @@ options = [
Option "C" ["color"] Option "C" ["color"]
(OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN") (OptArg (maybe (Flag "color" "always") (Flag "color")) "WHEN")
"Use color (auto, always, never)", "Use color (auto, always, never)",
Option "i" ["include"]
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
Option "e" ["exclude"] Option "e" ["exclude"]
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings", (ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
Option "f" ["format"] Option "f" ["format"]
(ReqArg (Flag "format") "FORMAT") $ (ReqArg (Flag "format") "FORMAT") $
"Output format (" ++ formatList ++ ")", "Output format (" ++ formatList ++ ")",
Option "" ["list-optional"]
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
Option "" ["norc"]
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
Option "o" ["enable"]
(ReqArg (Flag "enable") "check1,check2..")
"List of optional checks to enable (or 'all')",
Option "P" ["source-path"]
(ReqArg (Flag "source-path") "SOURCEPATHS")
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
Option "s" ["shell"] Option "s" ["shell"]
(ReqArg (Flag "shell") "SHELLNAME") (ReqArg (Flag "shell") "SHELLNAME")
"Specify dialect (sh, bash, dash, ksh)", "Specify dialect (sh, bash, dash, ksh)",
@@ -121,13 +102,10 @@ options = [
(NoArg $ Flag "version" "true") "Print version information", (NoArg $ Flag "version" "true") "Print version information",
Option "W" ["wiki-link-count"] Option "W" ["wiki-link-count"]
(ReqArg (Flag "wiki-link-count") "NUM") (ReqArg (Flag "wiki-link-count") "NUM")
"The number of wiki links to show, when applicable", "The number of wiki links to show, when applicable.",
Option "x" ["external-sources"] Option "x" ["external-sources"]
(NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES", (NoArg $ Flag "externals" "true") "Allow 'source' outside of FILES"
Option "" ["help"]
(NoArg $ Flag "help" "true") "Show this usage summary and exit"
] ]
getUsageInfo = usageInfo usageHeader options
printErr = lift . hPutStrLn stderr printErr = lift . hPutStrLn stderr
@@ -136,18 +114,15 @@ parseArguments argv =
case getOpt Permute options argv of case getOpt Permute options argv of
(opts, files, []) -> return (opts, files) (opts, files, []) -> return (opts, files)
(_, _, errors) -> do (_, _, errors) -> do
printErr $ concat errors ++ "\n" ++ getUsageInfo printErr $ concat errors ++ "\n" ++ usageInfo usageHeader options
throwError SyntaxFailure throwError SyntaxFailure
formats :: FormatterOptions -> Map.Map String (IO Formatter) formats :: FormatterOptions -> Map.Map String (IO Formatter)
formats options = Map.fromList [ formats options = Map.fromList [
("checkstyle", ShellCheck.Formatter.CheckStyle.format), ("checkstyle", ShellCheck.Formatter.CheckStyle.format),
("diff", ShellCheck.Formatter.Diff.format options),
("gcc", ShellCheck.Formatter.GCC.format), ("gcc", ShellCheck.Formatter.GCC.format),
("json", ShellCheck.Formatter.JSON.format), ("json", ShellCheck.Formatter.JSON.format),
("json1", ShellCheck.Formatter.JSON1.format), ("tty", ShellCheck.Formatter.TTY.format options)
("tty", ShellCheck.Formatter.TTY.format options),
("quiet", ShellCheck.Formatter.Quiet.format options)
] ]
formatList = intercalate ", " names formatList = intercalate ", " names
@@ -292,30 +267,10 @@ parseOption flag options =
} }
} }
Flag "include" str -> do
new <- mapM parseNum $ filter (not . null) $ split ',' str
let old = csIncludedWarnings . checkSpec $ options
return options {
checkSpec = (checkSpec options) {
csIncludedWarnings =
if null new
then old
else Just new `mappend` old
}
}
Flag "version" _ -> do Flag "version" _ -> do
liftIO printVersion liftIO printVersion
throwError NoProblems throwError NoProblems
Flag "list-optional" _ -> do
liftIO printOptional
throwError NoProblems
Flag "help" _ -> do
liftIO $ putStrLn getUsageInfo
throwError NoProblems
Flag "externals" _ -> Flag "externals" _ ->
return options { return options {
externalSources = True externalSources = True
@@ -329,12 +284,6 @@ parseOption flag options =
} }
} }
Flag "source-path" str -> do
let paths = splitSearchPath str
return options {
sourcePaths = (sourcePaths options) ++ paths
}
Flag "sourced" _ -> Flag "sourced" _ ->
return options { return options {
checkSpec = (checkSpec options) { checkSpec = (checkSpec options) {
@@ -358,26 +307,7 @@ parseOption flag options =
} }
} }
Flag "norc" _ -> _ -> return options
return options {
checkSpec = (checkSpec options) {
csIgnoreRC = True
}
}
Flag "enable" value ->
let cs = checkSpec options in return options {
checkSpec = cs {
csOptionalChecks = (csOptionalChecks cs) ++ split ',' value
}
}
-- This flag is handled specially in 'process'
Flag "format" _ -> return options
Flag str _ -> do
printErr $ "Internal error for --" ++ str ++ ". Please file a bug :("
return options
where where
die s = do die s = do
printErr s printErr s
@@ -392,16 +322,12 @@ parseOption flag options =
ioInterface options files = do ioInterface options files = do
inputs <- mapM normalize files inputs <- mapM normalize files
cache <- newIORef emptyCache cache <- newIORef emptyCache
configCache <- newIORef ("", Nothing)
return SystemInterface { return SystemInterface {
siReadFile = get cache inputs, siReadFile = get cache inputs
siFindSource = findSourceFile inputs (sourcePaths options),
siGetConfig = getConfig configCache
} }
where where
emptyCache :: Map.Map FilePath String emptyCache :: Map.Map FilePath String
emptyCache = Map.empty emptyCache = Map.empty
get cache inputs file = do get cache inputs file = do
map <- readIORef cache map <- readIORef cache
case Map.lookup file map of case Map.lookup file map of
@@ -418,6 +344,7 @@ ioInterface options files = do
return $ Right contents return $ Right contents
) `catch` handler ) `catch` handler
else return $ Left (file ++ " was not specified as input (see shellcheck -x).") else return $ Left (file ++ " was not specified as input (see shellcheck -x).")
where where
handler :: IOException -> IO (Either ErrorMessage String) handler :: IOException -> IO (Either ErrorMessage String)
handler ex = return . Left $ show ex handler ex = return . Left $ show ex
@@ -435,82 +362,6 @@ ioInterface options files = do
fallback :: FilePath -> IOException -> IO FilePath fallback :: FilePath -> IOException -> IO FilePath
fallback path _ = return path fallback path _ = return path
-- Returns the name and contents of .shellcheckrc for the given file
getConfig cache filename = do
path <- normalize filename
let dir = takeDirectory path
(previousPath, result) <- readIORef cache
if dir == previousPath
then return result
else do
paths <- getConfigPaths dir
result <- findConfig paths
writeIORef cache (dir, result)
return result
findConfig paths =
case paths of
(file:rest) -> do
contents <- readConfig file
if isJust contents
then return contents
else findConfig rest
[] -> return Nothing
-- Get a list of candidate filenames. This includes .shellcheckrc
-- in all parent directories, plus the user's home dir and xdg dir.
-- The dot is optional for Windows and Snap users.
getConfigPaths dir = do
let next = takeDirectory dir
rest <- if next /= dir
then getConfigPaths next
else defaultPaths `catch`
((const $ return []) :: IOException -> IO [FilePath])
return $ (dir </> ".shellcheckrc") : (dir </> "shellcheckrc") : rest
defaultPaths = do
home <- getAppUserDataDirectory "shellcheckrc"
xdg <- getXdgDirectory XdgConfig "shellcheckrc"
return [home, xdg]
readConfig file = do
exists <- doesFileExist file
if exists
then do
(contents, _) <- inputFile file `catch` handler file
return $ Just (file, contents)
else
return Nothing
where
handler :: FilePath -> IOException -> IO (String, Bool)
handler file err = do
putStrLn $ file ++ ": " ++ show err
return ("", True)
andM a b arg = do
first <- a arg
if not first then return False else b arg
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
if isAbsolute original
then
let (_, relative) = splitDrive original
in find relative original
else
find original original
where
find filename deflt = do
sources <- filterM ((allowable inputs) `andM` doesFileExist) $
(adjustPath filename):(map (</> filename) $ map adjustPath $ sourcePathFlag ++ sourcePathAnnotation)
case sources of
[] -> return deflt
(first:_) -> return first
scriptdir = dropFileName currentScript
adjustPath str =
case (splitDirectories str) of
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
_ -> str
inputFile file = do inputFile file = do
(handle, shouldCache) <- (handle, shouldCache) <-
if file == "-" if file == "-"
@@ -567,14 +418,3 @@ printVersion = do
putStrLn $ "version: " ++ shellcheckVersion putStrLn $ "version: " ++ shellcheckVersion
putStrLn "license: GNU General Public License, version 3" putStrLn "license: GNU General Public License, version 3"
putStrLn "website: https://www.shellcheck.net" putStrLn "website: https://www.shellcheck.net"
printOptional = do
mapM f list
where
list = sortOn cdName ShellCheck.Analyzer.optionalChecks
f item = do
putStrLn $ "name: " ++ cdName item
putStrLn $ "desc: " ++ cdDescription item
putStrLn $ "example: " ++ cdPositive item
putStrLn $ "fix: " ++ cdNegative item
putStrLn ""

View File

@@ -23,8 +23,7 @@ description: |
# snap connect shellcheck:removable-media # snap connect shellcheck:removable-media
version: git version: git
base: core18 grade: devel
grade: stable
confinement: strict confinement: strict
apps: apps:
@@ -35,11 +34,11 @@ apps:
parts: parts:
shellcheck: shellcheck:
plugin: dump plugin: dump
source: . source: ./
build-packages: build-packages:
- cabal-install - cabal-install
- squid - squid3
override-build: | build: |
# See comments in .snapsquid.conf # See comments in .snapsquid.conf
[ "$http_proxy" ] && { [ "$http_proxy" ] && {
squid3 -f .snapsquid.conf squid3 -f .snapsquid.conf
@@ -49,6 +48,6 @@ parts:
cabal sandbox init cabal sandbox init
cabal update || cat /var/log/squid/* cabal update || cat /var/log/squid/*
cabal install -j cabal install -j
install: |
install -d $SNAPCRAFT_PART_INSTALL/usr/bin install -d $SNAPCRAFT_PART_INSTALL/usr/bin
install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin install .cabal-sandbox/bin/shellcheck $SNAPCRAFT_PART_INSTALL/usr/bin

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -76,7 +76,7 @@ data Token =
| T_DSEMI Id | T_DSEMI Id
| T_Do Id | T_Do Id
| T_DollarArithmetic Id Token | T_DollarArithmetic Id Token
| T_DollarBraced Id Bool Token | T_DollarBraced Id Token
| T_DollarBracket Id Token | T_DollarBracket Id Token
| T_DollarDoubleQuoted Id [Token] | T_DollarDoubleQuoted Id [Token]
| T_DollarExpansion Id [Token] | T_DollarExpansion Id [Token]
@@ -121,7 +121,7 @@ data Token =
| T_Rbrace Id | T_Rbrace Id
| T_Redirecting Id [Token] Token | T_Redirecting Id [Token] Token
| T_Rparen Id | T_Rparen Id
| T_Script Id Token [Token] -- Shebang T_Literal, followed by script. | T_Script Id String [Token]
| T_Select Id | T_Select Id
| T_SelectIn Id String [Token] [Token] | T_SelectIn Id String [Token] [Token]
| T_Semi Id | T_Semi Id
@@ -139,15 +139,12 @@ data Token =
| T_CoProcBody Id Token | T_CoProcBody Id Token
| T_Include Id Token | T_Include Id Token
| T_SourceCommand Id Token Token | T_SourceCommand Id Token Token
| T_BatsTest Id Token Token
deriving (Show) deriving (Show)
data Annotation = data Annotation =
DisableComment Integer DisableComment Integer
| EnableComment String
| SourceOverride String | SourceOverride String
| ShellOverride String | ShellOverride String
| SourcePath String
deriving (Show, Eq) deriving (Show, Eq)
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq) data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
@@ -253,7 +250,7 @@ analyze f g i =
delve (T_Function id a b name body) = d1 body $ T_Function id a b name delve (T_Function id a b name body) = d1 body $ T_Function id a b name
delve (T_Condition id typ token) = d1 token $ T_Condition id typ delve (T_Condition id typ token) = d1 token $ T_Condition id typ
delve (T_Extglob id str l) = dl l $ T_Extglob id str delve (T_Extglob id str l) = dl l $ T_Extglob id str
delve (T_DollarBraced id braced op) = d1 op $ T_DollarBraced id braced delve (T_DollarBraced id op) = d1 op $ T_DollarBraced id
delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str delve (T_HereDoc id d q str l) = dl l $ T_HereDoc id d q str
delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str delve (TC_And id typ str t1 t2) = d2 t1 t2 $ TC_And id typ str
@@ -279,7 +276,6 @@ analyze f g i =
delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id delve (T_CoProcBody id t) = d1 t $ T_CoProcBody id
delve (T_Include id script) = d1 script $ T_Include id delve (T_Include id script) = d1 script $ T_Include id
delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id delve (T_SourceCommand id includer t_include) = d2 includer t_include $ T_SourceCommand id
delve (T_BatsTest id name t) = d2 name t $ T_BatsTest id
delve t = return t delve t = return t
getId :: Token -> Id getId :: Token -> Id
@@ -323,7 +319,7 @@ getId t = case t of
T_NormalWord id _ -> id T_NormalWord id _ -> id
T_DoubleQuoted id _ -> id T_DoubleQuoted id _ -> id
T_DollarExpansion id _ -> id T_DollarExpansion id _ -> id
T_DollarBraced id _ _ -> id T_DollarBraced id _ -> id
T_DollarArithmetic id _ -> id T_DollarArithmetic id _ -> id
T_BraceExpansion id _ -> id T_BraceExpansion id _ -> id
T_ParamSubSpecialChar id _ -> id T_ParamSubSpecialChar id _ -> id
@@ -384,7 +380,6 @@ getId t = case t of
T_UnparsedIndex id _ _ -> id T_UnparsedIndex id _ _ -> id
TC_Empty id _ -> id TC_Empty id _ -> id
TA_Variable id _ _ -> id TA_Variable id _ _ -> id
T_BatsTest id _ _ -> id
blank :: Monad m => Token -> m () blank :: Monad m => Token -> m ()
blank = const $ return () blank = const $ return ()

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -81,7 +81,7 @@ oversimplify token =
(T_NormalWord _ l) -> [concat (concatMap oversimplify l)] (T_NormalWord _ l) -> [concat (concatMap oversimplify l)]
(T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)] (T_DoubleQuoted _ l) -> [concat (concatMap oversimplify l)]
(T_SingleQuoted _ s) -> [s] (T_SingleQuoted _ s) -> [s]
(T_DollarBraced _ _ _) -> ["${VAR}"] (T_DollarBraced _ _) -> ["${VAR}"]
(T_DollarArithmetic _ _) -> ["${VAR}"] (T_DollarArithmetic _ _) -> ["${VAR}"]
(T_DollarExpansion _ _) -> ["${VAR}"] (T_DollarExpansion _ _) -> ["${VAR}"]
(T_Backticked _ _) -> ["${VAR}"] (T_Backticked _ _) -> ["${VAR}"]
@@ -133,11 +133,11 @@ isUnquotedFlag token = fromMaybe False $ do
return $ "-" `isPrefixOf` str return $ "-" `isPrefixOf` str
-- Given a T_DollarBraced, return a simplified version of the string contents. -- Given a T_DollarBraced, return a simplified version of the string contents.
bracedString (T_DollarBraced _ _ l) = concat $ oversimplify l bracedString (T_DollarBraced _ l) = concat $ oversimplify l
bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)" bracedString _ = error "Internal shellcheck error, please report! (bracedString on non-variable)"
-- Is this an expansion of multiple items of an array? -- Is this an expansion of multiple items of an array?
isArrayExpansion t@(T_DollarBraced _ _ _) = isArrayExpansion t@(T_DollarBraced _ _) =
let string = bracedString t in let string = bracedString t in
"@" `isPrefixOf` string || "@" `isPrefixOf` string ||
not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string not ("#" `isPrefixOf` string) && "[@]" `isInfixOf` string
@@ -146,7 +146,7 @@ isArrayExpansion _ = False
-- Is it possible that this arg becomes multiple args? -- Is it possible that this arg becomes multiple args?
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
where where
f t@(T_DollarBraced _ _ _) = f t@(T_DollarBraced _ _) =
let string = bracedString t in let string = bracedString t in
"!" `isPrefixOf` string "!" `isPrefixOf` string
f (T_DoubleQuoted _ parts) = any f parts f (T_DoubleQuoted _ parts) = any f parts
@@ -351,14 +351,6 @@ isOnlyRedirection t =
isFunction t = case t of T_Function {} -> True; _ -> False isFunction t = case t of T_Function {} -> True; _ -> False
-- Bats tests are functions for the purpose of 'local' and such
isFunctionLike t =
case t of
T_Function {} -> True
T_BatsTest {} -> True
_ -> False
isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False isBraceExpansion t = case t of T_BraceExpansion {} -> True; _ -> False
-- Get the lists of commands from tokens that contain them, such as -- Get the lists of commands from tokens that contain them, such as
@@ -493,21 +485,8 @@ wordsCanBeEqual x y = fromMaybe True $
-- Is this an expansion that can be quoted, -- Is this an expansion that can be quoted,
-- e.g. $(foo) `foo` $foo (but not {foo,})? -- e.g. $(foo) `foo` $foo (but not {foo,})?
isQuoteableExpansion t = case t of isQuoteableExpansion t = case t of
T_DollarBraced {} -> True
_ -> isCommandSubstitution t
isCommandSubstitution t = case t of
T_DollarExpansion {} -> True T_DollarExpansion {} -> True
T_DollarBraceCommandExpansion {} -> True T_DollarBraceCommandExpansion {} -> True
T_Backticked {} -> True T_Backticked {} -> True
T_DollarBraced {} -> True
_ -> False _ -> False
-- Is this a T_Annotation that ignores a specific code?
isAnnotationIgnoringCode code t =
case t of
T_Annotation _ anns _ -> any hasNum anns
_ -> False
where
hasNum (DisableComment ts) = code == ts
hasNum _ = False

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,7 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
module ShellCheck.Analyzer (analyzeScript, ShellCheck.Analyzer.optionalChecks) where module ShellCheck.Analyzer (analyzeScript) where
import ShellCheck.Analytics import ShellCheck.Analytics
import ShellCheck.AnalyzerLib import ShellCheck.AnalyzerLib
@@ -25,7 +25,6 @@ import ShellCheck.Interface
import Data.List import Data.List
import Data.Monoid import Data.Monoid
import qualified ShellCheck.Checks.Commands import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport import qualified ShellCheck.Checks.ShellSupport
@@ -42,10 +41,5 @@ analyzeScript spec = newAnalysisResult {
checkers params = mconcat $ map ($ params) [ checkers params = mconcat $ map ($ params) [
ShellCheck.Checks.Commands.checker, ShellCheck.Checks.Commands.checker,
ShellCheck.Checks.Custom.checker,
ShellCheck.Checks.ShellSupport.checker ShellCheck.Checks.ShellSupport.checker
] ]
optionalChecks = mconcat $ [
ShellCheck.Analytics.optionalChecks
]

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -18,30 +18,29 @@
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.AnalyzerLib where module ShellCheck.AnalyzerLib where
import ShellCheck.AST
import ShellCheck.ASTLib
import ShellCheck.Data
import ShellCheck.Interface
import ShellCheck.Parser
import ShellCheck.Regex
import ShellCheck.AST import Control.Arrow (first)
import ShellCheck.ASTLib import Control.DeepSeq
import ShellCheck.Data import Control.Monad.Identity
import ShellCheck.Interface import Control.Monad.RWS
import ShellCheck.Parser import Control.Monad.State
import ShellCheck.Regex import Control.Monad.Writer
import Data.Char
import Data.List
import qualified Data.Map as Map
import Data.Maybe
import Data.Semigroup
import Control.Arrow (first) prop :: Bool -> IO ()
import Control.DeepSeq prop False = putStrLn "FAIL"
import Control.Monad.Identity prop True = return ()
import Control.Monad.RWS
import Control.Monad.State
import Control.Monad.Writer
import Data.Char
import Data.List
import Data.Maybe
import Data.Semigroup
import qualified Data.Map as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (maxSuccess, quickCheckWithResult, stdArgs)
type Analysis = AnalyzerM () type Analysis = AnalyzerM ()
type AnalyzerM a = RWS Parameters [TokenComment] Cache a type AnalyzerM a = RWS Parameters [TokenComment] Cache a
@@ -77,22 +76,14 @@ composeAnalyzers :: (a -> Analysis) -> (a -> Analysis) -> a -> Analysis
composeAnalyzers f g x = f x >> g x composeAnalyzers f g x = f x >> g x
data Parameters = Parameters { data Parameters = Parameters {
-- Whether this script has the 'lastpipe' option set/default. hasLastpipe :: Bool, -- Whether this script has the 'lastpipe' option set/default.
hasLastpipe :: Bool, hasSetE :: Bool, -- Whether this script has 'set -e' anywhere.
-- Whether this script has 'set -e' anywhere. variableFlow :: [StackData], -- A linear (bad) analysis of data flow
hasSetE :: Bool, parentMap :: Map.Map Id Token, -- A map from Id to parent Token
-- A linear (bad) analysis of data flow shellType :: Shell, -- The shell type, such as Bash or Ksh
variableFlow :: [StackData], shellTypeSpecified :: Bool, -- True if shell type was forced via flags
-- A map from Id to parent Token rootNode :: Token, -- The root node of the AST
parentMap :: Map.Map Id Token, tokenPositions :: Map.Map Id (Position, Position) -- map from token id to start and end position
-- The shell type, such as Bash or Ksh
shellType :: Shell,
-- True if shell type was forced via flags
shellTypeSpecified :: Bool,
-- The root node of the AST
rootNode :: Token,
-- map from token id to start and end position
tokenPositions :: Map.Map Id (Position, Position)
} deriving (Show) } deriving (Show)
-- TODO: Cache results of common AST ops here -- TODO: Cache results of common AST ops here
@@ -163,14 +154,11 @@ err id code str = addComment $ makeComment ErrorC id code str
info id code str = addComment $ makeComment InfoC id code str info id code str = addComment $ makeComment InfoC id code str
style id code str = addComment $ makeComment StyleC id code str style id code str = addComment $ makeComment StyleC id code str
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () warnWithFix id code str fix = addComment $
warnWithFix = addCommentWithFix WarningC let comment = makeComment WarningC id code str in
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m () comment {
styleWithFix = addCommentWithFix StyleC tcFix = Just fix
}
addCommentWithFix :: MonadWriter [TokenComment] m => Severity -> Id -> Code -> String -> Fix -> m ()
addCommentWithFix severity id code str fix =
addComment $ makeCommentWithFix severity id code str fix
makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
makeCommentWithFix severity id code str fix = makeCommentWithFix severity id code str fix =
@@ -183,7 +171,7 @@ makeCommentWithFix severity id code str fix =
makeParameters spec = makeParameters spec =
let params = Parameters { let params = Parameters {
rootNode = root, rootNode = root,
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec, shellType = fromMaybe (determineShell root) $ asShellType spec,
hasSetE = containsSetE root, hasSetE = containsSetE root,
hasLastpipe = hasLastpipe =
case shellType params of case shellType params of
@@ -192,7 +180,7 @@ makeParameters spec =
Sh -> False Sh -> False
Ksh -> True, Ksh -> True,
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec), shellTypeSpecified = isJust $ asShellType spec,
parentMap = getParentTree root, parentMap = getParentTree root,
variableFlow = getVariableFlow params root, variableFlow = getVariableFlow params root,
tokenPositions = asTokenPositions spec tokenPositions = asTokenPositions spec
@@ -206,7 +194,7 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
where where
isSetE t = isSetE t =
case t of case t of
T_Script _ (T_Literal _ str) _ -> str `matches` re T_Script _ str _ -> str `matches` re
T_SimpleCommand {} -> T_SimpleCommand {} ->
t `isUnqualifiedCommand` "set" && t `isUnqualifiedCommand` "set" &&
("errexit" `elem` oversimplify t || ("errexit" `elem` oversimplify t ||
@@ -227,21 +215,19 @@ containsLastpipe root =
_ -> False _ -> False
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh -- |
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh -- >>> prop $ determineShellTest "#!/bin/sh" == Sh
prop_determineShell2 = determineShellTest "" == Bash -- >>> prop $ determineShellTest "#!/usr/bin/env ksh" == Ksh
prop_determineShell3 = determineShellTest "#!/bin/sh -e" == Sh -- >>> prop $ determineShellTest "" == Bash
prop_determineShell4 = determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh -- >>> prop $ determineShellTest "#!/bin/sh -e" == Sh
prop_determineShell5 = determineShellTest "#shellcheck shell=sh\nfoo" == Sh -- >>> prop $ determineShellTest "#!/bin/ksh\n#shellcheck shell=sh\nfoo" == Sh
prop_determineShell6 = determineShellTest "#! /bin/sh" == Sh -- >>> prop $ determineShellTest "#shellcheck shell=sh\nfoo" == Sh
prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash -- >>> prop $ determineShellTest "#! /bin/sh" == Sh
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh -- >>> prop $ determineShellTest "#! /bin/ash" == Dash
determineShellTest = determineShell . fromJust . prRoot . pScript
determineShellTest = determineShellTest' Nothing determineShell t = fromMaybe Bash $ do
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
determineShell fallbackShell t = fromMaybe Bash $ do
shellString <- foldl mplus Nothing $ getCandidates t shellString <- foldl mplus Nothing $ getCandidates t
shellForExecutable shellString `mplus` fallbackShell shellForExecutable shellString
where where
forAnnotation t = forAnnotation t =
case t of case t of
@@ -252,7 +238,7 @@ determineShell fallbackShell t = fromMaybe Bash $ do
getCandidates (T_Annotation _ annotations s) = getCandidates (T_Annotation _ annotations s) =
map forAnnotation annotations ++ map forAnnotation annotations ++
[Just $ fromShebang s] [Just $ fromShebang s]
fromShebang (T_Script _ (T_Literal _ s) _) = executableFromShebang s fromShebang (T_Script _ s t) = executableFromShebang s
-- Given a string like "/bin/bash" or "/usr/bin/env dash", -- Given a string like "/bin/bash" or "/usr/bin/env dash",
-- return the shell basename like "bash" or "dash" -- return the shell basename like "bash" or "dash"
@@ -390,6 +376,10 @@ isParentOf tree parent child =
parents params = getPath (parentMap params) parents params = getPath (parentMap params)
pathTo t = do
parents <- reader parentMap
return $ getPath parents t
-- Find the first match in a list where the predicate is Just True. -- Find the first match in a list where the predicate is Just True.
-- Stops if it's Just False and ignores Nothing. -- Stops if it's Just False and ignores Nothing.
findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a findFirst :: (a -> Maybe Bool) -> [a] -> Maybe a
@@ -433,7 +423,6 @@ getVariableFlow params t =
assignFirst T_ForIn {} = True assignFirst T_ForIn {} = True
assignFirst T_SelectIn {} = True assignFirst T_SelectIn {} = True
assignFirst (T_BatsTest {}) = True
assignFirst _ = False assignFirst _ = False
setRead t = setRead t =
@@ -451,7 +440,6 @@ leadType params t =
T_Backticked _ _ -> SubshellScope "`..` expansion" T_Backticked _ _ -> SubshellScope "`..` expansion"
T_Backgrounded _ _ -> SubshellScope "backgrounding &" T_Backgrounded _ _ -> SubshellScope "backgrounding &"
T_Subshell _ _ -> SubshellScope "(..) group" T_Subshell _ _ -> SubshellScope "(..) group"
T_BatsTest {} -> SubshellScope "@bats test"
T_CoProcBody _ _ -> SubshellScope "coproc" T_CoProcBody _ _ -> SubshellScope "coproc"
T_Redirecting {} -> T_Redirecting {} ->
if fromMaybe False causesSubshell if fromMaybe False causesSubshell
@@ -492,12 +480,6 @@ getModifiedVariables t =
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="] guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
return (t, t, name, DataString $ SourceFrom [rhs]) return (t, t, name, DataString $ SourceFrom [rhs])
T_BatsTest {} -> [
(t, t, "lines", DataArray SourceExternal),
(t, t, "status", DataString SourceInteger),
(t, t, "output", DataString SourceExternal)
]
-- Count [[ -v foo ]] as an "assignment". -- Count [[ -v foo ]] as an "assignment".
-- This is to prevent [ -v foo ] being unassigned or unused. -- This is to prevent [ -v foo ] being unassigned or unused.
TC_Unary id _ "-v" token -> maybeToList $ do TC_Unary id _ "-v" token -> maybeToList $ do
@@ -510,7 +492,7 @@ getModifiedVariables t =
guard . not . null $ str guard . not . null $ str
return (t, token, str, DataString SourceChecked) return (t, token, str, DataString SourceChecked)
T_DollarBraced _ _ l -> maybeToList $ do T_DollarBraced _ l -> maybeToList $ do
let string = bracedString t let string = bracedString t
let modifier = getBracedModifier string let modifier = getBracedModifier string
guard $ ":=" `isPrefixOf` modifier guard $ ":=" `isPrefixOf` modifier
@@ -546,6 +528,10 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
(not $ any (`elem` flags) ["f", "F"]) (not $ any (`elem` flags) ["f", "F"])
then concatMap getReference rest then concatMap getReference rest
else [] else []
"readonly" ->
if any (`elem` flags) ["f", "p"]
then []
else concatMap getReference rest
"trap" -> "trap" ->
case rest of case rest of
head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head head:_ -> map (\x -> (head, head, x)) $ getVariablesFromLiteralToken head
@@ -602,11 +588,6 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
"mapfile" -> maybeToList $ getMapfileArray base rest "mapfile" -> maybeToList $ getMapfileArray base rest
"readarray" -> maybeToList $ getMapfileArray base rest "readarray" -> maybeToList $ getMapfileArray base rest
"DEFINE_boolean" -> maybeToList $ getFlagVariable rest
"DEFINE_float" -> maybeToList $ getFlagVariable rest
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
"DEFINE_string" -> maybeToList $ getFlagVariable rest
_ -> [] _ -> []
where where
flags = map snd $ getAllFlags base flags = map snd $ getAllFlags base
@@ -658,11 +639,7 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
where where
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list) f ((_, Just "-v") : (t, Just var) : _) = return (base, t, var, DataString $ SourceFrom list)
where
(varName, varType) = case elemIndex '[' var of
Just i -> (take i var, DataArray)
Nothing -> (var, DataString)
f (_:rest) = f rest f (_:rest) = f rest
f [] = fail "not found" f [] = fail "not found"
@@ -676,22 +653,9 @@ getModifiedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal
return (base, lastArg, name, DataArray SourceExternal) return (base, lastArg, name, DataArray SourceExternal)
-- get all the array variables used in read, e.g. read -a arr -- get all the array variables used in read, e.g. read -a arr
getReadArrayVariables args = getReadArrayVariables args = do
map (getLiteralArray . snd) map (getLiteralArray . snd)
(filter (isArrayFlag . fst) (zip args (tail args))) (filter (\(x,_) -> getLiteralString x == Just "-a") (zip (args) (tail args)))
isArrayFlag x = fromMaybe False $ do
str <- getLiteralString x
return $ case str of
'-':'-':_ -> False
'-':str -> 'a' `elem` str
_ -> False
-- get the FLAGS_ variable created by a shflags DEFINE_ call
getFlagVariable (n:v:_) = do
name <- getLiteralString n
return (base, n, "FLAGS_" ++ name, DataString $ SourceExternal)
getFlagVariable _ = Nothing
getModifiedVariableCommand _ = [] getModifiedVariableCommand _ = []
@@ -702,10 +666,11 @@ getIndexReferences s = fromMaybe [] $ do
where where
re = mkRegex "(\\[.*\\])" re = mkRegex "(\\[.*\\])"
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"] -- |
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"] -- >>> prop $ getOffsetReferences ":bar" == ["bar"]
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"] -- >>> prop $ getOffsetReferences ":bar:baz" == ["bar", "baz"]
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"] -- >>> prop $ getOffsetReferences "[foo]:bar" == ["bar"]
-- >>> prop $ getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
getOffsetReferences mods = fromMaybe [] $ do getOffsetReferences mods = fromMaybe [] $ do
-- if mods start with [, then drop until ] -- if mods start with [, then drop until ]
match <- matchRegex re mods match <- matchRegex re mods
@@ -716,7 +681,7 @@ getOffsetReferences mods = fromMaybe [] $ do
getReferencedVariables parents t = getReferencedVariables parents t =
case t of case t of
T_DollarBraced id _ l -> let str = bracedString t in T_DollarBraced id l -> let str = bracedString t in
(t, t, getBracedReference str) : (t, t, getBracedReference str) :
map (\x -> (l, l, x)) ( map (\x -> (l, l, x)) (
getIndexReferences str getIndexReferences str
@@ -735,12 +700,6 @@ getReferencedVariables parents t =
then concatMap (getIfReference t) [lhs, rhs] then concatMap (getIfReference t) [lhs, rhs]
else [] else []
T_BatsTest {} -> [ -- pretend @test references vars to avoid warnings
(t, t, "lines"),
(t, t, "status"),
(t, t, "output")
]
t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo t@(T_FdRedirect _ ('{':var) op) -> -- {foo}>&- references and closes foo
[(t, t, takeWhile (/= '}') var) | isClosingFileOp op] [(t, t, takeWhile (/= '}') var) | isClosingFileOp op]
x -> getReferencedVariableCommand x x -> getReferencedVariableCommand x
@@ -786,21 +745,28 @@ isUnqualifiedCommand token str = isCommandMatch token (== str)
isCommandMatch token matcher = fromMaybe False $ isCommandMatch token matcher = fromMaybe False $
fmap matcher (getCommandName token) fmap matcher (getCommandName token)
-- |
-- Does this regex look like it was intended as a glob? -- Does this regex look like it was intended as a glob?
-- True: *foo* --
-- False: .*foo.* -- >>> isConfusedGlobRegex "*foo*"
-- True
--
-- >>> isConfusedGlobRegex ".*foo.*"
-- False
--
isConfusedGlobRegex :: String -> Bool isConfusedGlobRegex :: String -> Bool
isConfusedGlobRegex ('*':_) = True isConfusedGlobRegex ('*':_) = True
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True isConfusedGlobRegex [x,'*'] | x /= '\\' = True
isConfusedGlobRegex _ = False isConfusedGlobRegex _ = False
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
isVariableChar x = isVariableStartChar x || isDigit x isVariableChar x = isVariableStartChar x || isDigit x
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*" variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
prop_isVariableName1 = isVariableName "_fo123" -- |
prop_isVariableName2 = not $ isVariableName "4" -- >>> prop $ isVariableName "_fo123"
prop_isVariableName3 = not $ isVariableName "test: " -- >>> prop $ not $ isVariableName "4"
-- >>> prop $ not $ isVariableName "test: "
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
isVariableName _ = False isVariableName _ = False
@@ -809,27 +775,28 @@ getVariablesFromLiteralToken token =
-- Try to get referenced variables from a literal string like "$foo" -- Try to get referenced variables from a literal string like "$foo"
-- Ignores tons of cases like arithmetic evaluation and array indices. -- Ignores tons of cases like arithmetic evaluation and array indices.
prop_getVariablesFromLiteral1 = -- >>> prop $ getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral "$foo${bar//a/b}$BAZ" == ["foo", "bar", "BAZ"]
getVariablesFromLiteral string = getVariablesFromLiteral string =
map (!! 0) $ matchAllSubgroups variableRegex string map (!! 0) $ matchAllSubgroups variableRegex string
where where
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)" variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
-- |
-- Get the variable name from an expansion like ${var:-foo} -- Get the variable name from an expansion like ${var:-foo}
prop_getBracedReference1 = getBracedReference "foo" == "foo" --
prop_getBracedReference2 = getBracedReference "#foo" == "foo" -- >>> prop $ getBracedReference "foo" == "foo"
prop_getBracedReference3 = getBracedReference "#" == "#" -- >>> prop $ getBracedReference "#foo" == "foo"
prop_getBracedReference4 = getBracedReference "##" == "#" -- >>> prop $ getBracedReference "#" == "#"
prop_getBracedReference5 = getBracedReference "#!" == "!" -- >>> prop $ getBracedReference "##" == "#"
prop_getBracedReference6 = getBracedReference "!#" == "#" -- >>> prop $ getBracedReference "#!" == "!"
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo" -- >>> prop $ getBracedReference "!#" == "#"
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo" -- >>> prop $ getBracedReference "!foo#?" == "foo"
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo" -- >>> prop $ getBracedReference "foo-bar" == "foo"
prop_getBracedReference10= getBracedReference "foo: -1" == "foo" -- >>> prop $ getBracedReference "foo:-bar" == "foo"
prop_getBracedReference11= getBracedReference "!os*" == "" -- >>> prop $ getBracedReference "foo: -1" == "foo"
prop_getBracedReference12= getBracedReference "!os?bar**" == "" -- >>> prop $ getBracedReference "!os*" == ""
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo" -- >>> prop $ getBracedReference "!os?bar**" == ""
-- >>> prop $ getBracedReference "foo[bar]" == "foo"
getBracedReference s = fromMaybe s $ getBracedReference s = fromMaybe s $
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
where where
@@ -852,9 +819,10 @@ getBracedReference s = fromMaybe s $
return "" return ""
nameExpansion _ = Nothing nameExpansion _ = Nothing
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz" -- |
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo" -- >>> prop $ getBracedModifier "foo:bar:baz" == ":bar:baz"
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]" -- >>> prop $ getBracedModifier "!var:-foo" == ":-foo"
-- >>> prop $ getBracedModifier "foo[bar]" == "[bar]"
getBracedModifier s = fromMaybe "" . listToMaybe $ do getBracedModifier s = fromMaybe "" . listToMaybe $ do
let var = getBracedReference s let var = getBracedReference s
a <- dropModifier s a <- dropModifier s
@@ -871,10 +839,13 @@ getBracedModifier s = fromMaybe "" . listToMaybe $ do
-- Run an action in a Maybe (or do nothing). -- Run an action in a Maybe (or do nothing).
-- Example: -- Example:
--
-- @
-- potentially $ do -- potentially $ do
-- s <- getLiteralString cmd -- s <- getLiteralString cmd
-- guard $ s `elem` ["--recursive", "-r"] -- guard $ s `elem` ["--recursive", "-r"]
-- return $ warn .. "Something something recursive" -- return $ warn .. "Something something recursive"
-- @
potentially :: Monad m => Maybe (m ()) -> m () potentially :: Monad m => Maybe (m ()) -> m ()
potentially = fromMaybe (return ()) potentially = fromMaybe (return ())
@@ -901,17 +872,18 @@ filterByAnnotation asSpec params =
shouldIgnore note = shouldIgnore note =
any (shouldIgnoreFor (getCode note)) $ any (shouldIgnoreFor (getCode note)) $
getPath parents (T_Bang $ tcId note) getPath parents (T_Bang $ tcId note)
shouldIgnoreFor num (T_Annotation _ anns _) =
any hasNum anns
where
hasNum (DisableComment ts) = num == ts
hasNum _ = False
shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec shouldIgnoreFor _ T_Include {} = not $ asCheckSourced asSpec
shouldIgnoreFor code t = isAnnotationIgnoringCode code t shouldIgnoreFor _ _ = False
parents = parentMap params parents = parentMap params
getCode = cCode . tcComment getCode = cCode . tcComment
shouldIgnoreCode params code t =
any (isAnnotationIgnoringCode code) $
getPath (parentMap params) t
-- Is this a ${#anything}, to get string length or array count? -- Is this a ${#anything}, to get string length or array count?
isCountingReference (T_DollarBraced id _ token) = isCountingReference (T_DollarBraced id token) =
case concat $ oversimplify token of case concat $ oversimplify token of
'#':_ -> True '#':_ -> True
_ -> False _ -> False
@@ -920,7 +892,7 @@ isCountingReference _ = False
-- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"} -- FIXME: doesn't handle ${a:+$var} vs ${a:+"$var"}
isQuotedAlternativeReference t = isQuotedAlternativeReference t =
case t of case t of
T_DollarBraced _ _ _ -> T_DollarBraced _ _ ->
getBracedModifier (bracedString t) `matches` re getBracedModifier (bracedString t) `matches` re
_ -> False _ -> False
where where
@@ -958,17 +930,3 @@ getOpts flagTokenizer string cmd = process flags
else do else do
more <- process rest2 more <- process rest2
return $ (flag1, token1) : more return $ (flag1, token1) : more
supportsArrays shell = shell == Bash || shell == Ksh
-- Returns true if the shell is Bash or Ksh (sorry for the name, Ksh)
isBashLike :: Parameters -> Bool
isBashLike params =
case shellType params of
Bash -> True
Ksh -> True
Dash -> False
Sh -> False
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,8 +17,7 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-} module ShellCheck.Checker (checkScript) where
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Parser import ShellCheck.Parser
@@ -35,8 +34,6 @@ import qualified System.IO
import Prelude hiding (readFile) import Prelude hiding (readFile)
import Control.Monad import Control.Monad
import Test.QuickCheck.All
tokenToPosition startMap t = fromMaybe fail $ do tokenToPosition startMap t = fromMaybe fail $ do
span <- Map.lookup (tcId t) startMap span <- Map.lookup (tcId t) startMap
return $ newPositionedComment { return $ newPositionedComment {
@@ -48,17 +45,6 @@ tokenToPosition startMap t = fromMaybe fail $ do
where where
fail = error "Internal shellcheck error: id doesn't exist. Please report!" fail = error "Internal shellcheck error: id doesn't exist. Please report!"
shellFromFilename filename = foldl mplus Nothing candidates
where
shellExtensions = [(".ksh", Ksh)
,(".bash", Bash)
,(".bats", Bash)
,(".dash", Dash)]
-- The `.sh` is too generic to determine the shell:
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
candidates =
map (\(ext,sh) -> if ext `isSuffixOf` filename then Just sh else Nothing) shellExtensions
checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult checkScript :: Monad m => SystemInterface m -> CheckSpec -> m CheckResult
checkScript sys spec = do checkScript sys spec = do
results <- checkScript (csScript spec) results <- checkScript (csScript spec)
@@ -72,7 +58,6 @@ checkScript sys spec = do
psFilename = csFilename spec, psFilename = csFilename spec,
psScript = contents, psScript = contents,
psCheckSourced = csCheckSourced spec, psCheckSourced = csCheckSourced spec,
psIgnoreRC = csIgnoreRC spec,
psShellTypeOverride = csShellTypeOverride spec psShellTypeOverride = csShellTypeOverride spec
} }
let parseMessages = prComments result let parseMessages = prComments result
@@ -81,11 +66,9 @@ checkScript sys spec = do
as { as {
asScript = root, asScript = root,
asShellType = csShellTypeOverride spec, asShellType = csShellTypeOverride spec,
asFallbackShell = shellFromFilename $ csFilename spec,
asCheckSourced = csCheckSourced spec, asCheckSourced = csCheckSourced spec,
asExecutionMode = Executed, asExecutionMode = Executed,
asTokenPositions = tokenPositions, asTokenPositions = tokenPositions
asOptionalChecks = csOptionalChecks spec
} where as = newAnalysisSpec root } where as = newAnalysisSpec root
let analysisMessages = let analysisMessages =
fromMaybe [] $ fromMaybe [] $
@@ -96,13 +79,11 @@ checkScript sys spec = do
(parseMessages ++ map translator analysisMessages) (parseMessages ++ map translator analysisMessages)
shouldInclude pc = shouldInclude pc =
severity <= csMinSeverity spec && let code = cCode (pcComment pc)
case csIncludedWarnings spec of
Nothing -> code `notElem` csExcludedWarnings spec
Just includedWarnings -> code `elem` includedWarnings
where
code = cCode (pcComment pc)
severity = cSeverity (pcComment pc) severity = cSeverity (pcComment pc)
in
code `notElem` csExcludedWarnings spec &&
severity <= csMinSeverity spec
sortMessages = sortBy (comparing order) sortMessages = sortBy (comparing order)
order pc = order pc =
@@ -141,254 +122,132 @@ checkRecursive includes src =
csCheckSourced = True csCheckSourced = True
} }
checkOptionIncludes includes src = -- | Dummy binding for doctest to run
checkWithSpec [] emptyCheckSpec { --
csScript = src, -- >>> check "echo \"$12\""
csIncludedWarnings = includes, -- [1037]
csCheckSourced = True --
} -- >>> check "#shellcheck disable=SC1037\necho \"$12\""
-- []
checkWithRc rc = getErrors --
(mockRcFile rc $ mockedSystemInterface []) -- >>> check "#shellcheck disable=SC1037\n#lol\necho \"$12\""
-- []
checkWithIncludesAndSourcePath includes mapper = getErrors --
(mockedSystemInterface includes) { -- >>> check "echo $1"
siFindSource = mapper -- [2086]
} --
-- >>> check "#shellcheck disable=SC2086\necho $1"
prop_findsParseIssue = check "echo \"$12\"" == [1037] -- []
--
prop_commentDisablesParseIssue1 = -- >>> check "#shellcheck disable=SC2086\n#lol\necho $1"
null $ check "#shellcheck disable=SC1037\necho \"$12\"" -- []
prop_commentDisablesParseIssue2 = --
null $ check "#shellcheck disable=SC1037\n#lol\necho \"$12\"" -- >>> :{
-- getErrors
prop_findsAnalysisIssue = -- (mockedSystemInterface [])
check "echo $1" == [2086] -- emptyCheckSpec {
prop_commentDisablesAnalysisIssue1 = -- csScript = "echo $1",
null $ check "#shellcheck disable=SC2086\necho $1" -- csExcludedWarnings = [2148, 2086]
prop_commentDisablesAnalysisIssue2 = -- }
null $ check "#shellcheck disable=SC2086\n#lol\necho $1" -- :}
-- []
prop_optionDisablesIssue1 = --
null $ getErrors -- >>> :{
(mockedSystemInterface []) -- getErrors
emptyCheckSpec { -- (mockedSystemInterface [])
csScript = "echo $1", -- emptyCheckSpec {
csExcludedWarnings = [2148, 2086] -- csScript = "echo \"$10\"",
} -- csExcludedWarnings = [2148, 1037]
-- }
prop_optionDisablesIssue2 = -- :}
null $ getErrors -- []
(mockedSystemInterface []) --
emptyCheckSpec { -- >>> check "#!/usr/bin/python\ntrue $1\n"
csScript = "echo \"$10\"", -- [1071]
csExcludedWarnings = [2148, 1037] --
} -- >>> :{
-- getErrors
prop_wontParseBadShell = -- (mockedSystemInterface [])
[1071] == check "#!/usr/bin/python\ntrue $1\n" -- emptyCheckSpec {
-- csScript = "#!/usr/bin/python\ntrue\n",
prop_optionDisablesBadShebang = -- csShellTypeOverride = Just Sh
null $ getErrors -- }
(mockedSystemInterface []) -- :}
emptyCheckSpec { -- []
csScript = "#!/usr/bin/python\ntrue\n", --
csShellTypeOverride = Just Sh -- >>> check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n"
} -- []
--
prop_annotationDisablesBadShebang = -- >>> check "source /dev/null"
[] == check "#!/usr/bin/python\n# shellcheck shell=sh\ntrue\n" -- []
--
-- >>> check "source lol; echo \"$bar\""
prop_canParseDevNull = -- [1091,2154]
[] == check "source /dev/null" --
-- >>> checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\""
prop_failsWhenNotSourcing = -- []
[1091, 2154] == check "source lol; echo \"$bar\"" --
-- >>> checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\""
prop_worksWhenSourcing = -- []
null $ checkWithIncludes [("lib", "bar=1")] "source lib; echo \"$bar\"" --
-- >>> checkWithIncludes [("lib", "source lib")] "source lib"
prop_worksWhenSourcingWithDashDash = -- []
null $ checkWithIncludes [("lib", "bar=1")] "source -- lib; echo \"$bar\"" --
-- >>> checkWithIncludes [("lib", "for f; do")] "source lib; echo $1"
prop_worksWhenDotting = -- [1094,2086]
null $ checkWithIncludes [("lib", "bar=1")] ". lib; echo \"$bar\"" --
-- >>> checkWithIncludes [("lib", "")] ". \"$1\""
-- FIXME: This should really be giving [1093], "recursively sourced" -- [1090]
prop_noInfiniteSourcing = --
[] == checkWithIncludes [("lib", "source lib")] "source lib" -- >>> checkWithIncludes [("lib", "")] "source ~/foo"
-- [1090]
prop_canSourceBadSyntax = --
[1094, 2086] == checkWithIncludes [("lib", "for f; do")] "source lib; echo $1" -- >>> checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
-- []
prop_cantSourceDynamic = --
[1090] == checkWithIncludes [("lib", "")] ". \"$1\"" -- >>> checkRecursive [("lib", "echo $1")] "source lib"
-- [2086]
prop_cantSourceDynamic2 = --
[1090] == checkWithIncludes [("lib", "")] "source ~/foo" -- >>> checkRecursive [("lib", "echo \"$10\"")] "source lib"
-- [1037]
prop_canSourceDynamicWhenRedirected = --
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\"" -- >>> checkWithIncludes [("foo", "source bar"), ("bar", "baz=3")] "#shellcheck source=foo\n. \"$1\"; echo \"$baz\""
-- []
prop_recursiveAnalysis = --
[2086] == checkRecursive [("lib", "echo $1")] "source lib" -- >>> check "#!/bin/sh\necho $1"
-- [2086]
prop_recursiveParsing = --
[1037] == checkRecursive [("lib", "echo \"$10\"")] "source lib" -- >>> check "#!/bin/sh\n# shellcheck disable=2086\necho $1"
-- []
prop_nonRecursiveAnalysis = --
[] == checkWithIncludes [("lib", "echo $1")] "source lib" -- >>> check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1"
-- []
prop_nonRecursiveParsing = --
[] == checkWithIncludes [("lib", "echo \"$10\"")] "source lib" -- >>> check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1"
-- []
prop_sourceDirectiveDoesntFollowFile = --
null $ checkWithIncludes -- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
[("foo", "source bar"), ("bar", "baz=3")] -- []
"#shellcheck source=foo\n. \"$1\"; echo \"$baz\"" --
-- >>> check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotationBase = [2086] == check "#!/bin/sh\necho $1" -- []
prop_filewideAnnotation1 = null $ --
check "#!/bin/sh\n# shellcheck disable=2086\necho $1" -- >>> check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1"
prop_filewideAnnotation2 = null $ -- []
check "#!/bin/sh\n# shellcheck disable=2086\ntrue\necho $1" --
prop_filewideAnnotation3 = null $ -- >>> check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1"
check "#!/bin/sh\n#unrelated\n# shellcheck disable=2086\ntrue\necho $1" -- []
prop_filewideAnnotation4 = null $ --
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" -- check "true\n[ $? == 0 ] && echo $1"
prop_filewideAnnotation5 = null $ -- [2086, 2181]
check "#!/bin/sh\n\n\n\n#shellcheck disable=2086\ntrue\necho $1" --
prop_filewideAnnotation6 = null $ -- check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1"
check "#shellcheck shell=sh\n#unrelated\n#shellcheck disable=2086\ntrue\necho $1" -- []
prop_filewideAnnotation7 = null $ --
check "#!/bin/sh\n# shellcheck disable=2086\n#unrelated\ntrue\necho $1" -- >>> 2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
-- True
prop_filewideAnnotationBase2 = [2086, 2181] == check "true\n[ $? == 0 ] && echo $1" --
prop_filewideAnnotation8 = null $ -- >>> check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
check "# Disable $? warning\n#shellcheck disable=SC2181\n# Disable quoting warning\n#shellcheck disable=2086\ntrue\n[ $? == 0 ] && echo $1" -- []
doctests :: ()
prop_sourcePartOfOriginalScript = -- #1181: -x disabled posix warning for 'source' doctests = ()
2039 `elem` checkWithIncludes [("./saywhat.sh", "echo foo")] "#!/bin/sh\nsource ./saywhat.sh"
prop_spinBug1413 = null $ check "fun() {\n# shellcheck disable=SC2188\n> /dev/null\n}\n"
prop_deducesTypeFromExtension = null result
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.ksh",
csScript = "(( 3.14 ))"
}
prop_deducesTypeFromExtension2 = result == [2079]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.bash",
csScript = "(( 3.14 ))"
}
prop_shExtensionDoesntMatter = result == [2148]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "echo 'hello world'"
}
prop_sourcedFileUsesOriginalShellExtension = result == [2079]
where
result = checkWithSpec [("file.ksh", "(( 3.14 ))")] emptyCheckSpec {
csFilename = "file.bash",
csScript = "source file.ksh",
csCheckSourced = True
}
prop_canEnableOptionalsWithSpec = result == [2244]
where
result = checkWithSpec [] emptyCheckSpec {
csFilename = "file.sh",
csScript = "#!/bin/sh\n[ \"$1\" ]",
csOptionalChecks = ["avoid-nullary-conditions"]
}
prop_optionIncludes1 =
-- expect 2086, but not included, so nothing reported
null $ checkOptionIncludes (Just [2080]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes2 =
-- expect 2086, included, so it is reported
[2086] == checkOptionIncludes (Just [2086]) "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes3 =
-- expect 2086, no inclusions provided, so it is reported
[2086] == checkOptionIncludes Nothing "#!/bin/sh\n var='a b'\n echo $var"
prop_optionIncludes4 =
-- expect 2086 & 2154, only 2154 included, so only that's reported
[2154] == checkOptionIncludes (Just [2154]) "#!/bin/sh\n var='a b'\n echo $var\n echo $bar"
prop_readsRcFile = result == []
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canUseNoRC = result == [2086]
where
result = checkWithRc "disable=2086" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_NoRCWontLookAtFile = result == [2086]
where
result = checkWithRc (error "Fail") emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = True
}
prop_brokenRcGetsWarning = result == [1134, 2086]
where
result = checkWithRc "rofl" emptyCheckSpec {
csScript = "#!/bin/sh\necho $1",
csIgnoreRC = False
}
prop_canEnableOptionalsWithRc = result == [2244]
where
result = checkWithRc "enable=avoid-nullary-conditions" emptyCheckSpec {
csScript = "#!/bin/sh\n[ \"$1\" ]"
}
prop_sourcePathRedirectsName = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathAddsAnnotation = result == [2086]
where
f "dir/myscript" ["mypath"] "lib" = return "foo/lib"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source-path=mypath\nsource lib",
csFilename = "dir/myscript",
csCheckSourced = True
}
prop_sourcePathRedirectsDirective = result == [2086]
where
f "dir/myscript" _ "lib" = return "foo/lib"
f _ _ _ = return "/dev/null"
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
csScript = "#!/bin/bash\n# shellcheck source=lib\nsource kittens",
csFilename = "dir/myscript",
csCheckSourced = True
}
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -17,11 +17,9 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
-- This module contains checks that examine specific commands by name. -- This module contains checks that examine specific commands by name.
module ShellCheck.Checks.Commands (checker , ShellCheck.Checks.Commands.runTests) where module ShellCheck.Checks.Commands (checker) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -37,8 +35,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map.Strict as Map import qualified Data.Map.Strict as Map
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data CommandName = Exactly String | Basename String data CommandName = Exactly String | Basename String
deriving (Eq, Ord) deriving (Eq, Ord)
@@ -46,7 +42,6 @@ data CommandName = Exactly String | Basename String
data CommandCheck = data CommandCheck =
CommandCheck CommandName (Token -> Analysis) CommandCheck CommandName (Token -> Analysis)
verify :: CommandCheck -> String -> Bool verify :: CommandCheck -> String -> Bool
verify f s = producesComments (getChecker [f]) s == Just True verify f s = producesComments (getChecker [f]) s == Just True
verifyNot f s = producesComments (getChecker [f]) s == Just False verifyNot f s = producesComments (getChecker [f]) s == Just False
@@ -94,7 +89,6 @@ commandChecks = [
,checkSudoRedirect ,checkSudoRedirect
,checkSudoArgs ,checkSudoArgs
,checkSourceArgs ,checkSourceArgs
,checkChmodDashr
] ]
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis) buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
@@ -131,20 +125,21 @@ getChecker list = Checker {
checker :: Parameters -> Checker checker :: Parameters -> Checker
checker params = getChecker commandChecks checker params = getChecker commandChecks
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]" -- |
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'" -- >>> prop $ verify checkTr "tr [a-f] [A-F]"
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'" -- >>> prop $ verify checkTr "tr 'a-z' 'A-Z'"
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'" -- >>> prop $ verify checkTr "tr '[a-z]' '[A-Z]'"
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'" -- >>> prop $ verifyNot checkTr "tr -d '[:lower:]'"
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'" -- >>> prop $ verifyNot checkTr "tr -d '[:upper:]'"
prop_checkTr4 = verifyNot checkTr "ls [a-z]" -- >>> prop $ verifyNot checkTr "tr -d '|/_[:upper:]'"
prop_checkTr5 = verify checkTr "tr foo bar" -- >>> prop $ verifyNot checkTr "ls [a-z]"
prop_checkTr6 = verify checkTr "tr 'hello' 'world'" -- >>> prop $ verify checkTr "tr foo bar"
prop_checkTr8 = verifyNot checkTr "tr aeiou _____" -- >>> prop $ verify checkTr "tr 'hello' 'world'"
prop_checkTr9 = verifyNot checkTr "a-z n-za-m" -- >>> prop $ verifyNot checkTr "tr aeiou _____"
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr" -- >>> prop $ verifyNot checkTr "a-z n-za-m"
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'" -- >>> prop $ verifyNot checkTr "tr --squeeze-repeats rl lr"
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'" -- >>> prop $ verifyNot checkTr "tr abc '[d*]'"
-- >>> prop $ verifyNot checkTr "tr '[=e=]' 'e'"
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments) checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
where where
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme? f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
@@ -165,9 +160,10 @@ checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
let relevant = filter isAlpha s let relevant = filter isAlpha s
in relevant /= nub relevant in relevant /= nub relevant
prop_checkFindNameGlob1 = verify checkFindNameGlob "find / -name *.php" -- |
prop_checkFindNameGlob2 = verify checkFindNameGlob "find / -type f -ipath *(foo)" -- >>> prop $ verify checkFindNameGlob "find / -name *.php"
prop_checkFindNameGlob3 = verifyNot checkFindNameGlob "find * -name '*.php'" -- >>> prop $ verify checkFindNameGlob "find / -type f -ipath *(foo)"
-- >>> prop $ verifyNot checkFindNameGlob "find * -name '*.php'"
checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ] acceptsGlob (Just s) = s `elem` [ "-ilname", "-iname", "-ipath", "-iregex", "-iwholename", "-lname", "-name", "-path", "-regex", "-wholename" ]
acceptsGlob _ = False acceptsGlob _ = False
@@ -180,10 +176,11 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
f (b:r) f (b:r)
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)" -- |
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``" -- >>> prop $ verify checkNeedlessExpr "foo=$(expr 3 + 2)"
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)" -- >>> prop $ verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)" -- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
-- >>> prop $ verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
checkNeedlessExpr = CommandCheck (Basename "expr") f where checkNeedlessExpr = CommandCheck (Basename "expr") f where
f t = f t =
when (all (`notElem` exceptions) (words $ arguments t)) $ when (all (`notElem` exceptions) (words $ arguments t)) $
@@ -194,29 +191,22 @@ checkNeedlessExpr = CommandCheck (Basename "expr") f where
words = mapMaybe getLiteralString words = mapMaybe getLiteralString
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3" -- |
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3" -- >>> prop $ verify checkGrepRe "cat foo | grep *.mp3"
prop_checkGrepRe3 = verify checkGrepRe "grep --regex=*.mp3 file" -- >>> prop $ verify checkGrepRe "grep -Ev cow*test *.mp3"
prop_checkGrepRe4 = verifyNot checkGrepRe "grep foo *.mp3" -- >>> prop $ verify checkGrepRe "grep --regex=*.mp3 file"
prop_checkGrepRe5 = verifyNot checkGrepRe "grep-v --regex=moo *" -- >>> prop $ verifyNot checkGrepRe "grep foo *.mp3"
prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3" -- >>> prop $ verifyNot checkGrepRe "grep-v --regex=moo *"
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file" -- >>> prop $ verifyNot checkGrepRe "grep foo \\*.mp3"
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg" -- >>> prop $ verify checkGrepRe "grep *foo* file"
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file" -- >>> prop $ verify checkGrepRe "ls | grep foo*.jpg"
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file" -- >>> prop $ verifyNot checkGrepRe "grep '[0-9]*' file"
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo" -- >>> prop $ verifyNot checkGrepRe "grep '^aa*' file"
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file" -- >>> prop $ verifyNot checkGrepRe "grep --include=*.png foo"
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -F 'Foo*' file"
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -- -foo bar*"
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*" -- >>> prop $ verifyNot checkGrepRe "grep -e -foo bar*"
prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file" -- >>> prop $ verifyNot checkGrepRe "grep --regex -foo bar*"
prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"
prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"
prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"
prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"
prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
checkGrepRe = CommandCheck (Basename "grep") check where checkGrepRe = CommandCheck (Basename "grep") check where
check cmd = f cmd (arguments cmd) check cmd = f cmd (arguments cmd)
@@ -239,7 +229,7 @@ checkGrepRe = CommandCheck (Basename "grep") check where
when (isGlob re) $ when (isGlob re) $
warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it." warn (getId re) 2062 "Quote the grep pattern so the shell won't interpret it."
unless (any (`elem` flags) grepGlobFlags) $ do unless (cmd `hasFlag` "F") $ do
let string = concat $ oversimplify re let string = concat $ oversimplify re
if isConfusedGlobRegex string then if isConfusedGlobRegex string then
warn (getId re) 2063 "Grep uses regex, but this looks like a glob." warn (getId re) 2063 "Grep uses regex, but this looks like a glob."
@@ -247,9 +237,6 @@ checkGrepRe = CommandCheck (Basename "grep") check where
char <- getSuspiciousRegexWildcard string char <- getSuspiciousRegexWildcard string
return $ info (getId re) 2022 $ return $ info (getId re) 2022 $
"Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'." "Note that unlike globs, " ++ [char] ++ "* here matches '" ++ [char, char, char] ++ "' but not '" ++ wordStartingWith char ++ "'."
where
flags = map snd $ getAllFlags cmd
grepGlobFlags = ["fixed-strings", "F", "include", "exclude", "exclude-dir", "o", "only-matching"]
wordStartingWith c = wordStartingWith c =
head . filter ([c] `isPrefixOf`) $ candidates head . filter ([c] `isPrefixOf`) $ candidates
@@ -270,10 +257,11 @@ checkGrepRe = CommandCheck (Basename "grep") check where
contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]" contra = mkRegex "[^a-zA-Z1-9]\\*|[][^$+\\\\]"
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT" -- |
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT" -- >>> prop $ verify checkTrapQuotes "trap \"echo $num\" INT"
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT" -- >>> prop $ verify checkTrapQuotes "trap \"echo `ls`\" INT"
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG" -- >>> prop $ verifyNot checkTrapQuotes "trap 'echo $num' INT"
-- >>> prop $ verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
f (x:_) = checkTrap x f (x:_) = checkTrap x
f _ = return () f _ = return ()
@@ -282,29 +270,31 @@ checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled." warning id = warn id 2064 "Use single quotes, otherwise this expands now rather than when signalled."
checkExpansions (T_DollarExpansion id _) = warning id checkExpansions (T_DollarExpansion id _) = warning id
checkExpansions (T_Backticked id _) = warning id checkExpansions (T_Backticked id _) = warning id
checkExpansions (T_DollarBraced id _ _) = warning id checkExpansions (T_DollarBraced id _) = warning id
checkExpansions (T_DollarArithmetic id _) = warning id checkExpansions (T_DollarArithmetic id _) = warning id
checkExpansions _ = return () checkExpansions _ = return ()
prop_checkReturn1 = verifyNot checkReturn "return" -- |
prop_checkReturn2 = verifyNot checkReturn "return 1" -- >>> prop $ verifyNot checkReturn "return"
prop_checkReturn3 = verifyNot checkReturn "return $var" -- >>> prop $ verifyNot checkReturn "return 1"
prop_checkReturn4 = verifyNot checkReturn "return $((a|b))" -- >>> prop $ verifyNot checkReturn "return $var"
prop_checkReturn5 = verify checkReturn "return -1" -- >>> prop $ verifyNot checkReturn "return $((a|b))"
prop_checkReturn6 = verify checkReturn "return 1000" -- >>> prop $ verify checkReturn "return -1"
prop_checkReturn7 = verify checkReturn "return 'hello world'" -- >>> prop $ verify checkReturn "return 1000"
-- >>> prop $ verify checkReturn "return 'hello world'"
checkReturn = CommandCheck (Exactly "return") (returnOrExit checkReturn = CommandCheck (Exactly "return") (returnOrExit
(\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.") (\c -> err c 2151 "Only one integer 0-255 can be returned. Use stdout for other data.")
(\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout.")) (\c -> err c 2152 "Can only return 0-255. Other data should be written to stdout."))
prop_checkExit1 = verifyNot checkExit "exit" -- |
prop_checkExit2 = verifyNot checkExit "exit 1" -- >>> prop $ verifyNot checkExit "exit"
prop_checkExit3 = verifyNot checkExit "exit $var" -- >>> prop $ verifyNot checkExit "exit 1"
prop_checkExit4 = verifyNot checkExit "exit $((a|b))" -- >>> prop $ verifyNot checkExit "exit $var"
prop_checkExit5 = verify checkExit "exit -1" -- >>> prop $ verifyNot checkExit "exit $((a|b))"
prop_checkExit6 = verify checkExit "exit 1000" -- >>> prop $ verify checkExit "exit -1"
prop_checkExit7 = verify checkExit "exit 'hello world'" -- >>> prop $ verify checkExit "exit 1000"
-- >>> prop $ verify checkExit "exit 'hello world'"
checkExit = CommandCheck (Exactly "exit") (returnOrExit checkExit = CommandCheck (Exactly "exit") (returnOrExit
(\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.") (\c -> err c 2241 "The exit status can only be one integer 0-255. Use stdout for other data.")
(\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr.")) (\c -> err c 2242 "Can only exit with status 0-255. Other data should be written to stdout/stderr."))
@@ -329,9 +319,10 @@ returnOrExit multi invalid = (f . arguments)
lit _ = return "WTF" lit _ = return "WTF"
prop_checkFindExecWithSingleArgument1 = verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;" -- |
prop_checkFindExecWithSingleArgument2 = verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +" -- >>> prop $ verify checkFindExecWithSingleArgument "find . -exec 'cat {} | wc -l' \\;"
prop_checkFindExecWithSingleArgument3 = verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;" -- >>> prop $ verify checkFindExecWithSingleArgument "find . -execdir 'cat {} | wc -l' +"
-- >>> prop $ verifyNot checkFindExecWithSingleArgument "find . -exec wc -l {} \\;"
checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments) checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
where where
f = void . sequence . mapMaybe check . tails f = void . sequence . mapMaybe check . tails
@@ -347,11 +338,12 @@ checkFindExecWithSingleArgument = CommandCheck (Basename "find") (f . arguments)
commandRegex = mkRegex "[ |;]" commandRegex = mkRegex "[ |;]"
prop_checkUnusedEchoEscapes1 = verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'" -- |
prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'" -- >>> prop $ verify checkUnusedEchoEscapes "echo 'foo\\nbar\\n'"
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\"" -- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -e 'foi\\nbar'"
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol" -- >>> prop $ verify checkUnusedEchoEscapes "echo \"n:\\t42\""
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'" -- >>> prop $ verifyNot checkUnusedEchoEscapes "echo lol"
-- >>> prop $ verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
where where
hasEscapes = mkRegex "\\\\[rnt]" hasEscapes = mkRegex "\\\\[rnt]"
@@ -366,9 +358,10 @@ checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
info (getId token) 2028 "echo may not expand escape sequences. Use printf." info (getId token) 2028 "echo may not expand escape sequences. Use printf."
prop_checkInjectableFindSh1 = verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;" -- |
prop_checkInjectableFindSh2 = verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'" -- >>> prop $ verify checkInjectableFindSh "find . -exec sh -c 'echo {}' \\;"
prop_checkInjectableFindSh3 = verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;" -- >>> prop $ verify checkInjectableFindSh "find . -execdir bash -c 'rm \"{}\"' ';'"
-- >>> prop $ verifyNot checkInjectableFindSh "find . -exec sh -c 'rm \"$@\"' _ {} \\;"
checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments) checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
where where
check args = do check args = do
@@ -391,9 +384,10 @@ checkInjectableFindSh = CommandCheck (Basename "find") (check . arguments)
warn id 2156 "Injecting filenames is fragile and insecure. Use parameters." warn id 2156 "Injecting filenames is fragile and insecure. Use parameters."
prop_checkFindActionPrecedence1 = verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +" -- |
prop_checkFindActionPrecedence2 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)" -- >>> prop $ verify checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au' -exec rm {} +"
prop_checkFindActionPrecedence3 = verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'" -- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o \\( -name '*.au' -exec rm {} + \\)"
-- >>> prop $ verifyNot checkFindActionPrecedence "find . -name '*.wav' -o -name '*.au'"
checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments) checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
where where
pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction] pattern = [isMatch, const True, isParam ["-o", "-or"], isMatch, const True, isAction]
@@ -410,28 +404,29 @@ checkFindActionPrecedence = CommandCheck (Basename "find") (f . arguments)
warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group." warnFor t = warn (getId t) 2146 "This action ignores everything before the -o. Use \\( \\) to group."
prop_checkMkdirDashPM0 = verify checkMkdirDashPM "mkdir -p -m 0755 a/b" -- |
prop_checkMkdirDashPM1 = verify checkMkdirDashPM "mkdir -pm 0755 $dir" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 a/b"
prop_checkMkdirDashPM2 = verify checkMkdirDashPM "mkdir -vpm 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 $dir"
prop_checkMkdirDashPM3 = verify checkMkdirDashPM "mkdir -pm 0755 -v a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -vpm 0755 a/b"
prop_checkMkdirDashPM4 = verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -pm 0755 -v a/b"
prop_checkMkdirDashPM5 = verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode=0755 a/b"
prop_checkMkdirDashPM6 = verify checkMkdirDashPM "mkdir -p --mode=0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents --mode 0755 a/b"
prop_checkMkdirDashPM7 = verify checkMkdirDashPM "mkdir --parents -m 0755 a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir -p --mode=0755 a/b"
prop_checkMkdirDashPM8 = verifyNot checkMkdirDashPM "mkdir -p a/b" -- >>> prop $ verify checkMkdirDashPM "mkdir --parents -m 0755 a/b"
prop_checkMkdirDashPM9 = verifyNot checkMkdirDashPM "mkdir -m 0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p a/b"
prop_checkMkdirDashPM10 = verifyNot checkMkdirDashPM "mkdir a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -m 0755 a/b"
prop_checkMkdirDashPM11 = verifyNot checkMkdirDashPM "mkdir --parents a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir a/b"
prop_checkMkdirDashPM12 = verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir --parents a/b"
prop_checkMkdirDashPM13 = verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir --mode=0755 a/b"
prop_checkMkdirDashPM14 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir_func -pm 0755 a/b"
prop_checkMkdirDashPM15 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 singlelevel"
prop_checkMkdirDashPM16 = verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../bin"
prop_checkMkdirDashPM17 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ../bin/laden"
prop_checkMkdirDashPM18 = verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./bin"
prop_checkMkdirDashPM19 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin" -- >>> prop $ verify checkMkdirDashPM "mkdir -p -m 0755 ./bin/laden"
prop_checkMkdirDashPM20 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ./../bin"
prop_checkMkdirDashPM21 = verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin" -- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 .././bin"
-- >>> prop $ verifyNot checkMkdirDashPM "mkdir -p -m 0755 ../../bin"
checkMkdirDashPM = CommandCheck (Basename "mkdir") check checkMkdirDashPM = CommandCheck (Basename "mkdir") check
where where
check t = potentially $ do check t = potentially $ do
@@ -447,13 +442,14 @@ checkMkdirDashPM = CommandCheck (Basename "mkdir") check
re = mkRegex "^(\\.\\.?\\/)+[^/]+$" re = mkRegex "^(\\.\\.?\\/)+[^/]+$"
prop_checkNonportableSignals1 = verify checkNonportableSignals "trap f 8" -- |
prop_checkNonportableSignals2 = verifyNot checkNonportableSignals "trap f 0" -- >>> prop $ verify checkNonportableSignals "trap f 8"
prop_checkNonportableSignals3 = verifyNot checkNonportableSignals "trap f 14" -- >>> prop $ verifyNot checkNonportableSignals "trap f 0"
prop_checkNonportableSignals4 = verify checkNonportableSignals "trap f SIGKILL" -- >>> prop $ verifyNot checkNonportableSignals "trap f 14"
prop_checkNonportableSignals5 = verify checkNonportableSignals "trap f 9" -- >>> prop $ verify checkNonportableSignals "trap f SIGKILL"
prop_checkNonportableSignals6 = verify checkNonportableSignals "trap f stop" -- >>> prop $ verify checkNonportableSignals "trap f 9"
prop_checkNonportableSignals7 = verifyNot checkNonportableSignals "trap 'stop' int" -- >>> prop $ verify checkNonportableSignals "trap f stop"
-- >>> prop $ verifyNot checkNonportableSignals "trap 'stop' int"
checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments) checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
where where
f args = case args of f args = case args of
@@ -482,14 +478,15 @@ checkNonportableSignals = CommandCheck (Exactly "trap") (f . arguments)
"SIGKILL/SIGSTOP can not be trapped." "SIGKILL/SIGSTOP can not be trapped."
prop_checkInteractiveSu1 = verify checkInteractiveSu "su; rm file; su $USER" -- |
prop_checkInteractiveSu2 = verify checkInteractiveSu "su foo; something; exit" -- >>> prop $ verify checkInteractiveSu "su; rm file; su $USER"
prop_checkInteractiveSu3 = verifyNot checkInteractiveSu "echo rm | su foo" -- >>> prop $ verify checkInteractiveSu "su foo; something; exit"
prop_checkInteractiveSu4 = verifyNot checkInteractiveSu "su root < script" -- >>> prop $ verifyNot checkInteractiveSu "echo rm | su foo"
-- >>> prop $ verifyNot checkInteractiveSu "su root < script"
checkInteractiveSu = CommandCheck (Basename "su") f checkInteractiveSu = CommandCheck (Basename "su") f
where where
f cmd = when (length (arguments cmd) <= 1) $ do f cmd = when (length (arguments cmd) <= 1) $ do
path <- getPathM cmd path <- pathTo cmd
when (all undirected path) $ when (all undirected path) $
info (getId cmd) 2117 info (getId cmd) 2117
"To run commands as another user, use su -c or sudo." "To run commands as another user, use su -c or sudo."
@@ -500,11 +497,13 @@ checkInteractiveSu = CommandCheck (Basename "su") f
undirected _ = True undirected _ = True
-- |
-- This is hard to get right without properly parsing ssh args -- This is hard to get right without properly parsing ssh args
prop_checkSshCmdStr1 = verify checkSshCommandString "ssh host \"echo $PS1\"" --
prop_checkSshCmdStr2 = verifyNot checkSshCommandString "ssh host \"ls foo\"" -- >>> prop $ verify checkSshCommandString "ssh host \"echo $PS1\""
prop_checkSshCmdStr3 = verifyNot checkSshCommandString "ssh \"$host\"" -- >>> prop $ verifyNot checkSshCommandString "ssh host \"ls foo\""
prop_checkSshCmdStr4 = verifyNot checkSshCommandString "ssh -i key \"$host\"" -- >>> prop $ verifyNot checkSshCommandString "ssh \"$host\""
-- >>> prop $ verifyNot checkSshCommandString "ssh -i key \"$host\""
checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments) checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
where where
isOption x = "-" `isPrefixOf` (concat $ oversimplify x) isOption x = "-" `isPrefixOf` (concat $ oversimplify x)
@@ -520,121 +519,93 @@ checkSshCommandString = CommandCheck (Basename "ssh") (f . arguments)
checkArg _ = return () checkArg _ = return ()
prop_checkPrintfVar1 = verify checkPrintfVar "printf \"Lol: $s\"" -- |
prop_checkPrintfVar2 = verifyNot checkPrintfVar "printf 'Lol: $s'" -- >>> prop $ verify checkPrintfVar "printf \"Lol: $s\""
prop_checkPrintfVar3 = verify checkPrintfVar "printf -v cow $(cmd)" -- >>> prop $ verifyNot checkPrintfVar "printf 'Lol: $s'"
prop_checkPrintfVar4 = verifyNot checkPrintfVar "printf \"%${count}s\" var" -- >>> prop $ verify checkPrintfVar "printf -v cow $(cmd)"
prop_checkPrintfVar5 = verify checkPrintfVar "printf '%s %s %s' foo bar" -- >>> prop $ verifyNot checkPrintfVar "printf \"%${count}s\" var"
prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz" -- >>> prop $ verify checkPrintfVar "printf '%s %s %s' foo bar"
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz" -- >>> prop $ verify checkPrintfVar "printf foo bar baz"
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\"" -- >>> prop $ verify checkPrintfVar "printf -- foo bar baz"
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3" -- >>> prop $ verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4" -- >>> prop $ verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1" -- >>> prop $ verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2" -- >>> prop $ verify checkPrintfVar "printf '%*s\\n' 1"
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'" -- >>> prop $ verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1" -- >>> prop $ verifyNot checkPrintfVar "printf $'string'"
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2" -- >>> prop $ verify checkPrintfVar "printf '%-*s\\n' 1"
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'" -- >>> prop $ verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest f (dashv:var:rest) | getLiteralString dashv == Just "-v" = f rest
f (format:params) = check format params f (format:params) = check format params
f _ = return () f _ = return ()
countFormats string =
case string of
'%':'%':rest -> countFormats rest
'%':'(':rest -> 1 + countFormats (dropWhile (/= ')') rest)
'%':rest -> regexBasedCountFormats rest + countFormats (dropWhile (/= '%') rest)
_:rest -> countFormats rest
[] -> 0
regexBasedCountFormats rest =
maybe 1 (foldl (\acc group -> acc + (if group == "*" then 1 else 0)) 1) (matchRegex re rest)
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*).?(\\d*|\\*)[diouxXfFeEgGaAcsb]"
-- \____ _____/\___ ____/ \____ ____/\________ ________/
-- V V V V
-- flags field width precision format character
-- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
check format more = do check format more = do
fromMaybe (return ()) $ do fromMaybe (return ()) $ do
string <- getLiteralString format string <- getLiteralString format
let formats = getPrintfFormats string let vars = countFormats string
let formatCount = length formats
let argCount = length more return $ do
when (vars == 0 && more /= []) $
err (getId format) 2182
"This printf format string has no variables. Other arguments are ignored."
when (vars > 0
&& ((length more) `mod` vars /= 0 || null more)
&& all (not . mayBecomeMultipleArgs) more) $
warn (getId format) 2183 $
"This format string has " ++ show vars ++ " variables, but is passed " ++ show (length more) ++ " arguments."
return $
case () of
() | argCount == 0 && formatCount == 0 ->
return () -- This is fine
() | formatCount == 0 && argCount > 0 ->
err (getId format) 2182
"This printf format string has no variables. Other arguments are ignored."
() | any mayBecomeMultipleArgs more ->
return () -- We don't know so trust the user
() | argCount < formatCount && onlyTrailingTs formats argCount ->
return () -- Allow trailing %()Ts since they use the current time
() | argCount > 0 && argCount `mod` formatCount == 0 ->
return () -- Great: a suitable number of arguments
() ->
warn (getId format) 2183 $
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $ unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
info (getId format) 2059 info (getId format) 2059
"Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"." "Don't use variables in the printf format string. Use printf \"..%s..\" \"$foo\"."
where
onlyTrailingTs format argCount =
all (== 'T') $ drop argCount format
prop_checkGetPrintfFormats1 = getPrintfFormats "%s" == "s"
prop_checkGetPrintfFormats2 = getPrintfFormats "%0*s" == "*s"
prop_checkGetPrintfFormats3 = getPrintfFormats "%(%s)T" == "T"
prop_checkGetPrintfFormats4 = getPrintfFormats "%d%%%(%s)T" == "dT"
prop_checkGetPrintfFormats5 = getPrintfFormats "%bPassed: %d, %bFailed: %d%b, Skipped: %d, %bErrored: %d%b\\n" == "bdbdbdbdb"
getPrintfFormats = getFormats
where
-- Get the arguments in the string as a string of type characters,
-- e.g. "Hello %s" -> "s" and "%(%s)T %0*d\n" -> "T*d"
getFormats :: String -> String
getFormats string =
case string of
'%':'%':rest -> getFormats rest
'%':'(':rest ->
case dropWhile (/= ')') rest of
')':c:trailing -> c : getFormats trailing
_ -> ""
'%':rest -> regexBasedGetFormats rest
_:rest -> getFormats rest
[] -> ""
regexBasedGetFormats rest =
case matchRegex re rest of
Just [width, precision, typ, rest] ->
(if width == "*" then "*" else "") ++
(if precision == "*" then "*" else "") ++
typ ++ getFormats rest
Nothing -> take 1 rest ++ getFormats rest
where
-- constructed based on specifications in "man printf"
re = mkRegex "#?-?\\+? ?0?(\\*|\\d*)\\.?(\\d*|\\*)([diouxXfFeEgGaAcsbq])(.*)"
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \ /
-- V V V V V
-- flags field width precision format character rest
-- field width and precision can be specified with a '*' instead of a digit,
-- in which case printf will accept one more argument for each '*' used
prop_checkUuoeCmd1 = verify checkUuoeCmd "echo $(date)" -- |
prop_checkUuoeCmd2 = verify checkUuoeCmd "echo `date`" -- >>> prop $ verify checkUuoeCmd "echo $(date)"
prop_checkUuoeCmd3 = verify checkUuoeCmd "echo \"$(date)\"" -- >>> prop $ verify checkUuoeCmd "echo `date`"
prop_checkUuoeCmd4 = verify checkUuoeCmd "echo \"`date`\"" -- >>> prop $ verify checkUuoeCmd "echo \"$(date)\""
prop_checkUuoeCmd5 = verifyNot checkUuoeCmd "echo \"The time is $(date)\"" -- >>> prop $ verify checkUuoeCmd "echo \"`date`\""
prop_checkUuoeCmd6 = verifyNot checkUuoeCmd "echo \"$(<file)\"" -- >>> prop $ verifyNot checkUuoeCmd "echo \"The time is $(date)\""
-- >>> prop $ verifyNot checkUuoeCmd "echo \"$(<file)\""
checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where checkUuoeCmd = CommandCheck (Exactly "echo") (f . arguments) where
msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'." msg id = style id 2005 "Useless echo? Instead of 'echo $(cmd)', just use 'cmd'."
f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token) f [token] = when (tokenIsJustCommandOutput token) $ msg (getId token)
f _ = return () f _ = return ()
prop_checkSetAssignment1 = verify checkSetAssignment "set foo 42" -- |
prop_checkSetAssignment2 = verify checkSetAssignment "set foo = 42" -- >>> prop $ verify checkSetAssignment "set foo 42"
prop_checkSetAssignment3 = verify checkSetAssignment "set foo=42" -- >>> prop $ verify checkSetAssignment "set foo = 42"
prop_checkSetAssignment4 = verifyNot checkSetAssignment "set -- if=/dev/null" -- >>> prop $ verify checkSetAssignment "set foo=42"
prop_checkSetAssignment5 = verifyNot checkSetAssignment "set 'a=5'" -- >>> prop $ verifyNot checkSetAssignment "set -- if=/dev/null"
prop_checkSetAssignment6 = verifyNot checkSetAssignment "set" -- >>> prop $ verifyNot checkSetAssignment "set 'a=5'"
-- >>> prop $ verifyNot checkSetAssignment "set"
checkSetAssignment = CommandCheck (Exactly "set") (f . arguments) checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
where where
f (var:value:rest) = f (var:value:rest) =
@@ -654,10 +625,11 @@ checkSetAssignment = CommandCheck (Exactly "set") (f . arguments)
literal _ = "*" literal _ = "*"
prop_checkExportedExpansions1 = verify checkExportedExpansions "export $foo" -- |
prop_checkExportedExpansions2 = verify checkExportedExpansions "export \"$foo\"" -- >>> prop $ verify checkExportedExpansions "export $foo"
prop_checkExportedExpansions3 = verifyNot checkExportedExpansions "export foo" -- >>> prop $ verify checkExportedExpansions "export \"$foo\""
prop_checkExportedExpansions4 = verifyNot checkExportedExpansions "export ${foo?}" -- >>> prop $ verifyNot checkExportedExpansions "export foo"
-- >>> prop $ verifyNot checkExportedExpansions "export ${foo?}"
checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments) checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . arguments)
where where
check t = potentially $ do check t = potentially $ do
@@ -666,14 +638,15 @@ checkExportedExpansions = CommandCheck (Exactly "export") (mapM_ check . argumen
return . warn (getId t) 2163 $ return . warn (getId t) 2163 $
"This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet." "This does not export '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
prop_checkReadExpansions1 = verify checkReadExpansions "read $var" -- |
prop_checkReadExpansions2 = verify checkReadExpansions "read -r $var" -- >>> prop $ verify checkReadExpansions "read $var"
prop_checkReadExpansions3 = verifyNot checkReadExpansions "read -p $var" -- >>> prop $ verify checkReadExpansions "read -r $var"
prop_checkReadExpansions4 = verifyNot checkReadExpansions "read -rd $delim name" -- >>> prop $ verifyNot checkReadExpansions "read -p $var"
prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\"" -- >>> prop $ verifyNot checkReadExpansions "read -rd $delim name"
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var" -- >>> prop $ verify checkReadExpansions "read \"$var\""
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1" -- >>> prop $ verify checkReadExpansions "read -a $var"
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}" -- >>> prop $ verifyNot checkReadExpansions "read $1"
-- >>> prop $ verifyNot checkReadExpansions "read ${var?}"
checkReadExpansions = CommandCheck (Exactly "read") check checkReadExpansions = CommandCheck (Exactly "read") check
where where
options = getGnuOpts "sreu:n:N:i:p:a:" options = getGnuOpts "sreu:n:N:i:p:a:"
@@ -700,9 +673,10 @@ getSingleUnmodifiedVariable word =
in guard (contents == name) >> return t in guard (contents == name) >> return t
_ -> Nothing _ -> Nothing
prop_checkAliasesUsesArgs1 = verify checkAliasesUsesArgs "alias a='cp $1 /a'" -- |
prop_checkAliasesUsesArgs2 = verifyNot checkAliasesUsesArgs "alias $1='foo'" -- >>> prop $ verify checkAliasesUsesArgs "alias a='cp $1 /a'"
prop_checkAliasesUsesArgs3 = verify checkAliasesUsesArgs "alias a=\"echo \\${@}\"" -- >>> prop $ verifyNot checkAliasesUsesArgs "alias $1='foo'"
-- >>> prop $ verify checkAliasesUsesArgs "alias a=\"echo \\${@}\""
checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments) checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
where where
re = mkRegex "\\$\\{?[0-9*@]" re = mkRegex "\\$\\{?[0-9*@]"
@@ -714,9 +688,10 @@ checkAliasesUsesArgs = CommandCheck (Exactly "alias") (f . arguments)
"Aliases can't use positional parameters. Use a function." "Aliases can't use positional parameters. Use a function."
prop_checkAliasesExpandEarly1 = verify checkAliasesExpandEarly "alias foo=\"echo $PWD\"" -- |
prop_checkAliasesExpandEarly2 = verifyNot checkAliasesExpandEarly "alias -p" -- >>> prop $ verify checkAliasesExpandEarly "alias foo=\"echo $PWD\""
prop_checkAliasesExpandEarly3 = verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'" -- >>> prop $ verifyNot checkAliasesExpandEarly "alias -p"
-- >>> prop $ verifyNot checkAliasesExpandEarly "alias foo='echo {1..10}'"
checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments) checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
where where
f = mapM_ checkArg f = mapM_ checkArg
@@ -726,8 +701,8 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
checkArg _ = return () checkArg _ = return ()
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]" -- >>> prop $ verify checkUnsetGlobs "unset foo[1]"
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo" -- >>> prop $ verifyNot checkUnsetGlobs "unset foo"
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments) checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
where where
check arg = check arg =
@@ -735,14 +710,15 @@ checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded." warn (getId arg) 2184 "Quote arguments to unset so they're not glob expanded."
prop_checkFindWithoutPath1 = verify checkFindWithoutPath "find -type f" -- |
prop_checkFindWithoutPath2 = verify checkFindWithoutPath "find" -- >>> prop $ verify checkFindWithoutPath "find -type f"
prop_checkFindWithoutPath3 = verifyNot checkFindWithoutPath "find . -type f" -- >>> prop $ verify checkFindWithoutPath "find"
prop_checkFindWithoutPath4 = verifyNot checkFindWithoutPath "find -H -L \"$path\" -print" -- >>> prop $ verifyNot checkFindWithoutPath "find . -type f"
prop_checkFindWithoutPath5 = verifyNot checkFindWithoutPath "find -O3 ." -- >>> prop $ verifyNot checkFindWithoutPath "find -H -L \"$path\" -print"
prop_checkFindWithoutPath6 = verifyNot checkFindWithoutPath "find -D exec ." -- >>> prop $ verifyNot checkFindWithoutPath "find -O3 ."
prop_checkFindWithoutPath7 = verifyNot checkFindWithoutPath "find --help" -- >>> prop $ verifyNot checkFindWithoutPath "find -D exec ."
prop_checkFindWithoutPath8 = verifyNot checkFindWithoutPath "find -Hx . -print" -- >>> prop $ verifyNot checkFindWithoutPath "find --help"
-- >>> prop $ verifyNot checkFindWithoutPath "find -Hx . -print"
checkFindWithoutPath = CommandCheck (Basename "find") f checkFindWithoutPath = CommandCheck (Basename "find") f
where where
f t@(T_SimpleCommand _ _ (cmd:args)) = f t@(T_SimpleCommand _ _ (cmd:args)) =
@@ -761,10 +737,11 @@ checkFindWithoutPath = CommandCheck (Basename "find") f
leadingFlagChars="-EHLPXdfsxO0123456789" leadingFlagChars="-EHLPXdfsxO0123456789"
prop_checkTimeParameters1 = verify checkTimeParameters "time -f lol sleep 10" -- |
prop_checkTimeParameters2 = verifyNot checkTimeParameters "time sleep 10" -- >>> prop $ verify checkTimeParameters "time -f lol sleep 10"
prop_checkTimeParameters3 = verifyNot checkTimeParameters "time -p foo" -- >>> prop $ verifyNot checkTimeParameters "time sleep 10"
prop_checkTimeParameters4 = verifyNot checkTimeParameters "command time -f lol sleep 10" -- >>> prop $ verifyNot checkTimeParameters "time -p foo"
-- >>> prop $ verifyNot checkTimeParameters "command time -f lol sleep 10"
checkTimeParameters = CommandCheck (Exactly "time") f checkTimeParameters = CommandCheck (Exactly "time") f
where where
f (T_SimpleCommand _ _ (cmd:args:_)) = f (T_SimpleCommand _ _ (cmd:args:_)) =
@@ -775,9 +752,10 @@ checkTimeParameters = CommandCheck (Exactly "time") f
f _ = return () f _ = return ()
prop_checkTimedCommand1 = verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar" -- |
prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )" -- >>> prop $ verify checkTimedCommand "#!/bin/sh\ntime -p foo | bar"
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1" -- >>> prop $ verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar; )"
-- >>> prop $ verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
checkTimedCommand = CommandCheck (Exactly "time") f where checkTimedCommand = CommandCheck (Exactly "time") f where
f (T_SimpleCommand _ _ (c:args@(_:_))) = f (T_SimpleCommand _ _ (c:args@(_:_))) =
whenShell [Sh, Dash] $ do whenShell [Sh, Dash] $ do
@@ -801,32 +779,37 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
T_SimpleCommand {} -> return True T_SimpleCommand {} -> return True
_ -> return False _ -> return False
prop_checkLocalScope1 = verify checkLocalScope "local foo=3" -- |
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }" -- >>> prop $ verify checkLocalScope "local foo=3"
-- >>> prop $ verifyNot checkLocalScope "f() { local foo=3; }"
checkLocalScope = CommandCheck (Exactly "local") $ \t -> checkLocalScope = CommandCheck (Exactly "local") $ \t ->
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
path <- getPathM t path <- getPathM t
unless (any isFunctionLike path) $ unless (any isFunction path) $
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions." err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)" -- |
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)" -- >>> prop $ verify checkDeprecatedTempfile "var=$(tempfile)"
-- >>> prop $ verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $ checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
\t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead." \t -> warn (getId $ getCommandTokenOrThis t) 2186 "tempfile is deprecated. Use mktemp instead."
prop_checkDeprecatedEgrep = verify checkDeprecatedEgrep "egrep '.+'" -- |
-- >>> prop $ verify checkDeprecatedEgrep "egrep '.+'"
checkDeprecatedEgrep = CommandCheck (Basename "egrep") $ checkDeprecatedEgrep = CommandCheck (Basename "egrep") $
\t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead." \t -> info (getId $ getCommandTokenOrThis t) 2196 "egrep is non-standard and deprecated. Use grep -E instead."
prop_checkDeprecatedFgrep = verify checkDeprecatedFgrep "fgrep '*' files" -- |
-- >>> prop $ verify checkDeprecatedFgrep "fgrep '*' files"
checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $ checkDeprecatedFgrep = CommandCheck (Basename "fgrep") $
\t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead." \t -> info (getId $ getCommandTokenOrThis t) 2197 "fgrep is non-standard and deprecated. Use grep -F instead."
prop_checkWhileGetoptsCase1 = verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done" -- |
prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done" -- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; esac; done"
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done" -- >>> prop $ verify checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; b) bar;; esac; done"
prop_checkWhileGetoptsCase4 = verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done" -- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $x in a) foo;; b) bar;; *) :;esac; done"
prop_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done" -- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:123' x; do case $x in a) foo;; [0-9]) bar;; esac; done"
-- >>> prop $ verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
where where
f :: Token -> Analysis f :: Token -> Analysis
@@ -891,19 +874,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
T_Redirecting _ _ x@(T_CaseExpression {}) -> return x T_Redirecting _ _ x@(T_CaseExpression {}) -> return x
_ -> Nothing _ -> Nothing
prop_checkCatastrophicRm1 = verify checkCatastrophicRm "rm -r $1/$2" -- |
prop_checkCatastrophicRm2 = verify checkCatastrophicRm "rm -r /home/$foo" -- >>> prop $ verify checkCatastrophicRm "rm -r $1/$2"
prop_checkCatastrophicRm3 = verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*" -- >>> prop $ verify checkCatastrophicRm "rm -r /home/$foo"
prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:?}/*"
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*" -- >>> prop $ verify checkCatastrophicRm "rm -fr /home/$(whoami)/*"
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home" -- >>> prop $ verify checkCatastrophicRm "rm --recursive /etc/*$config*"
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}" -- >>> prop $ verify checkCatastrophicRm "rm -rf /home"
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec" -- >>> prop $ verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec" -- >>> prop $ verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg" -- >>> prop $ verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*" -- >>> prop $ verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
-- >>> prop $ verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t -> checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
when (isRecursive t) $ when (isRecursive t) $
mapM_ (mapM_ checkWord . braceExpand) $ arguments t mapM_ (mapM_ checkWord . braceExpand) $ arguments t
@@ -931,7 +915,7 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
getPotentialPath = getLiteralStringExt f getPotentialPath = getLiteralStringExt f
where where
f (T_Glob _ str) = return str f (T_Glob _ str) = return str
f (T_DollarBraced _ _ word) = f (T_DollarBraced _ word) =
let var = onlyLiteralString word in let var = onlyLiteralString word in
-- This shouldn't handle non-colon cases. -- This shouldn't handle non-colon cases.
if any (`isInfixOf` var) [":?", ":-", ":="] if any (`isInfixOf` var) [":?", ":-", ":="]
@@ -952,8 +936,9 @@ checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths) ["", "/", "/*", "/*/*"] >>= (\x -> map (++x) paths)
prop_checkLetUsage1 = verify checkLetUsage "let a=1" -- |
prop_checkLetUsage2 = verifyNot checkLetUsage "(( a=1 ))" -- >>> prop $ verify checkLetUsage "let a=1"
-- >>> prop $ verifyNot checkLetUsage "(( a=1 ))"
checkLetUsage = CommandCheck (Exactly "let") f checkLetUsage = CommandCheck (Exactly "let") f
where where
f t = whenShell [Bash,Ksh] $ do f t = whenShell [Bash,Ksh] $ do
@@ -973,15 +958,16 @@ missingDestination handler token = do
any (\x -> x /= "" && x `isPrefixOf` "target-directory") $ any (\x -> x /= "" && x `isPrefixOf` "target-directory") $
map snd args map snd args
prop_checkMvArguments1 = verify checkMvArguments "mv 'foo bar'" -- |
prop_checkMvArguments2 = verifyNot checkMvArguments "mv foo bar" -- >>> prop $ verify checkMvArguments "mv 'foo bar'"
prop_checkMvArguments3 = verifyNot checkMvArguments "mv 'foo bar'{,bak}" -- >>> prop $ verifyNot checkMvArguments "mv foo bar"
prop_checkMvArguments4 = verifyNot checkMvArguments "mv \"$@\"" -- >>> prop $ verifyNot checkMvArguments "mv 'foo bar'{,bak}"
prop_checkMvArguments5 = verifyNot checkMvArguments "mv -t foo bar" -- >>> prop $ verifyNot checkMvArguments "mv \"$@\""
prop_checkMvArguments6 = verifyNot checkMvArguments "mv --target-directory=foo bar" -- >>> prop $ verifyNot checkMvArguments "mv -t foo bar"
prop_checkMvArguments7 = verifyNot checkMvArguments "mv --target-direc=foo bar" -- >>> prop $ verifyNot checkMvArguments "mv --target-directory=foo bar"
prop_checkMvArguments8 = verifyNot checkMvArguments "mv --version" -- >>> prop $ verifyNot checkMvArguments "mv --target-direc=foo bar"
prop_checkMvArguments9 = verifyNot checkMvArguments "mv \"${!var}\"" -- >>> prop $ verifyNot checkMvArguments "mv --version"
-- >>> prop $ verifyNot checkMvArguments "mv \"${!var}\""
checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f checkMvArguments = CommandCheck (Basename "mv") $ missingDestination f
where where
f t = err (getId t) 2224 "This mv has no destination. Check the arguments." f t = err (getId t) 2224 "This mv has no destination. Check the arguments."
@@ -995,9 +981,10 @@ checkLnArguments = CommandCheck (Basename "ln") $ missingDestination f
f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly." f t = warn (getId t) 2226 "This ln has no destination. Check the arguments, or specify '.' explicitly."
prop_checkFindRedirections1 = verify checkFindRedirections "find . -exec echo {} > file \\;" -- |
prop_checkFindRedirections2 = verifyNot checkFindRedirections "find . -exec echo {} \\; > file" -- >>> prop $ verify checkFindRedirections "find . -exec echo {} > file \\;"
prop_checkFindRedirections3 = verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;" -- >>> prop $ verifyNot checkFindRedirections "find . -exec echo {} \\; > file"
-- >>> prop $ verifyNot checkFindRedirections "find . -execdir sh -c 'foo > file' \\;"
checkFindRedirections = CommandCheck (Basename "find") f checkFindRedirections = CommandCheck (Basename "find") f
where where
f t = do f t = do
@@ -1012,17 +999,18 @@ checkFindRedirections = CommandCheck (Basename "find") f
"Redirection applies to the find command itself. Rewrite to work per action (or move to end)." "Redirection applies to the find command itself. Rewrite to work per action (or move to end)."
_ -> return () _ -> return ()
prop_checkWhich = verify checkWhich "which '.+'" -- >>> prop $ verify checkWhich "which '.+'"
checkWhich = CommandCheck (Basename "which") $ checkWhich = CommandCheck (Basename "which") $
\t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead." \t -> info (getId $ getCommandTokenOrThis t) 2230 "which is non-standard. Use builtin 'command -v' instead."
prop_checkSudoRedirect1 = verify checkSudoRedirect "sudo echo 3 > /proc/file" -- |
prop_checkSudoRedirect2 = verify checkSudoRedirect "sudo cmd < input" -- >>> prop $ verify checkSudoRedirect "sudo echo 3 > /proc/file"
prop_checkSudoRedirect3 = verify checkSudoRedirect "sudo cmd >> file" -- >>> prop $ verify checkSudoRedirect "sudo cmd < input"
prop_checkSudoRedirect4 = verify checkSudoRedirect "sudo cmd &> file" -- >>> prop $ verify checkSudoRedirect "sudo cmd >> file"
prop_checkSudoRedirect5 = verifyNot checkSudoRedirect "sudo cmd 2>&1" -- >>> prop $ verify checkSudoRedirect "sudo cmd &> file"
prop_checkSudoRedirect6 = verifyNot checkSudoRedirect "sudo cmd 2> log" -- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2>&1"
prop_checkSudoRedirect7 = verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1" -- >>> prop $ verifyNot checkSudoRedirect "sudo cmd 2> log"
-- >>> prop $ verifyNot checkSudoRedirect "sudo cmd > /dev/null 2>&1"
checkSudoRedirect = CommandCheck (Basename "sudo") f checkSudoRedirect = CommandCheck (Basename "sudo") f
where where
f t = do f t = do
@@ -1046,13 +1034,14 @@ checkSudoRedirect = CommandCheck (Basename "sudo") f
warnAbout _ = return () warnAbout _ = return ()
special file = concat (oversimplify file) == "/dev/null" special file = concat (oversimplify file) == "/dev/null"
prop_checkSudoArgs1 = verify checkSudoArgs "sudo cd /root" -- |
prop_checkSudoArgs2 = verify checkSudoArgs "sudo export x=3" -- >>> prop $ verify checkSudoArgs "sudo cd /root"
prop_checkSudoArgs3 = verifyNot checkSudoArgs "sudo ls /usr/local/protected" -- >>> prop $ verify checkSudoArgs "sudo export x=3"
prop_checkSudoArgs4 = verifyNot checkSudoArgs "sudo ls && export x=3" -- >>> prop $ verifyNot checkSudoArgs "sudo ls /usr/local/protected"
prop_checkSudoArgs5 = verifyNot checkSudoArgs "sudo echo ls" -- >>> prop $ verifyNot checkSudoArgs "sudo ls && export x=3"
prop_checkSudoArgs6 = verifyNot checkSudoArgs "sudo -n -u export ls" -- >>> prop $ verifyNot checkSudoArgs "sudo echo ls"
prop_checkSudoArgs7 = verifyNot checkSudoArgs "sudo docker export foo" -- >>> prop $ verifyNot checkSudoArgs "sudo -n -u export ls"
-- >>> prop $ verifyNot checkSudoArgs "sudo docker export foo"
checkSudoArgs = CommandCheck (Basename "sudo") f checkSudoArgs = CommandCheck (Basename "sudo") f
where where
f t = potentially $ do f t = potentially $ do
@@ -1066,9 +1055,10 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
-- This mess is why ShellCheck prefers not to know. -- This mess is why ShellCheck prefers not to know.
parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:" parseOpts = getBsdOpts "vAknSbEHPa:g:h:p:u:c:T:r:"
prop_checkSourceArgs1 = verify checkSourceArgs "#!/bin/sh\n. script arg" -- |
prop_checkSourceArgs2 = verifyNot checkSourceArgs "#!/bin/sh\n. script" -- >>> prop $ verify checkSourceArgs "#!/bin/sh\n. script arg"
prop_checkSourceArgs3 = verifyNot checkSourceArgs "#!/bin/bash\n. script arg" -- >>> prop $ verifyNot checkSourceArgs "#!/bin/sh\n. script"
-- >>> prop $ verifyNot checkSourceArgs "#!/bin/bash\n. script arg"
checkSourceArgs = CommandCheck (Exactly ".") f checkSourceArgs = CommandCheck (Exactly ".") f
where where
f t = whenShell [Sh, Dash] $ f t = whenShell [Sh, Dash] $
@@ -1076,17 +1066,3 @@ checkSourceArgs = CommandCheck (Exactly ".") f
(file:arg1:_) -> warn (getId arg1) 2240 $ (file:arg1:_) -> warn (getId arg1) 2240 $
"The dot command does not support arguments in sh/dash. Set them as variables." "The dot command does not support arguments in sh/dash. Set them as variables."
_ -> return () _ -> return ()
prop_checkChmodDashr1 = verify checkChmodDashr "chmod -r 0755 dir"
prop_checkChmodDashr2 = verifyNot checkChmodDashr "chmod -R 0755 dir"
prop_checkChmodDashr3 = verifyNot checkChmodDashr "chmod a-r dir"
checkChmodDashr = CommandCheck (Basename "chmod") f
where
f t = mapM_ check $ arguments t
check t = potentially $ do
flag <- getLiteralString t
guard $ flag == "-r"
return $ warn (getId t) 2253 "Use -R to recurse, or explicitly a-r to remove read permissions."
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -1,21 +0,0 @@
{-
This empty file is provided for ease of patching in site specific checks.
However, there are no guarantees regarding compatibility between versions.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Checks.Custom (checker, ShellCheck.Checks.Custom.runTests) where
import ShellCheck.AnalyzerLib
import Test.QuickCheck
checker :: Parameters -> Checker
checker params = Checker {
perScript = const $ return (),
perToken = const $ return ()
}
prop_CustomTestsWork = True
return []
runTests = $quickCheckAll

View File

@@ -17,9 +17,8 @@
You should have received a copy of the GNU General Public License You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
-} -}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE FlexibleContexts #-}
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where module ShellCheck.Checks.ShellSupport (checker) where
import ShellCheck.AST import ShellCheck.AST
import ShellCheck.ASTLib import ShellCheck.ASTLib
@@ -33,9 +32,6 @@ import Data.Char
import Data.List import Data.List
import Data.Maybe import Data.Maybe
import qualified Data.Map as Map import qualified Data.Map as Map
import qualified Data.Set as Set
import Test.QuickCheck.All (forAllProperties)
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
data ForShell = ForShell [Shell] (Token -> Analysis) data ForShell = ForShell [Shell] (Token -> Analysis)
@@ -68,9 +64,10 @@ testChecker (ForShell _ t) =
verify c s = producesComments (testChecker c) s == Just True verify c s = producesComments (testChecker c) s == Just True
verifyNot c s = producesComments (testChecker c) s == Just False verifyNot c s = producesComments (testChecker c) s == Just False
prop_checkForDecimals1 = verify checkForDecimals "((3.14*c))" -- |
prop_checkForDecimals2 = verify checkForDecimals "foo[1.2]=bar" -- >>> prop $ verify checkForDecimals "((3.14*c))"
prop_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar" -- >>> prop $ verify checkForDecimals "foo[1.2]=bar"
-- >>> prop $ verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
checkForDecimals = ForShell [Sh, Dash, Bash] f checkForDecimals = ForShell [Sh, Dash, Bash] f
where where
f t@(TA_Expansion id _) = potentially $ do f t@(TA_Expansion id _) = potentially $ do
@@ -81,102 +78,63 @@ checkForDecimals = ForShell [Sh, Dash, Bash] f
f _ = return () f _ = return ()
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)" -- |
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]" -- >>> prop $ verify checkBashisms "while read a; do :; done < <(a)"
prop_checkBashisms3 = verify checkBashisms "echo $((i++))" -- >>> prop $ verify checkBashisms "[ foo -nt bar ]"
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)" -- >>> prop $ verify checkBashisms "echo $((i++))"
prop_checkBashisms5 = verify checkBashisms "source file" -- >>> prop $ verify checkBashisms "rm !(*.hs)"
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]" -- >>> prop $ verify checkBashisms "source file"
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}" -- >>> prop $ verify checkBashisms "[ \"$a\" == 42 ]"
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}" -- >>> prop $ verify checkBashisms "echo ${var[1]}"
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}" -- >>> prop $ verify checkBashisms "echo ${!var[@]}"
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}" -- >>> prop $ verify checkBashisms "echo ${!var*}"
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}" -- >>> prop $ verify checkBashisms "echo ${var:4:12}"
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}" -- >>> prop $ verifyNot checkBashisms "echo ${var:-4}"
prop_checkBashisms13= verify checkBashisms "exec -c env" -- >>> prop $ verify checkBashisms "echo ${var//foo/bar}"
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \"" -- >>> prop $ verify checkBashisms "exec -c env"
prop_checkBashisms15= verify checkBashisms "let n++" -- >>> prop $ verify checkBashisms "echo -n \"Foo: \""
prop_checkBashisms16= verify checkBashisms "echo $RANDOM" -- >>> prop $ verify checkBashisms "let n++"
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))" -- >>> prop $ verify checkBashisms "echo $RANDOM"
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null" -- >>> prop $ verify checkBashisms "echo $((RANDOM%6+1))"
prop_checkBashisms19= verify checkBashisms "foo > file*.txt" -- >>> prop $ verify checkBashisms "foo &> /dev/null"
prop_checkBashisms20= verify checkBashisms "read -ra foo" -- >>> prop $ verify checkBashisms "foo > file*.txt"
prop_checkBashisms21= verify checkBashisms "[ -a foo ]" -- >>> prop $ verify checkBashisms "read -ra foo"
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]" -- >>> prop $ verify checkBashisms "[ -a foo ]"
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT" -- >>> prop $ verifyNot checkBashisms "[ foo -a bar ]"
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM" -- >>> prop $ verify checkBashisms "trap mything ERR INT"
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123" -- >>> prop $ verifyNot checkBashisms "trap mything INT TERM"
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM" -- >>> prop $ verify checkBashisms "cat < /dev/tcp/host/123"
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*" -- >>> prop $ verify checkBashisms "trap mything ERR SIGTERM"
prop_checkBashisms28= verify checkBashisms "exec {n}>&2" -- >>> prop $ verify checkBashisms "echo *[^0-9]*"
prop_checkBashisms29= verify checkBashisms "echo ${!var}" -- >>> prop $ verify checkBashisms "exec {n}>&2"
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\"" -- >>> prop $ verify checkBashisms "echo ${!var}"
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\"" -- >>> prop $ verify checkBashisms "printf -v '%s' \"$1\""
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]" -- >>> prop $ verify checkBashisms "printf '%q' \"$1\""
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo" -- >>> prop $ verify checkBashisms "#!/bin/sh\necho -n foo"
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\necho -n foo"
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nlocal foo"
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM" -- >>> prop $ verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }" -- >>> prop $ verify checkBashisms "RANDOM=9; echo $RANDOM"
prop_checkBashisms40= verify checkBashisms "echo $(<file)" -- >>> prop $ verify checkBashisms "foo-bar() { true; }"
prop_checkBashisms41= verify checkBashisms "echo `<file`" -- >>> prop $ verify checkBashisms "echo $(<file)"
prop_checkBashisms42= verify checkBashisms "trap foo int" -- >>> prop $ verify checkBashisms "echo `<file`"
prop_checkBashisms43= verify checkBashisms "trap foo sigint" -- >>> prop $ verify checkBashisms "trap foo int"
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int" -- >>> prop $ verify checkBashisms "trap foo sigint"
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT" -- >>> prop $ verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null" -- >>> prop $ verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO" -- >>> prop $ verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file" -- >>> prop $ verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1" -- >>> prop $ verify checkBashisms "#!/bin/sh\ncmd >& file"
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}" -- >>> prop $ verify checkBashisms "#!/bin/sh\nfoo+=bar"
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}" -- >>> prop $ verify checkBashisms "#!/bin/sh\necho ${@%foo}"
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0" -- >>> prop $ verifyNot checkBashisms "#!/bin/sh\necho ${##}"
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
prop_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
prop_checkBashisms62 = verify checkBashisms "#!/bin/sh\nexport -f foo"
prop_checkBashisms63 = verifyNot checkBashisms "#!/bin/sh\nexport -p"
prop_checkBashisms64 = verify checkBashisms "#!/bin/sh\nreadonly -a"
prop_checkBashisms65 = verifyNot checkBashisms "#!/bin/sh\nreadonly -p"
prop_checkBashisms66 = verifyNot checkBashisms "#!/bin/sh\ncd -P ."
prop_checkBashisms67 = verify checkBashisms "#!/bin/sh\ncd -P -e ."
prop_checkBashisms68 = verify checkBashisms "#!/bin/sh\numask -p"
prop_checkBashisms69 = verifyNot checkBashisms "#!/bin/sh\numask -S"
prop_checkBashisms70 = verify checkBashisms "#!/bin/sh\ntrap -l"
prop_checkBashisms71 = verify checkBashisms "#!/bin/sh\ntype -a ls"
prop_checkBashisms72 = verifyNot checkBashisms "#!/bin/sh\ntype ls"
prop_checkBashisms73 = verify checkBashisms "#!/bin/sh\nunset -n namevar"
prop_checkBashisms74 = verifyNot checkBashisms "#!/bin/sh\nunset -f namevar"
prop_checkBashisms75 = verifyNot checkBashisms "#!/bin/sh\necho \"-n foo\""
prop_checkBashisms76 = verifyNot checkBashisms "#!/bin/sh\necho \"-ne foo\""
prop_checkBashisms77 = verifyNot checkBashisms "#!/bin/sh\necho -Q foo"
prop_checkBashisms78 = verify checkBashisms "#!/bin/sh\necho -ne foo"
prop_checkBashisms79 = verify checkBashisms "#!/bin/sh\nhash -l"
prop_checkBashisms80 = verifyNot checkBashisms "#!/bin/sh\nhash -r"
prop_checkBashisms81 = verifyNot checkBashisms "#!/bin/dash\nhash -v"
prop_checkBashisms82 = verifyNot checkBashisms "#!/bin/sh\nset -v +o allexport -o errexit -C"
prop_checkBashisms83 = verifyNot checkBashisms "#!/bin/sh\nset --"
prop_checkBashisms84 = verify checkBashisms "#!/bin/sh\nset -o pipefail"
prop_checkBashisms85 = verify checkBashisms "#!/bin/sh\nset -B"
prop_checkBashisms86 = verifyNot checkBashisms "#!/bin/dash\nset -o emacs"
prop_checkBashisms87 = verify checkBashisms "#!/bin/sh\nset -o emacs"
prop_checkBashisms88 = verifyNot checkBashisms "#!/bin/sh\nset -- wget -o foo 'https://some.url'"
prop_checkBashisms89 = verifyNot checkBashisms "#!/bin/sh\nopts=$-\nset -\"$opts\""
prop_checkBashisms90 = verifyNot checkBashisms "#!/bin/sh\nset -o \"$opt\""
prop_checkBashisms91 = verify checkBashisms "#!/bin/sh\nwait -n"
prop_checkBashisms92 = verify checkBashisms "#!/bin/sh\necho $((16#FF))"
prop_checkBashisms93 = verify checkBashisms "#!/bin/sh\necho $(( 10#$(date +%m) ))"
prop_checkBashisms94 = verify checkBashisms "#!/bin/sh\n[ -v var ]"
prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
checkBashisms = ForShell [Sh, Dash] $ \t -> do checkBashisms = ForShell [Sh, Dash] $ \t -> do
params <- ask params <- ask
kludge params t kludge params t
@@ -211,8 +169,6 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id "== in place of = is" warnMsg id "== in place of = is"
bashism (TC_Binary id SingleBracket "=~" _ _) = bashism (TC_Binary id SingleBracket "=~" _ _) =
warnMsg id "=~ regex matching is" warnMsg id "=~ regex matching is"
bashism (TC_Unary id SingleBracket "-v" _) =
warnMsg id "unary -v (in place of [ -n \"${var+x}\" ]) is"
bashism (TC_Unary id _ "-a" _) = bashism (TC_Unary id _ "-a" _) =
warnMsg id "unary -a in place of -e is" warnMsg id "unary -a in place of -e is"
bashism (TA_Unary id op _) bashism (TA_Unary id op _)
@@ -237,7 +193,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
bashism t@(TA_Variable id str _) | isBashVariable str = bashism t@(TA_Variable id str _) | isBashVariable str =
warnMsg id $ str ++ " is" warnMsg id $ str ++ " is"
bashism t@(T_DollarBraced id _ token) = do bashism t@(T_DollarBraced id token) = do
mapM_ check expansion mapM_ check expansion
when (isBashVariable var) $ when (isBashVariable var) $
warnMsg id $ var ++ " is" warnMsg id $ var ++ " is"
@@ -265,71 +221,20 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
warnMsg id "`<file` to read files is" warnMsg id "`<file` to read files is"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "echo" && argString `matches` flagRegex = | t `isCommand` "echo" && "-" `isPrefixOf` argString =
if isDash unless ("--" `isPrefixOf` argString) $ -- echo "-----"
then if isDash
when (argString /= "-n") $ then
warnMsg (getId arg) "echo flags besides -n" when (argString /= "-n") $
else warnMsg (getId arg) "echo flags besides -n"
warnMsg (getId arg) "echo flags are" else
where warnMsg (getId arg) "echo flags are"
argString = concat $ oversimplify arg where argString = concat $ oversimplify arg
flagRegex = mkRegex "^-[eEsn]+$"
bashism t@(T_SimpleCommand _ _ (cmd:arg:_)) bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
| t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) = | t `isCommand` "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
warnMsg (getId arg) "exec flags are" warnMsg (getId arg) "exec flags are"
bashism t@(T_SimpleCommand id _ _) bashism t@(T_SimpleCommand id _ _)
| t `isCommand` "let" = warnMsg id "'let' is" | t `isCommand` "let" = warnMsg id "'let' is"
bashism t@(T_SimpleCommand _ _ (cmd:args))
| t `isCommand` "set" = unless isDash $
checkOptions $ getLiteralArgs args
where
-- Get the literal options from a list of arguments,
-- up until the first non-literal one
getLiteralArgs :: [Token] -> [(Id, String)]
getLiteralArgs (first:rest) = fromMaybe [] $ do
str <- getLiteralString first
return $ (getId first, str) : getLiteralArgs rest
getLiteralArgs [] = []
-- Check a flag-option pair (such as -o errexit)
checkOptions (flag@(fid,flag') : opt@(oid,opt') : rest)
| flag' `matches` oFlagRegex = do
when (opt' `notElem` longOptions) $
warnMsg oid $ "set option " <> opt' <> " is"
checkFlags (flag:rest)
| otherwise = checkFlags (flag:opt:rest)
checkOptions (flag:rest) = checkFlags (flag:rest)
checkOptions _ = return ()
-- Check that each option in a sequence of flags
-- (such as -aveo) is valid
checkFlags (flag@(fid, flag'):rest)
| startsOption flag' = do
unless (flag' `matches` validFlagsRegex) $
forM_ (tail flag') $ \letter ->
when (letter `notElem` optionsSet) $
warnMsg fid $ "set flag " <> ('-':letter:" is")
checkOptions rest
| beginsWithDoubleDash flag' = do
warnMsg fid $ "set flag " <> flag' <> " is"
checkOptions rest
-- Either a word that doesn't start with a dash, or simply '--',
-- so stop checking.
| otherwise = return ()
checkFlags [] = return ()
options = "abCefhmnuvxo"
optionsSet = Set.fromList options
startsOption = (`matches` mkRegex "^(\\+|-[^-])")
oFlagRegex = mkRegex $ "^[-+][" <> options <> "]*o$"
validFlagsRegex = mkRegex $ "^[-+]([" <> options <> "]+o?|o)$"
beginsWithDoubleDash = (`matches` mkRegex "^--.+$")
longOptions = Set.fromList
[ "allexport", "errexit", "ignoreeof", "monitor", "noclobber"
, "noexec", "noglob", "nolog", "notify" , "nounset", "verbose"
, "vi", "xtrace" ]
bashism t@(T_SimpleCommand id _ (cmd:rest)) = bashism t@(T_SimpleCommand id _ (cmd:rest)) =
let name = fromMaybe "" $ getCommandName t let name = fromMaybe "" $ getCommandName t
@@ -338,8 +243,7 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
when (name `elem` unsupportedCommands) $ when (name `elem` unsupportedCommands) $
warnMsg id $ "'" ++ name ++ "' is" warnMsg id $ "'" ++ name ++ "' is"
potentially $ do potentially $ do
allowed' <- Map.lookup name allowedFlags allowed <- Map.lookup name allowedFlags
allowed <- allowed'
(word, flag) <- listToMaybe $ (word, flag) <- listToMaybe $
filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags filter (\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is" return . warnMsg (getId word) $ name ++ " -" ++ flag ++ " is"
@@ -374,28 +278,16 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
"typeset" "typeset"
] ++ if not isDash then ["local"] else [] ] ++ if not isDash then ["local"] else []
allowedFlags = Map.fromList [ allowedFlags = Map.fromList [
("cd", Just ["L", "P"]), ("exec", []),
("exec", Just []), ("export", ["-p"]),
("export", Just ["p"]), ("printf", []),
("hash", Just $ if isDash then ["r", "v"] else ["r"]), ("read", if isDash then ["r", "p"] else ["r"]),
("jobs", Just ["l", "p"]), ("ulimit", ["f"])
("printf", Just []),
("read", Just $ if isDash then ["r", "p"] else ["r"]),
("readonly", Just ["p"]),
("trap", Just []),
("type", Just []),
("ulimit", if isDash then Nothing else Just ["f"]),
("umask", Just ["S"]),
("unset", Just ["f", "v"]),
("wait", Just [])
] ]
bashism t@(T_SourceCommand id src _) = bashism t@(T_SourceCommand id src _) =
let name = fromMaybe "" $ getCommandName src let name = fromMaybe "" $ getCommandName src
in when (name == "source") $ warnMsg id "'source' in place of '.' is" in do
bashism (TA_Expansion _ (T_Literal id str : _)) | str `matches` radix = when (name == "source") $ warnMsg id "'source' in place of '.' is"
when (str `matches` radix) $ warnMsg id "arithmetic base conversion is"
where
radix = mkRegex "^[0-9]+#"
bashism _ = return () bashism _ = return ()
varChars="_0-9a-zA-Z" varChars="_0-9a-zA-Z"
@@ -410,11 +302,10 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
] ]
bashVars = [ bashVars = [
"OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME", "OSTYPE", "MACHTYPE", "HOSTTYPE", "HOSTNAME",
"DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS", "DIRSTACK", "EUID", "UID", "SHLVL", "PIPESTATUS", "SHELLOPTS"
"_"
] ]
bashDynamicVars = [ "RANDOM", "SECONDS" ] bashDynamicVars = [ "RANDOM", "SECONDS" ]
dashVars = [ "_" ] dashVars = [ ]
isBashVariable var = isBashVariable var =
(var `elem` bashDynamicVars (var `elem` bashDynamicVars
|| var `elem` bashVars && not (isAssigned var)) || var `elem` bashVars && not (isAssigned var))
@@ -425,51 +316,39 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
Assignment (_, _, name, _) -> name == var Assignment (_, _, name, _) -> name == var
_ -> False _ -> False
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')" -- |
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")" -- >>> prop $ verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')" -- >>> prop $ verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
checkEchoSed = ForShell [Bash, Ksh] f checkEchoSed = ForShell [Bash, Ksh] f
where where
f (T_Redirecting id lefts r) =
when (any redirectHereString lefts) $
checkSed id rcmd
where
redirectHereString :: Token -> Bool
redirectHereString t = case t of
(T_FdRedirect _ _ T_HereString{}) -> True
_ -> False
rcmd = oversimplify r
f (T_Pipeline id _ [a, b]) = f (T_Pipeline id _ [a, b]) =
when (acmd == ["echo", "${VAR}"]) $ when (acmd == ["echo", "${VAR}"]) $
checkSed id bcmd case bcmd of
["sed", v] -> checkIn v
["sed", "-e", v] -> checkIn v
_ -> return ()
where where
-- This should have used backreferences, but TDFA doesn't support them
sedRe = mkRegex "^s(.)([^\n]*)g?$"
isSimpleSed s = fromMaybe False $ do
[first,rest] <- matchRegex sedRe s
let delimiters = filter (== head first) rest
guard $ length delimiters == 2
return True
acmd = oversimplify a acmd = oversimplify a
bcmd = oversimplify b bcmd = oversimplify b
checkIn s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
f _ = return () f _ = return ()
checkSed id ["sed", v] = checkIn id v
checkSed id ["sed", "-e", v] = checkIn id v
checkSed _ _ = return ()
-- This should have used backreferences, but TDFA doesn't support them -- |
sedRe = mkRegex "^s(.)([^\n]*)g?$" -- >>> prop $ verify checkBraceExpansionVars "echo {1..$n}"
isSimpleSed s = fromMaybe False $ do -- >>> prop $ verifyNot checkBraceExpansionVars "echo {1,3,$n}"
[first,rest] <- matchRegex sedRe s -- >>> prop $ verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
let delimiters = filter (== head first) rest -- >>> prop $ verify checkBraceExpansionVars "echo {$i..100}"
guard $ length delimiters == 2
return True
checkIn id s =
when (isSimpleSed s) $
style id 2001 "See if you can use ${variable//search/replace} instead."
prop_checkBraceExpansionVars1 = verify checkBraceExpansionVars "echo {1..$n}"
prop_checkBraceExpansionVars2 = verifyNot checkBraceExpansionVars "echo {1,3,$n}"
prop_checkBraceExpansionVars3 = verify checkBraceExpansionVars "eval echo DSC{0001..$n}.jpg"
prop_checkBraceExpansionVars4 = verify checkBraceExpansionVars "echo {$i..100}"
checkBraceExpansionVars = ForShell [Bash] f checkBraceExpansionVars = ForShell [Bash] f
where where
f t@(T_BraceExpansion id list) = mapM_ check list f t@(T_BraceExpansion id list) = mapM_ check list
@@ -494,12 +373,13 @@ checkBraceExpansionVars = ForShell [Bash] f
return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval" return $ isJust cmd && fromJust cmd `isUnqualifiedCommand` "eval"
prop_checkMultiDimensionalArrays1 = verify checkMultiDimensionalArrays "foo[a][b]=3" -- |
prop_checkMultiDimensionalArrays2 = verifyNot checkMultiDimensionalArrays "foo[a]=3" -- >>> prop $ verify checkMultiDimensionalArrays "foo[a][b]=3"
prop_checkMultiDimensionalArrays3 = verify checkMultiDimensionalArrays "foo=( [a][b]=c )" -- >>> prop $ verifyNot checkMultiDimensionalArrays "foo[a]=3"
prop_checkMultiDimensionalArrays4 = verifyNot checkMultiDimensionalArrays "foo=( [a]=c )" -- >>> prop $ verify checkMultiDimensionalArrays "foo=( [a][b]=c )"
prop_checkMultiDimensionalArrays5 = verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}" -- >>> prop $ verifyNot checkMultiDimensionalArrays "foo=( [a]=c )"
prop_checkMultiDimensionalArrays6 = verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}" -- >>> prop $ verify checkMultiDimensionalArrays "echo ${foo[bar][baz]}"
-- >>> prop $ verifyNot checkMultiDimensionalArrays "echo ${foo[bar]}"
checkMultiDimensionalArrays = ForShell [Bash] f checkMultiDimensionalArrays = ForShell [Bash] f
where where
f token = f token =
@@ -514,16 +394,17 @@ checkMultiDimensionalArrays = ForShell [Bash] f
re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well re = mkRegex "^\\[.*\\]\\[.*\\]" -- Fixme, this matches ${foo:- [][]} and such as well
isMultiDim t = getBracedModifier (bracedString t) `matches` re isMultiDim t = getBracedModifier (bracedString t) `matches` re
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '" -- |
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '" -- >>> prop $ verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '" -- >>> prop $ verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '" -- >>> prop $ verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '" -- >>> prop $ verify checkPS1Assignments "PS1=$'\\x1b[c '"
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '" -- >>> prop $ verify checkPS1Assignments "PS1=$'\\e[3m; '"
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '" -- >>> prop $ verify checkPS1Assignments "export PS1=$'\\e[3m; '"
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '" -- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'" -- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
prop_checkPS18 = verifyNot checkPS1Assignments "PS1='\\[\\e\\]'" -- >>> prop $ verifyNot checkPS1Assignments "PS1='e033x1B'"
-- >>> prop $ verifyNot checkPS1Assignments "PS1='\\[\\e\\]'"
checkPS1Assignments = ForShell [Bash] f checkPS1Assignments = ForShell [Bash] f
where where
f token = case token of f token = case token of
@@ -539,7 +420,3 @@ checkPS1Assignments = ForShell [Bash] f
isJust $ matchRegex escapeRegex unenclosed isJust $ matchRegex escapeRegex unenclosed
enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager enclosedRegex = mkRegex "\\\\\\[.*\\\\\\]" -- FIXME: shouldn't be eager
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033" escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
return []
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])

View File

@@ -36,29 +36,13 @@ internalVariables = [
-- Ksh -- Ksh
, ".sh.version" , ".sh.version"
-- shflags
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
"flags_error", "flags_return"
] ]
specialVariablesWithoutSpaces = [ variablesWithoutSpaces = [
"$", "-", "?", "!", "#" "$", "-", "?", "!", "#",
]
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO", "BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID", "OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES" "COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
-- shflags
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
]
specialVariables = specialVariablesWithoutSpaces ++ ["@", "*"]
unbracedVariables = specialVariables ++ [
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
] ]
arrayVariables = [ arrayVariables = [
@@ -125,7 +109,6 @@ shellForExecutable name =
case name of case name of
"sh" -> return Sh "sh" -> return Sh
"bash" -> return Bash "bash" -> return Bash
"bats" -> return Bash
"dash" -> return Dash "dash" -> return Dash
"ash" -> return Dash -- There's also a warning for this. "ash" -> return Dash -- There's also a warning for this.
"ksh" -> return Ksh "ksh" -> return Ksh

View File

@@ -1,409 +0,0 @@
{-
Copyright 2018-2019 Vidar Holen, Ng Zhi An
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
import ShellCheck.Interface
import Control.Monad.State
import Data.Array
import Data.List
import Data.Semigroup
import GHC.Exts (sortWith)
import Test.QuickCheck
-- The Ranged class is used for types that has a start and end position.
class Ranged a where
start :: a -> Position
end :: a -> Position
overlap :: a -> a -> Bool
overlap x y =
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
where
yStart = start y
yEnd = end y
xStart = start x
xEnd = end x
-- Set a new start and end position on a Ranged
setRange :: (Position, Position) -> a -> a
-- Tests auto-verify that overlap commutes
assertOverlap x y = overlap x y && overlap y x
assertNoOverlap x y = not (overlap x y) && not (overlap y x)
prop_overlap_contiguous = assertNoOverlap
(tFromStart 10 12 "foo" 1)
(tFromStart 12 14 "bar" 2)
prop_overlap_adjacent_zerowidth = assertNoOverlap
(tFromStart 3 3 "foo" 1)
(tFromStart 3 3 "bar" 2)
prop_overlap_enclosed = assertOverlap
(tFromStart 3 5 "foo" 1)
(tFromStart 1 10 "bar" 2)
prop_overlap_partial = assertOverlap
(tFromStart 1 5 "foo" 1)
(tFromStart 3 7 "bar" 2)
instance Ranged PositionedComment where
start = pcStartPos
end = pcEndPos
setRange (s, e) pc = pc {
pcStartPos = s,
pcEndPos = e
}
instance Ranged Replacement where
start = repStartPos
end = repEndPos
setRange (s, e) r = r {
repStartPos = s,
repEndPos = e
}
-- The Monoid instance for Fix merges fixes that do not conflict.
-- TODO: Make an efficient 'mconcat'
instance Monoid Fix where
mempty = newFix
mappend = (<>)
instance Semigroup Fix where
f1 <> f2 =
-- FIXME: This might need to also discard adjacent zero-width ranges for
-- when two fixes change the same AST node, e.g. `foo` -> "$(foo)"
if or [ r2 `overlap` r1 | r1 <- fixReplacements f1, r2 <- fixReplacements f2 ]
then f1
else newFix {
fixReplacements = fixReplacements f1 ++ fixReplacements f2
}
-- Conveniently apply a transformation to positions in a Fix
mapPositions :: (Position -> Position) -> Fix -> Fix
mapPositions f = adjustFix
where
adjustReplacement rep =
rep {
repStartPos = f $ repStartPos rep,
repEndPos = f $ repEndPos rep
}
adjustFix fix =
fix {
fixReplacements = map adjustReplacement $ fixReplacements fix
}
-- Rewrite a Ranged from a tabstop of 8 to 1
removeTabStops :: Ranged a => a -> Array Int String -> a
removeTabStops range ls =
let startColumn = realignColumn lineNo colNo range
endColumn = realignColumn endLineNo endColNo range
startPosition = (start range) { posColumn = startColumn }
endPosition = (end range) { posColumn = endColumn } in
setRange (startPosition, endPosition) range
where
realignColumn lineNo colNo c =
if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
then real (ls ! fromIntegral (lineNo c)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
-- hit this case at the end of line, and if we don't hit the target
-- return real + (target - v)
real [] r v target = r + (target - v)
real ('\t':rest) r v target = real rest (r+1) (v + 8 - (v `mod` 8)) target
real (_:rest) r v target = real rest (r+1) (v+1) target
lineNo = posLine . start
endLineNo = posLine . end
colNo = posColumn . start
endColNo = posColumn . end
-- A replacement that spans multiple line is applied by:
-- 1. merging the affected lines into a single string using `unlines`
-- 2. apply the replacement as if it only spanned a single line
-- The tricky part is adjusting the end column of the replacement
-- (the end line doesn't matter because there is only one line)
--
-- aaS <--- start of replacement (row 1 column 3)
-- bbbb
-- cEc
-- \------- end of replacement (row 3 column 2)
--
-- a flattened string will look like:
--
-- "aaS\nbbbb\ncEc\n"
--
-- The column of E has to be adjusted by:
-- 1. lengths of lines to be replaced, except the end row itself
-- 2. end column of the replacement
-- 3. number of '\n' by `unlines`
multiToSingleLine :: [Fix] -> Array Int String -> ([Fix], String)
multiToSingleLine fixes lines =
(map (mapPositions adjust) fixes, unlines $ elems lines)
where
-- A prefix sum tree from line number to column shift.
-- FIXME: The tree will be totally unbalanced.
shiftTree :: PSTree Int
shiftTree =
foldl (\t (n,s) -> addPSValue (n+1) (length s + 1) t) newPSTree $
assocs lines
singleString = unlines $ elems lines
adjust pos =
pos {
posLine = 1,
posColumn = (posColumn pos) +
(fromIntegral $ getPrefixSum (fromIntegral $ posLine pos) shiftTree)
}
-- Apply a fix and return resulting lines.
-- The number of lines can increase or decrease with no obvious mapping back, so
-- the function does not return an array.
applyFix :: Fix -> Array Int String -> [String]
applyFix fix fileLines =
let
untabbed = fix {
fixReplacements =
map (\c -> removeTabStops c fileLines) $
fixReplacements fix
}
(adjustedFixes, singleLine) = multiToSingleLine [untabbed] fileLines
in
lines . runFixer $ applyFixes2 adjustedFixes singleLine
-- start and end comes from pos, which is 1 based
prop_doReplace1 = doReplace 0 0 "1234" "A" == "A1234" -- technically not valid
prop_doReplace2 = doReplace 1 1 "1234" "A" == "A1234"
prop_doReplace3 = doReplace 1 2 "1234" "A" == "A234"
prop_doReplace4 = doReplace 3 3 "1234" "A" == "12A34"
prop_doReplace5 = doReplace 4 4 "1234" "A" == "123A4"
prop_doReplace6 = doReplace 5 5 "1234" "A" == "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
in
x ++ r ++ z
-- Fail if the 'expected' string is not result when applying 'fixes' to 'original'.
testFixes :: String -> String -> [Fix] -> Bool
testFixes expected original fixes =
actual == expected
where
actual = runFixer (applyFixes2 fixes original)
-- A Fixer allows doing repeated modifications of a string where each
-- replacement automatically accounts for shifts from previous ones.
type Fixer a = State (PSTree Int) a
-- Apply a single replacement using its indices into the original string.
-- It does not handle multiple lines, all line indices must be 1.
applyReplacement2 :: Replacement -> String -> Fixer String
applyReplacement2 rep string = do
tree <- get
let transform pos = pos + getPrefixSum pos tree
let originalPos = (repStartPos rep, repEndPos rep)
(oldStart, oldEnd) = tmap (fromInteger . posColumn) originalPos
(newStart, newEnd) = tmap transform (oldStart, oldEnd)
let (l1, l2) = tmap posLine originalPos in
when (l1 /= 1 || l2 /= 1) $
error "ShellCheck internal error, please report: bad cross-line fix"
let replacer = repString rep
let shift = (length replacer) - (oldEnd - oldStart)
let insertionPoint =
case repInsertionPoint rep of
InsertBefore -> oldStart
InsertAfter -> oldEnd+1
put $ addPSValue insertionPoint shift tree
return $ doReplace newStart newEnd string replacer
where
tmap f (a,b) = (f a, f b)
-- Apply a list of Replacements in the correct order
applyReplacements2 :: [Replacement] -> String -> Fixer String
applyReplacements2 reps str =
foldM (flip applyReplacement2) str $
reverse $ sortWith repPrecedence reps
-- Apply all fixes with replacements in the correct order
applyFixes2 :: [Fix] -> String -> Fixer String
applyFixes2 fixes = applyReplacements2 (concatMap fixReplacements fixes)
-- Get the final value of a Fixer.
runFixer :: Fixer a -> a
runFixer f = evalState f newPSTree
-- A Prefix Sum Tree that lets you look up the sum of values at and below an index.
-- It's implemented essentially as a Fenwick tree without the bit-based balancing.
-- The last Num is the sum of the left branch plus current element.
data PSTree n = PSBranch n (PSTree n) (PSTree n) n | PSLeaf
deriving (Show)
newPSTree :: Num n => PSTree n
newPSTree = PSLeaf
-- Get the sum of values whose keys are <= 'target'
getPrefixSum :: (Ord n, Num n) => n -> PSTree n -> n
getPrefixSum = f 0
where
f sum _ PSLeaf = sum
f sum target (PSBranch pivot left right cumulative) =
case () of
_ | target < pivot -> f sum target left
_ | target > pivot -> f (sum+cumulative) target right
_ -> sum+cumulative
-- Add a value to the Prefix Sum tree at the given index.
-- Values accumulate: addPSValue 42 2 . addPSValue 42 3 == addPSValue 42 5
addPSValue :: (Ord n, Num n) => n -> n -> PSTree n -> PSTree n
addPSValue key value tree = if value == 0 then tree else f tree
where
f PSLeaf = PSBranch key PSLeaf PSLeaf value
f (PSBranch pivot left right sum) =
case () of
_ | key < pivot -> PSBranch pivot (f left) right (sum + value)
_ | key > pivot -> PSBranch pivot left (f right) sum
_ -> PSBranch pivot left right (sum + value)
prop_pstreeSumsCorrectly kvs targets =
let
-- Trivial O(n * m) implementation
dumbPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
dumbPrefixSums kvs targets =
let prefixSum target = sum . map snd . filter (\(k,v) -> k <= target) $ kvs
in map prefixSum targets
-- PSTree O(n * log m) implementation
smartPrefixSums :: [(Int, Int)] -> [Int] -> [Int]
smartPrefixSums kvs targets =
let tree = foldl (\tree (pos, shift) -> addPSValue pos shift tree) PSLeaf kvs
in map (\x -> getPrefixSum x tree) targets
in smartPrefixSums kvs targets == dumbPrefixSums kvs targets
-- Semi-convenient functions for constructing tests.
testFix :: [Replacement] -> Fix
testFix list = newFix {
fixReplacements = list
}
tFromStart :: Int -> Int -> String -> Int -> Replacement
tFromStart start end repl order =
newReplacement {
repStartPos = newPosition {
posLine = 1,
posColumn = fromIntegral start
},
repEndPos = newPosition {
posLine = 1,
posColumn = fromIntegral end
},
repString = repl,
repPrecedence = order,
repInsertionPoint = InsertAfter
}
tFromEnd start end repl order =
(tFromStart start end repl order) {
repInsertionPoint = InsertBefore
}
prop_simpleFix1 = testFixes "hello world" "hell world" [
testFix [
tFromEnd 5 5 "o" 1
]]
prop_anchorsLeft = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "foo" 1,
tFromStart 4 4 "bar" 2
]]
prop_anchorsRight = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "bar" 1,
tFromEnd 4 4 "foo" 2
]]
prop_anchorsBoth1 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromStart 4 4 "bar" 2,
tFromEnd 4 4 "foo" 1
]]
prop_anchorsBoth2 = testFixes "-->foobar<--" "--><--" [
testFix [
tFromEnd 4 4 "foo" 2,
tFromStart 4 4 "bar" 1
]]
prop_composeFixes1 = testFixes "cd \"$1\" || exit" "cd $1" [
testFix [
tFromStart 4 4 "\"" 10,
tFromEnd 6 6 "\"" 10
],
testFix [
tFromEnd 6 6 " || exit" 5
]]
prop_composeFixes2 = testFixes "$(\"$1\")" "`$1`" [
testFix [
tFromStart 1 2 "$(" 5,
tFromEnd 4 5 ")" 5
],
testFix [
tFromStart 2 2 "\"" 10,
tFromEnd 4 4 "\"" 10
]]
prop_composeFixes3 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromEnd 2 2 ")" 3,
tFromStart 2 2 "[" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes4 = testFixes "(x)[x]" "xx" [
testFix [
tFromStart 1 1 "(" 4,
tFromStart 2 2 "[" 3,
tFromEnd 2 2 ")" 2,
tFromEnd 3 3 "]" 1
]]
prop_composeFixes5 = testFixes "\"$(x)\"" "`x`" [
testFix [
tFromStart 1 2 "$(" 2,
tFromEnd 3 4 ")" 2,
tFromStart 1 1 "\"" 1,
tFromEnd 4 4 "\"" 1
]]
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net

View File

@@ -1,255 +0,0 @@
{-
Copyright 2019 Vidar 'koala_man' Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
{-# LANGUAGE TemplateHaskell #-}
module ShellCheck.Formatter.Diff (format, ShellCheck.Formatter.Diff.runTests) where
import ShellCheck.Interface
import ShellCheck.Fixer
import ShellCheck.Formatter.Format
import Control.Monad
import Data.Algorithm.Diff
import Data.Array
import Data.IORef
import Data.List
import qualified Data.Monoid as Monoid
import Data.Maybe
import qualified Data.Map as M
import GHC.Exts (sortWith)
import System.IO
import System.FilePath
import Test.QuickCheck
import Debug.Trace
ltt x = trace (show x) x
format :: FormatterOptions -> IO Formatter
format options = do
didOutput <- newIORef False
shouldColor <- shouldOutputColor (foColorOption options)
let color = if shouldColor then colorize else nocolor
return Formatter {
header = return (),
footer = checkFooter didOutput color,
onFailure = reportFailure color,
onResult = reportResult didOutput color
}
contextSize = 3
red = 31
green = 32
yellow = 33
cyan = 36
bold = 1
nocolor n = id
colorize n s = (ansi n) ++ s ++ (ansi 0)
ansi n = "\x1B[" ++ show n ++ "m"
printErr :: ColorFunc -> String -> IO ()
printErr color = hPutStrLn stderr . color bold . color red
reportFailure color file msg = printErr color $ file ++ ": " ++ msg
checkFooter didOutput color = do
output <- readIORef didOutput
unless output $
printErr color "Issues were detected, but none were auto-fixable. Use another format to see them."
type ColorFunc = (Int -> String -> String)
data LFStatus = LinefeedMissing | LinefeedOk
data DiffDoc a = DiffDoc String LFStatus [DiffRegion a]
data DiffRegion a = DiffRegion (Int, Int) (Int, Int) [Diff a]
reportResult :: (IORef Bool) -> ColorFunc -> CheckResult -> SystemInterface IO -> IO ()
reportResult didOutput color result sys = do
let comments = crComments result
let suggestedFixes = mapMaybe pcFix comments
let fixmap = buildFixMap suggestedFixes
mapM_ output $ M.toList fixmap
where
output (name, fix) = do
file <- (siReadFile sys) name
case file of
Right contents -> do
putStrLn $ formatDoc color $ makeDiff name contents fix
writeIORef didOutput True
Left msg -> reportFailure color name msg
hasTrailingLinefeed str =
case str of
[] -> True
_ -> last str == '\n'
coversLastLine regions =
case regions of
[] -> False
_ -> (fst $ last regions)
-- TODO: Factor this out into a unified diff library because we're doing a lot
-- of the heavy lifting anyways.
makeDiff :: String -> String -> Fix -> DiffDoc String
makeDiff name contents fix = do
let hunks = groupDiff $ computeDiff contents fix
let lf = if coversLastLine hunks && not (hasTrailingLinefeed contents)
then LinefeedMissing
else LinefeedOk
DiffDoc name lf $ findRegions hunks
computeDiff :: String -> Fix -> [Diff String]
computeDiff contents fix =
let old = lines contents
array = listArray (1, fromIntegral $ (length old)) old
new = applyFix fix array
in getDiff old new
-- Group changes into hunks
groupDiff :: [Diff a] -> [(Bool, [Diff a])]
groupDiff = filter (\(_, l) -> not (null l)) . hunt []
where
-- Churn through 'Both's until we find a difference
hunt current [] = [(False, reverse current)]
hunt current (x@Both {}:rest) = hunt (x:current) rest
hunt current list =
let (context, previous) = splitAt contextSize current
in (False, reverse previous) : gather context 0 list
-- Pick out differences until we find a run of Both's
gather current n [] =
let (extras, patch) = splitAt (max 0 $ n - contextSize) current
in [(True, reverse patch), (False, reverse extras)]
gather current n list@(Both {}:_) | n == contextSize*2 =
let (context, previous) = splitAt contextSize current
in (True, reverse previous) : hunt context list
gather current n (x@Both {}:rest) = gather (x:current) (n+1) rest
gather current n (x:rest) = gather (x:current) 0 rest
-- Get line numbers for hunks
findRegions :: [(Bool, [Diff String])] -> [DiffRegion String]
findRegions = find' 1 1
where
find' _ _ [] = []
find' left right ((output, run):rest) =
let (dl, dr) = countDelta run
remainder = find' (left+dl) (right+dr) rest
in
if output
then DiffRegion (left, dl) (right, dr) run : remainder
else remainder
-- Get left/right line counts for a hunk
countDelta :: [Diff a] -> (Int, Int)
countDelta = count' 0 0
where
count' left right [] = (left, right)
count' left right (x:rest) =
case x of
Both {} -> count' (left+1) (right+1) rest
First {} -> count' (left+1) right rest
Second {} -> count' left (right+1) rest
formatRegion :: ColorFunc -> LFStatus -> DiffRegion String -> String
formatRegion color lf (DiffRegion left right diffs) =
let header = color cyan ("@@ -" ++ (tup left) ++ " +" ++ (tup right) ++" @@")
in
unlines $ header : reverse (getStrings lf (reverse diffs))
where
noLF = "\\ No newline at end of file"
getStrings LinefeedOk list = map format list
getStrings LinefeedMissing list@((Both _ _):_) = noLF : map format list
getStrings LinefeedMissing list@((First _):_) = noLF : map format list
getStrings LinefeedMissing (last:rest) = format last : getStrings LinefeedMissing rest
tup (a,b) = (show a) ++ "," ++ (show b)
format (Both x _) = ' ':x
format (First x) = color red $ '-':x
format (Second x) = color green $ '+':x
splitLast [] = ([], [])
splitLast x =
let (last, rest) = splitAt 1 $ reverse x
in (reverse rest, last)
formatDoc color (DiffDoc name lf regions) =
let (most, last) = splitLast regions
in
(color bold $ "--- " ++ ("a" </> name)) ++ "\n" ++
(color bold $ "+++ " ++ ("b" </> name)) ++ "\n" ++
concatMap (formatRegion color LinefeedOk) most ++
concatMap (formatRegion color lf) last
-- Create a Map from filename to Fix
buildFixMap :: [Fix] -> M.Map String Fix
buildFixMap fixes = perFile
where
splitFixes = concatMap splitFixByFile fixes
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
-- There are currently no multi-file fixes, but let's handle it anyways
splitFixByFile :: Fix -> [Fix]
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
where
sameFile rep1 rep2 = (posFile $ repStartPos rep1) == (posFile $ repStartPos rep2)
makeFix reps = newFix { fixReplacements = reps }
groupByMap :: (Ord k, Monoid v) => (v -> k) -> [v] -> M.Map k v
groupByMap f = M.fromListWith Monoid.mappend . map (\x -> (f x, x))
-- For building unit tests
b n = Both n n
l = First
r = Second
prop_identifiesProperContext = groupDiff [b 1, b 2, b 3, b 4, l 5, b 6, b 7, b 8, b 9] ==
[(False, [b 1]), -- Omitted
(True, [b 2, b 3, b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextFromStartIfNecessary = groupDiff [b 4, l 5, b 6, b 7, b 8, b 9] ==
[ -- Nothing omitted
(True, [b 4, l 5, b 6, b 7, b 8]), -- A change with three lines of context
(False, [b 9])] -- Omitted
prop_includesContextUntilEndIfNecessary = groupDiff [b 4, l 5] ==
[ -- Nothing omitted
(True, [b 4, l 5])
] -- Nothing Omitted
prop_splitsIntoMultipleHunks = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, b 7, r 8] ==
[ -- Nothing omitted
(True, [l 1, b 1, b 2, b 3]),
(False, [b 4]),
(True, [b 5, b 6, b 7, r 8])
] -- Nothing Omitted
prop_splitsIntoMultipleHunksUnlessTouching = groupDiff [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7] ==
[
(True, [l 1, b 1, b 2, b 3, b 4, b 5, b 6, r 7])
]
prop_countDeltasWorks = countDelta [b 1, l 2, r 3, r 4, b 5] == (3,4)
prop_countDeltasWorks2 = countDelta [] == (0,0)
return []
runTests = $quickCheckAll

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -21,13 +21,6 @@ module ShellCheck.Formatter.Format where
import ShellCheck.Data import ShellCheck.Data
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Fixer
import Control.Monad
import Data.Array
import Data.List
import System.IO
import System.Info
-- A formatter that carries along an arbitrary piece of data -- A formatter that carries along an arbitrary piece of data
data Formatter = Formatter { data Formatter = Formatter {
@@ -57,23 +50,21 @@ severityText pc =
makeNonVirtual comments contents = makeNonVirtual comments contents =
map fix comments map fix comments
where where
list = lines contents ls = lines contents
arr = listArray (1, length list) list fix c = c {
untabbedFix f = newFix { pcStartPos = (pcStartPos c) {
fixReplacements = map (\r -> removeTabStops r arr) (fixReplacements f) posColumn = realignColumn lineNo colNo c
}
, pcEndPos = (pcEndPos c) {
posColumn = realignColumn endLineNo endColNo c
}
} }
fix c = (removeTabStops c arr) { realignColumn lineNo colNo c =
pcFix = fmap untabbedFix (pcFix c) if lineNo c > 0 && lineNo c <= fromIntegral (length ls)
} then real (ls !! fromIntegral (lineNo c - 1)) 0 0 (colNo c)
else colNo c
real _ r v target | target <= v = r
shouldOutputColor :: ColorOption -> IO Bool real [] r v _ = r -- should never happen
shouldOutputColor colorOption = do real ('\t':rest) r v target =
term <- hIsTerminalDevice stdout real rest (r+1) (v + 8 - (v `mod` 8)) target
let windows = "mingw" `isPrefixOf` os real (_:rest) r v target = real rest (r+1) (v+1) target
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return useColor

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net

View File

@@ -1,6 +1,6 @@
{-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE OverloadedStrings #-}
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -30,7 +30,6 @@ import GHC.Exts
import System.IO import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do format = do
ref <- newIORef [] ref <- newIORef []
return Formatter { return Formatter {
@@ -46,16 +45,11 @@ instance ToJSON Replacement where
end = repEndPos replacement end = repEndPos replacement
str = repString replacement in str = repString replacement in
object [ object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start, "line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end, "endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end, "endColumn" .= posColumn end,
"replacement" .= str "replaceWith" .= str
] ]
instance ToJSON PositionedComment where instance ToJSON PositionedComment where
@@ -97,13 +91,8 @@ instance ToJSON Fix where
] ]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref result _ =
collectResult ref cr sys = mapM_ f groups modifyIORef ref (\x -> crComments result ++ x)
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = modifyIORef ref (\x -> comments ++ x)
finish ref = do finish ref = do
list <- readIORef ref list <- readIORef ref

View File

@@ -1,127 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
{-
Copyright 2012-2019 Vidar Holen
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.JSON1 (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Data.Aeson
import Data.IORef
import Data.Monoid
import GHC.Exts
import System.IO
import qualified Data.ByteString.Lazy.Char8 as BL
format :: IO Formatter
format = do
ref <- newIORef []
return Formatter {
header = return (),
onResult = collectResult ref,
onFailure = outputError,
footer = finish ref
}
data Json1Output = Json1Output {
comments :: [PositionedComment]
}
instance ToJSON Json1Output where
toJSON result = object [
"comments" .= comments result
]
toEncoding result = pairs (
"comments" .= comments result
)
instance ToJSON Replacement where
toJSON replacement =
let start = repStartPos replacement
end = repEndPos replacement
str = repString replacement in
object [
"precedence" .= repPrecedence replacement,
"insertionPoint" .=
case repInsertionPoint replacement of
InsertBefore -> "beforeStart" :: String
InsertAfter -> "afterEnd",
"line" .= posLine start,
"column" .= posColumn start,
"endLine" .= posLine end,
"endColumn" .= posColumn end,
"replacement" .= str
]
instance ToJSON PositionedComment where
toJSON comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
object [
"file" .= posFile start,
"line" .= posLine start,
"endLine" .= posLine end,
"column" .= posColumn start,
"endColumn" .= posColumn end,
"level" .= severityText comment,
"code" .= cCode c,
"message" .= cMessage c,
"fix" .= pcFix comment
]
toEncoding comment =
let start = pcStartPos comment
end = pcEndPos comment
c = pcComment comment in
pairs (
"file" .= posFile start
<> "line" .= posLine start
<> "endLine" .= posLine end
<> "column" .= posColumn start
<> "endColumn" .= posColumn end
<> "level" .= severityText comment
<> "code" .= cCode c
<> "message" .= cMessage c
<> "fix" .= pcFix comment
)
instance ToJSON Fix where
toJSON fix = object [
"replacements" .= fixReplacements fix
]
outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
collectResult ref cr sys = mapM_ f groups
where
comments = crComments cr
groups = groupWith sourceFile comments
f :: [PositionedComment] -> IO ()
f group = do
let filename = sourceFile (head group)
result <- siReadFile sys filename
let contents = either (const "") id result
let comments' = makeNonVirtual comments contents
modifyIORef ref (\x -> comments' ++ x)
finish ref = do
list <- readIORef ref
BL.putStrLn $ encode $ Json1Output { comments = list }

View File

@@ -1,36 +0,0 @@
{-
Copyright 2019 Austin Voecks
This file is part of ShellCheck.
https://www.shellcheck.net
ShellCheck is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
ShellCheck is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-}
module ShellCheck.Formatter.Quiet (format) where
import ShellCheck.Interface
import ShellCheck.Formatter.Format
import Control.Monad
import Data.IORef
import System.Exit
format :: FormatterOptions -> IO Formatter
format options =
return Formatter {
header = return (),
footer = return (),
onFailure = \ _ _ -> exitFailure,
onResult = \ result _ -> unless (null $ crComments result) exitFailure
}

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -19,14 +19,10 @@
-} -}
module ShellCheck.Formatter.TTY (format) where module ShellCheck.Formatter.TTY (format) where
import ShellCheck.Fixer
import ShellCheck.Interface import ShellCheck.Interface
import ShellCheck.Formatter.Format import ShellCheck.Formatter.Format
import Control.Monad import Control.Monad
import Data.Array
import Data.Foldable
import Data.Ord
import Data.IORef import Data.IORef
import Data.List import Data.List
import Data.Maybe import Data.Maybe
@@ -38,8 +34,6 @@ wikiLink = "https://www.shellcheck.net/wiki/"
-- An arbitrary Ord thing to order warnings -- An arbitrary Ord thing to order warnings
type Ranking = (Char, Severity, Integer) type Ranking = (Char, Severity, Integer)
-- Ansi coloring function
type ColorFunc = (String -> String -> String)
format :: FormatterOptions -> IO Formatter format :: FormatterOptions -> IO Formatter
format options = do format options = do
@@ -57,7 +51,6 @@ colorForLevel level =
"warning" -> 33 -- yellow "warning" -> 33 -- yellow
"info" -> 32 -- green "info" -> 32 -- green
"style" -> 32 -- green "style" -> 32 -- green
"verbose" -> 32 -- green
"message" -> 1 -- bold "message" -> 1 -- bold
"source" -> 0 -- none "source" -> 0 -- none
_ -> 0 -- none _ -> 0 -- none
@@ -123,54 +116,72 @@ outputForFile color sys comments = do
let fileName = sourceFile (head comments) let fileName = sourceFile (head comments)
result <- (siReadFile sys) fileName result <- (siReadFile sys) fileName
let contents = either (const "") id result let contents = either (const "") id result
let fileLinesList = lines contents let fileLines = lines contents
let lineCount = length fileLinesList let lineCount = fromIntegral $ length fileLines
let fileLines = listArray (1, lineCount) fileLinesList
let groups = groupWith lineNo comments let groups = groupWith lineNo comments
mapM_ (\commentsForLine -> do mapM_ (\commentsForLine -> do
let lineNum = fromIntegral $ lineNo (head commentsForLine) let lineNum = lineNo (head commentsForLine)
let line = if lineNum < 1 || lineNum > lineCount let line = if lineNum < 1 || lineNum > lineCount
then "" then ""
else fileLines ! fromIntegral lineNum else fileLines !! fromIntegral (lineNum - 1)
putStrLn "" putStrLn ""
putStrLn $ color "message" $ putStrLn $ color "message" $
"In " ++ fileName ++" line " ++ show lineNum ++ ":" "In " ++ fileName ++" line " ++ show lineNum ++ ":"
putStrLn (color "source" line) putStrLn (color "source" line)
mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine mapM_ (\c -> putStrLn (color (severityText c) $ cuteIndent c)) commentsForLine
putStrLn "" putStrLn ""
showFixedString color commentsForLine (fromIntegral lineNum) fileLines showFixedString color comments lineNum line
) groups ) groups
-- Pick out only the lines necessary to show a fix in action hasApplicableFix lineNum comment = fromMaybe False $ do
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String) replacements <- fixReplacements <$> pcFix comment
sliceFile fix lines = guard $ all (\c -> onSameLine (repStartPos c) && onSameLine (repEndPos c)) replacements
(mapPositions adjust fix, sliceLines lines) return True
where where
(minLine, maxLine) = onSameLine pos = posLine pos == lineNum
foldl (\(mm, mx) pos -> ((min mm $ fromIntegral $ posLine pos), (max mx $ fromIntegral $ posLine pos)))
(maxBound, minBound) $
concatMap (\x -> [repStartPos x, repEndPos x]) $ fixReplacements fix
sliceLines :: Array Int String -> Array Int String
sliceLines = ixmap (1, maxLine - minLine + 1) (\x -> x + minLine - 1)
adjust pos =
pos {
posLine = posLine pos - (fromIntegral minLine) + 1
}
showFixedString :: ColorFunc -> [PositionedComment] -> Int -> Array Int String -> IO () -- FIXME: Work correctly with multiple replacements
showFixedString color comments lineNum fileLines = showFixedString color comments lineNum line =
let line = fileLines ! fromIntegral lineNum in case filter (hasApplicableFix lineNum) comments of
case mapMaybe pcFix comments of (first:_) -> do
[] -> return ()
fixes -> do
-- Folding automatically removes overlap
let mergedFix = fold fixes
-- We show the complete, associated fixes, whether or not it includes this
-- and/or other unrelated lines.
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
-- in the spirit of error prone -- in the spirit of error prone
putStrLn $ color "message" "Did you mean: " putStrLn $ color "message" "Did you mean: "
putStrLn $ unlines $ applyFix excerptFix excerpt putStrLn $ fixedString first line
putStrLn ""
_ -> return ()
-- need to do something smart about sorting by end index
fixedString :: PositionedComment -> String -> String
fixedString comment line =
case (pcFix comment) of
Nothing -> ""
Just rs ->
applyReplacement (fixReplacements rs) line 0
where
applyReplacement [] s _ = s
applyReplacement (rep:xs) s offset =
let replacementString = repString rep
start = (posColumn . repStartPos) rep
end = (posColumn . repEndPos) rep
z = doReplace start end s replacementString
len_r = (fromIntegral . length) replacementString in
applyReplacement xs z (offset + (end - start) + len_r)
-- FIXME: Work correctly with tabs
-- start and end comes from pos, which is 1 based
-- doReplace 0 0 "1234" "A" -> "A1234" -- technically not valid
-- doReplace 1 1 "1234" "A" -> "A1234"
-- doReplace 1 2 "1234" "A" -> "A234"
-- doReplace 3 3 "1234" "A" -> "12A34"
-- doReplace 4 4 "1234" "A" -> "123A4"
-- doReplace 5 5 "1234" "A" -> "1234A"
doReplace start end o r =
let si = fromIntegral (start-1)
ei = fromIntegral (end-1)
(x, xs) = splitAt si o
(y, z) = splitAt (ei - si) xs
in
x ++ r ++ z
cuteIndent :: PositionedComment -> String cuteIndent :: PositionedComment -> String
cuteIndent comment = cuteIndent comment =
@@ -186,9 +197,14 @@ cuteIndent comment =
code num = "SC" ++ show num code num = "SC" ++ show num
getColorFunc :: ColorOption -> IO ColorFunc
getColorFunc colorOption = do getColorFunc colorOption = do
useColor <- shouldOutputColor colorOption term <- hIsTerminalDevice stdout
let windows = "mingw" `isPrefixOf` os
let isUsableTty = term && not windows
let useColor = case colorOption of
ColorAlways -> True
ColorNever -> False
ColorAuto -> isUsableTty
return $ if useColor then colorComment else const id return $ if useColor then colorComment else const id
where where
colorComment level comment = colorComment level comment =

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -21,11 +21,11 @@
module ShellCheck.Interface module ShellCheck.Interface
( (
SystemInterface(..) SystemInterface(..)
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks) , CheckSpec(csFilename, csScript, csCheckSourced, csExcludedWarnings, csShellTypeOverride, csMinSeverity)
, CheckResult(crFilename, crComments) , CheckResult(crFilename, crComments)
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride) , ParseSpec(psFilename, psScript, psCheckSourced, psShellTypeOverride)
, ParseResult(prComments, prTokenPositions, prRoot) , ParseResult(prComments, prTokenPositions, prRoot)
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks) , AnalysisSpec(asScript, asShellType, asExecutionMode, asCheckSourced, asTokenPositions)
, AnalysisResult(arComments) , AnalysisResult(arComments)
, FormatterOptions(foColorOption, foWikiLinkCount) , FormatterOptions(foColorOption, foWikiLinkCount)
, Shell(Ksh, Sh, Bash, Dash) , Shell(Ksh, Sh, Bash, Dash)
@@ -46,43 +46,28 @@ module ShellCheck.Interface
, newPosition , newPosition
, newTokenComment , newTokenComment
, mockedSystemInterface , mockedSystemInterface
, mockRcFile
, newParseSpec , newParseSpec
, emptyCheckSpec , emptyCheckSpec
, newPositionedComment , newPositionedComment
, newComment , newComment
, Fix(fixReplacements) , Fix(fixReplacements)
, newFix , newFix
, InsertionPoint(InsertBefore, InsertAfter) , Replacement(repStartPos, repEndPos, repString)
, Replacement(repStartPos, repEndPos, repString, repPrecedence, repInsertionPoint)
, newReplacement , newReplacement
, CheckDescription(cdName, cdDescription, cdPositive, cdNegative)
, newCheckDescription
) where ) where
import ShellCheck.AST import ShellCheck.AST
import Control.DeepSeq import Control.DeepSeq
import Control.Monad.Identity import Control.Monad.Identity
import Data.List
import Data.Monoid import Data.Monoid
import Data.Ord
import Data.Semigroup
import GHC.Generics (Generic) import GHC.Generics (Generic)
import qualified Data.Map as Map import qualified Data.Map as Map
data SystemInterface m = SystemInterface { newtype SystemInterface m = SystemInterface {
-- Read a file by filename, or return an error -- Read a file by filename, or return an error
siReadFile :: String -> m (Either ErrorMessage String), siReadFile :: String -> m (Either ErrorMessage String)
-- Given:
-- the current script,
-- a list of source-path annotations in effect,
-- and a sourced file,
-- find the sourced file
siFindSource :: String -> [String] -> String -> m FilePath,
-- Get the configuration file (name, contents) for a filename
siGetConfig :: String -> m (Maybe (FilePath, String))
} }
-- ShellCheck input and output -- ShellCheck input and output
@@ -90,12 +75,9 @@ data CheckSpec = CheckSpec {
csFilename :: String, csFilename :: String,
csScript :: String, csScript :: String,
csCheckSourced :: Bool, csCheckSourced :: Bool,
csIgnoreRC :: Bool,
csExcludedWarnings :: [Integer], csExcludedWarnings :: [Integer],
csIncludedWarnings :: Maybe [Integer],
csShellTypeOverride :: Maybe Shell, csShellTypeOverride :: Maybe Shell,
csMinSeverity :: Severity, csMinSeverity :: Severity
csOptionalChecks :: [String]
} deriving (Show, Eq) } deriving (Show, Eq)
data CheckResult = CheckResult { data CheckResult = CheckResult {
@@ -114,12 +96,9 @@ emptyCheckSpec = CheckSpec {
csFilename = "", csFilename = "",
csScript = "", csScript = "",
csCheckSourced = False, csCheckSourced = False,
csIgnoreRC = False,
csExcludedWarnings = [], csExcludedWarnings = [],
csIncludedWarnings = Nothing,
csShellTypeOverride = Nothing, csShellTypeOverride = Nothing,
csMinSeverity = StyleC, csMinSeverity = StyleC
csOptionalChecks = []
} }
newParseSpec :: ParseSpec newParseSpec :: ParseSpec
@@ -127,7 +106,6 @@ newParseSpec = ParseSpec {
psFilename = "", psFilename = "",
psScript = "", psScript = "",
psCheckSourced = False, psCheckSourced = False,
psIgnoreRC = False,
psShellTypeOverride = Nothing psShellTypeOverride = Nothing
} }
@@ -136,7 +114,6 @@ data ParseSpec = ParseSpec {
psFilename :: String, psFilename :: String,
psScript :: String, psScript :: String,
psCheckSourced :: Bool, psCheckSourced :: Bool,
psIgnoreRC :: Bool,
psShellTypeOverride :: Maybe Shell psShellTypeOverride :: Maybe Shell
} deriving (Show, Eq) } deriving (Show, Eq)
@@ -157,20 +134,16 @@ newParseResult = ParseResult {
data AnalysisSpec = AnalysisSpec { data AnalysisSpec = AnalysisSpec {
asScript :: Token, asScript :: Token,
asShellType :: Maybe Shell, asShellType :: Maybe Shell,
asFallbackShell :: Maybe Shell,
asExecutionMode :: ExecutionMode, asExecutionMode :: ExecutionMode,
asCheckSourced :: Bool, asCheckSourced :: Bool,
asOptionalChecks :: [String],
asTokenPositions :: Map.Map Id (Position, Position) asTokenPositions :: Map.Map Id (Position, Position)
} }
newAnalysisSpec token = AnalysisSpec { newAnalysisSpec token = AnalysisSpec {
asScript = token, asScript = token,
asShellType = Nothing, asShellType = Nothing,
asFallbackShell = Nothing,
asExecutionMode = Executed, asExecutionMode = Executed,
asCheckSourced = False, asCheckSourced = False,
asOptionalChecks = [],
asTokenPositions = Map.empty asTokenPositions = Map.empty
} }
@@ -193,19 +166,6 @@ newFormatterOptions = FormatterOptions {
foWikiLinkCount = 3 foWikiLinkCount = 3
} }
data CheckDescription = CheckDescription {
cdName :: String,
cdDescription :: String,
cdPositive :: String,
cdNegative :: String
}
newCheckDescription = CheckDescription {
cdName = "",
cdDescription = "",
cdPositive = "",
cdNegative = ""
}
-- Supporting data types -- Supporting data types
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq) data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
@@ -220,7 +180,7 @@ data Position = Position {
posFile :: String, -- Filename posFile :: String, -- Filename
posLine :: Integer, -- 1 based source line posLine :: Integer, -- 1 based source line
posColumn :: Integer -- 1 based source column, where tabs are 8 posColumn :: Integer -- 1 based source column, where tabs are 8
} deriving (Show, Eq, Generic, NFData, Ord) } deriving (Show, Eq, Generic, NFData)
newPosition :: Position newPosition :: Position
newPosition = Position { newPosition = Position {
@@ -246,25 +206,13 @@ newComment = Comment {
data Replacement = Replacement { data Replacement = Replacement {
repStartPos :: Position, repStartPos :: Position,
repEndPos :: Position, repEndPos :: Position,
repString :: String, repString :: String
-- Order in which the replacements should happen: highest precedence first.
repPrecedence :: Int,
-- Whether to insert immediately before or immediately after the specified region.
repInsertionPoint :: InsertionPoint
} deriving (Show, Eq, Generic, NFData) } deriving (Show, Eq, Generic, NFData)
data InsertionPoint = InsertBefore | InsertAfter
deriving (Show, Eq, Generic, NFData)
instance Ord Replacement where
compare r1 r2 = (repStartPos r1) `compare` (repStartPos r2)
newReplacement = Replacement { newReplacement = Replacement {
repStartPos = newPosition, repStartPos = newPosition,
repEndPos = newPosition, repEndPos = newPosition,
repString = "", repString = ""
repPrecedence = 1,
repInsertionPoint = InsertAfter
} }
data Fix = Fix { data Fix = Fix {
@@ -311,18 +259,11 @@ data ColorOption =
-- For testing -- For testing
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
mockedSystemInterface files = SystemInterface { mockedSystemInterface files = SystemInterface {
siReadFile = rf, siReadFile = rf
siFindSource = fs,
siGetConfig = const $ return Nothing
} }
where where
rf file = rf file =
case filter ((== file) . fst) files of case filter ((== file) . fst) files of
[] -> return $ Left "File not included in mock." [] -> return $ Left "File not included in mock."
[(_, contents)] -> return $ Right contents [(_, contents)] -> return $ Right contents
fs _ _ file = return file
mockRcFile rcfile mock = mock {
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,5 @@
{- {-
Copyright 2012-2019 Vidar Holen Copyright 2012-2015 Vidar Holen
This file is part of ShellCheck. This file is part of ShellCheck.
https://www.shellcheck.net https://www.shellcheck.net
@@ -30,7 +30,7 @@ import Text.Regex.TDFA
-- Precompile the regex -- Precompile the regex
mkRegex :: String -> Regex mkRegex :: String -> Regex
mkRegex str = mkRegex str =
let make :: String -> Regex let make :: RegexMaker Regex CompOption ExecOption String => String -> Regex
make = makeRegex make = makeRegex
in in
make str make str

View File

@@ -1,35 +1,3 @@
# This file was automatically generated by stack init resolver: lts-12.9
# For more information, see: https://docs.haskellstack.org/en/stable/yaml_configuration/
# Specifies the GHC version and set of packages available (e.g., lts-3.5, nightly-2015-09-21, ghc-7.10.2)
resolver: lts-13.26
# Local packages, usually specified by relative directory name
packages: packages:
- '.' - '.'
# Packages to be pulled from upstream that are not in the resolver (e.g., acme-missiles-0.3)
extra-deps: []
# Override default flag values for local packages and extra-deps
flags: {}
# Extra package databases containing global packages
extra-package-dbs: []
# Control whether we use the GHC we find on the path
# system-ghc: true
# Require a specific version of stack, using version ranges
# require-stack-version: -any # Default
# require-stack-version: >= 1.0.0
# Override the architecture used by stack, especially useful on Windows
# arch: i386
# arch: x86_64
# Extra directories used by stack for building
# extra-include-dirs: [/path/to/dir]
# extra-lib-dirs: [/path/to/dir]
# Allow a newer minor version of GHC than the snapshot specifies
# compiler-check: newer-minor

View File

@@ -1,78 +1,2 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# This file strips all unit tests from ShellCheck, removing # This file was deprecated by the doctest build.
# the dependency on QuickCheck and Template Haskell and
# reduces the binary size considerably.
set -o pipefail
sponge() {
local data
data="$(cat)"
printf '%s\n' "$data" > "$1"
}
modify() {
if ! "${@:2}" < "$1" | sponge "$1"
then
{
printf 'Failed to modify %s: ' "$1"
printf '%q ' "${@:2}"
printf '\n'
} >&2
exit 1
fi
}
detestify() {
printf '%s\n' '-- AUTOGENERATED from ShellCheck by striptests. Do not modify.'
awk '
BEGIN {
state = 0;
}
/LANGUAGE TemplateHaskell/ { next; }
/^import.*Test\./ { next; }
/^module/ {
sub(/,[^,)]*runTests/, "");
}
# Delete tests
/^prop_/ { state = 1; next; }
# ..and any blank lines following them.
state == 1 && /^ / { next; }
# Template Haskell marker
/^return / {
exit;
}
{ state = 0; print; }
'
}
if [[ ! -e 'ShellCheck.cabal' ]]
then
echo "Run me from the ShellCheck directory." >&2
exit 1
fi
if [[ -d '.git' ]] && ! git diff --exit-code > /dev/null 2>&1
then
echo "You have local changes! These may be overwritten." >&2
exit 2
fi
modify 'ShellCheck.cabal' sed -e '
/QuickCheck/d
/^test-suite/{ s/.*//; q; }
'
find . -name '.git' -prune -o -type f -name '*.hs' -print |
while IFS= read -r file
do
modify "$file" detestify
done

View File

@@ -1,75 +0,0 @@
#!/usr/bin/env bash
failed=0
fail() {
echo "$(tput setaf 1)$*$(tput sgr0)"
failed=1
}
if git diff | grep -q ""
then
fail "There are uncommited changes"
fi
current=$(git tag --points-at)
if [[ -z "$current" ]]
then
fail "No git tag on the current commit"
echo "Create one with: git tag -a v0.0.0"
fi
if [[ "$current" != v* ]]
then
fail "Bad tag format: expected v0.0.0"
fi
if [[ "$(git cat-file -t "$current")" != "tag" ]]
then
fail "Current tag is not annotated (required for Snap)."
fi
if [[ "$(git tag --points-at master)" != "$current" ]]
then
fail "You are not on master"
fi
version=${current#v}
if ! grep "Version:" ShellCheck.cabal | grep -qFw "$version"
then
fail "The cabal file does not match tag version $version"
fi
if ! grep -qF "## $current" CHANGELOG.md
then
fail "CHANGELOG.md does not contain '## $current'"
fi
if [[ $(git log -1 --pretty=%B) != "Stable version "* ]]
then
fail "Expected git log message to be 'Stable version ...'"
fi
i=1 j=1
cat << EOF
Manual Checklist
$((i++)). Make sure none of the automated checks above failed
$((i++)). Make sure Travis build currently passes: https://travis-ci.org/koalaman/shellcheck
$((i++)). Make sure SnapCraft build currently works: https://build.snapcraft.io/user/koalaman
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
$((i++)). Format and read over the manual for bad formatting and outdated info.
$((i++)). Make sure the Hackage package builds, so that all files are
Release Steps
$((j++)). \`cabal sdist\` to generate a Hackage package
$((j++)). \`git push --follow-tags\` to push commit
$((j++)). Wait for Travis to build
$((j++)). Verify release:
a. Check that the new versions are uploaded: https://shellcheck.storage.googleapis.com/index.html
b. Check that the docker images have version tags: https://hub.docker.com/u/koalaman
$((j++)). If no disaster, upload to Hackage: http://hackage.haskell.org/upload
$((j++)). Push a new commit that updates CHANGELOG.md
EOF
exit "$failed"

View File

@@ -61,11 +61,11 @@ done << EOF
debian:stable apt-get update && apt-get install -y cabal-install debian:stable apt-get update && apt-get install -y cabal-install
debian:testing apt-get update && apt-get install -y cabal-install debian:testing apt-get update && apt-get install -y cabal-install
ubuntu:latest apt-get update && apt-get install -y cabal-install ubuntu:latest apt-get update && apt-get install -y cabal-install
opensuse/leap:latest zypper install -y cabal-install ghc opensuse:latest zypper install -y cabal-install ghc
# Other Ubuntu versions we want to support # Older Ubuntu versions we want to support
ubuntu:19.04 apt-get update && apt-get install -y cabal-install ubuntu:18.04 apt-get update && apt-get install -y cabal-install
ubuntu:18.10 apt-get update && apt-get install -y cabal-install ubuntu:17.10 apt-get update && apt-get install -y cabal-install
# Misc Haskell including current and latest Stack build # Misc Haskell including current and latest Stack build
ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest ubuntu:18.10 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
@@ -74,7 +74,7 @@ haskell:latest true
# Known to currently fail # Known to currently fail
centos:latest yum install -y epel-release && yum install -y cabal-install centos:latest yum install -y epel-release && yum install -y cabal-install
fedora:latest dnf install -y cabal-install fedora:latest dnf install -y cabal-install
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel base/archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
EOF EOF
exit "$final" exit "$final"

12
test/doctests.hs Normal file
View File

@@ -0,0 +1,12 @@
module Main where
import Build_doctests (flags, pkgs, module_sources)
import Data.Foldable (traverse_)
import Test.DocTest (doctest)
main :: IO ()
main = do
traverse_ putStrLn args
doctest args
where
args = flags ++ pkgs ++ module_sources

View File

@@ -1,30 +0,0 @@
module Main where
import Control.Monad
import System.Exit
import qualified ShellCheck.Analytics
import qualified ShellCheck.AnalyzerLib
import qualified ShellCheck.Checker
import qualified ShellCheck.Checks.Commands
import qualified ShellCheck.Checks.Custom
import qualified ShellCheck.Checks.ShellSupport
import qualified ShellCheck.Fixer
import qualified ShellCheck.Formatter.Diff
import qualified ShellCheck.Parser
main = do
putStrLn "Running ShellCheck tests..."
results <- sequence [
ShellCheck.Analytics.runTests
,ShellCheck.AnalyzerLib.runTests
,ShellCheck.Checker.runTests
,ShellCheck.Checks.Commands.runTests
,ShellCheck.Checks.Custom.runTests
,ShellCheck.Checks.ShellSupport.runTests
,ShellCheck.Fixer.runTests
,ShellCheck.Formatter.Diff.runTests
,ShellCheck.Parser.runTests
]
if and results
then exitSuccess
else exitFailure