mirror of
https://github.com/koalaman/shellcheck
synced 2025-07-06 13:01:39 -07:00
Compare commits
No commits in common. "master" and "v0.7.2" have entirely different histories.
69 changed files with 1503 additions and 7654 deletions
4
.github/ISSUE_TEMPLATE.md
vendored
4
.github/ISSUE_TEMPLATE.md
vendored
|
@ -1,6 +1,6 @@
|
|||
#### For bugs
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- Rule Id (if any, e.g. SC1000):
|
||||
- My shellcheck version (`shellcheck --version` or "online"):
|
||||
- [ ] The rule's wiki page does not already cover this (e.g. https://shellcheck.net/wiki/SC2086)
|
||||
- [ ] I tried on https://www.shellcheck.net/ and verified that this is still a problem on the latest commit
|
||||
|
||||
|
|
7
.github/dependabot.yml
vendored
7
.github/dependabot.yml
vendored
|
@ -1,7 +0,0 @@
|
|||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
87
.github/workflows/build.yml
vendored
87
.github/workflows/build.yml
vendored
|
@ -15,69 +15,43 @@ jobs:
|
|||
sudo apt-get install cabal-install
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Deduce tags
|
||||
run: |
|
||||
mkdir source
|
||||
echo "latest" > source/tags
|
||||
if tag=$(git describe --exact-match --tags)
|
||||
then
|
||||
echo "stable" >> source/tags
|
||||
echo "$tag" >> source/tags
|
||||
fi
|
||||
cat source/tags
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Package Source
|
||||
run: |
|
||||
grep "stable" source/tags || ./setgitversion
|
||||
mkdir source
|
||||
cabal sdist
|
||||
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||
|
||||
- name: Deduce tags
|
||||
run: |
|
||||
exec > source/tags
|
||||
echo "latest"
|
||||
if tag=$(git describe --exact-match --tags)
|
||||
then
|
||||
echo "stable"
|
||||
echo "$tag"
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: source
|
||||
path: source/
|
||||
|
||||
run_tests:
|
||||
name: Run tests
|
||||
needs: package_source
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
sudo apt-get update && sudo apt-get install ghc cabal-install
|
||||
cabal update
|
||||
|
||||
- name: Unpack source
|
||||
run: |
|
||||
cd source
|
||||
tar xvf source.tar.gz --strip-components=1
|
||||
|
||||
- name: Build and run tests
|
||||
run: |
|
||||
cd source
|
||||
cabal test
|
||||
|
||||
build_source:
|
||||
name: Build
|
||||
name: Build Source Code
|
||||
needs: package_source
|
||||
strategy:
|
||||
matrix:
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, linux.riscv64, darwin.x86_64, darwin.aarch64, windows.x86_64]
|
||||
build: [linux.x86_64, linux.aarch64, linux.armv6hf, darwin.x86_64, windows.x86_64]
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- name: Build source
|
||||
run: |
|
||||
|
@ -86,9 +60,9 @@ jobs:
|
|||
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{matrix.build}}.bin
|
||||
name: bin
|
||||
path: bin/
|
||||
|
||||
package_binary:
|
||||
|
@ -97,25 +71,25 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- name: Work around GitHub permissions bug
|
||||
run: chmod +x *.bin/*/shellcheck*
|
||||
run: chmod +x bin/*/shellcheck*
|
||||
|
||||
- name: Package binaries
|
||||
run: |
|
||||
export TAGS="$(cat source/tags)"
|
||||
mkdir -p deploy
|
||||
cp -r *.bin/* deploy
|
||||
cp -r bin/* deploy
|
||||
cd deploy
|
||||
../.prepare_deploy
|
||||
rm -rf */ README* LICENSE*
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: deploy
|
||||
path: deploy/
|
||||
|
@ -126,16 +100,11 @@ jobs:
|
|||
runs-on: ubuntu-latest
|
||||
environment: Deploy
|
||||
steps:
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install hub
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v2
|
||||
|
||||
- name: Upload to GitHub
|
||||
env:
|
||||
|
@ -144,10 +113,6 @@ jobs:
|
|||
export TAGS="$(cat source/tags)"
|
||||
./.github_deploy
|
||||
|
||||
- name: Waiting for GitHub to replicate uploaded releases
|
||||
run: |
|
||||
sleep 300
|
||||
|
||||
- name: Upload to Docker Hub
|
||||
env:
|
||||
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
|
||||
|
|
|
@ -26,3 +26,4 @@ do
|
|||
done
|
||||
gh release upload "$tag" "${files[@]}" --clobber || exit 1
|
||||
done
|
||||
|
||||
|
|
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -20,4 +20,3 @@ cabal.config
|
|||
/parts/
|
||||
/prime/
|
||||
*.snap
|
||||
/dist-newstyle/
|
||||
|
|
|
@ -3,10 +3,28 @@
|
|||
# binaries previously built and deployed to GitHub.
|
||||
|
||||
function multi_arch_docker::install_docker_buildx() {
|
||||
# Install up-to-date version of docker, with buildx support.
|
||||
local -r docker_apt_repo='https://download.docker.com/linux/ubuntu'
|
||||
curl -fsSL "${docker_apt_repo}/gpg" | sudo apt-key add -
|
||||
local -r os="$(lsb_release -cs)"
|
||||
sudo add-apt-repository "deb [arch=amd64] $docker_apt_repo $os stable"
|
||||
sudo apt-get update
|
||||
sudo apt-get -y -o Dpkg::Options::="--force-confnew" install docker-ce
|
||||
|
||||
# Enable docker daemon experimental support (for 'pull --platform').
|
||||
local -r config='/etc/docker/daemon.json'
|
||||
if [[ -e "$config" ]]; then
|
||||
sudo sed -i -e 's/{/{ "experimental": true, /' "$config"
|
||||
else
|
||||
echo '{ "experimental": true }' | sudo tee "$config"
|
||||
fi
|
||||
sudo systemctl restart docker
|
||||
|
||||
# Install QEMU multi-architecture support for docker buildx.
|
||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||
|
||||
# Instantiate docker buildx builder with multi-architecture support.
|
||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
||||
docker buildx create --name mybuilder
|
||||
docker buildx use mybuilder
|
||||
# Start up buildx and verify that all is OK.
|
||||
|
@ -80,7 +98,6 @@ function multi_arch_docker::main() {
|
|||
export DOCKER_PLATFORMS='linux/amd64'
|
||||
DOCKER_PLATFORMS+=' linux/arm64'
|
||||
DOCKER_PLATFORMS+=' linux/arm/v6'
|
||||
DOCKER_PLATFORMS+=' linux/riscv64'
|
||||
|
||||
multi_arch_docker::install_docker_buildx
|
||||
multi_arch_docker::login_to_docker_hub
|
||||
|
|
14
.snapsquid.conf
Normal file
14
.snapsquid.conf
Normal file
|
@ -0,0 +1,14 @@
|
|||
# In 2015, cabal-install had a http bug triggered when proxies didn't keep
|
||||
# the connection open. This version made it into Ubuntu Xenial as used by
|
||||
# Snapcraft. In June 2018, Snapcraft's proxy started triggering this bug.
|
||||
#
|
||||
# https://bugs.launchpad.net/launchpad-buildd/+bug/1797809
|
||||
#
|
||||
# Workaround: add more proxy
|
||||
|
||||
visible_hostname localhost
|
||||
http_port 8888
|
||||
cache_peer 10.10.10.1 parent 8222 0 no-query default
|
||||
cache_peer_domain localhost !.internal
|
||||
http_access allow all
|
||||
|
111
CHANGELOG.md
111
CHANGELOG.md
|
@ -1,108 +1,3 @@
|
|||
## Git
|
||||
### Added
|
||||
- SC2327/SC2328: Warn about capturing the output of redirected commands.
|
||||
- SC2329: Warn when (non-escaping) functions are never invoked.
|
||||
- SC2330: Warn about unsupported glob matches with [[ .. ]] in BusyBox.
|
||||
- SC2331: Suggest using standard -e instead of unary -a in tests.
|
||||
- SC2332: Warn about `[ ! -o opt ]` being unconditionally true in Bash.
|
||||
- SC3062: Warn about bashism `[ -o opt ]`.
|
||||
- Precompiled binaries for Linux riscv64 (linux.riscv64)
|
||||
### Changed
|
||||
- SC2002 about Useless Use Of Cat is now disabled by default. It can be
|
||||
re-enabled with `--enable=useless-use-of-cat` or equivalent directive.
|
||||
- SC2015 about `A && B || C` no longer triggers when B is a test command.
|
||||
- SC3012: Do not warn about `\<` and `\>` in test/[] as specified in POSIX.1-2024
|
||||
### Fixed
|
||||
- SC2218 about function use-before-define is now more accurate.
|
||||
- SC2317 about unreachable commands is now less spammy for nested ones.
|
||||
- SC2292, optional suggestion for [[ ]], now triggers for Busybox.
|
||||
|
||||
### Removed
|
||||
- SC3013: removed since the operators `-ot/-nt/-ef` are specified in POSIX.1-2024
|
||||
|
||||
## v0.10.0 - 2024-03-07
|
||||
### Added
|
||||
- Precompiled binaries for macOS ARM64 (darwin.aarch64)
|
||||
- Added support for busybox sh
|
||||
- Added flag --rcfile to specify an rc file by name.
|
||||
- Added `extended-analysis=true` directive to enable/disable dataflow analysis
|
||||
(with a corresponding --extended-analysis flag).
|
||||
- SC2324: Warn when x+=1 appends instead of increments
|
||||
- SC2325: Warn about multiple `!`s in dash/sh.
|
||||
- SC2326: Warn about `foo | ! bar` in bash/dash/sh.
|
||||
- SC3012: Warn about lexicographic-compare bashism in test like in [ ]
|
||||
- SC3013: Warn bashism `test _ -op/-nt/-ef _` like in [ ]
|
||||
- SC3014: Warn bashism `test _ == _` like in [ ]
|
||||
- SC3015: Warn bashism `test _ =~ _` like in [ ]
|
||||
- SC3016: Warn bashism `test -v _` like in [ ]
|
||||
- SC3017: Warn bashism `test -a _` like in [ ]
|
||||
|
||||
### Fixed
|
||||
- source statements with here docs now work correctly
|
||||
- "(Array.!): undefined array element" error should no longer occur
|
||||
|
||||
|
||||
## v0.9.0 - 2022-12-12
|
||||
### Added
|
||||
- SC2316: Warn about 'local readonly foo' and similar (thanks, patrickxia!)
|
||||
- SC2317: Warn about unreachable commands
|
||||
- SC2318: Warn about backreferences in 'declare x=1 y=$x'
|
||||
- SC2319/SC2320: Warn when $? refers to echo/printf/[ ]/[[ ]]/test
|
||||
- SC2321: Suggest removing $((..)) in array[$((idx))]=val
|
||||
- SC2322: Suggest collapsing double parentheses in arithmetic contexts
|
||||
- SC2323: Suggest removing wrapping parentheses in a[(x+1)]=val
|
||||
|
||||
### Fixed
|
||||
- SC2086: Now uses DFA to make more accurate predictions about values
|
||||
- SC2086: No longer warns about values declared as integer with declare -i
|
||||
|
||||
### Changed
|
||||
- ShellCheck now has a Data Flow Analysis engine to make smarter decisions
|
||||
based on control flow rather than just syntax. Existing checks will
|
||||
gradually start using it, which may cause them to trigger differently
|
||||
(but more accurately).
|
||||
- Values in directives/shellcheckrc can now be quoted with '' or ""
|
||||
|
||||
|
||||
## v0.8.0 - 2021-11-06
|
||||
### Added
|
||||
- `disable=all` now conveniently disables all warnings
|
||||
- `external-sources=true` directive can be added to .shellcheckrc to make
|
||||
shellcheck behave as if `-x` was specified.
|
||||
- Optional `check-extra-masked-returns` for pointing out commands with
|
||||
suppressed exit codes (SC2312).
|
||||
- Optional `require-double-brackets` for recommending \[\[ ]] (SC2292).
|
||||
- SC2286-SC2288: Warn when command name ends in a symbol like `/.)'"`
|
||||
- SC2289: Warn when command name contains tabs or linefeeds
|
||||
- SC2291: Warn about repeated unquoted spaces between words in echo
|
||||
- SC2292: Suggest [[ over [ in Bash/Ksh scripts (optional)
|
||||
- SC2293/SC2294: Warn when calling `eval` with arrays
|
||||
- SC2295: Warn about "${x#$y}" treating $y as a pattern when not quoted
|
||||
- SC2296-SC2301: Improved warnings for bad parameter expansions
|
||||
- SC2302/SC2303: Warn about loops over array values when using them as keys
|
||||
- SC2304-SC2306: Warn about unquoted globs in expr arguments
|
||||
- SC2307: Warn about insufficient number of arguments to expr
|
||||
- SC2308: Suggest other approaches for non-standard expr extensions
|
||||
- SC2313: Warn about `read` with unquoted, array indexed variable
|
||||
|
||||
### Fixed
|
||||
- SC2102 about repetitions in ranges no longer triggers on [[ -v arr[xx] ]]
|
||||
- SC2155 now recognizes `typeset` and local read-only `declare` statements
|
||||
- SC2181 now tries to avoid triggering for error handling functions
|
||||
- SC2290: Warn about misused = in declare & co, which were not caught by SC2270+
|
||||
- The flag --color=auto no longer outputs color when TERM is "dumb" or unset
|
||||
|
||||
### Changed
|
||||
- SC2048: Warning about $\* now also applies to ${array[\*]}
|
||||
- SC2181 now only triggers on single condition tests like `[ $? = 0 ]`.
|
||||
- Quote warnings are now emitted for declaration utilities in sh
|
||||
- Leading `_` can now be used to suppress warnings about unused variables
|
||||
- TTY output now includes warning level in text as well as color
|
||||
|
||||
### Removed
|
||||
- SC1004: Literal backslash+linefeed in '' was found to be usually correct
|
||||
|
||||
|
||||
## v0.7.2 - 2021-04-19
|
||||
### Added
|
||||
- `disable` directives can now be a range, e.g. `disable=SC3000-SC4000`
|
||||
|
@ -300,7 +195,7 @@
|
|||
- SC2185: Suggest explicitly adding path for `find`
|
||||
- SC2184: Warn about unsetting globs (e.g. `unset foo[1]`)
|
||||
- SC2183: Warn about `printf` with more formatters than variables
|
||||
- SC2182: Warn about ignored arguments with `printf`
|
||||
- SC2182: Warn about ignored arguments with `printf`
|
||||
- SC2181: Suggest using command directly instead of `if [ $? -eq 0 ]`
|
||||
- SC1106: Warn when using `test` operators in `(( 1 -eq 2 ))`
|
||||
|
||||
|
@ -471,7 +366,7 @@
|
|||
### Added
|
||||
- SC2121: Warn about trying to `set` variables, e.g. `set var = value`
|
||||
- SC2120/SC2119: Warn when a function uses `$1..` if none are ever passed
|
||||
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
|
||||
- SC2117: Warn when using `su` in interactive mode, e.g. `su foo; whoami`
|
||||
- SC2116: Detect useless use of echo, e.g. `for i in $(echo $var)`
|
||||
- SC2115/SC2114: Detect some catastrophic `rm -r "$empty/"` mistakes
|
||||
- SC1081: Warn when capitalizing keywords like `While`
|
||||
|
@ -522,7 +417,7 @@
|
|||
|
||||
### Removed
|
||||
- Suggestions about using parameter expansion over basename
|
||||
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
|
||||
- The `jsoncheck` binary. Use `shellcheck -f json` instead.
|
||||
|
||||
|
||||
## v0.2.0 - 2013-10-27
|
||||
|
|
12
LICENSE
12
LICENSE
|
@ -1,3 +1,13 @@
|
|||
Employer mandated disclaimer:
|
||||
|
||||
I am providing code in the repository to you under an open source license.
|
||||
Because this is my personal repository, the license you receive to my code is
|
||||
from me and other individual contributors, and not my employer (Facebook).
|
||||
|
||||
- Vidar "koala_man" Holen
|
||||
|
||||
----
|
||||
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
|
@ -671,4 +681,4 @@ into proprietary programs. If your program is a subroutine library, you
|
|||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||
<https://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
|
|
63
README.md
63
README.md
|
@ -1,5 +1,4 @@
|
|||
[](https://github.com/koalaman/shellcheck/actions/workflows/build.yml)
|
||||
|
||||
[](https://travis-ci.org/koalaman/shellcheck)
|
||||
|
||||
# ShellCheck - A shell script static analysis tool
|
||||
|
||||
|
@ -77,7 +76,7 @@ You can see ShellCheck suggestions directly in a variety of editors.
|
|||
|
||||
* Sublime, through [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter-shellcheck).
|
||||
|
||||
* Pulsar Edit (former Atom), through [linter-shellcheck-pulsar](https://github.com/pulsar-cooperative/linter-shellcheck-pulsar).
|
||||
* Atom, through [Linter](https://github.com/AtomLinter/linter-shellcheck).
|
||||
|
||||
* VSCode, through [vscode-shellcheck](https://github.com/timonwong/vscode-shellcheck).
|
||||
|
||||
|
@ -110,11 +109,12 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
|
|||
* [Codacy](https://www.codacy.com/)
|
||||
* [Code Climate](https://codeclimate.com/)
|
||||
* [Code Factor](https://www.codefactor.io/)
|
||||
* [Codety](https://www.codety.io/) via the [Codety Scanner](https://github.com/codetyio/codety-scanner)
|
||||
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
||||
* [Github](https://github.com/features/actions) (only Linux)
|
||||
* [Trunk Check](https://trunk.io/products/check) (universal linter; [allows you to explicitly version your shellcheck install](https://github.com/trunk-io/plugins/blob/bcbb361dcdbe4619af51ea7db474d7fb87540d20/.trunk/trunk.yaml#L32)) via the [shellcheck plugin](https://github.com/trunk-io/plugins/blob/main/linters/shellcheck/plugin.yaml)
|
||||
* [CodeRabbit](https://coderabbit.ai/)
|
||||
|
||||
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)),
|
||||
|
@ -143,7 +143,7 @@ On systems with Stack (installs to `~/.local/bin`):
|
|||
|
||||
On Debian based distros:
|
||||
|
||||
sudo apt install shellcheck
|
||||
apt-get install shellcheck
|
||||
|
||||
On Arch Linux based distros:
|
||||
|
||||
|
@ -157,8 +157,8 @@ On Gentoo based distros:
|
|||
|
||||
On EPEL based distros:
|
||||
|
||||
sudo yum -y install epel-release
|
||||
sudo yum install ShellCheck
|
||||
yum -y install epel-release
|
||||
yum install ShellCheck
|
||||
|
||||
On Fedora based distros:
|
||||
|
||||
|
@ -196,12 +196,6 @@ On Windows (via [chocolatey](https://chocolatey.org/packages/shellcheck)):
|
|||
C:\> choco install shellcheck
|
||||
```
|
||||
|
||||
Or Windows (via [winget](https://github.com/microsoft/winget-pkgs)):
|
||||
|
||||
```cmd
|
||||
C:\> winget install --id koalaman.shellcheck
|
||||
```
|
||||
|
||||
Or Windows (via [scoop](http://scoop.sh)):
|
||||
|
||||
```cmd
|
||||
|
@ -230,26 +224,17 @@ Using the [nix package manager](https://nixos.org/nix):
|
|||
nix-env -iA nixpkgs.shellcheck
|
||||
```
|
||||
|
||||
Using the [Flox package manager](https://flox.dev/)
|
||||
```sh
|
||||
flox install shellcheck
|
||||
```
|
||||
|
||||
Alternatively, you can download pre-compiled binaries for the latest release here:
|
||||
|
||||
* [Linux, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.x86_64.tar.xz) (statically linked)
|
||||
* [Linux, armv6hf](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.armv6hf.tar.xz), i.e. Raspberry Pi (statically linked)
|
||||
* [Linux, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.linux.aarch64.tar.xz) aka ARM64 (statically linked)
|
||||
* [macOS, aarch64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.aarch64.tar.xz)
|
||||
* [macOS, x86_64](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.darwin.x86_64.tar.xz)
|
||||
* [Windows, x86](https://github.com/koalaman/shellcheck/releases/download/stable/shellcheck-stable.zip)
|
||||
|
||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
||||
(including the [latest](https://github.com/koalaman/shellcheck/releases/tag/latest) meta-release for daily git builds).
|
||||
|
||||
There are currently no official binaries for Apple Silicon, but third party builds are available via
|
||||
[ShellCheck for Visual Studio Code](https://github.com/vscode-shellcheck/shellcheck-binaries/releases).
|
||||
|
||||
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||
|
||||
```console
|
||||
|
@ -257,19 +242,6 @@ pandoc -s -f markdown-smart -t man shellcheck.1.md -o shellcheck.1
|
|||
sudo mv shellcheck.1 /usr/share/man/man1
|
||||
```
|
||||
|
||||
### pre-commit
|
||||
|
||||
To run ShellCheck via [pre-commit](https://pre-commit.com/), add the hook to your `.pre-commit-config.yaml`:
|
||||
|
||||
```
|
||||
repos:
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.7.2
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
# args: ["--severity=warning"] # Optionally only show errors and warnings
|
||||
```
|
||||
|
||||
### Travis CI
|
||||
|
||||
Travis CI has now integrated ShellCheck by default, so you don't need to manually install it.
|
||||
|
@ -317,6 +289,10 @@ Verify that `cabal` is installed and update its dependency list with
|
|||
|
||||
$ cabal install
|
||||
|
||||
Or if you intend to run the tests:
|
||||
|
||||
$ cabal install --enable-tests
|
||||
|
||||
This will compile ShellCheck and install it to your `~/.cabal/bin` directory.
|
||||
|
||||
Add this directory to your `PATH` (for bash, add this to your `~/.bashrc`):
|
||||
|
@ -372,7 +348,6 @@ echo 'Don't forget to restart!' # Singlequote closed by apostrophe
|
|||
echo 'Don\'t try this at home' # Attempting to escape ' in ''
|
||||
echo 'Path is $PATH' # Variables in single quotes
|
||||
trap "echo Took ${SECONDS}s" 0 # Prematurely expanded trap
|
||||
unset var[i] # Array index treated as glob
|
||||
```
|
||||
|
||||
### Conditionals
|
||||
|
@ -391,7 +366,6 @@ ShellCheck can recognize many types of incorrect test statements.
|
|||
[ grep -q foo file ] # Command without $(..)
|
||||
[[ "$$file" == *.jpg ]] # Comparisons that can't succeed
|
||||
(( 1 -lt 2 )) # Using test operators in ((..))
|
||||
[ x ] & [ y ] | [ z ] # Accidental backgrounding and piping
|
||||
```
|
||||
|
||||
### Frequently misused commands
|
||||
|
@ -463,8 +437,6 @@ echo "Hello $name" # Unassigned lowercase variables
|
|||
cmd | read bar; echo $bar # Assignments in subshells
|
||||
cat foo | cp bar # Piping to commands that don't read
|
||||
printf '%s: %s\n' foo # Mismatches in printf argument count
|
||||
eval "${array[@]}" # Lost word boundaries in array eval
|
||||
for i in "${x[@]}"; do ${x[$i]} # Using array value as key
|
||||
```
|
||||
|
||||
### Robustness
|
||||
|
@ -489,7 +461,6 @@ ShellCheck will warn when using features not supported by the shebang. For examp
|
|||
echo {1..$n} # Works in ksh, but not bash/dash/sh
|
||||
echo {1..10} # Works in ksh and bash, but not dash/sh
|
||||
echo -n 42 # Works in ksh, bash and dash, undefined in sh
|
||||
expr match str regex # Unportable alias for `expr str : regex`
|
||||
trap 'exit 42' sigint # Unportable signal spec
|
||||
cmd &> file # Unportable redirection operator
|
||||
read foo < /dev/tcp/host/22 # Unportable intercepted files
|
||||
|
@ -510,15 +481,10 @@ rm “file” # Unicode quotes
|
|||
echo "Hello world" # Carriage return / DOS line endings
|
||||
echo hello \ # Trailing spaces after \
|
||||
var=42 echo $var # Expansion of inlined environment
|
||||
!# bin/bash -x -e # Common shebang errors
|
||||
#!/bin/bash -x -e # Common shebang errors
|
||||
echo $((n/180*100)) # Unnecessary loss of precision
|
||||
ls *[:digit:].txt # Bad character class globs
|
||||
sed 's/foo/bar/' file > file # Redirecting to input
|
||||
var2=$var2 # Variable assigned to itself
|
||||
[ x$var = xval ] # Antiquated x-comparisons
|
||||
ls() { ls -l "$@"; } # Infinitely recursive wrapper
|
||||
alias ls='ls -l'; ls foo # Alias used before it takes effect
|
||||
for x; do for x; do # Nested loop uses same variable
|
||||
while getopts "a" f; do case $f in "b") # Unhandled getopts flags
|
||||
```
|
||||
|
||||
|
@ -562,3 +528,4 @@ Happy ShellChecking!
|
|||
|
||||
* 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)!
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
Name: ShellCheck
|
||||
Version: 0.10.0
|
||||
Version: 0.7.2
|
||||
Synopsis: Shell script analysis tool
|
||||
License: GPL-3
|
||||
License-file: LICENSE
|
||||
|
@ -8,7 +8,7 @@ Author: Vidar Holen
|
|||
Maintainer: vidar@vidarholen.net
|
||||
Homepage: https://www.shellcheck.net/
|
||||
Build-Type: Simple
|
||||
Cabal-Version: 1.18
|
||||
Cabal-Version: >= 1.8
|
||||
Bug-reports: https://github.com/koalaman/shellcheck/issues
|
||||
Description:
|
||||
The goals of ShellCheck are:
|
||||
|
@ -22,11 +22,9 @@ Description:
|
|||
* To point out subtle caveats, corner cases and pitfalls, that may cause an
|
||||
advanced user's otherwise working script to fail under future circumstances.
|
||||
|
||||
Extra-Doc-Files:
|
||||
README.md
|
||||
CHANGELOG.md
|
||||
Extra-Source-Files:
|
||||
-- documentation
|
||||
README.md
|
||||
shellcheck.1.md
|
||||
-- A script to build the man page using pandoc
|
||||
manpage
|
||||
|
@ -45,26 +43,19 @@ library
|
|||
build-depends:
|
||||
semigroups
|
||||
build-depends:
|
||||
-- The lower bounds are based on GHC 7.10.3
|
||||
-- The upper bounds are based on GHC 9.8.1
|
||||
aeson >= 1.4.0 && < 2.3,
|
||||
array >= 0.5.1 && < 0.6,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
bytestring >= 0.10.6 && < 0.13,
|
||||
containers >= 0.5.6 && < 0.8,
|
||||
deepseq >= 1.4.1 && < 1.6,
|
||||
Diff >= 0.4.0 && < 1.1,
|
||||
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
|
||||
filepath >= 1.4.0 && < 1.6,
|
||||
mtl >= 2.2.2 && < 2.4,
|
||||
parsec >= 3.1.14 && < 3.2,
|
||||
QuickCheck >= 2.14.2 && < 2.16,
|
||||
regex-tdfa >= 1.2.0 && < 1.4,
|
||||
transformers >= 0.4.2 && < 0.7,
|
||||
|
||||
-- getXdgDirectory from 1.2.3.0
|
||||
directory >= 1.2.3 && < 1.4,
|
||||
|
||||
aeson,
|
||||
array,
|
||||
base >= 4.8.0.0 && < 5,
|
||||
bytestring,
|
||||
containers >= 0.5,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec,
|
||||
regex-tdfa,
|
||||
QuickCheck >= 2.7.4,
|
||||
-- When cabal supports it, move this to setup-depends:
|
||||
process
|
||||
exposed-modules:
|
||||
|
@ -73,15 +64,11 @@ library
|
|||
ShellCheck.Analytics
|
||||
ShellCheck.Analyzer
|
||||
ShellCheck.AnalyzerLib
|
||||
ShellCheck.CFG
|
||||
ShellCheck.CFGAnalysis
|
||||
ShellCheck.Checker
|
||||
ShellCheck.Checks.Commands
|
||||
ShellCheck.Checks.ControlFlow
|
||||
ShellCheck.Checks.Custom
|
||||
ShellCheck.Checks.ShellSupport
|
||||
ShellCheck.Data
|
||||
ShellCheck.Debug
|
||||
ShellCheck.Fixer
|
||||
ShellCheck.Formatter.Format
|
||||
ShellCheck.Formatter.CheckStyle
|
||||
|
@ -93,11 +80,9 @@ library
|
|||
ShellCheck.Formatter.Quiet
|
||||
ShellCheck.Interface
|
||||
ShellCheck.Parser
|
||||
ShellCheck.Prelude
|
||||
ShellCheck.Regex
|
||||
other-modules:
|
||||
Paths_ShellCheck
|
||||
default-language: Haskell98
|
||||
|
||||
executable shellcheck
|
||||
if impl(ghc < 8.0)
|
||||
|
@ -106,21 +91,18 @@ executable shellcheck
|
|||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq,
|
||||
Diff,
|
||||
directory,
|
||||
fgl,
|
||||
mtl,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
parsec,
|
||||
QuickCheck,
|
||||
parsec >= 3.0,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa,
|
||||
transformers,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: shellcheck.hs
|
||||
|
||||
test-suite test-shellcheck
|
||||
|
@ -128,19 +110,17 @@ test-suite test-shellcheck
|
|||
build-depends:
|
||||
aeson,
|
||||
array,
|
||||
base,
|
||||
base >= 4 && < 5,
|
||||
bytestring,
|
||||
containers,
|
||||
deepseq,
|
||||
Diff,
|
||||
directory,
|
||||
fgl,
|
||||
deepseq >= 1.4.0.0,
|
||||
Diff >= 0.2.0,
|
||||
directory >= 1.2.3.0,
|
||||
mtl >= 2.2.1,
|
||||
filepath,
|
||||
mtl,
|
||||
parsec,
|
||||
QuickCheck,
|
||||
QuickCheck >= 2.7.4,
|
||||
regex-tdfa,
|
||||
transformers,
|
||||
ShellCheck
|
||||
default-language: Haskell98
|
||||
main-is: test/shellcheck.hs
|
||||
|
||||
|
|
|
@ -11,7 +11,3 @@ This makes it simple to build any release without exotic hardware or software.
|
|||
|
||||
An image can be built and tagged using `build_builder`,
|
||||
and run on a source tarball using `run_builder`.
|
||||
|
||||
Tip: Are you developing an image that relies on QEmu usermode emulation?
|
||||
It's easy to accidentally depend on binfmt\_misc on the host OS.
|
||||
Do a `echo 0 | sudo tee /proc/sys/fs/binfmt_misc/status` before testing.
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
FROM ghcr.io/shepherdjerred/macos-cross-compiler:latest
|
||||
|
||||
ENV TARGET aarch64-apple-darwin22
|
||||
ENV TARGETNAME darwin.aarch64
|
||||
|
||||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
ENV LC_ALL C.utf8
|
||||
|
||||
# Install basic deps
|
||||
RUN apt-get update && apt-get install -y automake autoconf build-essential curl xz-utils qemu-user-static
|
||||
|
||||
# Install a more suitable host compiler
|
||||
WORKDIR /host-ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||
RUN curl -L 'https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-x86_64-deb10-linux.tar.xz' | tar xJ --strip-components=1
|
||||
RUN ./configure && make install
|
||||
|
||||
# Build GHC. We have to use an old version because cross-compilation across OS has since broken.
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.7/ghc-8.10.7-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN apt-get install -y llvm-12
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||
RUN make install
|
||||
|
||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||
ENV CABALOPTS "--ghc-options;-optc-Os -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;--constraint=hashable==1.3.5.0"
|
||||
|
||||
# Prebuild the dependencies
|
||||
RUN cabal update
|
||||
RUN IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
||||
# Copy the build script
|
||||
COPY build /usr/bin
|
||||
|
||||
WORKDIR /scratch
|
||||
ENTRYPOINT ["/usr/bin/build"]
|
|
@ -1,16 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -xe
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
( IFS=';'; cabal build $CABALOPTS )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
# Stripping invalidates the code signature and the build image does
|
||||
# not appear to have anything similar to the 'codesign' tool.
|
||||
# "$TARGET-strip" "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
file "$TARGETNAME/shellcheck" | grep "Mach-O 64-bit arm64 executable"
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
|
@ -1 +0,0 @@
|
|||
koalaman/scbuilder-darwin-aarch64
|
|
@ -1,4 +1,5 @@
|
|||
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||
FROM liushuyu/osxcross:latest
|
||||
|
||||
ENV TARGET x86_64-apple-darwin18
|
||||
ENV TARGETNAME darwin.x86_64
|
||||
|
@ -6,18 +7,15 @@ ENV TARGETNAME darwin.x86_64
|
|||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
|
||||
RUN apt-get update
|
||||
RUN apt-get dist-upgrade -y
|
||||
RUN apt-get install -y ghc automake autoconf llvm curl alex happy
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||
RUN make install
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
|
||||
|
||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||
|
|
|
@ -4,6 +4,7 @@ set -xe
|
|||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
|
|
|
@ -6,29 +6,19 @@ ENV TARGETNAME linux.aarch64
|
|||
# Build dependencies
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# These deps are from 20.04, because GHC's compiler/llvm support moves slowly
|
||||
RUN apt-get update && apt-get install -y llvm gcc-$TARGET
|
||||
|
||||
# The rest are from 22.10
|
||||
RUN sed -e 's/focal/kinetic/g' -i /etc/apt/sources.list
|
||||
# Kinetic does not receive updates anymore, switch to last available
|
||||
RUN sed -e 's/archive.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||
RUN sed -e 's/security.ubuntu.com/old-releases.ubuntu.com/g' -i /etc/apt/sources.list
|
||||
|
||||
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
|
||||
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
|
||||
|
||||
# Build GHC
|
||||
WORKDIR /ghc
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.8/ghc-9.2.8-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
||||
RUN cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||
RUN make install
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.9.0.0/cabal-install-3.9-x86_64-linux-alpine.tar.xz" | tar xJv -C /usr/local/bin
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/local/bin
|
||||
|
||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
||||
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg;-c;hashable -arch-native"
|
||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
|
||||
|
||||
# Prebuild the dependencies
|
||||
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
|
|
@ -4,6 +4,7 @@ set -xe
|
|||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS --enable-executable-static )
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
# This Docker file uses a custom QEmu fork with patches to follow execve
|
||||
# to build all of ShellCheck emulated.
|
||||
# I've again spent days trying to get a working armv6hf compiler going.
|
||||
# God only knows how many recompilations of GCC, GHC, libraries, and
|
||||
# ShellCheck itself, has gone into it.
|
||||
#
|
||||
# I tried Debian's toolchain. I tried my custom one built according to
|
||||
# RPi `gcc -v`. I tried GHC9, glibc, musl, registerised vs not, but
|
||||
# nothing has yielded an armv6hf binary that does not immediately
|
||||
# segfault on qemu-arm-static or the RPi itself.
|
||||
#
|
||||
# I then tried the same but with armv7hf. Same story.
|
||||
#
|
||||
# Emulating the entire userspace with balenalib again? Very strange build
|
||||
# failures where programs would fail to execute with > ~100 arguments.
|
||||
#
|
||||
# Finally, creating our own appears to work when using a custom QEmu
|
||||
# patched to follow execve calls.
|
||||
#
|
||||
# PS: $100 bounty for getting a RPi1 compatible static build going
|
||||
# with cross-compilation, similar to what the aarch64 build does.
|
||||
#
|
||||
|
||||
FROM ubuntu:24.04
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGETNAME linux.armv6hf
|
||||
|
||||
|
@ -9,34 +27,33 @@ ENV TARGETNAME linux.armv6hf
|
|||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update
|
||||
RUN apt-get install -y --no-install-recommends build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev python3-setuptools ca-certificates debootstrap
|
||||
WORKDIR /qemu
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu .
|
||||
RUN ./configure --static --disable-werror && cd build && ninja qemu-arm
|
||||
RUN apt-get install -y build-essential git ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev
|
||||
WORKDIR /build
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu
|
||||
RUN cd qemu && ./configure --static && cd build && ninja qemu-arm
|
||||
RUN cp qemu/build/qemu-arm /build/qemu-arm-static
|
||||
ENV QEMU_EXECVE 1
|
||||
|
||||
# Convenience utility
|
||||
COPY scutil /bin/scutil
|
||||
COPY scutil /chroot/bin/scutil
|
||||
RUN chmod +x /bin/scutil /chroot/bin/scutil
|
||||
|
||||
# Set up an armv6 userspace
|
||||
WORKDIR /
|
||||
RUN debootstrap --arch armhf --variant=minbase --foreign bookworm /chroot http://mirrordirector.raspbian.org/raspbian
|
||||
RUN cp /qemu/build/qemu-arm /chroot/bin/qemu
|
||||
RUN scutil emu /debootstrap/debootstrap --second-stage
|
||||
RUN apt-get install -y debootstrap qemu-user-static
|
||||
# We expect this to fail if the host doesn't have binfmt qemu support
|
||||
RUN qemu-debootstrap --arch armhf bullseye pi http://mirrordirector.raspbian.org/raspbian || [ -e /pi/etc/issue ]
|
||||
RUN cp /build/qemu-arm-static /pi/usr/bin/qemu-arm-static
|
||||
RUN printf > /bin/pirun '%s\n' '#!/bin/sh' 'chroot /pi /usr/bin/qemu-arm-static /usr/bin/env "$@"' && chmod +x /bin/pirun
|
||||
# If the debootstrap process didn't finish, continue it
|
||||
RUN [ ! -e /pi/debootstrap ] || pirun '/debootstrap/debootstrap' --second-stage
|
||||
|
||||
# Install deps in the chroot
|
||||
RUN scutil emu apt-get update
|
||||
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
|
||||
RUN scutil emu cabal update
|
||||
RUN pirun apt-get update
|
||||
RUN pirun apt-get install -y ghc cabal-install
|
||||
|
||||
# Finally we can build the current dependencies. This takes hours.
|
||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
|
||||
# Generated with `cabal freeze --constraint 'hashable -arch-native'`
|
||||
COPY cabal.project.freeze /chroot/etc
|
||||
RUN IFS=";" && scutil install_from_freeze /chroot/etc/cabal.project.freeze emu cabal install $CABALOPTS
|
||||
RUN pirun cabal update
|
||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||
|
||||
# Copy the build script
|
||||
COPY build /chroot/bin
|
||||
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
||||
WORKDIR /pi/scratch
|
||||
COPY build /pi/usr/bin
|
||||
ENTRYPOINT ["/bin/pirun", "/usr/bin/build"]
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
#!/bin/sh
|
||||
set -xe
|
||||
mkdir /scratch && cd /scratch
|
||||
cd /scratch
|
||||
{
|
||||
tar xzv --strip-components=1
|
||||
cp /etc/cabal.project.freeze .
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
# This script does not cabal update because compiling anything new is slow
|
||||
|
|
|
@ -1,93 +0,0 @@
|
|||
active-repositories: hackage.haskell.org:merge
|
||||
constraints: any.Diff ==0.5,
|
||||
any.OneTuple ==0.4.2,
|
||||
any.QuickCheck ==2.14.3,
|
||||
QuickCheck -old-random +templatehaskell,
|
||||
any.StateVar ==1.2.2,
|
||||
any.aeson ==2.2.3.0,
|
||||
aeson +ordered-keymap,
|
||||
any.array ==0.5.4.0,
|
||||
any.assoc ==1.1.1,
|
||||
assoc -tagged,
|
||||
any.base ==4.15.1.0,
|
||||
any.base-orphans ==0.9.2,
|
||||
any.bifunctors ==5.6.2,
|
||||
bifunctors +tagged,
|
||||
any.binary ==0.8.8.0,
|
||||
any.bytestring ==0.10.12.1,
|
||||
any.character-ps ==0.1,
|
||||
any.comonad ==5.0.8,
|
||||
comonad +containers +distributive +indexed-traversable,
|
||||
any.containers ==0.6.4.1,
|
||||
any.contravariant ==1.5.5,
|
||||
contravariant +semigroups +statevar +tagged,
|
||||
any.data-array-byte ==0.1.0.1,
|
||||
any.data-fix ==0.3.3,
|
||||
any.deepseq ==1.4.5.0,
|
||||
any.directory ==1.3.6.2,
|
||||
any.distributive ==0.6.2.1,
|
||||
distributive +semigroups +tagged,
|
||||
any.dlist ==1.0,
|
||||
dlist -werror,
|
||||
any.exceptions ==0.10.4,
|
||||
any.fgl ==5.8.2.0,
|
||||
fgl +containers042,
|
||||
any.filepath ==1.4.2.1,
|
||||
any.foldable1-classes-compat ==0.1,
|
||||
foldable1-classes-compat +tagged,
|
||||
any.generically ==0.1.1,
|
||||
any.ghc-bignum ==1.1,
|
||||
any.ghc-boot-th ==9.0.2,
|
||||
any.ghc-prim ==0.7.0,
|
||||
any.hashable ==1.4.6.0,
|
||||
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||
any.indexed-traversable ==0.1.4,
|
||||
any.indexed-traversable-instances ==0.1.2,
|
||||
any.integer-conversion ==0.1.1,
|
||||
any.integer-logarithms ==1.0.3.1,
|
||||
integer-logarithms -check-bounds +integer-gmp,
|
||||
any.mtl ==2.2.2,
|
||||
any.network-uri ==2.6.4.2,
|
||||
any.parsec ==3.1.14.0,
|
||||
any.pretty ==1.1.3.6,
|
||||
any.primitive ==0.9.0.0,
|
||||
any.process ==1.6.13.2,
|
||||
any.random ==1.2.1.2,
|
||||
any.regex-base ==0.94.0.2,
|
||||
any.regex-tdfa ==1.3.2.2,
|
||||
regex-tdfa +doctest -force-o2,
|
||||
any.rts ==1.0.2,
|
||||
any.scientific ==0.3.8.0,
|
||||
scientific -integer-simple,
|
||||
any.semialign ==1.3.1,
|
||||
semialign +semigroupoids,
|
||||
any.semigroupoids ==6.0.1,
|
||||
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||
any.splitmix ==0.1.0.5,
|
||||
splitmix -optimised-mixer,
|
||||
any.stm ==2.5.0.0,
|
||||
any.strict ==0.5,
|
||||
any.tagged ==0.8.8,
|
||||
tagged +deepseq +transformers,
|
||||
any.template-haskell ==2.17.0.0,
|
||||
any.text ==1.2.5.0,
|
||||
any.text-iso8601 ==0.1.1,
|
||||
any.text-short ==0.1.6,
|
||||
text-short -asserts,
|
||||
any.th-abstraction ==0.7.0.0,
|
||||
any.th-compat ==0.1.5,
|
||||
any.these ==1.2.1,
|
||||
any.time ==1.9.3,
|
||||
any.time-compat ==1.9.7,
|
||||
any.transformers ==0.5.6.2,
|
||||
any.transformers-compat ==0.7.2,
|
||||
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||
any.unix ==2.7.2.2,
|
||||
any.unordered-containers ==0.2.20,
|
||||
unordered-containers -debug,
|
||||
any.uuid-types ==1.0.6,
|
||||
any.vector ==0.13.1.0,
|
||||
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||
any.vector-stream ==0.1.0.1,
|
||||
any.witherable ==0.5
|
||||
index-state: hackage.haskell.org 2024-06-18T02:21:19Z
|
|
@ -1,48 +0,0 @@
|
|||
#!/bin/dash
|
||||
# Various ShellCheck build utility functions
|
||||
|
||||
# Generally set a ulimit to avoid QEmu using too much memory
|
||||
ulimit -v "$((10*1024*1024))"
|
||||
# If we happen to invoke or run under QEmu, make sure to follow execve.
|
||||
# This requires a patched QEmu.
|
||||
export QEMU_EXECVE=1
|
||||
|
||||
# Retry a command until it succeeds
|
||||
# Usage: scutil retry 3 mycmd
|
||||
retry() {
|
||||
n="$1"
|
||||
ret=1
|
||||
shift
|
||||
while [ "$n" -gt 0 ]
|
||||
do
|
||||
"$@"
|
||||
ret=$?
|
||||
[ "$ret" = 0 ] && break
|
||||
n=$((n-1))
|
||||
done
|
||||
return "$ret"
|
||||
}
|
||||
|
||||
# Install all dependencies from a freeze file
|
||||
# Usage: scutil install_from_freeze /path/cabal.project.freeze cabal install
|
||||
install_from_freeze() {
|
||||
linefeed=$(printf '\nx')
|
||||
linefeed=${linefeed%x}
|
||||
flags=$(
|
||||
sed 's/constraints:/&\n /' "$1" |
|
||||
grep -vw -e rts -e base |
|
||||
sed -n -e 's/^ *\([^,]*\).*/\1/p' |
|
||||
sed -e 's/any\.\([^ ]*\) ==\(.*\)/\1-\2/; te; s/.*/--constraint\n&/; :e')
|
||||
shift
|
||||
# shellcheck disable=SC2086
|
||||
( IFS=$linefeed; set -x; "$@" $flags )
|
||||
}
|
||||
|
||||
# Run a command under emulation.
|
||||
# This assumes the correct emulator is named 'qemu' and the chroot is /chroot
|
||||
# Usage: scutil emu echo "Hello World"
|
||||
emu() {
|
||||
chroot /chroot /bin/qemu /usr/bin/env "$@"
|
||||
}
|
||||
|
||||
"$@"
|
|
@ -1,46 +0,0 @@
|
|||
FROM ubuntu:24.04
|
||||
|
||||
ENV TARGETNAME linux.riscv64
|
||||
ENV TARGET riscv64-linux-gnu
|
||||
|
||||
USER root
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
|
||||
# Init base
|
||||
RUN apt-get update -y
|
||||
|
||||
# Install qemu
|
||||
RUN apt-get install -y --no-install-recommends build-essential ninja-build python3 pkg-config libglib2.0-dev libpixman-1-dev curl ca-certificates python3-virtualenv git python3-setuptools debootstrap
|
||||
WORKDIR /qemu
|
||||
RUN git clone --depth 1 https://github.com/koalaman/qemu .
|
||||
RUN ./configure --target-list=riscv64-linux-user --static --disable-system --disable-pie --disable-werror
|
||||
RUN cd build && ninja qemu-riscv64
|
||||
ENV QEMU_EXECVE 1
|
||||
|
||||
# Convenience utility
|
||||
COPY scutil /bin/scutil
|
||||
# We have to copy to /usr/bin because debootstrap will try to symlink /bin and fail if it exists
|
||||
COPY scutil /chroot/usr/bin/scutil
|
||||
RUN chmod +x /bin/scutil /chroot/usr/bin/scutil
|
||||
|
||||
# Set up a riscv64 userspace
|
||||
WORKDIR /
|
||||
RUN debootstrap --arch=riscv64 --variant=minbase --components=main,universe --foreign noble /chroot http://ports.ubuntu.com/ubuntu-ports
|
||||
RUN cp /qemu/build/qemu-riscv64 /chroot/bin/qemu
|
||||
RUN scutil emu /debootstrap/debootstrap --second-stage
|
||||
|
||||
# Install deps in the chroot
|
||||
RUN scutil emu apt-get update
|
||||
RUN scutil emu apt-get install -y --no-install-recommends ghc cabal-install
|
||||
RUN scutil emu cabal update
|
||||
|
||||
# Generated with: cabal freeze -c 'hashable -arch-native'. We put it in /etc so cabal won't find it.
|
||||
COPY cabal.project.freeze /chroot/etc
|
||||
|
||||
# Build all dependencies from the freeze file. The emulator segfaults at random,
|
||||
# so retry a few times.
|
||||
RUN scutil install_from_freeze /chroot/etc/cabal.project.freeze retry 5 emu cabal install --keep-going
|
||||
|
||||
# Copy the build script
|
||||
COPY build /chroot/bin/build
|
||||
ENTRYPOINT ["/bin/scutil", "emu", "/bin/build"]
|
|
@ -1,21 +0,0 @@
|
|||
#!/bin/sh
|
||||
set -xe
|
||||
IFS=';'
|
||||
{
|
||||
mkdir -p /tmp/scratch
|
||||
cd /tmp/scratch
|
||||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
# Use a freeze file to ensure we use the same dependencies we cached during
|
||||
# the docker image build. We don't want to spend time compiling anything new.
|
||||
cp /etc/cabal.project.freeze .
|
||||
mkdir "$TARGETNAME"
|
||||
# Retry in case of random segfault
|
||||
scutil retry 3 cabal build --enable-executable-static
|
||||
find . -name shellcheck -type f -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGET-strip" -s "$TARGETNAME/shellcheck"
|
||||
ls -l "$TARGETNAME"
|
||||
"$TARGETNAME/shellcheck" --version
|
||||
} >&2
|
||||
tar czv "$TARGETNAME"
|
|
@ -1,93 +0,0 @@
|
|||
active-repositories: hackage.haskell.org:merge
|
||||
constraints: any.Diff ==0.5,
|
||||
any.OneTuple ==0.4.2,
|
||||
any.QuickCheck ==2.14.3,
|
||||
QuickCheck -old-random +templatehaskell,
|
||||
any.StateVar ==1.2.2,
|
||||
any.aeson ==2.2.3.0,
|
||||
aeson +ordered-keymap,
|
||||
any.array ==0.5.4.0,
|
||||
any.assoc ==1.1.1,
|
||||
assoc -tagged,
|
||||
any.base ==4.17.2.0,
|
||||
any.base-orphans ==0.9.2,
|
||||
any.bifunctors ==5.6.2,
|
||||
bifunctors +tagged,
|
||||
any.binary ==0.8.9.1,
|
||||
any.bytestring ==0.11.5.2,
|
||||
any.character-ps ==0.1,
|
||||
any.comonad ==5.0.8,
|
||||
comonad +containers +distributive +indexed-traversable,
|
||||
any.containers ==0.6.7,
|
||||
any.contravariant ==1.5.5,
|
||||
contravariant +semigroups +statevar +tagged,
|
||||
any.data-fix ==0.3.3,
|
||||
any.deepseq ==1.4.8.0,
|
||||
any.directory ==1.3.7.1,
|
||||
any.distributive ==0.6.2.1,
|
||||
distributive +semigroups +tagged,
|
||||
any.dlist ==1.0,
|
||||
dlist -werror,
|
||||
any.exceptions ==0.10.5,
|
||||
any.fgl ==5.8.2.0,
|
||||
fgl +containers042,
|
||||
any.filepath ==1.4.2.2,
|
||||
any.foldable1-classes-compat ==0.1,
|
||||
foldable1-classes-compat +tagged,
|
||||
any.generically ==0.1.1,
|
||||
any.ghc-bignum ==1.3,
|
||||
any.ghc-boot-th ==9.4.7,
|
||||
any.ghc-prim ==0.9.1,
|
||||
any.hashable ==1.4.6.0,
|
||||
hashable -arch-native +integer-gmp -random-initial-seed,
|
||||
any.indexed-traversable ==0.1.4,
|
||||
any.indexed-traversable-instances ==0.1.2,
|
||||
any.integer-conversion ==0.1.1,
|
||||
any.integer-logarithms ==1.0.3.1,
|
||||
integer-logarithms -check-bounds +integer-gmp,
|
||||
any.mtl ==2.2.2,
|
||||
any.network-uri ==2.6.4.2,
|
||||
any.os-string ==2.0.3,
|
||||
any.parsec ==3.1.16.1,
|
||||
any.pretty ==1.1.3.6,
|
||||
any.primitive ==0.9.0.0,
|
||||
any.process ==1.6.17.0,
|
||||
any.random ==1.2.1.2,
|
||||
any.regex-base ==0.94.0.2,
|
||||
any.regex-tdfa ==1.3.2.2,
|
||||
regex-tdfa +doctest -force-o2,
|
||||
any.rts ==1.0.2,
|
||||
any.scientific ==0.3.8.0,
|
||||
scientific -integer-simple,
|
||||
any.semialign ==1.3.1,
|
||||
semialign +semigroupoids,
|
||||
any.semigroupoids ==6.0.1,
|
||||
semigroupoids +comonad +containers +contravariant +distributive +tagged +unordered-containers,
|
||||
any.splitmix ==0.1.0.5,
|
||||
splitmix -optimised-mixer,
|
||||
any.stm ==2.5.1.0,
|
||||
any.strict ==0.5,
|
||||
any.tagged ==0.8.8,
|
||||
tagged +deepseq +transformers,
|
||||
any.template-haskell ==2.19.0.0,
|
||||
any.text ==2.0.2,
|
||||
any.text-iso8601 ==0.1.1,
|
||||
any.text-short ==0.1.6,
|
||||
text-short -asserts,
|
||||
any.th-abstraction ==0.7.0.0,
|
||||
any.th-compat ==0.1.5,
|
||||
any.these ==1.2.1,
|
||||
any.time ==1.12.2,
|
||||
any.time-compat ==1.9.7,
|
||||
any.transformers ==0.5.6.2,
|
||||
any.transformers-compat ==0.7.2,
|
||||
transformers-compat -five +five-three -four +generic-deriving +mtl -three -two,
|
||||
any.unix ==2.7.3,
|
||||
any.unordered-containers ==0.2.20,
|
||||
unordered-containers -debug,
|
||||
any.uuid-types ==1.0.6,
|
||||
any.vector ==0.13.1.0,
|
||||
vector +boundschecks -internalchecks -unsafechecks -wall,
|
||||
any.vector-stream ==0.1.0.1,
|
||||
any.witherable ==0.5
|
||||
index-state: hackage.haskell.org 2024-06-17T00:48:51Z
|
|
@ -1 +0,0 @@
|
|||
koalaman/scbuilder-linux-riscv64
|
|
@ -1,14 +1,16 @@
|
|||
FROM alpine:3.16
|
||||
# alpine:3.16 (GHC 9.0.1): 5.8 megabytes
|
||||
# alpine:3.17 (GHC 9.0.2): 15.0 megabytes
|
||||
# alpine:3.18 (GHC 9.4.4): 29.0 megabytes
|
||||
# alpine:3.19 (GHC 9.4.7): 29.0 megabytes
|
||||
FROM ubuntu:20.04
|
||||
|
||||
ENV TARGETNAME linux.x86_64
|
||||
|
||||
# Install GHC and cabal
|
||||
USER root
|
||||
RUN apk add ghc cabal g++ libffi-dev curl bash
|
||||
ENV DEBIAN_FRONTEND noninteractive
|
||||
RUN apt-get update && apt-get install -y ghc curl xz-utils
|
||||
|
||||
# So we'd like a later version of Cabal that supports --enable-executable-static,
|
||||
# but we can't use Ubuntu 20.10 because coreutils has switched to new syscalls that
|
||||
# the TravisCI kernel doesn't support. Download it manually.
|
||||
RUN curl "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-linux.tar.xz" | tar xJv -C /usr/bin
|
||||
|
||||
# Use ld.bfd instead of ld.gold due to
|
||||
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
|
||||
|
|
|
@ -12,7 +12,7 @@ WORKDIR /haskell
|
|||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-x86_64-unknown-mingw32.tar.xz" | tar xJ --strip-components=1
|
||||
WORKDIR /haskell/bin
|
||||
RUN curl -L "https://downloads.haskell.org/~cabal/cabal-install-3.2.0.0/cabal-install-3.2.0.0-x86_64-unknown-mingw32.zip" | busybox unzip -
|
||||
RUN curl -L "https://curl.se/windows/dl-8.7.1_7/curl-8.7.1_7-win64-mingw.zip" | busybox unzip - && mv curl-*-win64-mingw/bin/* .
|
||||
RUN curl -L "https://curl.se/windows/dl-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-win64-mingw/bin/* .
|
||||
ENV WINEPATH /haskell/bin
|
||||
|
||||
# It's unknown whether Cabal on Windows suffers from the same issue
|
||||
|
|
|
@ -8,6 +8,7 @@ set -xe
|
|||
tar xzv --strip-components=1
|
||||
chmod +x striptests && ./striptests
|
||||
mkdir "$TARGETNAME"
|
||||
cabal update
|
||||
( IFS=';'; cabal build $CABALOPTS )
|
||||
find dist*/ -name shellcheck.exe -type f -ls -exec mv {} "$TARGETNAME/" \;
|
||||
ls -l "$TARGETNAME"
|
||||
|
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 244 KiB |
10
quickrun
10
quickrun
|
@ -2,12 +2,4 @@
|
|||
# quickrun runs ShellCheck in an interpreted mode.
|
||||
# This allows testing changes without recompiling.
|
||||
|
||||
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
|
||||
if [ -z "$path" ]
|
||||
then
|
||||
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
|
||||
exit 1
|
||||
fi
|
||||
path="${path%/*}"
|
||||
|
||||
exec runghc -isrc -i"$path" shellcheck.hs "$@"
|
||||
runghc -isrc -idist/build/autogen shellcheck.hs "$@"
|
||||
|
|
11
quicktest
11
quicktest
|
@ -3,17 +3,8 @@
|
|||
# This allows running tests without compiling, which can be faster.
|
||||
# 'cabal test' remains the source of truth.
|
||||
|
||||
path=$(find . -type f -path './dist*/Paths_ShellCheck.hs' | sort | head -n 1)
|
||||
if [ -z "$path" ]
|
||||
then
|
||||
echo >&2 "Unable to find Paths_ShellCheck.hs. Please 'cabal build' once."
|
||||
exit 1
|
||||
fi
|
||||
path="${path%/*}"
|
||||
|
||||
|
||||
(
|
||||
var=$(echo 'main' | ghci -isrc -i"$path" test/shellcheck.hs 2>&1 | tee /dev/stderr)
|
||||
var=$(echo 'main' | ghci test/shellcheck.hs 2>&1 | tee /dev/stderr)
|
||||
if [[ $var == *ExitSuccess* ]]
|
||||
then
|
||||
exit 0
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
#!/bin/sh -xe
|
||||
# This script hardcodes the `git describe` version as ShellCheck's version number.
|
||||
# This is done to allow shellcheck --version to differ from the cabal version when
|
||||
# building git snapshots.
|
||||
|
||||
file="src/ShellCheck/Data.hs"
|
||||
test -e "$file"
|
||||
tmp=$(mktemp)
|
||||
version=$(git describe)
|
||||
sed -e "s/=.*VERSIONSTRING.*/= \"$version\" -- VERSIONSTRING, DO NOT SUBMIT/" "$file" > "$tmp"
|
||||
mv "$tmp" "$file"
|
|
@ -56,13 +56,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||
options are cumulative, but all the codes can be specified at once,
|
||||
comma-separated as a single argument.
|
||||
|
||||
**--extended-analysis=true/false**
|
||||
|
||||
: Enable/disable Dataflow Analysis to identify more issues (default true). If
|
||||
ShellCheck uses too much CPU/RAM when checking scripts with several
|
||||
thousand lines of code, extended analysis can be disabled with this flag
|
||||
or a directive. This flag overrides directives and rc files.
|
||||
|
||||
**-f** *FORMAT*, **--format=***FORMAT*
|
||||
|
||||
: Specify the output format of shellcheck, which prints its results in the
|
||||
|
@ -78,11 +71,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||
|
||||
: Don't try to look for .shellcheckrc configuration files.
|
||||
|
||||
**--rcfile** *RCFILE*
|
||||
|
||||
: Prefer the specified configuration file over searching for one
|
||||
in the default locations.
|
||||
|
||||
**-o**\ *NAME1*[,*NAME2*...],\ **--enable=***NAME1*[,*NAME2*...]
|
||||
|
||||
: Enable optional checks. The special name *all* enables all of them.
|
||||
|
@ -97,8 +85,7 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||
|
||||
**-s**\ *shell*,\ **--shell=***shell*
|
||||
|
||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash*, *ksh*,
|
||||
and *busybox*.
|
||||
: 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,
|
||||
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
|
||||
POSIX `sh` (not the system's), and will warn of portability issues.
|
||||
|
@ -125,9 +112,6 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||
line (plus `/dev/null`). This option allows following any file the script
|
||||
may `source`.
|
||||
|
||||
This option may also be enabled using `external-sources=true` in
|
||||
`.shellcheckrc`. This flag takes precedence.
|
||||
|
||||
**FILES...**
|
||||
|
||||
: One or more script files to check, or "-" for standard input.
|
||||
|
@ -250,26 +234,11 @@ Valid keys are:
|
|||
The command can be a simple command like `echo foo`, or a compound command
|
||||
like a function definition, subshell block or loop. A range can be
|
||||
be specified with a dash, e.g. `disable=SC3000-SC4000` to exclude 3xxx.
|
||||
All warnings can be disabled with `disable=all`.
|
||||
|
||||
**enable**
|
||||
: Enable an optional check by name, as listed with **--list-optional**.
|
||||
Only file-wide `enable` directives are considered.
|
||||
|
||||
**extended-analysis**
|
||||
: Set to true/false to enable/disable dataflow analysis. Specifying
|
||||
`# shellcheck extended-analysis=false` in particularly large (2000+ line)
|
||||
auto-generated scripts will reduce ShellCheck's resource usage at the
|
||||
expense of certain checks. Extended analysis is enabled by default.
|
||||
|
||||
**external-sources**
|
||||
: Set to `true` in `.shellcheckrc` to always allow ShellCheck to open
|
||||
arbitrary files from 'source' statements (the way most tools do).
|
||||
|
||||
This option defaults to `false` only due to ShellCheck's origin as a
|
||||
remote service for checking untrusted scripts. It can safely be enabled
|
||||
for normal development.
|
||||
|
||||
**source**
|
||||
: 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
|
||||
|
@ -301,12 +270,6 @@ Here is an example `.shellcheckrc`:
|
|||
source-path=SCRIPTDIR
|
||||
source-path=/mnt/chroot
|
||||
|
||||
# Since 0.9.0, values can be quoted with '' or "" to allow spaces
|
||||
source-path="My Documents/scripts"
|
||||
|
||||
# Allow opening any 'source'd file, even if not specified as input
|
||||
external-sources=true
|
||||
|
||||
# Turn on warnings for unquoted variables with safe values
|
||||
enable=quote-safe-variables
|
||||
|
||||
|
@ -317,7 +280,7 @@ Here is an example `.shellcheckrc`:
|
|||
disable=SC2236
|
||||
|
||||
If no `.shellcheckrc` is found in any of the parent directories, ShellCheck
|
||||
will look in `~/.shellcheckrc` followed by the `$XDG_CONFIG_HOME`
|
||||
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.
|
||||
|
||||
|
@ -357,32 +320,10 @@ locales where encoding is unspecified (such as the `C` locale).
|
|||
Windows users seeing `commitBuffer: invalid argument (invalid character)`
|
||||
should set their terminal to use UTF-8 with `chcp 65001`.
|
||||
|
||||
# KNOWN INCOMPATIBILITIES
|
||||
# AUTHORS
|
||||
|
||||
(If nothing in this section makes sense, you are unlikely to be affected by it)
|
||||
|
||||
To avoid confusing and misguided suggestions, ShellCheck requires function
|
||||
bodies to be either `{ brace groups; }` or `( subshells )`, and function names
|
||||
containing `[]*=!` are only recognized after a `function` keyword.
|
||||
|
||||
The following unconventional function definitions are identical in Bash,
|
||||
but ShellCheck only recognizes the latter.
|
||||
|
||||
[x!=y] () [[ $1 ]]
|
||||
function [x!=y] () { [[ $1 ]]; }
|
||||
|
||||
Shells without the `function` keyword do not allow these characters in function
|
||||
names to begin with. Function names containing `{}` are not supported at all.
|
||||
|
||||
Further, if ShellCheck sees `[x!=y]` it will assume this is an invalid
|
||||
comparison. To invoke the above function, quote the command as in `'[x!=y]'`,
|
||||
or to retain the same globbing behavior, use `command [x!=y]`.
|
||||
|
||||
ShellCheck imposes additional restrictions on the `[` command to help diagnose
|
||||
common invalid uses. While `[ $x= 1 ]` is defined in POSIX, ShellCheck will
|
||||
assume it was intended as the much more likely comparison `[ "$x" = 1 ]` and
|
||||
fail accordingly. For unconventional or dynamic uses of the `[` command, use
|
||||
`test` or `\[` instead.
|
||||
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
|
||||
long list of wonderful contributors.
|
||||
|
||||
# REPORTING BUGS
|
||||
|
||||
|
@ -390,17 +331,12 @@ Bugs and issues can be reported on GitHub:
|
|||
|
||||
https://github.com/koalaman/shellcheck/issues
|
||||
|
||||
# AUTHORS
|
||||
|
||||
ShellCheck is developed and maintained by Vidar Holen, with assistance from a
|
||||
long list of wonderful contributors.
|
||||
|
||||
# COPYRIGHT
|
||||
|
||||
Copyright 2012-2024, Vidar Holen and contributors.
|
||||
Copyright 2012-2019, Vidar Holen and contributors.
|
||||
Licensed under the GNU General Public License version 3 or later,
|
||||
see https://gnu.org/licenses/gpl.html
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
sh(1) bash(1) dash(1) ksh(1)
|
||||
sh(1) bash(1)
|
||||
|
|
109
shellcheck.hs
109
shellcheck.hs
|
@ -34,8 +34,6 @@ import qualified ShellCheck.Formatter.Quiet
|
|||
|
||||
import Control.Exception
|
||||
import Control.Monad
|
||||
import Control.Monad.IO.Class
|
||||
import Control.Monad.Trans.Class
|
||||
import Control.Monad.Except
|
||||
import Data.Bits
|
||||
import Data.Char
|
||||
|
@ -76,8 +74,7 @@ data Options = Options {
|
|||
externalSources :: Bool,
|
||||
sourcePaths :: [FilePath],
|
||||
formatterOptions :: FormatterOptions,
|
||||
minSeverity :: Severity,
|
||||
rcfile :: Maybe FilePath
|
||||
minSeverity :: Severity
|
||||
}
|
||||
|
||||
defaultOptions = Options {
|
||||
|
@ -87,8 +84,7 @@ defaultOptions = Options {
|
|||
formatterOptions = newFormatterOptions {
|
||||
foColorOption = ColorAuto
|
||||
},
|
||||
minSeverity = StyleC,
|
||||
rcfile = Nothing
|
||||
minSeverity = StyleC
|
||||
}
|
||||
|
||||
usageHeader = "Usage: shellcheck [OPTIONS...] FILES..."
|
||||
|
@ -102,8 +98,6 @@ options = [
|
|||
(ReqArg (Flag "include") "CODE1,CODE2..") "Consider only given types of warnings",
|
||||
Option "e" ["exclude"]
|
||||
(ReqArg (Flag "exclude") "CODE1,CODE2..") "Exclude types of warnings",
|
||||
Option "" ["extended-analysis"]
|
||||
(ReqArg (Flag "extended-analysis") "bool") "Perform dataflow analysis (default true)",
|
||||
Option "f" ["format"]
|
||||
(ReqArg (Flag "format") "FORMAT") $
|
||||
"Output format (" ++ formatList ++ ")",
|
||||
|
@ -111,9 +105,6 @@ options = [
|
|||
(NoArg $ Flag "list-optional" "true") "List checks disabled by default",
|
||||
Option "" ["norc"]
|
||||
(NoArg $ Flag "norc" "true") "Don't look for .shellcheckrc files",
|
||||
Option "" ["rcfile"]
|
||||
(ReqArg (Flag "rcfile") "RCFILE")
|
||||
"Prefer the specified configuration file over searching for one",
|
||||
Option "o" ["enable"]
|
||||
(ReqArg (Flag "enable") "check1,check2..")
|
||||
"List of optional checks to enable (or 'all')",
|
||||
|
@ -122,7 +113,7 @@ options = [
|
|||
"Specify path when looking for sourced files (\"SCRIPTDIR\" for script's dir)",
|
||||
Option "s" ["shell"]
|
||||
(ReqArg (Flag "shell") "SHELLNAME")
|
||||
"Specify dialect (sh, bash, dash, ksh, busybox)",
|
||||
"Specify dialect (sh, bash, dash, ksh)",
|
||||
Option "S" ["severity"]
|
||||
(ReqArg (Flag "severity") "SEVERITY")
|
||||
"Minimum severity of errors to consider (error, warning, info, style)",
|
||||
|
@ -234,7 +225,7 @@ runFormatter sys format options files = do
|
|||
f :: Status -> FilePath -> IO Status
|
||||
f status file = do
|
||||
newStatus <- process file `catch` handler file
|
||||
return $! status `mappend` newStatus
|
||||
return $ status `mappend` newStatus
|
||||
handler :: FilePath -> IOException -> IO Status
|
||||
handler file e = reportFailure file (show e)
|
||||
reportFailure file str = do
|
||||
|
@ -243,7 +234,7 @@ runFormatter sys format options files = do
|
|||
|
||||
process :: FilePath -> IO Status
|
||||
process filename = do
|
||||
input <- siReadFile sys Nothing filename
|
||||
input <- siReadFile sys filename
|
||||
either (reportFailure filename) check input
|
||||
where
|
||||
check contents = do
|
||||
|
@ -259,9 +250,9 @@ runFormatter sys format options files = do
|
|||
else SomeProblems
|
||||
|
||||
parseEnum name value list =
|
||||
case lookup value list of
|
||||
Just value -> return value
|
||||
Nothing -> do
|
||||
case filter ((== value) . fst) list of
|
||||
[(name, value)] -> return value
|
||||
[] -> do
|
||||
printErr $ "Unknown value for --" ++ name ++ ". " ++
|
||||
"Valid options are: " ++ (intercalate ", " $ map fst list)
|
||||
throwError SupportFailure
|
||||
|
@ -374,11 +365,6 @@ parseOption flag options =
|
|||
}
|
||||
}
|
||||
|
||||
Flag "rcfile" str -> do
|
||||
return options {
|
||||
rcfile = Just str
|
||||
}
|
||||
|
||||
Flag "enable" value ->
|
||||
let cs = checkSpec options in return options {
|
||||
checkSpec = cs {
|
||||
|
@ -386,14 +372,6 @@ parseOption flag options =
|
|||
}
|
||||
}
|
||||
|
||||
Flag "extended-analysis" str -> do
|
||||
value <- parseBool str
|
||||
return options {
|
||||
checkSpec = (checkSpec options) {
|
||||
csExtendedAnalysis = Just value
|
||||
}
|
||||
}
|
||||
|
||||
-- This flag is handled specially in 'process'
|
||||
Flag "format" _ -> return options
|
||||
|
||||
|
@ -411,20 +389,11 @@ parseOption flag options =
|
|||
throwError SyntaxFailure
|
||||
return (Prelude.read num :: Integer)
|
||||
|
||||
parseBool str = do
|
||||
case str of
|
||||
"true" -> return True
|
||||
"false" -> return False
|
||||
_ -> do
|
||||
printErr $ "Invalid boolean, expected true/false: " ++ str
|
||||
throwError SyntaxFailure
|
||||
|
||||
ioInterface :: Options -> [FilePath] -> IO (SystemInterface IO)
|
||||
ioInterface options files = do
|
||||
inputs <- mapM normalize files
|
||||
cache <- newIORef emptyCache
|
||||
configCache <- newIORef ("", Nothing)
|
||||
return (newSystemInterface :: SystemInterface IO) {
|
||||
return SystemInterface {
|
||||
siReadFile = get cache inputs,
|
||||
siFindSource = findSourceFile inputs (sourcePaths options),
|
||||
siGetConfig = getConfig configCache
|
||||
|
@ -433,14 +402,14 @@ ioInterface options files = do
|
|||
emptyCache :: Map.Map FilePath String
|
||||
emptyCache = Map.empty
|
||||
|
||||
get cache inputs rcSuggestsExternal file = do
|
||||
get cache inputs file = do
|
||||
map <- readIORef cache
|
||||
case Map.lookup file map of
|
||||
Just x -> return $ Right x
|
||||
Nothing -> fetch cache inputs rcSuggestsExternal file
|
||||
Nothing -> fetch cache inputs file
|
||||
|
||||
fetch cache inputs rcSuggestsExternal file = do
|
||||
ok <- allowable rcSuggestsExternal inputs file
|
||||
fetch cache inputs file = do
|
||||
ok <- allowable inputs file
|
||||
if ok
|
||||
then (do
|
||||
(contents, shouldCache) <- inputFile file
|
||||
|
@ -448,16 +417,13 @@ ioInterface options files = do
|
|||
modifyIORef cache $ Map.insert file contents
|
||||
return $ Right contents
|
||||
) `catch` handler
|
||||
else
|
||||
if rcSuggestsExternal == Just False
|
||||
then return $ Left (file ++ " was not specified as input, and external files were disabled via directive.")
|
||||
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
|
||||
handler :: IOException -> IO (Either ErrorMessage String)
|
||||
handler ex = return . Left $ show ex
|
||||
|
||||
allowable rcSuggestsExternal inputs x =
|
||||
if fromMaybe (externalSources options) rcSuggestsExternal
|
||||
allowable inputs x =
|
||||
if externalSources options
|
||||
then return True
|
||||
else do
|
||||
path <- normalize x
|
||||
|
@ -469,33 +435,18 @@ ioInterface options files = do
|
|||
fallback :: FilePath -> IOException -> IO FilePath
|
||||
fallback path _ = return path
|
||||
|
||||
|
||||
-- Returns the name and contents of .shellcheckrc for the given file
|
||||
getConfig cache filename =
|
||||
case rcfile options of
|
||||
Just file -> do
|
||||
-- We have a specified rcfile. Ignore normal rcfile resolution.
|
||||
(path, result) <- readIORef cache
|
||||
if path == "/"
|
||||
then return result
|
||||
else do
|
||||
result <- readConfig file
|
||||
when (isNothing result) $
|
||||
hPutStrLn stderr $ "Warning: unable to read --rcfile " ++ file
|
||||
writeIORef cache ("/", result)
|
||||
return result
|
||||
|
||||
Nothing -> 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
|
||||
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
|
||||
|
@ -533,7 +484,7 @@ ioInterface options files = do
|
|||
where
|
||||
handler :: FilePath -> IOException -> IO (String, Bool)
|
||||
handler file err = do
|
||||
hPutStrLn stderr $ file ++ ": " ++ show err
|
||||
putStrLn $ file ++ ": " ++ show err
|
||||
return ("", True)
|
||||
|
||||
andM a b arg = do
|
||||
|
@ -546,7 +497,7 @@ ioInterface options files = do
|
|||
b <- p x
|
||||
if b then pure (Just x) else acc
|
||||
|
||||
findSourceFile inputs sourcePathFlag currentScript rcSuggestsExternal sourcePathAnnotation original =
|
||||
findSourceFile inputs sourcePathFlag currentScript sourcePathAnnotation original =
|
||||
if isAbsolute original
|
||||
then
|
||||
let (_, relative) = splitDrive original
|
||||
|
@ -555,7 +506,7 @@ ioInterface options files = do
|
|||
find original original
|
||||
where
|
||||
find filename deflt = do
|
||||
sources <- findM ((allowable rcSuggestsExternal inputs) `andM` doesFileExist) $
|
||||
sources <- findM ((allowable inputs) `andM` doesFileExist) $
|
||||
(adjustPath filename):(map ((</> filename) . adjustPath) $ sourcePathFlag ++ sourcePathAnnotation)
|
||||
case sources of
|
||||
Nothing -> return deflt
|
||||
|
|
|
@ -16,14 +16,14 @@ description: |
|
|||
advanced user's otherwise working script to fail under future
|
||||
circumstances.
|
||||
|
||||
By default ShellCheck can only check non-hidden files under /home, to make
|
||||
By default ShellCheck can only check non-hidden files under /home, to make
|
||||
ShellCheck be able to check files under /media and /run/media you must
|
||||
connect it to the `removable-media` interface manually:
|
||||
|
||||
# snap connect shellcheck:removable-media
|
||||
|
||||
|
||||
version: git
|
||||
base: core20
|
||||
base: core18
|
||||
grade: stable
|
||||
confinement: strict
|
||||
|
||||
|
@ -40,16 +40,16 @@ parts:
|
|||
source: .
|
||||
build-packages:
|
||||
- cabal-install
|
||||
stage-packages:
|
||||
- libatomic1
|
||||
- squid
|
||||
override-build: |
|
||||
# Give ourselves enough memory to build
|
||||
dd if=/dev/zero of=/tmp/swap bs=1M count=2000
|
||||
mkswap /tmp/swap
|
||||
swapon /tmp/swap
|
||||
|
||||
# See comments in .snapsquid.conf
|
||||
[ "$http_proxy" ] && {
|
||||
squid3 -f .snapsquid.conf
|
||||
export http_proxy="http://localhost:8888"
|
||||
sleep 3
|
||||
}
|
||||
cabal sandbox init
|
||||
cabal update
|
||||
cabal update || cat /var/log/squid/*
|
||||
cabal install -j
|
||||
|
||||
install -d $SNAPCRAFT_PART_INSTALL/usr/bin
|
||||
|
|
|
@ -45,7 +45,6 @@ data InnerToken t =
|
|||
| Inner_TA_Variable String [t]
|
||||
| Inner_TA_Expansion [t]
|
||||
| Inner_TA_Sequence [t]
|
||||
| Inner_TA_Parenthesis t
|
||||
| Inner_TA_Trinary t t t
|
||||
| Inner_TA_Unary String t
|
||||
| Inner_TC_And ConditionType String t t
|
||||
|
@ -138,11 +137,11 @@ data InnerToken t =
|
|||
| Inner_T_WhileExpression [t] [t]
|
||||
| Inner_T_Annotation [Annotation] t
|
||||
| Inner_T_Pipe String
|
||||
| Inner_T_CoProc (Maybe Token) t
|
||||
| Inner_T_CoProc (Maybe String) t
|
||||
| Inner_T_CoProcBody t
|
||||
| Inner_T_Include t
|
||||
| Inner_T_SourceCommand t t
|
||||
| Inner_T_BatsTest String t
|
||||
| Inner_T_BatsTest t t
|
||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||
|
||||
data Annotation =
|
||||
|
@ -151,8 +150,6 @@ data Annotation =
|
|||
| SourceOverride String
|
||||
| ShellOverride String
|
||||
| SourcePath String
|
||||
| ExternalSources Bool
|
||||
| ExtendedAnalysis Bool
|
||||
deriving (Show, Eq)
|
||||
data ConditionType = DoubleBracket | SingleBracket deriving (Show, Eq)
|
||||
|
||||
|
@ -206,7 +203,6 @@ pattern T_Annotation id anns t = OuterToken id (Inner_T_Annotation anns t)
|
|||
pattern T_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
||||
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||
pattern TA_Parenthesis id t = OuterToken id (Inner_TA_Parenthesis t)
|
||||
pattern T_Assignment id mode var indices value = OuterToken id (Inner_T_Assignment mode var indices value)
|
||||
pattern TA_Trinary id t1 t2 t3 = OuterToken id (Inner_TA_Trinary t1 t2 t3)
|
||||
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||
|
@ -259,7 +255,7 @@ pattern T_Subshell id l = OuterToken id (Inner_T_Subshell l)
|
|||
pattern T_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
||||
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression c l)
|
||||
|
||||
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, TA_Parenthesis, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
||||
{-# COMPLETE T_AND_IF, T_Bang, T_Case, TC_Empty, T_CLOBBER, T_DGREAT, T_DLESS, T_DLESSDASH, T_Do, T_DollarSingleQuoted, T_Done, T_DSEMI, T_Elif, T_Else, T_EOF, T_Esac, T_Fi, T_For, T_Glob, T_GREATAND, T_Greater, T_If, T_In, T_Lbrace, T_Less, T_LESSAND, T_LESSGREAT, T_Literal, T_Lparen, T_NEWLINE, T_OR_IF, T_ParamSubSpecialChar, T_Pipe, T_Rbrace, T_Rparen, T_Select, T_Semi, T_SingleQuoted, T_Then, T_UnparsedIndex, T_Until, T_While, TA_Assignment, TA_Binary, TA_Expansion, T_AndIf, T_Annotation, T_Arithmetic, T_Array, TA_Sequence, T_Assignment, TA_Trinary, TA_Unary, TA_Variable, T_Backgrounded, T_Backticked, T_Banged, T_BatsTest, T_BraceExpansion, T_BraceGroup, TC_And, T_CaseExpression, TC_Binary, TC_Group, TC_Nullary, T_Condition, T_CoProcBody, T_CoProc, TC_Or, TC_Unary, T_DollarArithmetic, T_DollarBraceCommandExpansion, T_DollarBraced, T_DollarBracket, T_DollarDoubleQuoted, T_DollarExpansion, T_DoubleQuoted, T_Extglob, T_FdRedirect, T_ForArithmetic, T_ForIn, T_Function, T_HereDoc, T_HereString, T_IfExpression, T_Include, T_IndexedElement, T_IoDuplicate, T_IoFile, T_NormalWord, T_OrIf, T_Pipeline, T_ProcSub, T_Redirecting, T_Script, T_SelectIn, T_SimpleCommand, T_SourceCommand, T_Subshell, T_UntilExpression, T_WhileExpression #-}
|
||||
|
||||
instance Eq Token where
|
||||
OuterToken _ a == OuterToken _ b = a == b
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2021 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -21,7 +21,6 @@
|
|||
module ShellCheck.ASTLib where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad.Writer
|
||||
|
@ -31,7 +30,6 @@ import Data.Functor
|
|||
import Data.Functor.Identity
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Map as Map
|
||||
import Numeric (showHex)
|
||||
|
||||
|
@ -61,28 +59,10 @@ willSplit x =
|
|||
T_NormalWord _ l -> any willSplit l
|
||||
_ -> False
|
||||
|
||||
isGlob t = case t of
|
||||
T_Extglob {} -> True
|
||||
T_Glob {} -> True
|
||||
T_NormalWord _ l -> any isGlob l || hasSplitRange l
|
||||
_ -> False
|
||||
where
|
||||
-- foo[x${var}y] gets parsed as foo,[,x,$var,y],
|
||||
-- so check if there's such an interval
|
||||
hasSplitRange l =
|
||||
let afterBracket = dropWhile (not . isHalfOpenRange) l
|
||||
in any isClosingRange afterBracket
|
||||
|
||||
isHalfOpenRange t =
|
||||
case t of
|
||||
T_Literal _ "[" -> True
|
||||
_ -> False
|
||||
|
||||
isClosingRange t =
|
||||
case t of
|
||||
T_Literal _ str -> ']' `elem` str
|
||||
_ -> False
|
||||
|
||||
isGlob T_Extglob {} = True
|
||||
isGlob T_Glob {} = True
|
||||
isGlob (T_NormalWord _ l) = any isGlob l
|
||||
isGlob _ = False
|
||||
|
||||
-- Is this shell word a constant?
|
||||
isConstant token =
|
||||
|
@ -140,7 +120,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
|||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||
flag (x, _) = [ (x, "") ]
|
||||
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
|
||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
||||
|
||||
-- Get all flags in a GNU way, up until --
|
||||
getAllFlags :: Token -> [(Token, String)]
|
||||
|
@ -158,10 +138,9 @@ isFlag token =
|
|||
_ -> False
|
||||
|
||||
-- Is this token a flag where the - is unquoted?
|
||||
isUnquotedFlag token =
|
||||
case getLeadingUnquotedString token of
|
||||
Just ('-':_) -> True
|
||||
_ -> False
|
||||
isUnquotedFlag token = fromMaybe False $ do
|
||||
str <- getLeadingUnquotedString token
|
||||
return $ "-" `isPrefixOf` str
|
||||
|
||||
-- getGnuOpts "erd:u:" will parse a list of arguments tokens like `read`
|
||||
-- -re -d : -u 3 bar
|
||||
|
@ -249,39 +228,6 @@ getOpts (gnu, arbitraryLongOpts) string longopts args = process args
|
|||
|
||||
listToArgs = map (\x -> ("", (x, x)))
|
||||
|
||||
|
||||
-- Generic getOpts that doesn't rely on a format string, but may also be inaccurate.
|
||||
-- This provides a best guess interpretation instead of failing when new options are added.
|
||||
--
|
||||
-- "--" is treated as end of arguments
|
||||
-- "--anything[=foo]" is treated as a long option without argument
|
||||
-- "-any" is treated as -a -n -y, with the next arg as an option to -y unless it starts with -
|
||||
-- anything else is an argument
|
||||
getGenericOpts :: [Token] -> [(String, (Token, Token))]
|
||||
getGenericOpts = process
|
||||
where
|
||||
process (token:rest) =
|
||||
case getLiteralStringDef "\0" token of
|
||||
"--" -> map (\c -> ("", (c,c))) rest
|
||||
'-':'-':word -> (takeWhile (`notElem` "\0=") word, (token, token)) : process rest
|
||||
'-':optString ->
|
||||
let opts = takeWhile (/= '\0') optString
|
||||
in
|
||||
case rest of
|
||||
next:_ | "-" `isPrefixOf` getLiteralStringDef "\0" next ->
|
||||
map (\c -> ([c], (token, token))) opts ++ process rest
|
||||
next:remainder ->
|
||||
case reverse opts of
|
||||
last:initial ->
|
||||
map (\c -> ([c], (token, token))) (reverse initial)
|
||||
++ [([last], (token, next))]
|
||||
++ process remainder
|
||||
[] -> process remainder
|
||||
[] -> map (\c -> ([c], (token, token))) opts
|
||||
_ -> ("", (token, token)) : process rest
|
||||
process [] = []
|
||||
|
||||
|
||||
-- Is this an expansion of multiple items of an array?
|
||||
isArrayExpansion (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
|
@ -290,14 +236,14 @@ isArrayExpansion (T_DollarBraced _ _ l) =
|
|||
isArrayExpansion _ = False
|
||||
|
||||
-- Is it possible that this arg becomes multiple args?
|
||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f False t
|
||||
mayBecomeMultipleArgs t = willBecomeMultipleArgs t || f t
|
||||
where
|
||||
f quoted (T_DollarBraced _ _ l) =
|
||||
f (T_DollarBraced _ _ l) =
|
||||
let string = concat $ oversimplify l in
|
||||
not quoted || "!" `isPrefixOf` string
|
||||
f quoted (T_DoubleQuoted _ parts) = any (f True) parts
|
||||
f quoted (T_NormalWord _ parts) = any (f quoted) parts
|
||||
f _ _ = False
|
||||
"!" `isPrefixOf` string
|
||||
f (T_DoubleQuoted _ parts) = any f parts
|
||||
f (T_NormalWord _ parts) = any f parts
|
||||
f _ = False
|
||||
|
||||
-- Is it certain that this word will becomes multiple words?
|
||||
willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
||||
|
@ -305,6 +251,7 @@ willBecomeMultipleArgs t = willConcatInAssignment t || f t
|
|||
f T_Extglob {} = True
|
||||
f T_Glob {} = True
|
||||
f T_BraceExpansion {} = True
|
||||
f (T_DoubleQuoted _ parts) = any f parts
|
||||
f (T_NormalWord _ parts) = any f parts
|
||||
f _ = False
|
||||
|
||||
|
@ -372,21 +319,6 @@ getGlobOrLiteralString = getLiteralStringExt f
|
|||
f (T_Glob _ str) = return str
|
||||
f _ = Nothing
|
||||
|
||||
|
||||
prop_getLiteralString1 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x01") == Just "\1"
|
||||
prop_getLiteralString2 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xyz") == Just "\\xyz"
|
||||
prop_getLiteralString3 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1") == Just "\x1"
|
||||
prop_getLiteralString4 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x1y") == Just "\x1y"
|
||||
prop_getLiteralString5 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\xy") == Just "\\xy"
|
||||
prop_getLiteralString6 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\x") == Just "\\x"
|
||||
prop_getLiteralString7 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1x") == Just "\1x"
|
||||
prop_getLiteralString8 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12x") == Just "\o12x"
|
||||
prop_getLiteralString9 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123x") == Just "\o123x"
|
||||
prop_getLiteralString10 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1234") == Just "\o123\&4"
|
||||
prop_getLiteralString11 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\1") == Just "\1"
|
||||
prop_getLiteralString12 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\12") == Just "\o12"
|
||||
prop_getLiteralString13 = getLiteralString (T_DollarSingleQuoted (Id 0) "\\123") == Just "\o123"
|
||||
|
||||
-- Maybe get the literal value of a token, using a custom function
|
||||
-- to map unrecognized Tokens into strings.
|
||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||
|
@ -419,15 +351,14 @@ getLiteralStringExt more = g
|
|||
'\\' -> '\\' : rest
|
||||
'x' ->
|
||||
case cs of
|
||||
(x:y:more) | isHexDigit x && isHexDigit y ->
|
||||
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
|
||||
(x:more) | isHexDigit x ->
|
||||
chr (digitToInt x) : decodeEscapes more
|
||||
more -> '\\' : 'x' : decodeEscapes more
|
||||
(x:y:more) ->
|
||||
if isHexDigit x && isHexDigit y
|
||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
||||
else '\\':c:rest
|
||||
_ | isOctDigit c ->
|
||||
let (digits, more) = spanMax isOctDigit 3 (c:cs)
|
||||
num = (parseOct digits) `mod` 256
|
||||
in (chr num) : decodeEscapes more
|
||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
||||
num = parseOct digits
|
||||
in (if num < 256 then chr num else '?') : rest
|
||||
_ -> '\\' : c : rest
|
||||
where
|
||||
rest = decodeEscapes cs
|
||||
|
@ -435,23 +366,12 @@ getLiteralStringExt more = g
|
|||
where
|
||||
f n "" = n
|
||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
||||
spanMax f n list =
|
||||
let (first, second) = span f list
|
||||
(prefix, suffix) = splitAt n first
|
||||
in
|
||||
(prefix, suffix ++ second)
|
||||
decodeEscapes (c:cs) = c : decodeEscapes cs
|
||||
decodeEscapes [] = []
|
||||
|
||||
-- Is this token a string literal?
|
||||
isLiteral t = isJust $ getLiteralString t
|
||||
|
||||
-- Is this token a string literal number?
|
||||
isLiteralNumber t = fromMaybe False $ do
|
||||
s <- getLiteralString t
|
||||
guard $ all isDigit s
|
||||
return True
|
||||
|
||||
-- Escape user data for messages.
|
||||
-- Messages generally avoid repeating user data, but sometimes it's helpful.
|
||||
e4m = escapeForMessage
|
||||
|
@ -766,8 +686,8 @@ prop_executableFromShebang6 = executableFromShebang "/usr/bin/env --split-string
|
|||
prop_executableFromShebang7 = executableFromShebang "/usr/bin/env --split-string bash -x" == "bash"
|
||||
prop_executableFromShebang8 = executableFromShebang "/usr/bin/env --split-string foo=bar bash -x" == "bash"
|
||||
prop_executableFromShebang9 = executableFromShebang "/usr/bin/env foo=bar dash" == "dash"
|
||||
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "busybox sh"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "busybox ash"
|
||||
prop_executableFromShebang10 = executableFromShebang "/bin/busybox sh" == "ash"
|
||||
prop_executableFromShebang11 = executableFromShebang "/bin/busybox ash" == "ash"
|
||||
|
||||
-- Get the shell executable from a string like '/usr/bin/env bash'
|
||||
executableFromShebang :: String -> String
|
||||
|
@ -784,8 +704,7 @@ executableFromShebang = shellFor
|
|||
[x] -> basename x
|
||||
(first:second:args) | basename first == "busybox" ->
|
||||
case basename second of
|
||||
"sh" -> "busybox sh"
|
||||
"ash" -> "busybox ash"
|
||||
"sh" -> "ash" -- busybox sh is ash
|
||||
x -> x
|
||||
(first:args) | basename first == "env" ->
|
||||
fromEnvArgs args
|
||||
|
@ -795,132 +714,5 @@ executableFromShebang = shellFor
|
|||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||
skipFlags = dropWhile ("-" `isPrefixOf`)
|
||||
|
||||
|
||||
-- Determining if a name is a variable
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
isSpecialVariableChar = (`elem` "*@#?-$!")
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
|
||||
-- Get the variable name from an expansion like ${var:-foo}
|
||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10 = getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11 = getBracedReference "!os*" == ""
|
||||
prop_getBracedReference11b = getBracedReference "!os@" == ""
|
||||
prop_getBracedReference12 = getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13 = getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) | c `elem` "!#" = rest
|
||||
dropPrefix cs = cs
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) | isSpecialVariableChar c = return [c]
|
||||
getSpecial _ = fail "empty or not special"
|
||||
|
||||
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||
guard $ isVariableChar next -- e.g. ${!@}
|
||||
first <- find (not . isVariableChar) rest
|
||||
guard $ first `elem` "*?@"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
-- Get the variable modifier like /a/b in ${var/a/b}
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
prop_getBracedModifier4 = getBracedModifier "foo[@]@Q" == "[@]@Q"
|
||||
prop_getBracedModifier5 = getBracedModifier "@@Q" == "@Q"
|
||||
getBracedModifier s = headOrDefault "" $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Get the variables from indices like ["x", "y"] in ${var[x+y+1]}
|
||||
prop_getIndexReferences1 = getIndexReferences "var[x+y+1]" == ["x", "y"]
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
index:_ <- matchRegex re s
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
_:offsets:_ <- matchRegex re mods
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
|
||||
-- Returns whether a token is a parameter expansion without any modifiers.
|
||||
-- True for $var ${var} $1 $#
|
||||
-- False for ${#var} ${var[x]} ${var:-0}
|
||||
isUnmodifiedParameterExpansion t =
|
||||
case t of
|
||||
T_DollarBraced _ False _ -> True
|
||||
T_DollarBraced _ _ list ->
|
||||
let str = concat $ oversimplify list
|
||||
in getBracedReference str == str
|
||||
_ -> False
|
||||
|
||||
-- Return the referenced variable if (and only if) it's an unmodified parameter expansion.
|
||||
getUnmodifiedParameterExpansion t =
|
||||
case t of
|
||||
T_DollarBraced _ _ list -> do
|
||||
let str = concat $ oversimplify list
|
||||
guard $ getBracedReference str == str
|
||||
return str
|
||||
_ -> Nothing
|
||||
|
||||
--- A list of the element and all its parents up to the root node.
|
||||
getPath tree = NE.unfoldr $ \t -> (t, Map.lookup (getId t) tree)
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
|
||||
getEnableDirectives root =
|
||||
case root of
|
||||
T_Annotation _ list _ -> [s | EnableComment s <- list]
|
||||
_ -> []
|
||||
|
||||
getExtendedAnalysisDirective :: Token -> Maybe Bool
|
||||
getExtendedAnalysisDirective root =
|
||||
case root of
|
||||
T_Annotation _ list _ -> listToMaybe $ [s | ExtendedAnalysis s <- list]
|
||||
_ -> Nothing
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -25,7 +25,6 @@ import ShellCheck.Interface
|
|||
import Data.List
|
||||
import Data.Monoid
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ControlFlow
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
|
||||
|
@ -35,21 +34,19 @@ analyzeScript :: AnalysisSpec -> AnalysisResult
|
|||
analyzeScript spec = newAnalysisResult {
|
||||
arComments =
|
||||
filterByAnnotation spec params . nub $
|
||||
runChecker params (checkers spec params)
|
||||
runAnalytics spec
|
||||
++ runChecker params (checkers spec params)
|
||||
}
|
||||
where
|
||||
params = makeParameters spec
|
||||
|
||||
checkers spec params = mconcat $ map ($ params) [
|
||||
ShellCheck.Analytics.checker spec,
|
||||
ShellCheck.Checks.Commands.checker spec,
|
||||
ShellCheck.Checks.ControlFlow.checker spec,
|
||||
ShellCheck.Checks.Custom.checker,
|
||||
ShellCheck.Checks.ShellSupport.checker
|
||||
]
|
||||
|
||||
optionalChecks = mconcat $ [
|
||||
ShellCheck.Analytics.optionalChecks,
|
||||
ShellCheck.Checks.Commands.optionalChecks,
|
||||
ShellCheck.Checks.ControlFlow.optionalChecks
|
||||
ShellCheck.Checks.Commands.optionalChecks
|
||||
]
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -23,16 +23,13 @@ module ShellCheck.AnalyzerLib where
|
|||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import qualified ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Arrow (first)
|
||||
import Control.DeepSeq
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.State
|
||||
|
@ -41,7 +38,6 @@ import Data.Char
|
|||
import Data.List
|
||||
import Data.Maybe
|
||||
import Data.Semigroup
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
|
@ -83,18 +79,10 @@ composeAnalyzers f g x = f x >> g x
|
|||
data Parameters = Parameters {
|
||||
-- Whether this script has the 'lastpipe' option set/default.
|
||||
hasLastpipe :: Bool,
|
||||
-- Whether this script has the 'inherit_errexit' option set/default.
|
||||
hasInheritErrexit :: Bool,
|
||||
-- Whether this script has 'set -e' anywhere.
|
||||
hasSetE :: Bool,
|
||||
-- Whether this script has 'set -o pipefail' anywhere.
|
||||
hasPipefail :: Bool,
|
||||
-- Whether this script has 'shopt -s execfail' anywhere.
|
||||
hasExecfail :: Bool,
|
||||
-- A linear (bad) analysis of data flow
|
||||
variableFlow :: [StackData],
|
||||
-- A map from Id to Token
|
||||
idMap :: Map.Map Id Token,
|
||||
-- A map from Id to parent Token
|
||||
parentMap :: Map.Map Id Token,
|
||||
-- The shell type, such as Bash or Ksh
|
||||
|
@ -104,9 +92,7 @@ data Parameters = Parameters {
|
|||
-- The root node of the AST
|
||||
rootNode :: Token,
|
||||
-- map from token id to start and end position
|
||||
tokenPositions :: Map.Map Id (Position, Position),
|
||||
-- Result from Control Flow Graph analysis (including data flow analysis)
|
||||
cfgAnalysis :: Maybe CF.CFGAnalysis
|
||||
tokenPositions :: Map.Map Id (Position, Position)
|
||||
} deriving (Show)
|
||||
|
||||
-- TODO: Cache results of common AST ops here
|
||||
|
@ -156,7 +142,7 @@ producesComments c s = do
|
|||
prRoot pr
|
||||
let spec = defaultSpec pr
|
||||
let params = makeParameters spec
|
||||
return . not . null $ filterByAnnotation spec params $ runChecker params c
|
||||
return . not . null $ runChecker params c
|
||||
|
||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||
makeComment severity id code note =
|
||||
|
@ -181,8 +167,6 @@ errWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m (
|
|||
errWithFix = addCommentWithFix ErrorC
|
||||
warnWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
warnWithFix = addCommentWithFix WarningC
|
||||
infoWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
infoWithFix = addCommentWithFix InfoC
|
||||
styleWithFix :: MonadWriter [TokenComment] m => Id -> Code -> String -> Fix -> m ()
|
||||
styleWithFix = addCommentWithFix StyleC
|
||||
|
||||
|
@ -194,58 +178,28 @@ makeCommentWithFix :: Severity -> Id -> Code -> String -> Fix -> TokenComment
|
|||
makeCommentWithFix severity id code str fix =
|
||||
let comment = makeComment severity id code str
|
||||
withFix = comment {
|
||||
-- If fix is empty, pretend it wasn't there.
|
||||
tcFix = if null (fixReplacements fix) then Nothing else Just fix
|
||||
tcFix = Just fix
|
||||
}
|
||||
in force withFix
|
||||
|
||||
-- makeParameters :: CheckSpec -> Parameters
|
||||
makeParameters spec = params
|
||||
where
|
||||
extendedAnalysis = fromMaybe True $ msum [asExtendedAnalysis spec, getExtendedAnalysisDirective root]
|
||||
params = Parameters {
|
||||
makeParameters spec =
|
||||
let params = Parameters {
|
||||
rootNode = root,
|
||||
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
||||
hasSetE = containsSetE root,
|
||||
hasLastpipe =
|
||||
case shellType params of
|
||||
Bash -> isOptionSet "lastpipe" root
|
||||
Bash -> containsLastpipe root
|
||||
Dash -> False
|
||||
BusyboxSh -> False
|
||||
Sh -> False
|
||||
Ksh -> True,
|
||||
hasInheritErrexit =
|
||||
case shellType params of
|
||||
Bash -> isOptionSet "inherit_errexit" root
|
||||
Dash -> True
|
||||
BusyboxSh -> True
|
||||
Sh -> True
|
||||
Ksh -> False,
|
||||
hasPipefail =
|
||||
case shellType params of
|
||||
Bash -> isOptionSet "pipefail" root
|
||||
Dash -> True
|
||||
BusyboxSh -> isOptionSet "pipefail" root
|
||||
Sh -> True
|
||||
Ksh -> isOptionSet "pipefail" root,
|
||||
hasExecfail =
|
||||
case shellType params of
|
||||
Bash -> isOptionSet "execfail" root
|
||||
_ -> False,
|
||||
|
||||
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
||||
idMap = getTokenMap root,
|
||||
parentMap = getParentTree root,
|
||||
variableFlow = getVariableFlow params root,
|
||||
tokenPositions = asTokenPositions spec,
|
||||
cfgAnalysis = do
|
||||
guard extendedAnalysis
|
||||
return $ CF.analyzeControlFlow cfParams root
|
||||
}
|
||||
cfParams = CF.CFGParameters {
|
||||
CF.cfLastpipe = hasLastpipe params,
|
||||
CF.cfPipefail = hasPipefail params
|
||||
}
|
||||
root = asScript spec
|
||||
tokenPositions = asTokenPositions spec
|
||||
} in params
|
||||
where root = asScript spec
|
||||
|
||||
|
||||
-- Does this script mention 'set -e' anywhere?
|
||||
|
@ -262,30 +216,18 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
|||
_ -> False
|
||||
re = mkRegex "[[:space:]]-[^-]*e"
|
||||
|
||||
|
||||
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
||||
where
|
||||
isPipefail t =
|
||||
case t of
|
||||
T_SimpleCommand {} ->
|
||||
t `isUnqualifiedCommand` "set" &&
|
||||
(opt `elem` oversimplify t ||
|
||||
"o" `elem` map snd (getAllFlags t))
|
||||
_ -> False
|
||||
|
||||
containsShopt shopt root =
|
||||
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
||||
-- Also used as a hack.
|
||||
containsLastpipe root =
|
||||
isNothing $ doAnalysis (guard . not . isShoptLastPipe) root
|
||||
where
|
||||
isShoptLastPipe t =
|
||||
case t of
|
||||
T_SimpleCommand {} ->
|
||||
t `isUnqualifiedCommand` "shopt" &&
|
||||
(shopt `elem` oversimplify t)
|
||||
("lastpipe" `elem` oversimplify t)
|
||||
_ -> False
|
||||
|
||||
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
|
||||
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
|
||||
|
||||
|
||||
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
||||
prop_determineShell1 = determineShellTest "#!/usr/bin/env ksh" == Ksh
|
||||
|
@ -298,8 +240,8 @@ prop_determineShell7 = determineShellTest "#! /bin/ash" == Dash
|
|||
prop_determineShell8 = determineShellTest' (Just Ksh) "#!/bin/sh" == Sh
|
||||
prop_determineShell9 = determineShellTest "#!/bin/env -S dash -x" == Dash
|
||||
prop_determineShell10 = determineShellTest "#!/bin/env --split-string= dash -x" == Dash
|
||||
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == BusyboxSh -- busybox sh is a specific shell, not posix sh
|
||||
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == BusyboxSh
|
||||
prop_determineShell11 = determineShellTest "#!/bin/busybox sh" == Dash -- busybox sh is a specific shell, not posix sh
|
||||
prop_determineShell12 = determineShellTest "#!/bin/busybox ash" == Dash
|
||||
|
||||
determineShellTest = determineShellTest' Nothing
|
||||
determineShellTest' fallbackShell = determineShell fallbackShell . fromJust . prRoot . pScript
|
||||
|
@ -345,16 +287,16 @@ isStrictlyQuoteFree = isQuoteFreeNode True
|
|||
isQuoteFree = isQuoteFreeNode False
|
||||
|
||||
|
||||
isQuoteFreeNode strict shell tree t =
|
||||
isQuoteFreeNode strict tree t =
|
||||
isQuoteFreeElement t ||
|
||||
(fromMaybe False $ msum $ map isQuoteFreeContext $ NE.tail $ getPath tree t)
|
||||
headOrDefault False (mapMaybe isQuoteFreeContext (drop 1 $ getPath tree t))
|
||||
where
|
||||
-- Is this node self-quoting in itself?
|
||||
isQuoteFreeElement t =
|
||||
case t of
|
||||
T_Assignment id _ _ _ _ -> assignmentIsQuoting id
|
||||
T_FdRedirect {} -> True
|
||||
_ -> False
|
||||
T_Assignment {} -> True
|
||||
T_FdRedirect {} -> True
|
||||
_ -> False
|
||||
|
||||
-- Are any subnodes inherently self-quoting?
|
||||
isQuoteFreeContext t =
|
||||
|
@ -364,7 +306,7 @@ isQuoteFreeNode strict shell tree t =
|
|||
TC_Binary _ DoubleBracket _ _ _ -> return True
|
||||
TA_Sequence {} -> return True
|
||||
T_Arithmetic {} -> return True
|
||||
T_Assignment id _ _ _ _ -> return $ assignmentIsQuoting id
|
||||
T_Assignment {} -> return True
|
||||
T_Redirecting {} -> return False
|
||||
T_DoubleQuoted _ _ -> return True
|
||||
T_DollarDoubleQuoted _ _ -> return True
|
||||
|
@ -376,18 +318,6 @@ isQuoteFreeNode strict shell tree t =
|
|||
T_SelectIn {} -> return (not strict)
|
||||
_ -> Nothing
|
||||
|
||||
-- Check whether this assignment is self-quoting due to being a recognized
|
||||
-- assignment passed to a Declaration Utility. This will soon be required
|
||||
-- by POSIX: https://austingroupbugs.net/view.php?id=351
|
||||
assignmentIsQuoting id = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand id)
|
||||
shellParsesParamsAsAssignments = shell /= Sh
|
||||
|
||||
-- Is this assignment a parameter to a command like export/typeset/etc?
|
||||
isAssignmentParamToCommand id =
|
||||
case Map.lookup id tree of
|
||||
Just (T_SimpleCommand _ _ (_:args)) -> id `elem` (map getId args)
|
||||
_ -> False
|
||||
|
||||
-- Check if a token is a parameter to a certain command by name:
|
||||
-- Example: isParamTo (parentMap params) "sed" t
|
||||
isParamTo :: Map.Map Id Token -> String -> Token -> Bool
|
||||
|
@ -409,7 +339,7 @@ isParamTo tree cmd =
|
|||
-- Get the parent command (T_Redirecting) of a Token, if any.
|
||||
getClosestCommand :: Map.Map Id Token -> Token -> Maybe Token
|
||||
getClosestCommand tree t =
|
||||
findFirst findCommand $ NE.toList $ getPath tree t
|
||||
findFirst findCommand $ getPath tree t
|
||||
where
|
||||
findCommand t =
|
||||
case t of
|
||||
|
@ -423,7 +353,7 @@ getClosestCommandM t = do
|
|||
return $ getClosestCommand (parentMap params) t
|
||||
|
||||
-- Is the token used as a command name (the first word in a T_SimpleCommand)?
|
||||
usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
|
||||
usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
||||
where
|
||||
go currentId (T_NormalWord id [word]:rest)
|
||||
| currentId == getId word = go id rest
|
||||
|
@ -433,6 +363,12 @@ usedAsCommandName tree token = go (getId token) (NE.tail $ getPath tree token)
|
|||
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||
go _ _ = False
|
||||
|
||||
-- A list of the element and all its parents up to the root node.
|
||||
getPath tree t = t :
|
||||
case Map.lookup (getId t) tree of
|
||||
Nothing -> []
|
||||
Just parent -> getPath tree parent
|
||||
|
||||
-- Version of the above taking the map from the current context
|
||||
-- Todo: give this the name "getPath"
|
||||
getPathM t = do
|
||||
|
@ -440,9 +376,7 @@ getPathM t = do
|
|||
return $ getPath (parentMap params) t
|
||||
|
||||
isParentOf tree parent child =
|
||||
any (\t -> parentId == getId t) (getPath tree child)
|
||||
where
|
||||
parentId = getId parent
|
||||
elem (getId parent) . map getId $ getPath tree child
|
||||
|
||||
parents params = getPath (parentMap params)
|
||||
|
||||
|
@ -532,24 +466,30 @@ getModifiedVariables t =
|
|||
T_SimpleCommand {} ->
|
||||
getModifiedVariableCommand t
|
||||
|
||||
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
|
||||
[(t, v, name, DataString SourceInteger)]
|
||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
||||
[(t, v, name, DataString $ SourceFrom [v])]
|
||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||
return (t, t, name, DataString SourceInteger)
|
||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
||||
|
||||
T_BatsTest {} -> [
|
||||
(t, t, "lines", DataArray SourceExternal),
|
||||
(t, t, "status", DataString SourceInteger),
|
||||
(t, t, "output", DataString SourceExternal),
|
||||
(t, t, "stderr", DataString SourceExternal),
|
||||
(t, t, "stderr_lines", DataArray SourceExternal)
|
||||
(t, t, "output", DataString SourceExternal)
|
||||
]
|
||||
|
||||
-- Count [[ -v foo ]] as an "assignment".
|
||||
-- This is to prevent [ -v foo ] being unassigned or unused.
|
||||
TC_Unary id _ "-v" token -> maybeToList $ do
|
||||
str <- getVariableForTestDashV token
|
||||
TC_Unary id _ "-v" token -> do
|
||||
str <- fmap (takeWhile (/= '[')) $ -- Quoted index
|
||||
flip getLiteralStringExt token $ \x ->
|
||||
case x of
|
||||
T_Glob _ s -> return s -- Unquoted index
|
||||
_ -> []
|
||||
|
||||
guard . not . null $ str
|
||||
return (t, token, str, DataString SourceChecked)
|
||||
|
||||
TC_Unary _ _ "-n" token -> markAsChecked t token
|
||||
|
@ -565,12 +505,8 @@ getModifiedVariables t =
|
|||
T_FdRedirect _ ('{':var) op -> -- {foo}>&2 modifies foo
|
||||
[(t, t, takeWhile (/= '}') var, DataString SourceInteger) | not $ isClosingFileOp op]
|
||||
|
||||
T_CoProc _ Nothing _ ->
|
||||
[(t, t, "COPROC", DataArray SourceInteger)]
|
||||
|
||||
T_CoProc _ (Just token) _ -> do
|
||||
name <- maybeToList $ getLiteralString token
|
||||
[(t, t, name, DataArray SourceInteger)]
|
||||
T_CoProc _ name _ ->
|
||||
[(t, t, fromMaybe "COPROC" name, DataArray SourceInteger)]
|
||||
|
||||
--Points to 'for' rather than variable
|
||||
T_ForIn id str [] _ -> [(t, t, str, DataString SourceExternal)]
|
||||
|
@ -586,16 +522,24 @@ getModifiedVariables t =
|
|||
return (place, t, str, DataString SourceChecked)
|
||||
_ -> Nothing
|
||||
|
||||
isClosingFileOp op =
|
||||
case op of
|
||||
T_IoDuplicate _ (T_GREATAND _) "-" -> True
|
||||
T_IoDuplicate _ (T_LESSAND _) "-" -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||
case x of
|
||||
"declare" -> forDeclare
|
||||
"typeset" -> forDeclare
|
||||
|
||||
"export" -> if "f" `elem` flags
|
||||
then []
|
||||
else concatMap getReference rest
|
||||
"declare" -> if
|
||||
any (`elem` flags) ["x", "p"] &&
|
||||
(not $ any (`elem` flags) ["f", "F"])
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
"local" -> if "x" `elem` flags
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
|
@ -606,13 +550,6 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
||||
_ -> []
|
||||
where
|
||||
forDeclare =
|
||||
if
|
||||
any (`elem` flags) ["x", "p"] &&
|
||||
(not $ any (`elem` flags) ["f", "F"])
|
||||
then concatMap getReference rest
|
||||
else []
|
||||
|
||||
getReference t@(T_Assignment _ _ name _ value) = [(t, t, name)]
|
||||
getReference t@(T_NormalWord _ [T_Literal _ name]) | not ("-" `isPrefixOf` name) = [(t, t, name)]
|
||||
getReference _ = []
|
||||
|
@ -652,8 +589,8 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||
"export" ->
|
||||
if "f" `elem` flags then [] else concatMap getModifierParamString rest
|
||||
|
||||
"declare" -> forDeclare
|
||||
"typeset" -> forDeclare
|
||||
"declare" -> if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||
"typeset" -> declaredVars
|
||||
|
||||
"local" -> concatMap getModifierParamString rest
|
||||
"readonly" ->
|
||||
|
@ -665,7 +602,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||
return (base, base, "@", DataString $ SourceFrom params)
|
||||
|
||||
"printf" -> maybeToList $ getPrintfVariable rest
|
||||
"wait" -> maybeToList $ getWaitVariable rest
|
||||
|
||||
"mapfile" -> maybeToList $ getMapfileArray base rest
|
||||
"readarray" -> maybeToList $ getMapfileArray base rest
|
||||
|
@ -685,8 +621,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||
T_NormalWord id1 [T_DoubleQuoted id2 [T_Literal id3 (stripEquals s)]]
|
||||
stripEqualsFrom t = t
|
||||
|
||||
forDeclare = if any (`elem` flags) ["F", "f", "p"] then [] else declaredVars
|
||||
|
||||
declaredVars = concatMap (getModifierParam defaultType) rest
|
||||
where
|
||||
defaultType = if any (`elem` flags) ["a", "A"] then DataArray else DataString
|
||||
|
@ -725,15 +659,15 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||
_ -> return (t:fromMaybe [] (getSetParams rest))
|
||||
getSetParams [] = Nothing
|
||||
|
||||
getPrintfVariable list = getFlagAssignedVariable "v" (SourceFrom list) $ getBsdOpts "v:" list
|
||||
getWaitVariable list = getFlagAssignedVariable "p" SourceInteger $ return $ getGenericOpts list
|
||||
|
||||
getFlagAssignedVariable str dataSource maybeFlags = do
|
||||
flags <- maybeFlags
|
||||
(_, (flag, value)) <- find ((== str) . fst) flags
|
||||
variableName <- getLiteralStringExt (const $ return "!") value
|
||||
let (baseName, index) = span (/= '[') variableName
|
||||
return (base, value, baseName, (if null index then DataString else DataArray) dataSource)
|
||||
getPrintfVariable list = f $ map (\x -> (x, getLiteralString x)) list
|
||||
where
|
||||
f ((_, Just "-v") : (t, Just var) : _) = return (base, t, varName, varType $ SourceFrom list)
|
||||
where
|
||||
(varName, varType) = case elemIndex '[' var of
|
||||
Just i -> (take i var, DataArray)
|
||||
Nothing -> (var, DataString)
|
||||
f (_:rest) = f rest
|
||||
f [] = fail "not found"
|
||||
|
||||
-- mapfile has some curious syntax allowing flags plus 0..n variable names
|
||||
-- where only the first non-option one is used if any.
|
||||
|
@ -767,19 +701,24 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||
|
||||
getModifiedVariableCommand _ = []
|
||||
|
||||
-- Given a NormalWord like foo or foo[$bar], get foo.
|
||||
-- Primarily used to get references for [[ -v foo[bar] ]]
|
||||
getVariableForTestDashV :: Token -> Maybe String
|
||||
getVariableForTestDashV t = do
|
||||
str <- takeWhile ('[' /=) <$> getLiteralStringExt toStr t
|
||||
guard $ isVariableName str
|
||||
return str
|
||||
getIndexReferences s = fromMaybe [] $ do
|
||||
match <- matchRegex re s
|
||||
index <- match !!! 0
|
||||
return $ matchAllStrings variableNameRegex index
|
||||
where
|
||||
-- foo[bar] gets parsed with [bar] as a glob, so undo that
|
||||
toStr (T_Glob _ s) = return s
|
||||
-- Turn foo[$x] into foo[\0] so that we can get the constant array name
|
||||
-- in a non-constant expression (while filtering out foo$x[$y])
|
||||
toStr _ = return "\0"
|
||||
re = mkRegex "(\\[.*\\])"
|
||||
|
||||
prop_getOffsetReferences1 = getOffsetReferences ":bar" == ["bar"]
|
||||
prop_getOffsetReferences2 = getOffsetReferences ":bar:baz" == ["bar", "baz"]
|
||||
prop_getOffsetReferences3 = getOffsetReferences "[foo]:bar" == ["bar"]
|
||||
prop_getOffsetReferences4 = getOffsetReferences "[foo]:bar:baz" == ["bar", "baz"]
|
||||
getOffsetReferences mods = fromMaybe [] $ do
|
||||
-- if mods start with [, then drop until ]
|
||||
match <- matchRegex re mods
|
||||
offsets <- match !!! 1
|
||||
return $ matchAllStrings variableNameRegex offsets
|
||||
where
|
||||
re = mkRegex "^(\\[.+\\])? *:([^-=?+].*)"
|
||||
|
||||
getReferencedVariables parents t =
|
||||
case t of
|
||||
|
@ -798,7 +737,7 @@ getReferencedVariables parents t =
|
|||
TC_Unary id _ "-v" token -> getIfReference t token
|
||||
TC_Unary id _ "-R" token -> getIfReference t token
|
||||
TC_Binary id DoubleBracket op lhs rhs ->
|
||||
if isDereferencingBinaryOp op
|
||||
if isDereferencing op
|
||||
then concatMap (getIfReference t) [lhs, rhs]
|
||||
else []
|
||||
|
||||
|
@ -827,15 +766,16 @@ getReferencedVariables parents t =
|
|||
T_Glob _ s -> return s -- Also when parsed as globs
|
||||
_ -> []
|
||||
|
||||
getIfReference context token = maybeToList $ do
|
||||
str <- getVariableForTestDashV token
|
||||
getIfReference context token = do
|
||||
str@(h:_) <- getLiteralStringExt literalizer token
|
||||
when (isDigit h) $ fail "is a number"
|
||||
return (context, token, getBracedReference str)
|
||||
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this NE.:| TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
isDereferencing = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
|
||||
isDereferencingBinaryOp = (`elem` ["-eq", "-ne", "-lt", "-le", "-gt", "-ge"])
|
||||
isArithmeticAssignment t = case getPath parents t of
|
||||
this: TA_Assignment _ "=" lhs _ :_ -> lhs == t
|
||||
_ -> False
|
||||
|
||||
dataTypeFrom defaultType v = (case v of T_Array {} -> DataArray; _ -> defaultType) $ SourceFrom [v]
|
||||
|
||||
|
@ -859,6 +799,16 @@ isConfusedGlobRegex ('*':_) = True
|
|||
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
||||
isConfusedGlobRegex _ = False
|
||||
|
||||
isVariableStartChar x = x == '_' || isAsciiLower x || isAsciiUpper x
|
||||
isVariableChar x = isVariableStartChar x || isDigit x
|
||||
variableNameRegex = mkRegex "[_a-zA-Z][_a-zA-Z0-9]*"
|
||||
|
||||
prop_isVariableName1 = isVariableName "_fo123"
|
||||
prop_isVariableName2 = not $ isVariableName "4"
|
||||
prop_isVariableName3 = not $ isVariableName "test: "
|
||||
isVariableName (x:r) = isVariableStartChar x && all isVariableChar r
|
||||
isVariableName _ = False
|
||||
|
||||
getVariablesFromLiteralToken token =
|
||||
getVariablesFromLiteral (getLiteralStringDef " " token)
|
||||
|
||||
|
@ -871,6 +821,67 @@ getVariablesFromLiteral string =
|
|||
where
|
||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||
|
||||
-- Get the variable name from an expansion like ${var:-foo}
|
||||
prop_getBracedReference1 = getBracedReference "foo" == "foo"
|
||||
prop_getBracedReference2 = getBracedReference "#foo" == "foo"
|
||||
prop_getBracedReference3 = getBracedReference "#" == "#"
|
||||
prop_getBracedReference4 = getBracedReference "##" == "#"
|
||||
prop_getBracedReference5 = getBracedReference "#!" == "!"
|
||||
prop_getBracedReference6 = getBracedReference "!#" == "#"
|
||||
prop_getBracedReference7 = getBracedReference "!foo#?" == "foo"
|
||||
prop_getBracedReference8 = getBracedReference "foo-bar" == "foo"
|
||||
prop_getBracedReference9 = getBracedReference "foo:-bar" == "foo"
|
||||
prop_getBracedReference10= getBracedReference "foo: -1" == "foo"
|
||||
prop_getBracedReference11= getBracedReference "!os*" == ""
|
||||
prop_getBracedReference11b= getBracedReference "!os@" == ""
|
||||
prop_getBracedReference12= getBracedReference "!os?bar**" == ""
|
||||
prop_getBracedReference13= getBracedReference "foo[bar]" == "foo"
|
||||
getBracedReference s = fromMaybe s $
|
||||
nameExpansion s `mplus` takeName noPrefix `mplus` getSpecial noPrefix `mplus` getSpecial s
|
||||
where
|
||||
noPrefix = dropPrefix s
|
||||
dropPrefix (c:rest) | c `elem` "!#" = rest
|
||||
dropPrefix cs = cs
|
||||
takeName s = do
|
||||
let name = takeWhile isVariableChar s
|
||||
guard . not $ null name
|
||||
return name
|
||||
getSpecial (c:_) | c `elem` "*@#?-$!" = return [c]
|
||||
getSpecial _ = fail "empty or not special"
|
||||
|
||||
nameExpansion ('!':next:rest) = do -- e.g. ${!foo*bar*}
|
||||
guard $ isVariableChar next -- e.g. ${!@}
|
||||
first <- find (not . isVariableChar) rest
|
||||
guard $ first `elem` "*?@"
|
||||
return ""
|
||||
nameExpansion _ = Nothing
|
||||
|
||||
prop_getBracedModifier1 = getBracedModifier "foo:bar:baz" == ":bar:baz"
|
||||
prop_getBracedModifier2 = getBracedModifier "!var:-foo" == ":-foo"
|
||||
prop_getBracedModifier3 = getBracedModifier "foo[bar]" == "[bar]"
|
||||
getBracedModifier s = headOrDefault "" $ do
|
||||
let var = getBracedReference s
|
||||
a <- dropModifier s
|
||||
dropPrefix var a
|
||||
where
|
||||
dropPrefix [] t = return t
|
||||
dropPrefix (a:b) (c:d) | a == c = dropPrefix b d
|
||||
dropPrefix _ _ = []
|
||||
|
||||
dropModifier (c:rest) | c `elem` "#!" = [rest, c:rest]
|
||||
dropModifier x = [x]
|
||||
|
||||
-- Useful generic functions.
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
-- Run a command if the shell is in the given list
|
||||
whenShell l c = do
|
||||
|
@ -914,31 +925,14 @@ supportsArrays Bash = True
|
|||
supportsArrays Ksh = True
|
||||
supportsArrays _ = False
|
||||
|
||||
isTrueAssignmentSource c =
|
||||
case c of
|
||||
DataString SourceChecked -> False
|
||||
DataString SourceDeclaration -> False
|
||||
DataArray SourceChecked -> False
|
||||
DataArray SourceDeclaration -> False
|
||||
_ -> True
|
||||
|
||||
modifiesVariable params token name =
|
||||
or $ map check flow
|
||||
where
|
||||
flow = getVariableFlow params token
|
||||
check t =
|
||||
case t of
|
||||
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
|
||||
_ -> False
|
||||
|
||||
isTestCommand t =
|
||||
case t of
|
||||
T_Condition {} -> True
|
||||
T_SimpleCommand {} -> t `isCommand` "test"
|
||||
T_Redirecting _ _ t -> isTestCommand t
|
||||
T_Annotation _ _ t -> isTestCommand t
|
||||
T_Pipeline _ _ [t] -> isTestCommand t
|
||||
_ -> False
|
||||
-- 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 }) ) |])
|
||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -20,12 +20,10 @@
|
|||
{-# LANGUAGE TemplateHaskell #-}
|
||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||
|
||||
import ShellCheck.Analyzer
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Analyzer
|
||||
|
||||
import Debug.Trace -- DO NOT SUBMIT
|
||||
import Data.Either
|
||||
import Data.Functor
|
||||
import Data.List
|
||||
|
@ -87,8 +85,7 @@ checkScript sys spec = do
|
|||
asCheckSourced = csCheckSourced spec,
|
||||
asExecutionMode = Executed,
|
||||
asTokenPositions = tokenPositions,
|
||||
asExtendedAnalysis = csExtendedAnalysis spec,
|
||||
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
|
||||
asOptionalChecks = csOptionalChecks spec
|
||||
} where as = newAnalysisSpec root
|
||||
let analysisMessages =
|
||||
maybe []
|
||||
|
@ -159,11 +156,6 @@ checkWithIncludesAndSourcePath includes mapper = getErrors
|
|||
siFindSource = mapper
|
||||
}
|
||||
|
||||
checkWithRcIncludesAndSourcePath rc includes mapper = getErrors
|
||||
(mockRcFile rc $ mockedSystemInterface includes) {
|
||||
siFindSource = mapper
|
||||
}
|
||||
|
||||
prop_findsParseIssue = check "echo \"$12\"" == [1037]
|
||||
|
||||
prop_commentDisablesParseIssue1 =
|
||||
|
@ -246,9 +238,6 @@ prop_canStripPrefixAndSource2 =
|
|||
prop_canSourceDynamicWhenRedirected =
|
||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||
|
||||
prop_canRedirectWithSpaces =
|
||||
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
|
||||
|
||||
prop_recursiveAnalysis =
|
||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
||||
|
||||
|
@ -312,13 +301,6 @@ prop_canDisableShebangWarning = null $ result
|
|||
csScript = "#shellcheck disable=SC2148\nfoo"
|
||||
}
|
||||
|
||||
prop_canDisableAllWarnings = result == [2086]
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
csFilename = "file.sh",
|
||||
csScript = "#!/bin/sh\necho $1\n#shellcheck disable=all\necho `echo $1`"
|
||||
}
|
||||
|
||||
prop_canDisableParseErrors = null $ result
|
||||
where
|
||||
result = checkWithSpec [] emptyCheckSpec {
|
||||
|
@ -402,7 +384,7 @@ prop_canEnableOptionalsWithRc = result == [2244]
|
|||
|
||||
prop_sourcePathRedirectsName = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||
f "dir/myscript" _ "lib" = return "foo/lib"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\nsource lib",
|
||||
csFilename = "dir/myscript",
|
||||
|
@ -411,154 +393,22 @@ prop_sourcePathRedirectsName = result == [2086]
|
|||
|
||||
prop_sourcePathAddsAnnotation = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ ["mypath"] "lib" = return "foo/lib"
|
||||
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_sourcePathWorksWithSpaces = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ ["my path"] "lib" = return "foo/lib"
|
||||
result = checkWithIncludesAndSourcePath [("foo/lib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck source-path='my path'\nsource lib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_sourcePathRedirectsDirective = result == [2086]
|
||||
where
|
||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||
f _ _ _ _ = return "/dev/null"
|
||||
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
|
||||
}
|
||||
|
||||
prop_rcCanAllowExternalSources = result == [2086]
|
||||
where
|
||||
f "dir/myscript" (Just True) _ "mylib" = return "resolved/mylib"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\nsource mylib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_rcCanDenyExternalSources = result == [2086]
|
||||
where
|
||||
f "dir/myscript" (Just False) _ "mylib" = return "resolved/mylib"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\nsource mylib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_rcCanLeaveExternalSourcesUnspecified = result == [2086]
|
||||
where
|
||||
f "dir/myscript" Nothing _ "mylib" = return "resolved/mylib"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "" [("resolved/mylib", "echo $1")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\nsource mylib",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_fileCanDisableExternalSources = result == [2006, 2086]
|
||||
where
|
||||
f "dir/myscript" (Just True) _ "withExternal" = return "withExternal"
|
||||
f "dir/myscript" (Just False) _ "withoutExternal" = return "withoutExternal"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "external-sources=true" [("withExternal", "echo $1"), ("withoutExternal", "_=`foo`")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\ntrue\nsource withExternal\n# shellcheck external-sources=false\nsource withoutExternal",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_fileCannotEnableExternalSources = result == [1144]
|
||||
where
|
||||
f "dir/myscript" Nothing _ "foo" = return "foo"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "" [("foo", "true")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_fileCannotEnableExternalSources2 = result == [1144]
|
||||
where
|
||||
f "dir/myscript" (Just False) _ "foo" = return "foo"
|
||||
f a b c d = error $ show ("Unexpected", a, b, c, d)
|
||||
result = checkWithRcIncludesAndSourcePath "external-sources=false" [("foo", "true")] f emptyCheckSpec {
|
||||
csScript = "#!/bin/bash\n# shellcheck external-sources=true\nsource foo",
|
||||
csFilename = "dir/myscript",
|
||||
csCheckSourced = True
|
||||
}
|
||||
|
||||
prop_rcCanSuppressEarlyProblems1 = null result
|
||||
where
|
||||
result = checkWithRc "disable=1071" emptyCheckSpec {
|
||||
csScript = "#!/bin/zsh\necho $1"
|
||||
}
|
||||
|
||||
prop_rcCanSuppressEarlyProblems2 = null result
|
||||
where
|
||||
result = checkWithRc "disable=1104" emptyCheckSpec {
|
||||
csScript = "!/bin/bash\necho 'hello world'"
|
||||
}
|
||||
|
||||
prop_sourceWithHereDocWorks = null result
|
||||
where
|
||||
result = checkWithIncludes [("bar", "true\n")] "source bar << eof\nlol\neof"
|
||||
|
||||
prop_hereDocsAreParsedWithoutTrailingLinefeed = 1044 `elem` result
|
||||
where
|
||||
result = check "cat << eof"
|
||||
|
||||
prop_hereDocsWillHaveParsedIndices = null result
|
||||
where
|
||||
result = check "#!/bin/bash\nmy_array=(a b)\ncat <<EOF >> ./test\n $(( 1 + my_array[1] ))\nEOF"
|
||||
|
||||
prop_rcCanSuppressDfa = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileCanSuppressDfa = null $ traceShowId result
|
||||
where
|
||||
result = checkWithRc "" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileWinsWhenSuppressingDfa1 = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_fileWinsWhenSuppressingDfa2 = result == [2317]
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;"
|
||||
}
|
||||
|
||||
prop_flagWinsWhenSuppressingDfa1 = result == [2317]
|
||||
where
|
||||
result = checkWithRc "extended-analysis=false" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=false\nexit; foo;",
|
||||
csExtendedAnalysis = Just True
|
||||
}
|
||||
|
||||
prop_flagWinsWhenSuppressingDfa2 = null result
|
||||
where
|
||||
result = checkWithRc "extended-analysis=true" emptyCheckSpec {
|
||||
csScript = "#!/bin/sh\n# shellcheck extended-analysis=true\nexit; foo;",
|
||||
csExtendedAnalysis = Just False
|
||||
}
|
||||
|
||||
return []
|
||||
runTests = $quickCheckAll
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -20,7 +20,6 @@
|
|||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE MultiWayIf #-}
|
||||
{-# LANGUAGE PatternGuards #-}
|
||||
|
||||
-- This module contains checks that examine specific commands by name.
|
||||
module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Commands.runTests) where
|
||||
|
@ -28,29 +27,21 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
|
|||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.CFG
|
||||
import qualified ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.RWS
|
||||
import Data.Char
|
||||
import Data.Functor.Identity
|
||||
import qualified Data.Graph.Inductive.Graph as G
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Map.Strict as M
|
||||
import qualified Data.Set as S
|
||||
import qualified Data.Map.Strict as Map
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
import Debug.Trace -- STRIP
|
||||
|
||||
data CommandName = Exactly String | Basename String
|
||||
deriving (Eq, Ord)
|
||||
|
||||
|
@ -66,7 +57,7 @@ commandChecks :: [CommandCheck]
|
|||
commandChecks = [
|
||||
checkTr
|
||||
,checkFindNameGlob
|
||||
,checkExpr
|
||||
,checkNeedlessExpr
|
||||
,checkGrepRe
|
||||
,checkTrapQuotes
|
||||
,checkReturn
|
||||
|
@ -104,14 +95,7 @@ commandChecks = [
|
|||
,checkSourceArgs
|
||||
,checkChmodDashr
|
||||
,checkXargsDashi
|
||||
,checkUnquotedEchoSpaces
|
||||
,checkEvalArray
|
||||
]
|
||||
++ map checkArgComparison ("alias" : declaringCommands)
|
||||
++ map checkMaskedReturns declaringCommands
|
||||
++ map checkMultipleDeclaring declaringCommands
|
||||
++ map checkBackreferencingDeclaration declaringCommands
|
||||
|
||||
|
||||
optionalChecks = map fst optionalCommandChecks
|
||||
optionalCommandChecks :: [(CheckDescription, CommandCheck)]
|
||||
|
@ -123,7 +107,7 @@ optionalCommandChecks = [
|
|||
cdNegative = "command -v javac"
|
||||
}, checkWhich)
|
||||
]
|
||||
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||
|
||||
prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||
where
|
||||
|
@ -147,51 +131,40 @@ prop_checkGetOptsS3 = checkGetOpts "-f -x" ["f", "x"] [] $ getOpts (True, True)
|
|||
prop_checkGetOptsS4 = checkGetOpts "-f -x" ["f"] [] $ getOpts (True, True) "f:" []
|
||||
prop_checkGetOptsS5 = checkGetOpts "-fx" [] [] $ getOpts (True, True) "fx:" []
|
||||
|
||||
prop_checkGenericOptsS1 = checkGetOpts "-f x" ["f"] [] $ return . getGenericOpts
|
||||
prop_checkGenericOptsS2 = checkGetOpts "-abc x" ["a", "b", "c"] [] $ return . getGenericOpts
|
||||
prop_checkGenericOptsS3 = checkGetOpts "-abc -x" ["a", "b", "c", "x"] [] $ return . getGenericOpts
|
||||
prop_checkGenericOptsS4 = checkGetOpts "-x" ["x"] [] $ return . getGenericOpts
|
||||
|
||||
-- Long options
|
||||
prop_checkGetOptsL1 = checkGetOpts "--foo=bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
|
||||
prop_checkGetOptsL2 = checkGetOpts "--foo bar baz" ["foo"] ["baz"] $ getOpts (True, False) "" [("foo", True)]
|
||||
prop_checkGetOptsL3 = checkGetOpts "--foo baz" ["foo"] ["baz"] $ getOpts (True, True) "" []
|
||||
prop_checkGetOptsL4 = checkGetOpts "--foo baz" [] [] $ getOpts (True, False) "" []
|
||||
|
||||
prop_checkGenericOptsL1 = checkGetOpts "--foo=bar" ["foo"] [] $ return . getGenericOpts
|
||||
prop_checkGenericOptsL2 = checkGetOpts "--foo bar" ["foo"] ["bar"] $ return . getGenericOpts
|
||||
prop_checkGenericOptsL3 = checkGetOpts "-x --foo" ["x", "foo"] [] $ return . getGenericOpts
|
||||
|
||||
-- Know when to terminate
|
||||
prop_checkGetOptsT1 = checkGetOpts "-a x -b" ["a", "b"] ["x"] $ getOpts (True, True) "ab" []
|
||||
prop_checkGetOptsT2 = checkGetOpts "-a x -b" ["a"] ["x","-b"] $ getOpts (False, True) "ab" []
|
||||
prop_checkGetOptsT3 = checkGetOpts "-a -- -b" ["a"] ["-b"] $ getOpts (True, True) "ab" []
|
||||
prop_checkGetOptsT4 = checkGetOpts "-a -- -b" ["a", "b"] [] $ getOpts (True, True) "a:b" []
|
||||
|
||||
prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGenericOpts
|
||||
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
|
||||
|
||||
|
||||
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck M.empty
|
||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
||||
buildCommandMap = foldl' addCheck Map.empty
|
||||
where
|
||||
addCheck map (CommandCheck name function) =
|
||||
M.insertWith composeAnalyzers name function map
|
||||
Map.insertWith composeAnalyzers name function map
|
||||
|
||||
|
||||
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
||||
name <- getLiteralString cmd
|
||||
return $
|
||||
if | '/' `elem` name ->
|
||||
M.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
| name == "builtin", (h:_) <- rest ->
|
||||
let t' = T_SimpleCommand id cmdPrefix rest
|
||||
selectedBuiltin = onlyLiteralString h
|
||||
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||
| otherwise -> do
|
||||
M.findWithDefault nullCheck (Exactly name) map t
|
||||
M.findWithDefault nullCheck (Basename name) map t
|
||||
if '/' `elem` name
|
||||
then
|
||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
||||
else if name == "builtin" && not (null rest) then
|
||||
let t' = T_SimpleCommand id cmdPrefix rest
|
||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
||||
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||
else do
|
||||
Map.findWithDefault nullCheck (Exactly name) map t
|
||||
Map.findWithDefault nullCheck (Basename name) map t
|
||||
|
||||
where
|
||||
basename = reverse . takeWhile (/= '/') . reverse
|
||||
|
@ -213,22 +186,22 @@ checker spec params = getChecker $ commandChecks ++ optionals
|
|||
optionals =
|
||||
if "all" `elem` keys
|
||||
then map snd optionalCommandChecks
|
||||
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
|
||||
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
|
||||
|
||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
||||
prop_checkTr2a = verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||
prop_checkTr2a= verify checkTr "tr '[a-z]' '[A-Z]'"
|
||||
prop_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||
prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'"
|
||||
prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
||||
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||
prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||
prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'"
|
||||
prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'"
|
||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
||||
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
|
||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||
where
|
||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||
|
@ -262,74 +235,19 @@ checkFindNameGlob = CommandCheck (Basename "find") (f . arguments) where
|
|||
acc b
|
||||
|
||||
|
||||
prop_checkExpr = verify checkExpr "foo=$(expr 3 + 2)"
|
||||
prop_checkExpr2 = verify checkExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||
prop_checkExpr3 = verifyNot checkExpr "foo=$(expr foo : regex)"
|
||||
prop_checkExpr4 = verifyNot checkExpr "foo=$(expr foo \\< regex)"
|
||||
prop_checkExpr5 = verify checkExpr "# shellcheck disable=SC2003\nexpr match foo bar"
|
||||
prop_checkExpr6 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo : fo*"
|
||||
prop_checkExpr7 = verify checkExpr "# shellcheck disable=SC2003\nexpr 5 -3"
|
||||
prop_checkExpr8 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr \"$@\""
|
||||
prop_checkExpr9 = verifyNot checkExpr "# shellcheck disable=SC2003\nexpr 5 $rest"
|
||||
prop_checkExpr10 = verify checkExpr "# shellcheck disable=SC2003\nexpr length \"$var\""
|
||||
prop_checkExpr11 = verify checkExpr "# shellcheck disable=SC2003\nexpr foo > bar"
|
||||
prop_checkExpr12 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 | 2"
|
||||
prop_checkExpr13 = verify checkExpr "# shellcheck disable=SC2003\nexpr 1 * 2"
|
||||
prop_checkExpr14 = verify checkExpr "# shellcheck disable=SC2003\nexpr \"$x\" >= \"$y\""
|
||||
|
||||
checkExpr = CommandCheck (Basename "expr") f where
|
||||
f t = do
|
||||
prop_checkNeedlessExpr = verify checkNeedlessExpr "foo=$(expr 3 + 2)"
|
||||
prop_checkNeedlessExpr2 = verify checkNeedlessExpr "foo=`echo \\`expr 3 + 2\\``"
|
||||
prop_checkNeedlessExpr3 = verifyNot checkNeedlessExpr "foo=$(expr foo : regex)"
|
||||
prop_checkNeedlessExpr4 = verifyNot checkNeedlessExpr "foo=$(expr foo \\< regex)"
|
||||
checkNeedlessExpr = CommandCheck (Basename "expr") f where
|
||||
f t =
|
||||
when (all (`notElem` exceptions) (words $ arguments t)) $
|
||||
style (getId $ getCommandTokenOrThis t) 2003
|
||||
"expr is antiquated. Consider rewriting this using $((..)), ${} or [[ ]]."
|
||||
|
||||
case arguments t of
|
||||
[lhs, op, rhs] -> do
|
||||
checkOp lhs
|
||||
case getWordParts op of
|
||||
[T_Glob _ "*"] ->
|
||||
err (getId op) 2304
|
||||
"* must be escaped to multiply: \\*. Modern $((x * y)) avoids this issue."
|
||||
[T_Literal _ ":"] | isGlob rhs ->
|
||||
warn (getId rhs) 2305
|
||||
"Quote regex argument to expr to avoid it expanding as a glob."
|
||||
_ -> return ()
|
||||
|
||||
[single] | not (willSplit single) ->
|
||||
warn (getId single) 2307
|
||||
"'expr' expects 3+ arguments but sees 1. Make sure each operator/operand is a separate argument, and escape <>&|."
|
||||
|
||||
[first, second] |
|
||||
onlyLiteralString first /= "length"
|
||||
&& not (willSplit first || willSplit second) -> do
|
||||
checkOp first
|
||||
warn (getId t) 2307
|
||||
"'expr' expects 3+ arguments, but sees 2. Make sure each operator/operand is a separate argument, and escape <>&|."
|
||||
|
||||
(first:rest) -> do
|
||||
checkOp first
|
||||
forM_ rest $ \t ->
|
||||
-- We already find 95%+ of multiplication and regex earlier, so don't bother classifying this further.
|
||||
when (isGlob t) $ warn (getId t) 2306 "Escape glob characters in arguments to expr to avoid pathname expansion."
|
||||
|
||||
_ -> return ()
|
||||
|
||||
-- These operators are hard to replicate in POSIX
|
||||
exceptions = [ ":", "<", ">", "<=", ">=",
|
||||
-- We can offer better suggestions for these
|
||||
"match", "length", "substr", "index"]
|
||||
exceptions = [ ":", "<", ">", "<=", ">=" ]
|
||||
words = mapMaybe getLiteralString
|
||||
|
||||
checkOp side =
|
||||
case getLiteralString side of
|
||||
Just "match" -> msg "'expr match' has unspecified results. Prefer 'expr str : regex'."
|
||||
Just "length" -> msg "'expr length' has unspecified results. Prefer ${#var}."
|
||||
Just "substr" -> msg "'expr substr' has unspecified results. Prefer 'cut' or ${var#???}."
|
||||
Just "index" -> msg "'expr index' has unspecified results. Prefer x=${var%%[chars]*}; $((${#x}+1))."
|
||||
_ -> return ()
|
||||
where
|
||||
msg = info (getId side) 2308
|
||||
|
||||
|
||||
prop_checkGrepRe1 = verify checkGrepRe "cat foo | grep *.mp3"
|
||||
prop_checkGrepRe2 = verify checkGrepRe "grep -Ev cow*test *.mp3"
|
||||
|
@ -340,20 +258,20 @@ prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
|||
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
||||
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
||||
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||
prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file"
|
||||
prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo"
|
||||
prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||
prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*"
|
||||
prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*"
|
||||
prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*"
|
||||
prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file"
|
||||
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"
|
||||
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
||||
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
||||
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
|
||||
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
|
||||
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
|
||||
prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"
|
||||
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
|
||||
check cmd = f cmd (arguments cmd)
|
||||
|
@ -401,7 +319,7 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
|||
|
||||
|
||||
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
||||
prop_checkTrapQuotes1a = verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||
prop_checkTrapQuotes1a= verify checkTrapQuotes "trap \"echo `ls`\" INT"
|
||||
prop_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||
|
@ -482,16 +400,9 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
|||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
||||
prop_checkUnusedEchoEscapes6 = verify checkUnusedEchoEscapes "echo '\\506'"
|
||||
prop_checkUnusedEchoEscapes7 = verify checkUnusedEchoEscapes "echo '\\5a'"
|
||||
prop_checkUnusedEchoEscapes8 = verifyNot checkUnusedEchoEscapes "echo '\\8a'"
|
||||
prop_checkUnusedEchoEscapes9 = verifyNot checkUnusedEchoEscapes "echo '\\d5a'"
|
||||
prop_checkUnusedEchoEscapes10 = verify checkUnusedEchoEscapes "echo '\\x4a'"
|
||||
prop_checkUnusedEchoEscapes11 = verify checkUnusedEchoEscapes "echo '\\xat'"
|
||||
prop_checkUnusedEchoEscapes12 = verifyNot checkUnusedEchoEscapes "echo '\\xth'"
|
||||
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||
where
|
||||
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
|
||||
hasEscapes = mkRegex "\\\\[rnt]"
|
||||
f cmd =
|
||||
whenShell [Sh, Bash, Ksh] $
|
||||
unless (cmd `hasFlag` "e") $
|
||||
|
@ -665,19 +576,19 @@ prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
|
|||
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||
prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||
prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||
prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||
prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1"
|
||||
prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||
prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'"
|
||||
prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||
prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"
|
||||
prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"
|
||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
|
||||
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
|
||||
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
|
||||
|
||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||
|
@ -691,7 +602,6 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||
let formats = getPrintfFormats string
|
||||
let formatCount = length formats
|
||||
let argCount = length more
|
||||
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
|
||||
|
||||
return $ if
|
||||
| argCount == 0 && formatCount == 0 ->
|
||||
|
@ -707,8 +617,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||
return () -- Great: a suitable number of arguments
|
||||
| otherwise ->
|
||||
warn (getId format) 2183 $
|
||||
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
|
||||
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
|
||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
||||
|
||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||
info (getId format) 2059
|
||||
|
@ -754,7 +663,7 @@ getPrintfFormats = getFormats
|
|||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||
-- V V V V V
|
||||
-- flags field width precision format character rest
|
||||
-- field width and precision can be specified with an '*' instead of a digit,
|
||||
-- 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
|
||||
|
||||
|
||||
|
@ -809,7 +718,6 @@ prop_checkReadExpansions5 = verify checkReadExpansions "read \"$var\""
|
|||
prop_checkReadExpansions6 = verify checkReadExpansions "read -a $var"
|
||||
prop_checkReadExpansions7 = verifyNot checkReadExpansions "read $1"
|
||||
prop_checkReadExpansions8 = verifyNot checkReadExpansions "read ${var?}"
|
||||
prop_checkReadExpansions9 = verify checkReadExpansions "read arr[val]"
|
||||
checkReadExpansions = CommandCheck (Exactly "read") check
|
||||
where
|
||||
options = getGnuOpts flagsForRead
|
||||
|
@ -817,26 +725,13 @@ checkReadExpansions = CommandCheck (Exactly "read") check
|
|||
opts <- options $ arguments cmd
|
||||
return [y | (x,(_, y)) <- opts, null x || x == "a"]
|
||||
|
||||
check cmd = do
|
||||
mapM_ dollarWarning $ getVars cmd
|
||||
mapM_ arrayWarning $ arguments cmd
|
||||
|
||||
dollarWarning t = sequence_ $ do
|
||||
check cmd = mapM_ warning $ getVars cmd
|
||||
warning t = sequence_ $ do
|
||||
name <- getSingleUnmodifiedBracedString t
|
||||
guard $ isVariableName name -- e.g. not $1
|
||||
return . warn (getId t) 2229 $
|
||||
"This does not read '" ++ name ++ "'. Remove $/${} for that, or use ${var?} to quiet."
|
||||
|
||||
arrayWarning word =
|
||||
when (any isUnquotedBracket $ getWordParts word) $
|
||||
warn (getId word) 2313 $
|
||||
"Quote array indices to avoid them expanding as globs."
|
||||
|
||||
isUnquotedBracket t =
|
||||
case t of
|
||||
T_Glob _ ('[':_) -> True
|
||||
_ -> False
|
||||
|
||||
-- Return the single variable expansion that makes up this word, if any.
|
||||
-- e.g. $foo -> $foo, "$foo"'' -> $foo , "hello $name" -> Nothing
|
||||
getSingleUnmodifiedBracedString :: Token -> Maybe String
|
||||
|
@ -876,9 +771,6 @@ checkAliasesExpandEarly = CommandCheck (Exactly "alias") (f . arguments)
|
|||
|
||||
prop_checkUnsetGlobs1 = verify checkUnsetGlobs "unset foo[1]"
|
||||
prop_checkUnsetGlobs2 = verifyNot checkUnsetGlobs "unset foo"
|
||||
prop_checkUnsetGlobs3 = verify checkUnsetGlobs "unset foo[$i]"
|
||||
prop_checkUnsetGlobs4 = verify checkUnsetGlobs "unset foo[x${i}y]"
|
||||
prop_checkUnsetGlobs5 = verifyNot checkUnsetGlobs "unset foo]["
|
||||
checkUnsetGlobs = CommandCheck (Exactly "unset") (mapM_ check . arguments)
|
||||
where
|
||||
check arg =
|
||||
|
@ -931,7 +823,7 @@ prop_checkTimedCommand2 = verify checkTimedCommand "#!/bin/dash\ntime ( foo; bar
|
|||
prop_checkTimedCommand3 = verifyNot checkTimedCommand "#!/bin/sh\ntime sleep 1"
|
||||
checkTimedCommand = CommandCheck (Exactly "time") f where
|
||||
f (T_SimpleCommand _ _ (c:args@(_:_))) =
|
||||
whenShell [Sh, Dash, BusyboxSh] $ do
|
||||
whenShell [Sh, Dash] $ do
|
||||
let cmd = last args -- "time" is parsed with a command as argument
|
||||
when (isPiped cmd) $
|
||||
warn (getId c) 2176 "'time' is undefined for pipelines. time single stage or bash -c instead."
|
||||
|
@ -955,27 +847,11 @@ checkTimedCommand = CommandCheck (Exactly "time") f where
|
|||
prop_checkLocalScope1 = verify checkLocalScope "local foo=3"
|
||||
prop_checkLocalScope2 = verifyNot checkLocalScope "f() { local foo=3; }"
|
||||
checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
||||
whenShell [Bash, Dash, BusyboxSh] $ do -- Ksh allows it, Sh doesn't support local
|
||||
whenShell [Bash, Dash] $ do -- Ksh allows it, Sh doesn't support local
|
||||
path <- getPathM t
|
||||
unless (any isFunctionLike path) $
|
||||
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
||||
|
||||
prop_checkMultipleDeclaring1 = verify (checkMultipleDeclaring "local") "q() { local readonly var=1; }"
|
||||
prop_checkMultipleDeclaring2 = verifyNot (checkMultipleDeclaring "local") "q() { local var=1; }"
|
||||
prop_checkMultipleDeclaring3 = verify (checkMultipleDeclaring "readonly") "readonly local foo=5"
|
||||
prop_checkMultipleDeclaring4 = verify (checkMultipleDeclaring "export") "export readonly foo=5"
|
||||
prop_checkMultipleDeclaring5 = verifyNot (checkMultipleDeclaring "local") "f() { local -r foo=5; }"
|
||||
prop_checkMultipleDeclaring6 = verifyNot (checkMultipleDeclaring "declare") "declare -rx foo=5"
|
||||
prop_checkMultipleDeclaring7 = verifyNot (checkMultipleDeclaring "readonly") "readonly 'local' foo=5"
|
||||
checkMultipleDeclaring cmd = CommandCheck (Exactly cmd) (mapM_ check . arguments)
|
||||
where
|
||||
check t = sequence_ $ do
|
||||
lit <- getUnquotedLiteral t
|
||||
guard $ lit `elem` declaringCommands
|
||||
return $ err (getId $ getCommandTokenOrThis t) 2316 $
|
||||
"This applies " ++ cmd ++ " to the variable named " ++ lit ++
|
||||
", which is probably not what you want. Use a separate command or the appropriate `declare` options instead."
|
||||
|
||||
prop_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||
|
@ -994,48 +870,34 @@ prop_checkWhileGetoptsCase2 = verify checkWhileGetoptsCase "while getopts 'a:' x
|
|||
prop_checkWhileGetoptsCase3 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' 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_checkWhileGetoptsCase5 = verifyNot checkWhileGetoptsCase "while getopts 'a:' x; do case $x in a) foo;; \\?) bar;; *) baz;; esac; done"
|
||||
prop_checkWhileGetoptsCase6 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case $y in a) foo;; esac; done"
|
||||
prop_checkWhileGetoptsCase7 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do case x$x in xa) foo;; xb) foo;; esac; done"
|
||||
prop_checkWhileGetoptsCase8 = verifyNot checkWhileGetoptsCase "while getopts 'a:b' x; do x=a; case $x in a) foo;; esac; done"
|
||||
checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
||||
where
|
||||
f :: Token -> Analysis
|
||||
f t@(T_SimpleCommand _ _ (cmd:arg1:name:_)) = do
|
||||
f t@(T_SimpleCommand _ _ (cmd:arg1:_)) = do
|
||||
path <- getPathM t
|
||||
params <- ask
|
||||
sequence_ $ do
|
||||
options <- getLiteralString arg1
|
||||
getoptsVar <- getLiteralString name
|
||||
(T_WhileExpression _ _ body) <- findFirst whileLoop (NE.toList path)
|
||||
T_CaseExpression id var list <- mapMaybe findCase body !!! 0
|
||||
|
||||
-- Make sure getopts name and case variable matches
|
||||
[T_DollarBraced _ _ bracedWord] <- return $ getWordParts var
|
||||
[T_Literal _ caseVar] <- return $ getWordParts bracedWord
|
||||
guard $ caseVar == getoptsVar
|
||||
|
||||
-- Make sure the variable isn't modified
|
||||
guard . not $ modifiesVariable params (T_BraceGroup (Id 0) body) getoptsVar
|
||||
|
||||
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) id list
|
||||
(T_WhileExpression _ _ body) <- findFirst whileLoop path
|
||||
caseCmd <- mapMaybe findCase body !!! 0
|
||||
return $ check (getId arg1) (map (:[]) $ filter (/= ':') options) caseCmd
|
||||
f _ = return ()
|
||||
|
||||
check :: Id -> [String] -> Id -> [(CaseType, [Token], [Token])] -> Analysis
|
||||
check optId opts id list = do
|
||||
unless (Nothing `M.member` handledMap) $ do
|
||||
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
|
||||
check :: Id -> [String] -> Token -> Analysis
|
||||
check optId opts (T_CaseExpression id _ list) = do
|
||||
unless (Nothing `Map.member` handledMap) $ do
|
||||
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
|
||||
|
||||
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
|
||||
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
|
||||
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
||||
|
||||
mapM_ warnRedundant $ M.toList notRequested
|
||||
mapM_ warnRedundant $ Map.toList notRequested
|
||||
|
||||
where
|
||||
handledMap = M.fromList (concatMap getHandledStrings list)
|
||||
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
|
||||
handledMap = Map.fromList (concatMap getHandledStrings list)
|
||||
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
|
||||
|
||||
notHandled = M.difference requestedMap handledMap
|
||||
notRequested = M.difference handledMap requestedMap
|
||||
notHandled = Map.difference requestedMap handledMap
|
||||
notRequested = Map.difference handledMap requestedMap
|
||||
|
||||
warnUnhandled optId caseId str =
|
||||
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
||||
|
@ -1079,10 +941,10 @@ prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*
|
|||
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
||||
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
||||
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
||||
prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
||||
prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
||||
prop_checkCatastrophicRm12 = verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
||||
prop_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
||||
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
||||
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
||||
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{bin,sbin}}/$exec"
|
||||
prop_checkCatastrophicRm13= verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
||||
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
||||
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
||||
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||
|
@ -1237,7 +1099,8 @@ checkSudoArgs = CommandCheck (Basename "sudo") f
|
|||
where
|
||||
f t = sequence_ $ do
|
||||
opts <- parseOpts $ arguments t
|
||||
(_,(commandArg, _)) <- find (null . fst) opts
|
||||
let nonFlags = [x | ("",(x, _)) <- opts]
|
||||
commandArg <- nonFlags !!! 0
|
||||
command <- getLiteralString commandArg
|
||||
guard $ command `elem` builtins
|
||||
return $ warn (getId t) 2232 $ "Can't use sudo with builtins like " ++ command ++ ". Did you want sudo sh -c .. instead?"
|
||||
|
@ -1280,193 +1143,5 @@ checkXargsDashi = CommandCheck (Basename "xargs") f
|
|||
return $ info (getId option) 2267 "GNU xargs -i is deprecated in favor of -I{}"
|
||||
parseOpts = getBsdOpts "0oprtxadR:S:J:L:l:n:P:s:e:E:i:I:"
|
||||
|
||||
|
||||
prop_checkArgComparison1 = verify (checkArgComparison "declare") "declare a = b"
|
||||
prop_checkArgComparison2 = verify (checkArgComparison "declare") "declare a =b"
|
||||
prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b"
|
||||
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
|
||||
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
|
||||
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
|
||||
prop_checkArgComparison9 = verify (checkArgComparison "alias") "alias x =0"
|
||||
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
|
||||
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
|
||||
where
|
||||
wordsWithEqual t = mapM_ check $ arguments t
|
||||
check arg = do
|
||||
sequence_ $ do
|
||||
str <- getLeadingUnquotedString arg
|
||||
case str of
|
||||
'=':_ ->
|
||||
return $ err (headId arg) 2290 $
|
||||
"Remove spaces around = to assign."
|
||||
'+':'=':_ ->
|
||||
return $ err (headId arg) 2290 $
|
||||
"Remove spaces around += to append."
|
||||
_ -> Nothing
|
||||
|
||||
-- 'let' is parsed as a sequence of arithmetic expansions,
|
||||
-- so we want the additional warning for "x="
|
||||
when (cmd == "let") $ sequence_ $ do
|
||||
token <- getTrailingUnquotedLiteral arg
|
||||
str <- getLiteralString token
|
||||
guard $ "=" `isSuffixOf` str
|
||||
return $ err (getId token) 2290 $
|
||||
"Remove spaces around = to assign."
|
||||
|
||||
headId t =
|
||||
case t of
|
||||
T_NormalWord _ (x:_) -> getId x
|
||||
_ -> getId t
|
||||
|
||||
|
||||
prop_checkMaskedReturns1 = verify (checkMaskedReturns "local") "f() { local a=$(false); }"
|
||||
prop_checkMaskedReturns2 = verify (checkMaskedReturns "declare") "declare a=$(false)"
|
||||
prop_checkMaskedReturns3 = verify (checkMaskedReturns "declare") "declare a=\"`false`\""
|
||||
prop_checkMaskedReturns4 = verify (checkMaskedReturns "readonly") "readonly a=$(false)"
|
||||
prop_checkMaskedReturns5 = verify (checkMaskedReturns "readonly") "readonly a=\"`false`\""
|
||||
prop_checkMaskedReturns6 = verifyNot (checkMaskedReturns "declare") "declare a; a=$(false)"
|
||||
prop_checkMaskedReturns7 = verifyNot (checkMaskedReturns "local") "f() { local -r a=$(false); }"
|
||||
prop_checkMaskedReturns8 = verifyNot (checkMaskedReturns "readonly") "a=$(false); readonly a"
|
||||
prop_checkMaskedReturns9 = verify (checkMaskedReturns "typeset") "#!/bin/ksh\n f() { typeset -r x=$(false); }"
|
||||
prop_checkMaskedReturns10 = verifyNot (checkMaskedReturns "typeset") "#!/bin/ksh\n function f { typeset -r x=$(false); }"
|
||||
prop_checkMaskedReturns11 = verifyNot (checkMaskedReturns "typeset") "#!/bin/bash\n f() { typeset -r x=$(false); }"
|
||||
prop_checkMaskedReturns12 = verify (checkMaskedReturns "typeset") "typeset -r x=$(false);"
|
||||
prop_checkMaskedReturns13 = verify (checkMaskedReturns "typeset") "f() { typeset -g x=$(false); }"
|
||||
prop_checkMaskedReturns14 = verify (checkMaskedReturns "declare") "declare x=${ false; }"
|
||||
prop_checkMaskedReturns15 = verify (checkMaskedReturns "declare") "f() { declare x=$(false); }"
|
||||
checkMaskedReturns str = CommandCheck (Exactly str) checkCmd
|
||||
where
|
||||
checkCmd t = do
|
||||
path <- getPathM t
|
||||
shell <- asks shellType
|
||||
sequence_ $ do
|
||||
name <- getCommandName t
|
||||
|
||||
let flags = map snd (getAllFlags t)
|
||||
let hasDashR = "r" `elem` flags
|
||||
let hasDashG = "g" `elem` flags
|
||||
let isInScopedFunction = any (isScopedFunction shell) path
|
||||
|
||||
let isLocal = not hasDashG && isLocalInFunction name && isInScopedFunction
|
||||
let isReadOnly = name == "readonly" || hasDashR
|
||||
|
||||
-- Don't warn about local variables that are declared readonly,
|
||||
-- because the workaround `local x; x=$(false); local -r x;` is annoying
|
||||
guard . not $ isLocal && isReadOnly
|
||||
|
||||
return $ mapM_ checkArgs $ arguments t
|
||||
|
||||
checkArgs (T_Assignment id _ _ _ word) | any hasReturn $ getWordParts word =
|
||||
warn id 2155 "Declare and assign separately to avoid masking return values."
|
||||
checkArgs _ = return ()
|
||||
|
||||
isLocalInFunction = (`elem` ["local", "declare", "typeset"])
|
||||
isScopedFunction shell t =
|
||||
case t of
|
||||
T_BatsTest {} -> True
|
||||
-- In ksh, only functions declared with 'function' have their own scope
|
||||
T_Function _ (FunctionKeyword hasFunction) _ _ _ -> shell /= Ksh || hasFunction
|
||||
_ -> False
|
||||
|
||||
hasReturn t = case t of
|
||||
T_Backticked {} -> True
|
||||
T_DollarExpansion {} -> True
|
||||
T_DollarBraceCommandExpansion {} -> True
|
||||
_ -> False
|
||||
|
||||
|
||||
prop_checkUnquotedEchoSpaces1 = verify checkUnquotedEchoSpaces "echo foo bar"
|
||||
prop_checkUnquotedEchoSpaces2 = verifyNot checkUnquotedEchoSpaces "echo foo"
|
||||
prop_checkUnquotedEchoSpaces3 = verifyNot checkUnquotedEchoSpaces "echo foo bar"
|
||||
prop_checkUnquotedEchoSpaces4 = verifyNot checkUnquotedEchoSpaces "echo 'foo bar'"
|
||||
prop_checkUnquotedEchoSpaces5 = verifyNot checkUnquotedEchoSpaces "echo a > myfile.txt b"
|
||||
prop_checkUnquotedEchoSpaces6 = verifyNot checkUnquotedEchoSpaces " echo foo\\\n bar"
|
||||
checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
|
||||
where
|
||||
check t = do
|
||||
let args = arguments t
|
||||
m <- asks tokenPositions
|
||||
redir <- getClosestCommandM t
|
||||
sequence_ $ do
|
||||
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
|
||||
let pairs = zip positions (drop 1 positions)
|
||||
(T_Redirecting _ redirTokens _) <- redir
|
||||
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
|
||||
guard $ any (hasSpacesBetween redirPositions) pairs
|
||||
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
|
||||
|
||||
hasSpacesBetween redirs ((a,b), (c,d)) =
|
||||
posLine a == posLine d
|
||||
&& ((posColumn c) - (posColumn b)) >= 4
|
||||
&& not (any (\x -> b < x && x < c) redirs)
|
||||
|
||||
|
||||
prop_checkEvalArray1 = verify checkEvalArray "eval $@"
|
||||
prop_checkEvalArray2 = verify checkEvalArray "eval \"${args[@]}\""
|
||||
prop_checkEvalArray3 = verify checkEvalArray "eval \"${args[@]@Q}\""
|
||||
prop_checkEvalArray4 = verifyNot checkEvalArray "eval \"${args[*]@Q}\""
|
||||
prop_checkEvalArray5 = verifyNot checkEvalArray "eval \"$*\""
|
||||
checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordParts . arguments)
|
||||
where
|
||||
check t =
|
||||
when (isArrayExpansion t) $
|
||||
if isEscaped t
|
||||
then style (getId t) 2293 "When eval'ing @Q-quoted words, use * rather than @ as the index."
|
||||
else warn (getId t) 2294 "eval negates the benefit of arrays. Drop eval to preserve whitespace/symbols (or eval as string)."
|
||||
|
||||
isEscaped q =
|
||||
case q of
|
||||
-- Match ${arr[@]@Q} and ${@@Q} and such
|
||||
T_DollarBraced _ _ l -> 'Q' `elem` getBracedModifier (concat $ oversimplify l)
|
||||
_ -> False
|
||||
|
||||
|
||||
prop_checkBackreferencingDeclaration1 = verify (checkBackreferencingDeclaration "declare") "declare x=1 y=foo$x"
|
||||
prop_checkBackreferencingDeclaration2 = verify (checkBackreferencingDeclaration "readonly") "readonly x=1 y=$((1+x))"
|
||||
prop_checkBackreferencingDeclaration3 = verify (checkBackreferencingDeclaration "local") "local x=1 y=$(echo $x)"
|
||||
prop_checkBackreferencingDeclaration4 = verify (checkBackreferencingDeclaration "local") "local x=1 y[$x]=z"
|
||||
prop_checkBackreferencingDeclaration5 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||
prop_checkBackreferencingDeclaration6 = verify (checkBackreferencingDeclaration "declare") "declare x=var $x=1"
|
||||
prop_checkBackreferencingDeclaration7 = verify (checkBackreferencingDeclaration "declare") "declare x=var $k=$x"
|
||||
checkBackreferencingDeclaration cmd = CommandCheck (Exactly cmd) check
|
||||
where
|
||||
check t = do
|
||||
maybeCfga <- asks cfgAnalysis
|
||||
mapM_ (\cfga -> foldM_ (perArg cfga) M.empty $ arguments t) maybeCfga
|
||||
|
||||
perArg cfga leftArgs t =
|
||||
case t of
|
||||
T_Assignment id _ name idx t -> do
|
||||
warnIfBackreferencing cfga leftArgs $ t:idx
|
||||
return $ M.insert name id leftArgs
|
||||
t -> do
|
||||
warnIfBackreferencing cfga leftArgs [t]
|
||||
return leftArgs
|
||||
|
||||
warnIfBackreferencing cfga backrefs l = do
|
||||
references <- findReferences cfga l
|
||||
let reused = M.intersection backrefs references
|
||||
mapM msg $ M.toList reused
|
||||
|
||||
msg (name, id) = warn id 2318 $ "This assignment is used again in this '" ++ cmd ++ "', but won't have taken effect. Use two '" ++ cmd ++ "'s."
|
||||
|
||||
findReferences cfga list = do
|
||||
let graph = CF.graph cfga
|
||||
let nodesMap = CF.tokenToNodes cfga
|
||||
let nodes = S.unions $ map (\id -> M.findWithDefault S.empty id nodesMap) $ map getId $ list
|
||||
let labels = mapMaybe (G.lab graph) $ S.toList nodes
|
||||
let references = M.fromList $ concatMap refFromLabel labels
|
||||
return references
|
||||
|
||||
refFromLabel lab =
|
||||
case lab of
|
||||
CFApplyEffects effects -> mapMaybe refFromEffect effects
|
||||
_ -> []
|
||||
refFromEffect e =
|
||||
case e of
|
||||
IdTagged id (CFReadVariable name) -> return (name, id)
|
||||
_ -> Nothing
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
|
|
@ -1,101 +0,0 @@
|
|||
{-
|
||||
Copyright 2022 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/>.
|
||||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
|
||||
-- Checks that run on the Control Flow Graph (as opposed to the AST)
|
||||
-- This is scaffolding for a work in progress.
|
||||
|
||||
module ShellCheck.Checks.ControlFlow (checker, optionalChecks, ShellCheck.Checks.ControlFlow.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.CFG hiding (cfgAnalysis)
|
||||
import ShellCheck.CFGAnalysis
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Reader
|
||||
import Data.Graph.Inductive.Graph
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
|
||||
import Test.QuickCheck.All (forAllProperties)
|
||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||
|
||||
|
||||
optionalChecks :: [CheckDescription]
|
||||
optionalChecks = []
|
||||
|
||||
-- A check that runs on the entire graph
|
||||
type ControlFlowCheck = Analysis
|
||||
-- A check invoked once per node, with its (pre,post) data
|
||||
type ControlFlowNodeCheck = LNode CFNode -> (ProgramState, ProgramState) -> Analysis
|
||||
-- A check invoked once per effect, with its node's (pre,post) data
|
||||
type ControlFlowEffectCheck = IdTagged CFEffect -> Node -> (ProgramState, ProgramState) -> Analysis
|
||||
|
||||
|
||||
checker :: AnalysisSpec -> Parameters -> Checker
|
||||
checker spec params = Checker {
|
||||
perScript = const $ sequence_ controlFlowChecks,
|
||||
perToken = const $ return ()
|
||||
}
|
||||
|
||||
controlFlowChecks :: [ControlFlowCheck]
|
||||
controlFlowChecks = [
|
||||
runNodeChecks controlFlowNodeChecks
|
||||
]
|
||||
|
||||
controlFlowNodeChecks :: [ControlFlowNodeCheck]
|
||||
controlFlowNodeChecks = [
|
||||
runEffectChecks controlFlowEffectChecks
|
||||
]
|
||||
|
||||
controlFlowEffectChecks :: [ControlFlowEffectCheck]
|
||||
controlFlowEffectChecks = [
|
||||
]
|
||||
|
||||
runNodeChecks :: [ControlFlowNodeCheck] -> ControlFlowCheck
|
||||
runNodeChecks perNode = do
|
||||
cfg <- asks cfgAnalysis
|
||||
mapM_ runOnAll cfg
|
||||
where
|
||||
getData datas n@(node, label) = do
|
||||
(pre, post) <- M.lookup node datas
|
||||
return (n, (pre, post))
|
||||
|
||||
runOn :: (LNode CFNode, (ProgramState, ProgramState)) -> Analysis
|
||||
runOn (node, prepost) = mapM_ (\c -> c node prepost) perNode
|
||||
runOnAll cfg = mapM_ runOn $ mapMaybe (getData $ nodeToData cfg) $ labNodes (graph cfg)
|
||||
|
||||
runEffectChecks :: [ControlFlowEffectCheck] -> ControlFlowNodeCheck
|
||||
runEffectChecks list = checkNode
|
||||
where
|
||||
checkNode (node, label) prepost =
|
||||
case label of
|
||||
CFApplyEffects effects -> mapM_ (\effect -> mapM_ (\c -> c effect node prepost) list) effects
|
||||
_ -> return ()
|
||||
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
|
@ -1,7 +1,7 @@
|
|||
{-
|
||||
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
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2020 Vidar Holen
|
||||
Copyright 2012-2016 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -19,14 +19,12 @@
|
|||
-}
|
||||
{-# LANGUAGE TemplateHaskell #-}
|
||||
{-# LANGUAGE FlexibleContexts #-}
|
||||
{-# LANGUAGE ViewPatterns #-}
|
||||
module ShellCheck.Checks.ShellSupport (checker , ShellCheck.Checks.ShellSupport.runTests) where
|
||||
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.ASTLib
|
||||
import ShellCheck.AnalyzerLib
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
import ShellCheck.Regex
|
||||
|
||||
import Control.Monad
|
||||
|
@ -61,9 +59,6 @@ checks = [
|
|||
,checkBraceExpansionVars
|
||||
,checkMultiDimensionalArrays
|
||||
,checkPS1Assignments
|
||||
,checkMultipleBangs
|
||||
,checkBangAfterPipe
|
||||
,checkNegatedUnaryOps
|
||||
]
|
||||
|
||||
testChecker (ForShell _ t) =
|
||||
|
@ -77,79 +72,74 @@ 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_checkForDecimals3 = verifyNot checkForDecimals "declare -A foo; foo[1.2]=bar"
|
||||
checkForDecimals = ForShell [Sh, Dash, BusyboxSh, Bash] f
|
||||
checkForDecimals = ForShell [Sh, Dash, Bash] f
|
||||
where
|
||||
f t@(TA_Expansion id _) = sequence_ $ do
|
||||
first:rest <- getLiteralString t
|
||||
guard $ isDigit first && '.' `elem` rest
|
||||
str <- getLiteralString t
|
||||
first <- str !!! 0
|
||||
guard $ isDigit first && '.' `elem` str
|
||||
return $ err id 2079 "(( )) doesn't support decimals. Use bc or awk."
|
||||
f _ = return ()
|
||||
|
||||
|
||||
prop_checkBashisms = verify checkBashisms "while read a; do :; done < <(a)"
|
||||
prop_checkBashisms2 = verifyNot checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms2 = verify checkBashisms "[ foo -nt bar ]"
|
||||
prop_checkBashisms3 = verify checkBashisms "echo $((i++))"
|
||||
prop_checkBashisms4 = verify checkBashisms "rm !(*.hs)"
|
||||
prop_checkBashisms5 = verify checkBashisms "source file"
|
||||
prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
||||
prop_checkBashisms6b = verify checkBashisms "test \"$a\" == 42"
|
||||
prop_checkBashisms6c = verify checkBashisms "[ foo =~ bar ]"
|
||||
prop_checkBashisms6d = verify checkBashisms "test foo =~ bar"
|
||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||
prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}"
|
||||
prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}"
|
||||
prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}"
|
||||
prop_checkBashisms13 = verify checkBashisms "exec -c env"
|
||||
prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \""
|
||||
prop_checkBashisms15 = verify checkBashisms "let n++"
|
||||
prop_checkBashisms16 = verify checkBashisms "echo $RANDOM"
|
||||
prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
|
||||
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
|
||||
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
|
||||
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
|
||||
prop_checkBashisms21b = verify checkBashisms "test -a foo"
|
||||
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
|
||||
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
|
||||
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
|
||||
prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM"
|
||||
prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*"
|
||||
prop_checkBashisms28 = verify checkBashisms "exec {n}>&2"
|
||||
prop_checkBashisms29 = verify checkBashisms "echo ${!var}"
|
||||
prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\""
|
||||
prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\""
|
||||
prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }"
|
||||
prop_checkBashisms40 = verify checkBashisms "echo $(<file)"
|
||||
prop_checkBashisms41 = verify checkBashisms "echo `<file`"
|
||||
prop_checkBashisms42 = verify checkBashisms "trap foo int"
|
||||
prop_checkBashisms43 = verify checkBashisms "trap foo sigint"
|
||||
prop_checkBashisms44 = verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45 = verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46 = verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47 = verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||
prop_checkBashisms49 = verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
|
||||
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
|
||||
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
prop_checkBashisms56 = verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
prop_checkBashisms57 = verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
|
||||
prop_checkBashisms58 = verify checkBashisms "#!/bin/sh\nulimit -c 0"
|
||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||
prop_checkBashisms45= verifyNot checkBashisms "#!/bin/dash\ntrap foo INT"
|
||||
prop_checkBashisms46= verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
||||
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
|
||||
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"
|
||||
|
@ -190,85 +180,51 @@ prop_checkBashisms95 = verify checkBashisms "#!/bin/sh\necho $_"
|
|||
prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
||||
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||
prop_checkBashisms100 = verify checkBashisms "read -r"
|
||||
prop_checkBashisms101 = verify checkBashisms "read"
|
||||
prop_checkBashisms102 = verifyNot checkBashisms "read -r foo"
|
||||
prop_checkBashisms103 = verifyNot checkBashisms "read foo"
|
||||
prop_checkBashisms104 = verifyNot checkBashisms "read ''"
|
||||
prop_checkBashisms105 = verifyNot checkBashisms "#!/bin/busybox sh\nset -o pipefail"
|
||||
prop_checkBashisms106 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[[ \"$x\" = \"$x\" ]]"
|
||||
prop_checkBashisms107 = verifyNot checkBashisms "#!/bin/busybox sh\nx=x\n[ \"$x\" == \"$x\" ]"
|
||||
prop_checkBashisms108 = verifyNot checkBashisms "#!/bin/busybox sh\necho magic &> /dev/null"
|
||||
prop_checkBashisms109 = verifyNot checkBashisms "#!/bin/busybox sh\ntrap stop EXIT SIGTERM"
|
||||
prop_checkBashisms110 = verifyNot checkBashisms "#!/bin/busybox sh\nsource /dev/null"
|
||||
prop_checkBashisms111 = verify checkBashisms "#!/bin/dash\nx='test'\n${x:0:3}" -- SC3057
|
||||
prop_checkBashisms112 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x:0:3}" -- SC3057
|
||||
prop_checkBashisms113 = verify checkBashisms "#!/bin/dash\nx='test'\n${x/st/xt}" -- SC3060
|
||||
prop_checkBashisms114 = verifyNot checkBashisms "#!/bin/busybox sh\nx='test'\n${x/st/xt}" -- SC3060
|
||||
prop_checkBashisms115 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x}" -- SC3053
|
||||
prop_checkBashisms116 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x[1]}" -- SC3054
|
||||
prop_checkBashisms117 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${!x[@]}" -- SC3055
|
||||
prop_checkBashisms118 = verify checkBashisms "#!/bin/busybox sh\nxyz=1\n${!x*}" -- SC3056
|
||||
prop_checkBashisms119 = verify checkBashisms "#!/bin/busybox sh\nx='test'\n${x^^[t]}" -- SC3059
|
||||
prop_checkBashisms120 = verify checkBashisms "#!/bin/sh\n[ x == y ]"
|
||||
prop_checkBashisms121 = verifyNot checkBashisms "#!/bin/sh\n# shellcheck shell=busybox\n[ x == y ]"
|
||||
prop_checkBashisms122 = verify checkBashisms "#!/bin/dash\n$'a'"
|
||||
prop_checkBashisms123 = verifyNot checkBashisms "#!/bin/busybox sh\n$'a'"
|
||||
prop_checkBashisms124 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||
prop_checkBashisms125 = verifyNot checkBashisms "#!/bin/busybox sh\ntype -p test"
|
||||
prop_checkBashisms126 = verifyNot checkBashisms "#!/bin/busybox sh\nread -p foo -r bar"
|
||||
prop_checkBashisms127 = verifyNot checkBashisms "#!/bin/busybox sh\necho -ne foo"
|
||||
prop_checkBashisms128 = verify checkBashisms "#!/bin/dash\ntype -p test"
|
||||
prop_checkBashisms129 = verify checkBashisms "#!/bin/sh\n[ -k /tmp ]"
|
||||
prop_checkBashisms130 = verifyNot checkBashisms "#!/bin/dash\ntest -k /tmp"
|
||||
prop_checkBashisms131 = verify checkBashisms "#!/bin/sh\n[ -o errexit ]"
|
||||
checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
||||
prop_checkBashisms99 = verifyNot checkBashisms "#!/bin/dash\necho [^f]oo"
|
||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||
params <- ask
|
||||
kludge params t
|
||||
where
|
||||
-- This code was copy-pasted from Analytics where params was a variable
|
||||
kludge params = bashism
|
||||
where
|
||||
isBusyboxSh = shellType params == BusyboxSh
|
||||
isDash = shellType params == Dash || isBusyboxSh
|
||||
isDash = shellType params == Dash
|
||||
warnMsg id code s =
|
||||
if isDash
|
||||
then err id code $ "In dash, " ++ s ++ " not supported."
|
||||
else warn id code $ "In POSIX sh, " ++ s ++ " undefined."
|
||||
asStr = getLiteralString
|
||||
|
||||
bashism (T_ProcSub id _ _) = warnMsg id 3001 "process substitution is"
|
||||
bashism (T_Extglob id _ _) = warnMsg id 3002 "extglob is"
|
||||
bashism (T_DollarSingleQuoted id _) =
|
||||
unless isBusyboxSh $ warnMsg id 3003 "$'..' is"
|
||||
bashism (T_DollarSingleQuoted id _) = warnMsg id 3003 "$'..' is"
|
||||
bashism (T_DollarDoubleQuoted id _) = warnMsg id 3004 "$\"..\" is"
|
||||
bashism (T_ForArithmetic id _ _ _ _) = warnMsg id 3005 "arithmetic for loops are"
|
||||
bashism (T_Arithmetic id _) = warnMsg id 3006 "standalone ((..)) is"
|
||||
bashism (T_DollarBracket id _) = warnMsg id 3007 "$[..] in place of $((..)) is"
|
||||
bashism (T_SelectIn id _ _ _) = warnMsg id 3008 "select loops are"
|
||||
bashism (T_BraceExpansion id _) = warnMsg id 3009 "brace expansion is"
|
||||
bashism (T_Condition id DoubleBracket _) =
|
||||
unless isBusyboxSh $ warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_Condition id DoubleBracket _) = warnMsg id 3010 "[[ ]] is"
|
||||
bashism (T_HereString id _) = warnMsg id 3011 "here-strings are"
|
||||
|
||||
bashism (TC_Binary id _ op _ _) =
|
||||
checkTestOp bashismBinaryTestFlags op id
|
||||
bashism (T_SimpleCommand id _ [asStr -> Just "test", lhs, asStr -> Just op, rhs]) =
|
||||
checkTestOp bashismBinaryTestFlags op id
|
||||
bashism (TC_Unary id _ op _) =
|
||||
checkTestOp bashismUnaryTestFlags op id
|
||||
bashism (T_SimpleCommand id _ [asStr -> Just "test", asStr -> Just op, _]) =
|
||||
checkTestOp bashismUnaryTestFlags op id
|
||||
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="] =
|
||||
unless isDash $ warnMsg id 3012 $ "lexicographical " ++ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket op _ _)
|
||||
| op `elem` [ "-ot", "-nt", "-ef" ] =
|
||||
unless isDash $ warnMsg id 3013 $ op ++ " is"
|
||||
bashism (TC_Binary id SingleBracket "==" _ _) =
|
||||
warnMsg id 3014 "== in place of = is"
|
||||
bashism (TC_Binary id SingleBracket "=~" _ _) =
|
||||
warnMsg id 3015 "=~ regex matching is"
|
||||
bashism (TC_Unary id SingleBracket "-v" _) =
|
||||
warnMsg id 3016 "unary -v (in place of [ -n \"${var+x}\" ]) is"
|
||||
bashism (TC_Unary id _ "-a" _) =
|
||||
warnMsg id 3017 "unary -a in place of -e is"
|
||||
bashism (TA_Unary id op _)
|
||||
| op `elem` [ "|++", "|--", "++|", "--|"] =
|
||||
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) =
|
||||
unless isBusyboxSh $ warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) file)) =
|
||||
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
|
||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> is"
|
||||
bashism (T_FdRedirect id "" (T_IoFile _ (T_GREATAND _) _)) = warnMsg id 3021 ">& is"
|
||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||
bashism (T_FdRedirect id num _)
|
||||
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||
|
@ -279,15 +235,14 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
where
|
||||
file = onlyLiteralString word
|
||||
isNetworked = any (`isPrefixOf` file) ["/dev/tcp", "/dev/udp"]
|
||||
bashism (T_Glob id str) | "[^" `isInfixOf` str =
|
||||
bashism (T_Glob id str) | not isDash && "[^" `isInfixOf` str =
|
||||
warnMsg id 3026 "^ in place of ! in glob bracket expressions is"
|
||||
|
||||
bashism t@(TA_Variable id str _) | isBashVariable str =
|
||||
warnMsg id 3028 $ str ++ " is"
|
||||
|
||||
bashism t@(T_DollarBraced id _ token) = do
|
||||
unless isBusyboxSh $ mapM_ check simpleExpansions
|
||||
mapM_ check advancedExpansions
|
||||
mapM_ check expansion
|
||||
when (isBashVariable var) $
|
||||
warnMsg id 3028 $ var ++ " is"
|
||||
where
|
||||
|
@ -315,11 +270,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| t `isCommand` "echo" && argString `matches` flagRegex =
|
||||
if isBusyboxSh
|
||||
then
|
||||
unless (argString `matches` busyboxFlagRegex) $
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n and -e"
|
||||
else if isDash
|
||||
if isDash
|
||||
then
|
||||
when (argString /= "-n") $
|
||||
warnMsg (getId arg) 3036 "echo flags besides -n"
|
||||
|
@ -328,7 +279,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
where
|
||||
argString = concat $ oversimplify arg
|
||||
flagRegex = mkRegex "^-[eEsn]+$"
|
||||
busyboxFlagRegex = mkRegex "^-[en]+$"
|
||||
|
||||
bashism t@(T_SimpleCommand _ _ (cmd:arg:_))
|
||||
| getLiteralString cmd == Just "exec" && "-" `isPrefixOf` concat (oversimplify arg) =
|
||||
|
@ -402,8 +352,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
(\x -> (not . null . snd $ x) && snd x `notElem` allowed) flags
|
||||
return . warnMsg (getId word) 3045 $ name ++ " -" ++ flag ++ " is"
|
||||
|
||||
when (name == "source" && not isBusyboxSh) $
|
||||
warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "source") $ warnMsg id 3046 "'source' in place of '.' is"
|
||||
when (name == "trap") $
|
||||
let
|
||||
check token = sequence_ $ do
|
||||
|
@ -412,7 +361,7 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
return $ do
|
||||
when (upper `elem` ["ERR", "DEBUG", "RETURN"]) $
|
||||
warnMsg (getId token) 3047 $ "trapping " ++ str ++ " is"
|
||||
when (not isBusyboxSh && "SIG" `isPrefixOf` upper) $
|
||||
when ("SIG" `isPrefixOf` upper) $
|
||||
warnMsg (getId token) 3048
|
||||
"prefixing signal names with 'SIG' is"
|
||||
when (not isDash && upper /= str) $
|
||||
|
@ -426,9 +375,6 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
let literal = onlyLiteralString format
|
||||
guard $ "%q" `isInfixOf` literal
|
||||
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||
|
||||
when (name == "read" && all isFlag rest) $
|
||||
warnMsg (getId cmd) 3061 "read without a variable is"
|
||||
where
|
||||
unsupportedCommands = [
|
||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||
|
@ -442,19 +388,17 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
("hash", Just $ if isDash then ["r", "v"] else ["r"]),
|
||||
("jobs", Just ["l", "p"]),
|
||||
("printf", Just []),
|
||||
("read", Just $ if isDash || isBusyboxSh then ["r", "p"] else ["r"]),
|
||||
("read", Just $ if isDash then ["r", "p"] else ["r"]),
|
||||
("readonly", Just ["p"]),
|
||||
("trap", Just []),
|
||||
("type", Just $ if isBusyboxSh then ["p"] else []),
|
||||
("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 _)
|
||||
| getCommandName src == Just "source" =
|
||||
unless isBusyboxSh $
|
||||
warnMsg id 3051 "'source' in place of '.' is"
|
||||
| getCommandName src == Just "source" = warnMsg id 3051 "'source' in place of '.' is"
|
||||
bashism (TA_Expansion _ (T_Literal id str : _))
|
||||
| str `matches` radix = warnMsg id 3052 "arithmetic base conversion is"
|
||||
where
|
||||
|
@ -462,16 +406,14 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
bashism _ = return ()
|
||||
|
||||
varChars="_0-9a-zA-Z"
|
||||
advancedExpansions = let re = mkRegex in [
|
||||
expansion = let re = mkRegex in [
|
||||
(re $ "^![" ++ varChars ++ "]", 3053, "indirect expansion is"),
|
||||
(re $ "^[" ++ varChars ++ "]+\\[.*\\]$", 3054, "array references are"),
|
||||
(re $ "^![" ++ varChars ++ "]+\\[[*@]]$", 3055, "array key expansion is"),
|
||||
(re $ "^![" ++ varChars ++ "]+[*@]$", 3056, "name matching prefixes are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is")
|
||||
]
|
||||
simpleExpansions = let re = mkRegex in [
|
||||
(re $ "^[" ++ varChars ++ "*@]+:[^-=?+]", 3057, "string indexing is"),
|
||||
(re $ "^([*@][%#]|#[@*])", 3058, "string operations on $@/$* are"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?[,^]", 3059, "case modification is"),
|
||||
(re $ "^[" ++ varChars ++ "*@]+(\\[.*\\])?/", 3060, "string replacement is")
|
||||
]
|
||||
bashVars = [
|
||||
|
@ -497,54 +439,10 @@ checkBashisms = ForShell [Sh, Dash, BusyboxSh] $ \t -> do
|
|||
Assignment (_, _, name, _) -> name == var
|
||||
_ -> False
|
||||
|
||||
checkTestOp table op id = sequence_ $ do
|
||||
(code, shells, msg) <- Map.lookup op table
|
||||
guard . not $ shellType params `elem` shells
|
||||
return $ warnMsg id code (msg op)
|
||||
|
||||
|
||||
buildTestFlagMap list = Map.fromList $ concatMap (\(x,y) -> map (\c -> (c,y)) x) list
|
||||
bashismBinaryTestFlags = buildTestFlagMap [
|
||||
-- ([list of applicable flags],
|
||||
-- (error code, exempt shells, message builder :: String -> String)),
|
||||
--
|
||||
-- Distinct error codes allow the wiki to give more helpful, targeted
|
||||
-- information.
|
||||
(["<", ">", "\\<", "\\>", "<=", ">=", "\\<=", "\\>="],
|
||||
(3012, [Dash, BusyboxSh], \op -> "lexicographical " ++ op ++ " is")),
|
||||
(["=="],
|
||||
(3014, [BusyboxSh], \op -> op ++ " in place of = is")),
|
||||
(["=~"],
|
||||
(3015, [], \op -> op ++ " regex matching is")),
|
||||
|
||||
([], (0,[],const ""))
|
||||
]
|
||||
bashismUnaryTestFlags = buildTestFlagMap [
|
||||
(["-v"],
|
||||
(3016, [], \op -> "test " ++ op ++ " (in place of [ -n \"${var+x}\" ]) is")),
|
||||
(["-a"],
|
||||
(3017, [], \op -> "unary " ++ op ++ " in place of -e is")),
|
||||
(["-o"],
|
||||
(3062, [], \op -> "test " ++ op ++ " to check options is")),
|
||||
(["-R"],
|
||||
(3063, [], \op -> "test " ++ op ++ " and namerefs in general are")),
|
||||
(["-N"],
|
||||
(3064, [], \op -> "test " ++ op ++ " is")),
|
||||
(["-k"],
|
||||
(3065, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||
(["-G"],
|
||||
(3066, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||
(["-O"],
|
||||
(3067, [Dash, BusyboxSh], \op -> "test " ++ op ++ " is")),
|
||||
|
||||
([], (0,[],const ""))
|
||||
]
|
||||
|
||||
|
||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
||||
prop_checkEchoSed1b = verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
|
||||
prop_checkEchoSed1b= verify checkEchoSed "FOO=$(sed 's/foo/bar/g' <<< \"$cow\")"
|
||||
prop_checkEchoSed2 = verify checkEchoSed "rm $(echo $cow | sed -e 's,foo,bar,')"
|
||||
prop_checkEchoSed2b = verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
|
||||
prop_checkEchoSed2b= verify checkEchoSed "rm $(sed -e 's,foo,bar,' <<< $cow)"
|
||||
checkEchoSed = ForShell [Bash, Ksh] f
|
||||
where
|
||||
f (T_Redirecting id lefts r) =
|
||||
|
@ -630,11 +528,11 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
|||
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||
|
||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPS11a = verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPS11a= verify checkPS1Assignments "export PS1='\\033[1;35m\\$ '"
|
||||
prop_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
||||
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
||||
prop_checkPS14a = verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
||||
prop_checkPS14a= verify checkPS1Assignments "export PS1=$'\\e[3m; '"
|
||||
prop_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
||||
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
||||
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
|
||||
|
@ -656,46 +554,5 @@ checkPS1Assignments = ForShell [Bash] f
|
|||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||
|
||||
|
||||
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
|
||||
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
|
||||
checkMultipleBangs = ForShell [Dash, BusyboxSh, Sh] f
|
||||
where
|
||||
f token = case token of
|
||||
T_Banged id (T_Banged _ _) ->
|
||||
err id 2325 "Multiple ! in front of pipelines are a bash/ksh extension. Use only 0 or 1."
|
||||
_ -> return ()
|
||||
|
||||
|
||||
prop_checkBangAfterPipe1 = verify checkBangAfterPipe "true | ! true"
|
||||
prop_checkBangAfterPipe2 = verifyNot checkBangAfterPipe "true | ( ! true )"
|
||||
prop_checkBangAfterPipe3 = verifyNot checkBangAfterPipe "! ! true | true"
|
||||
checkBangAfterPipe = ForShell [Dash, BusyboxSh, Sh, Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
T_Pipeline _ _ cmds -> mapM_ check cmds
|
||||
_ -> return ()
|
||||
|
||||
check token = case token of
|
||||
T_Banged id _ ->
|
||||
err id 2326 "! is not allowed in the middle of pipelines. Use command group as in cmd | { ! cmd; } if necessary."
|
||||
_ -> return ()
|
||||
|
||||
|
||||
prop_checkNegatedUnaryOps1 = verify checkNegatedUnaryOps "[ ! -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps2 = verifyNot checkNegatedUnaryOps "[ -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps3 = verifyNot checkNegatedUnaryOps "[[ ! -o braceexpand ]]"
|
||||
prop_checkNegatedUnaryOps4 = verifyNot checkNegatedUnaryOps "! [ -o braceexpand ]"
|
||||
prop_checkNegatedUnaryOps5 = verify checkNegatedUnaryOps "[ ! -a file ]"
|
||||
checkNegatedUnaryOps = ForShell [Bash] f
|
||||
where
|
||||
f token = case token of
|
||||
TC_Unary id SingleBracket "!" (TC_Unary _ _ op _) | op `elem` ["-a", "-o"] ->
|
||||
err id 2332 $ msg op
|
||||
_ -> return ()
|
||||
|
||||
msg "-o" = "[ ! -o opt ] is always true because -o becomes logical OR. Use [[ ]] or ! [ -o opt ]."
|
||||
msg "-a" = "[ ! -a file ] is always true because -a becomes logical AND. Use -e instead."
|
||||
msg _ = pleaseReport "unhandled negated unary message"
|
||||
|
||||
return []
|
||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||
|
|
|
@ -2,27 +2,9 @@ module ShellCheck.Data where
|
|||
|
||||
import ShellCheck.Interface
|
||||
import Data.Version (showVersion)
|
||||
|
||||
|
||||
{-
|
||||
If you are here because you saw an error about Paths_ShellCheck in this file,
|
||||
simply comment out the import below and define the version as a constant string.
|
||||
|
||||
Instead of:
|
||||
|
||||
import Paths_ShellCheck (version)
|
||||
shellcheckVersion = showVersion version
|
||||
|
||||
Use:
|
||||
|
||||
-- import Paths_ShellCheck (version)
|
||||
shellcheckVersion = "kludge"
|
||||
|
||||
-}
|
||||
|
||||
import Paths_ShellCheck (version)
|
||||
shellcheckVersion = showVersion version -- VERSIONSTRING
|
||||
|
||||
shellcheckVersion = showVersion version
|
||||
|
||||
internalVariables = [
|
||||
-- Generic
|
||||
|
@ -30,27 +12,23 @@ internalVariables = [
|
|||
|
||||
-- Bash
|
||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
|
||||
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
|
||||
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
|
||||
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
|
||||
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
|
||||
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
|
||||
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
|
||||
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
|
||||
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
|
||||
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
|
||||
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
|
||||
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
||||
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
||||
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
||||
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
||||
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
||||
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
||||
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
||||
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
||||
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||
"IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
|
||||
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
|
||||
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
|
||||
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
|
||||
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
|
||||
"BASH_MONOSECONDS", "BASH_TRAPSIG", "GLOBSORT",
|
||||
"auto_resume", "histchars",
|
||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
||||
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
||||
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
||||
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
||||
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
||||
|
||||
-- Other
|
||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||
|
@ -63,23 +41,15 @@ internalVariables = [
|
|||
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
||||
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
||||
"flags_error", "flags_return"
|
||||
|
||||
-- Bats
|
||||
,"stderr", "stderr_lines"
|
||||
]
|
||||
|
||||
specialIntegerVariables = [
|
||||
"$", "?", "!", "#"
|
||||
specialVariablesWithoutSpaces = [
|
||||
"$", "-", "?", "!", "#"
|
||||
]
|
||||
|
||||
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
|
||||
|
||||
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
|
||||
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
|
||||
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
|
||||
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
|
||||
"HISTSIZE", "LINES", "BASH_MONOSECONDS", "BASH_TRAPSIG"
|
||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
||||
|
||||
-- shflags
|
||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||
|
@ -125,10 +95,10 @@ commonCommands = [
|
|||
|
||||
nonReadingCommands = [
|
||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
|
||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
|
||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
||||
]
|
||||
|
||||
sampleWords = [
|
||||
|
@ -160,18 +130,11 @@ shellForExecutable name =
|
|||
"sh" -> return Sh
|
||||
"bash" -> return Bash
|
||||
"bats" -> return Bash
|
||||
"busybox" -> return BusyboxSh -- Used for directives and --shell=busybox
|
||||
"busybox sh" -> return BusyboxSh
|
||||
"busybox ash" -> return BusyboxSh
|
||||
"dash" -> return Dash
|
||||
"ash" -> return Dash -- There's also a warning for this.
|
||||
"ksh" -> return Ksh
|
||||
"ksh88" -> return Ksh
|
||||
"ksh93" -> return Ksh
|
||||
"oksh" -> return Ksh
|
||||
_ -> Nothing
|
||||
|
||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||
flagsForMapfile = "d:n:O:s:u:C:c:t"
|
||||
|
||||
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
||||
|
|
|
@ -1,313 +0,0 @@
|
|||
{-
|
||||
|
||||
This file contains useful functions for debugging and developing ShellCheck.
|
||||
|
||||
To invoke them interactively, run:
|
||||
|
||||
cabal repl
|
||||
|
||||
At the ghci prompt, enter:
|
||||
|
||||
:load ShellCheck.Debug
|
||||
|
||||
You can now invoke the functions. Here are some examples:
|
||||
|
||||
shellcheckString "echo $1"
|
||||
stringToAst "(( x+1 ))"
|
||||
stringToCfg "if foo; then bar; else baz; fi"
|
||||
writeFile "/tmp/test.dot" $ stringToCfgViz "while foo; do bar; done"
|
||||
|
||||
The latter file can be rendered to png with GraphViz:
|
||||
|
||||
dot -Tpng /tmp/test.dot > /tmp/test.png
|
||||
|
||||
To run all unit tests in a module:
|
||||
|
||||
ShellCheck.Parser.runTests
|
||||
ShellCheck.Analytics.runTests
|
||||
|
||||
To run a specific test:
|
||||
|
||||
:load ShellCheck.Analytics
|
||||
prop_checkUuoc3
|
||||
|
||||
If you make code changes, reload in seconds at any time with:
|
||||
|
||||
:r
|
||||
|
||||
===========================================================================
|
||||
|
||||
Crash course in printf debugging in Haskell:
|
||||
|
||||
import Debug.Trace
|
||||
|
||||
greet 0 = return ()
|
||||
-- Print when a function is invoked
|
||||
greet n | trace ("calling greet " ++ show n) False = undefined
|
||||
greet n = do
|
||||
putStrLn "Enter name"
|
||||
name <- getLine
|
||||
-- Print at some point in any monadic function
|
||||
traceM $ "user entered " ++ name
|
||||
putStrLn $ "Hello " ++ name
|
||||
-- Print a value before passing it on
|
||||
greet $ traceShowId (n - 1)
|
||||
|
||||
|
||||
===========================================================================
|
||||
|
||||
If you want to invoke `ghci` directly, such as on `shellcheck.hs`, to
|
||||
debug all of ShellCheck including I/O, you may see an error like this:
|
||||
|
||||
src/ShellCheck/Data.hs:5:1: error:
|
||||
Could not load module ‘Paths_ShellCheck’
|
||||
it is a hidden module in the package ‘ShellCheck-0.8.0’
|
||||
|
||||
This can easily be circumvented by running `./setgitversion` or manually
|
||||
editing src/ShellCheck/Data.hs to replace the auto-deduced version number
|
||||
with a constant string as indicated.
|
||||
|
||||
Afterwards, you can run the ShellCheck tool, as if from the shell, with:
|
||||
|
||||
$ ghci shellcheck.hs
|
||||
ghci> runMain ["-x", "file.sh"]
|
||||
|
||||
-}
|
||||
|
||||
module ShellCheck.Debug () where
|
||||
|
||||
import ShellCheck.Analyzer
|
||||
import ShellCheck.AST
|
||||
import ShellCheck.CFG
|
||||
import ShellCheck.Checker
|
||||
import ShellCheck.CFGAnalysis as CF
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Parser
|
||||
import ShellCheck.Prelude
|
||||
|
||||
import Control.Monad
|
||||
import Control.Monad.Identity
|
||||
import Control.Monad.RWS
|
||||
import Control.Monad.Writer
|
||||
import Data.Graph.Inductive.Graph as G
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import qualified Data.Map as M
|
||||
import qualified Data.Set as S
|
||||
|
||||
|
||||
-- Run all of ShellCheck (minus output formatters)
|
||||
shellcheckString :: String -> CheckResult
|
||||
shellcheckString scriptString =
|
||||
runIdentity $ checkScript dummySystemInterface checkSpec
|
||||
where
|
||||
checkSpec :: CheckSpec
|
||||
checkSpec = emptyCheckSpec {
|
||||
csScript = scriptString
|
||||
}
|
||||
|
||||
dummySystemInterface :: SystemInterface Identity
|
||||
dummySystemInterface = mockedSystemInterface [
|
||||
-- A tiny, fake filesystem for sourced files
|
||||
("lib/mylib1.sh", "foo=$(cat $1 | wc -l)"),
|
||||
("lib/mylib2.sh", "bar=42")
|
||||
]
|
||||
|
||||
-- Parameters used when generating Control Flow Graphs
|
||||
cfgParams :: CFGParameters
|
||||
cfgParams = CFGParameters {
|
||||
cfLastpipe = False,
|
||||
cfPipefail = False
|
||||
}
|
||||
|
||||
-- An example script to play with
|
||||
exampleScript :: String
|
||||
exampleScript = unlines [
|
||||
"#!/bin/sh",
|
||||
"count=0",
|
||||
"for file in *",
|
||||
"do",
|
||||
" (( count++ ))",
|
||||
"done",
|
||||
"echo $count"
|
||||
]
|
||||
|
||||
-- Parse the script string into ShellCheck's ParseResult
|
||||
parseScriptString :: String -> ParseResult
|
||||
parseScriptString scriptString =
|
||||
runIdentity $ parseScript dummySystemInterface parseSpec
|
||||
where
|
||||
parseSpec :: ParseSpec
|
||||
parseSpec = newParseSpec {
|
||||
psFilename = "myscript",
|
||||
psScript = scriptString
|
||||
}
|
||||
|
||||
|
||||
-- Parse the script string into an Abstract Syntax Tree
|
||||
stringToAst :: String -> Token
|
||||
stringToAst scriptString =
|
||||
case maybeRoot of
|
||||
Just root -> root
|
||||
Nothing -> error $ "Script failed to parse: " ++ show parserWarnings
|
||||
where
|
||||
parseResult :: ParseResult
|
||||
parseResult = parseScriptString scriptString
|
||||
|
||||
maybeRoot :: Maybe Token
|
||||
maybeRoot = prRoot parseResult
|
||||
|
||||
parserWarnings :: [PositionedComment]
|
||||
parserWarnings = prComments parseResult
|
||||
|
||||
|
||||
astToCfgResult :: Token -> CFGResult
|
||||
astToCfgResult = buildGraph cfgParams
|
||||
|
||||
astToDfa :: Token -> CFGAnalysis
|
||||
astToDfa = analyzeControlFlow cfgParams
|
||||
|
||||
astToCfg :: Token -> CFGraph
|
||||
astToCfg = cfGraph . astToCfgResult
|
||||
|
||||
stringToCfg :: String -> CFGraph
|
||||
stringToCfg = astToCfg . stringToAst
|
||||
|
||||
stringToDfa :: String -> CFGAnalysis
|
||||
stringToDfa = astToDfa . stringToAst
|
||||
|
||||
cfgToGraphViz :: CFGraph -> String
|
||||
cfgToGraphViz = cfgToGraphVizWith show
|
||||
|
||||
stringToCfgViz :: String -> String
|
||||
stringToCfgViz = cfgToGraphViz . stringToCfg
|
||||
|
||||
stringToDfaViz :: String -> String
|
||||
stringToDfaViz = dfaToGraphViz . stringToDfa
|
||||
|
||||
-- Dump a Control Flow Graph as GraphViz with extended information
|
||||
stringToDetailedCfgViz :: String -> String
|
||||
stringToDetailedCfgViz scriptString = cfgToGraphVizWith nodeLabel graph
|
||||
where
|
||||
ast :: Token
|
||||
ast = stringToAst scriptString
|
||||
|
||||
cfgResult :: CFGResult
|
||||
cfgResult = astToCfgResult ast
|
||||
|
||||
graph :: CFGraph
|
||||
graph = cfGraph cfgResult
|
||||
|
||||
idToToken :: M.Map Id Token
|
||||
idToToken = M.fromList $ execWriter $ doAnalysis (\c -> tell [(getId c, c)]) ast
|
||||
|
||||
idToNode :: M.Map Id (Node, Node)
|
||||
idToNode = cfIdToRange cfgResult
|
||||
|
||||
nodeToStartIds :: M.Map Node (S.Set Id)
|
||||
nodeToStartIds =
|
||||
M.fromListWith S.union $
|
||||
map (\(id, (start, _)) -> (start, S.singleton id)) $
|
||||
M.toList idToNode
|
||||
|
||||
nodeToEndIds :: M.Map Node (S.Set Id)
|
||||
nodeToEndIds =
|
||||
M.fromListWith S.union $
|
||||
map (\(id, (_, end)) -> (end, S.singleton id)) $
|
||||
M.toList idToNode
|
||||
|
||||
formatId :: Id -> String
|
||||
formatId id = fromMaybe ("Unknown " ++ show id) $ do
|
||||
(OuterToken _ token) <- M.lookup id idToToken
|
||||
firstWord <- words (show token) !!! 0
|
||||
-- Strip off "Inner_"
|
||||
(_ : tokenName) <- return $ dropWhile (/= '_') firstWord
|
||||
return $ tokenName ++ " " ++ show id
|
||||
|
||||
formatGroup :: S.Set Id -> String
|
||||
formatGroup set = intercalate ", " $ map formatId $ S.toList set
|
||||
|
||||
nodeLabel (node, label) = unlines [
|
||||
show node ++ ". " ++ show label,
|
||||
"Begin: " ++ formatGroup (M.findWithDefault S.empty node nodeToStartIds),
|
||||
"End: " ++ formatGroup (M.findWithDefault S.empty node nodeToEndIds)
|
||||
]
|
||||
|
||||
|
||||
-- Dump a Control Flow Graph with Data Flow Analysis as GraphViz
|
||||
dfaToGraphViz :: CF.CFGAnalysis -> String
|
||||
dfaToGraphViz analysis = cfgToGraphVizWith label $ CF.graph analysis
|
||||
where
|
||||
label (node, label) =
|
||||
let
|
||||
desc = show node ++ ". " ++ show label
|
||||
in
|
||||
fromMaybe ("No DFA available\n\n" ++ desc) $ do
|
||||
(pre, post) <- M.lookup node $ CF.nodeToData analysis
|
||||
return $ unlines [
|
||||
"Precondition: " ++ show pre,
|
||||
"",
|
||||
desc,
|
||||
"",
|
||||
"Postcondition: " ++ show post
|
||||
]
|
||||
|
||||
|
||||
-- Dump an Control Flow Graph to GraphViz with a given node formatter
|
||||
cfgToGraphVizWith :: (LNode CFNode -> String) -> CFGraph -> String
|
||||
cfgToGraphVizWith nodeLabel graph = concat [
|
||||
"digraph {\n",
|
||||
concatMap dumpNode (labNodes graph),
|
||||
concatMap dumpLink (labEdges graph),
|
||||
tagVizEntries graph,
|
||||
"}\n"
|
||||
]
|
||||
where
|
||||
dumpNode l@(node, label) = show node ++ " [label=" ++ quoteViz (nodeLabel l) ++ "]\n"
|
||||
dumpLink (from, to, typ) = show from ++ " -> " ++ show to ++ " [style=" ++ quoteViz (edgeStyle typ) ++ "]\n"
|
||||
edgeStyle CFEFlow = "solid"
|
||||
edgeStyle CFEExit = "bold"
|
||||
edgeStyle CFEFalseFlow = "dotted"
|
||||
|
||||
quoteViz str = "\"" ++ escapeViz str ++ "\""
|
||||
escapeViz [] = []
|
||||
escapeViz (c:rest) =
|
||||
case c of
|
||||
'\"' -> '\\' : '\"' : escapeViz rest
|
||||
'\n' -> '\\' : 'l' : escapeViz rest
|
||||
'\\' -> '\\' : '\\' : escapeViz rest
|
||||
_ -> c : escapeViz rest
|
||||
|
||||
|
||||
-- Dump an Abstract Syntax Tree (or branch thereof) to GraphViz format
|
||||
astToGraphViz :: Token -> String
|
||||
astToGraphViz token = concat [
|
||||
"digraph {\n",
|
||||
formatTree token,
|
||||
"}\n"
|
||||
]
|
||||
where
|
||||
formatTree :: Token -> String
|
||||
formatTree t = snd $ execRWS (doStackAnalysis push pop t) () []
|
||||
|
||||
push :: Token -> RWS () String [Int] ()
|
||||
push (OuterToken (Id n) inner) = do
|
||||
stack <- get
|
||||
put (n : stack)
|
||||
case stack of
|
||||
[] -> return ()
|
||||
(top:_) -> tell $ show top ++ " -> " ++ show n ++ "\n"
|
||||
tell $ show n ++ " [label=" ++ quoteViz (show n ++ ": " ++ take 32 (show inner)) ++ "]\n"
|
||||
|
||||
pop :: Token -> RWS () String [Int] ()
|
||||
pop _ = modify tail
|
||||
|
||||
|
||||
-- For each entry point, set the rank so that they'll align in the graph
|
||||
tagVizEntries :: CFGraph -> String
|
||||
tagVizEntries graph = "{ rank=same " ++ rank ++ " }"
|
||||
where
|
||||
entries = mapMaybe find $ labNodes graph
|
||||
find (node, CFEntryPoint name) = return (node, name)
|
||||
find _ = Nothing
|
||||
rank = unwords $ map (\(c, _) -> show c) entries
|
|
@ -22,8 +22,6 @@
|
|||
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
||||
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
import Control.Monad
|
||||
import Control.Monad.State
|
||||
import Data.Array
|
||||
import Data.List
|
||||
|
@ -37,7 +35,7 @@ class Ranged a where
|
|||
end :: a -> Position
|
||||
overlap :: a -> a -> Bool
|
||||
overlap x y =
|
||||
xEnd > yStart && yEnd > xStart
|
||||
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
|
||||
where
|
||||
yStart = start y
|
||||
yEnd = end y
|
||||
|
@ -88,7 +86,6 @@ instance Ranged Replacement where
|
|||
instance Monoid Fix where
|
||||
mempty = newFix
|
||||
mappend = (<>)
|
||||
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
|
||||
|
||||
instance Semigroup Fix where
|
||||
f1 <> f2 =
|
||||
|
@ -231,7 +228,7 @@ applyReplacement2 rep string = do
|
|||
|
||||
let (l1, l2) = tmap posLine originalPos in
|
||||
when (l1 /= 1 || l2 /= 1) $
|
||||
error $ pleaseReport "bad cross-line fix"
|
||||
error "ShellCheck internal error, please report: bad cross-line fix"
|
||||
|
||||
let replacer = repString rep
|
||||
let shift = (length replacer) - (oldEnd - oldStart)
|
||||
|
|
|
@ -24,8 +24,8 @@ import ShellCheck.Formatter.Format
|
|||
|
||||
import Data.Char
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
|
||||
format :: IO Formatter
|
||||
format = return Formatter {
|
||||
|
@ -45,12 +45,12 @@ outputResults cr sys =
|
|||
else mapM_ outputGroup fileGroups
|
||||
where
|
||||
comments = crComments cr
|
||||
fileGroups = NE.groupWith sourceFile comments
|
||||
fileGroups = groupWith sourceFile comments
|
||||
outputGroup group = do
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let filename = sourceFile (head group)
|
||||
result <- (siReadFile sys) filename
|
||||
let contents = either (const "") id result
|
||||
outputFile filename contents (NE.toList group)
|
||||
outputFile filename contents group
|
||||
|
||||
outputFile filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
|
@ -88,7 +88,7 @@ outputError file error = putStrLn $ concat [
|
|||
attr s v = concat [ s, "='", escape v, "' " ]
|
||||
escape = concatMap escape'
|
||||
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
|
||||
isOk x = any ($ x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
|
||||
isOk x = any ($x) [isAsciiUpper, isAsciiLower, isDigit, (`elem` " ./")]
|
||||
|
||||
severity "error" = "error"
|
||||
severity "warning" = "warning"
|
||||
|
|
|
@ -38,6 +38,9 @@ import System.FilePath
|
|||
|
||||
import Test.QuickCheck
|
||||
|
||||
import Debug.Trace
|
||||
ltt x = trace (show x) x
|
||||
|
||||
format :: FormatterOptions -> IO Formatter
|
||||
format options = do
|
||||
foundIssues <- newIORef False
|
||||
|
@ -87,7 +90,7 @@ reportResult foundIssues reportedIssues color result sys = do
|
|||
mapM_ output $ M.toList fixmap
|
||||
where
|
||||
output (name, fix) = do
|
||||
file <- siReadFile sys (Just True) name
|
||||
file <- (siReadFile sys) name
|
||||
case file of
|
||||
Right contents -> do
|
||||
putStrLn $ formatDoc color $ makeDiff name contents fix
|
||||
|
@ -203,9 +206,10 @@ formatDoc color (DiffDoc name lf regions) =
|
|||
buildFixMap :: [Fix] -> M.Map String Fix
|
||||
buildFixMap fixes = perFile
|
||||
where
|
||||
splitFixes = splitFixByFile $ mconcat fixes
|
||||
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
|
||||
|
|
|
@ -28,7 +28,6 @@ import Data.Array
|
|||
import Data.List
|
||||
import System.IO
|
||||
import System.Info
|
||||
import System.Environment
|
||||
|
||||
-- A formatter that carries along an arbitrary piece of data
|
||||
data Formatter = Formatter {
|
||||
|
@ -69,14 +68,12 @@ makeNonVirtual comments contents =
|
|||
|
||||
|
||||
shouldOutputColor :: ColorOption -> IO Bool
|
||||
shouldOutputColor colorOption =
|
||||
case colorOption of
|
||||
ColorAlways -> return True
|
||||
ColorNever -> return False
|
||||
ColorAuto -> do
|
||||
isTerminal <- hIsTerminalDevice stdout
|
||||
term <- lookupEnv "TERM"
|
||||
let windows = "mingw" `isPrefixOf` os
|
||||
let dumbTerm = term `elem` [Just "dumb", Just "", Nothing]
|
||||
let isUsableTty = isTerminal && not windows && not dumbTerm
|
||||
return isUsableTty
|
||||
shouldOutputColor colorOption = do
|
||||
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 useColor
|
||||
|
|
|
@ -23,8 +23,8 @@ import ShellCheck.Interface
|
|||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Data.List
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
|
||||
format :: IO Formatter
|
||||
format = return Formatter {
|
||||
|
@ -39,13 +39,13 @@ outputError file error = hPutStrLn stderr $ file ++ ": " ++ error
|
|||
outputAll cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = NE.groupWith sourceFile comments
|
||||
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let filename = sourceFile (head group)
|
||||
result <- (siReadFile sys) filename
|
||||
let contents = either (const "") id result
|
||||
outputResult filename contents (NE.toList group)
|
||||
outputResult filename contents group
|
||||
|
||||
outputResult filename contents warnings = do
|
||||
let comments = makeNonVirtual warnings contents
|
||||
|
|
|
@ -23,7 +23,6 @@ module ShellCheck.Formatter.JSON (format) where
|
|||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
|
@ -104,7 +103,7 @@ collectResult ref cr sys = mapM_ f groups
|
|||
comments = crComments cr
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
|
||||
f group = modifyIORef ref (\x -> comments ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
|
|
|
@ -23,13 +23,12 @@ module ShellCheck.Formatter.JSON1 (format) where
|
|||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Data.Aeson
|
||||
import Data.IORef
|
||||
import Data.Monoid
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
|
||||
format :: IO Formatter
|
||||
format = do
|
||||
|
@ -114,14 +113,14 @@ outputError file msg = hPutStrLn stderr $ file ++ ": " ++ msg
|
|||
collectResult ref cr sys = mapM_ f groups
|
||||
where
|
||||
comments = crComments cr
|
||||
groups = NE.groupWith sourceFile comments
|
||||
f :: NE.NonEmpty PositionedComment -> IO ()
|
||||
groups = groupWith sourceFile comments
|
||||
f :: [PositionedComment] -> IO ()
|
||||
f group = do
|
||||
let filename = sourceFile (NE.head group)
|
||||
result <- siReadFile sys (Just True) filename
|
||||
let filename = sourceFile (head group)
|
||||
result <- siReadFile sys filename
|
||||
let contents = either (const "") id result
|
||||
let comments' = makeNonVirtual comments contents
|
||||
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
|
||||
modifyIORef ref (\x -> comments' ++ x)
|
||||
|
||||
finish ref = do
|
||||
list <- readIORef ref
|
||||
|
|
|
@ -23,7 +23,6 @@ import ShellCheck.Fixer
|
|||
import ShellCheck.Interface
|
||||
import ShellCheck.Formatter.Format
|
||||
|
||||
import Control.DeepSeq
|
||||
import Control.Monad
|
||||
import Data.Array
|
||||
import Data.Foldable
|
||||
|
@ -31,9 +30,9 @@ import Data.Ord
|
|||
import Data.IORef
|
||||
import Data.List
|
||||
import Data.Maybe
|
||||
import GHC.Exts
|
||||
import System.IO
|
||||
import System.Info
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
|
||||
wikiLink = "https://www.shellcheck.net/wiki/"
|
||||
|
||||
|
@ -89,7 +88,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
|||
appendComments errRef comments max = do
|
||||
previous <- readIORef errRef
|
||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
|
||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
||||
where
|
||||
fst3 (x,_,_) = x
|
||||
equal x y = fst3 x == fst3 y
|
||||
|
@ -117,19 +116,19 @@ outputResult options ref result sys = do
|
|||
color <- getColorFunc $ foColorOption options
|
||||
let comments = crComments result
|
||||
appendComments ref comments (fromIntegral $ foWikiLinkCount options)
|
||||
let fileGroups = NE.groupWith sourceFile comments
|
||||
let fileGroups = groupWith sourceFile comments
|
||||
mapM_ (outputForFile color sys) fileGroups
|
||||
|
||||
outputForFile color sys comments = do
|
||||
let fileName = sourceFile (NE.head comments)
|
||||
result <- siReadFile sys (Just True) fileName
|
||||
let fileName = sourceFile (head comments)
|
||||
result <- (siReadFile sys) fileName
|
||||
let contents = either (const "") id result
|
||||
let fileLinesList = lines contents
|
||||
let lineCount = length fileLinesList
|
||||
let fileLines = listArray (1, lineCount) fileLinesList
|
||||
let groups = NE.groupWith lineNo comments
|
||||
let groups = groupWith lineNo comments
|
||||
forM_ groups $ \commentsForLine -> do
|
||||
let lineNum = fromIntegral $ lineNo (NE.head commentsForLine)
|
||||
let lineNum = fromIntegral $ lineNo (head commentsForLine)
|
||||
let line = if lineNum < 1 || lineNum > lineCount
|
||||
then ""
|
||||
else fileLines ! fromIntegral lineNum
|
||||
|
@ -139,7 +138,7 @@ outputForFile color sys comments = do
|
|||
putStrLn (color "source" line)
|
||||
forM_ commentsForLine $ \c -> putStrLn $ color (severityText c) $ cuteIndent c
|
||||
putStrLn ""
|
||||
showFixedString color (toList commentsForLine) (fromIntegral lineNum) fileLines
|
||||
showFixedString color commentsForLine (fromIntegral lineNum) fileLines
|
||||
|
||||
-- Pick out only the lines necessary to show a fix in action
|
||||
sliceFile :: Fix -> Array Int String -> (Fix, Array Int String)
|
||||
|
@ -169,13 +168,13 @@ showFixedString color comments lineNum fileLines =
|
|||
-- and/or other unrelated lines.
|
||||
let (excerptFix, excerpt) = sliceFile mergedFix fileLines
|
||||
-- in the spirit of error prone
|
||||
putStrLn $ color "message" "Did you mean:"
|
||||
putStrLn $ color "message" "Did you mean: "
|
||||
putStrLn $ unlines $ applyFix excerptFix excerpt
|
||||
|
||||
cuteIndent :: PositionedComment -> String
|
||||
cuteIndent comment =
|
||||
replicate (fromIntegral $ colNo comment - 1) ' ' ++
|
||||
makeArrow ++ " " ++ code (codeNo comment) ++ " (" ++ severityText comment ++ "): " ++ messageText comment
|
||||
makeArrow ++ " " ++ code (codeNo comment) ++ ": " ++ messageText comment
|
||||
where
|
||||
arrow n = '^' : replicate (fromIntegral $ n-2) '-' ++ "^"
|
||||
makeArrow =
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2024 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -21,14 +21,14 @@
|
|||
module ShellCheck.Interface
|
||||
(
|
||||
SystemInterface(..)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csExtendedAnalysis, csOptionalChecks)
|
||||
, CheckSpec(csFilename, csScript, csCheckSourced, csIncludedWarnings, csExcludedWarnings, csShellTypeOverride, csMinSeverity, csIgnoreRC, csOptionalChecks)
|
||||
, CheckResult(crFilename, crComments)
|
||||
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asExtendedAnalysis, asOptionalChecks)
|
||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
|
||||
, AnalysisResult(arComments)
|
||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||
, Shell(Ksh, Sh, Bash, Dash, BusyboxSh)
|
||||
, Shell(Ksh, Sh, Bash, Dash)
|
||||
, ExecutionMode(Executed, Sourced)
|
||||
, ErrorMessage
|
||||
, Code
|
||||
|
@ -39,12 +39,11 @@ module ShellCheck.Interface
|
|||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||
, TokenComment(tcId, tcComment, tcFix)
|
||||
, emptyCheckResult
|
||||
, newAnalysisResult
|
||||
, newAnalysisSpec
|
||||
, newFormatterOptions
|
||||
, newParseResult
|
||||
, newAnalysisSpec
|
||||
, newAnalysisResult
|
||||
, newFormatterOptions
|
||||
, newPosition
|
||||
, newSystemInterface
|
||||
, newTokenComment
|
||||
, mockedSystemInterface
|
||||
, mockRcFile
|
||||
|
@ -74,18 +73,14 @@ import qualified Data.Map as Map
|
|||
|
||||
|
||||
data SystemInterface m = SystemInterface {
|
||||
-- | Given:
|
||||
-- What annotations say about including external files (if anything)
|
||||
-- A resolved filename from siFindSource
|
||||
-- Read the file or return an error
|
||||
siReadFile :: Maybe Bool -> String -> m (Either ErrorMessage String),
|
||||
-- | Read a file by filename, or return an error
|
||||
siReadFile :: String -> m (Either ErrorMessage String),
|
||||
-- | Given:
|
||||
-- the current script,
|
||||
-- what annotations say about including external files (if anything)
|
||||
-- a list of source-path annotations in effect,
|
||||
-- and a sourced file,
|
||||
-- find the sourced file
|
||||
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
|
||||
siFindSource :: String -> [String] -> String -> m FilePath,
|
||||
-- | Get the configuration file (name, contents) for a filename
|
||||
siGetConfig :: String -> m (Maybe (FilePath, String))
|
||||
}
|
||||
|
@ -100,7 +95,6 @@ data CheckSpec = CheckSpec {
|
|||
csIncludedWarnings :: Maybe [Integer],
|
||||
csShellTypeOverride :: Maybe Shell,
|
||||
csMinSeverity :: Severity,
|
||||
csExtendedAnalysis :: Maybe Bool,
|
||||
csOptionalChecks :: [String]
|
||||
} deriving (Show, Eq)
|
||||
|
||||
|
@ -125,7 +119,6 @@ emptyCheckSpec = CheckSpec {
|
|||
csIncludedWarnings = Nothing,
|
||||
csShellTypeOverride = Nothing,
|
||||
csMinSeverity = StyleC,
|
||||
csExtendedAnalysis = Nothing,
|
||||
csOptionalChecks = []
|
||||
}
|
||||
|
||||
|
@ -138,14 +131,6 @@ newParseSpec = ParseSpec {
|
|||
psShellTypeOverride = Nothing
|
||||
}
|
||||
|
||||
newSystemInterface :: Monad m => SystemInterface m
|
||||
newSystemInterface =
|
||||
SystemInterface {
|
||||
siReadFile = \_ _ -> return $ Left "Not implemented",
|
||||
siFindSource = \_ _ _ name -> return name,
|
||||
siGetConfig = \_ -> return Nothing
|
||||
}
|
||||
|
||||
-- Parser input and output
|
||||
data ParseSpec = ParseSpec {
|
||||
psFilename :: String,
|
||||
|
@ -176,7 +161,6 @@ data AnalysisSpec = AnalysisSpec {
|
|||
asExecutionMode :: ExecutionMode,
|
||||
asCheckSourced :: Bool,
|
||||
asOptionalChecks :: [String],
|
||||
asExtendedAnalysis :: Maybe Bool,
|
||||
asTokenPositions :: Map.Map Id (Position, Position)
|
||||
}
|
||||
|
||||
|
@ -187,7 +171,6 @@ newAnalysisSpec token = AnalysisSpec {
|
|||
asExecutionMode = Executed,
|
||||
asCheckSourced = False,
|
||||
asOptionalChecks = [],
|
||||
asExtendedAnalysis = Nothing,
|
||||
asTokenPositions = Map.empty
|
||||
}
|
||||
|
||||
|
@ -225,7 +208,7 @@ newCheckDescription = CheckDescription {
|
|||
}
|
||||
|
||||
-- Supporting data types
|
||||
data Shell = Ksh | Sh | Bash | Dash | BusyboxSh deriving (Show, Eq)
|
||||
data Shell = Ksh | Sh | Bash | Dash deriving (Show, Eq)
|
||||
data ExecutionMode = Executed | Sourced deriving (Show, Eq)
|
||||
|
||||
type ErrorMessage = String
|
||||
|
@ -324,18 +307,19 @@ data ColorOption =
|
|||
|
||||
-- For testing
|
||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
|
||||
mockedSystemInterface files = SystemInterface {
|
||||
siReadFile = rf,
|
||||
siFindSource = fs,
|
||||
siGetConfig = const $ return Nothing
|
||||
}
|
||||
where
|
||||
rf _ file = return $
|
||||
rf file = return $
|
||||
case find ((== file) . fst) files of
|
||||
Nothing -> Left "File not included in mock."
|
||||
Just (_, contents) -> Right contents
|
||||
fs _ _ _ file = return file
|
||||
fs _ _ file = return file
|
||||
|
||||
mockRcFile rcfile mock = mock {
|
||||
siGetConfig = const . return $ Just (".shellcheckrc", rcfile)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
{-
|
||||
Copyright 2012-2022 Vidar Holen
|
||||
Copyright 2012-2019 Vidar Holen
|
||||
|
||||
This file is part of ShellCheck.
|
||||
https://www.shellcheck.net
|
||||
|
@ -27,7 +27,6 @@ import ShellCheck.AST
|
|||
import ShellCheck.ASTLib hiding (runTests)
|
||||
import ShellCheck.Data
|
||||
import ShellCheck.Interface
|
||||
import ShellCheck.Prelude
|
||||
|
||||
import Control.Applicative ((<*), (*>))
|
||||
import Control.Monad
|
||||
|
@ -38,6 +37,7 @@ import Data.Functor
|
|||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
|
||||
import Data.Maybe
|
||||
import Data.Monoid
|
||||
import Debug.Trace
|
||||
import GHC.Exts (sortWith)
|
||||
import Prelude hiding (readList)
|
||||
import System.IO
|
||||
|
@ -46,8 +46,7 @@ import Text.Parsec.Error
|
|||
import Text.Parsec.Pos
|
||||
import qualified Control.Monad.Reader as Mr
|
||||
import qualified Control.Monad.State as Ms
|
||||
import qualified Data.List.NonEmpty as NE
|
||||
import qualified Data.Map.Strict as Map
|
||||
import qualified Data.Map as Map
|
||||
|
||||
import Test.QuickCheck.All (quickCheckAll)
|
||||
|
||||
|
@ -67,7 +66,7 @@ doubleQuote = char '"'
|
|||
variableStart = upper <|> lower <|> oneOf "_"
|
||||
variableChars = upper <|> lower <|> digit <|> oneOf "_"
|
||||
-- Chars to allow in function names
|
||||
functionChars = variableChars <|> oneOf ":+?-./^@,"
|
||||
functionChars = variableChars <|> oneOf ":+?-./^@"
|
||||
-- Chars to allow in functions using the 'function' keyword
|
||||
extendedFunctionChars = functionChars <|> oneOf "[]*=!"
|
||||
specialVariable = oneOf (concat specialVariables)
|
||||
|
@ -141,9 +140,15 @@ carriageReturn = do
|
|||
parseProblemAt pos ErrorC 1017 "Literal carriage return. Run script through tr -d '\\r' ."
|
||||
return '\r'
|
||||
|
||||
almostSpace = do
|
||||
parseNote ErrorC 1018 $ "This is a unicode space. Delete and retype it."
|
||||
oneOf "\xA0\x2002\x2003\x2004\x2005\x2006\x2007\x2008\x2009\x200B\x202F"
|
||||
almostSpace =
|
||||
choice [
|
||||
check '\xA0' "unicode non-breaking space",
|
||||
check '\x200B' "unicode zerowidth space"
|
||||
]
|
||||
where
|
||||
check c name = do
|
||||
parseNote ErrorC 1018 $ "This is a " ++ name ++ ". Delete and retype it."
|
||||
char c
|
||||
return ' '
|
||||
|
||||
--------- Message/position annotation on top of user state
|
||||
|
@ -155,7 +160,7 @@ data Context =
|
|||
deriving (Show)
|
||||
|
||||
data HereDocContext =
|
||||
HereDocPending Id Dashed Quoted String [Context] -- on linefeed, read this T_HereDoc
|
||||
HereDocPending Token [Context] -- on linefeed, read this T_HereDoc
|
||||
deriving (Show)
|
||||
|
||||
data UserState = UserState {
|
||||
|
@ -205,7 +210,7 @@ getNextIdSpanningTokenList list =
|
|||
-- Get the span covered by an id
|
||||
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
|
||||
getSpanForId id =
|
||||
Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>
|
||||
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>
|
||||
getMap
|
||||
|
||||
-- Create a new id with the same span as an existing one
|
||||
|
@ -233,12 +238,12 @@ addToHereDocMap id list = do
|
|||
hereDocMap = Map.insert id list map
|
||||
}
|
||||
|
||||
addPendingHereDoc id d q str = do
|
||||
addPendingHereDoc t = do
|
||||
state <- getState
|
||||
context <- getCurrentContexts
|
||||
let docs = pendingHereDocs state
|
||||
putState $ state {
|
||||
pendingHereDocs = HereDocPending id d q str context : docs
|
||||
pendingHereDocs = HereDocPending t context : docs
|
||||
}
|
||||
|
||||
popPendingHereDocs = do
|
||||
|
@ -452,8 +457,8 @@ called s p = do
|
|||
pos <- getPosition
|
||||
withContext (ContextName pos s) p
|
||||
|
||||
withAnnotations anns p =
|
||||
if null anns then p else withContext (ContextAnnotation anns) p
|
||||
withAnnotations anns =
|
||||
withContext (ContextAnnotation anns)
|
||||
|
||||
readConditionContents single =
|
||||
readCondContents `attempting` lookAhead (do
|
||||
|
@ -551,7 +556,7 @@ readConditionContents single =
|
|||
notFollowedBy2 (try (spacing >> string "]"))
|
||||
x <- readNormalWord
|
||||
pos <- getPosition
|
||||
when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do
|
||||
when (endedWith "]" x && notArrayIndex x) $ do
|
||||
parseProblemAt pos ErrorC 1020 $
|
||||
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
|
||||
fail "Missing space before ]"
|
||||
|
@ -567,7 +572,6 @@ readConditionContents single =
|
|||
endedWith _ _ = False
|
||||
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
|
||||
notArrayIndex _ = True
|
||||
containsLiteral x s = s `isInfixOf` onlyLiteralString x
|
||||
|
||||
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
|
||||
|
||||
|
@ -713,20 +717,20 @@ prop_a6 = isOk readArithmeticContents " 1 | 2 ||3|4"
|
|||
prop_a7 = isOk readArithmeticContents "3*2**10"
|
||||
prop_a8 = isOk readArithmeticContents "3"
|
||||
prop_a9 = isOk readArithmeticContents "a^!-b"
|
||||
prop_a10 = isOk readArithmeticContents "! $?"
|
||||
prop_a11 = isOk readArithmeticContents "10#08 * 16#f"
|
||||
prop_a12 = isOk readArithmeticContents "\"$((3+2))\" + '37'"
|
||||
prop_a13 = isOk readArithmeticContents "foo[9*y+x]++"
|
||||
prop_a14 = isOk readArithmeticContents "1+`echo 2`"
|
||||
prop_a15 = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
|
||||
prop_a16 = isOk readArithmeticContents "$foo$bar"
|
||||
prop_a17 = isOk readArithmeticContents "i<(0+(1+1))"
|
||||
prop_a18 = isOk readArithmeticContents "a?b:c"
|
||||
prop_a19 = isOk readArithmeticContents "\\\n3 +\\\n 2"
|
||||
prop_a20 = isOk readArithmeticContents "a ? b ? c : d : e"
|
||||
prop_a21 = isOk readArithmeticContents "a ? b : c ? d : e"
|
||||
prop_a22 = isOk readArithmeticContents "!!a"
|
||||
prop_a23 = isOk readArithmeticContents "~0"
|
||||
prop_a10= isOk readArithmeticContents "! $?"
|
||||
prop_a11= isOk readArithmeticContents "10#08 * 16#f"
|
||||
prop_a12= isOk readArithmeticContents "\"$((3+2))\" + '37'"
|
||||
prop_a13= isOk readArithmeticContents "foo[9*y+x]++"
|
||||
prop_a14= isOk readArithmeticContents "1+`echo 2`"
|
||||
prop_a15= isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
|
||||
prop_a16= isOk readArithmeticContents "$foo$bar"
|
||||
prop_a17= isOk readArithmeticContents "i<(0+(1+1))"
|
||||
prop_a18= isOk readArithmeticContents "a?b:c"
|
||||
prop_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
|
||||
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
|
||||
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
|
||||
prop_a22= isOk readArithmeticContents "!!a"
|
||||
prop_a23= isOk readArithmeticContents "~0"
|
||||
readArithmeticContents :: Monad m => SCParser m Token
|
||||
readArithmeticContents =
|
||||
readSequence
|
||||
|
@ -815,13 +819,11 @@ readArithmeticContents =
|
|||
return $ TA_Expansion id pieces
|
||||
|
||||
readGroup = do
|
||||
start <- startSpan
|
||||
char '('
|
||||
s <- readSequence
|
||||
char ')'
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ TA_Parenthesis id s
|
||||
return s
|
||||
|
||||
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||
|
||||
|
@ -921,8 +923,8 @@ prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
|
|||
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
|
||||
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"
|
||||
prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]"
|
||||
prop_readCondition10a = isOk readCondition "[[\na == b ||\nc == d ]]"
|
||||
prop_readCondition10b = isOk readCondition "[[ a == b\n||\nc == d ]]"
|
||||
prop_readCondition10a= isOk readCondition "[[\na == b ||\nc == d ]]"
|
||||
prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
|
||||
prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]"
|
||||
prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]"
|
||||
prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
|
||||
|
@ -939,9 +941,6 @@ prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
|||
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
||||
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
|
||||
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
|
||||
prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"
|
||||
prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"
|
||||
|
||||
readCondition = called "test expression" $ do
|
||||
opos <- getPosition
|
||||
start <- startSpan
|
||||
|
@ -985,17 +984,12 @@ prop_readAnnotation4 = isWarning readAnnotation "# shellcheck cats=dogs disable=
|
|||
prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All cats are precious\n"
|
||||
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
||||
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
|
||||
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\n"
|
||||
prop_readAnnotation9 = isOk readAnnotation "# shellcheck source='foo bar' source-path=\"baz etc\"\n"
|
||||
prop_readAnnotation10 = isOk readAnnotation "# shellcheck disable='SC1234,SC2345' enable=\"foo\" shell='bash'\n"
|
||||
prop_readAnnotation11 = isOk (readAnnotationWithoutPrefix False) "external-sources='true'"
|
||||
|
||||
readAnnotation = called "shellcheck directive" $ do
|
||||
try readAnnotationPrefix
|
||||
many1 linewhitespace
|
||||
readAnnotationWithoutPrefix True
|
||||
readAnnotationWithoutPrefix
|
||||
|
||||
readAnnotationWithoutPrefix sandboxed = do
|
||||
readAnnotationWithoutPrefix = do
|
||||
values <- many1 readKey
|
||||
optional readAnyComment
|
||||
void linefeed <|> eof <|> do
|
||||
|
@ -1005,24 +999,13 @@ readAnnotationWithoutPrefix sandboxed = do
|
|||
many linewhitespace
|
||||
return $ concat values
|
||||
where
|
||||
plainOrQuoted p = quoted p <|> p
|
||||
quoted p = do
|
||||
c <- oneOf "'\""
|
||||
start <- getPosition
|
||||
str <- many1 $ noneOf (c:"\n")
|
||||
char c <|> fail "Missing terminating quote for directive."
|
||||
subParse start p str
|
||||
readKey = do
|
||||
keyPos <- getPosition
|
||||
key <- many1 (letter <|> char '-')
|
||||
char '=' <|> fail "Expected '=' after directive key"
|
||||
annotations <- case key of
|
||||
"disable" -> plainOrQuoted $ readElement `sepBy` char ','
|
||||
"disable" -> readRange `sepBy` char ','
|
||||
where
|
||||
readElement = readRange <|> readAll
|
||||
readAll = do
|
||||
string "all"
|
||||
return $ DisableComment 0 1000000
|
||||
readRange = do
|
||||
from <- readCode
|
||||
to <- choice [ char '-' *> readCode, return $ from+1 ]
|
||||
|
@ -1032,51 +1015,26 @@ readAnnotationWithoutPrefix sandboxed = do
|
|||
int <- many1 digit
|
||||
return $ read int
|
||||
|
||||
"enable" -> plainOrQuoted $ readName `sepBy` char ','
|
||||
"enable" -> readName `sepBy` char ','
|
||||
where
|
||||
readName = EnableComment <$> many1 (letter <|> char '-')
|
||||
|
||||
"source" -> do
|
||||
filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
filename <- many1 $ noneOf " \n"
|
||||
return [SourceOverride filename]
|
||||
|
||||
"source-path" -> do
|
||||
dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
dirname <- many1 $ noneOf " \n"
|
||||
return [SourcePath dirname]
|
||||
|
||||
"shell" -> do
|
||||
pos <- getPosition
|
||||
shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||
shell <- many1 $ noneOf " \n"
|
||||
when (isNothing $ shellForExecutable shell) $
|
||||
parseNoteAt pos ErrorC 1103
|
||||
"This shell type is unknown. Use e.g. sh or bash."
|
||||
return [ShellOverride shell]
|
||||
|
||||
"extended-analysis" -> do
|
||||
pos <- getPosition
|
||||
value <- plainOrQuoted $ many1 letter
|
||||
case value of
|
||||
"true" -> return [ExtendedAnalysis True]
|
||||
"false" -> return [ExtendedAnalysis False]
|
||||
_ -> do
|
||||
parseNoteAt pos ErrorC 1146 "Unknown extended-analysis value. Expected true/false."
|
||||
return []
|
||||
|
||||
"external-sources" -> do
|
||||
pos <- getPosition
|
||||
value <- plainOrQuoted $ many1 letter
|
||||
case value of
|
||||
"true" ->
|
||||
if sandboxed
|
||||
then do
|
||||
parseNoteAt pos ErrorC 1144 "external-sources can only be enabled in .shellcheckrc, not in individual files."
|
||||
return []
|
||||
else return [ExternalSources True]
|
||||
"false" -> return [ExternalSources False]
|
||||
_ -> do
|
||||
parseNoteAt pos ErrorC 1145 "Unknown external-sources value. Expected true/false."
|
||||
return []
|
||||
|
||||
_ -> do
|
||||
parseNoteAt keyPos WarningC 1107 "This directive is unknown. It will be ignored."
|
||||
anyChar `reluctantlyTill` whitespace
|
||||
|
@ -1199,7 +1157,7 @@ readDollarBracedPart = readSingleQuoted <|> readDoubleQuoted <|>
|
|||
|
||||
readDollarBracedLiteral = do
|
||||
start <- startSpan
|
||||
vars <- (readBraceEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` bracedQuotable
|
||||
vars <- (readBraceEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` bracedQuotable
|
||||
id <- endSpan start
|
||||
return $ T_Literal id $ concat vars
|
||||
|
||||
|
@ -1414,8 +1372,6 @@ prop_readGlob5 = isOk readGlob "[^[:alpha:]1-9]"
|
|||
prop_readGlob6 = isOk readGlob "[\\|]"
|
||||
prop_readGlob7 = isOk readGlob "[^[]"
|
||||
prop_readGlob8 = isOk readGlob "[*?]"
|
||||
prop_readGlob9 = isOk readGlob "[!]^]"
|
||||
prop_readGlob10 = isOk readGlob "[]]"
|
||||
readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
|
||||
where
|
||||
readSimple = do
|
||||
|
@ -1423,25 +1379,22 @@ readGlob = readExtglob <|> readSimple <|> readClass <|> readGlobbyLiteral
|
|||
c <- oneOf "*?"
|
||||
id <- endSpan start
|
||||
return $ T_Glob id [c]
|
||||
-- Doesn't handle weird things like [^]a] and [$foo]. fixme?
|
||||
readClass = try $ do
|
||||
start <- startSpan
|
||||
char '['
|
||||
negation <- charToString (oneOf "!^") <|> return ""
|
||||
leadingBracket <- charToString (oneOf "]") <|> return ""
|
||||
s <- many (predefined <|> readNormalLiteralPart "]" <|> globchars)
|
||||
guard $ not (null leadingBracket) || not (null s)
|
||||
s <- many1 (predefined <|> readNormalLiteralPart "]" <|> globchars)
|
||||
char ']'
|
||||
id <- endSpan start
|
||||
return $ T_Glob id $ "[" ++ concat (negation:leadingBracket:s) ++ "]"
|
||||
return $ T_Glob id $ "[" ++ concat s ++ "]"
|
||||
where
|
||||
globchars = charToString $ oneOf $ "![" ++ extglobStartChars
|
||||
globchars = fmap return . oneOf $ "!$[" ++ extglobStartChars
|
||||
predefined = do
|
||||
try $ string "[:"
|
||||
s <- many1 letter
|
||||
string ":]"
|
||||
return $ "[:" ++ s ++ ":]"
|
||||
|
||||
charToString = fmap return
|
||||
readGlobbyLiteral = do
|
||||
start <- startSpan
|
||||
c <- extglobStart <|> char '['
|
||||
|
@ -1533,6 +1486,7 @@ readSingleEscaped = do
|
|||
|
||||
case x of
|
||||
'\'' -> parseProblemAt pos InfoC 1003 "Want to escape a single quote? echo 'This is how it'\\''s done'.";
|
||||
'\n' -> parseProblemAt pos InfoC 1004 "This backslash+linefeed is literal. Break outside single quotes if you just want to break the line."
|
||||
_ -> return ()
|
||||
|
||||
return [s]
|
||||
|
@ -1561,7 +1515,7 @@ readGenericLiteral endChars = do
|
|||
return $ concat strings
|
||||
|
||||
readGenericLiteral1 endExp = do
|
||||
strings <- (readGenericEscaped <|> ((\x -> [x]) <$> anyChar)) `reluctantlyTill1` endExp
|
||||
strings <- (readGenericEscaped <|> (anyChar >>= \x -> return [x])) `reluctantlyTill1` endExp
|
||||
return $ concat strings
|
||||
|
||||
readGenericEscaped = do
|
||||
|
@ -1718,9 +1672,9 @@ readDollarBraced = called "parameter expansion" $ do
|
|||
id <- endSpan start
|
||||
return $ T_DollarBraced id True word
|
||||
|
||||
prop_readDollarExpansion1 = isOk readDollarExpansion "$(echo foo; ls\n)"
|
||||
prop_readDollarExpansion2 = isOk readDollarExpansion "$( )"
|
||||
prop_readDollarExpansion3 = isOk readDollarExpansion "$( command \n#comment \n)"
|
||||
prop_readDollarExpansion1= isOk readDollarExpansion "$(echo foo; ls\n)"
|
||||
prop_readDollarExpansion2= isOk readDollarExpansion "$( )"
|
||||
prop_readDollarExpansion3= isOk readDollarExpansion "$( command \n#comment \n)"
|
||||
readDollarExpansion = called "command expansion" $ do
|
||||
start <- startSpan
|
||||
try (string "$(")
|
||||
|
@ -1812,17 +1766,17 @@ prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
|||
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
||||
prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
|
||||
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
|
||||
prop_readHereDoc10 = isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
|
||||
prop_readHereDoc11 = isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
|
||||
prop_readHereDoc12 = isOk readScript "cat << foo|cat\nbar\nfoo"
|
||||
prop_readHereDoc13 = isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
||||
prop_readHereDoc14 = isWarning readScript "cat << foo\nbar\nfoo \n"
|
||||
prop_readHereDoc15 = isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
||||
prop_readHereDoc16 = isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
||||
prop_readHereDoc17 = isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
|
||||
prop_readHereDoc18 = isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
|
||||
prop_readHereDoc20 = isWarning readScript "cat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc21 = isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc10= isOk readScript "if true; then cat << foo << bar; fi\nfoo\nbar\n"
|
||||
prop_readHereDoc11= isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
|
||||
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
|
||||
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
||||
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
|
||||
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
||||
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
||||
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\n foo\n"
|
||||
prop_readHereDoc18= isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
|
||||
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc21= isOk readScript "# shellcheck disable=SC1039\ncat << foo\n foo\n()\nfoo\n"
|
||||
prop_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
|
||||
prop_readHereDoc23 = isNotOk readScript "cat << foo \r\ncow\r\nfoo\r\n"
|
||||
readHereDoc = called "here document" $ do
|
||||
|
@ -1840,7 +1794,7 @@ readHereDoc = called "here document" $ do
|
|||
|
||||
-- add empty tokens for now, read the rest in readPendingHereDocs
|
||||
let doc = T_HereDoc hid dashed quoted endToken []
|
||||
addPendingHereDoc hid dashed quoted endToken
|
||||
addPendingHereDoc doc
|
||||
return doc
|
||||
where
|
||||
unquote :: String -> (Quoted, String)
|
||||
|
@ -1861,7 +1815,7 @@ readPendingHereDocs = do
|
|||
docs <- popPendingHereDocs
|
||||
mapM_ readDoc docs
|
||||
where
|
||||
readDoc (HereDocPending id dashed quoted endToken ctx) =
|
||||
readDoc (HereDocPending (T_HereDoc id dashed quoted endToken _) ctx) =
|
||||
swapContext ctx $
|
||||
do
|
||||
docStartPos <- getPosition
|
||||
|
@ -1936,7 +1890,7 @@ readPendingHereDocs = do
|
|||
-- The end token is just a prefix
|
||||
skipLine
|
||||
| hasTrailer ->
|
||||
error $ pleaseReport "unexpected heredoc trailer"
|
||||
error "ShellCheck bug, please report (here doc trailer)."
|
||||
|
||||
-- The following cases assume no trailing text:
|
||||
| dashed == Undashed && (not $ null leadingSpace) -> do
|
||||
|
@ -2042,14 +1996,12 @@ readHereString = called "here string" $ do
|
|||
word <- readNormalWord
|
||||
return $ T_HereString id word
|
||||
|
||||
prop_readNewlineList1 = isOk readScript "&> /dev/null echo foo"
|
||||
readNewlineList =
|
||||
many1 ((linefeed <|> carriageReturn) `thenSkip` spacing) <* checkBadBreak
|
||||
where
|
||||
checkBadBreak = optional $ do
|
||||
pos <- getPosition
|
||||
try $ lookAhead (oneOf "|&") -- See if the next thing could be |, || or &&
|
||||
notFollowedBy2 (string "&>") -- Except &> or &>> which is valid
|
||||
parseProblemAt pos ErrorC 1133
|
||||
"Unexpected start of line. If breaking lines, |/||/&& should be at the end of the previous one."
|
||||
readLineBreak = optional readNewlineList
|
||||
|
@ -2109,7 +2061,6 @@ prop_readSimpleCommand4 = isOk readSimpleCommand "typeset -a foo=(lol)"
|
|||
prop_readSimpleCommand5 = isOk readSimpleCommand "time if true; then echo foo; fi"
|
||||
prop_readSimpleCommand6 = isOk readSimpleCommand "time -p ( ls -l; )"
|
||||
prop_readSimpleCommand7 = isOk readSimpleCommand "\\ls"
|
||||
prop_readSimpleCommand7b = isOk readSimpleCommand "\\:"
|
||||
prop_readSimpleCommand8 = isWarning readSimpleCommand "// Lol"
|
||||
prop_readSimpleCommand9 = isWarning readSimpleCommand "/* Lolbert */"
|
||||
prop_readSimpleCommand10 = isWarning readSimpleCommand "/**** Lolbert */"
|
||||
|
@ -2117,7 +2068,6 @@ prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
|
|||
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
|
||||
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
|
||||
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
|
||||
prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT"
|
||||
readSimpleCommand = called "simple command" $ do
|
||||
prefix <- option [] readCmdPrefix
|
||||
skipAnnotationAndWarn
|
||||
|
@ -2147,12 +2097,9 @@ readSimpleCommand = called "simple command" $ do
|
|||
id2 <- getNewIdFor id1
|
||||
|
||||
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
|
||||
case () of
|
||||
_ | isCommand ["source", "."] cmd -> readSource result
|
||||
_ | isCommand ["trap"] cmd -> do
|
||||
syntaxCheckTrap result
|
||||
return result
|
||||
_ -> return result
|
||||
if isCommand ["source", "."] cmd
|
||||
then readSource result
|
||||
else return result
|
||||
where
|
||||
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
|
||||
isCommand _ _ = False
|
||||
|
@ -2172,17 +2119,6 @@ readSimpleCommand = called "simple command" $ do
|
|||
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
|
||||
_ -> return ()
|
||||
|
||||
syntaxCheckTrap cmd =
|
||||
case cmd of
|
||||
(T_Redirecting _ _ (T_SimpleCommand _ _ (cmd:arg:_))) -> checkArg arg (getLiteralString arg)
|
||||
_ -> return ()
|
||||
where
|
||||
checkArg _ Nothing = return ()
|
||||
checkArg arg (Just ('-':_)) = return ()
|
||||
checkArg arg (Just str) = do
|
||||
(start,end) <- getSpanForId (getId arg)
|
||||
subParse start (tryWithErrors (readCompoundListOrEmpty >> verifyEof) <|> return ()) str
|
||||
|
||||
commentWarning id =
|
||||
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
|
||||
|
||||
|
@ -2232,12 +2168,10 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||
if filename == "/dev/null" -- always allow /dev/null
|
||||
then return (Right "", filename)
|
||||
else do
|
||||
allAnnotations <- getCurrentAnnotations True
|
||||
currentScript <- Mr.asks currentFilename
|
||||
let paths = mapMaybe getSourcePath allAnnotations
|
||||
let externalSources = listToMaybe $ mapMaybe getExternalSources allAnnotations
|
||||
resolved <- system $ siFindSource sys currentScript externalSources paths filename
|
||||
contents <- system $ siReadFile sys externalSources resolved
|
||||
paths <- mapMaybe getSourcePath <$> getCurrentAnnotations True
|
||||
resolved <- system $ siFindSource sys currentScript paths filename
|
||||
contents <- system $ siReadFile sys resolved
|
||||
return (contents, resolved)
|
||||
case input of
|
||||
Left err -> do
|
||||
|
@ -2271,11 +2205,6 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||
SourcePath x -> Just x
|
||||
_ -> Nothing
|
||||
|
||||
getExternalSources t =
|
||||
case t of
|
||||
ExternalSources b -> Just b
|
||||
_ -> Nothing
|
||||
|
||||
-- If the word has a single expansion as the directory, try stripping it
|
||||
-- This affects `$foo/bar` but not `${foo}-dir/bar` or `/foo/$file`
|
||||
stripDynamicPrefix word =
|
||||
|
@ -2288,31 +2217,22 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||
|
||||
subRead name script =
|
||||
withContext (ContextSource name) $
|
||||
inSeparateContext $ do
|
||||
oldState <- getState
|
||||
setState $ oldState { pendingHereDocs = [] }
|
||||
result <- subParse (initialPos name) (readScriptFile True) script
|
||||
newState <- getState
|
||||
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
|
||||
return result
|
||||
inSeparateContext $
|
||||
subParse (initialPos name) (readScriptFile True) script
|
||||
readSource t = return t
|
||||
|
||||
|
||||
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
|
||||
prop_readPipeline2 = isWarning readPipeline "!cat /etc/issue | grep -i ubuntu"
|
||||
prop_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
|
||||
prop_readPipeline4 = isOk readPipeline "! ! true"
|
||||
prop_readPipeline5 = isOk readPipeline "true | ! true"
|
||||
readPipeline = do
|
||||
unexpecting "keyword/token" readKeyword
|
||||
readBanged readPipeSequence
|
||||
|
||||
readBanged parser = do
|
||||
pos <- getPosition
|
||||
(T_Bang id) <- g_Bang
|
||||
next <- readBanged parser
|
||||
return $ T_Banged id next
|
||||
<|> parser
|
||||
do
|
||||
(T_Bang id) <- g_Bang
|
||||
pipe <- readPipeSequence
|
||||
return $ T_Banged id pipe
|
||||
<|>
|
||||
readPipeSequence
|
||||
|
||||
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
|
||||
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
|
||||
|
@ -2328,7 +2248,7 @@ readAndOr = do
|
|||
parseProblemAt apos ErrorC 1123 "ShellCheck directives are only valid in front of complete compound commands, like 'if', not e.g. individual 'elif' branches."
|
||||
|
||||
andOr <- withAnnotations annotations $
|
||||
chainl1 readPipeline $ do
|
||||
chainr1 readPipeline $ do
|
||||
op <- g_AND_IF <|> g_OR_IF
|
||||
readLineBreak
|
||||
return $ case op of T_AND_IF id -> T_AndIf id
|
||||
|
@ -2368,14 +2288,14 @@ readTerm = do
|
|||
|
||||
readPipeSequence = do
|
||||
start <- startSpan
|
||||
(cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
|
||||
(cmds, pipes) <- sepBy1WithSeparators readCommand
|
||||
(readPipe `thenSkip` (spacing >> readLineBreak))
|
||||
id <- endSpan start
|
||||
spacing
|
||||
return $ T_Pipeline id pipes cmds
|
||||
where
|
||||
sepBy1WithSeparators p s = do
|
||||
let elems = (\x -> ([x], [])) <$> p
|
||||
let elems = p >>= \x -> return ([x], [])
|
||||
let seps = do
|
||||
separator <- s
|
||||
return $ \(a,b) (c,d) -> (a++c, b ++ d ++ [separator])
|
||||
|
@ -2398,14 +2318,10 @@ readCommand = choice [
|
|||
]
|
||||
|
||||
readCmdName = do
|
||||
-- If the command name is `!` then
|
||||
optional . lookAhead . try $ do
|
||||
char '!'
|
||||
whitespace
|
||||
-- Ignore alias suppression
|
||||
optional . try $ do
|
||||
char '\\'
|
||||
lookAhead $ variableChars <|> oneOf ":."
|
||||
lookAhead $ variableChars
|
||||
readCmdWord
|
||||
|
||||
readCmdWord = do
|
||||
|
@ -2533,29 +2449,16 @@ readBraceGroup = called "brace group" $ do
|
|||
spacing
|
||||
return $ T_BraceGroup id list
|
||||
|
||||
prop_readBatsTest1 = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||
prop_readBatsTest2 = isOk readBatsTest "@test random text !(@*$Y&! {\n true\n}"
|
||||
prop_readBatsTest3 = isOk readBatsTest "@test foo { bar { baz {\n true\n}"
|
||||
prop_readBatsTest4 = isNotOk readBatsTest "@test foo \n{\n true\n}"
|
||||
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
||||
readBatsTest = called "bats @test" $ do
|
||||
start <- startSpan
|
||||
try $ string "@test "
|
||||
try $ string "@test"
|
||||
spacing
|
||||
name <- readBatsName
|
||||
name <- readNormalWord
|
||||
spacing
|
||||
test <- readBraceGroup
|
||||
id <- endSpan start
|
||||
return $ T_BatsTest id name test
|
||||
where
|
||||
readBatsName = do
|
||||
line <- try . lookAhead $ many1 $ noneOf "\n"
|
||||
let name = reverse $ f $ reverse line
|
||||
string name
|
||||
|
||||
-- We want everything before the last " {" in a string, so we find everything after "{ " in its reverse
|
||||
f ('{':' ':rest) = dropWhile isSpace rest
|
||||
f (a:rest) = f rest
|
||||
f [] = ""
|
||||
|
||||
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
|
||||
readWhileClause = called "while loop" $ do
|
||||
|
@ -2584,7 +2487,7 @@ readDoGroup kwId = do
|
|||
parseProblem ErrorC 1058 "Expected 'do'."
|
||||
return "Expected 'do'"
|
||||
|
||||
acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."
|
||||
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."
|
||||
allspacing
|
||||
|
||||
optional (do
|
||||
|
@ -2613,9 +2516,9 @@ prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"
|
|||
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
|
||||
prop_readForClause8 = isOk readForClause "for ((;;)) ; do echo $i\ndone"
|
||||
prop_readForClause9 = isOk readForClause "for i do true; done"
|
||||
prop_readForClause10 = isOk readForClause "for ((;;)) { true; }"
|
||||
prop_readForClause12 = isWarning readForClause "for $a in *; do echo \"$a\"; done"
|
||||
prop_readForClause13 = isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
|
||||
prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
|
||||
prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"
|
||||
prop_readForClause13= isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
|
||||
readForClause = called "for loop" $ do
|
||||
pos <- getPosition
|
||||
(T_For id) <- g_For
|
||||
|
@ -2747,10 +2650,10 @@ prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
|
|||
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
|
||||
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
|
||||
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
|
||||
prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { true; }"
|
||||
prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"
|
||||
prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"
|
||||
prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }"
|
||||
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
|
||||
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
|
||||
prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"
|
||||
prop_readFunctionDefinition13= isOk readFunctionDefinition "@require(){ true; }"
|
||||
readFunctionDefinition = called "function" $ do
|
||||
start <- startSpan
|
||||
functionSignature <- try readFunctionSignature
|
||||
|
@ -2795,29 +2698,17 @@ readFunctionDefinition = called "function" $ do
|
|||
prop_readCoProc1 = isOk readCoProc "coproc foo { echo bar; }"
|
||||
prop_readCoProc2 = isOk readCoProc "coproc { echo bar; }"
|
||||
prop_readCoProc3 = isOk readCoProc "coproc echo bar"
|
||||
prop_readCoProc4 = isOk readCoProc "coproc a=b echo bar"
|
||||
prop_readCoProc5 = isOk readCoProc "coproc 'foo' { echo bar; }"
|
||||
prop_readCoProc6 = isOk readCoProc "coproc \"foo$$\" { echo bar; }"
|
||||
prop_readCoProc7 = isOk readCoProc "coproc 'foo' ( echo bar )"
|
||||
prop_readCoProc8 = isOk readCoProc "coproc \"foo$$\" while true; do true; done"
|
||||
readCoProc = called "coproc" $ do
|
||||
start <- startSpan
|
||||
try $ do
|
||||
string "coproc"
|
||||
spacing1
|
||||
whitespace
|
||||
choice [ try $ readCompoundCoProc start, readSimpleCoProc start ]
|
||||
where
|
||||
readCompoundCoProc start = do
|
||||
notFollowedBy2 readAssignmentWord
|
||||
(var, body) <- choice [
|
||||
try $ do
|
||||
body <- readBody readCompoundCommand
|
||||
return (Nothing, body),
|
||||
try $ do
|
||||
var <- readNormalWord `thenSkip` spacing
|
||||
body <- readBody readCompoundCommand
|
||||
return (Just var, body)
|
||||
]
|
||||
var <- optionMaybe $
|
||||
readVariableName `thenSkip` whitespace
|
||||
body <- readBody readCompoundCommand
|
||||
id <- endSpan start
|
||||
return $ T_CoProc id var body
|
||||
readSimpleCoProc start = do
|
||||
|
@ -2916,13 +2807,13 @@ readLetSuffix = many1 (readIoRedirect <|> try readLetExpression <|> readCmdWord)
|
|||
startPos <- getPosition
|
||||
expression <- readStringForParser readCmdWord
|
||||
let (unQuoted, newPos) = kludgeAwayQuotes expression startPos
|
||||
subParse newPos (readArithmeticContents <* eof) unQuoted
|
||||
subParse newPos readArithmeticContents unQuoted
|
||||
|
||||
kludgeAwayQuotes :: String -> SourcePos -> (String, SourcePos)
|
||||
kludgeAwayQuotes s p =
|
||||
case s of
|
||||
first:second:rest ->
|
||||
let (last NE.:| backwards) = NE.reverse (second NE.:| rest)
|
||||
first:rest@(_:_) ->
|
||||
let (last:backwards) = reverse rest
|
||||
middle = reverse backwards
|
||||
in
|
||||
if first `elem` "'\"" && first == last
|
||||
|
@ -2960,14 +2851,14 @@ prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
|
|||
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
||||
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
||||
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
||||
prop_readAssignmentWord9a = isOk readAssignmentWord "foo="
|
||||
prop_readAssignmentWord9b = isOk readAssignmentWord "foo= "
|
||||
prop_readAssignmentWord9c = isOk readAssignmentWord "foo= #bar"
|
||||
prop_readAssignmentWord11 = isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||
prop_readAssignmentWord12 = isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
||||
prop_readAssignmentWord13 = isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
||||
prop_readAssignmentWord14 = isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
|
||||
prop_readAssignmentWord15 = isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
||||
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
|
||||
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
|
||||
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
|
||||
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
||||
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
||||
prop_readAssignmentWord14= isOk readAssignmentWord "var=( 1 [2]=(3 4) )"
|
||||
prop_readAssignmentWord15= isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
||||
readAssignmentWord = readAssignmentWordExt True
|
||||
readWellFormedAssignment = readAssignmentWordExt False
|
||||
readAssignmentWordExt lenient = called "variable assignment" $ do
|
||||
|
@ -3303,7 +3194,7 @@ prop_readConfigKVs4 = isOk readConfigKVs "\n\n\n\n\t \n"
|
|||
prop_readConfigKVs5 = isOk readConfigKVs "# shellcheck accepts annotation-like comments in rc files\ndisable=1234"
|
||||
readConfigKVs = do
|
||||
anySpacingOrComment
|
||||
annotations <- many (readAnnotationWithoutPrefix False <* anySpacingOrComment)
|
||||
annotations <- many (readAnnotationWithoutPrefix <* anySpacingOrComment)
|
||||
eof
|
||||
return $ concat annotations
|
||||
anySpacingOrComment =
|
||||
|
@ -3315,60 +3206,51 @@ prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
|||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
||||
prop_readScript6 = isOk readScript "#!/usr/bin/env -S X=FOO bash\n#This is an empty script\n\n"
|
||||
prop_readScript7 = isOk readScript "#!/bin/zsh\n# shellcheck disable=SC1071\nfor f (a b); echo $f\n"
|
||||
readScriptFile sourced = do
|
||||
start <- startSpan
|
||||
pos <- getPosition
|
||||
optional $ do
|
||||
readUtf8Bom
|
||||
parseProblem ErrorC 1082
|
||||
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
|
||||
shebang <- readShebang <|> readEmptyLiteral
|
||||
let (T_Literal _ shebangString) = shebang
|
||||
allspacing
|
||||
annotationStart <- startSpan
|
||||
fileAnnotations <- readAnnotations
|
||||
rcAnnotations <- if sourced
|
||||
then return []
|
||||
else do
|
||||
filename <- Mr.asks currentFilename
|
||||
readConfigFile filename
|
||||
let annotations = fileAnnotations ++ rcAnnotations
|
||||
annotationId <- endSpan annotationStart
|
||||
let shellAnnotationSpecified =
|
||||
any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations
|
||||
shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride
|
||||
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
|
||||
|
||||
-- Put the rc annotations on the stack so that one can ignore e.g. SC1084 in .shellcheckrc
|
||||
withAnnotations rcAnnotations $ do
|
||||
hasBom <- wasIncluded readUtf8Bom
|
||||
shebang <- readShebang <|> readEmptyLiteral
|
||||
let (T_Literal _ shebangString) = shebang
|
||||
allspacing
|
||||
annotationStart <- startSpan
|
||||
fileAnnotations <- readAnnotations
|
||||
|
||||
-- Similarly put the filewide annotations on the stack to allow earlier suppression
|
||||
withAnnotations fileAnnotations $ do
|
||||
when (hasBom) $
|
||||
parseProblemAt pos ErrorC 1082
|
||||
"This file has a UTF-8 BOM. Remove it with: LC_CTYPE=C sed '1s/^...//' < yourscript ."
|
||||
let annotations = fileAnnotations ++ rcAnnotations
|
||||
annotationId <- endSpan annotationStart
|
||||
let shellAnnotationSpecified =
|
||||
any (\x -> case x of ShellOverride {} -> True; _ -> False) annotations
|
||||
shellFlagSpecified <- isJust <$> Mr.asks shellTypeOverride
|
||||
let ignoreShebang = shellAnnotationSpecified || shellFlagSpecified
|
||||
|
||||
unless ignoreShebang $
|
||||
verifyShebang pos (executableFromShebang shebangString)
|
||||
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
||||
then do
|
||||
commands <- readCompoundListOrEmpty
|
||||
id <- endSpan start
|
||||
readPendingHereDocs
|
||||
verifyEof
|
||||
let script = T_Annotation annotationId annotations $
|
||||
T_Script id shebang commands
|
||||
userstate <- getState
|
||||
reparseIndices $ reattachHereDocs script (hereDocMap userstate)
|
||||
else do
|
||||
many anyChar
|
||||
id <- endSpan start
|
||||
return $ T_Script id shebang []
|
||||
unless ignoreShebang $
|
||||
verifyShebang pos (executableFromShebang shebangString)
|
||||
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
||||
then do
|
||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
||||
id <- endSpan start
|
||||
verifyEof
|
||||
let script = T_Annotation annotationId annotations $
|
||||
T_Script id shebang commands
|
||||
reparseIndices script
|
||||
else do
|
||||
many anyChar
|
||||
id <- endSpan start
|
||||
return $ T_Script id shebang []
|
||||
|
||||
where
|
||||
verifyShebang pos s = do
|
||||
case isValidShell s of
|
||||
Just True -> return ()
|
||||
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh/'busybox sh' scripts. Sorry!"
|
||||
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh/'busybox sh'. Add a 'shell' directive to specify."
|
||||
Just False -> parseProblemAt pos ErrorC 1071 "ShellCheck only supports sh/bash/dash/ksh scripts. Sorry!"
|
||||
Nothing -> parseProblemAt pos ErrorC 1008 "This shebang was unrecognized. ShellCheck only supports sh/bash/dash/ksh. Add a 'shell' directive to specify."
|
||||
|
||||
isValidShell s =
|
||||
let good = null s || any (`isPrefixOf` s) goodShells
|
||||
|
@ -3384,20 +3266,16 @@ readScriptFile sourced = do
|
|||
"sh",
|
||||
"ash",
|
||||
"dash",
|
||||
"busybox sh",
|
||||
"bash",
|
||||
"bats",
|
||||
"ksh",
|
||||
"oksh"
|
||||
"ksh"
|
||||
]
|
||||
badShells = [
|
||||
"awk",
|
||||
"csh",
|
||||
"expect",
|
||||
"fish",
|
||||
"perl",
|
||||
"python",
|
||||
"python3",
|
||||
"ruby",
|
||||
"tcsh",
|
||||
"zsh"
|
||||
|
@ -3450,22 +3328,23 @@ isOk p s = parsesCleanly p s == Just True -- The string parses with no wa
|
|||
isWarning p s = parsesCleanly p s == Just False -- The string parses with warnings
|
||||
isNotOk p s = parsesCleanly p s == Nothing -- The string does not parse
|
||||
|
||||
-- If the parser matches the string, return Right [ParseNotes+ParseProblems]
|
||||
-- If it does not match the string, return Left [ParseProblems]
|
||||
getParseOutput parser string = runIdentity $ do
|
||||
(res, systemState) <- runParser testEnvironment
|
||||
(parser >> eof >> getState) "-" string
|
||||
return $ case res of
|
||||
Right userState ->
|
||||
Right $ parseNotes userState ++ parseProblems systemState
|
||||
Left _ -> Left $ parseProblems systemState
|
||||
parsesCleanly parser string = runIdentity $ do
|
||||
(res, sys) <- runParser testEnvironment
|
||||
(parser >> eof >> getState) "-" string
|
||||
case (res, sys) of
|
||||
(Right userState, systemState) ->
|
||||
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
||||
(Left _, _) -> return Nothing
|
||||
|
||||
-- If the parser matches the string, return Just whether it was clean (without emitting suggestions)
|
||||
-- Otherwise, Nothing
|
||||
parsesCleanly parser string =
|
||||
case getParseOutput parser string of
|
||||
Right list -> Just $ null list
|
||||
Left _ -> Nothing
|
||||
-- For printf debugging: print the value of an expression
|
||||
-- Example: return $ dump $ T_Literal id [c]
|
||||
dump :: Show a => a -> a
|
||||
dump x = trace (show x) x
|
||||
|
||||
-- Like above, but print a specific expression:
|
||||
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
|
||||
dumps :: Show x => x -> a -> a
|
||||
dumps t = trace (show t)
|
||||
|
||||
parseWithNotes parser = do
|
||||
item <- parser
|
||||
|
@ -3483,8 +3362,9 @@ makeErrorFor parsecError =
|
|||
pos = errorPos parsecError
|
||||
|
||||
getStringFromParsec errors =
|
||||
headOrDefault "" (mapMaybe f $ reverse errors) ++
|
||||
" Fix any mentioned problems and try again."
|
||||
case map f errors of
|
||||
r -> unwords (take 1 $ catMaybes $ reverse r) ++
|
||||
" Fix any mentioned problems and try again."
|
||||
where
|
||||
f err =
|
||||
case err of
|
||||
|
@ -3515,7 +3395,8 @@ parseShell env name contents = do
|
|||
return newParseResult {
|
||||
prComments = map toPositionedComment $ nub $ parseNotes userstate ++ parseProblems state,
|
||||
prTokenPositions = Map.map startEndPosToPos (positionMap userstate),
|
||||
prRoot = Just script
|
||||
prRoot = Just $
|
||||
reattachHereDocs script (hereDocMap userstate)
|
||||
}
|
||||
Left err -> do
|
||||
let context = contextStack state
|
||||
|
@ -3533,18 +3414,20 @@ parseShell env name contents = do
|
|||
-- A final pass for ignoring parse errors after failed parsing
|
||||
isIgnored stack note = any (contextItemDisablesCode False (codeForParseNote note)) stack
|
||||
|
||||
notesForContext list = zipWith ($) [first, second] [(pos, str) | ContextName pos str <- list]
|
||||
notesForContext list = zipWith ($) [first, second] $ filter isName list
|
||||
where
|
||||
first (pos, str) = ParseNote pos pos ErrorC 1073 $
|
||||
isName (ContextName _ _) = True
|
||||
isName _ = False
|
||||
first (ContextName pos str) = ParseNote pos pos ErrorC 1073 $
|
||||
"Couldn't parse this " ++ str ++ ". Fix to allow more checks."
|
||||
second (pos, str) = ParseNote pos pos InfoC 1009 $
|
||||
second (ContextName pos str) = ParseNote pos pos InfoC 1009 $
|
||||
"The mentioned syntax error was in this " ++ str ++ "."
|
||||
|
||||
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
||||
-- depending on declare -A statements.
|
||||
reparseIndices root = process root
|
||||
reparseIndices root =
|
||||
analyze blank blank f root
|
||||
where
|
||||
process = analyze blank blank f
|
||||
associative = getAssociativeArrays root
|
||||
isAssociative s = s `elem` associative
|
||||
f (T_Assignment id mode name indices value) = do
|
||||
|
@ -3569,9 +3452,8 @@ reparseIndices root = process root
|
|||
|
||||
fixAssignmentIndex name word =
|
||||
case word of
|
||||
T_UnparsedIndex id pos src -> do
|
||||
idx <- parsed name pos src
|
||||
process idx -- Recursively parse for cases like x[y[z=1]]=1
|
||||
T_UnparsedIndex id pos src ->
|
||||
parsed name pos src
|
||||
_ -> return word
|
||||
|
||||
parsed name pos src =
|
||||
|
|
|
@ -1,51 +0,0 @@
|
|||
{-
|
||||
Copyright 2022 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/>.
|
||||
-}
|
||||
|
||||
-- Generic basic utility functions
|
||||
module ShellCheck.Prelude where
|
||||
|
||||
import Data.Semigroup
|
||||
|
||||
|
||||
-- Get element 0 or a default. Like `head` but safe.
|
||||
headOrDefault _ (a:_) = a
|
||||
headOrDefault def _ = def
|
||||
|
||||
-- Get the last element or a default. Like `last` but safe.
|
||||
lastOrDefault def [] = def
|
||||
lastOrDefault _ list = last list
|
||||
|
||||
--- Get element n of a list, or Nothing. Like `!!` but safe.
|
||||
(!!!) list i =
|
||||
case drop i list of
|
||||
[] -> Nothing
|
||||
(r:_) -> Just r
|
||||
|
||||
|
||||
-- Like mconcat but for Semigroups
|
||||
sconcat1 :: (Semigroup t) => [t] -> t
|
||||
sconcat1 [x] = x
|
||||
sconcat1 (x:xs) = x <> sconcat1 xs
|
||||
|
||||
sconcatOrDefault def [] = def
|
||||
sconcatOrDefault _ list = sconcat1 list
|
||||
|
||||
-- For more actionable "impossible" errors
|
||||
pleaseReport str = "ShellCheck internal error, please report: " ++ str
|
|
@ -2,7 +2,7 @@
|
|||
# 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-18.15
|
||||
resolver: lts-13.26
|
||||
|
||||
# Local packages, usually specified by relative directory name
|
||||
packages:
|
||||
|
|
|
@ -29,7 +29,6 @@ detestify() {
|
|||
state = 0;
|
||||
}
|
||||
|
||||
/STRIP/ { next; }
|
||||
/LANGUAGE TemplateHaskell/ { next; }
|
||||
/^import.*Test\./ { next; }
|
||||
|
||||
|
@ -76,3 +75,4 @@ find . -name '.git' -prune -o -type f -name '*.hs' -print |
|
|||
do
|
||||
modify "$file" detestify
|
||||
done
|
||||
|
||||
|
|
|
@ -22,16 +22,13 @@ fi
|
|||
|
||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||
cabal install --dependencies-only "${flags[@]}" ||
|
||||
cabal install --dependencies-only --max-backjumps -1 "${flags[@]}" ||
|
||||
die "can't install dependencies"
|
||||
die "can't install dependencies"
|
||||
cabal configure --enable-tests "${flags[@]}" ||
|
||||
die "configure failed"
|
||||
cabal build ||
|
||||
die "build failed"
|
||||
cabal test ||
|
||||
die "test failed"
|
||||
cabal haddock ||
|
||||
die "haddock failed"
|
||||
|
||||
sc="$(find . -name shellcheck -type f -perm -111)"
|
||||
[ -x "$sc" ] || die "Can't find executable"
|
||||
|
|
|
@ -12,17 +12,6 @@ then
|
|||
fail "There are uncommitted changes"
|
||||
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
|
||||
|
||||
current=$(git tag --points-at)
|
||||
if [[ -z "$current" ]]
|
||||
then
|
||||
|
@ -45,38 +34,41 @@ 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
|
||||
|
||||
if [[ $(git log -1 --pretty=%B) != *"CHANGELOG"* ]]
|
||||
then
|
||||
fail "Expected git log message to contain CHANGELOG"
|
||||
fi
|
||||
|
||||
i=1 j=1
|
||||
cat << EOF
|
||||
|
||||
Manual Checklist
|
||||
|
||||
$((i++)). Make sure none of the automated checks above failed
|
||||
$((i++)). Run \`build/build_builder build/*/\` to update all builder images.
|
||||
$((j++)). \`build/run_builder dist-newstyle/sdist/ShellCheck-*.tar.gz build/*/\` to verify that they work.
|
||||
$((j++)). \`for f in \$(cat build/*/tag); do docker push "\$f"; done\` to upload them.
|
||||
$((i++)). Run test/distrotest to ensure that most distros can build OOTB.
|
||||
$((i++)). Make sure GitHub Build currently passes: https://github.com/koalaman/shellcheck/actions
|
||||
$((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 locally.
|
||||
$((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 GitHub Actions to build.
|
||||
$((j++)). Wait for Travis to build
|
||||
$((j++)). Verify release:
|
||||
a. Check that the new versions are uploaded: https://github.com/koalaman/shellcheck/tags
|
||||
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
|
||||
|
|
|
@ -17,20 +17,13 @@ and is still highly experimental.
|
|||
Make sure you're plugged in and have screen/tmux in place,
|
||||
then re-run with $0 --run to continue.
|
||||
|
||||
Also note that dist*/ and .stack-work/ will be deleted.
|
||||
Also note that dist* will be deleted.
|
||||
EOF
|
||||
exit 0
|
||||
}
|
||||
|
||||
echo "Deleting 'dist', 'dist-newstyle', and '.stack-work'..."
|
||||
rm -rf dist dist-newstyle .stack-work
|
||||
|
||||
execs=$(find . -name shellcheck)
|
||||
|
||||
if [ -n "$execs" ]
|
||||
then
|
||||
die "Found unexpected executables. Remove and try again: $execs"
|
||||
fi
|
||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||
rm -rf dist dist-newstyle
|
||||
|
||||
log=$(mktemp) || die "Can't create temp file"
|
||||
date >> "$log" || die "Can't write to log"
|
||||
|
@ -70,16 +63,14 @@ debian:testing apt-get update && apt-get install -y cabal-install
|
|||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||
haskell:latest true
|
||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils libstdc++-static gcc-c++
|
||||
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
||||
archlinux/base:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||
|
||||
# Ubuntu LTS
|
||||
ubuntu:24.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
|
||||
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
||||
# Other versions we want to support
|
||||
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
||||
|
||||
# Stack on Ubuntu LTS
|
||||
ubuntu:24.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||
# Misc Haskell including current and latest Stack build
|
||||
ubuntu:18.04 set -e; apt-get update && apt-get install -y curl && curl -sSL https://get.haskellstack.org/ | sh -s - -f && cd /mnt && exec test/stacktest
|
||||
EOF
|
||||
|
||||
exit "$final"
|
||||
|
|
|
@ -5,11 +5,8 @@ import System.Exit
|
|||
import qualified ShellCheck.Analytics
|
||||
import qualified ShellCheck.AnalyzerLib
|
||||
import qualified ShellCheck.ASTLib
|
||||
import qualified ShellCheck.CFG
|
||||
import qualified ShellCheck.CFGAnalysis
|
||||
import qualified ShellCheck.Checker
|
||||
import qualified ShellCheck.Checks.Commands
|
||||
import qualified ShellCheck.Checks.ControlFlow
|
||||
import qualified ShellCheck.Checks.Custom
|
||||
import qualified ShellCheck.Checks.ShellSupport
|
||||
import qualified ShellCheck.Fixer
|
||||
|
@ -18,24 +15,18 @@ import qualified ShellCheck.Parser
|
|||
|
||||
main = do
|
||||
putStrLn "Running ShellCheck tests..."
|
||||
failures <- filter (not . snd) <$> mapM sequenceA tests
|
||||
if null failures then exitSuccess else do
|
||||
putStrLn "Tests failed for the following module(s):"
|
||||
mapM (putStrLn . ("- ShellCheck." ++) . fst) failures
|
||||
exitFailure
|
||||
where
|
||||
tests =
|
||||
[ ("Analytics" , ShellCheck.Analytics.runTests)
|
||||
, ("AnalyzerLib" , ShellCheck.AnalyzerLib.runTests)
|
||||
, ("ASTLib" , ShellCheck.ASTLib.runTests)
|
||||
, ("CFG" , ShellCheck.CFG.runTests)
|
||||
, ("CFGAnalysis" , ShellCheck.CFGAnalysis.runTests)
|
||||
, ("Checker" , ShellCheck.Checker.runTests)
|
||||
, ("Checks.Commands" , ShellCheck.Checks.Commands.runTests)
|
||||
, ("Checks.ControlFlow" , ShellCheck.Checks.ControlFlow.runTests)
|
||||
, ("Checks.Custom" , ShellCheck.Checks.Custom.runTests)
|
||||
, ("Checks.ShellSupport", ShellCheck.Checks.ShellSupport.runTests)
|
||||
, ("Fixer" , ShellCheck.Fixer.runTests)
|
||||
, ("Formatter.Diff" , ShellCheck.Formatter.Diff.runTests)
|
||||
, ("Parser" , ShellCheck.Parser.runTests)
|
||||
results <- sequence [
|
||||
ShellCheck.Analytics.runTests
|
||||
,ShellCheck.AnalyzerLib.runTests
|
||||
,ShellCheck.ASTLib.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
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
# various resolvers. It's run via distrotest.
|
||||
|
||||
resolvers=(
|
||||
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||
)
|
||||
|
||||
die() { echo "$*" >&2; exit 1; }
|
||||
|
@ -15,14 +15,13 @@ die() { echo "$*" >&2; exit 1; }
|
|||
command -v stack ||
|
||||
die "stack is missing"
|
||||
|
||||
stack setup --allow-different-user || die "Failed to setup with default resolver"
|
||||
stack setup || die "Failed to setup with default resolver"
|
||||
stack build --test || die "Failed to build/test with default resolver"
|
||||
|
||||
# Nice to haves, but not necessary
|
||||
for resolver in "${resolvers[@]}"
|
||||
do
|
||||
stack --resolver="$resolver" setup || die "Failed to setup $resolver. This probably doesn't matter."
|
||||
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver! This probably doesn't matter."
|
||||
stack --resolver="$resolver" setup || die "Failed to setup $resolver"
|
||||
stack --resolver="$resolver" build --test || die "Failed build/test with $resolver!"
|
||||
done
|
||||
|
||||
echo "Success"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue