mirror of
https://github.com/koalaman/shellcheck.git
synced 2025-10-01 01:09:18 +08:00
Compare commits
133 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
c9b8ad3439 | ||
|
e59fbfebda | ||
|
ce3414eeea | ||
|
feebbbb096 | ||
|
87ef5ae18a | ||
|
0138a6fafc | ||
|
90d3172dfe | ||
|
d18b2553cf | ||
|
dfa920c5d2 | ||
|
fc9b63fb5e | ||
|
272ef819b9 | ||
|
08ae7ef836 | ||
|
e3d8483e49 | ||
|
dd747b2a98 | ||
|
9490b94886 | ||
|
372c0b667e | ||
|
01aee1a859 | ||
|
c9e27c2470 | ||
|
4ffa9cc397 | ||
|
b625cc1acc | ||
|
f03c437e2f | ||
|
824c802b63 | ||
|
b3932dfa10 | ||
|
a54965dd2c | ||
|
46b678fca8 | ||
|
be0d5d4163 | ||
|
5fec3f9b34 | ||
|
1164aa4efc | ||
|
ff85a5a2a2 | ||
|
08b437974e | ||
|
15fd2c314c | ||
|
e6e8ab0415 | ||
|
b1ca3929e3 | ||
|
c05380d518 | ||
|
2842ce97b8 | ||
|
78dea1d4f9 | ||
|
5a3eb89e38 | ||
|
a526ee0829 | ||
|
8c5fdc3522 | ||
|
ae199edb68 | ||
|
7cfcf6db8a | ||
|
a7c5be93dc | ||
|
8754c21244 | ||
|
985ca2530d | ||
|
3cae6cd6ab | ||
|
74b1745a19 | ||
|
495e34d101 | ||
|
2a16a4e8c1 | ||
|
3342902d9a | ||
|
0786b2bf3c | ||
|
84d8530f14 | ||
|
86e2b76730 | ||
|
b770984dfc | ||
|
d9c9e60fb0 | ||
|
14056a7f3a | ||
|
a524929b69 | ||
|
fa7943ac0e | ||
|
81c2ecaccb | ||
|
fcba462a99 | ||
|
43aca62ca7 | ||
|
128351f5ef | ||
|
d71d6ff294 | ||
|
bd65b67578 | ||
|
149b4dbd6f | ||
|
ef5f9a7af5 | ||
|
581981ba76 | ||
|
fcc473e27f | ||
|
0845b81183 | ||
|
966fb3e3dd | ||
|
f28462b01c | ||
|
ccab132b38 | ||
|
4806719035 | ||
|
0df9345142 | ||
|
77069f7445 | ||
|
04db46381f | ||
|
c76b8d9a32 | ||
|
d0dd81e1fa | ||
|
f440912279 | ||
|
3ce310e939 | ||
|
a30ac402eb | ||
|
4a27c9a8d5 | ||
|
b5f5e6347d | ||
|
c57e447c89 | ||
|
e9784fa9a7 | ||
|
f1148b8b41 | ||
|
982681fc05 | ||
|
52dac51cd4 | ||
|
30bb0e0093 | ||
|
d1d574c091 | ||
|
ea4e0091c7 | ||
|
81d9f7e640 | ||
|
69469c3603 | ||
|
5cf6e01ce9 | ||
|
f7857028f7 | ||
|
b261ec24f9 | ||
|
819470fa1d | ||
|
2f28847b08 | ||
|
e47480e93a | ||
|
9caeec104b | ||
|
95b3cbf071 | ||
|
e7f05d662a | ||
|
3ee4419ef4 | ||
|
8dc0fdb4cc | ||
|
da4885a71d | ||
|
642ad86125 | ||
|
f77a545282 | ||
|
7946bf5657 | ||
|
cc04b40119 | ||
|
c3bce51de3 | ||
|
a4042f7523 | ||
|
363c0633e0 | ||
|
7ceb1f1519 | ||
|
f1bdda54cb | ||
|
9aa4c22aa6 | ||
|
399c04cc17 | ||
|
fd595d1058 | ||
|
7c44e1060f | ||
|
2821552688 | ||
|
2034e3886e | ||
|
fa15c0a454 | ||
|
88cdb4e2c9 | ||
|
2292e852e5 | ||
|
ade2bf7b87 | ||
|
e6e558946c | ||
|
3a118246ef | ||
|
dd626686c4 | ||
|
866cbd0aa4 | ||
|
d7971dafd1 | ||
|
9092080a84 | ||
|
499c99372e | ||
|
d9a9d5db86 | ||
|
c5de58ae84 | ||
|
4c186c20b9 |
7
.github/dependabot.yml
vendored
Normal file
7
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version: 2
|
||||||
|
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
20
.github/workflows/build.yml
vendored
20
.github/workflows/build.yml
vendored
@@ -15,7 +15,7 @@ jobs:
|
|||||||
sudo apt-get install cabal-install
|
sudo apt-get install cabal-install
|
||||||
|
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ jobs:
|
|||||||
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
mv dist-newstyle/sdist/*.tar.gz source/source.tar.gz
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: source
|
name: source
|
||||||
path: source/
|
path: source/
|
||||||
@@ -51,10 +51,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Build source
|
- name: Build source
|
||||||
run: |
|
run: |
|
||||||
@@ -63,7 +63,7 @@ jobs:
|
|||||||
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
( cd bin && ../build/run_builder ../source/source.tar.gz ../build/${{matrix.build}} )
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: bin
|
name: bin
|
||||||
path: bin/
|
path: bin/
|
||||||
@@ -74,10 +74,10 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Work around GitHub permissions bug
|
- name: Work around GitHub permissions bug
|
||||||
run: chmod +x bin/*/shellcheck*
|
run: chmod +x bin/*/shellcheck*
|
||||||
@@ -92,7 +92,7 @@ jobs:
|
|||||||
rm -rf */ README* LICENSE*
|
rm -rf */ README* LICENSE*
|
||||||
|
|
||||||
- name: Upload artifact
|
- name: Upload artifact
|
||||||
uses: actions/upload-artifact@v2
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: deploy
|
name: deploy
|
||||||
path: deploy/
|
path: deploy/
|
||||||
@@ -104,10 +104,10 @@ jobs:
|
|||||||
environment: Deploy
|
environment: Deploy
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Download artifacts
|
- name: Download artifacts
|
||||||
uses: actions/download-artifact@v2
|
uses: actions/download-artifact@v3
|
||||||
|
|
||||||
- name: Upload to GitHub
|
- name: Upload to GitHub
|
||||||
env:
|
env:
|
||||||
|
@@ -3,28 +3,10 @@
|
|||||||
# binaries previously built and deployed to GitHub.
|
# binaries previously built and deployed to GitHub.
|
||||||
|
|
||||||
function multi_arch_docker::install_docker_buildx() {
|
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.
|
# Install QEMU multi-architecture support for docker buildx.
|
||||||
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes
|
||||||
|
|
||||||
# Instantiate docker buildx builder with multi-architecture support.
|
# Instantiate docker buildx builder with multi-architecture support.
|
||||||
export DOCKER_CLI_EXPERIMENTAL=enabled
|
|
||||||
docker buildx create --name mybuilder
|
docker buildx create --name mybuilder
|
||||||
docker buildx use mybuilder
|
docker buildx use mybuilder
|
||||||
# Start up buildx and verify that all is OK.
|
# Start up buildx and verify that all is OK.
|
||||||
|
34
CHANGELOG.md
34
CHANGELOG.md
@@ -1,3 +1,37 @@
|
|||||||
|
## Git
|
||||||
|
### Added
|
||||||
|
- 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.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- source statements with here docs now work correctly
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
|
||||||
|
## 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
|
## v0.8.0 - 2021-11-06
|
||||||
### Added
|
### Added
|
||||||
- `disable=all` now conveniently disables all warnings
|
- `disable=all` now conveniently disables all warnings
|
||||||
|
10
LICENSE
10
LICENSE
@@ -1,13 +1,3 @@
|
|||||||
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
|
GNU GENERAL PUBLIC LICENSE
|
||||||
Version 3, 29 June 2007
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
@@ -112,6 +112,7 @@ Services and platforms that have ShellCheck pre-installed and ready to use:
|
|||||||
* [Code Factor](https://www.codefactor.io/)
|
* [Code Factor](https://www.codefactor.io/)
|
||||||
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
* [CircleCI](https://circleci.com) via the [ShellCheck Orb](https://circleci.com/orbs/registry/orb/circleci/shellcheck)
|
||||||
* [Github](https://github.com/features/actions) (only Linux)
|
* [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)
|
||||||
|
|
||||||
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
Most other services, including [GitLab](https://about.gitlab.com/), let you install
|
||||||
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
ShellCheck yourself, either through the system's package manager (see [Installing](#installing)),
|
||||||
@@ -232,6 +233,9 @@ Alternatively, you can download pre-compiled binaries for the latest release her
|
|||||||
or see the [GitHub Releases](https://github.com/koalaman/shellcheck/releases) for other releases
|
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).
|
(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:
|
Distro packages already come with a `man` page. If you are building from source, it can be installed with:
|
||||||
|
|
||||||
```console
|
```console
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
Name: ShellCheck
|
Name: ShellCheck
|
||||||
Version: 0.8.0
|
Version: 0.9.0
|
||||||
Synopsis: Shell script analysis tool
|
Synopsis: Shell script analysis tool
|
||||||
License: GPL-3
|
License: GPL-3
|
||||||
License-file: LICENSE
|
License-file: LICENSE
|
||||||
@@ -45,19 +45,26 @@ library
|
|||||||
build-depends:
|
build-depends:
|
||||||
semigroups
|
semigroups
|
||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
-- The lower bounds are based on GHC 7.10.3
|
||||||
array,
|
-- The upper bounds are based on GHC 9.6.1
|
||||||
|
aeson >= 1.4.0 && < 2.2,
|
||||||
|
array >= 0.5.1 && < 0.6,
|
||||||
base >= 4.8.0.0 && < 5,
|
base >= 4.8.0.0 && < 5,
|
||||||
bytestring,
|
bytestring >= 0.10.6 && < 0.12,
|
||||||
containers >= 0.5,
|
containers >= 0.5.6 && < 0.7,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq >= 1.4.1 && < 1.5,
|
||||||
Diff >= 0.2.0,
|
Diff >= 0.4.0 && < 0.5,
|
||||||
directory >= 1.2.3.0,
|
fgl (>= 5.7.0 && < 5.8.1.0) || (>= 5.8.1.1 && < 5.9),
|
||||||
mtl >= 2.2.1,
|
filepath >= 1.4.0 && < 1.5,
|
||||||
filepath,
|
mtl >= 2.2.2 && < 2.4,
|
||||||
parsec,
|
parsec >= 3.1.14 && < 3.2,
|
||||||
regex-tdfa,
|
QuickCheck >= 2.14.2 && < 2.15,
|
||||||
QuickCheck >= 2.7.4,
|
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,
|
||||||
|
|
||||||
-- When cabal supports it, move this to setup-depends:
|
-- When cabal supports it, move this to setup-depends:
|
||||||
process
|
process
|
||||||
exposed-modules:
|
exposed-modules:
|
||||||
@@ -66,11 +73,15 @@ library
|
|||||||
ShellCheck.Analytics
|
ShellCheck.Analytics
|
||||||
ShellCheck.Analyzer
|
ShellCheck.Analyzer
|
||||||
ShellCheck.AnalyzerLib
|
ShellCheck.AnalyzerLib
|
||||||
|
ShellCheck.CFG
|
||||||
|
ShellCheck.CFGAnalysis
|
||||||
ShellCheck.Checker
|
ShellCheck.Checker
|
||||||
ShellCheck.Checks.Commands
|
ShellCheck.Checks.Commands
|
||||||
|
ShellCheck.Checks.ControlFlow
|
||||||
ShellCheck.Checks.Custom
|
ShellCheck.Checks.Custom
|
||||||
ShellCheck.Checks.ShellSupport
|
ShellCheck.Checks.ShellSupport
|
||||||
ShellCheck.Data
|
ShellCheck.Data
|
||||||
|
ShellCheck.Debug
|
||||||
ShellCheck.Fixer
|
ShellCheck.Fixer
|
||||||
ShellCheck.Formatter.Format
|
ShellCheck.Formatter.Format
|
||||||
ShellCheck.Formatter.CheckStyle
|
ShellCheck.Formatter.CheckStyle
|
||||||
@@ -82,6 +93,8 @@ library
|
|||||||
ShellCheck.Formatter.Quiet
|
ShellCheck.Formatter.Quiet
|
||||||
ShellCheck.Interface
|
ShellCheck.Interface
|
||||||
ShellCheck.Parser
|
ShellCheck.Parser
|
||||||
|
ShellCheck.PortageVariables
|
||||||
|
ShellCheck.Prelude
|
||||||
ShellCheck.Regex
|
ShellCheck.Regex
|
||||||
other-modules:
|
other-modules:
|
||||||
Paths_ShellCheck
|
Paths_ShellCheck
|
||||||
@@ -94,17 +107,19 @@ executable shellcheck
|
|||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
aeson,
|
||||||
array,
|
array,
|
||||||
base >= 4 && < 5,
|
base,
|
||||||
bytestring,
|
bytestring,
|
||||||
containers,
|
containers,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq,
|
||||||
Diff >= 0.2.0,
|
Diff,
|
||||||
directory >= 1.2.3.0,
|
directory,
|
||||||
mtl >= 2.2.1,
|
fgl,
|
||||||
|
mtl,
|
||||||
filepath,
|
filepath,
|
||||||
parsec >= 3.0,
|
parsec,
|
||||||
QuickCheck >= 2.7.4,
|
QuickCheck,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
ShellCheck
|
ShellCheck
|
||||||
default-language: Haskell98
|
default-language: Haskell98
|
||||||
main-is: shellcheck.hs
|
main-is: shellcheck.hs
|
||||||
@@ -114,17 +129,19 @@ test-suite test-shellcheck
|
|||||||
build-depends:
|
build-depends:
|
||||||
aeson,
|
aeson,
|
||||||
array,
|
array,
|
||||||
base >= 4 && < 5,
|
base,
|
||||||
bytestring,
|
bytestring,
|
||||||
containers,
|
containers,
|
||||||
deepseq >= 1.4.0.0,
|
deepseq,
|
||||||
Diff >= 0.2.0,
|
Diff,
|
||||||
directory >= 1.2.3.0,
|
directory,
|
||||||
mtl >= 2.2.1,
|
fgl,
|
||||||
filepath,
|
filepath,
|
||||||
|
mtl,
|
||||||
parsec,
|
parsec,
|
||||||
QuickCheck >= 2.7.4,
|
QuickCheck,
|
||||||
regex-tdfa,
|
regex-tdfa,
|
||||||
|
transformers,
|
||||||
ShellCheck
|
ShellCheck
|
||||||
default-language: Haskell98
|
default-language: Haskell98
|
||||||
main-is: test/shellcheck.hs
|
main-is: test/shellcheck.hs
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
# DIGEST:sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
FROM liushuyu/osxcross@sha256:fa32af4677e2860a1c5950bc8c360f309e2a87e2ddfed27b642fddf7a6093b76
|
||||||
FROM liushuyu/osxcross:latest
|
|
||||||
|
|
||||||
ENV TARGET x86_64-apple-darwin18
|
ENV TARGET x86_64-apple-darwin18
|
||||||
ENV TARGETNAME darwin.x86_64
|
ENV TARGETNAME darwin.x86_64
|
||||||
@@ -7,15 +6,18 @@ ENV TARGETNAME darwin.x86_64
|
|||||||
# Build dependencies
|
# Build dependencies
|
||||||
USER root
|
USER root
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
RUN apt-get update && apt-get install -y ghc automake autoconf llvm curl
|
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
|
||||||
|
|
||||||
# Build GHC
|
# Build GHC
|
||||||
WORKDIR /ghc
|
WORKDIR /ghc
|
||||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
|
||||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
RUN ./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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
RUN make install
|
RUN make install
|
||||||
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
|
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
|
||||||
|
|
||||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
# 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
|
# It won't reuse caches if ghc-options are specified in ~/.cabal/config
|
||||||
|
@@ -6,19 +6,25 @@ ENV TARGETNAME linux.aarch64
|
|||||||
# Build dependencies
|
# Build dependencies
|
||||||
USER root
|
USER root
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
ENV DEBIAN_FRONTEND noninteractive
|
||||||
RUN apt-get update && apt-get install -y ghc automake autoconf build-essential llvm curl qemu-user-static gcc-$TARGET
|
|
||||||
|
# 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
|
||||||
|
RUN apt-get update && apt-get install -y ghc alex happy automake autoconf build-essential curl qemu-user-static
|
||||||
|
|
||||||
# Build GHC
|
# Build GHC
|
||||||
WORKDIR /ghc
|
WORKDIR /ghc
|
||||||
RUN curl -L "https://downloads.haskell.org/~ghc/8.10.4/ghc-8.10.4-src.tar.xz" | tar xJ --strip-components=1
|
RUN curl -L "https://downloads.haskell.org/~ghc/9.2.5/ghc-9.2.5-src.tar.xz" | tar xJ --strip-components=1
|
||||||
RUN ./boot && ./configure --host x86_64-linux-gnu --build x86_64-linux-gnu --target "$TARGET"
|
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 cp mk/flavours/quick-cross.mk mk/build.mk && make -j "$(nproc)"
|
||||||
RUN make install
|
RUN make install
|
||||||
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
|
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
|
||||||
|
|
||||||
# Due to an apparent cabal bug, we specify our options directly to cabal
|
# 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
|
# 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;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
|
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections -optc-fPIC;--with-ghc=$TARGET-ghc;--with-hc-pkg=$TARGET-ghc-pkg"
|
||||||
|
|
||||||
# Prebuild the dependencies
|
# Prebuild the dependencies
|
||||||
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
RUN cabal update && IFS=';' && cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
@@ -52,6 +52,7 @@ RUN pirun apt-get install -y ghc cabal-install
|
|||||||
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
|
ENV CABALOPTS "--ghc-options;-split-sections -optc-Os -optc-Wl,--gc-sections;--gcc-options;-Os -Wl,--gc-sections -ffunction-sections -fdata-sections"
|
||||||
RUN pirun cabal update
|
RUN pirun cabal update
|
||||||
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
RUN IFS=";" && pirun cabal install --dependencies-only $CABALOPTS ShellCheck
|
||||||
|
RUN IFS=';' && pirun cabal install $CABALOPTS --lib fgl
|
||||||
|
|
||||||
# Copy the build script
|
# Copy the build script
|
||||||
WORKDIR /pi/scratch
|
WORKDIR /pi/scratch
|
||||||
|
@@ -1,16 +1,10 @@
|
|||||||
FROM ubuntu:20.04
|
FROM alpine:latest
|
||||||
|
|
||||||
ENV TARGETNAME linux.x86_64
|
ENV TARGETNAME linux.x86_64
|
||||||
|
|
||||||
# Install GHC and cabal
|
# Install GHC and cabal
|
||||||
USER root
|
USER root
|
||||||
ENV DEBIAN_FRONTEND noninteractive
|
RUN apk add ghc cabal g++ libffi-dev curl bash
|
||||||
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
|
# Use ld.bfd instead of ld.gold due to
|
||||||
# x86_64-linux-gnu/libpthread.a(pthread_cond_init.o)(.note.stapsdt+0x14): error:
|
# 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
|
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
|
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://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-7.75.0/curl-7.75.0-win64-mingw.zip" | busybox unzip - && mv curl-7.75.0-win64-mingw/bin/* .
|
RUN curl -L "https://curl.se/windows/dl-7.84.0/curl-7.84.0-win64-mingw.zip" | busybox unzip - && mv curl-7.84.0-win64-mingw/bin/* .
|
||||||
ENV WINEPATH /haskell/bin
|
ENV WINEPATH /haskell/bin
|
||||||
|
|
||||||
# It's unknown whether Cabal on Windows suffers from the same issue
|
# It's unknown whether Cabal on Windows suffers from the same issue
|
||||||
|
294
doc/shellcheck_logo.svg
Normal file
294
doc/shellcheck_logo.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 244 KiB |
@@ -87,8 +87,9 @@ not warn at all, as `ksh` supports decimals in arithmetic contexts.
|
|||||||
|
|
||||||
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
: Specify Bourne shell dialect. Valid values are *sh*, *bash*, *dash* and *ksh*.
|
||||||
The default is to deduce the shell from the file's `shell` directive,
|
The default is to deduce the shell from the file's `shell` directive,
|
||||||
shebang, or `.bash/.bats/.dash/.ksh` extension, in that order. *sh* refers to
|
shebang, or `.bash/.bats/.dash/.ksh/.ebuild/.eclass` extension, in that
|
||||||
POSIX `sh` (not the system's), and will warn of portability issues.
|
order. *sh* refers to POSIX `sh` (not the system's), and will warn of
|
||||||
|
portability issues.
|
||||||
|
|
||||||
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
**-S**\ *SEVERITY*,\ **--severity=***severity*
|
||||||
|
|
||||||
@@ -282,6 +283,9 @@ Here is an example `.shellcheckrc`:
|
|||||||
source-path=SCRIPTDIR
|
source-path=SCRIPTDIR
|
||||||
source-path=/mnt/chroot
|
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
|
# Allow opening any 'source'd file, even if not specified as input
|
||||||
external-sources=true
|
external-sources=true
|
||||||
|
|
||||||
@@ -375,7 +379,7 @@ long list of wonderful contributors.
|
|||||||
|
|
||||||
# COPYRIGHT
|
# COPYRIGHT
|
||||||
|
|
||||||
Copyright 2012-2021, Vidar Holen and contributors.
|
Copyright 2012-2022, Vidar Holen and contributors.
|
||||||
Licensed under the GNU General Public License version 3 or later,
|
Licensed under the GNU General Public License version 3 or later,
|
||||||
see https://gnu.org/licenses/gpl.html
|
see https://gnu.org/licenses/gpl.html
|
||||||
|
|
||||||
|
@@ -21,6 +21,7 @@ import qualified ShellCheck.Analyzer
|
|||||||
import ShellCheck.Checker
|
import ShellCheck.Checker
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.PortageVariables
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import qualified ShellCheck.Formatter.CheckStyle
|
import qualified ShellCheck.Formatter.CheckStyle
|
||||||
@@ -34,6 +35,8 @@ import qualified ShellCheck.Formatter.Quiet
|
|||||||
|
|
||||||
import Control.Exception
|
import Control.Exception
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
|
import Control.Monad.IO.Class
|
||||||
|
import Control.Monad.Trans.Class
|
||||||
import Control.Monad.Except
|
import Control.Monad.Except
|
||||||
import Data.Bits
|
import Data.Bits
|
||||||
import Data.Char
|
import Data.Char
|
||||||
@@ -225,7 +228,7 @@ runFormatter sys format options files = do
|
|||||||
f :: Status -> FilePath -> IO Status
|
f :: Status -> FilePath -> IO Status
|
||||||
f status file = do
|
f status file = do
|
||||||
newStatus <- process file `catch` handler file
|
newStatus <- process file `catch` handler file
|
||||||
return $ status `mappend` newStatus
|
return $! status `mappend` newStatus
|
||||||
handler :: FilePath -> IOException -> IO Status
|
handler :: FilePath -> IOException -> IO Status
|
||||||
handler file e = reportFailure file (show e)
|
handler file e = reportFailure file (show e)
|
||||||
reportFailure file str = do
|
reportFailure file str = do
|
||||||
@@ -394,10 +397,12 @@ ioInterface options files = do
|
|||||||
inputs <- mapM normalize files
|
inputs <- mapM normalize files
|
||||||
cache <- newIORef emptyCache
|
cache <- newIORef emptyCache
|
||||||
configCache <- newIORef ("", Nothing)
|
configCache <- newIORef ("", Nothing)
|
||||||
return SystemInterface {
|
portageVars <- newIORef Nothing
|
||||||
|
return (newSystemInterface :: SystemInterface IO) {
|
||||||
siReadFile = get cache inputs,
|
siReadFile = get cache inputs,
|
||||||
siFindSource = findSourceFile inputs (sourcePaths options),
|
siFindSource = findSourceFile inputs (sourcePaths options),
|
||||||
siGetConfig = getConfig configCache
|
siGetConfig = getConfig configCache,
|
||||||
|
siGetPortageVariables = getOrLoadPortage portageVars
|
||||||
}
|
}
|
||||||
where
|
where
|
||||||
emptyCache :: Map.Map FilePath String
|
emptyCache :: Map.Map FilePath String
|
||||||
@@ -521,6 +526,21 @@ ioInterface options files = do
|
|||||||
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
|
("SCRIPTDIR":rest) -> joinPath (scriptdir:rest)
|
||||||
_ -> str
|
_ -> str
|
||||||
|
|
||||||
|
getOrLoadPortage cache = do
|
||||||
|
x <- readIORef cache
|
||||||
|
case x of
|
||||||
|
Just m -> do
|
||||||
|
return m
|
||||||
|
Nothing -> do
|
||||||
|
vars <- readPortageVariables `catch` handler
|
||||||
|
writeIORef cache $ Just vars
|
||||||
|
return vars
|
||||||
|
where
|
||||||
|
handler :: IOException -> IO (Map.Map String [String])
|
||||||
|
handler e = do
|
||||||
|
hPutStrLn stderr $ "Error finding portage repos, eclass definitions will be ignored: " ++ show e
|
||||||
|
return $ Map.empty
|
||||||
|
|
||||||
inputFile file = do
|
inputFile file = do
|
||||||
(handle, shouldCache) <-
|
(handle, shouldCache) <-
|
||||||
if file == "-"
|
if file == "-"
|
||||||
|
@@ -45,6 +45,7 @@ data InnerToken t =
|
|||||||
| Inner_TA_Variable String [t]
|
| Inner_TA_Variable String [t]
|
||||||
| Inner_TA_Expansion [t]
|
| Inner_TA_Expansion [t]
|
||||||
| Inner_TA_Sequence [t]
|
| Inner_TA_Sequence [t]
|
||||||
|
| Inner_TA_Parenthesis t
|
||||||
| Inner_TA_Trinary t t t
|
| Inner_TA_Trinary t t t
|
||||||
| Inner_TA_Unary String t
|
| Inner_TA_Unary String t
|
||||||
| Inner_TC_And ConditionType String t t
|
| Inner_TC_And ConditionType String t t
|
||||||
@@ -141,7 +142,7 @@ data InnerToken t =
|
|||||||
| Inner_T_CoProcBody t
|
| Inner_T_CoProcBody t
|
||||||
| Inner_T_Include t
|
| Inner_T_Include t
|
||||||
| Inner_T_SourceCommand t t
|
| Inner_T_SourceCommand t t
|
||||||
| Inner_T_BatsTest t t
|
| Inner_T_BatsTest String t
|
||||||
deriving (Show, Eq, Functor, Foldable, Traversable)
|
deriving (Show, Eq, Functor, Foldable, Traversable)
|
||||||
|
|
||||||
data Annotation =
|
data Annotation =
|
||||||
@@ -204,6 +205,7 @@ 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_Arithmetic id c = OuterToken id (Inner_T_Arithmetic c)
|
||||||
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
pattern T_Array id t = OuterToken id (Inner_T_Array t)
|
||||||
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
pattern TA_Sequence id l = OuterToken id (Inner_TA_Sequence l)
|
||||||
|
pattern TA_Parentesis 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 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_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)
|
pattern TA_Unary id op t1 = OuterToken id (Inner_TA_Unary op t1)
|
||||||
@@ -256,7 +258,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_UntilExpression id c l = OuterToken id (Inner_T_UntilExpression c l)
|
||||||
pattern T_WhileExpression id c l = OuterToken id (Inner_T_WhileExpression 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, 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, TA_Parentesis, 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
|
instance Eq Token where
|
||||||
OuterToken _ a == OuterToken _ b = a == b
|
OuterToken _ a == OuterToken _ b = a == b
|
||||||
|
@@ -21,6 +21,7 @@
|
|||||||
module ShellCheck.ASTLib where
|
module ShellCheck.ASTLib where
|
||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad.Writer
|
import Control.Monad.Writer
|
||||||
@@ -35,8 +36,6 @@ import Numeric (showHex)
|
|||||||
|
|
||||||
import Test.QuickCheck
|
import Test.QuickCheck
|
||||||
|
|
||||||
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
|
||||||
|
|
||||||
-- Is this a type of loop?
|
-- Is this a type of loop?
|
||||||
isLoop t = case t of
|
isLoop t = case t of
|
||||||
T_WhileExpression {} -> True
|
T_WhileExpression {} -> True
|
||||||
@@ -138,7 +137,7 @@ getFlagsUntil stopCondition (T_SimpleCommand _ _ (_:args)) =
|
|||||||
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
flag (x, '-':'-':arg) = [ (x, takeWhile (/= '=') arg) ]
|
||||||
flag (x, '-':args) = map (\v -> (x, [v])) args
|
flag (x, '-':args) = map (\v -> (x, [v])) args
|
||||||
flag (x, _) = [ (x, "") ]
|
flag (x, _) = [ (x, "") ]
|
||||||
getFlagsUntil _ _ = error "Internal shellcheck error, please report! (getFlags on non-command)"
|
getFlagsUntil _ _ = error $ pleaseReport "getFlags on non-command"
|
||||||
|
|
||||||
-- Get all flags in a GNU way, up until --
|
-- Get all flags in a GNU way, up until --
|
||||||
getAllFlags :: Token -> [(Token, String)]
|
getAllFlags :: Token -> [(Token, String)]
|
||||||
@@ -369,6 +368,21 @@ getGlobOrLiteralString = getLiteralStringExt f
|
|||||||
f (T_Glob _ str) = return str
|
f (T_Glob _ str) = return str
|
||||||
f _ = Nothing
|
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
|
-- Maybe get the literal value of a token, using a custom function
|
||||||
-- to map unrecognized Tokens into strings.
|
-- to map unrecognized Tokens into strings.
|
||||||
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
getLiteralStringExt :: Monad m => (Token -> m String) -> Token -> m String
|
||||||
@@ -401,14 +415,15 @@ getLiteralStringExt more = g
|
|||||||
'\\' -> '\\' : rest
|
'\\' -> '\\' : rest
|
||||||
'x' ->
|
'x' ->
|
||||||
case cs of
|
case cs of
|
||||||
(x:y:more) ->
|
(x:y:more) | isHexDigit x && isHexDigit y ->
|
||||||
if isHexDigit x && isHexDigit y
|
chr (16*(digitToInt x) + (digitToInt y)) : decodeEscapes more
|
||||||
then chr (16*(digitToInt x) + (digitToInt y)) : rest
|
(x:more) | isHexDigit x ->
|
||||||
else '\\':c:rest
|
chr (digitToInt x) : decodeEscapes more
|
||||||
|
more -> '\\' : 'x' : decodeEscapes more
|
||||||
_ | isOctDigit c ->
|
_ | isOctDigit c ->
|
||||||
let digits = take 3 $ takeWhile isOctDigit (c:cs)
|
let (digits, more) = spanMax isOctDigit 3 (c:cs)
|
||||||
num = parseOct digits
|
num = (parseOct digits) `mod` 256
|
||||||
in (if num < 256 then chr num else '?') : rest
|
in (chr num) : decodeEscapes more
|
||||||
_ -> '\\' : c : rest
|
_ -> '\\' : c : rest
|
||||||
where
|
where
|
||||||
rest = decodeEscapes cs
|
rest = decodeEscapes cs
|
||||||
@@ -416,6 +431,11 @@ getLiteralStringExt more = g
|
|||||||
where
|
where
|
||||||
f n "" = n
|
f n "" = n
|
||||||
f n (c:rest) = f (n * 8 + digitToInt c) rest
|
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 (c:cs) = c : decodeEscapes cs
|
||||||
decodeEscapes [] = []
|
decodeEscapes [] = []
|
||||||
|
|
||||||
@@ -524,17 +544,16 @@ getCommandNameAndToken direct t = fromMaybe (Nothing, t) $ do
|
|||||||
return t
|
return t
|
||||||
_ -> fail ""
|
_ -> fail ""
|
||||||
|
|
||||||
-- If a command substitution is a single command, get its name.
|
-- If a command substitution is a single SimpleCommand, return it.
|
||||||
-- $(date +%s) = Just "date"
|
getSimpleCommandFromExpansion :: Token -> Maybe Token
|
||||||
getCommandNameFromExpansion :: Token -> Maybe String
|
getSimpleCommandFromExpansion t =
|
||||||
getCommandNameFromExpansion t =
|
|
||||||
case t of
|
case t of
|
||||||
T_DollarExpansion _ [c] -> extract c
|
T_DollarExpansion _ [c] -> extract c
|
||||||
T_Backticked _ [c] -> extract c
|
T_Backticked _ [c] -> extract c
|
||||||
T_DollarBraceCommandExpansion _ [c] -> extract c
|
T_DollarBraceCommandExpansion _ [c] -> extract c
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
where
|
where
|
||||||
extract (T_Pipeline _ _ [cmd]) = getCommandName cmd
|
extract (T_Pipeline _ _ [c]) = getCommand c
|
||||||
extract _ = Nothing
|
extract _ = Nothing
|
||||||
|
|
||||||
-- Get the basename of a token representing a command
|
-- Get the basename of a token representing a command
|
||||||
@@ -542,6 +561,10 @@ getCommandBasename = fmap basename . getCommandName
|
|||||||
|
|
||||||
basename = reverse . takeWhile (/= '/') . reverse
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
|
|
||||||
|
-- Get the arguments to a command
|
||||||
|
arguments (T_SimpleCommand _ _ (cmd:args)) = args
|
||||||
|
arguments t = maybe [] arguments (getCommand t)
|
||||||
|
|
||||||
isAssignment t =
|
isAssignment t =
|
||||||
case t of
|
case t of
|
||||||
T_Redirecting _ _ w -> isAssignment w
|
T_Redirecting _ _ w -> isAssignment w
|
||||||
@@ -764,5 +787,146 @@ executableFromShebang = shellFor
|
|||||||
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
basename s = reverse . takeWhile (/= '/') . reverse $ s
|
||||||
skipFlags = dropWhile ("-" `isPrefixOf`)
|
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
|
||||||
|
match <- matchRegex re s
|
||||||
|
index <- match !!! 0
|
||||||
|
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 ]
|
||||||
|
match <- matchRegex re mods
|
||||||
|
offsets <- match !!! 1
|
||||||
|
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 t = t :
|
||||||
|
case Map.lookup (getId t) tree of
|
||||||
|
Nothing -> []
|
||||||
|
Just parent -> getPath tree parent
|
||||||
|
|
||||||
|
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]
|
||||||
|
_ -> []
|
||||||
|
|
||||||
|
|
||||||
|
commandExpansionShouldBeSplit t = do
|
||||||
|
cmd <- getSimpleCommandFromExpansion t
|
||||||
|
name <- getCommandName cmd
|
||||||
|
case () of
|
||||||
|
-- Should probably be split
|
||||||
|
_ | name `elem` ["seq", "pgrep"] -> return True
|
||||||
|
-- Portage macros that return a single word or nothing
|
||||||
|
_ | name `elem` ["usev", "use_with", "use_enable"] -> return True
|
||||||
|
-- Portage macros that are fine as long as the arguments have no spaces
|
||||||
|
_ | name `elem` ["usex", "meson_use", "meson_feature"] -> do
|
||||||
|
return . not $ any (' ' `elem`) $ map (getLiteralStringDef " ") $ arguments cmd
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{-
|
{-
|
||||||
Copyright 2012-2019 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
@@ -25,28 +25,31 @@ import ShellCheck.Interface
|
|||||||
import Data.List
|
import Data.List
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import qualified ShellCheck.Checks.Commands
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
import qualified ShellCheck.Checks.ControlFlow
|
||||||
import qualified ShellCheck.Checks.Custom
|
import qualified ShellCheck.Checks.Custom
|
||||||
import qualified ShellCheck.Checks.ShellSupport
|
import qualified ShellCheck.Checks.ShellSupport
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Clean up the cruft this is layered on
|
-- TODO: Clean up the cruft this is layered on
|
||||||
analyzeScript :: AnalysisSpec -> AnalysisResult
|
analyzeScript :: Monad m => SystemInterface m -> AnalysisSpec -> m AnalysisResult
|
||||||
analyzeScript spec = newAnalysisResult {
|
analyzeScript sys spec = do
|
||||||
|
params <- makeParameters sys spec
|
||||||
|
return $ newAnalysisResult {
|
||||||
arComments =
|
arComments =
|
||||||
filterByAnnotation spec params . nub $
|
filterByAnnotation spec params . nub $
|
||||||
runAnalytics spec
|
runChecker params (checkers spec params)
|
||||||
++ runChecker params (checkers spec params)
|
}
|
||||||
}
|
|
||||||
where
|
|
||||||
params = makeParameters spec
|
|
||||||
|
|
||||||
checkers spec params = mconcat $ map ($ params) [
|
checkers spec params = mconcat $ map ($ params) [
|
||||||
|
ShellCheck.Analytics.checker spec,
|
||||||
ShellCheck.Checks.Commands.checker spec,
|
ShellCheck.Checks.Commands.checker spec,
|
||||||
|
ShellCheck.Checks.ControlFlow.checker spec,
|
||||||
ShellCheck.Checks.Custom.checker,
|
ShellCheck.Checks.Custom.checker,
|
||||||
ShellCheck.Checks.ShellSupport.checker
|
ShellCheck.Checks.ShellSupport.checker
|
||||||
]
|
]
|
||||||
|
|
||||||
optionalChecks = mconcat $ [
|
optionalChecks = mconcat $ [
|
||||||
ShellCheck.Analytics.optionalChecks,
|
ShellCheck.Analytics.optionalChecks,
|
||||||
ShellCheck.Checks.Commands.optionalChecks
|
ShellCheck.Checks.Commands.optionalChecks,
|
||||||
|
ShellCheck.Checks.ControlFlow.optionalChecks
|
||||||
]
|
]
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{-
|
{-
|
||||||
Copyright 2012-2021 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
@@ -23,13 +23,16 @@ module ShellCheck.AnalyzerLib where
|
|||||||
|
|
||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
|
import qualified ShellCheck.CFGAnalysis as CF
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Arrow (first)
|
import Control.Arrow (first)
|
||||||
import Control.DeepSeq
|
import Control.DeepSeq
|
||||||
|
import Control.Monad
|
||||||
import Control.Monad.Identity
|
import Control.Monad.Identity
|
||||||
import Control.Monad.RWS
|
import Control.Monad.RWS
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
@@ -85,8 +88,12 @@ data Parameters = Parameters {
|
|||||||
hasSetE :: Bool,
|
hasSetE :: Bool,
|
||||||
-- Whether this script has 'set -o pipefail' anywhere.
|
-- Whether this script has 'set -o pipefail' anywhere.
|
||||||
hasPipefail :: Bool,
|
hasPipefail :: Bool,
|
||||||
|
-- Whether this script is an Ebuild file.
|
||||||
|
isPortage :: Bool,
|
||||||
-- A linear (bad) analysis of data flow
|
-- A linear (bad) analysis of data flow
|
||||||
variableFlow :: [StackData],
|
variableFlow :: [StackData],
|
||||||
|
-- A map from Id to Token
|
||||||
|
idMap :: Map.Map Id Token,
|
||||||
-- A map from Id to parent Token
|
-- A map from Id to parent Token
|
||||||
parentMap :: Map.Map Id Token,
|
parentMap :: Map.Map Id Token,
|
||||||
-- The shell type, such as Bash or Ksh
|
-- The shell type, such as Bash or Ksh
|
||||||
@@ -96,9 +103,14 @@ data Parameters = Parameters {
|
|||||||
-- The root node of the AST
|
-- The root node of the AST
|
||||||
rootNode :: Token,
|
rootNode :: Token,
|
||||||
-- map from token id to start and end position
|
-- map from token id to start and end position
|
||||||
tokenPositions :: Map.Map Id (Position, Position)
|
tokenPositions :: Map.Map Id (Position, Position),
|
||||||
|
-- Result from Control Flow Graph analysis (including data flow analysis)
|
||||||
|
cfgAnalysis :: CF.CFGAnalysis,
|
||||||
|
-- A set of additional variables known to be set (TODO: make this more general?)
|
||||||
|
additionalKnownVariables :: [String]
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
|
|
||||||
-- TODO: Cache results of common AST ops here
|
-- TODO: Cache results of common AST ops here
|
||||||
data Cache = Cache {}
|
data Cache = Cache {}
|
||||||
|
|
||||||
@@ -145,7 +157,7 @@ producesComments c s = do
|
|||||||
let pr = pScript s
|
let pr = pScript s
|
||||||
prRoot pr
|
prRoot pr
|
||||||
let spec = defaultSpec pr
|
let spec = defaultSpec pr
|
||||||
let params = makeParameters spec
|
let params = runIdentity $ makeParameters (mockedSystemInterface []) spec
|
||||||
return . not . null $ filterByAnnotation spec params $ runChecker params c
|
return . not . null $ filterByAnnotation spec params $ runChecker params c
|
||||||
|
|
||||||
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
makeComment :: Severity -> Id -> Code -> String -> TokenComment
|
||||||
@@ -189,35 +201,59 @@ makeCommentWithFix severity id code str fix =
|
|||||||
}
|
}
|
||||||
in force withFix
|
in force withFix
|
||||||
|
|
||||||
makeParameters spec =
|
makeParameters :: Monad m => SystemInterface m -> AnalysisSpec -> m Parameters
|
||||||
let params = Parameters {
|
makeParameters sys spec = do
|
||||||
|
extraVars <-
|
||||||
|
if asIsPortage spec
|
||||||
|
then do
|
||||||
|
vars <- siGetPortageVariables sys
|
||||||
|
let classes = getInheritedEclasses root
|
||||||
|
return $ concatMap (\c -> Map.findWithDefault [] c vars) classes
|
||||||
|
else
|
||||||
|
return []
|
||||||
|
|
||||||
|
return $ makeParams extraVars
|
||||||
|
where
|
||||||
|
shell = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec
|
||||||
|
makeParams extraVars = params
|
||||||
|
where
|
||||||
|
params = Parameters {
|
||||||
rootNode = root,
|
rootNode = root,
|
||||||
shellType = fromMaybe (determineShell (asFallbackShell spec) root) $ asShellType spec,
|
shellType = shell,
|
||||||
hasSetE = containsSetE root,
|
hasSetE = containsSetE root,
|
||||||
hasLastpipe =
|
hasLastpipe =
|
||||||
case shellType params of
|
case shellType params of
|
||||||
Bash -> containsLastpipe root
|
Bash -> isOptionSet "lastpipe" root
|
||||||
Dash -> False
|
Dash -> False
|
||||||
Sh -> False
|
Sh -> False
|
||||||
Ksh -> True,
|
Ksh -> True,
|
||||||
hasInheritErrexit =
|
hasInheritErrexit =
|
||||||
case shellType params of
|
case shellType params of
|
||||||
Bash -> containsInheritErrexit root
|
Bash -> isOptionSet "inherit_errexit" root
|
||||||
Dash -> True
|
Dash -> True
|
||||||
Sh -> True
|
Sh -> True
|
||||||
Ksh -> False,
|
Ksh -> False,
|
||||||
hasPipefail =
|
hasPipefail =
|
||||||
case shellType params of
|
case shellType params of
|
||||||
Bash -> containsPipefail root
|
Bash -> isOptionSet "pipefail" root
|
||||||
Dash -> True
|
Dash -> True
|
||||||
Sh -> True
|
Sh -> True
|
||||||
Ksh -> containsPipefail root,
|
Ksh -> isOptionSet "pipefail" root,
|
||||||
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
shellTypeSpecified = isJust (asShellType spec) || isJust (asFallbackShell spec),
|
||||||
|
isPortage = asIsPortage spec,
|
||||||
|
idMap = getTokenMap root,
|
||||||
parentMap = getParentTree root,
|
parentMap = getParentTree root,
|
||||||
variableFlow = getVariableFlow params root,
|
variableFlow = getVariableFlow params root,
|
||||||
tokenPositions = asTokenPositions spec
|
tokenPositions = asTokenPositions spec,
|
||||||
} in params
|
cfgAnalysis = CF.analyzeControlFlow cfParams root,
|
||||||
where root = asScript spec
|
additionalKnownVariables = extraVars
|
||||||
|
}
|
||||||
|
cfParams = CF.CFGParameters {
|
||||||
|
CF.cfLastpipe = hasLastpipe params,
|
||||||
|
CF.cfPipefail = hasPipefail params,
|
||||||
|
CF.cfAdditionalInitialVariables = additionalKnownVariables params
|
||||||
|
}
|
||||||
|
root = asScript spec
|
||||||
|
|
||||||
|
|
||||||
-- Does this script mention 'set -e' anywhere?
|
-- Does this script mention 'set -e' anywhere?
|
||||||
@@ -234,13 +270,14 @@ containsSetE root = isNothing $ doAnalysis (guard . not . isSetE) root
|
|||||||
_ -> False
|
_ -> False
|
||||||
re = mkRegex "[[:space:]]-[^-]*e"
|
re = mkRegex "[[:space:]]-[^-]*e"
|
||||||
|
|
||||||
containsPipefail root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
|
||||||
|
containsSetOption opt root = isNothing $ doAnalysis (guard . not . isPipefail) root
|
||||||
where
|
where
|
||||||
isPipefail t =
|
isPipefail t =
|
||||||
case t of
|
case t of
|
||||||
T_SimpleCommand {} ->
|
T_SimpleCommand {} ->
|
||||||
t `isUnqualifiedCommand` "set" &&
|
t `isUnqualifiedCommand` "set" &&
|
||||||
("pipefail" `elem` oversimplify t ||
|
(opt `elem` oversimplify t ||
|
||||||
"o" `elem` map snd (getAllFlags t))
|
"o" `elem` map snd (getAllFlags t))
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
@@ -254,12 +291,8 @@ containsShopt shopt root =
|
|||||||
(shopt `elem` oversimplify t)
|
(shopt `elem` oversimplify t)
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
-- Does this script mention 'shopt -s inherit_errexit' anywhere?
|
-- Does this script mention 'shopt -s $opt' or 'set -o $opt' anywhere?
|
||||||
containsInheritErrexit = containsShopt "inherit_errexit"
|
isOptionSet opt root = containsShopt opt root || containsSetOption opt root
|
||||||
|
|
||||||
-- Does this script mention 'shopt -s lastpipe' anywhere?
|
|
||||||
-- Also used as a hack.
|
|
||||||
containsLastpipe = containsShopt "lastpipe"
|
|
||||||
|
|
||||||
|
|
||||||
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
prop_determineShell0 = determineShellTest "#!/bin/sh" == Sh
|
||||||
@@ -351,7 +384,7 @@ isQuoteFreeNode strict shell tree t =
|
|||||||
T_SelectIn {} -> return (not strict)
|
T_SelectIn {} -> return (not strict)
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
-- Check whether this assigment is self-quoting due to being a recognized
|
-- Check whether this assignment is self-quoting due to being a recognized
|
||||||
-- assignment passed to a Declaration Utility. This will soon be required
|
-- assignment passed to a Declaration Utility. This will soon be required
|
||||||
-- by POSIX: https://austingroupbugs.net/view.php?id=351
|
-- by POSIX: https://austingroupbugs.net/view.php?id=351
|
||||||
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
|
assignmentIsQuoting t = shellParsesParamsAsAssignments || not (isAssignmentParamToCommand t)
|
||||||
@@ -408,12 +441,6 @@ usedAsCommandName tree token = go (getId token) (tail $ getPath tree token)
|
|||||||
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
getId word == currentId || getId (getCommandTokenOrThis t) == currentId
|
||||||
go _ _ = False
|
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
|
-- Version of the above taking the map from the current context
|
||||||
-- Todo: give this the name "getPath"
|
-- Todo: give this the name "getPath"
|
||||||
getPathM t = do
|
getPathM t = do
|
||||||
@@ -511,13 +538,11 @@ getModifiedVariables t =
|
|||||||
T_SimpleCommand {} ->
|
T_SimpleCommand {} ->
|
||||||
getModifiedVariableCommand t
|
getModifiedVariableCommand t
|
||||||
|
|
||||||
TA_Unary _ "++|" v@(TA_Variable _ name _) ->
|
TA_Unary _ op v@(TA_Variable _ name _) | "--" `isInfixOf` op || "++" `isInfixOf` op ->
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
[(t, v, name, DataString SourceInteger)]
|
||||||
TA_Unary _ "|++" v@(TA_Variable _ name _) ->
|
|
||||||
[(t, v, name, DataString $ SourceFrom [v])]
|
|
||||||
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
TA_Assignment _ op (TA_Variable _ name _) rhs -> do
|
||||||
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
guard $ op `elem` ["=", "*=", "/=", "%=", "+=", "-=", "<<=", ">>=", "&=", "^=", "|="]
|
||||||
return (t, t, name, DataString $ SourceFrom [rhs])
|
return (t, t, name, DataString SourceInteger)
|
||||||
|
|
||||||
T_BatsTest {} -> [
|
T_BatsTest {} -> [
|
||||||
(t, t, "lines", DataArray SourceExternal),
|
(t, t, "lines", DataArray SourceExternal),
|
||||||
@@ -561,12 +586,6 @@ getModifiedVariables t =
|
|||||||
return (place, t, str, DataString SourceChecked)
|
return (place, t, str, DataString SourceChecked)
|
||||||
_ -> Nothing
|
_ -> 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
|
-- Consider 'export/declare -x' a reference, since it makes the var available
|
||||||
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Literal _ x:_):rest)) =
|
||||||
@@ -585,6 +604,17 @@ getReferencedVariableCommand base@(T_SimpleCommand _ _ (T_NormalWord _ (T_Litera
|
|||||||
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
|
head:_ -> map (\x -> (base, head, x)) $ getVariablesFromLiteralToken head
|
||||||
_ -> []
|
_ -> []
|
||||||
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
"alias" -> [(base, token, name) | token <- rest, name <- getVariablesFromLiteralToken token]
|
||||||
|
|
||||||
|
-- tc-export makes a list of toolchain variables available, similar to export.
|
||||||
|
-- Usage tc-export CC CXX
|
||||||
|
"tc-export" -> concatMap getReference rest
|
||||||
|
|
||||||
|
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
|
||||||
|
-- Usage tc-export_build_env BUILD_CC
|
||||||
|
"tc-export_build_env" ->
|
||||||
|
concatMap getReference rest
|
||||||
|
++ [ (base, base, v) | v <- portageBuildEnvVariables ]
|
||||||
|
|
||||||
_ -> []
|
_ -> []
|
||||||
where
|
where
|
||||||
forDeclare =
|
forDeclare =
|
||||||
@@ -656,6 +686,16 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||||||
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
|
"DEFINE_integer" -> maybeToList $ getFlagVariable rest
|
||||||
"DEFINE_string" -> maybeToList $ getFlagVariable rest
|
"DEFINE_string" -> maybeToList $ getFlagVariable rest
|
||||||
|
|
||||||
|
"tc-export" -> concatMap getModifierParamString rest
|
||||||
|
|
||||||
|
-- tc-export_build_env exports the listed variables plus a bunch of BUILD_XX variables.
|
||||||
|
-- Usage tc-export_build_env BUILD_CC
|
||||||
|
"tc-export_build_env" ->
|
||||||
|
concatMap getModifierParamString rest
|
||||||
|
++ [ (base, base, var, DataString $ SourceExternal) |
|
||||||
|
var <- ["BUILD_" ++ x, x ++ "_FOR_BUILD" ],
|
||||||
|
x <- portageBuildEnvVariables ]
|
||||||
|
|
||||||
_ -> []
|
_ -> []
|
||||||
where
|
where
|
||||||
flags = map snd $ getAllFlags base
|
flags = map snd $ getAllFlags base
|
||||||
@@ -748,13 +788,6 @@ getModifiedVariableCommand base@(T_SimpleCommand id cmdPrefix (T_NormalWord _ (T
|
|||||||
|
|
||||||
getModifiedVariableCommand _ = []
|
getModifiedVariableCommand _ = []
|
||||||
|
|
||||||
getIndexReferences s = fromMaybe [] $ do
|
|
||||||
match <- matchRegex re s
|
|
||||||
index <- match !!! 0
|
|
||||||
return $ matchAllStrings variableNameRegex index
|
|
||||||
where
|
|
||||||
re = mkRegex "(\\[.*\\])"
|
|
||||||
|
|
||||||
-- Given a NormalWord like foo or foo[$bar], get foo.
|
-- Given a NormalWord like foo or foo[$bar], get foo.
|
||||||
-- Primarily used to get references for [[ -v foo[bar] ]]
|
-- Primarily used to get references for [[ -v foo[bar] ]]
|
||||||
getVariableForTestDashV :: Token -> Maybe String
|
getVariableForTestDashV :: Token -> Maybe String
|
||||||
@@ -769,18 +802,6 @@ getVariableForTestDashV t = do
|
|||||||
-- in a non-constant expression (while filtering out foo$x[$y])
|
-- in a non-constant expression (while filtering out foo$x[$y])
|
||||||
toStr _ = return "\0"
|
toStr _ = return "\0"
|
||||||
|
|
||||||
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 =
|
getReferencedVariables parents t =
|
||||||
case t of
|
case t of
|
||||||
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
|
T_DollarBraced id _ l -> let str = concat $ oversimplify l in
|
||||||
@@ -859,17 +880,6 @@ isConfusedGlobRegex ('*':_) = True
|
|||||||
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
isConfusedGlobRegex [x,'*'] | x `notElem` "\\." = True
|
||||||
isConfusedGlobRegex _ = False
|
isConfusedGlobRegex _ = False
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
getVariablesFromLiteralToken token =
|
getVariablesFromLiteralToken token =
|
||||||
getVariablesFromLiteral (getLiteralStringDef " " token)
|
getVariablesFromLiteral (getLiteralStringDef " " token)
|
||||||
|
|
||||||
@@ -882,73 +892,6 @@ getVariablesFromLiteral string =
|
|||||||
where
|
where
|
||||||
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
variableRegex = mkRegex "\\$\\{?([A-Za-z0-9_]+)"
|
||||||
|
|
||||||
-- Get the variable name from an expansion like ${var:-foo}
|
|
||||||
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
|
|
||||||
|
|
||||||
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]
|
|
||||||
|
|
||||||
-- Useful generic functions.
|
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
-- Run a command if the shell is in the given list
|
-- Run a command if the shell is in the given list
|
||||||
whenShell l c = do
|
whenShell l c = do
|
||||||
@@ -1001,17 +944,6 @@ isBashLike params =
|
|||||||
Dash -> False
|
Dash -> False
|
||||||
Sh -> False
|
Sh -> False
|
||||||
|
|
||||||
-- 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
|
|
||||||
|
|
||||||
isTrueAssignmentSource c =
|
isTrueAssignmentSource c =
|
||||||
case c of
|
case c of
|
||||||
DataString SourceChecked -> False
|
DataString SourceChecked -> False
|
||||||
@@ -1029,6 +961,13 @@ modifiesVariable params token name =
|
|||||||
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
|
Assignment (_, _, n, source) -> isTrueAssignmentSource source && n == name
|
||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
|
-- Ebuild files inherit eclasses using 'inherit myclass1 myclass2'
|
||||||
|
getInheritedEclasses :: Token -> [String]
|
||||||
|
getInheritedEclasses root = execWriter $ doAnalysis findInheritedEclasses root
|
||||||
|
where
|
||||||
|
findInheritedEclasses cmd
|
||||||
|
| cmd `isCommand` "inherit" = tell $ catMaybes $ getLiteralString <$> (arguments cmd)
|
||||||
|
findInheritedEclasses _ = return ()
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
1312
src/ShellCheck/CFG.hs
Normal file
1312
src/ShellCheck/CFG.hs
Normal file
File diff suppressed because it is too large
Load Diff
1441
src/ShellCheck/CFGAnalysis.hs
Normal file
1441
src/ShellCheck/CFGAnalysis.hs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
|||||||
{-
|
{-
|
||||||
Copyright 2012-2020 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
@@ -20,10 +20,12 @@
|
|||||||
{-# LANGUAGE TemplateHaskell #-}
|
{-# LANGUAGE TemplateHaskell #-}
|
||||||
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
module ShellCheck.Checker (checkScript, ShellCheck.Checker.runTests) where
|
||||||
|
|
||||||
|
import ShellCheck.Analyzer
|
||||||
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
import ShellCheck.Analyzer
|
|
||||||
|
|
||||||
|
import Data.Char
|
||||||
import Data.Either
|
import Data.Either
|
||||||
import Data.Functor
|
import Data.Functor
|
||||||
import Data.List
|
import Data.List
|
||||||
@@ -53,6 +55,8 @@ shellFromFilename filename = listToMaybe candidates
|
|||||||
shellExtensions = [(".ksh", Ksh)
|
shellExtensions = [(".ksh", Ksh)
|
||||||
,(".bash", Bash)
|
,(".bash", Bash)
|
||||||
,(".bats", Bash)
|
,(".bats", Bash)
|
||||||
|
,(".ebuild", Bash)
|
||||||
|
,(".eclass", Bash)
|
||||||
,(".dash", Dash)]
|
,(".dash", Dash)]
|
||||||
-- The `.sh` is too generic to determine the shell:
|
-- The `.sh` is too generic to determine the shell:
|
||||||
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
|
-- We fallback to Bash in this case and emit SC2148 if there is no shebang
|
||||||
@@ -83,18 +87,24 @@ checkScript sys spec = do
|
|||||||
asShellType = csShellTypeOverride spec,
|
asShellType = csShellTypeOverride spec,
|
||||||
asFallbackShell = shellFromFilename $ csFilename spec,
|
asFallbackShell = shellFromFilename $ csFilename spec,
|
||||||
asCheckSourced = csCheckSourced spec,
|
asCheckSourced = csCheckSourced spec,
|
||||||
|
asIsPortage = isPortage $ csFilename spec,
|
||||||
asExecutionMode = Executed,
|
asExecutionMode = Executed,
|
||||||
asTokenPositions = tokenPositions,
|
asTokenPositions = tokenPositions,
|
||||||
asOptionalChecks = csOptionalChecks spec
|
asOptionalChecks = getEnableDirectives root ++ csOptionalChecks spec
|
||||||
} where as = newAnalysisSpec root
|
} where as = newAnalysisSpec root
|
||||||
let analysisMessages =
|
let getAnalysisMessages =
|
||||||
maybe []
|
case prRoot result of
|
||||||
(arComments . analyzeScript . analysisSpec)
|
Just root -> arComments <$> (analyzeScript sys $ analysisSpec root)
|
||||||
$ prRoot result
|
Nothing -> return []
|
||||||
let translator = tokenToPosition tokenPositions
|
let translator = tokenToPosition tokenPositions
|
||||||
|
analysisMessages <- getAnalysisMessages
|
||||||
return . nub . sortMessages . filter shouldInclude $
|
return . nub . sortMessages . filter shouldInclude $
|
||||||
(parseMessages ++ map translator analysisMessages)
|
(parseMessages ++ map translator analysisMessages)
|
||||||
|
|
||||||
|
isPortage filename =
|
||||||
|
let f = map toLower filename in
|
||||||
|
".ebuild" `isSuffixOf` f || ".eclass" `isSuffixOf` f
|
||||||
|
|
||||||
shouldInclude pc =
|
shouldInclude pc =
|
||||||
severity <= csMinSeverity spec &&
|
severity <= csMinSeverity spec &&
|
||||||
case csIncludedWarnings spec of
|
case csIncludedWarnings spec of
|
||||||
@@ -243,6 +253,9 @@ prop_canStripPrefixAndSource2 =
|
|||||||
prop_canSourceDynamicWhenRedirected =
|
prop_canSourceDynamicWhenRedirected =
|
||||||
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
null $ checkWithIncludes [("lib", "")] "#shellcheck source=lib\n. \"$1\""
|
||||||
|
|
||||||
|
prop_canRedirectWithSpaces =
|
||||||
|
null $ checkWithIncludes [("my file", "")] "#shellcheck source=\"my file\"\n. \"$1\""
|
||||||
|
|
||||||
prop_recursiveAnalysis =
|
prop_recursiveAnalysis =
|
||||||
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
[2086] == checkRecursive [("lib", "echo $1")] "source lib"
|
||||||
|
|
||||||
@@ -412,6 +425,15 @@ prop_sourcePathAddsAnnotation = result == [2086]
|
|||||||
csCheckSourced = True
|
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]
|
prop_sourcePathRedirectsDirective = result == [2086]
|
||||||
where
|
where
|
||||||
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
f "dir/myscript" _ _ "lib" = return "foo/lib"
|
||||||
@@ -483,6 +505,26 @@ prop_fileCannotEnableExternalSources2 = result == [1144]
|
|||||||
csCheckSourced = True
|
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"
|
||||||
|
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $quickCheckAll
|
runTests = $quickCheckAll
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{-
|
{-
|
||||||
Copyright 2012-2021 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
@@ -27,21 +27,28 @@ module ShellCheck.Checks.Commands (checker, optionalChecks, ShellCheck.Checks.Co
|
|||||||
import ShellCheck.AST
|
import ShellCheck.AST
|
||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.AnalyzerLib
|
import ShellCheck.AnalyzerLib
|
||||||
|
import ShellCheck.CFG
|
||||||
|
import qualified ShellCheck.CFGAnalysis as CF
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Parser
|
import ShellCheck.Parser
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Control.Monad.RWS
|
import Control.Monad.RWS
|
||||||
import Data.Char
|
import Data.Char
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
|
import qualified Data.Graph.Inductive.Graph as G
|
||||||
import Data.List
|
import Data.List
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import qualified Data.Map.Strict as Map
|
import qualified Data.Map.Strict as M
|
||||||
|
import qualified Data.Set as S
|
||||||
import Test.QuickCheck.All (forAllProperties)
|
import Test.QuickCheck.All (forAllProperties)
|
||||||
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
import Test.QuickCheck.Test (quickCheckWithResult, stdArgs, maxSuccess)
|
||||||
|
|
||||||
|
import Debug.Trace -- STRIP
|
||||||
|
|
||||||
data CommandName = Exactly String | Basename String
|
data CommandName = Exactly String | Basename String
|
||||||
deriving (Eq, Ord)
|
deriving (Eq, Ord)
|
||||||
|
|
||||||
@@ -98,8 +105,10 @@ commandChecks = [
|
|||||||
,checkUnquotedEchoSpaces
|
,checkUnquotedEchoSpaces
|
||||||
,checkEvalArray
|
,checkEvalArray
|
||||||
]
|
]
|
||||||
++ map checkArgComparison declaringCommands
|
++ map checkArgComparison ("alias" : declaringCommands)
|
||||||
++ map checkMaskedReturns declaringCommands
|
++ map checkMaskedReturns declaringCommands
|
||||||
|
++ map checkMultipleDeclaring declaringCommands
|
||||||
|
++ map checkBackreferencingDeclaration declaringCommands
|
||||||
|
|
||||||
|
|
||||||
optionalChecks = map fst optionalCommandChecks
|
optionalChecks = map fst optionalCommandChecks
|
||||||
@@ -112,7 +121,7 @@ optionalCommandChecks = [
|
|||||||
cdNegative = "command -v javac"
|
cdNegative = "command -v javac"
|
||||||
}, checkWhich)
|
}, checkWhich)
|
||||||
]
|
]
|
||||||
optionalCheckMap = Map.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
optionalCheckMap = M.fromList $ map (\(desc, check) -> (cdName desc, check)) optionalCommandChecks
|
||||||
|
|
||||||
prop_verifyOptionalExamples = all check optionalCommandChecks
|
prop_verifyOptionalExamples = all check optionalCommandChecks
|
||||||
where
|
where
|
||||||
@@ -161,27 +170,27 @@ prop_checkGenericOptsT1 = checkGetOpts "-x -- -y" ["x"] ["-y"] $ return . getGen
|
|||||||
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
|
prop_checkGenericOptsT2 = checkGetOpts "-xy --" ["x", "y"] [] $ return . getGenericOpts
|
||||||
|
|
||||||
|
|
||||||
buildCommandMap :: [CommandCheck] -> Map.Map CommandName (Token -> Analysis)
|
buildCommandMap :: [CommandCheck] -> M.Map CommandName (Token -> Analysis)
|
||||||
buildCommandMap = foldl' addCheck Map.empty
|
buildCommandMap = foldl' addCheck M.empty
|
||||||
where
|
where
|
||||||
addCheck map (CommandCheck name function) =
|
addCheck map (CommandCheck name function) =
|
||||||
Map.insertWith composeAnalyzers name function map
|
M.insertWith composeAnalyzers name function map
|
||||||
|
|
||||||
|
|
||||||
checkCommand :: Map.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
checkCommand :: M.Map CommandName (Token -> Analysis) -> Token -> Analysis
|
||||||
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
checkCommand map t@(T_SimpleCommand id cmdPrefix (cmd:rest)) = sequence_ $ do
|
||||||
name <- getLiteralString cmd
|
name <- getLiteralString cmd
|
||||||
return $
|
return $
|
||||||
if '/' `elem` name
|
if '/' `elem` name
|
||||||
then
|
then
|
||||||
Map.findWithDefault nullCheck (Basename $ basename name) map t
|
M.findWithDefault nullCheck (Basename $ basename name) map t
|
||||||
else if name == "builtin" && not (null rest) then
|
else if name == "builtin" && not (null rest) then
|
||||||
let t' = T_SimpleCommand id cmdPrefix rest
|
let t' = T_SimpleCommand id cmdPrefix rest
|
||||||
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
selectedBuiltin = fromMaybe "" $ getLiteralString . head $ rest
|
||||||
in Map.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
in M.findWithDefault nullCheck (Exactly selectedBuiltin) map t'
|
||||||
else do
|
else do
|
||||||
Map.findWithDefault nullCheck (Exactly name) map t
|
M.findWithDefault nullCheck (Exactly name) map t
|
||||||
Map.findWithDefault nullCheck (Basename name) map t
|
M.findWithDefault nullCheck (Basename name) map t
|
||||||
|
|
||||||
where
|
where
|
||||||
basename = reverse . takeWhile (/= '/') . reverse
|
basename = reverse . takeWhile (/= '/') . reverse
|
||||||
@@ -203,22 +212,22 @@ checker spec params = getChecker $ commandChecks ++ optionals
|
|||||||
optionals =
|
optionals =
|
||||||
if "all" `elem` keys
|
if "all" `elem` keys
|
||||||
then map snd optionalCommandChecks
|
then map snd optionalCommandChecks
|
||||||
else mapMaybe (\x -> Map.lookup x optionalCheckMap) keys
|
else mapMaybe (\x -> M.lookup x optionalCheckMap) keys
|
||||||
|
|
||||||
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
prop_checkTr1 = verify checkTr "tr [a-f] [A-F]"
|
||||||
prop_checkTr2 = verify checkTr "tr 'a-z' 'A-Z'"
|
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_checkTr3 = verifyNot checkTr "tr -d '[:lower:]'"
|
||||||
prop_checkTr3a= verifyNot checkTr "tr -d '[:upper:]'"
|
prop_checkTr3a = verifyNot checkTr "tr -d '[:upper:]'"
|
||||||
prop_checkTr3b= verifyNot checkTr "tr -d '|/_[:upper:]'"
|
prop_checkTr3b = verifyNot checkTr "tr -d '|/_[:upper:]'"
|
||||||
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
prop_checkTr4 = verifyNot checkTr "ls [a-z]"
|
||||||
prop_checkTr5 = verify checkTr "tr foo bar"
|
prop_checkTr5 = verify checkTr "tr foo bar"
|
||||||
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
prop_checkTr6 = verify checkTr "tr 'hello' 'world'"
|
||||||
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
prop_checkTr8 = verifyNot checkTr "tr aeiou _____"
|
||||||
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
prop_checkTr9 = verifyNot checkTr "a-z n-za-m"
|
||||||
prop_checkTr10= verifyNot checkTr "tr --squeeze-repeats rl lr"
|
prop_checkTr10 = verifyNot checkTr "tr --squeeze-repeats rl lr"
|
||||||
prop_checkTr11= verifyNot checkTr "tr abc '[d*]'"
|
prop_checkTr11 = verifyNot checkTr "tr abc '[d*]'"
|
||||||
prop_checkTr12= verifyNot checkTr "tr '[=e=]' 'e'"
|
prop_checkTr12 = verifyNot checkTr "tr '[=e=]' 'e'"
|
||||||
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
checkTr = CommandCheck (Basename "tr") (mapM_ f . arguments)
|
||||||
where
|
where
|
||||||
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
f w | isGlob w = -- The user will go [ab] -> '[ab]' -> 'ab'. Fixme?
|
||||||
@@ -330,20 +339,20 @@ prop_checkGrepRe6 = verifyNot checkGrepRe "grep foo \\*.mp3"
|
|||||||
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
prop_checkGrepRe7 = verify checkGrepRe "grep *foo* file"
|
||||||
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
prop_checkGrepRe8 = verify checkGrepRe "ls | grep foo*.jpg"
|
||||||
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
prop_checkGrepRe9 = verifyNot checkGrepRe "grep '[0-9]*' file"
|
||||||
prop_checkGrepRe10= verifyNot checkGrepRe "grep '^aa*' file"
|
prop_checkGrepRe10 = verifyNot checkGrepRe "grep '^aa*' file"
|
||||||
prop_checkGrepRe11= verifyNot checkGrepRe "grep --include=*.png foo"
|
prop_checkGrepRe11 = verifyNot checkGrepRe "grep --include=*.png foo"
|
||||||
prop_checkGrepRe12= verifyNot checkGrepRe "grep -F 'Foo*' file"
|
prop_checkGrepRe12 = verifyNot checkGrepRe "grep -F 'Foo*' file"
|
||||||
prop_checkGrepRe13= verifyNot checkGrepRe "grep -- -foo bar*"
|
prop_checkGrepRe13 = verifyNot checkGrepRe "grep -- -foo bar*"
|
||||||
prop_checkGrepRe14= verifyNot checkGrepRe "grep -e -foo bar*"
|
prop_checkGrepRe14 = verifyNot checkGrepRe "grep -e -foo bar*"
|
||||||
prop_checkGrepRe15= verifyNot checkGrepRe "grep --regex -foo bar*"
|
prop_checkGrepRe15 = verifyNot checkGrepRe "grep --regex -foo bar*"
|
||||||
prop_checkGrepRe16= verifyNot checkGrepRe "grep --include 'Foo*' file"
|
prop_checkGrepRe16 = verifyNot checkGrepRe "grep --include 'Foo*' file"
|
||||||
prop_checkGrepRe17= verifyNot checkGrepRe "grep --exclude 'Foo*' file"
|
prop_checkGrepRe17 = verifyNot checkGrepRe "grep --exclude 'Foo*' file"
|
||||||
prop_checkGrepRe18= verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
|
prop_checkGrepRe18 = verifyNot checkGrepRe "grep --exclude-dir 'Foo*' file"
|
||||||
prop_checkGrepRe19= verify checkGrepRe "grep -- 'Foo*' file"
|
prop_checkGrepRe19 = verify checkGrepRe "grep -- 'Foo*' file"
|
||||||
prop_checkGrepRe20= verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
|
prop_checkGrepRe20 = verifyNot checkGrepRe "grep --fixed-strings 'Foo*' file"
|
||||||
prop_checkGrepRe21= verifyNot checkGrepRe "grep -o 'x*' file"
|
prop_checkGrepRe21 = verifyNot checkGrepRe "grep -o 'x*' file"
|
||||||
prop_checkGrepRe22= verifyNot checkGrepRe "grep --only-matching 'x*' file"
|
prop_checkGrepRe22 = verifyNot checkGrepRe "grep --only-matching 'x*' file"
|
||||||
prop_checkGrepRe23= verifyNot checkGrepRe "grep '.*' file"
|
prop_checkGrepRe23 = verifyNot checkGrepRe "grep '.*' file"
|
||||||
|
|
||||||
checkGrepRe = CommandCheck (Basename "grep") check where
|
checkGrepRe = CommandCheck (Basename "grep") check where
|
||||||
check cmd = f cmd (arguments cmd)
|
check cmd = f cmd (arguments cmd)
|
||||||
@@ -391,7 +400,7 @@ checkGrepRe = CommandCheck (Basename "grep") check where
|
|||||||
|
|
||||||
|
|
||||||
prop_checkTrapQuotes1 = verify checkTrapQuotes "trap \"echo $num\" INT"
|
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_checkTrapQuotes2 = verifyNot checkTrapQuotes "trap 'echo $num' INT"
|
||||||
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
prop_checkTrapQuotes3 = verify checkTrapQuotes "trap \"echo $((1+num))\" EXIT DEBUG"
|
||||||
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
checkTrapQuotes = CommandCheck (Exactly "trap") (f . arguments) where
|
||||||
@@ -472,9 +481,16 @@ prop_checkUnusedEchoEscapes2 = verifyNot checkUnusedEchoEscapes "echo -e 'foi\\n
|
|||||||
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
prop_checkUnusedEchoEscapes3 = verify checkUnusedEchoEscapes "echo \"n:\\t42\""
|
||||||
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
prop_checkUnusedEchoEscapes4 = verifyNot checkUnusedEchoEscapes "echo lol"
|
||||||
prop_checkUnusedEchoEscapes5 = verifyNot checkUnusedEchoEscapes "echo -n -e '\n'"
|
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
|
checkUnusedEchoEscapes = CommandCheck (Basename "echo") f
|
||||||
where
|
where
|
||||||
hasEscapes = mkRegex "\\\\[rnt]"
|
hasEscapes = mkRegex "\\\\([rntabefv\\']|[0-7]{1,3}|x([0-9]|[A-F]|[a-f]){1,2})"
|
||||||
f cmd =
|
f cmd =
|
||||||
whenShell [Sh, Bash, Ksh] $
|
whenShell [Sh, Bash, Ksh] $
|
||||||
unless (cmd `hasFlag` "e") $
|
unless (cmd `hasFlag` "e") $
|
||||||
@@ -648,19 +664,19 @@ prop_checkPrintfVar6 = verify checkPrintfVar "printf foo bar baz"
|
|||||||
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
prop_checkPrintfVar7 = verify checkPrintfVar "printf -- foo bar baz"
|
||||||
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
prop_checkPrintfVar8 = verifyNot checkPrintfVar "printf '%s %s %s' \"${var[@]}\""
|
||||||
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
prop_checkPrintfVar9 = verifyNot checkPrintfVar "printf '%s %s %s\\n' *.png"
|
||||||
prop_checkPrintfVar10= verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
prop_checkPrintfVar10 = verifyNot checkPrintfVar "printf '%s %s %s' foo bar baz"
|
||||||
prop_checkPrintfVar11= verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
prop_checkPrintfVar11 = verifyNot checkPrintfVar "printf '%(%s%s)T' -1"
|
||||||
prop_checkPrintfVar12= verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
prop_checkPrintfVar12 = verify checkPrintfVar "printf '%s %s\\n' 1 2 3"
|
||||||
prop_checkPrintfVar13= verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
prop_checkPrintfVar13 = verifyNot checkPrintfVar "printf '%s %s\\n' 1 2 3 4"
|
||||||
prop_checkPrintfVar14= verify checkPrintfVar "printf '%*s\\n' 1"
|
prop_checkPrintfVar14 = verify checkPrintfVar "printf '%*s\\n' 1"
|
||||||
prop_checkPrintfVar15= verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
prop_checkPrintfVar15 = verifyNot checkPrintfVar "printf '%*s\\n' 1 2"
|
||||||
prop_checkPrintfVar16= verifyNot checkPrintfVar "printf $'string'"
|
prop_checkPrintfVar16 = verifyNot checkPrintfVar "printf $'string'"
|
||||||
prop_checkPrintfVar17= verify checkPrintfVar "printf '%-*s\\n' 1"
|
prop_checkPrintfVar17 = verify checkPrintfVar "printf '%-*s\\n' 1"
|
||||||
prop_checkPrintfVar18= verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
prop_checkPrintfVar18 = verifyNot checkPrintfVar "printf '%-*s\\n' 1 2"
|
||||||
prop_checkPrintfVar19= verifyNot checkPrintfVar "printf '%(%s)T'"
|
prop_checkPrintfVar19 = verifyNot checkPrintfVar "printf '%(%s)T'"
|
||||||
prop_checkPrintfVar20= verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
prop_checkPrintfVar20 = verifyNot checkPrintfVar "printf '%d %(%s)T' 42"
|
||||||
prop_checkPrintfVar21= verify checkPrintfVar "printf '%d %(%s)T'"
|
prop_checkPrintfVar21 = verify checkPrintfVar "printf '%d %(%s)T'"
|
||||||
prop_checkPrintfVar22= verify checkPrintfVar "printf '%s\n%s' foo"
|
prop_checkPrintfVar22 = verify checkPrintfVar "printf '%s\n%s' foo"
|
||||||
|
|
||||||
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
||||||
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
f (doubledash:rest) | getLiteralString doubledash == Just "--" = f rest
|
||||||
@@ -674,6 +690,7 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
let formats = getPrintfFormats string
|
let formats = getPrintfFormats string
|
||||||
let formatCount = length formats
|
let formatCount = length formats
|
||||||
let argCount = length more
|
let argCount = length more
|
||||||
|
let pluraliseIfMany word n = if n > 1 then word ++ "s" else word
|
||||||
|
|
||||||
return $ if
|
return $ if
|
||||||
| argCount == 0 && formatCount == 0 ->
|
| argCount == 0 && formatCount == 0 ->
|
||||||
@@ -689,7 +706,8 @@ checkPrintfVar = CommandCheck (Exactly "printf") (f . arguments) where
|
|||||||
return () -- Great: a suitable number of arguments
|
return () -- Great: a suitable number of arguments
|
||||||
| otherwise ->
|
| otherwise ->
|
||||||
warn (getId format) 2183 $
|
warn (getId format) 2183 $
|
||||||
"This format string has " ++ show formatCount ++ " variables, but is passed " ++ show argCount ++ " arguments."
|
"This format string has " ++ show formatCount ++ " " ++ pluraliseIfMany "variable" formatCount ++
|
||||||
|
", but is passed " ++ show argCount ++ pluraliseIfMany " argument" argCount ++ "."
|
||||||
|
|
||||||
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
unless ('%' `elem` concat (oversimplify format) || isLiteral format) $
|
||||||
info (getId format) 2059
|
info (getId format) 2059
|
||||||
@@ -735,7 +753,7 @@ getPrintfFormats = getFormats
|
|||||||
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
-- \____ _____/\___ ____/ \____ ____/\_________ _________/ \______ /
|
||||||
-- V V V V V
|
-- V V V V V
|
||||||
-- flags field width precision format character rest
|
-- flags field width precision format character rest
|
||||||
-- field width and precision can be specified with a '*' instead of a digit,
|
-- field width and precision can be specified with an '*' instead of a digit,
|
||||||
-- in which case printf will accept one more argument for each '*' used
|
-- in which case printf will accept one more argument for each '*' used
|
||||||
|
|
||||||
|
|
||||||
@@ -941,6 +959,22 @@ checkLocalScope = CommandCheck (Exactly "local") $ \t ->
|
|||||||
unless (any isFunctionLike path) $
|
unless (any isFunctionLike path) $
|
||||||
err (getId $ getCommandTokenOrThis t) 2168 "'local' is only valid in functions."
|
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_checkDeprecatedTempfile1 = verify checkDeprecatedTempfile "var=$(tempfile)"
|
||||||
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
prop_checkDeprecatedTempfile2 = verifyNot checkDeprecatedTempfile "tempfile=$(mktemp)"
|
||||||
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
checkDeprecatedTempfile = CommandCheck (Basename "tempfile") $
|
||||||
@@ -987,20 +1021,20 @@ checkWhileGetoptsCase = CommandCheck (Exactly "getopts") f
|
|||||||
|
|
||||||
check :: Id -> [String] -> Token -> Analysis
|
check :: Id -> [String] -> Token -> Analysis
|
||||||
check optId opts (T_CaseExpression id _ list) = do
|
check optId opts (T_CaseExpression id _ list) = do
|
||||||
unless (Nothing `Map.member` handledMap) $ do
|
unless (Nothing `M.member` handledMap) $ do
|
||||||
mapM_ (warnUnhandled optId id) $ catMaybes $ Map.keys notHandled
|
mapM_ (warnUnhandled optId id) $ catMaybes $ M.keys notHandled
|
||||||
|
|
||||||
unless (any (`Map.member` handledMap) [Just "*",Just "?"]) $
|
unless (any (`M.member` handledMap) [Just "*",Just "?"]) $
|
||||||
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
warn id 2220 "Invalid flags are not handled. Add a *) case."
|
||||||
|
|
||||||
mapM_ warnRedundant $ Map.toList notRequested
|
mapM_ warnRedundant $ M.toList notRequested
|
||||||
|
|
||||||
where
|
where
|
||||||
handledMap = Map.fromList (concatMap getHandledStrings list)
|
handledMap = M.fromList (concatMap getHandledStrings list)
|
||||||
requestedMap = Map.fromList $ map (\x -> (Just x, ())) opts
|
requestedMap = M.fromList $ map (\x -> (Just x, ())) opts
|
||||||
|
|
||||||
notHandled = Map.difference requestedMap handledMap
|
notHandled = M.difference requestedMap handledMap
|
||||||
notRequested = Map.difference handledMap requestedMap
|
notRequested = M.difference handledMap requestedMap
|
||||||
|
|
||||||
warnUnhandled optId caseId str =
|
warnUnhandled optId caseId str =
|
||||||
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
warn caseId 2213 $ "getopts specified -" ++ (e4m str) ++ ", but it's not handled by this 'case'."
|
||||||
@@ -1044,10 +1078,10 @@ prop_checkCatastrophicRm4 = verify checkCatastrophicRm "rm -fr /home/$(whoami)/*
|
|||||||
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
prop_checkCatastrophicRm5 = verifyNot checkCatastrophicRm "rm -r /home/${USER:-thing}/*"
|
||||||
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
prop_checkCatastrophicRm6 = verify checkCatastrophicRm "rm --recursive /etc/*$config*"
|
||||||
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
prop_checkCatastrophicRm8 = verify checkCatastrophicRm "rm -rf /home"
|
||||||
prop_checkCatastrophicRm10= verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
prop_checkCatastrophicRm10 = verifyNot checkCatastrophicRm "rm -r \"${DIR}\"/{.gitignore,.gitattributes,ci}"
|
||||||
prop_checkCatastrophicRm11= verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
prop_checkCatastrophicRm11 = verify checkCatastrophicRm "rm -r /{bin,sbin}/$exec"
|
||||||
prop_checkCatastrophicRm12= verify checkCatastrophicRm "rm -r /{{usr,},{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_checkCatastrophicRm13 = verifyNot checkCatastrophicRm "rm -r /{{a,b},{c,d}}/$exec"
|
||||||
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
prop_checkCatastrophicRmA = verify checkCatastrophicRm "rm -rf /usr /lib/nvidia-current/xorg/xorg"
|
||||||
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
prop_checkCatastrophicRmB = verify checkCatastrophicRm "rm -rf \"$STEAMROOT/\"*"
|
||||||
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
checkCatastrophicRm = CommandCheck (Basename "rm") $ \t ->
|
||||||
@@ -1253,6 +1287,7 @@ prop_checkArgComparison3 = verifyNot (checkArgComparison "declare") "declare a=b
|
|||||||
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
|
prop_checkArgComparison4 = verify (checkArgComparison "export") "export a +=b"
|
||||||
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
|
prop_checkArgComparison7 = verifyNot (checkArgComparison "declare") "declare -a +i foo"
|
||||||
prop_checkArgComparison8 = verify (checkArgComparison "let") "let x = 0"
|
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
|
-- This mirrors checkSecondArgIsComparison but for arguments to local/readonly/declare/export
|
||||||
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
|
checkArgComparison cmd = CommandCheck (Exactly cmd) wordsWithEqual
|
||||||
where
|
where
|
||||||
@@ -1353,10 +1388,10 @@ checkUnquotedEchoSpaces = CommandCheck (Basename "echo") check
|
|||||||
m <- asks tokenPositions
|
m <- asks tokenPositions
|
||||||
redir <- getClosestCommandM t
|
redir <- getClosestCommandM t
|
||||||
sequence_ $ do
|
sequence_ $ do
|
||||||
let positions = mapMaybe (\c -> Map.lookup (getId c) m) args
|
let positions = mapMaybe (\c -> M.lookup (getId c) m) args
|
||||||
let pairs = zip positions (drop 1 positions)
|
let pairs = zip positions (drop 1 positions)
|
||||||
(T_Redirecting _ redirTokens _) <- redir
|
(T_Redirecting _ redirTokens _) <- redir
|
||||||
let redirPositions = mapMaybe (\c -> fst <$> Map.lookup (getId c) m) redirTokens
|
let redirPositions = mapMaybe (\c -> fst <$> M.lookup (getId c) m) redirTokens
|
||||||
guard $ any (hasSpacesBetween redirPositions) pairs
|
guard $ any (hasSpacesBetween redirPositions) pairs
|
||||||
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
|
return $ info (getId t) 2291 "Quote repeated spaces to avoid them collapsing into one."
|
||||||
|
|
||||||
@@ -1386,5 +1421,51 @@ checkEvalArray = CommandCheck (Exactly "eval") (mapM_ check . concatMap getWordP
|
|||||||
_ -> False
|
_ -> 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 = foldM_ perArg M.empty $ arguments t
|
||||||
|
|
||||||
|
perArg leftArgs t =
|
||||||
|
case t of
|
||||||
|
T_Assignment id _ name idx t -> do
|
||||||
|
warnIfBackreferencing leftArgs $ t:idx
|
||||||
|
return $ M.insert name id leftArgs
|
||||||
|
t -> do
|
||||||
|
warnIfBackreferencing leftArgs [t]
|
||||||
|
return leftArgs
|
||||||
|
|
||||||
|
warnIfBackreferencing backrefs l = do
|
||||||
|
references <- findReferences 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 list = do
|
||||||
|
cfga <- asks cfgAnalysis
|
||||||
|
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 []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
101
src/ShellCheck/Checks/ControlFlow.hs
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
{-
|
||||||
|
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
|
||||||
|
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 }) ) |])
|
@@ -25,6 +25,7 @@ import ShellCheck.AST
|
|||||||
import ShellCheck.ASTLib
|
import ShellCheck.ASTLib
|
||||||
import ShellCheck.AnalyzerLib
|
import ShellCheck.AnalyzerLib
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Prelude
|
||||||
import ShellCheck.Regex
|
import ShellCheck.Regex
|
||||||
|
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
@@ -59,6 +60,8 @@ checks = [
|
|||||||
,checkBraceExpansionVars
|
,checkBraceExpansionVars
|
||||||
,checkMultiDimensionalArrays
|
,checkMultiDimensionalArrays
|
||||||
,checkPS1Assignments
|
,checkPS1Assignments
|
||||||
|
,checkMultipleBangs
|
||||||
|
,checkBangAfterPipe
|
||||||
]
|
]
|
||||||
|
|
||||||
testChecker (ForShell _ t) =
|
testChecker (ForShell _ t) =
|
||||||
@@ -91,55 +94,57 @@ prop_checkBashisms6 = verify checkBashisms "[ \"$a\" == 42 ]"
|
|||||||
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
prop_checkBashisms7 = verify checkBashisms "echo ${var[1]}"
|
||||||
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
prop_checkBashisms8 = verify checkBashisms "echo ${!var[@]}"
|
||||||
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
prop_checkBashisms9 = verify checkBashisms "echo ${!var*}"
|
||||||
prop_checkBashisms10= verify checkBashisms "echo ${var:4:12}"
|
prop_checkBashisms10 = verify checkBashisms "echo ${var:4:12}"
|
||||||
prop_checkBashisms11= verifyNot checkBashisms "echo ${var:-4}"
|
prop_checkBashisms11 = verifyNot checkBashisms "echo ${var:-4}"
|
||||||
prop_checkBashisms12= verify checkBashisms "echo ${var//foo/bar}"
|
prop_checkBashisms12 = verify checkBashisms "echo ${var//foo/bar}"
|
||||||
prop_checkBashisms13= verify checkBashisms "exec -c env"
|
prop_checkBashisms13 = verify checkBashisms "exec -c env"
|
||||||
prop_checkBashisms14= verify checkBashisms "echo -n \"Foo: \""
|
prop_checkBashisms14 = verify checkBashisms "echo -n \"Foo: \""
|
||||||
prop_checkBashisms15= verify checkBashisms "let n++"
|
prop_checkBashisms15 = verify checkBashisms "let n++"
|
||||||
prop_checkBashisms16= verify checkBashisms "echo $RANDOM"
|
prop_checkBashisms16 = verify checkBashisms "echo $RANDOM"
|
||||||
prop_checkBashisms17= verify checkBashisms "echo $((RANDOM%6+1))"
|
prop_checkBashisms17 = verify checkBashisms "echo $((RANDOM%6+1))"
|
||||||
prop_checkBashisms18= verify checkBashisms "foo &> /dev/null"
|
prop_checkBashisms18 = verify checkBashisms "foo &> /dev/null"
|
||||||
prop_checkBashisms19= verify checkBashisms "foo > file*.txt"
|
prop_checkBashisms19 = verify checkBashisms "foo > file*.txt"
|
||||||
prop_checkBashisms20= verify checkBashisms "read -ra foo"
|
prop_checkBashisms20 = verify checkBashisms "read -ra foo"
|
||||||
prop_checkBashisms21= verify checkBashisms "[ -a foo ]"
|
prop_checkBashisms21 = verify checkBashisms "[ -a foo ]"
|
||||||
prop_checkBashisms22= verifyNot checkBashisms "[ foo -a bar ]"
|
prop_checkBashisms22 = verifyNot checkBashisms "[ foo -a bar ]"
|
||||||
prop_checkBashisms23= verify checkBashisms "trap mything ERR INT"
|
prop_checkBashisms23 = verify checkBashisms "trap mything ERR INT"
|
||||||
prop_checkBashisms24= verifyNot checkBashisms "trap mything INT TERM"
|
prop_checkBashisms24 = verifyNot checkBashisms "trap mything INT TERM"
|
||||||
prop_checkBashisms25= verify checkBashisms "cat < /dev/tcp/host/123"
|
prop_checkBashisms25 = verify checkBashisms "cat < /dev/tcp/host/123"
|
||||||
prop_checkBashisms26= verify checkBashisms "trap mything ERR SIGTERM"
|
prop_checkBashisms26 = verify checkBashisms "trap mything ERR SIGTERM"
|
||||||
prop_checkBashisms27= verify checkBashisms "echo *[^0-9]*"
|
prop_checkBashisms27 = verify checkBashisms "echo *[^0-9]*"
|
||||||
prop_checkBashisms28= verify checkBashisms "exec {n}>&2"
|
prop_checkBashisms28 = verify checkBashisms "exec {n}>&2"
|
||||||
prop_checkBashisms29= verify checkBashisms "echo ${!var}"
|
prop_checkBashisms29 = verify checkBashisms "echo ${!var}"
|
||||||
prop_checkBashisms30= verify checkBashisms "printf -v '%s' \"$1\""
|
prop_checkBashisms30 = verify checkBashisms "printf -v '%s' \"$1\""
|
||||||
prop_checkBashisms31= verify checkBashisms "printf '%q' \"$1\""
|
prop_checkBashisms31 = verify checkBashisms "printf '%q' \"$1\""
|
||||||
prop_checkBashisms32= verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
prop_checkBashisms32 = verifyNot checkBashisms "#!/bin/dash\n[ foo -nt bar ]"
|
||||||
prop_checkBashisms33= verify checkBashisms "#!/bin/sh\necho -n foo"
|
prop_checkBashisms33 = verify checkBashisms "#!/bin/sh\necho -n foo"
|
||||||
prop_checkBashisms34= verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
prop_checkBashisms34 = verifyNot checkBashisms "#!/bin/dash\necho -n foo"
|
||||||
prop_checkBashisms35= verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
prop_checkBashisms35 = verifyNot checkBashisms "#!/bin/dash\nlocal foo"
|
||||||
prop_checkBashisms36= verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
prop_checkBashisms36 = verifyNot checkBashisms "#!/bin/dash\nread -p foo -r bar"
|
||||||
prop_checkBashisms37= verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
prop_checkBashisms37 = verifyNot checkBashisms "HOSTNAME=foo; echo $HOSTNAME"
|
||||||
prop_checkBashisms38= verify checkBashisms "RANDOM=9; echo $RANDOM"
|
prop_checkBashisms38 = verify checkBashisms "RANDOM=9; echo $RANDOM"
|
||||||
prop_checkBashisms39= verify checkBashisms "foo-bar() { true; }"
|
prop_checkBashisms39 = verify checkBashisms "foo-bar() { true; }"
|
||||||
prop_checkBashisms40= verify checkBashisms "echo $(<file)"
|
prop_checkBashisms40 = verify checkBashisms "echo $(<file)"
|
||||||
prop_checkBashisms41= verify checkBashisms "echo `<file`"
|
prop_checkBashisms41 = verify checkBashisms "echo `<file`"
|
||||||
prop_checkBashisms42= verify checkBashisms "trap foo int"
|
prop_checkBashisms42 = verify checkBashisms "trap foo int"
|
||||||
prop_checkBashisms43= verify checkBashisms "trap foo sigint"
|
prop_checkBashisms43 = verify checkBashisms "trap foo sigint"
|
||||||
prop_checkBashisms44= verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
prop_checkBashisms44 = verifyNot checkBashisms "#!/bin/dash\ntrap foo int"
|
||||||
prop_checkBashisms45= 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_checkBashisms46 = verify checkBashisms "#!/bin/dash\ntrap foo SIGINT"
|
||||||
prop_checkBashisms47= verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
prop_checkBashisms47 = verify checkBashisms "#!/bin/dash\necho foo 42>/dev/null"
|
||||||
prop_checkBashisms48= verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
prop_checkBashisms48 = verifyNot checkBashisms "#!/bin/sh\necho $LINENO"
|
||||||
prop_checkBashisms49= verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
prop_checkBashisms49 = verify checkBashisms "#!/bin/dash\necho $MACHTYPE"
|
||||||
prop_checkBashisms50= verify checkBashisms "#!/bin/sh\ncmd >& file"
|
prop_checkBashisms50 = verify checkBashisms "#!/bin/sh\ncmd >& file"
|
||||||
prop_checkBashisms51= verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
prop_checkBashisms51 = verifyNot checkBashisms "#!/bin/sh\ncmd 2>&1"
|
||||||
prop_checkBashisms52= verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
prop_checkBashisms52 = verifyNot checkBashisms "#!/bin/sh\ncmd >&2"
|
||||||
prop_checkBashisms53= verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
prop_checkBashisms52b = verifyNot checkBashisms "#!/bin/sh\ncmd >& $var"
|
||||||
prop_checkBashisms54= verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
prop_checkBashisms52c = verify checkBashisms "#!/bin/sh\ncmd >& $dir/$var"
|
||||||
prop_checkBashisms55= verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
prop_checkBashisms53 = verifyNot checkBashisms "#!/bin/sh\nprintf -- -f\n"
|
||||||
prop_checkBashisms56= verifyNot checkBashisms "#!/bin/sh\necho ${##}"
|
prop_checkBashisms54 = verify checkBashisms "#!/bin/sh\nfoo+=bar"
|
||||||
prop_checkBashisms57= verifyNot checkBashisms "#!/bin/dash\nulimit -c 0"
|
prop_checkBashisms55 = verify checkBashisms "#!/bin/sh\necho ${@%foo}"
|
||||||
prop_checkBashisms58= verify checkBashisms "#!/bin/sh\nulimit -c 0"
|
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_checkBashisms59 = verify checkBashisms "#!/bin/sh\njobs -s"
|
||||||
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
|
prop_checkBashisms60 = verifyNot checkBashisms "#!/bin/sh\njobs -p"
|
||||||
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
|
prop_checkBashisms61 = verifyNot checkBashisms "#!/bin/sh\njobs -lp"
|
||||||
@@ -181,6 +186,11 @@ prop_checkBashisms96 = verifyNot checkBashisms "#!/bin/dash\necho $_"
|
|||||||
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
prop_checkBashisms97 = verify checkBashisms "#!/bin/sh\necho ${var,}"
|
||||||
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
prop_checkBashisms98 = verify checkBashisms "#!/bin/sh\necho ${var^^}"
|
||||||
prop_checkBashisms99 = verify checkBashisms "#!/bin/dash\necho [^f]oo"
|
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 ''"
|
||||||
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
||||||
params <- ask
|
params <- ask
|
||||||
kludge params t
|
kludge params t
|
||||||
@@ -224,7 +234,8 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
warnMsg id 3018 $ filter (/= '|') op ++ " is"
|
||||||
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
bashism (TA_Binary id "**" _ _) = warnMsg id 3019 "exponentials are"
|
||||||
bashism (T_FdRedirect id "&" (T_IoFile _ (T_Greater _) _)) = warnMsg id 3020 "&> 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 "" (T_IoFile _ (T_GREATAND _) file)) =
|
||||||
|
unless (all isDigit $ onlyLiteralString file) $ warnMsg id 3021 ">& filename (as opposed to >& fd) is"
|
||||||
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
bashism (T_FdRedirect id ('{':_) _) = warnMsg id 3022 "named file descriptors are"
|
||||||
bashism (T_FdRedirect id num _)
|
bashism (T_FdRedirect id num _)
|
||||||
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
| all isDigit num && length num > 1 = warnMsg id 3023 "FDs outside 0-9 are"
|
||||||
@@ -375,6 +386,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
let literal = onlyLiteralString format
|
let literal = onlyLiteralString format
|
||||||
guard $ "%q" `isInfixOf` literal
|
guard $ "%q" `isInfixOf` literal
|
||||||
return $ warnMsg (getId format) 3050 "printf %q is"
|
return $ warnMsg (getId format) 3050 "printf %q is"
|
||||||
|
|
||||||
|
when (name == "read" && all isFlag rest) $
|
||||||
|
warnMsg (getId cmd) 3061 "read without a variable is"
|
||||||
where
|
where
|
||||||
unsupportedCommands = [
|
unsupportedCommands = [
|
||||||
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
"let", "caller", "builtin", "complete", "compgen", "declare", "dirs", "disown",
|
||||||
@@ -440,9 +454,9 @@ checkBashisms = ForShell [Sh, Dash] $ \t -> do
|
|||||||
_ -> False
|
_ -> False
|
||||||
|
|
||||||
prop_checkEchoSed1 = verify checkEchoSed "FOO=$(echo \"$cow\" | sed 's/foo/bar/g')"
|
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_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
|
checkEchoSed = ForShell [Bash, Ksh] f
|
||||||
where
|
where
|
||||||
f (T_Redirecting id lefts r) =
|
f (T_Redirecting id lefts r) =
|
||||||
@@ -528,11 +542,11 @@ checkMultiDimensionalArrays = ForShell [Bash] f
|
|||||||
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
isMultiDim l = getBracedModifier (concat $ oversimplify l) `matches` re
|
||||||
|
|
||||||
prop_checkPS11 = verify checkPS1Assignments "PS1='\\033[1;35m\\$ '"
|
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_checkPSf2 = verify checkPS1Assignments "PS1='\\h \\e[0m\\$ '"
|
||||||
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
prop_checkPS13 = verify checkPS1Assignments "PS1=$'\\x1b[c '"
|
||||||
prop_checkPS14 = verify checkPS1Assignments "PS1=$'\\e[3m; '"
|
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_checkPS15 = verifyNot checkPS1Assignments "PS1='\\[\\033[1;35m\\]\\$ '"
|
||||||
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
prop_checkPS16 = verifyNot checkPS1Assignments "PS1='\\[\\e1m\\e[1m\\]\\$ '"
|
||||||
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
|
prop_checkPS17 = verifyNot checkPS1Assignments "PS1='e033x1B'"
|
||||||
@@ -554,5 +568,29 @@ checkPS1Assignments = ForShell [Bash] f
|
|||||||
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
escapeRegex = mkRegex "\\\\x1[Bb]|\\\\e|\x1B|\\\\033"
|
||||||
|
|
||||||
|
|
||||||
|
prop_checkMultipleBangs1 = verify checkMultipleBangs "! ! true"
|
||||||
|
prop_checkMultipleBangs2 = verifyNot checkMultipleBangs "! true"
|
||||||
|
checkMultipleBangs = ForShell [Dash, 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, 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 ()
|
||||||
|
|
||||||
return []
|
return []
|
||||||
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
runTests = $( [| $(forAllProperties) (quickCheckWithResult (stdArgs { maxSuccess = 1 }) ) |])
|
||||||
|
@@ -2,33 +2,54 @@ module ShellCheck.Data where
|
|||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import Data.Version (showVersion)
|
import Data.Version (showVersion)
|
||||||
import Paths_ShellCheck (version)
|
|
||||||
|
|
||||||
|
|
||||||
|
{-
|
||||||
|
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 -- VERSIONSTRING
|
||||||
|
|
||||||
|
|
||||||
internalVariables = [
|
internalVariables = [
|
||||||
-- Generic
|
-- Generic
|
||||||
"", "_", "rest", "REST",
|
"", "_", "rest", "REST",
|
||||||
|
|
||||||
-- Bash
|
-- Bash
|
||||||
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
"BASH", "BASHOPTS", "BASHPID", "BASH_ALIASES", "BASH_ARGC",
|
||||||
"BASH_ARGV", "BASH_CMDS", "BASH_COMMAND", "BASH_EXECUTION_STRING",
|
"BASH_ARGV", "BASH_ARGV0", "BASH_CMDS", "BASH_COMMAND",
|
||||||
"BASH_LINENO", "BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL",
|
"BASH_EXECUTION_STRING", "BASH_LINENO", "BASH_LOADABLES_PATH",
|
||||||
"BASH_VERSINFO", "BASH_VERSION", "COMP_CWORD", "COMP_KEY",
|
"BASH_REMATCH", "BASH_SOURCE", "BASH_SUBSHELL", "BASH_VERSINFO",
|
||||||
"COMP_LINE", "COMP_POINT", "COMP_TYPE", "COMP_WORDBREAKS",
|
"BASH_VERSION", "COMP_CWORD", "COMP_KEY", "COMP_LINE", "COMP_POINT",
|
||||||
"COMP_WORDS", "COPROC", "DIRSTACK", "EUID", "FUNCNAME", "GROUPS",
|
"COMP_TYPE", "COMP_WORDBREAKS", "COMP_WORDS", "COPROC", "DIRSTACK",
|
||||||
"HISTCMD", "HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE",
|
"EPOCHREALTIME", "EPOCHSECONDS", "EUID", "FUNCNAME", "GROUPS", "HISTCMD",
|
||||||
"OLDPWD", "OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD",
|
"HOSTNAME", "HOSTTYPE", "LINENO", "MACHTYPE", "MAPFILE", "OLDPWD",
|
||||||
"RANDOM", "READLINE_LINE", "READLINE_POINT", "REPLY", "SECONDS",
|
"OPTARG", "OPTIND", "OSTYPE", "PIPESTATUS", "PPID", "PWD", "RANDOM",
|
||||||
"SHELLOPTS", "SHLVL", "UID", "BASH_ENV", "BASH_XTRACEFD", "CDPATH",
|
"READLINE_ARGUMENT", "READLINE_LINE", "READLINE_MARK", "READLINE_POINT",
|
||||||
"COLUMNS", "COMPREPLY", "EMACS", "ENV", "FCEDIT", "FIGNORE",
|
"REPLY", "SECONDS", "SHELLOPTS", "SHLVL", "SRANDOM", "UID", "BASH_COMPAT",
|
||||||
|
"BASH_ENV", "BASH_XTRACEFD", "CDPATH", "CHILD_MAX", "COLUMNS",
|
||||||
|
"COMPREPLY", "EMACS", "ENV", "EXECIGNORE", "FCEDIT", "FIGNORE",
|
||||||
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
"FUNCNEST", "GLOBIGNORE", "HISTCONTROL", "HISTFILE", "HISTFILESIZE",
|
||||||
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
"HISTIGNORE", "HISTSIZE", "HISTTIMEFORMAT", "HOME", "HOSTFILE", "IFS",
|
||||||
"IGNOREEOF", "INPUTRC", "LANG", "LC_ALL", "LC_COLLATE", "LC_CTYPE",
|
"IGNOREEOF", "INPUTRC", "INSIDE_EMACS", "LANG", "LC_ALL", "LC_COLLATE",
|
||||||
"LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME", "LINES", "MAIL",
|
"LC_CTYPE", "LC_MESSAGES", "LC_MONETARY", "LC_NUMERIC", "LC_TIME",
|
||||||
"MAILCHECK", "MAILPATH", "OPTERR", "PATH", "POSIXLY_CORRECT",
|
"LINES", "MAIL", "MAILCHECK", "MAILPATH", "OPTERR", "PATH",
|
||||||
"PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS1", "PS2", "PS3", "PS4", "SHELL",
|
"POSIXLY_CORRECT", "PROMPT_COMMAND", "PROMPT_DIRTRIM", "PS0", "PS1",
|
||||||
"TIMEFORMAT", "TMOUT", "TMPDIR", "auto_resume", "histchars", "COPROC",
|
"PS2", "PS3", "PS4", "SHELL", "TIMEFORMAT", "TMOUT", "TMPDIR",
|
||||||
|
"auto_resume", "histchars",
|
||||||
|
|
||||||
-- Other
|
-- Other
|
||||||
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
"USER", "TZ", "TERM", "LOGNAME", "LD_LIBRARY_PATH", "LANGUAGE", "DISPLAY",
|
||||||
@@ -41,15 +62,104 @@ internalVariables = [
|
|||||||
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
, "FLAGS_ARGC", "FLAGS_ARGV", "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_HELP",
|
||||||
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
"FLAGS_PARENT", "FLAGS_RESERVED", "FLAGS_TRUE", "FLAGS_VERSION",
|
||||||
"flags_error", "flags_return"
|
"flags_error", "flags_return"
|
||||||
|
] ++ portageManualInternalVariables
|
||||||
|
|
||||||
|
|
||||||
|
portageManualInternalVariables = [
|
||||||
|
-- toolchain settings
|
||||||
|
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS", "FFLAGS", "FCFLAGS",
|
||||||
|
"CBUILD", "CHOST", "MAKEOPTS",
|
||||||
|
-- TODO: Delete these if we can handle `tc-export CC` implicit export.
|
||||||
|
"CC", "CPP", "CXX",
|
||||||
|
|
||||||
|
-- portage internals
|
||||||
|
"EBUILD_PHASE", "EBUILD_SH_ARGS", "EMERGE_FROM", "FILESDIR",
|
||||||
|
"MERGE_TYPE", "PM_EBUILD_HOOK_DIR", "PORTAGE_ACTUAL_DISTDIR",
|
||||||
|
"PORTAGE_ARCHLIST", "PORTAGE_BASHRC", "PORTAGE_BINPKG_FILE",
|
||||||
|
"PORTAGE_BINPKG_TAR_OPTS", "PORTAGE_BINPKG_TMPFILE", "PORTAGE_BIN_PATH",
|
||||||
|
"PORTAGE_BUILDDIR", "PORTAGE_BUILD_GROUP", "PORTAGE_BUILD_USER",
|
||||||
|
"PORTAGE_BUNZIP2_COMMAND", "PORTAGE_BZIP2_COMMAND", "PORTAGE_COLORMAP",
|
||||||
|
"PORTAGE_CONFIGROOT", "PORTAGE_DEBUG", "PORTAGE_DEPCACHEDIR",
|
||||||
|
"PORTAGE_EBUILD_EXIT_FILE", "PORTAGE_ECLASS_LOCATIONS", "PORTAGE_GID",
|
||||||
|
"PORTAGE_GRPNAME", "PORTAGE_INST_GID", "PORTAGE_INST_UID",
|
||||||
|
"PORTAGE_INTERNAL_CALLER", "PORTAGE_IPC_DAEMON", "PORTAGE_IUSE",
|
||||||
|
"PORTAGE_LOG_FILE", "PORTAGE_MUTABLE_FILTERED_VARS",
|
||||||
|
"PORTAGE_OVERRIDE_EPREFIX", "PORTAGE_PYM_PATH", "PORTAGE_PYTHON",
|
||||||
|
"PORTAGE_PYTHONPATH", "PORTAGE_READONLY_METADATA", "PORTAGE_READONLY_VARS",
|
||||||
|
"PORTAGE_REPO_NAME", "PORTAGE_REPOSITORIES", "PORTAGE_RESTRICT",
|
||||||
|
"PORTAGE_SAVED_READONLY_VARS", "PORTAGE_SIGPIPE_STATUS", "PORTAGE_TMPDIR",
|
||||||
|
"PORTAGE_UPDATE_ENV", "PORTAGE_USERNAME", "PORTAGE_VERBOSE",
|
||||||
|
"PORTAGE_WORKDIR_MODE", "PORTAGE_XATTR_EXCLUDE", "REPLACING_VERSIONS",
|
||||||
|
"REPLACED_BY_VERSION", "__PORTAGE_HELPER", "__PORTAGE_TEST_HARDLINK_LOCKS",
|
||||||
|
|
||||||
|
-- generic ebuilds
|
||||||
|
"A", "ARCH", "BDEPEND", "BOARD_USE", "BROOT", "CATEGORY", "D",
|
||||||
|
"DEFINED_PHASES", "DEPEND", "DESCRIPTION", "DISTDIR", "DOCS", "EAPI",
|
||||||
|
"ECLASS", "ED", "EPREFIX", "EROOT", "ESYSROOT", "EXTRA_ECONF",
|
||||||
|
"EXTRA_EINSTALL", "EXTRA_MAKE", "FEATURES", "FILESDIR", "HOME", "HOMEPAGE",
|
||||||
|
"HTML_DOCS", "INHERITED", "IUSE", "KEYWORDS", "LICENSE", "P", "PATCHES",
|
||||||
|
"PDEPEND", "PF", "PKG_INSTALL_MASK", "PKGUSE", "PN", "PR", "PROPERTIES",
|
||||||
|
"PROVIDES_EXCLUDE", "PV", "PVR", "QA_AM_MAINTAINER_MODE",
|
||||||
|
"QA_CONFIGURE_OPTIONS", "QA_DESKTOP_FILE", "QA_DT_NEEDED", "QA_EXECSTACK",
|
||||||
|
"QA_FLAGS_IGNORED", "QA_MULTILIB_PATHS", "QA_PREBUILT", "QA_PRESTRIPPED",
|
||||||
|
"QA_SONAME", "QA_SONAME_NO_SYMLINK", "QA_TEXTRELS", "QA_WX_LOAD", "RDEPEND",
|
||||||
|
"REPOSITORY", "REQUIRED_USE", "REQUIRES_EXCLUDE", "RESTRICT", "ROOT", "S",
|
||||||
|
"SLOT", "SRC_TEST", "SRC_URI", "STRIP_MASK", "SUBSLOT", "SYSROOT", "T",
|
||||||
|
"WORKDIR",
|
||||||
|
|
||||||
|
-- autotest.eclass declared incorrectly
|
||||||
|
"AUTOTEST_CLIENT_TESTS", "AUTOTEST_CLIENT_SITE_TESTS",
|
||||||
|
"AUTOTEST_SERVER_TESTS", "AUTOTEST_SERVER_SITE_TESTS", "AUTOTEST_CONFIG",
|
||||||
|
"AUTOTEST_DEPS", "AUTOTEST_PROFILERS", "AUTOTEST_CONFIG_LIST",
|
||||||
|
"AUTOTEST_DEPS_LIST", "AUTOTEST_PROFILERS_LIST",
|
||||||
|
|
||||||
|
-- cros-board.eclass declared incorrectly
|
||||||
|
"CROS_BOARDS",
|
||||||
|
|
||||||
|
-- Undeclared cros-kernel2 vars
|
||||||
|
"AFDO_PROFILE_VERSION",
|
||||||
|
|
||||||
|
-- haskell-cabal.eclass declared incorrectly
|
||||||
|
"CABAL_FEATURES",
|
||||||
|
|
||||||
|
-- Undeclared haskell-cabal.eclass vars
|
||||||
|
"CABAL_CORE_LIB_GHC_PV",
|
||||||
|
|
||||||
|
-- Undeclared readme.gentoo.eclass vars
|
||||||
|
"DOC_CONTENTS",
|
||||||
|
|
||||||
|
-- Backwards compatibility perl-module.eclass vars
|
||||||
|
"MODULE_AUTHOR", "MODULE_VERSION",
|
||||||
|
|
||||||
|
-- Undeclared perl-module.eclass vars
|
||||||
|
"mydoc",
|
||||||
|
|
||||||
|
-- python-utils-r1.eclass declared incorrectly
|
||||||
|
"RESTRICT_PYTHON_ABIS", "PYTHON_MODNAME",
|
||||||
|
|
||||||
|
-- ABI variables
|
||||||
|
"ABI", "DEFAULT_ABI",
|
||||||
|
|
||||||
|
-- AFDO variables
|
||||||
|
"AFDO_LOCATION",
|
||||||
|
|
||||||
|
-- Linguas
|
||||||
|
"LINGUAS"
|
||||||
]
|
]
|
||||||
|
|
||||||
specialVariablesWithoutSpaces = [
|
|
||||||
"$", "-", "?", "!", "#"
|
specialIntegerVariables = [
|
||||||
|
"$", "?", "!", "#"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
specialVariablesWithoutSpaces = "-" : specialIntegerVariables
|
||||||
|
|
||||||
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
variablesWithoutSpaces = specialVariablesWithoutSpaces ++ [
|
||||||
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID", "LINENO",
|
"BASHPID", "BASH_ARGC", "BASH_LINENO", "BASH_SUBSHELL", "EUID",
|
||||||
"OPTIND", "PPID", "RANDOM", "SECONDS", "SHELLOPTS", "SHLVL", "UID",
|
"EPOCHREALTIME", "EPOCHSECONDS", "LINENO", "OPTIND", "PPID", "RANDOM",
|
||||||
"COLUMNS", "HISTFILESIZE", "HISTSIZE", "LINES"
|
"READLINE_ARGUMENT", "READLINE_MARK", "READLINE_POINT", "SECONDS",
|
||||||
|
"SHELLOPTS", "SHLVL", "SRANDOM", "UID", "COLUMNS", "HISTFILESIZE",
|
||||||
|
"HISTSIZE", "LINES"
|
||||||
|
|
||||||
-- shflags
|
-- shflags
|
||||||
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
, "FLAGS_ERROR", "FLAGS_FALSE", "FLAGS_TRUE"
|
||||||
@@ -64,7 +174,9 @@ unbracedVariables = specialVariables ++ [
|
|||||||
arrayVariables = [
|
arrayVariables = [
|
||||||
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
"BASH_ALIASES", "BASH_ARGC", "BASH_ARGV", "BASH_CMDS", "BASH_LINENO",
|
||||||
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
|
"BASH_REMATCH", "BASH_SOURCE", "BASH_VERSINFO", "COMP_WORDS", "COPROC",
|
||||||
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY"
|
"DIRSTACK", "FUNCNAME", "GROUPS", "MAPFILE", "PIPESTATUS", "COMPREPLY",
|
||||||
|
-- For Portage
|
||||||
|
"PATCHES"
|
||||||
]
|
]
|
||||||
|
|
||||||
commonCommands = [
|
commonCommands = [
|
||||||
@@ -95,10 +207,10 @@ commonCommands = [
|
|||||||
|
|
||||||
nonReadingCommands = [
|
nonReadingCommands = [
|
||||||
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
"alias", "basename", "bg", "cal", "cd", "chgrp", "chmod", "chown",
|
||||||
"cp", "du", "echo", "export", "false", "fg", "fuser", "getconf",
|
"cp", "du", "echo", "export", "fg", "fuser", "getconf",
|
||||||
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
"getopt", "getopts", "ipcrm", "ipcs", "jobs", "kill", "ln", "ls",
|
||||||
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
"locale", "mv", "printf", "ps", "pwd", "renice", "rm", "rmdir",
|
||||||
"set", "sleep", "touch", "trap", "true", "ulimit", "unalias", "uname"
|
"set", "sleep", "touch", "trap", "ulimit", "unalias", "uname"
|
||||||
]
|
]
|
||||||
|
|
||||||
sampleWords = [
|
sampleWords = [
|
||||||
@@ -124,6 +236,11 @@ unaryTestOps = [
|
|||||||
"-o", "-v", "-R"
|
"-o", "-v", "-R"
|
||||||
]
|
]
|
||||||
|
|
||||||
|
-- Variables inspected by Portage tc-export_build_env
|
||||||
|
portageBuildEnvVariables = [
|
||||||
|
"CFLAGS", "CXXFLAGS", "CPPFLAGS", "LDFLAGS"
|
||||||
|
]
|
||||||
|
|
||||||
shellForExecutable :: String -> Maybe Shell
|
shellForExecutable :: String -> Maybe Shell
|
||||||
shellForExecutable name =
|
shellForExecutable name =
|
||||||
case name of
|
case name of
|
||||||
@@ -138,5 +255,6 @@ shellForExecutable name =
|
|||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
flagsForRead = "sreu:n:N:i:p:a:t:"
|
flagsForRead = "sreu:n:N:i:p:a:t:"
|
||||||
|
flagsForMapfile = "d:n:O:s:u:C:c:t"
|
||||||
|
|
||||||
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
declaringCommands = ["local", "declare", "export", "readonly", "typeset", "let"]
|
||||||
|
314
src/ShellCheck/Debug.hs
Normal file
314
src/ShellCheck/Debug.hs
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
{-
|
||||||
|
|
||||||
|
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,
|
||||||
|
cfAdditionalInitialVariables = []
|
||||||
|
}
|
||||||
|
|
||||||
|
-- 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,6 +22,8 @@
|
|||||||
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
module ShellCheck.Fixer (applyFix, removeTabStops, mapPositions, Ranged(..), runTests) where
|
||||||
|
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Prelude
|
||||||
|
import Control.Monad
|
||||||
import Control.Monad.State
|
import Control.Monad.State
|
||||||
import Data.Array
|
import Data.Array
|
||||||
import Data.List
|
import Data.List
|
||||||
@@ -35,7 +37,7 @@ class Ranged a where
|
|||||||
end :: a -> Position
|
end :: a -> Position
|
||||||
overlap :: a -> a -> Bool
|
overlap :: a -> a -> Bool
|
||||||
overlap x y =
|
overlap x y =
|
||||||
(yStart >= xStart && yStart < xEnd) || (yStart < xStart && yEnd > xStart)
|
xEnd > yStart && yEnd > xStart
|
||||||
where
|
where
|
||||||
yStart = start y
|
yStart = start y
|
||||||
yEnd = end y
|
yEnd = end y
|
||||||
@@ -86,6 +88,7 @@ instance Ranged Replacement where
|
|||||||
instance Monoid Fix where
|
instance Monoid Fix where
|
||||||
mempty = newFix
|
mempty = newFix
|
||||||
mappend = (<>)
|
mappend = (<>)
|
||||||
|
mconcat = foldl mappend mempty -- fold left to right since <> discards right on overlap
|
||||||
|
|
||||||
instance Semigroup Fix where
|
instance Semigroup Fix where
|
||||||
f1 <> f2 =
|
f1 <> f2 =
|
||||||
@@ -228,7 +231,7 @@ applyReplacement2 rep string = do
|
|||||||
|
|
||||||
let (l1, l2) = tmap posLine originalPos in
|
let (l1, l2) = tmap posLine originalPos in
|
||||||
when (l1 /= 1 || l2 /= 1) $
|
when (l1 /= 1 || l2 /= 1) $
|
||||||
error "ShellCheck internal error, please report: bad cross-line fix"
|
error $ pleaseReport "bad cross-line fix"
|
||||||
|
|
||||||
let replacer = repString rep
|
let replacer = repString rep
|
||||||
let shift = (length replacer) - (oldEnd - oldStart)
|
let shift = (length replacer) - (oldEnd - oldStart)
|
||||||
|
@@ -88,7 +88,7 @@ outputError file error = putStrLn $ concat [
|
|||||||
attr s v = concat [ s, "='", escape v, "' " ]
|
attr s v = concat [ s, "='", escape v, "' " ]
|
||||||
escape = concatMap escape'
|
escape = concatMap escape'
|
||||||
escape' c = if isOk c then [c] else "&#" ++ show (ord c) ++ ";"
|
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 "error" = "error"
|
||||||
severity "warning" = "warning"
|
severity "warning" = "warning"
|
||||||
|
@@ -203,10 +203,9 @@ formatDoc color (DiffDoc name lf regions) =
|
|||||||
buildFixMap :: [Fix] -> M.Map String Fix
|
buildFixMap :: [Fix] -> M.Map String Fix
|
||||||
buildFixMap fixes = perFile
|
buildFixMap fixes = perFile
|
||||||
where
|
where
|
||||||
splitFixes = concatMap splitFixByFile fixes
|
splitFixes = splitFixByFile $ mconcat fixes
|
||||||
perFile = groupByMap (posFile . repStartPos . head . fixReplacements) splitFixes
|
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 -> [Fix]
|
||||||
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
|
splitFixByFile fix = map makeFix $ groupBy sameFile (fixReplacements fix)
|
||||||
where
|
where
|
||||||
|
@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON (format) where
|
|||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
@@ -103,7 +104,7 @@ collectResult ref cr sys = mapM_ f groups
|
|||||||
comments = crComments cr
|
comments = crComments cr
|
||||||
groups = groupWith sourceFile comments
|
groups = groupWith sourceFile comments
|
||||||
f :: [PositionedComment] -> IO ()
|
f :: [PositionedComment] -> IO ()
|
||||||
f group = modifyIORef ref (\x -> comments ++ x)
|
f group = deepseq comments $ modifyIORef ref (\x -> comments ++ x)
|
||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
|
@@ -23,6 +23,7 @@ module ShellCheck.Formatter.JSON1 (format) where
|
|||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.IORef
|
import Data.IORef
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
@@ -120,7 +121,7 @@ collectResult ref cr sys = mapM_ f groups
|
|||||||
result <- siReadFile sys (Just True) filename
|
result <- siReadFile sys (Just True) filename
|
||||||
let contents = either (const "") id result
|
let contents = either (const "") id result
|
||||||
let comments' = makeNonVirtual comments contents
|
let comments' = makeNonVirtual comments contents
|
||||||
modifyIORef ref (\x -> comments' ++ x)
|
deepseq comments' $ modifyIORef ref (\x -> comments' ++ x)
|
||||||
|
|
||||||
finish ref = do
|
finish ref = do
|
||||||
list <- readIORef ref
|
list <- readIORef ref
|
||||||
|
@@ -23,6 +23,7 @@ import ShellCheck.Fixer
|
|||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
import ShellCheck.Formatter.Format
|
import ShellCheck.Formatter.Format
|
||||||
|
|
||||||
|
import Control.DeepSeq
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
import Data.Array
|
import Data.Array
|
||||||
import Data.Foldable
|
import Data.Foldable
|
||||||
@@ -88,7 +89,7 @@ rankError err = (ranking, cSeverity $ pcComment err, cCode $ pcComment err)
|
|||||||
appendComments errRef comments max = do
|
appendComments errRef comments max = do
|
||||||
previous <- readIORef errRef
|
previous <- readIORef errRef
|
||||||
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
let current = map (\x -> (rankError x, cCode $ pcComment x, cMessage $ pcComment x)) comments
|
||||||
writeIORef errRef . take max . nubBy equal . sort $ previous ++ current
|
writeIORef errRef $! force . take max . nubBy equal . sort $ previous ++ current
|
||||||
where
|
where
|
||||||
fst3 (x,_,_) = x
|
fst3 (x,_,_) = x
|
||||||
equal x y = fst3 x == fst3 y
|
equal x y = fst3 x == fst3 y
|
||||||
|
@@ -25,7 +25,7 @@ module ShellCheck.Interface
|
|||||||
, CheckResult(crFilename, crComments)
|
, CheckResult(crFilename, crComments)
|
||||||
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
, ParseSpec(psFilename, psScript, psCheckSourced, psIgnoreRC, psShellTypeOverride)
|
||||||
, ParseResult(prComments, prTokenPositions, prRoot)
|
, ParseResult(prComments, prTokenPositions, prRoot)
|
||||||
, AnalysisSpec(asScript, asShellType, asFallbackShell, asExecutionMode, asCheckSourced, asTokenPositions, asOptionalChecks)
|
, AnalysisSpec(..)
|
||||||
, AnalysisResult(arComments)
|
, AnalysisResult(arComments)
|
||||||
, FormatterOptions(foColorOption, foWikiLinkCount)
|
, FormatterOptions(foColorOption, foWikiLinkCount)
|
||||||
, Shell(Ksh, Sh, Bash, Dash)
|
, Shell(Ksh, Sh, Bash, Dash)
|
||||||
@@ -39,11 +39,12 @@ module ShellCheck.Interface
|
|||||||
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
, ColorOption(ColorAuto, ColorAlways, ColorNever)
|
||||||
, TokenComment(tcId, tcComment, tcFix)
|
, TokenComment(tcId, tcComment, tcFix)
|
||||||
, emptyCheckResult
|
, emptyCheckResult
|
||||||
, newParseResult
|
|
||||||
, newAnalysisSpec
|
|
||||||
, newAnalysisResult
|
, newAnalysisResult
|
||||||
|
, newAnalysisSpec
|
||||||
, newFormatterOptions
|
, newFormatterOptions
|
||||||
|
, newParseResult
|
||||||
, newPosition
|
, newPosition
|
||||||
|
, newSystemInterface
|
||||||
, newTokenComment
|
, newTokenComment
|
||||||
, mockedSystemInterface
|
, mockedSystemInterface
|
||||||
, mockRcFile
|
, mockRcFile
|
||||||
@@ -86,7 +87,9 @@ data SystemInterface m = SystemInterface {
|
|||||||
-- find the sourced file
|
-- find the sourced file
|
||||||
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
|
siFindSource :: String -> Maybe Bool -> [String] -> String -> m FilePath,
|
||||||
-- | Get the configuration file (name, contents) for a filename
|
-- | Get the configuration file (name, contents) for a filename
|
||||||
siGetConfig :: String -> m (Maybe (FilePath, String))
|
siGetConfig :: String -> m (Maybe (FilePath, String)),
|
||||||
|
-- | Look up Portage Eclass variables
|
||||||
|
siGetPortageVariables :: m (Map.Map String [String])
|
||||||
}
|
}
|
||||||
|
|
||||||
-- ShellCheck input and output
|
-- ShellCheck input and output
|
||||||
@@ -135,6 +138,15 @@ newParseSpec = ParseSpec {
|
|||||||
psShellTypeOverride = Nothing
|
psShellTypeOverride = Nothing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
newSystemInterface :: Monad m => SystemInterface m
|
||||||
|
newSystemInterface =
|
||||||
|
SystemInterface {
|
||||||
|
siReadFile = \_ _ -> return $ Left "Not implemented",
|
||||||
|
siFindSource = \_ _ _ name -> return name,
|
||||||
|
siGetConfig = \_ -> return Nothing,
|
||||||
|
siGetPortageVariables = return Map.empty
|
||||||
|
}
|
||||||
|
|
||||||
-- Parser input and output
|
-- Parser input and output
|
||||||
data ParseSpec = ParseSpec {
|
data ParseSpec = ParseSpec {
|
||||||
psFilename :: String,
|
psFilename :: String,
|
||||||
@@ -164,6 +176,7 @@ data AnalysisSpec = AnalysisSpec {
|
|||||||
asFallbackShell :: Maybe Shell,
|
asFallbackShell :: Maybe Shell,
|
||||||
asExecutionMode :: ExecutionMode,
|
asExecutionMode :: ExecutionMode,
|
||||||
asCheckSourced :: Bool,
|
asCheckSourced :: Bool,
|
||||||
|
asIsPortage :: Bool,
|
||||||
asOptionalChecks :: [String],
|
asOptionalChecks :: [String],
|
||||||
asTokenPositions :: Map.Map Id (Position, Position)
|
asTokenPositions :: Map.Map Id (Position, Position)
|
||||||
}
|
}
|
||||||
@@ -174,6 +187,7 @@ newAnalysisSpec token = AnalysisSpec {
|
|||||||
asFallbackShell = Nothing,
|
asFallbackShell = Nothing,
|
||||||
asExecutionMode = Executed,
|
asExecutionMode = Executed,
|
||||||
asCheckSourced = False,
|
asCheckSourced = False,
|
||||||
|
asIsPortage = False,
|
||||||
asOptionalChecks = [],
|
asOptionalChecks = [],
|
||||||
asTokenPositions = Map.empty
|
asTokenPositions = Map.empty
|
||||||
}
|
}
|
||||||
@@ -311,7 +325,7 @@ data ColorOption =
|
|||||||
|
|
||||||
-- For testing
|
-- For testing
|
||||||
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
mockedSystemInterface :: [(String, String)] -> SystemInterface Identity
|
||||||
mockedSystemInterface files = SystemInterface {
|
mockedSystemInterface files = (newSystemInterface :: SystemInterface Identity) {
|
||||||
siReadFile = rf,
|
siReadFile = rf,
|
||||||
siFindSource = fs,
|
siFindSource = fs,
|
||||||
siGetConfig = const $ return Nothing
|
siGetConfig = const $ return Nothing
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
{-
|
{-
|
||||||
Copyright 2012-2021 Vidar Holen
|
Copyright 2012-2022 Vidar Holen
|
||||||
|
|
||||||
This file is part of ShellCheck.
|
This file is part of ShellCheck.
|
||||||
https://www.shellcheck.net
|
https://www.shellcheck.net
|
||||||
@@ -27,6 +27,7 @@ import ShellCheck.AST
|
|||||||
import ShellCheck.ASTLib hiding (runTests)
|
import ShellCheck.ASTLib hiding (runTests)
|
||||||
import ShellCheck.Data
|
import ShellCheck.Data
|
||||||
import ShellCheck.Interface
|
import ShellCheck.Interface
|
||||||
|
import ShellCheck.Prelude
|
||||||
|
|
||||||
import Control.Applicative ((<*), (*>))
|
import Control.Applicative ((<*), (*>))
|
||||||
import Control.Monad
|
import Control.Monad
|
||||||
@@ -37,7 +38,6 @@ import Data.Functor
|
|||||||
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
|
import Data.List (isPrefixOf, isInfixOf, isSuffixOf, partition, sortBy, intercalate, nub, find)
|
||||||
import Data.Maybe
|
import Data.Maybe
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
import Debug.Trace -- STRIP
|
|
||||||
import GHC.Exts (sortWith)
|
import GHC.Exts (sortWith)
|
||||||
import Prelude hiding (readList)
|
import Prelude hiding (readList)
|
||||||
import System.IO
|
import System.IO
|
||||||
@@ -46,7 +46,7 @@ import Text.Parsec.Error
|
|||||||
import Text.Parsec.Pos
|
import Text.Parsec.Pos
|
||||||
import qualified Control.Monad.Reader as Mr
|
import qualified Control.Monad.Reader as Mr
|
||||||
import qualified Control.Monad.State as Ms
|
import qualified Control.Monad.State as Ms
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map.Strict as Map
|
||||||
|
|
||||||
import Test.QuickCheck.All (quickCheckAll)
|
import Test.QuickCheck.All (quickCheckAll)
|
||||||
|
|
||||||
@@ -210,7 +210,7 @@ getNextIdSpanningTokenList list =
|
|||||||
-- Get the span covered by an id
|
-- Get the span covered by an id
|
||||||
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
|
getSpanForId :: Monad m => Id -> SCParser m (SourcePos, SourcePos)
|
||||||
getSpanForId id =
|
getSpanForId id =
|
||||||
Map.findWithDefault (error "Internal error: no position for id. Please report!") id <$>
|
Map.findWithDefault (error $ pleaseReport "no parser span for id") id <$>
|
||||||
getMap
|
getMap
|
||||||
|
|
||||||
-- Create a new id with the same span as an existing one
|
-- Create a new id with the same span as an existing one
|
||||||
@@ -457,8 +457,8 @@ called s p = do
|
|||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
withContext (ContextName pos s) p
|
withContext (ContextName pos s) p
|
||||||
|
|
||||||
withAnnotations anns =
|
withAnnotations anns p =
|
||||||
withContext (ContextAnnotation anns)
|
if null anns then p else withContext (ContextAnnotation anns) p
|
||||||
|
|
||||||
readConditionContents single =
|
readConditionContents single =
|
||||||
readCondContents `attempting` lookAhead (do
|
readCondContents `attempting` lookAhead (do
|
||||||
@@ -556,7 +556,7 @@ readConditionContents single =
|
|||||||
notFollowedBy2 (try (spacing >> string "]"))
|
notFollowedBy2 (try (spacing >> string "]"))
|
||||||
x <- readNormalWord
|
x <- readNormalWord
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
when (endedWith "]" x && notArrayIndex x) $ do
|
when (notArrayIndex x && endedWith "]" x && not (x `containsLiteral` "[")) $ do
|
||||||
parseProblemAt pos ErrorC 1020 $
|
parseProblemAt pos ErrorC 1020 $
|
||||||
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
|
"You need a space before the " ++ (if single then "]" else "]]") ++ "."
|
||||||
fail "Missing space before ]"
|
fail "Missing space before ]"
|
||||||
@@ -572,6 +572,7 @@ readConditionContents single =
|
|||||||
endedWith _ _ = False
|
endedWith _ _ = False
|
||||||
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
|
notArrayIndex (T_NormalWord id s@(_:T_Literal _ t:_)) = t /= "["
|
||||||
notArrayIndex _ = True
|
notArrayIndex _ = True
|
||||||
|
containsLiteral x s = s `isInfixOf` onlyLiteralString x
|
||||||
|
|
||||||
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
|
readCondAndOp = readAndOrOp TC_And "&&" False <|> readAndOrOp TC_And "-a" True
|
||||||
|
|
||||||
@@ -717,20 +718,20 @@ prop_a6 = isOk readArithmeticContents " 1 | 2 ||3|4"
|
|||||||
prop_a7 = isOk readArithmeticContents "3*2**10"
|
prop_a7 = isOk readArithmeticContents "3*2**10"
|
||||||
prop_a8 = isOk readArithmeticContents "3"
|
prop_a8 = isOk readArithmeticContents "3"
|
||||||
prop_a9 = isOk readArithmeticContents "a^!-b"
|
prop_a9 = isOk readArithmeticContents "a^!-b"
|
||||||
prop_a10= isOk readArithmeticContents "! $?"
|
prop_a10 = isOk readArithmeticContents "! $?"
|
||||||
prop_a11= isOk readArithmeticContents "10#08 * 16#f"
|
prop_a11 = isOk readArithmeticContents "10#08 * 16#f"
|
||||||
prop_a12= isOk readArithmeticContents "\"$((3+2))\" + '37'"
|
prop_a12 = isOk readArithmeticContents "\"$((3+2))\" + '37'"
|
||||||
prop_a13= isOk readArithmeticContents "foo[9*y+x]++"
|
prop_a13 = isOk readArithmeticContents "foo[9*y+x]++"
|
||||||
prop_a14= isOk readArithmeticContents "1+`echo 2`"
|
prop_a14 = isOk readArithmeticContents "1+`echo 2`"
|
||||||
prop_a15= isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
|
prop_a15 = isOk readArithmeticContents "foo[`echo foo | sed s/foo/4/g` * 3] + 4"
|
||||||
prop_a16= isOk readArithmeticContents "$foo$bar"
|
prop_a16 = isOk readArithmeticContents "$foo$bar"
|
||||||
prop_a17= isOk readArithmeticContents "i<(0+(1+1))"
|
prop_a17 = isOk readArithmeticContents "i<(0+(1+1))"
|
||||||
prop_a18= isOk readArithmeticContents "a?b:c"
|
prop_a18 = isOk readArithmeticContents "a?b:c"
|
||||||
prop_a19= isOk readArithmeticContents "\\\n3 +\\\n 2"
|
prop_a19 = isOk readArithmeticContents "\\\n3 +\\\n 2"
|
||||||
prop_a20= isOk readArithmeticContents "a ? b ? c : d : e"
|
prop_a20 = isOk readArithmeticContents "a ? b ? c : d : e"
|
||||||
prop_a21= isOk readArithmeticContents "a ? b : c ? d : e"
|
prop_a21 = isOk readArithmeticContents "a ? b : c ? d : e"
|
||||||
prop_a22= isOk readArithmeticContents "!!a"
|
prop_a22 = isOk readArithmeticContents "!!a"
|
||||||
prop_a23= isOk readArithmeticContents "~0"
|
prop_a23 = isOk readArithmeticContents "~0"
|
||||||
readArithmeticContents :: Monad m => SCParser m Token
|
readArithmeticContents :: Monad m => SCParser m Token
|
||||||
readArithmeticContents =
|
readArithmeticContents =
|
||||||
readSequence
|
readSequence
|
||||||
@@ -819,11 +820,13 @@ readArithmeticContents =
|
|||||||
return $ TA_Expansion id pieces
|
return $ TA_Expansion id pieces
|
||||||
|
|
||||||
readGroup = do
|
readGroup = do
|
||||||
|
start <- startSpan
|
||||||
char '('
|
char '('
|
||||||
s <- readSequence
|
s <- readSequence
|
||||||
char ')'
|
char ')'
|
||||||
|
id <- endSpan start
|
||||||
spacing
|
spacing
|
||||||
return s
|
return $ TA_Parentesis id s
|
||||||
|
|
||||||
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
readArithTerm = readGroup <|> readVariable <|> readExpansion
|
||||||
|
|
||||||
@@ -923,8 +926,8 @@ prop_readCondition7 = isOk readCondition "[[ ${line} =~ ^[[:space:]]*# ]]"
|
|||||||
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
|
prop_readCondition8 = isOk readCondition "[[ $l =~ ogg|flac ]]"
|
||||||
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"
|
prop_readCondition9 = isOk readCondition "[ foo -a -f bar ]"
|
||||||
prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]"
|
prop_readCondition10 = isOk readCondition "[[\na == b\n||\nc == d ]]"
|
||||||
prop_readCondition10a= isOk readCondition "[[\na == b ||\nc == d ]]"
|
prop_readCondition10a = isOk readCondition "[[\na == b ||\nc == d ]]"
|
||||||
prop_readCondition10b= isOk readCondition "[[ a == b\n||\nc == d ]]"
|
prop_readCondition10b = isOk readCondition "[[ a == b\n||\nc == d ]]"
|
||||||
prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]"
|
prop_readCondition11 = isOk readCondition "[[ a == b ||\n c == d ]]"
|
||||||
prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]"
|
prop_readCondition12 = isWarning readCondition "[ a == b \n -o c == d ]"
|
||||||
prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
|
prop_readCondition13 = isOk readCondition "[[ foo =~ ^fo{1,3}$ ]]"
|
||||||
@@ -941,6 +944,9 @@ prop_readCondition23 = isOk readCondition "[[ -v arr[$var] ]]"
|
|||||||
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
prop_readCondition25 = isOk readCondition "[[ lex.yy.c -ot program.l ]]"
|
||||||
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
|
prop_readCondition26 = isOk readScript "[[ foo ]]\\\n && bar"
|
||||||
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
|
prop_readCondition27 = not $ isOk readConditionCommand "[[ x ]] foo"
|
||||||
|
prop_readCondition28 = isOk readCondition "[[ x = [\"$1\"] ]]"
|
||||||
|
prop_readCondition29 = isOk readCondition "[[ x = [*] ]]"
|
||||||
|
|
||||||
readCondition = called "test expression" $ do
|
readCondition = called "test expression" $ do
|
||||||
opos <- getPosition
|
opos <- getPosition
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
@@ -985,6 +991,10 @@ prop_readAnnotation5 = isOk readAnnotation "# shellcheck disable=SC2002 # All ca
|
|||||||
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
prop_readAnnotation6 = isOk readAnnotation "# shellcheck disable=SC1234 # shellcheck foo=bar\n"
|
||||||
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
|
prop_readAnnotation7 = isOk readAnnotation "# shellcheck disable=SC1000,SC2000-SC3000,SC1001\n"
|
||||||
prop_readAnnotation8 = isOk readAnnotation "# shellcheck disable=all\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
|
readAnnotation = called "shellcheck directive" $ do
|
||||||
try readAnnotationPrefix
|
try readAnnotationPrefix
|
||||||
many1 linewhitespace
|
many1 linewhitespace
|
||||||
@@ -1000,12 +1010,19 @@ readAnnotationWithoutPrefix sandboxed = do
|
|||||||
many linewhitespace
|
many linewhitespace
|
||||||
return $ concat values
|
return $ concat values
|
||||||
where
|
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
|
readKey = do
|
||||||
keyPos <- getPosition
|
keyPos <- getPosition
|
||||||
key <- many1 (letter <|> char '-')
|
key <- many1 (letter <|> char '-')
|
||||||
char '=' <|> fail "Expected '=' after directive key"
|
char '=' <|> fail "Expected '=' after directive key"
|
||||||
annotations <- case key of
|
annotations <- case key of
|
||||||
"disable" -> readElement `sepBy` char ','
|
"disable" -> plainOrQuoted $ readElement `sepBy` char ','
|
||||||
where
|
where
|
||||||
readElement = readRange <|> readAll
|
readElement = readRange <|> readAll
|
||||||
readAll = do
|
readAll = do
|
||||||
@@ -1020,21 +1037,21 @@ readAnnotationWithoutPrefix sandboxed = do
|
|||||||
int <- many1 digit
|
int <- many1 digit
|
||||||
return $ read int
|
return $ read int
|
||||||
|
|
||||||
"enable" -> readName `sepBy` char ','
|
"enable" -> plainOrQuoted $ readName `sepBy` char ','
|
||||||
where
|
where
|
||||||
readName = EnableComment <$> many1 (letter <|> char '-')
|
readName = EnableComment <$> many1 (letter <|> char '-')
|
||||||
|
|
||||||
"source" -> do
|
"source" -> do
|
||||||
filename <- many1 $ noneOf " \n"
|
filename <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||||
return [SourceOverride filename]
|
return [SourceOverride filename]
|
||||||
|
|
||||||
"source-path" -> do
|
"source-path" -> do
|
||||||
dirname <- many1 $ noneOf " \n"
|
dirname <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||||
return [SourcePath dirname]
|
return [SourcePath dirname]
|
||||||
|
|
||||||
"shell" -> do
|
"shell" -> do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
shell <- many1 $ noneOf " \n"
|
shell <- quoted (many1 anyChar) <|> (many1 $ noneOf " \n")
|
||||||
when (isNothing $ shellForExecutable shell) $
|
when (isNothing $ shellForExecutable shell) $
|
||||||
parseNoteAt pos ErrorC 1103
|
parseNoteAt pos ErrorC 1103
|
||||||
"This shell type is unknown. Use e.g. sh or bash."
|
"This shell type is unknown. Use e.g. sh or bash."
|
||||||
@@ -1042,7 +1059,7 @@ readAnnotationWithoutPrefix sandboxed = do
|
|||||||
|
|
||||||
"external-sources" -> do
|
"external-sources" -> do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
value <- many1 letter
|
value <- plainOrQuoted $ many1 letter
|
||||||
case value of
|
case value of
|
||||||
"true" ->
|
"true" ->
|
||||||
if sandboxed
|
if sandboxed
|
||||||
@@ -1696,9 +1713,9 @@ readDollarBraced = called "parameter expansion" $ do
|
|||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
return $ T_DollarBraced id True word
|
return $ T_DollarBraced id True word
|
||||||
|
|
||||||
prop_readDollarExpansion1= isOk readDollarExpansion "$(echo foo; ls\n)"
|
prop_readDollarExpansion1 = isOk readDollarExpansion "$(echo foo; ls\n)"
|
||||||
prop_readDollarExpansion2= isOk readDollarExpansion "$( )"
|
prop_readDollarExpansion2 = isOk readDollarExpansion "$( )"
|
||||||
prop_readDollarExpansion3= isOk readDollarExpansion "$( command \n#comment \n)"
|
prop_readDollarExpansion3 = isOk readDollarExpansion "$( command \n#comment \n)"
|
||||||
readDollarExpansion = called "command expansion" $ do
|
readDollarExpansion = called "command expansion" $ do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
try (string "$(")
|
try (string "$(")
|
||||||
@@ -1790,17 +1807,17 @@ prop_readHereDoc6 = isOk readScript "cat << foo\\ bar\ncow\nfoo bar"
|
|||||||
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
prop_readHereDoc7 = isOk readScript "cat << foo\n\\$(f ())\nfoo"
|
||||||
prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
|
prop_readHereDoc8 = isOk readScript "cat <<foo>>bar\netc\nfoo"
|
||||||
prop_readHereDoc9 = isOk readScript "if true; then cat << foo; fi\nbar\nfoo\n"
|
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_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_readHereDoc11 = isOk readScript "cat << foo $(\nfoo\n)lol\nfoo\n"
|
||||||
prop_readHereDoc12= isOk readScript "cat << foo|cat\nbar\nfoo"
|
prop_readHereDoc12 = isOk readScript "cat << foo|cat\nbar\nfoo"
|
||||||
prop_readHereDoc13= isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
prop_readHereDoc13 = isOk readScript "cat <<'#!'\nHello World\n#!\necho Done"
|
||||||
prop_readHereDoc14= isWarning readScript "cat << foo\nbar\nfoo \n"
|
prop_readHereDoc14 = isWarning readScript "cat << foo\nbar\nfoo \n"
|
||||||
prop_readHereDoc15= isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
prop_readHereDoc15 = isWarning readScript "cat <<foo\nbar\nfoo bar\nfoo"
|
||||||
prop_readHereDoc16= isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
prop_readHereDoc16 = isOk readScript "cat <<- ' foo'\nbar\n foo\n"
|
||||||
prop_readHereDoc17= isWarning readScript "cat <<- ' foo'\nbar\n foo\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_readHereDoc18 = isOk readScript "cat <<'\"foo'\nbar\n\"foo\n"
|
||||||
prop_readHereDoc20= isWarning readScript "cat << foo\n foo\n()\nfoo\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_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_readHereDoc22 = isWarning readScript "cat << foo\r\ncow\r\nfoo\r\n"
|
||||||
prop_readHereDoc23 = isNotOk 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
|
readHereDoc = called "here document" $ do
|
||||||
@@ -1914,7 +1931,7 @@ readPendingHereDocs = do
|
|||||||
-- The end token is just a prefix
|
-- The end token is just a prefix
|
||||||
skipLine
|
skipLine
|
||||||
| hasTrailer ->
|
| hasTrailer ->
|
||||||
error "ShellCheck bug, please report (here doc trailer)."
|
error $ pleaseReport "unexpected heredoc trailer"
|
||||||
|
|
||||||
-- The following cases assume no trailing text:
|
-- The following cases assume no trailing text:
|
||||||
| dashed == Undashed && (not $ null leadingSpace) -> do
|
| dashed == Undashed && (not $ null leadingSpace) -> do
|
||||||
@@ -2095,6 +2112,7 @@ prop_readSimpleCommand11 = isOk readSimpleCommand "/\\* foo"
|
|||||||
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
|
prop_readSimpleCommand12 = isWarning readSimpleCommand "elsif foo"
|
||||||
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
|
prop_readSimpleCommand13 = isWarning readSimpleCommand "ElseIf foo"
|
||||||
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
|
prop_readSimpleCommand14 = isWarning readSimpleCommand "elseif[$i==2]"
|
||||||
|
prop_readSimpleCommand15 = isWarning readSimpleCommand "trap 'foo\"bar' INT"
|
||||||
readSimpleCommand = called "simple command" $ do
|
readSimpleCommand = called "simple command" $ do
|
||||||
prefix <- option [] readCmdPrefix
|
prefix <- option [] readCmdPrefix
|
||||||
skipAnnotationAndWarn
|
skipAnnotationAndWarn
|
||||||
@@ -2124,9 +2142,12 @@ readSimpleCommand = called "simple command" $ do
|
|||||||
id2 <- getNewIdFor id1
|
id2 <- getNewIdFor id1
|
||||||
|
|
||||||
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
|
let result = makeSimpleCommand id1 id2 prefix [cmd] suffix
|
||||||
if isCommand ["source", "."] cmd
|
case () of
|
||||||
then readSource result
|
_ | isCommand ["source", "."] cmd -> readSource result
|
||||||
else return result
|
_ | isCommand ["trap"] cmd -> do
|
||||||
|
syntaxCheckTrap result
|
||||||
|
return result
|
||||||
|
_ -> return result
|
||||||
where
|
where
|
||||||
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
|
isCommand strings (T_NormalWord _ [T_Literal _ s]) = s `elem` strings
|
||||||
isCommand _ _ = False
|
isCommand _ _ = False
|
||||||
@@ -2146,6 +2167,17 @@ readSimpleCommand = called "simple command" $ do
|
|||||||
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
|
parseProblemAtId (getId cmd) ErrorC 1131 "Use 'elif' to start another branch."
|
||||||
_ -> return ()
|
_ -> 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 =
|
commentWarning id =
|
||||||
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
|
parseProblemAtId id ErrorC 1127 "Was this intended as a comment? Use # in sh."
|
||||||
|
|
||||||
@@ -2251,22 +2283,31 @@ readSource t@(T_Redirecting _ _ (T_SimpleCommand cmdId _ (cmd:file':rest'))) = d
|
|||||||
|
|
||||||
subRead name script =
|
subRead name script =
|
||||||
withContext (ContextSource name) $
|
withContext (ContextSource name) $
|
||||||
inSeparateContext $
|
inSeparateContext $ do
|
||||||
subParse (initialPos name) (readScriptFile True) script
|
oldState <- getState
|
||||||
|
setState $ oldState { pendingHereDocs = [] }
|
||||||
|
result <- subParse (initialPos name) (readScriptFile True) script
|
||||||
|
newState <- getState
|
||||||
|
setState $ newState { pendingHereDocs = pendingHereDocs oldState }
|
||||||
|
return result
|
||||||
readSource t = return t
|
readSource t = return t
|
||||||
|
|
||||||
|
|
||||||
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
|
prop_readPipeline = isOk readPipeline "! cat /etc/issue | grep -i ubuntu"
|
||||||
prop_readPipeline2 = isWarning 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_readPipeline3 = isOk readPipeline "for f; do :; done|cat"
|
||||||
|
prop_readPipeline4 = isOk readPipeline "! ! true"
|
||||||
|
prop_readPipeline5 = isOk readPipeline "true | ! true"
|
||||||
readPipeline = do
|
readPipeline = do
|
||||||
unexpecting "keyword/token" readKeyword
|
unexpecting "keyword/token" readKeyword
|
||||||
do
|
readBanged readPipeSequence
|
||||||
|
|
||||||
|
readBanged parser = do
|
||||||
|
pos <- getPosition
|
||||||
(T_Bang id) <- g_Bang
|
(T_Bang id) <- g_Bang
|
||||||
pipe <- readPipeSequence
|
next <- readBanged parser
|
||||||
return $ T_Banged id pipe
|
return $ T_Banged id next
|
||||||
<|>
|
<|> parser
|
||||||
readPipeSequence
|
|
||||||
|
|
||||||
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
|
prop_readAndOr = isOk readAndOr "grep -i lol foo || exit 1"
|
||||||
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
|
prop_readAndOr1 = isOk readAndOr "# shellcheck disable=1\nfoo"
|
||||||
@@ -2282,7 +2323,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."
|
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 $
|
andOr <- withAnnotations annotations $
|
||||||
chainr1 readPipeline $ do
|
chainl1 readPipeline $ do
|
||||||
op <- g_AND_IF <|> g_OR_IF
|
op <- g_AND_IF <|> g_OR_IF
|
||||||
readLineBreak
|
readLineBreak
|
||||||
return $ case op of T_AND_IF id -> T_AndIf id
|
return $ case op of T_AND_IF id -> T_AndIf id
|
||||||
@@ -2322,7 +2363,7 @@ readTerm = do
|
|||||||
|
|
||||||
readPipeSequence = do
|
readPipeSequence = do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
(cmds, pipes) <- sepBy1WithSeparators readCommand
|
(cmds, pipes) <- sepBy1WithSeparators (readBanged readCommand)
|
||||||
(readPipe `thenSkip` (spacing >> readLineBreak))
|
(readPipe `thenSkip` (spacing >> readLineBreak))
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
spacing
|
spacing
|
||||||
@@ -2352,6 +2393,10 @@ readCommand = choice [
|
|||||||
]
|
]
|
||||||
|
|
||||||
readCmdName = do
|
readCmdName = do
|
||||||
|
-- If the command name is `!` then
|
||||||
|
optional . lookAhead . try $ do
|
||||||
|
char '!'
|
||||||
|
whitespace
|
||||||
-- Ignore alias suppression
|
-- Ignore alias suppression
|
||||||
optional . try $ do
|
optional . try $ do
|
||||||
char '\\'
|
char '\\'
|
||||||
@@ -2483,16 +2528,29 @@ readBraceGroup = called "brace group" $ do
|
|||||||
spacing
|
spacing
|
||||||
return $ T_BraceGroup id list
|
return $ T_BraceGroup id list
|
||||||
|
|
||||||
prop_readBatsTest = isOk readBatsTest "@test 'can parse' {\n true\n}"
|
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}"
|
||||||
readBatsTest = called "bats @test" $ do
|
readBatsTest = called "bats @test" $ do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
try $ string "@test"
|
try $ string "@test "
|
||||||
spacing
|
spacing
|
||||||
name <- readNormalWord
|
name <- readBatsName
|
||||||
spacing
|
spacing
|
||||||
test <- readBraceGroup
|
test <- readBraceGroup
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
return $ T_BatsTest id name test
|
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"
|
prop_readWhileClause = isOk readWhileClause "while [[ -e foo ]]; do sleep 1; done"
|
||||||
readWhileClause = called "while loop" $ do
|
readWhileClause = called "while loop" $ do
|
||||||
@@ -2521,7 +2579,7 @@ readDoGroup kwId = do
|
|||||||
parseProblem ErrorC 1058 "Expected 'do'."
|
parseProblem ErrorC 1058 "Expected 'do'."
|
||||||
return "Expected 'do'"
|
return "Expected 'do'"
|
||||||
|
|
||||||
acceptButWarn g_Semi ErrorC 1059 "No semicolons directly after 'do'."
|
acceptButWarn g_Semi ErrorC 1059 "Semicolon is not allowed directly after 'do'. You can just delete it."
|
||||||
allspacing
|
allspacing
|
||||||
|
|
||||||
optional (do
|
optional (do
|
||||||
@@ -2550,9 +2608,9 @@ prop_readForClause6 = isOk readForClause "for ((;;))\ndo echo $i\ndone"
|
|||||||
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
|
prop_readForClause7 = isOk readForClause "for ((;;)) do echo $i\ndone"
|
||||||
prop_readForClause8 = 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_readForClause9 = isOk readForClause "for i do true; done"
|
||||||
prop_readForClause10= isOk readForClause "for ((;;)) { true; }"
|
prop_readForClause10 = isOk readForClause "for ((;;)) { true; }"
|
||||||
prop_readForClause12= isWarning readForClause "for $a in *; do echo \"$a\"; done"
|
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_readForClause13 = isOk readForClause "for foo\nin\\\n bar\\\n baz\ndo true; done"
|
||||||
readForClause = called "for loop" $ do
|
readForClause = called "for loop" $ do
|
||||||
pos <- getPosition
|
pos <- getPosition
|
||||||
(T_For id) <- g_For
|
(T_For id) <- g_For
|
||||||
@@ -2684,10 +2742,10 @@ prop_readFunctionDefinition6 = isOk readFunctionDefinition "?(){ foo; }"
|
|||||||
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
|
prop_readFunctionDefinition7 = isOk readFunctionDefinition "..(){ cd ..; }"
|
||||||
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
|
prop_readFunctionDefinition8 = isOk readFunctionDefinition "foo() (ls)"
|
||||||
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
|
prop_readFunctionDefinition9 = isOk readFunctionDefinition "function foo { true; }"
|
||||||
prop_readFunctionDefinition10= isOk readFunctionDefinition "function foo () { true; }"
|
prop_readFunctionDefinition10 = isOk readFunctionDefinition "function foo () { true; }"
|
||||||
prop_readFunctionDefinition11= isWarning readFunctionDefinition "function foo{\ntrue\n}"
|
prop_readFunctionDefinition11 = isWarning readFunctionDefinition "function foo{\ntrue\n}"
|
||||||
prop_readFunctionDefinition12= isOk readFunctionDefinition "function []!() { true; }"
|
prop_readFunctionDefinition12 = isOk readFunctionDefinition "function []!() { true; }"
|
||||||
prop_readFunctionDefinition13= isOk readFunctionDefinition "@require(){ true; }"
|
prop_readFunctionDefinition13 = isOk readFunctionDefinition "@require(){ true; }"
|
||||||
readFunctionDefinition = called "function" $ do
|
readFunctionDefinition = called "function" $ do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
functionSignature <- try readFunctionSignature
|
functionSignature <- try readFunctionSignature
|
||||||
@@ -2885,14 +2943,14 @@ prop_readAssignmentWord5 = isOk readAssignmentWord "b+=lol"
|
|||||||
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
prop_readAssignmentWord7 = isOk readAssignmentWord "a[3$n'']=42"
|
||||||
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
prop_readAssignmentWord8 = isOk readAssignmentWord "a[4''$(cat foo)]=42"
|
||||||
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
prop_readAssignmentWord9 = isOk readAssignmentWord "IFS= "
|
||||||
prop_readAssignmentWord9a= isOk readAssignmentWord "foo="
|
prop_readAssignmentWord9a = isOk readAssignmentWord "foo="
|
||||||
prop_readAssignmentWord9b= isOk readAssignmentWord "foo= "
|
prop_readAssignmentWord9b = isOk readAssignmentWord "foo= "
|
||||||
prop_readAssignmentWord9c= isOk readAssignmentWord "foo= #bar"
|
prop_readAssignmentWord9c = isOk readAssignmentWord "foo= #bar"
|
||||||
prop_readAssignmentWord11= isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
prop_readAssignmentWord11 = isOk readAssignmentWord "foo=([a]=b [c] [d]= [e f )"
|
||||||
prop_readAssignmentWord12= isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
prop_readAssignmentWord12 = isOk readAssignmentWord "a[b <<= 3 + c]='thing'"
|
||||||
prop_readAssignmentWord13= isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
prop_readAssignmentWord13 = isOk readAssignmentWord "var=( (1 2) (3 4) )"
|
||||||
prop_readAssignmentWord14= 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_readAssignmentWord15 = isOk readAssignmentWord "var=(1 [2]=(3 4))"
|
||||||
readAssignmentWord = readAssignmentWordExt True
|
readAssignmentWord = readAssignmentWordExt True
|
||||||
readWellFormedAssignment = readAssignmentWordExt False
|
readWellFormedAssignment = readAssignmentWordExt False
|
||||||
readAssignmentWordExt lenient = called "variable assignment" $ do
|
readAssignmentWordExt lenient = called "variable assignment" $ do
|
||||||
@@ -3240,23 +3298,30 @@ prop_readScript3 = isWarning readScript "#!/bin/bash\necho hello\xA0world"
|
|||||||
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
prop_readScript4 = isWarning readScript "#!/usr/bin/perl\nfoo=("
|
||||||
prop_readScript5 = isOk readScript "#!/bin/bash\n#This is an empty script\n\n"
|
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_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
|
readScriptFile sourced = do
|
||||||
start <- startSpan
|
start <- startSpan
|
||||||
pos <- getPosition
|
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
|
rcAnnotations <- if sourced
|
||||||
then return []
|
then return []
|
||||||
else do
|
else do
|
||||||
filename <- Mr.asks currentFilename
|
filename <- Mr.asks currentFilename
|
||||||
readConfigFile filename
|
readConfigFile filename
|
||||||
|
|
||||||
|
-- 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
|
let annotations = fileAnnotations ++ rcAnnotations
|
||||||
annotationId <- endSpan annotationStart
|
annotationId <- endSpan annotationStart
|
||||||
let shellAnnotationSpecified =
|
let shellAnnotationSpecified =
|
||||||
@@ -3268,8 +3333,9 @@ readScriptFile sourced = do
|
|||||||
verifyShebang pos (executableFromShebang shebangString)
|
verifyShebang pos (executableFromShebang shebangString)
|
||||||
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
if ignoreShebang || isValidShell (executableFromShebang shebangString) /= Just False
|
||||||
then do
|
then do
|
||||||
commands <- withAnnotations annotations readCompoundListOrEmpty
|
commands <- readCompoundListOrEmpty
|
||||||
id <- endSpan start
|
id <- endSpan start
|
||||||
|
readPendingHereDocs
|
||||||
verifyEof
|
verifyEof
|
||||||
let script = T_Annotation annotationId annotations $
|
let script = T_Annotation annotationId annotations $
|
||||||
T_Script id shebang commands
|
T_Script id shebang commands
|
||||||
@@ -3308,6 +3374,7 @@ readScriptFile sourced = do
|
|||||||
"awk",
|
"awk",
|
||||||
"csh",
|
"csh",
|
||||||
"expect",
|
"expect",
|
||||||
|
"fish",
|
||||||
"perl",
|
"perl",
|
||||||
"python",
|
"python",
|
||||||
"ruby",
|
"ruby",
|
||||||
@@ -3370,16 +3437,6 @@ parsesCleanly parser string = runIdentity $ do
|
|||||||
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
return $ Just . null $ parseNotes userState ++ parseProblems systemState
|
||||||
(Left _, _) -> return Nothing
|
(Left _, _) -> return Nothing
|
||||||
|
|
||||||
-- For printf debugging: print the value of an expression
|
|
||||||
-- Example: return $ dump $ T_Literal id [c]
|
|
||||||
dump :: Show a => a -> a -- STRIP
|
|
||||||
dump x = trace (show x) x -- STRIP
|
|
||||||
|
|
||||||
-- Like above, but print a specific expression:
|
|
||||||
-- Example: return $ dumps ("Returning: " ++ [c]) $ T_Literal id [c]
|
|
||||||
dumps :: Show x => x -> a -> a -- STRIP
|
|
||||||
dumps t = trace (show t) -- STRIP
|
|
||||||
|
|
||||||
parseWithNotes parser = do
|
parseWithNotes parser = do
|
||||||
item <- parser
|
item <- parser
|
||||||
state <- getState
|
state <- getState
|
||||||
@@ -3459,9 +3516,9 @@ notesForContext list = zipWith ($) [first, second] $ filter isName list
|
|||||||
|
|
||||||
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
-- Go over all T_UnparsedIndex and reparse them as either arithmetic or text
|
||||||
-- depending on declare -A statements.
|
-- depending on declare -A statements.
|
||||||
reparseIndices root =
|
reparseIndices root = process root
|
||||||
analyze blank blank f root
|
|
||||||
where
|
where
|
||||||
|
process = analyze blank blank f
|
||||||
associative = getAssociativeArrays root
|
associative = getAssociativeArrays root
|
||||||
isAssociative s = s `elem` associative
|
isAssociative s = s `elem` associative
|
||||||
f (T_Assignment id mode name indices value) = do
|
f (T_Assignment id mode name indices value) = do
|
||||||
@@ -3486,8 +3543,9 @@ reparseIndices root =
|
|||||||
|
|
||||||
fixAssignmentIndex name word =
|
fixAssignmentIndex name word =
|
||||||
case word of
|
case word of
|
||||||
T_UnparsedIndex id pos src ->
|
T_UnparsedIndex id pos src -> do
|
||||||
parsed name pos src
|
idx <- parsed name pos src
|
||||||
|
process idx -- Recursively parse for cases like x[y[z=1]]=1
|
||||||
_ -> return word
|
_ -> return word
|
||||||
|
|
||||||
parsed name pos src =
|
parsed name pos src =
|
||||||
|
138
src/ShellCheck/PortageVariables.hs
Normal file
138
src/ShellCheck/PortageVariables.hs
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
{-# LANGUAGE ApplicativeDo #-}
|
||||||
|
{-# LANGUAGE LambdaCase #-}
|
||||||
|
{-# LANGUAGE OverloadedStrings #-}
|
||||||
|
{-# LANGUAGE TupleSections #-}
|
||||||
|
|
||||||
|
module ShellCheck.PortageVariables (
|
||||||
|
readPortageVariables
|
||||||
|
) where
|
||||||
|
|
||||||
|
import ShellCheck.Regex
|
||||||
|
|
||||||
|
import Control.Exception
|
||||||
|
import Control.Monad
|
||||||
|
import Data.Maybe
|
||||||
|
import System.Directory (listDirectory)
|
||||||
|
import System.Exit (ExitCode(..))
|
||||||
|
import System.FilePath
|
||||||
|
import System.IO
|
||||||
|
import System.Process
|
||||||
|
|
||||||
|
import qualified Data.ByteString as B
|
||||||
|
import qualified Data.Map as M
|
||||||
|
|
||||||
|
type RepoName = String
|
||||||
|
type RepoPath = String
|
||||||
|
type EclassName = String
|
||||||
|
type EclassVar = String
|
||||||
|
|
||||||
|
-- | This is used for looking up what eclass variables are inherited,
|
||||||
|
-- keyed by the name of the eclass.
|
||||||
|
type EclassMap = M.Map EclassName [EclassVar]
|
||||||
|
|
||||||
|
data Repository = Repository
|
||||||
|
{ repositoryName :: RepoName
|
||||||
|
, repositoryLocation :: RepoPath
|
||||||
|
, repositoryEclasses :: [Eclass]
|
||||||
|
} deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
data Eclass = Eclass
|
||||||
|
{ eclassName :: EclassName
|
||||||
|
, eclassVars :: [EclassVar]
|
||||||
|
} deriving (Show, Eq, Ord)
|
||||||
|
|
||||||
|
readPortageVariables :: IO (M.Map String [String])
|
||||||
|
readPortageVariables = portageVariables <$> scanRepos
|
||||||
|
|
||||||
|
-- | Map from eclass names to a list of eclass variables
|
||||||
|
portageVariables :: [Repository] -> EclassMap
|
||||||
|
portageVariables = foldMap $ foldMap go . repositoryEclasses
|
||||||
|
where
|
||||||
|
go e = M.singleton (eclassName e) (eclassVars e)
|
||||||
|
|
||||||
|
-- | Run @portageq@ to gather a list of repo names and paths, then scan each
|
||||||
|
-- one for eclasses and ultimately eclass metadata.
|
||||||
|
scanRepos :: IO [Repository]
|
||||||
|
scanRepos = do
|
||||||
|
let cmd = "portageq"
|
||||||
|
let args = ["repos_config", "/"]
|
||||||
|
out <- runOrDie cmd args
|
||||||
|
forM (reposParser $ lines out) $ \(n,p) -> Repository n p <$> getEclasses p
|
||||||
|
|
||||||
|
-- | Get the name of the repo and its path from blocks outputted by
|
||||||
|
-- @portageq@. If the path doesn't exist, this will return @Nothing@.
|
||||||
|
reposParser :: [String] -> [(RepoName, RepoPath)]
|
||||||
|
reposParser = f ""
|
||||||
|
where
|
||||||
|
segmentRegex = mkRegex "^\\[(.*)\\].*"
|
||||||
|
locationRegex = mkRegex "^[[:space:]]*location[[:space:]]*=[[:space:]]*(.*)[[:space:]]*$"
|
||||||
|
f name [] = []
|
||||||
|
f name (line:rest) =
|
||||||
|
case (matchRegex segmentRegex line, matchRegex locationRegex line) of
|
||||||
|
(Just [next], _) -> f next rest
|
||||||
|
(_, Just [location]) -> (name, location) : f name rest
|
||||||
|
_ -> f name rest
|
||||||
|
|
||||||
|
-- | Scan the repo path for @*.eclass@ files in @eclass/@, then run
|
||||||
|
-- 'eclassParser' on each of them to produce @[Eclass]@.
|
||||||
|
--
|
||||||
|
-- If the @eclass/@ directory doesn't exist, the scan is skipped for that
|
||||||
|
-- repo.
|
||||||
|
getEclasses :: RepoPath -> IO [Eclass]
|
||||||
|
getEclasses repoLoc = do
|
||||||
|
let eclassDir = repoLoc </> "eclass"
|
||||||
|
|
||||||
|
files <- handle catcher $ listDirectory eclassDir
|
||||||
|
let names = filter (\(_, e) -> e == ".eclass") $ map splitExtension files
|
||||||
|
|
||||||
|
forM (names :: [(String, String)]) $ \(name, ext) -> do
|
||||||
|
contents <- withFile (eclassDir </> name <.> ext) ReadMode readFully
|
||||||
|
return $ Eclass name $ eclassParser (lines contents)
|
||||||
|
|
||||||
|
where
|
||||||
|
catcher :: IOException -> IO [String]
|
||||||
|
catcher e = do
|
||||||
|
hPutStrLn stderr $ "Unable to find .eclass files: " ++ show e
|
||||||
|
return []
|
||||||
|
|
||||||
|
-- | Scan a @.eclass@ file for any @@@ECLASS_VARIABLE:@ comments, generating
|
||||||
|
-- a list of eclass variables.
|
||||||
|
eclassParser :: [String] -> [String]
|
||||||
|
eclassParser lines = mapMaybe match lines
|
||||||
|
where
|
||||||
|
varRegex = mkRegex "^[[:space:]]*#[[:space:]]*@ECLASS_VARIABLE:[[:space:]]*([^[:space:]]*)[[:space:]]*$"
|
||||||
|
match str = head <$> matchRegex varRegex str
|
||||||
|
|
||||||
|
-- | Run the command and return the full stdout string (stdin is ignored).
|
||||||
|
--
|
||||||
|
-- If the command exits with a non-zero exit code, this will throw an
|
||||||
|
-- error including the captured contents of stdout and stderr.
|
||||||
|
runOrDie :: FilePath -> [String] -> IO String
|
||||||
|
runOrDie cmd args = bracket acquire release $ \(_,o,e,p) -> do
|
||||||
|
ot <- readFully (fromJust o)
|
||||||
|
et <- readFully (fromJust e)
|
||||||
|
ec <- waitForProcess p
|
||||||
|
case ec of
|
||||||
|
ExitSuccess -> pure ot
|
||||||
|
ExitFailure i -> fail $ unlines $ map unwords
|
||||||
|
$ [ [ show cmd ]
|
||||||
|
++ map show args
|
||||||
|
++ [ "failed with exit code", show i]
|
||||||
|
, [ "stdout:" ], [ ot ]
|
||||||
|
, [ "stderr:" ], [ et ]
|
||||||
|
]
|
||||||
|
where
|
||||||
|
acquire = createProcess (proc cmd args)
|
||||||
|
{ std_in = NoStream
|
||||||
|
, std_out = CreatePipe
|
||||||
|
, std_err = CreatePipe
|
||||||
|
}
|
||||||
|
release (i,o,e,p) = do
|
||||||
|
_ <- waitForProcess p
|
||||||
|
forM_ [i,o,e] $ mapM_ hClose
|
||||||
|
|
||||||
|
readFully :: Handle -> IO String
|
||||||
|
readFully handle = do
|
||||||
|
hSetBinaryMode handle True
|
||||||
|
str <- hGetContents handle
|
||||||
|
length str `seq` return str
|
51
src/ShellCheck/Prelude.hs
Normal file
51
src/ShellCheck/Prelude.hs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
{-
|
||||||
|
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
|
@@ -22,6 +22,7 @@ fi
|
|||||||
|
|
||||||
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
cabal install --dependencies-only --enable-tests "${flags[@]}" ||
|
||||||
cabal install --dependencies-only "${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[@]}" ||
|
cabal configure --enable-tests "${flags[@]}" ||
|
||||||
die "configure failed"
|
die "configure failed"
|
||||||
|
@@ -25,6 +25,13 @@ exit 0
|
|||||||
echo "Deleting 'dist' and 'dist-newstyle'..."
|
echo "Deleting 'dist' and 'dist-newstyle'..."
|
||||||
rm -rf dist dist-newstyle
|
rm -rf dist dist-newstyle
|
||||||
|
|
||||||
|
execs=$(find . -name shellcheck)
|
||||||
|
|
||||||
|
if [ -n "$execs" ]
|
||||||
|
then
|
||||||
|
die "Found unexpected executables. Remove and try again: $execs"
|
||||||
|
fi
|
||||||
|
|
||||||
log=$(mktemp) || die "Can't create temp file"
|
log=$(mktemp) || die "Can't create temp file"
|
||||||
date >> "$log" || die "Can't write to log"
|
date >> "$log" || die "Can't write to log"
|
||||||
|
|
||||||
@@ -63,14 +70,17 @@ debian:testing apt-get update && apt-get install -y cabal-install
|
|||||||
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
ubuntu:latest apt-get update && apt-get install -y cabal-install
|
||||||
haskell:latest true
|
haskell:latest true
|
||||||
opensuse/leap:latest zypper install -y cabal-install ghc
|
opensuse/leap:latest zypper install -y cabal-install ghc
|
||||||
fedora:latest dnf install -y cabal-install ghc-template-haskell-devel findutils
|
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
|
archlinux:latest pacman -S -y --noconfirm cabal-install ghc-static base-devel
|
||||||
|
|
||||||
# Ubuntu LTS
|
# Ubuntu LTS
|
||||||
|
ubuntu:22.04 apt-get update && apt-get install -y cabal-install
|
||||||
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
ubuntu:20.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
ubuntu:18.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
ubuntu:16.04 apt-get update && apt-get install -y cabal-install
|
||||||
|
|
||||||
# Stack on Ubuntu LTS
|
# Stack on Ubuntu LTS
|
||||||
ubuntu:20.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
|
ubuntu:22.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
|
EOF
|
||||||
|
|
||||||
exit "$final"
|
exit "$final"
|
||||||
|
@@ -5,8 +5,11 @@ import System.Exit
|
|||||||
import qualified ShellCheck.Analytics
|
import qualified ShellCheck.Analytics
|
||||||
import qualified ShellCheck.AnalyzerLib
|
import qualified ShellCheck.AnalyzerLib
|
||||||
import qualified ShellCheck.ASTLib
|
import qualified ShellCheck.ASTLib
|
||||||
|
import qualified ShellCheck.CFG
|
||||||
|
import qualified ShellCheck.CFGAnalysis
|
||||||
import qualified ShellCheck.Checker
|
import qualified ShellCheck.Checker
|
||||||
import qualified ShellCheck.Checks.Commands
|
import qualified ShellCheck.Checks.Commands
|
||||||
|
import qualified ShellCheck.Checks.ControlFlow
|
||||||
import qualified ShellCheck.Checks.Custom
|
import qualified ShellCheck.Checks.Custom
|
||||||
import qualified ShellCheck.Checks.ShellSupport
|
import qualified ShellCheck.Checks.ShellSupport
|
||||||
import qualified ShellCheck.Fixer
|
import qualified ShellCheck.Fixer
|
||||||
@@ -19,8 +22,11 @@ main = do
|
|||||||
ShellCheck.Analytics.runTests
|
ShellCheck.Analytics.runTests
|
||||||
,ShellCheck.AnalyzerLib.runTests
|
,ShellCheck.AnalyzerLib.runTests
|
||||||
,ShellCheck.ASTLib.runTests
|
,ShellCheck.ASTLib.runTests
|
||||||
|
,ShellCheck.CFG.runTests
|
||||||
|
,ShellCheck.CFGAnalysis.runTests
|
||||||
,ShellCheck.Checker.runTests
|
,ShellCheck.Checker.runTests
|
||||||
,ShellCheck.Checks.Commands.runTests
|
,ShellCheck.Checks.Commands.runTests
|
||||||
|
,ShellCheck.Checks.ControlFlow.runTests
|
||||||
,ShellCheck.Checks.Custom.runTests
|
,ShellCheck.Checks.Custom.runTests
|
||||||
,ShellCheck.Checks.ShellSupport.runTests
|
,ShellCheck.Checks.ShellSupport.runTests
|
||||||
,ShellCheck.Fixer.runTests
|
,ShellCheck.Fixer.runTests
|
||||||
|
@@ -3,7 +3,7 @@
|
|||||||
# various resolvers. It's run via distrotest.
|
# various resolvers. It's run via distrotest.
|
||||||
|
|
||||||
resolvers=(
|
resolvers=(
|
||||||
nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
# nightly-"$(date -d "3 days ago" +"%Y-%m-%d")"
|
||||||
)
|
)
|
||||||
|
|
||||||
die() { echo "$*" >&2; exit 1; }
|
die() { echo "$*" >&2; exit 1; }
|
||||||
|
Reference in New Issue
Block a user