├── .editorconfig
├── .github
└── workflows
│ ├── build-and-tests.yml
│ └── create-gh-release.yml
├── CHANGELOG.md
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── TODO.md
├── docker
└── Dockerfile
├── how_to_do_things_safely_in_bash.md
├── img
├── ex-artificial.png
├── ex-realworld.png
└── logo.png
├── moduletests
├── expected
│ ├── arithmetic.bash
│ ├── assoc.bash
│ ├── backtick.bash
│ ├── cmdsub.bash
│ ├── control_structures_1.bash
│ ├── control_structures_2.bash
│ ├── eof_after_command.bash
│ ├── eof_after_rvalue.bash
│ ├── error_unexpected_eof_arith.bash
│ ├── error_unexpected_eof_doublebracket.bash
│ ├── error_unexpected_eof_esc.bash
│ ├── error_unexpected_eof_heredoc.bash
│ ├── esac_1.bash
│ ├── esac_2.bash
│ ├── esac_3.bash
│ ├── heredoc_complicated.bash
│ ├── heredoc_vs_var.bash
│ ├── local.bash
│ ├── nesting.bash
│ ├── phantomstring.bash
│ ├── premature_esac.bash
│ ├── preserve_syntaxerror_emptyvar.bash
│ ├── pwd.bash
│ ├── quoting_unneeded.bash
│ ├── stresc.bash
│ ├── test.bash
│ ├── unsupp_numeral_variable_quot.bash
│ ├── unsupp_numeral_variable_unquot.bash
│ ├── var.bash
│ └── var_unchanged.bash
├── original
│ ├── backtick.bash
│ ├── cmdsub.bash
│ ├── control_structures_2.bash
│ ├── error_unexpected_eof_arith.bash
│ ├── error_unexpected_eof_doublebracket.bash
│ ├── error_unexpected_eof_esc.bash
│ ├── error_unexpected_eof_heredoc.bash
│ ├── esac_1.bash
│ ├── esac_2.bash
│ ├── heredoc_vs_var.bash
│ ├── local.bash
│ ├── nesting.bash
│ ├── phantomstring.bash
│ ├── premature_esac.bash
│ ├── preserve_syntaxerror_emptyvar.bash
│ ├── pwd.bash
│ ├── quoting_unneeded.bash
│ ├── test.bash
│ ├── unsupp_numeral_variable_quot.bash
│ ├── unsupp_numeral_variable_unquot.bash
│ └── var.bash
└── run
├── src
├── commonargcmd.rs
├── commonstrcmd.rs
├── errfmt.rs
├── filestream.rs
├── machine.rs
├── main.rs
├── microparsers.rs
├── sitcase.rs
├── sitcmd.rs
├── sitcomment.rs
├── sitextent.rs
├── sitfor.rs
├── sitmagic.rs
├── sitrvalue.rs
├── sitstrdq.rs
├── sitstrphantom.rs
├── sitstrsqesc.rs
├── sittest.rs
├── situation.rs
├── situntilbyte.rs
├── sitvarbrace.rs
├── sitvarident.rs
├── sitvec.rs
└── testhelpers.rs
└── tests
└── moduletest.rs
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | trim_trailing_whitespace = true
8 | indent_style = tab
9 | indent_size = 4
10 |
11 | [*.md]
12 | indent_style = spaces
13 |
--------------------------------------------------------------------------------
/.github/workflows/build-and-tests.yml:
--------------------------------------------------------------------------------
1 | name: build-and-tests
2 |
3 | on: [push]
4 |
5 | env:
6 | CARGO_TERM_COLOR: always
7 |
8 | jobs:
9 | build-and-tests:
10 | runs-on: ubuntu-latest
11 |
12 | steps:
13 | - uses: actions/checkout@v2
14 |
15 | - name: Build debug
16 | run: cargo build
17 | - name: Test debug
18 | run: cargo test
19 |
20 | - name: Build release
21 | run: cargo build --release
22 | - name: Test release
23 | run: cargo test --release
24 |
--------------------------------------------------------------------------------
/.github/workflows/create-gh-release.yml:
--------------------------------------------------------------------------------
1 | name: Create GitHub release
2 |
3 | on:
4 | push:
5 | tags:
6 | - "v*.*.*"
7 |
8 | permissions:
9 | contents: write
10 |
11 | jobs:
12 | create-release:
13 | runs-on: ubuntu-latest
14 | steps:
15 | - name: Checkout
16 | uses: actions/checkout@v4
17 | - name: Release
18 | uses: softprops/action-gh-release@v1
19 |
20 | upload-assets:
21 | needs: create-release
22 |
23 | strategy:
24 | matrix:
25 | include:
26 | - os: ubuntu-latest
27 | target: x86_64-unknown-linux-gnu
28 | - os: ubuntu-latest
29 | target: x86_64-unknown-linux-musl
30 | - os: ubuntu-latest
31 | target: aarch64-unknown-linux-gnu
32 | - os: ubuntu-latest
33 | target: aarch64-unknown-linux-musl
34 | - os: macos-latest
35 | target: x86_64-apple-darwin
36 | - os: macos-latest
37 | target: aarch64-apple-darwin
38 | - os: windows-latest
39 | target: x86_64-pc-windows-msvc
40 |
41 | runs-on: ${{ matrix.os }}
42 |
43 | steps:
44 | - uses: actions/checkout@v4
45 | - uses: taiki-e/upload-rust-binary-action@v1
46 | with:
47 | bin: shellharden
48 | target: ${{ matrix.target }}
49 | archive: shellharden-$target
50 | checksum: sha512
51 | token: ${{ secrets.GITHUB_TOKEN }}
52 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 4.3.1
4 |
5 | * Fix misdetection of line continuation as a command
6 | * Fix undetected opening quote in word
7 | * Give more context on syntax error
8 |
9 | ## 4.3.0
10 |
11 | *One long awaited rule change*
12 |
13 | * Unnecessary braces on variable expansions in string interpolations are now
14 | allowed.
15 |
16 | ## 4.2.0
17 |
18 | *More helpful with `for` and `test`*
19 |
20 | * Variables that must necessarily be arrays in order to be quoted now become
21 | array expansions instead of simply being quoted
22 | (they must still manually be changed to arrays):
23 | * `for i in $a` → `for i in "${a[@]}"`
24 | * Rewriting array serialization to array expansion in contexts where quoting
25 | is needed, now also for named arrays
26 | (analogous to `$*` → `"$@"`):
27 | * `${a[*]}` → `"${a[@]}"`
28 | * `test` command normalization (see the howto for justification):
29 | * Empty string tests:
30 | * `test -n "$s"` → `test "$s" != ""`
31 | * `test -z "$s"` → `test "$s" = ""`
32 | * xyes deemed unnecessary:
33 | * `test x"$s" = xyes` → `test "$s" = yes`
34 | * Bugfix: Fix lookahead on short read. See commit 21904cce1.
35 |
36 | ## 4.1.3
37 |
38 | *A pandemy's worth of maintenance*
39 | (when "Covid" stopped being a perfect name for a video conferencing company)
40 |
41 | * Syntactic fixes
42 | * Fix nested case (#37)
43 | * Recognise nested variable expansion (#39)
44 | * Recognise arithmetic statement (#42)
45 | * Recognise export assignments (like local, declare, readonly)
46 | * Allow unquoted $* in contexts where quoting is not required, such as s=$*
47 | * Recognise negation, or rather that what follows is in command position,
48 | as needed to recognise assignments like || ! s=$i in loop conditons.
49 | * \`pwd\` rewrites to $PWD, also where quoting is not required
50 | (this was an oversight)
51 | * Feature fixes
52 | * --check no longer leaks out syntax errors or other error output.
53 | * Testing
54 | * Make tests run on GitHub
55 | * Find & test the right build's executable (debug/release), not just both.
56 | * Test that Shellharden is idempotent and exercise --check on current tests
57 | * Color:
58 | * Brighten the comment color 3× for readability on IPS screens where dark
59 | colors look black (or KDE Breeze's not so black terminal background).
60 | * Change 'single quoted string' from yellow to gold.
61 | * Use 3-bit background color for terminals that don't support 24-bit color.
62 | This is merely the most important coloring; the syntax is still
63 | highlighted in 24-bit color and requires a 24-bit terminal to see.
64 | * Change color lazily (less work for the terminal).
65 |
66 | ## 4.1.2
67 |
68 | *One refactoring, plenty of necessary fixes*
69 |
70 | * Fix old bug: Wrong quoting of associative array assignments (#31)
71 | * More permissive: Allow unquoted arguments to local, declare and readonly (#30)
72 | * Consistency: Rewrite `` `pwd` `` → `$PWD` directly, not via `$(pwd)`
73 | * Compatibility with newer Rust: rustc 1.37 through 1.41 and 2018 edition
74 | * Less code: Collapse nested enums in oft-used return type
75 | * Maintainers: Cargo.lock is now included (#28)
76 |
77 | ## 4.1.1
78 |
79 | *More testing*
80 |
81 | * Allow "$*" (no need to rewrite it to "$@" as long as the quotes are on).
82 | * Recognise premature esac to avoid parse error (seen on rustup.sh).
83 | * Write this changelog.
84 | * Cargo Clippy compliant.
85 | * Unittests! Currently focused on corner cases that are hard to moduletest,
86 | namely lookahead.
87 | * Corner cases in the keyword detection inside the `case` statement were fixed.
88 | This would manifest as false positive and false negative detection of the `in`
89 | and `esac` keywords, followed by a likely parse error.
90 | This stems from version 4.0 and was not seen in the wild AFAIK.
91 | The most glaring bug was false positive detection when prefixed.
92 | Less so were the false negatives related to lookahead.
93 | * The special variables $$, $! and $- are now recognized.
94 |
95 | ## 4.1
96 |
97 | *The feature continuation of 4.0*
98 |
99 | Allow non-quoting in more contexts exposed by the 4.0 parser.
100 |
101 | * Allow unquoted rvalues (the value part of an assignment).
102 | * Allow unquoted switch and case expressions.
103 | * Allow backticks where quoting is unneeded.
104 |
105 | ## 4.0.1
106 |
107 | * Implement the --version option.
108 | * Expand help text to account for the fact that there is no manpage (I gave that up).
109 |
110 | ## 4.0
111 |
112 | *The version with the one big feature*
113 |
114 | * Recognise double square brackets as a context where all quoting rules are off.
115 | * More detailed syntax highlighting to reflect the added parser states, such as keywords and command position
116 |
117 | Because not all double square brackets start a double square bracket context
118 | (because they are not in command position),
119 | it is necessary to recognise where commands begin.
120 | Before this, Shellharden kept track of little more than words, quotes and the occasional heredoc.
121 | To track the command position, it is necessary to add a lot more states to the state machine, including control structures, keywords, assignments, redirection, arrays and group commands (hereunder, functions).
122 | The code was also split from one file into many, and I failed to convince git to track most of the code across the split.
123 |
124 | Smaller changes:
125 |
126 | * Holistic quoting fix: Don't swallow the question sign glob as part of the string.
127 | * Hook the tests into `cargo --test`.
128 | * Implement the -- option.
129 |
130 | ## 3.2
131 |
132 | *The second publicity feedback version*
133 |
134 | * Make it build with Cargo (instead of just rustc).
135 | * Add the -h option (as an alias to --help).
136 | * Tests that actually run. Thanks, Robert!
137 | * Fix crash when the file ends prematurely in a heredoc.
138 |
139 | ## 3.1
140 |
141 | *The immediate publicity feedback version*
142 |
143 | * Typo fixes
144 | * Add license
145 | * Add the --check and --replace options
146 |
147 | ## 3.0
148 |
149 | *The publicity compatible version*
150 |
151 | * Project rename
152 | * Rename an option for consistency
153 | * Support arithmetic expansion
154 |
155 | Hindsight: This release made some headlines and took the project out of obscurity:
156 |
157 | * [lobste.rs](https://lobste.rs/s/4jegyk/how_do_things_safely_bash) (by me)
158 | * [Hacker News](https://news.ycombinator.com/item?id=17057596) (not by me)
159 |
160 | ## 2.0
161 |
162 | *The even better version*
163 |
164 | * Holistic quoting:
165 | * $a$b → "$a$b"
166 | * $a"string" → "${a}string"
167 | * Smaller replacement diff
168 | * Bail, and print a big warning, on multi-decimal numbered args like $10.
169 | * Improved support for heredocs
170 |
171 | ## 1.0
172 |
173 | *The first usable version.* I could have stopped here.
174 |
175 | * Modes of operation
176 | * Visible replacements
177 | * Limited support for heredocs
178 |
179 | ## Commit fe7b3eb
180 |
181 | *Proof of concept*
182 |
183 | The first usable syntax highlighter that
184 | sneaks in quotes relatively unnoticeably.
185 |
186 | * Parsing works except for heredocs
187 | * One mode of operation
188 |
189 | ## First commit
190 |
191 | *Reinvent cat.*
192 |
--------------------------------------------------------------------------------
/Cargo.lock:
--------------------------------------------------------------------------------
1 | # This file is automatically @generated by Cargo.
2 | # It is not intended for manual editing.
3 | version = 3
4 |
5 | [[package]]
6 | name = "shellharden"
7 | version = "4.3.1"
8 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "shellharden"
3 | version = "4.3.1"
4 | authors = ["Andreas Nordal"]
5 | repository = "https://github.com/anordal/shellharden/"
6 | description = "The corrective bash syntax highlighter"
7 | license = "MPL-2.0"
8 |
9 | [dependencies]
10 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Mozilla Public License Version 2.0
2 | ==================================
3 |
4 | 1. Definitions
5 | --------------
6 |
7 | 1.1. "Contributor"
8 | means each individual or legal entity that creates, contributes to
9 | the creation of, or owns Covered Software.
10 |
11 | 1.2. "Contributor Version"
12 | means the combination of the Contributions of others (if any) used
13 | by a Contributor and that particular Contributor's Contribution.
14 |
15 | 1.3. "Contribution"
16 | means Covered Software of a particular Contributor.
17 |
18 | 1.4. "Covered Software"
19 | means Source Code Form to which the initial Contributor has attached
20 | the notice in Exhibit A, the Executable Form of such Source Code
21 | Form, and Modifications of such Source Code Form, in each case
22 | including portions thereof.
23 |
24 | 1.5. "Incompatible With Secondary Licenses"
25 | means
26 |
27 | (a) that the initial Contributor has attached the notice described
28 | in Exhibit B to the Covered Software; or
29 |
30 | (b) that the Covered Software was made available under the terms of
31 | version 1.1 or earlier of the License, but not also under the
32 | terms of a Secondary License.
33 |
34 | 1.6. "Executable Form"
35 | means any form of the work other than Source Code Form.
36 |
37 | 1.7. "Larger Work"
38 | means a work that combines Covered Software with other material, in
39 | a separate file or files, that is not Covered Software.
40 |
41 | 1.8. "License"
42 | means this document.
43 |
44 | 1.9. "Licensable"
45 | means having the right to grant, to the maximum extent possible,
46 | whether at the time of the initial grant or subsequently, any and
47 | all of the rights conveyed by this License.
48 |
49 | 1.10. "Modifications"
50 | means any of the following:
51 |
52 | (a) any file in Source Code Form that results from an addition to,
53 | deletion from, or modification of the contents of Covered
54 | Software; or
55 |
56 | (b) any new file in Source Code Form that contains any Covered
57 | Software.
58 |
59 | 1.11. "Patent Claims" of a Contributor
60 | means any patent claim(s), including without limitation, method,
61 | process, and apparatus claims, in any patent Licensable by such
62 | Contributor that would be infringed, but for the grant of the
63 | License, by the making, using, selling, offering for sale, having
64 | made, import, or transfer of either its Contributions or its
65 | Contributor Version.
66 |
67 | 1.12. "Secondary License"
68 | means either the GNU General Public License, Version 2.0, the GNU
69 | Lesser General Public License, Version 2.1, the GNU Affero General
70 | Public License, Version 3.0, or any later versions of those
71 | licenses.
72 |
73 | 1.13. "Source Code Form"
74 | means the form of the work preferred for making modifications.
75 |
76 | 1.14. "You" (or "Your")
77 | means an individual or a legal entity exercising rights under this
78 | License. For legal entities, "You" includes any entity that
79 | controls, is controlled by, or is under common control with You. For
80 | purposes of this definition, "control" means (a) the power, direct
81 | or indirect, to cause the direction or management of such entity,
82 | whether by contract or otherwise, or (b) ownership of more than
83 | fifty percent (50%) of the outstanding shares or beneficial
84 | ownership of such entity.
85 |
86 | 2. License Grants and Conditions
87 | --------------------------------
88 |
89 | 2.1. Grants
90 |
91 | Each Contributor hereby grants You a world-wide, royalty-free,
92 | non-exclusive license:
93 |
94 | (a) under intellectual property rights (other than patent or trademark)
95 | Licensable by such Contributor to use, reproduce, make available,
96 | modify, display, perform, distribute, and otherwise exploit its
97 | Contributions, either on an unmodified basis, with Modifications, or
98 | as part of a Larger Work; and
99 |
100 | (b) under Patent Claims of such Contributor to make, use, sell, offer
101 | for sale, have made, import, and otherwise transfer either its
102 | Contributions or its Contributor Version.
103 |
104 | 2.2. Effective Date
105 |
106 | The licenses granted in Section 2.1 with respect to any Contribution
107 | become effective for each Contribution on the date the Contributor first
108 | distributes such Contribution.
109 |
110 | 2.3. Limitations on Grant Scope
111 |
112 | The licenses granted in this Section 2 are the only rights granted under
113 | this License. No additional rights or licenses will be implied from the
114 | distribution or licensing of Covered Software under this License.
115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a
116 | Contributor:
117 |
118 | (a) for any code that a Contributor has removed from Covered Software;
119 | or
120 |
121 | (b) for infringements caused by: (i) Your and any other third party's
122 | modifications of Covered Software, or (ii) the combination of its
123 | Contributions with other software (except as part of its Contributor
124 | Version); or
125 |
126 | (c) under Patent Claims infringed by Covered Software in the absence of
127 | its Contributions.
128 |
129 | This License does not grant any rights in the trademarks, service marks,
130 | or logos of any Contributor (except as may be necessary to comply with
131 | the notice requirements in Section 3.4).
132 |
133 | 2.4. Subsequent Licenses
134 |
135 | No Contributor makes additional grants as a result of Your choice to
136 | distribute the Covered Software under a subsequent version of this
137 | License (see Section 10.2) or under the terms of a Secondary License (if
138 | permitted under the terms of Section 3.3).
139 |
140 | 2.5. Representation
141 |
142 | Each Contributor represents that the Contributor believes its
143 | Contributions are its original creation(s) or it has sufficient rights
144 | to grant the rights to its Contributions conveyed by this License.
145 |
146 | 2.6. Fair Use
147 |
148 | This License is not intended to limit any rights You have under
149 | applicable copyright doctrines of fair use, fair dealing, or other
150 | equivalents.
151 |
152 | 2.7. Conditions
153 |
154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
155 | in Section 2.1.
156 |
157 | 3. Responsibilities
158 | -------------------
159 |
160 | 3.1. Distribution of Source Form
161 |
162 | All distribution of Covered Software in Source Code Form, including any
163 | Modifications that You create or to which You contribute, must be under
164 | the terms of this License. You must inform recipients that the Source
165 | Code Form of the Covered Software is governed by the terms of this
166 | License, and how they can obtain a copy of this License. You may not
167 | attempt to alter or restrict the recipients' rights in the Source Code
168 | Form.
169 |
170 | 3.2. Distribution of Executable Form
171 |
172 | If You distribute Covered Software in Executable Form then:
173 |
174 | (a) such Covered Software must also be made available in Source Code
175 | Form, as described in Section 3.1, and You must inform recipients of
176 | the Executable Form how they can obtain a copy of such Source Code
177 | Form by reasonable means in a timely manner, at a charge no more
178 | than the cost of distribution to the recipient; and
179 |
180 | (b) You may distribute such Executable Form under the terms of this
181 | License, or sublicense it under different terms, provided that the
182 | license for the Executable Form does not attempt to limit or alter
183 | the recipients' rights in the Source Code Form under this License.
184 |
185 | 3.3. Distribution of a Larger Work
186 |
187 | You may create and distribute a Larger Work under terms of Your choice,
188 | provided that You also comply with the requirements of this License for
189 | the Covered Software. If the Larger Work is a combination of Covered
190 | Software with a work governed by one or more Secondary Licenses, and the
191 | Covered Software is not Incompatible With Secondary Licenses, this
192 | License permits You to additionally distribute such Covered Software
193 | under the terms of such Secondary License(s), so that the recipient of
194 | the Larger Work may, at their option, further distribute the Covered
195 | Software under the terms of either this License or such Secondary
196 | License(s).
197 |
198 | 3.4. Notices
199 |
200 | You may not remove or alter the substance of any license notices
201 | (including copyright notices, patent notices, disclaimers of warranty,
202 | or limitations of liability) contained within the Source Code Form of
203 | the Covered Software, except that You may alter any license notices to
204 | the extent required to remedy known factual inaccuracies.
205 |
206 | 3.5. Application of Additional Terms
207 |
208 | You may choose to offer, and to charge a fee for, warranty, support,
209 | indemnity or liability obligations to one or more recipients of Covered
210 | Software. However, You may do so only on Your own behalf, and not on
211 | behalf of any Contributor. You must make it absolutely clear that any
212 | such warranty, support, indemnity, or liability obligation is offered by
213 | You alone, and You hereby agree to indemnify every Contributor for any
214 | liability incurred by such Contributor as a result of warranty, support,
215 | indemnity or liability terms You offer. You may include additional
216 | disclaimers of warranty and limitations of liability specific to any
217 | jurisdiction.
218 |
219 | 4. Inability to Comply Due to Statute or Regulation
220 | ---------------------------------------------------
221 |
222 | If it is impossible for You to comply with any of the terms of this
223 | License with respect to some or all of the Covered Software due to
224 | statute, judicial order, or regulation then You must: (a) comply with
225 | the terms of this License to the maximum extent possible; and (b)
226 | describe the limitations and the code they affect. Such description must
227 | be placed in a text file included with all distributions of the Covered
228 | Software under this License. Except to the extent prohibited by statute
229 | or regulation, such description must be sufficiently detailed for a
230 | recipient of ordinary skill to be able to understand it.
231 |
232 | 5. Termination
233 | --------------
234 |
235 | 5.1. The rights granted under this License will terminate automatically
236 | if You fail to comply with any of its terms. However, if You become
237 | compliant, then the rights granted under this License from a particular
238 | Contributor are reinstated (a) provisionally, unless and until such
239 | Contributor explicitly and finally terminates Your grants, and (b) on an
240 | ongoing basis, if such Contributor fails to notify You of the
241 | non-compliance by some reasonable means prior to 60 days after You have
242 | come back into compliance. Moreover, Your grants from a particular
243 | Contributor are reinstated on an ongoing basis if such Contributor
244 | notifies You of the non-compliance by some reasonable means, this is the
245 | first time You have received notice of non-compliance with this License
246 | from such Contributor, and You become compliant prior to 30 days after
247 | Your receipt of the notice.
248 |
249 | 5.2. If You initiate litigation against any entity by asserting a patent
250 | infringement claim (excluding declaratory judgment actions,
251 | counter-claims, and cross-claims) alleging that a Contributor Version
252 | directly or indirectly infringes any patent, then the rights granted to
253 | You by any and all Contributors for the Covered Software under Section
254 | 2.1 of this License shall terminate.
255 |
256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all
257 | end user license agreements (excluding distributors and resellers) which
258 | have been validly granted by You or Your distributors under this License
259 | prior to termination shall survive termination.
260 |
261 | ************************************************************************
262 | * *
263 | * 6. Disclaimer of Warranty *
264 | * ------------------------- *
265 | * *
266 | * Covered Software is provided under this License on an "as is" *
267 | * basis, without warranty of any kind, either expressed, implied, or *
268 | * statutory, including, without limitation, warranties that the *
269 | * Covered Software is free of defects, merchantable, fit for a *
270 | * particular purpose or non-infringing. The entire risk as to the *
271 | * quality and performance of the Covered Software is with You. *
272 | * Should any Covered Software prove defective in any respect, You *
273 | * (not any Contributor) assume the cost of any necessary servicing, *
274 | * repair, or correction. This disclaimer of warranty constitutes an *
275 | * essential part of this License. No use of any Covered Software is *
276 | * authorized under this License except under this disclaimer. *
277 | * *
278 | ************************************************************************
279 |
280 | ************************************************************************
281 | * *
282 | * 7. Limitation of Liability *
283 | * -------------------------- *
284 | * *
285 | * Under no circumstances and under no legal theory, whether tort *
286 | * (including negligence), contract, or otherwise, shall any *
287 | * Contributor, or anyone who distributes Covered Software as *
288 | * permitted above, be liable to You for any direct, indirect, *
289 | * special, incidental, or consequential damages of any character *
290 | * including, without limitation, damages for lost profits, loss of *
291 | * goodwill, work stoppage, computer failure or malfunction, or any *
292 | * and all other commercial damages or losses, even if such party *
293 | * shall have been informed of the possibility of such damages. This *
294 | * limitation of liability shall not apply to liability for death or *
295 | * personal injury resulting from such party's negligence to the *
296 | * extent applicable law prohibits such limitation. Some *
297 | * jurisdictions do not allow the exclusion or limitation of *
298 | * incidental or consequential damages, so this exclusion and *
299 | * limitation may not apply to You. *
300 | * *
301 | ************************************************************************
302 |
303 | 8. Litigation
304 | -------------
305 |
306 | Any litigation relating to this License may be brought only in the
307 | courts of a jurisdiction where the defendant maintains its principal
308 | place of business and such litigation shall be governed by laws of that
309 | jurisdiction, without reference to its conflict-of-law provisions.
310 | Nothing in this Section shall prevent a party's ability to bring
311 | cross-claims or counter-claims.
312 |
313 | 9. Miscellaneous
314 | ----------------
315 |
316 | This License represents the complete agreement concerning the subject
317 | matter hereof. If any provision of this License is held to be
318 | unenforceable, such provision shall be reformed only to the extent
319 | necessary to make it enforceable. Any law or regulation which provides
320 | that the language of a contract shall be construed against the drafter
321 | shall not be used to construe this License against a Contributor.
322 |
323 | 10. Versions of the License
324 | ---------------------------
325 |
326 | 10.1. New Versions
327 |
328 | Mozilla Foundation is the license steward. Except as provided in Section
329 | 10.3, no one other than the license steward has the right to modify or
330 | publish new versions of this License. Each version will be given a
331 | distinguishing version number.
332 |
333 | 10.2. Effect of New Versions
334 |
335 | You may distribute the Covered Software under the terms of the version
336 | of the License under which You originally received the Covered Software,
337 | or under the terms of any subsequent version published by the license
338 | steward.
339 |
340 | 10.3. Modified Versions
341 |
342 | If you create software not governed by this License, and you want to
343 | create a new license for such software, you may create and use a
344 | modified version of this License if you rename the license and remove
345 | any references to the name of the license steward (except to note that
346 | such modified license differs from this License).
347 |
348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary
349 | Licenses
350 |
351 | If You choose to distribute Source Code Form that is Incompatible With
352 | Secondary Licenses under the terms of this version of the License, the
353 | notice described in Exhibit B of this License must be attached.
354 |
355 | Exhibit A - Source Code Form License Notice
356 | -------------------------------------------
357 |
358 | This Source Code Form is subject to the terms of the Mozilla Public
359 | License, v. 2.0. If a copy of the MPL was not distributed with this
360 | file, You can obtain one at http://mozilla.org/MPL/2.0/.
361 |
362 | If it is not possible or desirable to put the notice in a particular
363 | file, then You may include the notice in a location (such as a LICENSE
364 | file in a relevant directory) where a recipient would be likely to look
365 | for such a notice.
366 |
367 | You may add additional accurate notices of copyright ownership.
368 |
369 | Exhibit B - "Incompatible With Secondary Licenses" Notice
370 | ---------------------------------------------------------
371 |
372 | This Source Code Form is "Incompatible With Secondary Licenses", as
373 | defined by the Mozilla Public License, v. 2.0.
374 |
375 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | [](https://github.com/anordal/shellharden/actions)
4 |
5 | Shellharden
6 | ===========
7 |
8 | Shellharden is a syntax highlighter and a tool to semi-automate the rewriting
9 | of scripts to ShellCheck conformance, mainly focused on quoting.
10 |
11 | The default mode of operation is like `cat`, but with syntax highlighting in
12 | foreground colors and suggestive changes in background colors:
13 |
14 | 
15 |
16 | Above: Selected portions of `xdg-desktop-menu` as highlighted by Shellharden.
17 | The foreground colors are syntax highlighting, whereas the background colors
18 | (green and red) show characters that Shellharden would have added or removed
19 | if let loose with the `--transform` option.
20 | Below: An artificial example that shows more tricky cases and special features.
21 |
22 | 
23 |
24 | Why
25 | ---
26 |
27 | A variable in bash is like a hand grenade – take off its quotes, and it starts ticking. Hence, rule zero of [bash pitfalls][1]: Always use quotes.
28 |
29 | Name
30 | ----
31 |
32 | Shellharden can do what Shellcheck can't: Apply the suggested changes.
33 |
34 | In other words, harden vulnerable shellscripts.
35 | The builtin assumption is that the script does not *depend* on the vulnerable behavior –
36 | the user is responsible for the code review.
37 |
38 | Shellharden was previously known as "Naziquote".
39 | In the right jargon, that was the best name ever,
40 | but oh so misleading and unspeakable to outsiders.
41 |
42 | I couldn't call it "bash cleaner" either, as that means "poo smearer" in Norwegian.
43 |
44 | Prior art
45 | ---------
46 |
47 | * [Shellcheck][2] is a wonderful tool to *detect*, and give general advice, about vulnerable bash code. The only thing missing is something to say yes with, and *apply* those advice (assuming proper review of course).
48 |
49 | * I asked [this SO question][3], for a tool that could rewrite bash scripts with proper quoting. One answerer beat me to it. But if it was me, I would do a syntax highlighter in the same tool (as a way to see if the parser gets lost, and make the most out of the parser, because bash is like quantum mechanics – nobody really knows how it works).
50 |
51 | Get it
52 | ------
53 |
54 | Distro packages:
55 |
56 | [](https://repology.org/project/shellharden/versions)
57 |
58 | [Official rust package](https://crates.io/crates/shellharden):
59 |
60 | cargo install shellharden
61 |
62 | Build from source
63 | -----------------
64 |
65 | cargo build --release
66 |
67 | ### Install
68 |
69 | mv target/release/shellharden ~/.local/bin/
70 |
71 | ### Run tests
72 |
73 | cargo test
74 |
75 | (requires bash)
76 |
77 | ### Test coverage
78 |
79 | env RUSTFLAGS="-C instrument-coverage" LLVM_PROFILE_FILE='run-%m.profraw' cargo test
80 | grcov . --binary-path ./target/debug/ -s . -t html -o ./coverage/
81 | rm run-*.profraw
82 | open coverage/src/index.html
83 |
84 | ### Fuzz test
85 |
86 | cargo install cargo-afl
87 | cargo afl build --release
88 | cargo afl fuzz -i moduletests/original -o /tmp/fuzz-shellharden target/release/shellharden ''
89 |
90 | Usage advice
91 | ------------
92 |
93 | Don't apply `--transform` blindly; code review is still necessary: A script that *relies* on unquoted behavior (implicit word splitting and glob expansion from variables and command substitutions) to work as intended will do none of that after getting the `--transform` treatment!
94 |
95 | In that unlucky case, ask yourself whether the script has any business in doing that. All too often, it's just a product of classical shellscripting, and would be better off rewritten, such as by using arrays. Even in the opposite case, say the business logic involves word splitting; that can still be done without invoking globbing. In short: There is always a better way than the forbidden syntax (if not more explicit), but some times, a human must step in to rewrite. See how, in the accompanying [how to do things safely in bash](how_to_do_things_safely_in_bash.md).
96 |
97 | [1]: http://mywiki.wooledge.org/BashPitfalls
98 | [2]: https://www.shellcheck.net/
99 | [3]: http://stackoverflow.com/questions/41104131/tool-to-automatically-rewrite-a-bash-script-with-proper-quoting
100 |
--------------------------------------------------------------------------------
/TODO.md:
--------------------------------------------------------------------------------
1 |
2 | # Features
3 | * -c 'check this'
4 | * Things that never work: $10 and [ -n $var ]:
5 | Fail by default, add --unbreak/--fix-neverworking
6 | * --keep-varbraces
7 |
8 | # Rewriting
9 | * sort | uniq → sort -u (man 1p sort approves)
10 | * alias → function
11 | * eval is evil: Color blinking red?
12 | * for i in seq → for ((i…))
13 | * for i in … → while read < <(…)
14 |
15 | # Code organisation
16 | * reduce perilous boilerplate
17 | * make flush an error for easier propagation
18 | * approach agreement with rust-fmt
19 |
20 | # Write about:
21 | * errexit → errtrace ?
22 | * Gotcha: Command substitution "$()" trims whitespace
23 | * Useless uses of find
24 | * cp file dir → cp file dir/
25 | * realpath → readlink -f ?
26 |
--------------------------------------------------------------------------------
/docker/Dockerfile:
--------------------------------------------------------------------------------
1 | # docker build . -f docker/Dockerfile --tag shellharden:latest
2 |
3 |
4 | FROM rust:1.53.0-alpine AS build
5 |
6 | WORKDIR /src
7 | COPY . .
8 | RUN cargo build --release
9 |
10 |
11 | FROM scratch
12 | COPY --from=build /src/target/release/shellharden /shellharden
13 | ENTRYPOINT ["/shellharden"]
14 |
--------------------------------------------------------------------------------
/how_to_do_things_safely_in_bash.md:
--------------------------------------------------------------------------------
1 | Safe ways to do things in bash
2 | ==============================
3 |
4 | Like programming in C or driving a car,
5 | contemporary shellscript languages require some knowledge and discipline to use safely,
6 | but that's not to say it can't be done.
7 |
8 | Purpose of this guide
9 | ---------------------
10 |
11 | This guide accompanies Shellharden, the corrective syntax highlighter.
12 |
13 | Shellharden suggests, and can apply, changes to remove vulnerabilities in shellscripts. This is in accordance with [ShellCheck](https://github.com/koalaman/shellcheck/) and [BashPitfalls](http://mywiki.wooledge.org/BashPitfalls) – Shellharden shall not disagree with these.
14 |
15 | The problem is that not all scripts will work with their vulnerabilities simply removed, because *that* was their working principle, and must be rewritten quite differently.
16 | Thus the need for a human in the loop and a holistic methodology.
17 |
18 | Why focus on bash?
19 | ------------------
20 |
21 | This guide is here to show that bash *can* be used safely.
22 |
23 | It is the goal and realization of this methodology that
24 | all bash scripts are possible to rewrite into wellformedness,
25 | a representation free of those idiomatic bugs that the language otherwise practically imposes.
26 | This is because the set of bad language features is finite, and each has a substitute.
27 |
28 | Unfortunately, [it is hard to defend the correct way of doing something when it isn't also the seemingly simplest][Jussi's two ways].
29 | With this in mind, the python manifesto (`python3 -c 'import this'`),
30 | which says that there should only be one obvious way to do things, and that "explicit is better than implicit",
31 | makes a lot of sense.
32 | While that says something about the impossibility of convincing the vast number of users to adopt a safe methodology,
33 | it is nevertheless possible for those who care.
34 |
35 | Clearly, bash is a bad choice, but other prevalent alternatives are not better:
36 |
37 | * POSIX shell (a language subset that many shells support) lacks arrays. → Disqualified.
38 | * Hereunder: dash, busybox ash
39 | * Fish is a relief – easy to use correctly, but (still) lacks a strict mode. → Disqualified.
40 | * Zsh is largely compatible with Bash. → Also qualifies.
41 |
42 | ### What about non-shellscript languages?
43 |
44 | That is in principle the wrong question. Always use the right tool for the job™.
45 | Shellscript languages are languages for running programs, and for using that as a building block.
46 | That is a domain of its own.
47 |
48 | This is by no means a defense of shellscripting.
49 | Shellscripts keep getting written, and this is how to do it safely.
50 | However, there is one greater sin than writing something that is obviously a shellscript.
51 | When you know you have a shellscript, you know what to worry about, you can bring in the right expertise, and you have the full arsenal of shell linters.
52 | Not so much if [implicitly invoking the shell with improper quoting](#how-to-avoid-invoking-the-shell-with-improper-quoting).
53 |
54 | The first thing to know about bash coding
55 | -----------------------------------------
56 |
57 | If there is anything like a driver's license for safe bash coding,
58 | it must be rule zero of [BashPitfalls](http://mywiki.wooledge.org/BashPitfalls):
59 | **Always use quotes.**
60 |
61 | An unquoted variable is to be treated as an armed bomb: It explodes upon contact with whitespace and wildcards. Yes, "explode" as in [splitting a string into an array](http://php.net/manual/en/function.explode.php). Specifically, variable expansions, like `$var`, and also command substitutions, like `$(cmd)`, undergo *word splitting*, whereby the string is split on any of the characters in the special `$IFS` variable, which is whitespace by default. Furthermore, any wildcard characters (`*?`) in the resulting words are used to expand those words to match files on your filesystem (*indirect pathname expansion*). This is mostly invisible, because most of the time, the result is a 1-element array, which is indistinguishable from the original string value.
62 |
63 | Quoting inhibits word splitting and indirect pathname expansion, both for variables and command substitutions.
64 |
65 | Variable expansion:
66 |
67 | * Good: `"$my_var"`
68 | * Bad: `$my_var`
69 |
70 | Command substitution:
71 |
72 | * Good: `"$(cmd)"`
73 | * Bad: `$(cmd)`
74 |
75 | There are exceptions where quoting is not necessary, but because it never hurts to quote, and the general rule is to be scared when you see an unquoted variable, pursuing the non-obvious exceptions is, for the sake of your readers, questionable. It looks wrong, and the wrong practice is common enough to raise suspicion: Enough scripts are being written with broken handling of filenames that whitespace in filenames is often avoided…
76 |
77 | The exceptions only matter in discussions of style – feel welcome to ignore them. For the sake of style neutrality, Shellharden does honor a few exceptions:
78 |
79 | * variables of invariably numeric content: `$?`, `$$`, `$!`, `$#` and array length `${#array[@]}`
80 | * assignments: `a=$b`
81 | * the magical case command: `case $var in … esac`
82 | * the magical context between double-brackets (`[[` and `]]`) – this is a language of its own.
83 |
84 | ### What quoting is not about
85 |
86 | Shellscript is not a macro (read: injection prone) language. This is not scary:
87 |
88 | ```
89 | input="\"; rm -rf /"
90 | echo "$input"
91 | ```
92 |
93 | ### Should I use backticks?
94 |
95 | Command substitutions also come in this form:
96 |
97 | * Correct: `` "`cmd`" ``
98 | * Bad: `` `cmd` ``
99 |
100 | While it is possible to use this style correctly, it is harder: [Backticks require escaping when nested, and examples in the wild are improperly quoted more often than not](http://mywiki.wooledge.org/BashFAQ/082).
101 |
102 | Not to mention this insidious trick:
103 |
104 | ```
105 | > x=`echo "This is a doublequote: \""`; echo "$x"
106 | This is a doublequote: "
107 | > x="`echo "This is a doublequote: \""`"; echo "$x"
108 | bash: command substitution: line 1: unexpected EOF while looking for matching `"'
109 | ```
110 |
111 | The [Bash documentation](https://www.gnu.org/software/bash/manual/html_node/Command-Substitution.html)
112 | describes the escaping rules as is correct of an unquoted backtick command substitution,
113 | but does not say that a quoted one is different.
114 | Which is perplexing enough, understandably, to be a documentation bug.
115 |
116 | Shellharden accepts unquoted backticks in contexts that don't require quotes, but otherwise rewrites them into the dollar-parenthesis form.
117 |
118 | ### Should I use curly braces?
119 |
120 | Variable substitution: This is not the controversy, but just to get it out of the way: These braces are of course needed:
121 |
122 | "${image%.png}.jpg"
123 |
124 | String interpolation: Braces also have this role:
125 |
126 | "${var}string literal"
127 |
128 | When expanding a variable inside a string, a closing brace can be used to delimit the end of the variable name from subsequent characters of the string literal.
129 | This makes a difference if and only if the next character can take part in a variable name, in other words, if it is an identifier tail character, in regex `[_0-9a-zA-Z]`.
130 |
131 | Strictly speaking, it never hurts to always use braces. Does that make it a good idea?
132 |
133 | Note that this is not a question of correctness, but of brittleness
134 | (the script would have to be edited, and a mistake be made, before it becomes incorrect).
135 | Strictly speaking not that either, because the problem itself is unnecessary:
136 | Quotes are obligatory anyway; just quote variables individually to avoid the problem
137 | (quotes can always replace braces, but not the opposite, and you never need both).
138 | The result is a concatenation instead of interpolation:
139 |
140 | "$var"'string literal'
141 |
142 | Now that the question is clear, your author would say: Mostly not.
143 | In terms of which way to go for consistency's sake, considert that
144 | most variable expansions aren't interpolations. And they shouldn't:
145 | The noble thing to do for a shellscript (or any glue code) is to pass arguments cleanly.
146 | Let's focus on passing arguments cleanly:
147 |
148 | * Bad: `some_command $arg1 $arg2 $arg3`
149 | * Bad and verbose: `some_command ${arg1} ${arg2} ${arg3}`
150 | * Good but verbose: `some_command "${arg1}" "${arg2}" "${arg3}"`
151 | * Good: `some_command "$arg1" "$arg2" "$arg3"`
152 |
153 | The braces don't do anything objectively good here.
154 |
155 | In your author's experience, there is rather a negative correlation between unnecessary use of braces and proper use of quotes – nearly everyone chooses the "bad and verbose" instead of "good but verbose" form! My speculations:
156 |
157 | * Fear of the wrong thing: Instead of worrying about the real danger (missing quotes), a beginner might worry that a variable named `$prefix` would influence the expansion of `"$prefix_postfix"` – this is simply not how it works.
158 | * Cargo cult – writing code in testament to the wrong fear perpetuates it.
159 | * Braces compete with quotes under the limits of tolerable verbosity.
160 |
161 | Shellharden will add and remove braces on an as-needed basis when it needs to add quotes:
162 |
163 | `${arg} $arg"ument"` → `"$arg" "${arg}ument"`
164 |
165 | It will also remove braces on individually quoted variables:
166 |
167 | `"${arg}"` → `"$arg"`
168 |
169 | As of Shellharden 4.3.0, braces are allowed in string interpolations (not that it adds them):
170 |
171 | "${var} "
172 | " ${var}"
173 | "${var}${var}"
174 |
175 | Thus, a neutral stance in interpolations
176 | (being not a style formatter, Shellharden is not supposed to make subjective changes).
177 | Previously, it rewrote interpolations too on an as-needed basis,
178 | but as noted here, this could indeed be relaxed.
179 |
180 | #### Gotcha: Numbered arguments
181 |
182 | Unlike normal *identifier* variable names (in regex: `[_a-zA-Z][_a-zA-Z0-9]*`), numbered arguments require braces (string interpolation or not). ShellCheck says:
183 |
184 | echo "$10"
185 | ^-- SC1037: Braces are required for positionals over 9, e.g. ${10}.
186 |
187 | This was deemed too subtle to either fix or ignore: Shellharden will print a big error message and bail if it sees this.
188 |
189 | Since braces are required above 9, Shellharden permits them on all numbered arguments.
190 |
191 | Use arrays FTW
192 | --------------
193 |
194 | In order to be able to quote all variables, you must use real arrays when that's what you need, not whitespace delimited strings.
195 |
196 | The syntax is verbose, but get over it. This bashism single-handedly disqualifies the POSIX shell for the purpose of this guide.
197 |
198 | Good:
199 |
200 | files=(
201 | a
202 | b
203 | )
204 | duplicates=()
205 | for f in "${files[@]}"; do
206 | if cmp -- "$f" other/"$f"; then
207 | duplicates+=("$f")
208 | fi
209 | done
210 | if [ "${#duplicates[@]}" -gt 0 ]; then
211 | rm -- "${duplicates[@]}"
212 | fi
213 |
214 | Bad:
215 |
216 | files=" \
217 | a \
218 | b \
219 | "
220 | duplicates=
221 | for f in $files; do
222 | if cmp -- "$f" other/"$f"; then
223 | duplicates+=" $f"
224 | fi
225 | done
226 | if ! [ "$duplicates" = '' ]; then
227 | rm -- $duplicates
228 | fi
229 |
230 | Look how similar the two examples are: There is no algorithmical difference between using real arrays instead of strings as a (bad) substitute.
231 | A bonus point goes to the array syntax for not needing line continuations, making those lines possible to comment.
232 | They are not equivalent, of course, as the "bad" example uses a whitespace delimited string,
233 | which breaks down as soon as a filename contains whitespace, and risks deleting the wrong files.
234 |
235 | Is the second example fixable? In theory, yes; in practice, no.
236 | While it is *possible* to represent a list in a string,
237 | even approachable if a suitable delimiter is known,
238 | it becomes hairy (escaping and unescaping the delimiter) to do 100% generically correct.
239 | Worse, getting it back into array form can not be abstracted away (try `set -- a b c` in a function).
240 | The final blow is that fighting such an abstraction failure of the language is pointless if you can choose a different language.
241 |
242 | Arrays is the feature that becomes absurdly impractical to program correctly without. Here is why:
243 | * You need *some* datastructure, that can take zero or more values, for passing zero or more values around cleanly.
244 | * In particular, [command arguments are fundamentally arrays](http://manpag.es/RHEL6/3p+exec). Hint: Shell scripting is all about commands and arguments.
245 | * All POSIX shells secretly support arrays anyway, in the form of the argument list `"$@"`.
246 |
247 | The recommendation of this guide must therefore be to not give POSIX compatibility a second thought.
248 | The POSIX shell standard is hereby declared unfit for our purposes.
249 | Likewise, sadly, for minimalistic POSIX compatible shells like [Dash](https://wiki.ubuntu.com/DashAsBinSh#A.24.7B....7D) and Ash that don't support arrays either.
250 | As for Zsh, it supports a superset of Bash's array syntax, so it is good.
251 |
252 | The lack of a minimalistic shell with array support is a bummer for embedded computuers, where shipping another language is cost sensitive, yet expectations for safety are high. Busybox is impressive for what you get in a small size, but as part of it, you get Ash, which is a hair puller.
253 |
254 | ### Those exceptional cases where you actually intend to split the string
255 |
256 | Splitting `$string` on the separator `$sep` into `$array`:
257 |
258 | Bad (indirect pathname expansion):
259 |
260 | IFS="$sep"
261 | array=($string)
262 |
263 | Good:
264 |
265 | array=()
266 | while read -rd "$sep" i; do
267 | array+=("$i")
268 | done < <(printf '%s%s' "$string" "$sep")
269 |
270 | This works for any separator byte (no UTF-8 or multi-character separator string) except NUL. To make it work for NUL, hardcode the literal `$'\0'` in place of `$sep`.
271 |
272 | The reason for appending the separator to the end is that the field separator is really a field *terminator* (postfix, not infix). The distinction matters to the notion of an empty field at the end. Skip this if your input is already field terminated.
273 |
274 | Alternatively, for Bash 4:
275 |
276 | readarray -td "$sep" array < <(printf '%s%s' "$string" "$sep")
277 |
278 | The same notes apply to readarray (hardcoding of NUL, already field terminated input):
279 |
280 | readarray -td $'\0' array < <(find -print0)
281 |
282 | Readarray gets a small minus point for only working with ASCII separators (still no UTF-8).
283 |
284 | If the separator consists of multiple bytes, it is also possible to do this correctly by string processing (such as by [parameter substitution](https://www.tldp.org/LDP/abs/html/parameter-substitution.html#PSOREX2)).
285 |
286 | #### An alternative with 3 corner cases
287 |
288 | The otherwise evil IFS variable has a legitimate use in the `read` command, where it can be used as another way to separate fields without invoking indirect pathname expansion.
289 | IFS is brought into significance by requesting either multiple variables or using the array option to `read`.
290 | By disabling the delimiter `-d ''`, we read all the way to the end.
291 | Because read returns nonzero when it encounters the end, it must be guarded against errexit (`|| true`) if that is enabled.
292 |
293 | Split to separate variables:
294 |
295 | IFS="$sep" read -rd '' a b rest < <(printf '%s%s' "$string" "$sep") || true
296 |
297 | Split to an array:
298 |
299 | IFS="$sep" read -rd '' -a array < <(printf '%s%s' "$string" "$sep") || true
300 |
301 | The 3 corner cases are tab, newline and space – when IFS is set to one of these as above, `read` drops empty fields!
302 | Because this is often useful though, this method makes the bottom of the recommendation list instead of disqualification.
303 |
304 | ### Corollary: Use while loops to iterate strings and command output
305 |
306 | Shellharden won't let you get away with this:
307 |
308 | for i in $(seq 1 10); do
309 | printf '%s\n' "$i"
310 | done
311 |
312 | The intuitive fix – piping into the loop – is not always cool,
313 | because the pipe operator's right operand becomes a subshell.
314 | Not that it matters for this silly example, but it would surprise many
315 | to find that this loop can't manipulate outside variables:
316 |
317 | seq 1 10 | while read -r i; do
318 | printf '%s\n' "$i"
319 | done
320 |
321 | To avoid future surprises, the bulk of the code should typically not be the subshell.
322 | This is all right:
323 |
324 | while read -r i; do
325 | printf '%s\n' "$i"
326 | done < <(seq 1 10)
327 |
328 | How to begin a bash script
329 | --------------------------
330 |
331 | ### hashbang
332 |
333 | #!/usr/bin/env bash
334 |
335 | * Portability consideration: The absolute path to `env` is likely more portable than the absolute path to `bash`. Case in point: [NixOS](https://nixos.wiki/wiki/NixOS). POSIX mandates [the existence of `env`](http://manpag.es/RHEL6/1p+env), but bash is not a posix thing.
336 | * Safety consideration: No language flavor options like `-euo pipefail` here! It is not actually possible when using the `env` redirection, but even if your hashbang begins with `#!/bin/bash`, it is not the right place for options that influence the meaning of the script, because it can be overridden, which would make it possible to run your script the wrong way. However, options that don't influence the meaning of the script, such as `set -x` would be a bonus to make overridable (if used).
337 |
338 | ### Safer and better globbing
339 |
340 | shopt -s nullglob globstar
341 |
342 | * `nullglob` is what makes `for f in *.txt` work also when zero files happen to match the expression. It removes a special case in the default behavior:
343 | * The default behavior (unofficially called [passglob](https://github.com/fish-shell/fish-shell/issues/2394#issuecomment-182047129)) is to pass the pattern as-is in that event.
344 | As always, special cases are an enemy of correctness: It creates a two-sided source of bugs that likes to defy test coverage:
345 | On one side, it necessitates workarounds when you wanted the general behavior (file existence checks in this case);
346 | on the other side, it supports a convenient and wrong use case ([nothing is worse than the intersection between convenient and wrong][Jussi's two ways]).
347 | When you mean to pass the pattern literally, the safe thing to do is to just do that instead: Quote it.
348 | * `failglob` is also a fine alternative, but not as generally usable:
349 | It can be used if zero matches would always be an error (and conveniently makes it so),
350 | whereas `nullglob` makes it both non-special and easy to check for
351 | (`txt_files=(*.txt); test "${#txt_files[@]}" -eq 0`).
352 | Also, `failglob` depends on `errexit` (aka. `set -e`) to actually exit on failure.
353 | * `globstar` enables recursive globbing. Since globbing is easier to use correctly than `find`, use it.
354 |
355 | ### Strict Mode – safe and relevant subset edition
356 |
357 | if test "$BASH" = "" || "$BASH" -uc "a=();true \"\${a[@]}\"" 2>/dev/null; then
358 | # Bash 4.4, Zsh
359 | set -euo pipefail
360 | else
361 | # Bash 4.3 and older chokes on empty arrays with set -u.
362 | set -eo pipefail
363 | fi
364 |
365 | This is [Bash's unofficial strict mode](http://redsymbol.net/articles/unofficial-bash-strict-mode/) except:
366 |
367 | * `nounset` (aka. `set -u`) is behind a feature check.
368 | * Setting IFS to something safer (but still unsafe): Doesn't hurt, but is irrelevant: Being shellcheck/shellharden compliant means quoting everything – implicit use of IFS is forbidden anyway.
369 |
370 | As it turns out, `nounset` **is dangerous** in Bash 4.3 and earlier: In those versions, it [treats empty arrays as unset](http://stackoverflow.com/questions/7577052/bash-empty-array-expansion-with-set-u). What have we just learned about special cases? They are an enemy of correctness. Also, this can't be worked around, and since using arrays is rather basic in this methodology, and they definitely need to be able to hold empty values, this is far from an ignorable problem. If using `nounset` at all, make sure to use Bash 4.4 or another sane shell like Zsh (easier said than done if you are writing a script and someone else is using it). Fortunately, what works with `nounset` will also work without (unlike `errexit`). Thus why putting it behind a feature check is sane at all.
371 |
372 | Other alternatives:
373 |
374 | * Setting IFS (the *internal field separator*) to the empty string disables word splitting. Sounds like the holy grail, but isn't: Firstly, empty strings still become empty arrays (very uncool in expressions like `test $x = ""`) – you still need to quote everything that *can* be an empty string, and for purposes of static verification, *everything* can (with the exception of a handful of special variables). Secondly, indirect pathname expansion is still a thing (can be turned off, se the next point). Thirdly, it interferes with commands like `read` that also use it, breaking constructs like `cat /etc/fstab | while read -r dev mnt fs opt dump pass; do printf '%s\n' "$fs"; done'`.
375 | * Disabling pathname expansion (globbing) altogether: If there was an option to only disable indirect pathname expansion, I would. Giving up the unproblematic direct one too, that I'm saying you should want to use, is a tradeoff that I can't imagine being necessary, yet currently is.
376 |
377 | ### Assert that command dependencies are installed
378 |
379 | [Declaring your dependencies](https://12factor.net/dependencies) has many benefits, but until this becomes statically verifiable, concentrate on uncommon commands here.
380 | This prevents your script from failing for external reasons in hard-to-reach sections of code, such as in error handling or the end of a long-running script.
381 | It also prevents misbehavior such as `make -j"$(nproc)"` becoming a fork bomb.
382 |
383 | require(){ hash "$@" || exit 127; }
384 | require …
385 | require …
386 | require …
387 |
388 | Benefits of using `hash` for this purpose are its low overhead and that it gives you an error message in the failure case.
389 | This doesn't check option compatibility, of course, but it's also not forbidden to add feature checks for that.
390 |
391 | How to end a bash script
392 | ------------------------
393 |
394 | Goal: The script's exit status should convey its overall success or failure.
395 |
396 | Reality: The script's exit status is that of the last command executed.
397 |
398 | There is a wrong way to end a bash script:
399 | Letting a command used as a condition be the last command executed, so that the script "fails" iff the last condition is false.
400 | While that might happen to be correct for a script, it is a way to encode the exit status that looks accidental and is easily broken by adding or removing code to the end.
401 |
402 | The rightness criterion here is that the last statement follows the "Errexit basics" below. When in doubt, end the script with an explicit exit status:
403 |
404 | exit 0
405 |
406 | How to use errexit
407 | ------------------
408 |
409 | Aka `set -e`.
410 |
411 | ### Errexit basics
412 |
413 | Background: If a command that is not used as a condition returns nonzero, the interpreter exits at that point.
414 |
415 | Failure is trivial to suppress:
416 |
417 | command || true
418 |
419 | Don't skimp on if-statements. You can't use `&&` as a shorthand if-statement without always using `||` as an else-branch. Otherwise, the script terminates if the condition is false.
420 |
421 | Bad:
422 |
423 | command && …
424 |
425 | Good (contrived):
426 |
427 | command && … || true
428 |
429 | Good (contrived):
430 |
431 | ! command || …
432 |
433 | Good (idiomatic):
434 |
435 | if command; then
436 | …
437 | fi
438 |
439 | To capture a command's output while using it as a condition, use an assignment as the condition (but see below on not using `local` on assignments):
440 |
441 | if output="$(command)"; then
442 | …
443 | fi
444 |
445 | If at all using the exit status variable `$?` with errexit, it is of course no substitute for the direct check for command success (otherwise, your script won't live to see this variable whenever it is nonzero). Corollary: The failure case is the only place it makes sense to expand the exit status variable `$?` (because success only has one exit status, which we are checking). A second pitfall is that if we negate the command as part of the check, the exit status will be that of the negated command – a boolean with precisely the useful information removed.
446 |
447 | Bad:
448 |
449 | command
450 | if test $? -ne 0; then
451 | echo Command returned $?
452 | fi
453 |
454 | Bad:
455 |
456 | if ! command; then
457 | echo Command returned $?
458 | fi
459 |
460 | Good:
461 |
462 | if command; then
463 | true
464 | else
465 | echo Command returned $?
466 | fi
467 |
468 | Good:
469 |
470 | command || echo Command returned $?
471 |
472 | ### Program-level deferred cleanup
473 |
474 | In case errexit does its thing, use this to set up any necessary cleanup to happen at exit.
475 |
476 | tmpfile="$(mktemp -t myprogram-XXXXXX)"
477 | cleanup() {
478 | rm -f "$tmpfile"
479 | }
480 | trap cleanup EXIT
481 |
482 | ### Gotcha: Errexit is ignored in command arguments
483 |
484 | Here is a nice underhanded fork bomb that I learnt the hard way – my build script worked fine on various developer machines, but brought my company's buildserver to its knees:
485 |
486 | set -e # Fail if nproc is not installed
487 | make -j"$(nproc)"
488 |
489 | Correct (command substitution in assignment):
490 |
491 | set -e # Fail if nproc is not installed
492 | jobs="$(nproc)"
493 | make -j"$jobs"
494 |
495 | Caution: Builtins like `local` and `export` are also commands, so this is still wrong:
496 |
497 | set -e # Fail if nproc is not installed
498 | local jobs="$(nproc)"
499 | make -j"$jobs"
500 |
501 | ShellCheck warns only about special commands like `local` in this case.
502 |
503 | To use `local`, separate the declaration from the assignment:
504 |
505 | set -e # Fail if nproc is not installed
506 | local jobs
507 | jobs="$(nproc)"
508 | make -j"$jobs"
509 |
510 | ### Gotcha: Errexit is ignored depending on caller context
511 |
512 | Sometimes, POSIX is cruel. Errexit is ignored in functions, group commands and even subshells if the caller is checking its success. These examples all print `Unreachable` and `Great success`, despite all sanity.
513 |
514 | Subshell:
515 |
516 | (
517 | set -e
518 | false
519 | echo Unreachable
520 | ) && echo Great success
521 |
522 | Group command:
523 |
524 | {
525 | set -e
526 | false
527 | echo Unreachable
528 | } && echo Great success
529 |
530 | Function:
531 |
532 | f() {
533 | set -e
534 | false
535 | echo Unreachable
536 | }
537 | f && echo Great success
538 |
539 | This makes bash with errexit practically incomposable – it is *possible* to wrap your errexit functions so that they still work, but the effort it saves (over explicit error handling) becomes questionable. Consider splitting into completely standalone scripts instead.
540 |
541 | How to write conditions
542 | -----------------------
543 |
544 | ### Should I use double-bracket conditions?
545 |
546 | That is unimportant, but let's dispel some myths about these largely interchangeable forms of conditions:
547 |
548 | ```
549 | test …
550 | [ … ]
551 | [[ … ]]
552 | ```
553 |
554 | When in doubt, ask your shell:
555 |
556 | > type test
557 | test is a shell builtin
558 | > type [
559 | [ is a shell builtin
560 | > type [[
561 | [[ is a shell keyword
562 |
563 | * None of them are external commands (even in Fish).
564 | * The two first are mere commands; the third has syntactic superpowers.
565 | * Double brackets are not POSIX. Busybox `ash` supports them, but the wrong way.
566 |
567 | The thing about double-bracket conditions is that, being magic syntax,
568 | it has access to its arguments before expansion, unlike any command.
569 | What it does is make many unsafe habits safe, such as not quoting variable expansions.
570 | Is that a good thing?
571 |
572 | Understanding and exploiting its improved safety aspects requires more of your readers,
573 | and the problems it solves are problems you don't have if you are following this guide.
574 |
575 | For pedagogical purposes, the `test` command is the most honest about being a command. Issues like whitespace sensitivity and how to combine them (unambiguously) become self-evident when looked at the right way.
576 |
577 | Double-bracket conditions also have more features. But they have good POSIX substitutes for the most part:
578 |
579 | * Pattern matching (`[[ $path == *.png || $path == *.gif ]]`): This is what `case` is for.
580 | * Logical operators: The usual suspects `&&` and `||` work just fine – outside commands – and can be grouped with group commands: `if { true || false; } && true; then echo 1; else echo 0; fi`.
581 | * Checking if a variable exists (`[[ -v varname ]]`): Yes, this is possibly a killer argument, but consider the programming style of always setting variables, so you don't need to check if they exist.
582 |
583 | ### Are empty string comparisons any special?
584 |
585 | Of course not:
586 | Quoting also works when strings are empty on purpose!
587 | For readability, prefer normal string comparisons.
588 |
589 | Good:
590 |
591 | test "$s" != ""
592 | test "$s" = ""
593 | [ "$s" != "" ]
594 | [ "$s" = "" ]
595 |
596 | (Many shells also support `==`, but a single equal-sign is posixly correct.)
597 |
598 | Bad (readability):
599 |
600 | test -n "$s"
601 | test -z "$s"
602 | [ -n "$s" ]
603 | [ -z "$s" ]
604 |
605 | Plain wrong (always true):
606 |
607 | test -n $s
608 | [ -n $s ]
609 |
610 | Shellharden replaces the `-z/-n` flags with their equivalent string comparisons.
611 |
612 | ### How to check if a variable exists
613 |
614 | The correct way to check if a variable exists came with Bash 4.2 (also verified for zsh 5.6.2) and is not a feature of POSIX.
615 | Consider therefore to avoid the problem by always setting variables, so you don't need to check if they exist.
616 |
617 | Alternative (POSIX blessed):
618 |
619 | "${var-val}" # default value if unset
620 | "${var:-val}" # default value if unset or empty
621 |
622 | POSIX allows expanding a possibly unset variable (even with `set -u`) by giving a default value.
623 |
624 | Good (if you must):
625 |
626 | ```
627 | # Feature check to fail early on Mac OS with Bash 3.9:
628 | [[ -v PWD ]]
629 |
630 | [[ -v var ]]
631 | ```
632 |
633 | If using this and there is any chance someone might try to run your script with an earlier Bash version, it is best to fail early. The feature check above tests for a variable that we know exists and results in a syntax error and termination in earlier versions.
634 |
635 | Bad:
636 |
637 | test -n "$var"
638 | [ -n "$var" ]
639 | [[ -n $var ]]
640 | [[ -n "$var" ]]
641 |
642 | These don't distinguish being unset with being empty (as a string or array) and obviously precludes the use of `set -u`.
643 |
644 | Recall that the `-z/-n` flags are effectively string comparisons in disguise. As such, they are even less suitable as variable existence checks. Other than to distinguish intent – a fair but not good argument.
645 |
646 | ### How to combine conditions (unambiguously)
647 |
648 | This problem evaporates when realizing that conditions are commands – POSIX already defines this:
649 | The shell syntax for conjunction `&&`, disjunction `||`, negation `!` and grouping `{}` of commands applies to all commands,
650 | and the arguments to those commands can contain shell syntax all they want.
651 |
652 | The confusing part is that the `test` or `[` command has operators for the same:
653 | POSIX (man 1p test) defines these as `-a`, `-o`, `!`, and parentheses.
654 | But is everything that POSIX standardizes safe? Hint: POSIX is more about portability.
655 |
656 | These operators are ill-conceived because strings can evaluate to operators,
657 | and different operators take different numbers of operands,
658 | which together can lead to desynchronization.
659 | To prevent string content from changing the meaning of the test,
660 | the same standard prescribes that the number of arguments has the highest precedence:
661 | POSIX defines the unambiguous meaning of a test, from 0 to 4 arguments, but not more.
662 | These definitions do not include any conjunctions or disjunctions
663 | (likely because the unambiguity breaks down in the combinatorial explosion).
664 |
665 | Bad (unless your shell can unambiguously interpret a 13-argument test):
666 |
667 | test ! -e "$f" -a \( "$s" = yes -o "$s" = y \)
668 | [ ! -e "$f" -a \( "$s" = yes -o "$s" = y \) ]
669 |
670 | Good:
671 |
672 | ! test -e "$f" && { test "$s" = yes || test "$s" = y; }
673 | ! [ -e "$f" ] && { [ "$s" = yes ] || [ "$s" = y ]; }
674 |
675 | ### xyes deemed unnecessary
676 |
677 | Is this idiom of any use?
678 |
679 | test x"$s" = xyes
680 |
681 | 1. Not to preserve empty strings: Use quoting.
682 | 2. Not to prevent ambiguity in case the variable contains a valid operator: The number of arguments has highter precedence.
683 | 3. When used with the AND/OR operators (`-a/-o`), it can prevent ambiguity. However, this use is unnecessary (see above).
684 |
685 | Shellharden simplifies "xyes" conditions when the "x" part is unquoted.
686 |
687 | Commands with better alternatives
688 | ---------------------------------
689 |
690 | ### echo → printf
691 |
692 | As with any command, there must be a way to control its option parsing to prevent it from interpreting data as options.
693 | The standard way to signify the end of options is with a double-dash `--` argument.
694 |
695 | Significance of the double-dash `--` argument, explained in error messages:
696 |
697 | > shellharden --hlep
698 | --hlep: No such option
699 | > shellharden -- --hlep
700 | --hlep: No such file or directory
701 |
702 | As such, the GNU version of `echo` (both the bash builtin and `command echo`) is fatally flawed.
703 | Unlike the POSIX version, it takes options, yet it offers no way to suppress further option parsing.
704 | (Specifically, it interprets any number of leading arguments as options until the first argument that is not an option.)
705 |
706 | The result is that `echo` is not *generally* possible to use correctly.
707 | (It is safe as long as its first non-option character is provably not a dash – we can not just print anything unpredictable like a variable or command substitution; we must first print *some* literal character, that is not the dash, and *then* the unpredictable data!)
708 |
709 | In contrast, `printf` is always possible to use correctly (not saying it is easier)
710 | and can do a superset of `echo` (including its bashisms, just without bashisms).
711 |
712 | Bad:
713 |
714 | echo "$var"
715 | echo -n "$var"
716 | echo -en "$var\r"
717 |
718 | echo "$a" "$b"
719 | echo "${array[@]}"
720 |
721 | Good:
722 |
723 | printf '%s\n' "$var"
724 | printf '%s' "$var"
725 | printf '%s\r' "$var"
726 |
727 | printf '%s %s\n' "$a" "$b"
728 | printf '%s\n' "${array[*]}"
729 |
730 | At this point, it gets tempting to redefine `echo` to something sane,
731 | except that overloading existing functionality is generally not a robust and reassuring practice – it breaks unnoticeably.
732 | For verifiability's sake, better leave `echo` forever broken, and call yours something else:
733 |
734 | println() {
735 | printf '%s\n' "$*"
736 | }
737 |
738 | How to avoid invoking the shell with improper quoting
739 | -----------------------------------------------------
740 |
741 | When invoking a command from other programming languages, the wrong thing to do is often the easiest: implicitly invoking the shell. If that shell command is static, fine – either it works, or it doesn't. But if your program is doing any kind of string processing to assemble that command, realize that you are **generating a shellscript**! Rarely what you want, and tedious to do correctly:
742 |
743 | * quote each argument
744 | * escape relevant characters in the arguments
745 |
746 | No matter which programming language you are doing this from, there are at least 3 ways to construct the command correctly. In order of preferece:
747 |
748 | ### Plan A: Avoid the shell
749 |
750 | If it's just a command with arguments (i.e. no shell features like piping or redirection), choose the array representation.
751 |
752 | * Bad (python3): `subprocess.check_call('rm -rf ' + path)`
753 | * Good (python3): `subprocess.check_call(['rm', '-rf', path])`
754 |
755 | Bad (C++):
756 |
757 | std::string cmd = "rm -rf ";
758 | cmd += path;
759 | system(cmd);
760 |
761 | Good (C/POSIX), minus error handling:
762 |
763 | char* const args[] = {"rm", "-rf", path, NULL};
764 | pid_t child;
765 | posix_spawnp(&child, args[0], NULL, NULL, args, NULL);
766 | int status;
767 | waitpid(child, &status, 0);
768 |
769 | ### Plan B: Static shellscript
770 |
771 | If the shell is needed, let arguments be arguments. You might think this was cumbersome – writing a special-purpose shellscript to its own file and invoking that – until you have seen this trick:
772 |
773 | * Bad (python3): `subprocess.check_call('docker exec {} bash -ec "printf %s {} > {}"'.format(instance, content, path))`
774 | * Good (python3): `subprocess.check_call(['docker', 'exec', instance, 'bash', '-ec', 'printf %s "$0" > "$1"', content, path])`
775 |
776 | Can you spot the shellscript?
777 |
778 | That's right, the printf command with the redirection. Note the correctly quoted numbered arguments. Embedding a static shellscript is fine.
779 |
780 | The examples run in Docker because they wouldn't be as useful otherwise, but Docker is also a fine example of a command that runs other commands based on arguments. This is unlike Ssh, as we will see.
781 |
782 | ### Last option: String processing
783 |
784 | If it *has* to be a string (e.g. because it has to run over `ssh`), there is no way around it. We must quote each argument and escape whatever characters are necessary to escape within those quotes. The simplest is to go for single quotes, since these have the simplest escaping rules – only one: `'` → `'\''`.
785 |
786 | A very typical filename, in single quotes:
787 |
788 | echo 'Don'\''t stop (12" dub mix).mp3'
789 |
790 | Now, how to use this trick to run commands safely over ssh? It's impossible! Well, here is an "often correct" solution:
791 |
792 | * Often correct (python3): `subprocess.check_call(['ssh', 'user@host', "sha1sum '{}'".format(path.replace("'", "'\\''"))])`
793 |
794 | The reason we have to concatenate all the args to a string in the first place, is so that Ssh won't do it the wrong way for us: If you try to give multiple arguments to ssh, it will treacherously space-concatenate the arguments without quoting.
795 |
796 | The reason this is not generally possible is that the correct solution depends on user preference at the other end, namely the remote shell, which can be anything. It can be your mother, in principle. Assuming that the remote shell is bash or another POSIX compatible shell, the "often correct" will in fact be correct, but [fish is incompatible on this point](https://github.com/fish-shell/fish-shell/issues/4907).
797 |
798 | #### How to be Fish compatible
799 |
800 | This is only necessary if you are forced to interoperate with a user's favourite shell, such as when implementing [ssh-copy-id](https://github.com/fish-shell/fish-shell/issues/2292).
801 |
802 | The issue with supporting Fish is that the subset of common syntax with POSIX/Bash is mostly useless.
803 | The general approach is therefore to duplicate the code – obviously against any safety recommendation.
804 |
805 | But if you must, so be it:
806 |
807 | test '\'
808 |
809 | echo "This is POSIX!"
810 |
811 | test '
812 |
813 | echo "This is fish!"
814 |
815 | test \'
816 |
817 | [Jussi's two ways]: "If there is an easy way to do something, and another, correct way to do the same, programmers will always choose the easy way. As a corollary for language design, the correct thing to do must also be the easiest."
818 |
--------------------------------------------------------------------------------
/img/ex-artificial.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anordal/shellharden/29f400beb23d995c7d62dd85c63cc113a8c5f917/img/ex-artificial.png
--------------------------------------------------------------------------------
/img/ex-realworld.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anordal/shellharden/29f400beb23d995c7d62dd85c63cc113a8c5f917/img/ex-realworld.png
--------------------------------------------------------------------------------
/img/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/anordal/shellharden/29f400beb23d995c7d62dd85c63cc113a8c5f917/img/logo.png
--------------------------------------------------------------------------------
/moduletests/expected/arithmetic.bash:
--------------------------------------------------------------------------------
1 | a=3*4
2 | echo $((a)) $((a+2))
3 | ((++a))
4 | echo $((a*(4+4)))
5 |
6 | ver1=(0 9 9)
7 | ver2=(1 0 0)
8 | for ((i = 0; i < ${#ver1[@]}; i++)); do
9 | if ((10#${ver1[i]} > 10#${ver2[i]})); then
10 | break
11 | fi
12 | done
13 |
--------------------------------------------------------------------------------
/moduletests/expected/assoc.bash:
--------------------------------------------------------------------------------
1 |
2 | declare -A assoc
3 | assoc[$1]=$3
4 | assoc[$1]+=_1
5 | assoc[$2]=$3
6 | assoc[$2]+=_2
7 | echo "«${assoc[$1]}»"
8 | echo "«${assoc[$2]}»"
9 |
--------------------------------------------------------------------------------
/moduletests/expected/backtick.bash:
--------------------------------------------------------------------------------
1 | echo "$(echo -ne '\n')"
2 | echo "$(echo #`
3 | ls)" && ok
4 | echo "$(echo '`'ls)" && ok
5 | echo "$(echo "$(ls "$oddvar")")"
6 |
--------------------------------------------------------------------------------
/moduletests/expected/cmdsub.bash:
--------------------------------------------------------------------------------
1 | "$(echo "$oddvar")"
2 | "$(echo "$oddvar")"
3 |
--------------------------------------------------------------------------------
/moduletests/expected/control_structures_1.bash:
--------------------------------------------------------------------------------
1 | let ivar be the test pilot
2 |
3 | #
4 | # Ivar is in command position.
5 | #
6 | [[ $ivar ]] && [[ $ivar ]] || ! [[ $ivar ]]; [[ $ivar ]] & [[ $ivar ]] | [[ $ivar ]]
7 |
8 | if [[ $ivar ]]; then [[ $ivar ]]; elif [[ $ivar ]]; then [[ $ivar ]]; else [[ $ivar ]]; fi
9 | if [[ $ivar ]]
10 | then
11 | [[ $ivar ]]
12 | elif [[ $ivar ]]
13 | then
14 | [[ $ivar ]]
15 | else
16 | [[ $ivar ]]
17 | fi
18 |
19 | while [[ $ivar ]]; do [[ $ivar ]]; done
20 | while [[ $ivar ]]
21 | do
22 | [[ $ivar ]]
23 | done
24 |
25 | until [[ $ivar ]]; do [[ $ivar ]]; done
26 | until [[ $ivar ]]
27 | do
28 | [[ $ivar ]]
29 | done
30 |
31 | for i in {,}; do [[ $i ]]; done
32 | for i in {,}
33 | do
34 | [[ $i ]]
35 | done
36 |
37 | \
38 | oddvar=$ivar a b && \
39 | [[ $ivar ]]
40 | case $ivar in
41 | *)
42 | \
43 | oddvar=$ivar a b && \
44 | [[ $ivar ]]
45 | ;;
46 | esac
47 |
48 | true \
49 | # Comments don't have line continuations. \
50 | [[ $ivar ]]
51 |
52 | "$([[ $ivar ]])"
53 | <([[ $ivar ]])
54 | >([[ $ivar ]])
55 | ([[ $ivar ]])
56 | { [[ $ivar ]] } [[ $ivar ]]
57 | f()
58 | {
59 | [[ $ivar ]]
60 | }
61 | f(){ [[ $ivar ]] }
62 | function f(){ [[ $ivar ]] }
63 |
64 | oddvar="$(
65 | case [[ in
66 | # comment
67 | [[) # comment
68 | # comment
69 | [[ $ivar ]] # comment
70 | # comment
71 | ;; # comment
72 | # comment
73 | esac
74 | )here is where the string continues"
75 |
76 | case true$(true)true in
77 | true$(true)true)([[ $ivar ]]);;
78 | esac
79 | case true"$(true)"true in
80 | true"$(true)"true)([[ $ivar ]]);;
81 | esac
82 |
--------------------------------------------------------------------------------
/moduletests/expected/control_structures_2.bash:
--------------------------------------------------------------------------------
1 | #
2 | # Ivar is an argument.
3 | #
4 | true [[ "$ivar" ]] && true [[ "$ivar" ]] || true [[ "$ivar" ]]; true [[ "$ivar" ]] & true [[ "$ivar" ]] | true [[ "$ivar" ]]
5 | [ "$ivar" ] && [ "$ivar" ] || [ "$ivar" ]; [ "$ivar" ] & [ "$ivar" ] | [ "$ivar" ]
6 | test "$ivar" && test "$ivar" || test "$ivar"; test "$ivar" & test "$ivar" | test "$ivar"
7 |
8 | if true [[ "$ivar" ]]; then true [[ "$ivar" ]]; elif true [[ "$ivar" ]]; then true [[ "$ivar" ]]; else true [[ "$ivar" ]]; fi
9 | if true [[ "$ivar" ]]
10 | then
11 | true [[ "$ivar" ]]
12 | elif true [[ "$ivar" ]]
13 | then
14 | true [[ "$ivar" ]]
15 | else
16 | true [[ "$ivar" ]]
17 | fi
18 |
19 | echo line continuation \
20 | [[ "$ivar" ]]
21 |
22 | {[[ "$ivar" ]]}
23 | echo {} [[ "$ivar" ]]
24 |
25 | for i in [[ "$ivar" ]]; do :; done
26 | select i in [[ "$ivar" ]]; do break; done
27 |
28 | for i in
29 | do echo "$i"; done
30 |
31 | for i in "${pseudoarray[@]}"
32 | do echo "$i"; done
33 |
34 | for i in except "$this"
35 | do echo "$i"; done
36 |
37 | for i in "$or" this
38 | do echo "$i"; done
39 |
40 | for i in "$(seq 1 3)"
41 | do echo "$i"; done
42 |
43 | array=(
44 | [[ "$ivar" ]]
45 | )
46 | array+=(
47 | [[ "$ivar" ]]
48 | )
49 |
50 | # Filedescriptor redirection: The code does not make sense,
51 | # but tests that the succeeding expression is not a command,
52 | # which is all shellharden needs to know for now.
53 | : >&[[ "$ivar" ]]
54 | : 1>&[[ "$ivar" ]]
55 | : >& [[ "$ivar" ]]
56 | : 1>& [[ "$ivar" ]]
57 |
--------------------------------------------------------------------------------
/moduletests/expected/eof_after_command.bash:
--------------------------------------------------------------------------------
1 | w
--------------------------------------------------------------------------------
/moduletests/expected/eof_after_rvalue.bash:
--------------------------------------------------------------------------------
1 | v=
--------------------------------------------------------------------------------
/moduletests/expected/error_unexpected_eof_arith.bash:
--------------------------------------------------------------------------------
1 | $((
2 |
3 | moduletests/original/error_unexpected_eof_arith.bash: Unexpected end of file
4 | The file's end was reached without closing all sytactic scopes.
5 | Either, the parser got lost, or the file is truncated or malformed.
6 |
--------------------------------------------------------------------------------
/moduletests/expected/error_unexpected_eof_doublebracket.bash:
--------------------------------------------------------------------------------
1 | [[
2 |
3 | moduletests/original/error_unexpected_eof_doublebracket.bash: Unexpected end of file
4 | The file's end was reached without closing all sytactic scopes.
5 | Either, the parser got lost, or the file is truncated or malformed.
6 |
--------------------------------------------------------------------------------
/moduletests/expected/error_unexpected_eof_esc.bash:
--------------------------------------------------------------------------------
1 | \
2 | moduletests/original/error_unexpected_eof_esc.bash: Unexpected end of file
3 | The file's end was reached without closing all sytactic scopes.
4 | Either, the parser got lost, or the file is truncated or malformed.
5 |
--------------------------------------------------------------------------------
/moduletests/expected/error_unexpected_eof_heredoc.bash:
--------------------------------------------------------------------------------
1 | cat <"/dev/null"
57 | echo "$a"<"/dev/null"
58 |
--------------------------------------------------------------------------------
/moduletests/expected/premature_esac.bash:
--------------------------------------------------------------------------------
1 |
2 | rustup() {
3 | case $(uname -m) in
4 | *)
5 | false
6 | ;; esac
7 | }
8 | case i in *)case i in *);;esac;;esac
9 |
--------------------------------------------------------------------------------
/moduletests/expected/preserve_syntaxerror_emptyvar.bash:
--------------------------------------------------------------------------------
1 | "${}"
2 | "${}"?
3 | "${}"
4 | "${}?"
5 |
--------------------------------------------------------------------------------
/moduletests/expected/pwd.bash:
--------------------------------------------------------------------------------
1 | # implemented
2 | echo "$PWD."
3 | echo "$PWD."
4 | echo "${PWD}a"
5 | echo "${PWD}a"
6 |
7 | echo "$PWD."
8 | echo "$PWD."
9 | echo "${PWD}a"
10 | echo "${PWD}a"
11 |
12 | echo "$1$PWD."
13 | echo "$1$PWD."
14 | echo "$1${PWD}a"
15 | echo "$1${PWD}a"
16 |
17 | # not optimally handled
18 | echo "$PWD".
19 | echo "$PWD".
20 | echo "${PWD}"a
21 | echo "${PWD}"a
22 |
23 | echo "$1$PWD".
24 | echo "$1$PWD".
25 | echo "$1${PWD}"a
26 | echo "$1${PWD}"a
27 |
28 | # not implemented
29 | echo "$( pwd)"
30 | echo "$(pwd )"
31 |
--------------------------------------------------------------------------------
/moduletests/expected/quoting_unneeded.bash:
--------------------------------------------------------------------------------
1 | # The few places where omitting quotes is ok
2 |
3 | # Not that I like exceptions, but legal is legal.
4 | # See "Where you can omit the double quotes":
5 | # https://unix.stackexchange.com/a/68748
6 |
7 | # Assignments
8 | asterisk=$(echo '*')
9 | spacestar=$IFS
10 | spacestar+=$asterisk
11 | a=(a b)
12 | b=${a[@]}
13 | c=$*
14 | pwd=`pwd`hazard
15 |
16 | # In the case expression
17 | case $spacestar in
18 | $' \t\n*')
19 | echo pass
20 | ;;
21 | *)
22 | echo fail
23 | ;;
24 | esac
25 | case $(printf ' \t\n*') in
26 | $' \t\n*')
27 | echo pass
28 | ;;
29 | *)
30 | echo fail
31 | ;;
32 | esac
33 |
34 | # Case arms
35 | case $' \t\n*' in
36 | $spacestar)
37 | echo pass
38 | ;;
39 | *)
40 | echo fail
41 | ;;
42 | esac
43 | case $' \t\n*' in
44 | $(printf ' \t\n*'))
45 | echo pass
46 | ;;
47 | *)
48 | echo fail
49 | ;;
50 | esac
51 |
52 | # Double brackets
53 | if [[ ${a[@]} == ${b[@]} ]]; then
54 | echo pass
55 | else
56 | echo fail
57 | fi
58 |
59 | # Numeric content
60 | echo $? + $# - ${#a[@]} = $(($?+$#-${#a[@]}))
61 |
62 | # Let's allow backticks where they don't hurt
63 | a=`uname -a`
64 |
65 | # Counterexamples
66 | pwd=$PWD; case $PWD in esac
67 | pwd=$PWD
68 | pwd+=$PWD
69 | files=("$(ls)")
70 | files+=("$(ls)")
71 |
--------------------------------------------------------------------------------
/moduletests/expected/stresc.bash:
--------------------------------------------------------------------------------
1 | echo e$''e$'\n'e$'k\nk'e
2 | echo "e$''e$'\n'e$'k\nk'e"
3 |
--------------------------------------------------------------------------------
/moduletests/expected/test.bash:
--------------------------------------------------------------------------------
1 |
2 | test "$(test "$1" = "")" = ""
3 | test "$(test "$1" = "")" = ""
4 | [ "$([ "$1" = "" ])" = "" ]
5 | [ "$([ "$1" = "" ])" = "" ]
6 |
7 | # NB: Unquoted `test -n` is always true.
8 | test "$(test "$1" != "")" != ""
9 | test "$(test "$1" != "")" != ""
10 | [ "$([ "$1" != "" ])" != "" ]
11 | [ "$([ "$1" != "" ])" != "" ]
12 |
13 | # xyes
14 | test "$([ "$a$b" = "" ])$b" = yes
15 | test "$([ "$a$b" != "" ])$b" != ''
16 | test "$([ "$a$b" == "" ])$b" == ""
17 | test x"$([ x"$a$b" == "" ])$b" == ex
18 |
--------------------------------------------------------------------------------
/moduletests/expected/unsupp_numeral_variable_quot.bash:
--------------------------------------------------------------------------------
1 | echo above
2 | echo beyond
3 | echo "
4 | moduletests/original/unsupp_numeral_variable_quot.bash: Unsupported syntax: Syntactic pitfall
5 | echo "$10" ()
6 | ^^^
7 | This does not mean what it looks like. You may be forgiven to think that the full string of numerals is the variable name. Only the fist is.
8 |
9 | Try this and be shocked: f() { echo "$9" "$10"; }; f a b c d e f g h i j
10 |
11 | Here is where braces should be used to disambiguate, e.g. "${10}" vs "${1}0".
12 |
13 | Syntactic pitfalls are deemed too dangerous to fix automatically
14 | (the purpose of Shellharden is to fix vulnerable code – code that mostly does what it looks like, as opposed to code that never does what it looks like):
15 | * Fixing what it does would be 100% subtle and might slip through code review unnoticed.
16 | * Fixing its look would make a likely bug look intentional.
17 |
--------------------------------------------------------------------------------
/moduletests/expected/unsupp_numeral_variable_unquot.bash:
--------------------------------------------------------------------------------
1 | echo above
2 | echo beyond
3 | echo
4 | moduletests/original/unsupp_numeral_variable_unquot.bash: Unsupported syntax: Syntactic pitfall
5 | echo $10 ()
6 | ^^^
7 | This does not mean what it looks like. You may be forgiven to think that the full string of numerals is the variable name. Only the fist is.
8 |
9 | Try this and be shocked: f() { echo "$9" "$10"; }; f a b c d e f g h i j
10 |
11 | Here is where braces should be used to disambiguate, e.g. "${10}" vs "${1}0".
12 |
13 | Syntactic pitfalls are deemed too dangerous to fix automatically
14 | (the purpose of Shellharden is to fix vulnerable code – code that mostly does what it looks like, as opposed to code that never does what it looks like):
15 | * Fixing what it does would be 100% subtle and might slip through code review unnoticed.
16 | * Fixing its look would make a likely bug look intentional.
17 |
--------------------------------------------------------------------------------
/moduletests/expected/var.bash:
--------------------------------------------------------------------------------
1 | echo "$identifier_azAZ09"
2 | echo "$Identifier_azAZ09"
3 | echo "$_identifier_azAZ09"
4 | echo "$0"
5 | echo "$1"
6 | echo "$2"
7 | echo "$3"
8 | echo "$4"
9 | echo "$5"
10 | echo "$6"
11 | echo "$7"
12 | echo "$8"
13 | echo "$9"
14 | echo "$@"
15 | echo "$@"
16 | echo "$-"
17 |
18 | echo "$identifier_azAZ09"
19 | echo "$Identifier_azAZ09"
20 | echo "$_identifier_azAZ09"
21 | echo "${identifier_azAZ09}"a
22 | echo "${Identifier_azAZ09}"a
23 | echo "${_identifier_azAZ09}"a
24 | echo "${identifier_azAZ09}a"
25 | echo "${Identifier_azAZ09}a"
26 | echo "${_identifier_azAZ09}a"
27 | echo "$identifier_azAZ09/a"
28 | echo "$Identifier_azAZ09/a"
29 | echo "$_identifier_azAZ09/a"
30 | echo "$identifier_azAZ09$identifier_azAZ09"
31 | echo "$Identifier_azAZ09$Identifier_azAZ09"
32 | echo "$_identifier_azAZ09$_identifier_azAZ09"
33 | echo "${0}"
34 | echo "${1}"
35 | echo "${2}"
36 | echo "${3}"
37 | echo "${4}"
38 | echo "${5}"
39 | echo "${6}"
40 | echo "${7}"
41 | echo "${8}"
42 | echo "${9}"
43 | echo "${1}"0
44 | echo "${10}"
45 | echo "${@}"
46 | echo "${-}"
47 |
48 | echo "${array[0]}"
49 | echo "${array[@]}"
50 | echo "${array[@]}"
51 | echo "${subst##*/}"
52 | echo "${subst#*/}"
53 | echo "${subst%/*}"
54 | echo "${subst%%/*}"
55 |
56 | echo "$identifier_azAZ09"
57 | echo "$Identifier_azAZ09"
58 | echo "$_identifier_azAZ09"
59 |
--------------------------------------------------------------------------------
/moduletests/expected/var_unchanged.bash:
--------------------------------------------------------------------------------
1 | # Expands to a number
2 | echo $#
3 | echo $?
4 | echo $$
5 | echo $!
6 | echo ${#}
7 | echo ${?}
8 | echo ${$}
9 | echo ${!}
10 | echo ${#array[@]}
11 |
12 | echo "$identifier_azAZ09"
13 | echo "$Identifier_azAZ09"
14 | echo "$_identifier_azAZ09"
15 | echo "$0"
16 | echo "$1"
17 | echo "$2"
18 | echo "$3"
19 | echo "$4"
20 | echo "$5"
21 | echo "$6"
22 | echo "$7"
23 | echo "$8"
24 | echo "$9"
25 | echo "$@"
26 | echo "$*"
27 | echo "$-"
28 | echo "$#"
29 | echo "$?"
30 | echo "$$"
31 | echo "$!"
32 |
33 | echo " ${identifier_azAZ09}"
34 | echo " ${Identifier_azAZ09}"
35 | echo " ${_identifier_azAZ09}"
36 | echo "${identifier_azAZ09} "
37 | echo "${Identifier_azAZ09} "
38 | echo "${_identifier_azAZ09} "
39 | echo "${identifier_azAZ09}${identifier_azAZ09}"
40 | echo "${Identifier_azAZ09}${Identifier_azAZ09}"
41 | echo "${_identifier_azAZ09}${_identifier_azAZ09}"
42 | echo "${0}"
43 | echo "${1}"
44 | echo "${2}"
45 | echo "${3}"
46 | echo "${4}"
47 | echo "${5}"
48 | echo "${6}"
49 | echo "${7}"
50 | echo "${8}"
51 | echo "${9}"
52 | echo "${1}0"
53 | echo "${10}"
54 | echo "${@}"
55 | echo "${*}"
56 | echo "${-}"
57 | echo "${#}"
58 | echo "${?}"
59 | echo "${$}"
60 | echo "${!}"
61 |
62 | echo "${#array[@]}"
63 | echo "${array[0]}"
64 | echo "${array[@]}"
65 | echo "${array[*]}"
66 | echo "${subst##*/}"
67 | echo "${subst#*/}"
68 | echo "${subst%/*}"
69 | echo "${subst%%/*}"
70 |
71 | option2='abc[<{().[]def[<{().[]ghi'
72 | option2=${option2%%[<{().[]*}
73 | test "$option2" = abc && echo yes || echo no
74 |
75 | option2='abc[<{().[]def[<{().[]ghi'
76 | rm='[<{().[]'
77 | option2=${option2%%${rm}*}
78 | test "$option2" = abc && echo yes || echo no
79 |
80 |
--------------------------------------------------------------------------------
/moduletests/original/backtick.bash:
--------------------------------------------------------------------------------
1 | echo `echo -ne '\n'`
2 | echo `echo #`
3 | ls` && ok
4 | echo `echo '`'ls` && ok
5 | echo `echo "`ls "$oddvar"`"`
6 |
--------------------------------------------------------------------------------
/moduletests/original/cmdsub.bash:
--------------------------------------------------------------------------------
1 | `echo $oddvar`
2 | $(echo $oddvar)
3 |
--------------------------------------------------------------------------------
/moduletests/original/control_structures_2.bash:
--------------------------------------------------------------------------------
1 | #
2 | # Ivar is an argument.
3 | #
4 | true [[ $ivar ]] && true [[ $ivar ]] || true [[ $ivar ]]; true [[ $ivar ]] & true [[ $ivar ]] | true [[ $ivar ]]
5 | [ $ivar ] && [ $ivar ] || [ $ivar ]; [ $ivar ] & [ $ivar ] | [ $ivar ]
6 | test $ivar && test $ivar || test $ivar; test $ivar & test $ivar | test $ivar
7 |
8 | if true [[ $ivar ]]; then true [[ $ivar ]]; elif true [[ $ivar ]]; then true [[ $ivar ]]; else true [[ $ivar ]]; fi
9 | if true [[ $ivar ]]
10 | then
11 | true [[ $ivar ]]
12 | elif true [[ $ivar ]]
13 | then
14 | true [[ $ivar ]]
15 | else
16 | true [[ $ivar ]]
17 | fi
18 |
19 | echo line continuation \
20 | [[ $ivar ]]
21 |
22 | {[[ $ivar ]]}
23 | echo {} [[ $ivar ]]
24 |
25 | for i in [[ $ivar ]]; do :; done
26 | select i in [[ $ivar ]]; do break; done
27 |
28 | for i in
29 | do echo $i; done
30 |
31 | for i in $pseudoarray
32 | do echo $i; done
33 |
34 | for i in except $this
35 | do echo $i; done
36 |
37 | for i in $or this
38 | do echo $i; done
39 |
40 | for i in `seq 1 3`
41 | do echo $i; done
42 |
43 | array=(
44 | [[ $ivar ]]
45 | )
46 | array+=(
47 | [[ $ivar ]]
48 | )
49 |
50 | # Filedescriptor redirection: The code does not make sense,
51 | # but tests that the succeeding expression is not a command,
52 | # which is all shellharden needs to know for now.
53 | : >&[[ $ivar ]]
54 | : 1>&[[ $ivar ]]
55 | : >& [[ $ivar ]]
56 | : 1>& [[ $ivar ]]
57 |
--------------------------------------------------------------------------------
/moduletests/original/error_unexpected_eof_arith.bash:
--------------------------------------------------------------------------------
1 | $((
2 |
--------------------------------------------------------------------------------
/moduletests/original/error_unexpected_eof_doublebracket.bash:
--------------------------------------------------------------------------------
1 | [[
2 |
--------------------------------------------------------------------------------
/moduletests/original/error_unexpected_eof_esc.bash:
--------------------------------------------------------------------------------
1 | \
--------------------------------------------------------------------------------
/moduletests/original/error_unexpected_eof_heredoc.bash:
--------------------------------------------------------------------------------
1 | cat <"/dev/null"
57 | echo $a<"/dev/null"
58 |
--------------------------------------------------------------------------------
/moduletests/original/premature_esac.bash:
--------------------------------------------------------------------------------
1 |
2 | rustup() {
3 | case $(uname -m) in
4 | *)
5 | false
6 | esac
7 | }
8 | case i in *)case i in *);;esac;;esac
9 |
--------------------------------------------------------------------------------
/moduletests/original/preserve_syntaxerror_emptyvar.bash:
--------------------------------------------------------------------------------
1 | ${}
2 | ${}?
3 | "${}"
4 | "${}?"
5 |
--------------------------------------------------------------------------------
/moduletests/original/pwd.bash:
--------------------------------------------------------------------------------
1 | # implemented
2 | echo "$(pwd)."
3 | echo "`pwd`."
4 | echo "$(pwd)a"
5 | echo "`pwd`a"
6 |
7 | echo $(pwd)"."
8 | echo `pwd`"."
9 | echo $(pwd)"a"
10 | echo `pwd`"a"
11 |
12 | echo $1$(pwd)"."
13 | echo $1`pwd`"."
14 | echo $1$(pwd)"a"
15 | echo $1`pwd`"a"
16 |
17 | # not optimally handled
18 | echo $(pwd).
19 | echo `pwd`.
20 | echo $(pwd)a
21 | echo `pwd`a
22 |
23 | echo $1$(pwd).
24 | echo $1`pwd`.
25 | echo $1$(pwd)a
26 | echo $1`pwd`a
27 |
28 | # not implemented
29 | echo "$( pwd)"
30 | echo "$(pwd )"
31 |
--------------------------------------------------------------------------------
/moduletests/original/quoting_unneeded.bash:
--------------------------------------------------------------------------------
1 | # The few places where omitting quotes is ok
2 |
3 | # Not that I like exceptions, but legal is legal.
4 | # See "Where you can omit the double quotes":
5 | # https://unix.stackexchange.com/a/68748
6 |
7 | # Assignments
8 | asterisk=$(echo '*')
9 | spacestar=$IFS
10 | spacestar+=$asterisk
11 | a=(a b)
12 | b=${a[@]}
13 | c=$*
14 | pwd=`pwd`hazard
15 |
16 | # In the case expression
17 | case $spacestar in
18 | $' \t\n*')
19 | echo pass
20 | ;;
21 | *)
22 | echo fail
23 | ;;
24 | esac
25 | case $(printf ' \t\n*') in
26 | $' \t\n*')
27 | echo pass
28 | ;;
29 | *)
30 | echo fail
31 | ;;
32 | esac
33 |
34 | # Case arms
35 | case $' \t\n*' in
36 | $spacestar)
37 | echo pass
38 | ;;
39 | *)
40 | echo fail
41 | ;;
42 | esac
43 | case $' \t\n*' in
44 | $(printf ' \t\n*'))
45 | echo pass
46 | ;;
47 | *)
48 | echo fail
49 | ;;
50 | esac
51 |
52 | # Double brackets
53 | if [[ ${a[@]} == ${b[@]} ]]; then
54 | echo pass
55 | else
56 | echo fail
57 | fi
58 |
59 | # Numeric content
60 | echo $? + $# - ${#a[@]} = $(($?+$#-${#a[@]}))
61 |
62 | # Let's allow backticks where they don't hurt
63 | a=`uname -a`
64 |
65 | # Counterexamples
66 | pwd=`pwd`; case `pwd` in esac
67 | pwd=$(pwd)
68 | pwd+=$(pwd)
69 | files=($(ls))
70 | files+=($(ls))
71 |
--------------------------------------------------------------------------------
/moduletests/original/test.bash:
--------------------------------------------------------------------------------
1 |
2 | test -z `test -z $1`
3 | test -z "$(test -z "$1")"
4 | [ -z `[ -z $1 ]` ]
5 | [ -z "$([ -z "$1" ])" ]
6 |
7 | # NB: Unquoted `test -n` is always true.
8 | test -n `test -n $1`
9 | test -n "$(test -n "$1")"
10 | [ -n `[ -n $1 ]` ]
11 | [ -n "$([ -n "$1" ])" ]
12 |
13 | # xyes
14 | test x$([ x"$a$b" = x"" ])$b = xyes
15 | test x$([ x"$a$b" != x"" ])$b != x''
16 | test x$([ x"$a$b" == x"" ])$b == x
17 | test x$([ x"$a$b" == "" ])$b == ex
18 |
--------------------------------------------------------------------------------
/moduletests/original/unsupp_numeral_variable_quot.bash:
--------------------------------------------------------------------------------
1 | echo above
2 | echo beyond
3 | echo "$10" ()
4 |
--------------------------------------------------------------------------------
/moduletests/original/unsupp_numeral_variable_unquot.bash:
--------------------------------------------------------------------------------
1 | echo above
2 | echo beyond
3 | echo $10 ()
4 |
--------------------------------------------------------------------------------
/moduletests/original/var.bash:
--------------------------------------------------------------------------------
1 | echo $identifier_azAZ09
2 | echo $Identifier_azAZ09
3 | echo $_identifier_azAZ09
4 | echo $0
5 | echo $1
6 | echo $2
7 | echo $3
8 | echo $4
9 | echo $5
10 | echo $6
11 | echo $7
12 | echo $8
13 | echo $9
14 | echo $@
15 | echo $*
16 | echo $-
17 |
18 | echo ${identifier_azAZ09}
19 | echo ${Identifier_azAZ09}
20 | echo ${_identifier_azAZ09}
21 | echo ${identifier_azAZ09}a
22 | echo ${Identifier_azAZ09}a
23 | echo ${_identifier_azAZ09}a
24 | echo ${identifier_azAZ09}"a"
25 | echo ${Identifier_azAZ09}"a"
26 | echo ${_identifier_azAZ09}"a"
27 | echo ${identifier_azAZ09}"/a"
28 | echo ${Identifier_azAZ09}"/a"
29 | echo ${_identifier_azAZ09}"/a"
30 | echo ${identifier_azAZ09}${identifier_azAZ09}
31 | echo ${Identifier_azAZ09}${Identifier_azAZ09}
32 | echo ${_identifier_azAZ09}${_identifier_azAZ09}
33 | echo ${0}
34 | echo ${1}
35 | echo ${2}
36 | echo ${3}
37 | echo ${4}
38 | echo ${5}
39 | echo ${6}
40 | echo ${7}
41 | echo ${8}
42 | echo ${9}
43 | echo ${1}0
44 | echo ${10}
45 | echo ${@}
46 | echo ${-}
47 |
48 | echo ${array[0]}
49 | echo ${array[@]}
50 | echo ${array[*]}
51 | echo ${subst##*/}
52 | echo ${subst#*/}
53 | echo ${subst%/*}
54 | echo ${subst%%/*}
55 |
56 | echo "${identifier_azAZ09}"
57 | echo "${Identifier_azAZ09}"
58 | echo "${_identifier_azAZ09}"
59 |
--------------------------------------------------------------------------------
/moduletests/run:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | if test $# -ne 2; then
5 | echo "Usage: $0 target/debug/shellharden moduletests/"
6 | exit 1
7 | fi
8 | exe="$1"
9 | dir="$2"
10 |
11 | compare(){
12 | local original=$1
13 | local expected=$2
14 | if diff=$(diff -u -- "$expected" <("$exe" --transform -- "$original" 2>&1)); then
15 | return 0
16 | fi
17 | printf '\n——— \e[1m%s\e[m ———\n%s\n' "$original" "$diff"
18 | return 1
19 | }
20 |
21 | check(){
22 | local file=$1
23 | local expect_status=$2
24 | status=0
25 | if output=$("$exe" --check "$file"); then
26 | true
27 | else
28 | status=$?
29 | fi
30 | if test "$status" -ne "$expect_status"; then
31 | output+="Expecting --check to return $expect_status, got $status"
32 | fi
33 | if test "$output" = ""; then
34 | return 0
35 | fi
36 | printf '\n——— --check \e[1m%s\e[m ———\n%s\n' "$file" "$output"
37 | return 1
38 | }
39 |
40 | pass=()
41 | fail=()
42 |
43 | for i in "${dir%/}"/original/*; do
44 | if compare "$i" "${i%/original/*}/expected/${i##*/}" && check "$i" 2; then
45 | pass+=("$i")
46 | else
47 | fail+=("$i")
48 | fi
49 | done
50 |
51 | for i in "${dir%/}"/expected/* "$0"; do
52 | case ${i##*/} in
53 | error_*|unsupp_*)
54 | continue
55 | ;;
56 | esac
57 | if compare "$i" "$i" && check "$i" 0; then
58 | pass+=("$i")
59 | else
60 | fail+=("$i")
61 | fi
62 | done
63 |
64 | echo
65 | echo Passes:
66 | printf '\t\e[32m%s\e[m\n' "${pass[@]}"
67 |
68 | echo
69 | echo Fails:
70 | printf '\t\e[31m%s\e[m\n' "${fail[@]}"
71 |
72 | exit ${#fail[@]}
73 |
--------------------------------------------------------------------------------
/src/commonargcmd.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::WhatNow;
11 | use crate::situation::flush;
12 | use crate::situation::if_needed;
13 | use crate::situation::pop;
14 | use crate::situation::push;
15 | use crate::situation::COLOR_HERE;
16 | use crate::situation::COLOR_KWD;
17 | use crate::situation::COLOR_SQ;
18 | use crate::situation::COLOR_VAR;
19 | use crate::situation::COLOR_ESC;
20 |
21 | use crate::microparsers::prefixlen;
22 | use crate::microparsers::predlen;
23 | use crate::microparsers::identifierlen;
24 | use crate::microparsers::is_whitespace;
25 | use crate::microparsers::is_word;
26 |
27 | use crate::commonstrcmd::QuotingCtx;
28 | use crate::commonstrcmd::CommonStrCmdResult;
29 | use crate::commonstrcmd::common_str_cmd;
30 |
31 | use crate::sitcase::SitCase;
32 | use crate::sitfor::SitFor;
33 | use crate::sitcmd::SitNormal;
34 | use crate::sitcmd::SitCmd;
35 | use crate::sitcomment::SitComment;
36 | use crate::sitextent::push_extent;
37 | use crate::sitextent::push_replaceable;
38 | use crate::sitmagic::push_magic;
39 | use crate::sitrvalue::SitLvalue;
40 | use crate::sitstrdq::SitStrDq;
41 | use crate::sitstrphantom::SitStrPhantom;
42 | use crate::sitstrsqesc::SitStrSqEsc;
43 | use crate::sittest::SitTest;
44 | use crate::situntilbyte::SitUntilByte;
45 | use crate::sitvec::SitVec;
46 |
47 | pub fn keyword_or_command(
48 | end_trigger :u16,
49 | horizon: Horizon,
50 | i: usize,
51 | ) -> WhatNow {
52 | if horizon.input[i] == b'#' {
53 | return push_comment(i);
54 | }
55 | if horizon.input[i] == b'\\' {
56 | return push_extent(COLOR_ESC, i, 2);
57 | }
58 | let (found, len) = find_lvalue(&horizon.input[i..]);
59 | if found == Tri::Maybe && (i > 0 || horizon.is_lengthenable) {
60 | return flush(i);
61 | }
62 | if found == Tri::Yes {
63 | return push((i, 0, None), Box::new(SitLvalue { len, end_trigger }));
64 | }
65 | let len = predlen(is_word, &horizon.input[i..]);
66 | let len = if len != 0 { len } else { prefixlen(&horizon.input[i..], b"((") };
67 | if i + len == horizon.input.len() && (i > 0 || horizon.is_lengthenable) {
68 | return flush(i);
69 | }
70 | let word = &horizon.input[i..i+len];
71 | match word {
72 | b"(" => push(
73 | (i, 1, None),
74 | Box::new(SitNormal {
75 | end_trigger: u16::from(b')'),
76 | end_replace: None,
77 | }),
78 | ),
79 | b"((" => push_magic(i, 1, b')'),
80 | b"[[" => push_magic(i, 1, b']'),
81 | b"case" => push((i, len, None), Box::new(SitCase {})),
82 | b"for" |
83 | b"select" => push((i, len, None), Box::new(SitFor {})),
84 | b"!" |
85 | b"declare" |
86 | b"do" |
87 | b"done" |
88 | b"elif" |
89 | b"else" |
90 | b"export" |
91 | b"fi" |
92 | b"function" |
93 | b"if" |
94 | b"local" |
95 | b"readonly" |
96 | b"then" |
97 | b"until" |
98 | b"while" |
99 | b"{" |
100 | b"}" => push_extent(COLOR_KWD, i, len),
101 | b"[" |
102 | b"test" if predlen(|x| x == b' ', &horizon.input[i + len ..]) == 1 => {
103 | push((i, len + 1, None), Box::new(SitTest { end_trigger }))
104 | },
105 | _ => push((i, 0, None), Box::new(SitCmd { end_trigger })),
106 | }
107 | }
108 |
109 | pub fn common_arg(
110 | end_trigger :u16,
111 | horizon :Horizon,
112 | i :usize,
113 | ) -> Option {
114 | if let Some(res) = find_command_enders(horizon, i) {
115 | return Some(res);
116 | }
117 | common_expr(end_trigger, horizon, i)
118 | }
119 |
120 | pub fn common_cmd(
121 | end_trigger :u16,
122 | horizon :Horizon,
123 | i :usize,
124 | ) -> Option {
125 | if let Some(res) = find_command_enders(horizon, i) {
126 | return Some(res);
127 | }
128 | common_token(end_trigger, horizon, i)
129 | }
130 |
131 | pub fn common_expr(
132 | end_trigger :u16,
133 | horizon :Horizon,
134 | i :usize,
135 | ) -> Option {
136 | if horizon.input[i] == b'#' {
137 | return Some(push_comment(i));
138 | }
139 | common_token(end_trigger, horizon, i)
140 | }
141 |
142 | pub fn common_token(
143 | end_trigger :u16,
144 | horizon :Horizon,
145 | i :usize,
146 | ) -> Option {
147 | if let Some(res) = find_usual_suspects(end_trigger, horizon, i, true) {
148 | return Some(res);
149 | }
150 | match common_str_cmd(horizon, i, QuotingCtx::Need) {
151 | CommonStrCmdResult::None => None,
152 | CommonStrCmdResult::Some(x) => Some(x),
153 | CommonStrCmdResult::OnlyWithQuotes(_) => Some(push(
154 | (i, 0, Some(b"\"")),
155 | Box::new(SitStrPhantom {
156 | cmd_end_trigger: end_trigger,
157 | }),
158 | )),
159 | }
160 | }
161 |
162 | pub fn common_cmd_quoting_unneeded(
163 | end_trigger :u16,
164 | horizon :Horizon,
165 | i :usize,
166 | ) -> Option {
167 | if let Some(res) = find_command_enders(horizon, i) {
168 | return Some(res);
169 | }
170 | common_token_quoting_unneeded(end_trigger, horizon, i)
171 | }
172 |
173 | pub fn common_expr_quoting_unneeded(
174 | end_trigger :u16,
175 | horizon :Horizon,
176 | i :usize,
177 | ) -> Option {
178 | if horizon.input[i] == b'#' {
179 | return Some(push_comment(i));
180 | }
181 | common_token_quoting_unneeded(end_trigger, horizon, i)
182 | }
183 |
184 | pub fn common_token_quoting_unneeded(
185 | end_trigger :u16,
186 | horizon :Horizon,
187 | i :usize,
188 | ) -> Option {
189 | if let Some(res) = find_usual_suspects(end_trigger, horizon, i, false) {
190 | return Some(res);
191 | }
192 | match common_str_cmd(horizon, i, QuotingCtx::Dontneed) {
193 | CommonStrCmdResult::None => None,
194 | CommonStrCmdResult::Some(x) => Some(x),
195 | CommonStrCmdResult::OnlyWithQuotes(x) => {
196 | let (_, len, alt) = x.transform;
197 | if let Some(replacement) = alt {
198 | if replacement.len() >= len {
199 | #[allow(clippy::collapsible_if)] // Could be expanded.
200 | if horizon.input[i] == b'`' {
201 | return Some(push(
202 | (i, 1, None),
203 | Box::new(SitNormal {
204 | end_trigger: u16::from(b'`'),
205 | end_replace: None,
206 | }),
207 | ));
208 | }
209 | }
210 | }
211 | Some(x)
212 | }
213 | }
214 | }
215 |
216 | // Does not pop on eof → Callers must use flush_or_pop
217 | fn find_command_enders(
218 | horizon :Horizon,
219 | i :usize,
220 | ) -> Option {
221 | let plen = prefixlen(&horizon.input[i..], b">&");
222 | if plen == 2 {
223 | return Some(flush(i + 2));
224 | }
225 | if i + plen == horizon.input.len() && (i > 0 || horizon.is_lengthenable) {
226 | return Some(flush(i));
227 | }
228 | let a = horizon.input[i];
229 | if a == b'\n' || a == b';' || a == b'|' || a == b'&' {
230 | return Some(pop(i, 0, None));
231 | }
232 | None
233 | }
234 |
235 | fn find_usual_suspects(
236 | end_trigger :u16,
237 | horizon :Horizon,
238 | i :usize,
239 | quoting_needed : bool,
240 | ) -> Option {
241 | let a = horizon.input[i];
242 | if u16::from(a) == end_trigger {
243 | return Some(pop(i, 0, None));
244 | }
245 | if a == b'\'' {
246 | return Some(push(
247 | (i, 1, None),
248 | Box::new(SitUntilByte {
249 | until: b'\'',
250 | color: COLOR_SQ,
251 | }),
252 | ));
253 | }
254 | if a == b'\"' {
255 | return Some(push((i, 1, None), Box::new(SitStrDq::new())));
256 | }
257 | if a == b'$' {
258 | if i+1 >= horizon.input.len() {
259 | if i > 0 || horizon.is_lengthenable {
260 | return Some(flush(i));
261 | }
262 | return None;
263 | }
264 | let b = horizon.input[i+1];
265 | if b == b'\'' {
266 | return Some(push((i, 2, None), Box::new(SitStrSqEsc {})));
267 | } else if b == b'*' {
268 | // $* → "$@" but not "$*" → "$@"
269 | return Some(push_replaceable(COLOR_VAR, i, 2, if_needed(quoting_needed, b"\"$@\"")));
270 | }
271 | }
272 | let (ate, delimiter) = find_heredoc(&horizon.input[i ..]);
273 | if i + ate == horizon.input.len() {
274 | if i > 0 || horizon.is_lengthenable {
275 | return Some(flush(i));
276 | }
277 | } else if !delimiter.is_empty() {
278 | return Some(push(
279 | (i, ate, None),
280 | Box::new(SitVec {
281 | terminator: delimiter,
282 | color: COLOR_HERE,
283 | }),
284 | ));
285 | } else if ate > 0 {
286 | return Some(flush(i + ate));
287 | }
288 | None
289 | }
290 |
291 | fn push_comment(pre: usize) -> WhatNow {
292 | push((pre, 1, None), Box::new(SitComment {}))
293 | }
294 |
295 | #[derive(PartialEq)]
296 | #[derive(Clone)]
297 | #[derive(Copy)]
298 | pub enum Tri {
299 | No,
300 | Maybe,
301 | Yes,
302 | }
303 |
304 | pub fn find_lvalue(horizon: &[u8]) -> (Tri, usize) {
305 | let mut ate = identifierlen(horizon);
306 | if ate == 0 {
307 | return (Tri::No, ate);
308 | }
309 |
310 | #[derive(Clone)]
311 | #[derive(Copy)]
312 | enum Lex {
313 | Ident,
314 | Brack,
315 | Pluss,
316 | }
317 | let mut state = Lex::Ident;
318 |
319 | loop {
320 | if ate == horizon.len() {
321 | return (Tri::Maybe, ate);
322 | }
323 | let byte :u8 = horizon[ate];
324 |
325 | // Recursion: There is now an expression_tracker() if needed.
326 | match (state, byte) {
327 | (Lex::Ident, b'=') => return (Tri::Yes, ate),
328 | (Lex::Pluss, b'=') => return (Tri::Yes, ate),
329 | (Lex::Ident, b'[') => state = Lex::Brack,
330 | (Lex::Brack, b']') => state = Lex::Ident,
331 | (Lex::Ident, b'+') => state = Lex::Pluss,
332 | (Lex::Ident, _) => return (Tri::No, ate),
333 | (Lex::Pluss, _) => return (Tri::No, ate),
334 | (Lex::Brack, _) => {}
335 | }
336 | ate += 1;
337 | }
338 | }
339 |
340 | fn find_heredoc(horizon: &[u8]) -> (usize, Vec) {
341 | let mut ate = predlen(|x| x == b'<', horizon);
342 | let mut found = Vec::::new();
343 | if ate != 2 {
344 | return (ate, found);
345 | }
346 | ate += predlen(|x| x == b'-', &horizon[ate ..]);
347 | ate += predlen(is_whitespace, &horizon[ate ..]);
348 |
349 | // Lex one word.
350 | let herein = &horizon[ate ..];
351 | found.reserve(herein.len());
352 |
353 | #[derive(Clone)]
354 | #[derive(Copy)]
355 | enum DelimiterSyntax {
356 | Word,
357 | WordEsc,
358 | Sq,
359 | Dq,
360 | DqEsc,
361 | }
362 | let mut state = DelimiterSyntax::Word;
363 |
364 | for byte_ref in herein {
365 | let byte: u8 = *byte_ref;
366 | state = match (state, byte) {
367 | (DelimiterSyntax::Word, b' ' ) => break,
368 | (DelimiterSyntax::Word, b'\n') => break,
369 | (DelimiterSyntax::Word, b'\t') => break,
370 | (DelimiterSyntax::Word, b'\\') => DelimiterSyntax::WordEsc,
371 | (DelimiterSyntax::Word, b'\'') => DelimiterSyntax::Sq,
372 | (DelimiterSyntax::Word, b'\"') => DelimiterSyntax::Dq,
373 | (DelimiterSyntax::Sq, b'\'') => DelimiterSyntax::Word,
374 | (DelimiterSyntax::Dq, b'\"') => DelimiterSyntax::Word,
375 | (DelimiterSyntax::Dq, b'\\') => DelimiterSyntax::DqEsc,
376 | (DelimiterSyntax::WordEsc, b'\n') => DelimiterSyntax::Word,
377 | (DelimiterSyntax::WordEsc, _) => {
378 | found.push(byte);
379 | DelimiterSyntax::Word
380 | }
381 | (DelimiterSyntax::DqEsc, b'\n') => DelimiterSyntax::Dq,
382 | (DelimiterSyntax::DqEsc, _) => {
383 | if byte != b'\"' && byte != b'\\' {
384 | found.push(b'\\');
385 | }
386 | found.push(byte);
387 | DelimiterSyntax::Dq
388 | }
389 | (_, _) => {
390 | found.push(byte);
391 | state
392 | }
393 | };
394 | ate += 1;
395 | }
396 | (ate, found)
397 | }
398 |
399 | #[test]
400 | fn test_find_lvalue() {
401 | assert!(find_lvalue(b"") == (Tri::No, 0));
402 | assert!(find_lvalue(b"=") == (Tri::No, 0));
403 | assert!(find_lvalue(b"[]") == (Tri::No, 0));
404 | assert!(find_lvalue(b"esa") == (Tri::Maybe, 3));
405 | assert!(find_lvalue(b"esa+") == (Tri::Maybe, 4));
406 | assert!(find_lvalue(b"esa+ ") == (Tri::No, 4));
407 | assert!(find_lvalue(b"esa[]") == (Tri::Maybe, 5));
408 | assert!(find_lvalue(b"esa[]+") == (Tri::Maybe, 6));
409 | assert!(find_lvalue(b"esa ") == (Tri::No, 3));
410 | assert!(find_lvalue(b"esa]") == (Tri::No, 3));
411 | assert!(find_lvalue(b"esa=") == (Tri::Yes, 3));
412 | assert!(find_lvalue(b"esa+=") == (Tri::Yes, 4));
413 | assert!(find_lvalue(b"esa[]=") == (Tri::Yes, 5));
414 | assert!(find_lvalue(b"esa[]+=") == (Tri::Yes, 6));
415 | }
416 |
--------------------------------------------------------------------------------
/src/commonstrcmd.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Transition;
11 | use crate::situation::UnsupportedSyntax;
12 | use crate::situation::WhatNow;
13 | use crate::situation::flush;
14 | use crate::situation::push;
15 | use crate::situation::if_needed;
16 | use crate::situation::COLOR_ESC;
17 | use crate::situation::COLOR_VAR;
18 |
19 | use crate::microparsers::predlen;
20 | use crate::microparsers::is_identifierhead;
21 | use crate::microparsers::is_identifiertail;
22 | use crate::microparsers::identifierlen;
23 |
24 |
25 | use crate::sitcmd::SitNormal;
26 | use crate::sitextent::push_extent;
27 | use crate::sitextent::push_replaceable;
28 | use crate::sitmagic::push_magic;
29 | use crate::sitvarbrace::SitVarBrace;
30 | use crate::sitvarident::SitVarIdent;
31 |
32 | #[derive(Copy)]
33 | #[derive(Clone)]
34 | #[derive(PartialEq)]
35 | pub enum QuotingCtx {
36 | Need,
37 | Dontneed,
38 | Interpolation,
39 | }
40 |
41 | pub enum CommonStrCmdResult {
42 | None,
43 | Some(WhatNow),
44 | OnlyWithQuotes(WhatNow),
45 | }
46 |
47 | pub fn common_str_cmd(
48 | horizon: Horizon,
49 | i: usize,
50 | ctx: QuotingCtx,
51 | ) -> CommonStrCmdResult {
52 | let need_quotes = ctx == QuotingCtx::Need;
53 | let is_interpolation = ctx == QuotingCtx::Interpolation;
54 |
55 | if horizon.input[i] == b'`' {
56 | let found_pwd = find_pwd(horizon, i, 1, b'`');
57 | match found_pwd {
58 | CommonStrCmdResult::None => {}
59 | CommonStrCmdResult::Some(_) |
60 | CommonStrCmdResult::OnlyWithQuotes(_) => {
61 | return found_pwd;
62 | }
63 | }
64 | return CommonStrCmdResult::OnlyWithQuotes(push(
65 | (i, 1, Some(b"$(")),
66 | Box::new(SitNormal {
67 | end_trigger: u16::from(b'`'),
68 | end_replace: Some(b")"),
69 | }),
70 | ));
71 | }
72 | if horizon.input[i] == b'\\' {
73 | return CommonStrCmdResult::Some(push_extent(COLOR_ESC, i, 2));
74 | }
75 | if horizon.input[i] != b'$' {
76 | return CommonStrCmdResult::None;
77 | }
78 | if i+1 >= horizon.input.len() {
79 | if i > 0 || horizon.is_lengthenable {
80 | return CommonStrCmdResult::Some(flush(i));
81 | }
82 | return CommonStrCmdResult::None;
83 | }
84 | let c = horizon.input[i+1];
85 | if c == b'(' {
86 | let found_pwd = find_pwd(horizon, i, 2, b')');
87 | match found_pwd {
88 | CommonStrCmdResult::None => {}
89 | CommonStrCmdResult::Some(_) |
90 | CommonStrCmdResult::OnlyWithQuotes(_) => {
91 | return found_pwd;
92 | }
93 | }
94 | if i+2 >= horizon.input.len() {
95 | // Reachable, but already handled by find_pwd.
96 | } else if horizon.input[i+2] == b'(' {
97 | return CommonStrCmdResult::Some(push_magic(i, 2, b')'));
98 | }
99 | return CommonStrCmdResult::OnlyWithQuotes(push(
100 | (i, 2, None),
101 | Box::new(SitNormal {
102 | end_trigger: u16::from(b')'),
103 | end_replace: None,
104 | }),
105 | ));
106 | } else if is_variable_of_numeric_content(c) {
107 | return CommonStrCmdResult::Some(push_extent(COLOR_VAR, i, 2));
108 | } else if c == b'@' || c == b'*' || c == b'-' || is_decimal(c) {
109 | let digitlen = predlen(is_decimal, &horizon.input[i+1 ..]);
110 | if digitlen > 1 {
111 | return bail_doubledigit(i, 1 + digitlen);
112 | }
113 | return CommonStrCmdResult::OnlyWithQuotes(push_extent(COLOR_VAR, i, 2));
114 | } else if is_identifierhead(c) {
115 | let tailhazard;
116 | if need_quotes {
117 | let cand: &[u8] = &horizon.input[i+1 ..];
118 | let (_, pos_hazard) = pos_tailhazard(cand, b'\"');
119 | if pos_hazard == cand.len() {
120 | if i > 0 || horizon.is_lengthenable {
121 | return CommonStrCmdResult::Some(flush(i));
122 | }
123 | tailhazard = true;
124 | } else {
125 | tailhazard = is_identifiertail(cand[pos_hazard]);
126 | }
127 | } else {
128 | tailhazard = false;
129 | }
130 | return CommonStrCmdResult::OnlyWithQuotes(push(
131 | (i, 1, if_needed(tailhazard, b"${")),
132 | Box::new(SitVarIdent {
133 | end_insert: if_needed(tailhazard, b"}"),
134 | }),
135 | ));
136 | } else if c == b'{' {
137 | let cand: &[u8] = &horizon.input[i+2 ..];
138 | let (idlen, pos_hazard) = pos_tailhazard(cand, b'}');
139 | let mut rm_braces = false;
140 | let mut is_number = false;
141 | if pos_hazard == cand.len() {
142 | if i > 0 || horizon.is_lengthenable {
143 | return CommonStrCmdResult::Some(flush(i));
144 | }
145 | } else if idlen == 0 {
146 | is_number = is_variable_of_numeric_content(cand[0]);
147 | } else if idlen < pos_hazard && !is_identifiertail(cand[pos_hazard]) {
148 | let is_interpolation = is_interpolation || pos_hazard - idlen == 1;
149 | rm_braces = need_quotes || !is_interpolation;
150 | }
151 | let wn = push(
152 | (i, 2, if_needed(rm_braces, b"$")),
153 | Box::new(SitVarBrace::new(rm_braces, need_quotes)),
154 | );
155 | return if is_number {
156 | CommonStrCmdResult::Some(wn)
157 | } else {
158 | CommonStrCmdResult::OnlyWithQuotes(wn)
159 | };
160 | }
161 | CommonStrCmdResult::None
162 | }
163 |
164 | fn find_pwd(
165 | horizon: Horizon,
166 | i: usize,
167 | candidate_offset: usize,
168 | end: u8,
169 | ) -> CommonStrCmdResult {
170 | let cand: &[u8] = &horizon.input[i + candidate_offset ..];
171 | let (idlen, pos_hazard) = pos_tailhazard(cand, end);
172 | if pos_hazard == cand.len() {
173 | if i > 0 || horizon.is_lengthenable {
174 | return CommonStrCmdResult::Some(flush(i));
175 | }
176 | } else if idlen == 3 && pos_hazard >= 4 && cand[.. 3].eq(b"pwd") {
177 | let tailhazard = is_identifiertail(cand[pos_hazard]);
178 | let replacement: &'static [u8] = if tailhazard {
179 | b"${PWD}"
180 | } else {
181 | b"$PWD"
182 | };
183 | let what = push_replaceable(COLOR_VAR, i, candidate_offset + idlen + 1, Some(replacement));
184 | return CommonStrCmdResult::OnlyWithQuotes(what);
185 | }
186 | CommonStrCmdResult::None
187 | }
188 |
189 | fn pos_tailhazard(horizon: &[u8], end: u8) -> (usize, usize) {
190 | let idlen = identifierlen(horizon);
191 | let mut pos = idlen;
192 | if pos < horizon.len() && horizon[pos] == end {
193 | pos += 1;
194 | pos += predlen(|x| x == b'\"', &horizon[pos ..]);
195 | }
196 | (idlen, pos)
197 | }
198 |
199 | fn is_decimal(byte: u8) -> bool {
200 | byte.is_ascii_digit()
201 | }
202 |
203 | fn is_variable_of_numeric_content(c: u8) -> bool {
204 | matches!(c, b'#' | b'?' | b'$' | b'!')
205 | }
206 |
207 | fn bail_doubledigit(pos: usize, len: usize) -> CommonStrCmdResult {
208 | CommonStrCmdResult::Some(WhatNow {
209 | transform: (pos, len, None),
210 | transition: Transition::Err(UnsupportedSyntax {
211 | typ: "Unsupported syntax: Syntactic pitfall",
212 | msg: "This does not mean what it looks like. You may be forgiven to think that the full string of \
213 | numerals is the variable name. Only the fist is.\n\
214 | \n\
215 | Try this and be shocked: f() { echo \"$9\" \"$10\"; }; f a b c d e f g h i j\n\
216 | \n\
217 | Here is where braces should be used to disambiguate, \
218 | e.g. \"${10}\" vs \"${1}0\".\n\
219 | \n\
220 | Syntactic pitfalls are deemed too dangerous to fix automatically\n\
221 | (the purpose of Shellharden is to fix vulnerable code – code that mostly \
222 | does what it looks like, as opposed to code that never does what it looks like):\n\
223 | * Fixing what it does would be 100% subtle \
224 | and might slip through code review unnoticed.\n\
225 | * Fixing its look would make a likely bug look intentional."
226 | }),
227 | })
228 | }
229 |
--------------------------------------------------------------------------------
/src/errfmt.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2018 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use std::io::Write;
10 |
11 | pub struct ContextualError{
12 | pub typ: &'static str,
13 | pub ctx: Vec,
14 | pub pos: usize,
15 | pub len: usize,
16 | pub msg: &'static str,
17 | }
18 |
19 | fn stderr_write_or_panic(lock: &mut std::io::StderrLock, bytes: &[u8]) {
20 | if let Err(e) = lock.write_all(bytes) {
21 | panic!("Unable to write to stderr: {}", e);
22 | }
23 | }
24 |
25 | pub fn blame_path(path: &std::ffi::OsString, blame: &str) {
26 | let printable = path.to_string_lossy();
27 | eprintln!("{}: {}", printable, blame);
28 | }
29 |
30 | pub fn blame_path_io(path: &std::ffi::OsString, e: &std::io::Error) {
31 | let printable = path.to_string_lossy();
32 | eprintln!("{}: {}", printable, e);
33 | }
34 |
35 | pub fn blame_syntax(path: &std::ffi::OsString, fail: &ContextualError) {
36 | blame_path(path, fail.typ);
37 | if fail.pos < fail.ctx.len() {
38 | let mut i = fail.pos;
39 | while i > 0 {
40 | i -= 1;
41 | if fail.ctx[i] == b'\n' {
42 | break;
43 | }
44 | }
45 | let failing_line_begin = if fail.ctx[i] == b'\n' { i + 1 } else { 0 };
46 | let mut i = fail.pos;
47 | while i < fail.ctx.len() && fail.ctx[i] != b'\n' {
48 | i += 1;
49 | }
50 | let failing_line = &fail.ctx[failing_line_begin .. i];
51 |
52 | // FIXME: This counts codepoints, not displayed width.
53 | let mut width = 0;
54 | for c in &fail.ctx[failing_line_begin .. fail.pos] {
55 | if c >> b'\x06' != b'\x02' {
56 | width += 1;
57 | }
58 | }
59 | let width = width;
60 |
61 | let stderr = std::io::stderr();
62 | let mut stderr_lock = stderr.lock();
63 | stderr_write_or_panic(&mut stderr_lock, failing_line);
64 | stderr_write_or_panic(&mut stderr_lock, b"\n");
65 | for _ in 0 .. width {
66 | stderr_write_or_panic(&mut stderr_lock, b" ");
67 | }
68 | for _ in 0 .. fail.len {
69 | stderr_write_or_panic(&mut stderr_lock, b"^");
70 | }
71 | stderr_write_or_panic(&mut stderr_lock, b"\n");
72 | }
73 | eprintln!("{}", fail.msg);
74 | }
75 |
--------------------------------------------------------------------------------
/src/filestream.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2018 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use std::io::{Read, Seek, Write};
10 | use std::fmt::{Write as FmtWrite};
11 |
12 | pub enum InputSource<'a> {
13 | File(std::fs::File),
14 | Stdin(std::io::StdinLock<'a>),
15 | }
16 |
17 | impl<'a> InputSource<'a> {
18 | pub fn open_file(path: &std::ffi::OsString) -> Result {
19 | Ok(InputSource::File(std::fs::File::open(path)?))
20 | }
21 | pub fn open_stdin(stdin: &std::io::Stdin) -> InputSource {
22 | InputSource::Stdin(stdin.lock())
23 | }
24 | pub fn read(&mut self, buf: &mut [u8]) -> Result {
25 | match *self {
26 | InputSource::Stdin(ref mut fh) => fh.read(buf),
27 | InputSource::File (ref mut fh) => fh.read(buf),
28 | }
29 | }
30 | pub fn size(&mut self) -> Result {
31 | match *self {
32 | InputSource::Stdin(_) => panic!("filesize of stdin"),
33 | InputSource::File (ref mut fh) => {
34 | let off :u64 = fh.seek(std::io::SeekFrom::End(0))?;
35 | fh.seek(std::io::SeekFrom::Start(0))?;
36 | Ok(off)
37 | }
38 | }
39 | }
40 | }
41 |
42 | pub enum OutputSink<'a> {
43 | Stdout(std::io::StdoutLock<'a>),
44 | Soak(Vec),
45 | None,
46 | }
47 |
48 | pub struct FileOut<'a> {
49 | pub sink :OutputSink<'a>,
50 | pub change :bool,
51 | }
52 |
53 | impl<'a> FileOut<'a> {
54 | pub fn open_stdout(stdout: &std::io::Stdout) -> FileOut {
55 | FileOut{sink: OutputSink::Stdout(stdout.lock()), change: false}
56 | }
57 | pub fn open_soak(reserve: u64) -> FileOut<'a> {
58 | FileOut{sink: OutputSink::Soak(Vec::with_capacity(reserve as usize)), change: false}
59 | }
60 | pub fn open_none() -> FileOut<'a> {
61 | FileOut{sink: OutputSink::None, change: false}
62 | }
63 | pub fn write_all(&mut self, buf: &[u8]) -> Result<(), std::io::Error> {
64 | match self.sink {
65 | OutputSink::Stdout(ref mut fh) => fh.write_all(buf)?,
66 | OutputSink::Soak(ref mut vec) => vec.extend_from_slice(buf),
67 | OutputSink::None => {}
68 | }
69 | Ok(())
70 | }
71 | pub fn write_fmt(&mut self, args: std::fmt::Arguments) -> Result<(), std::io::Error> {
72 | match self.sink {
73 | OutputSink::Stdout(ref mut fh) => fh.write_fmt(args)?,
74 | OutputSink::Soak(ref mut buf) => {
75 | // TODO: Format directly to vec
76 | let mut s = String::new();
77 | if s.write_fmt(args).is_err() {
78 | panic!("fmt::Error");
79 | }
80 | buf.extend_from_slice(s.as_bytes());
81 | }
82 | OutputSink::None => {}
83 | }
84 | Ok(())
85 | }
86 | pub fn commit(&mut self, path: &std::ffi::OsString) -> Result<(), std::io::Error> {
87 | if self.change {
88 | if let OutputSink::Soak(ref vec) = self.sink {
89 | std::fs::OpenOptions::new()
90 | .write(true)
91 | .truncate(true)
92 | .create(false)
93 | .open(path)?
94 | .write_all(vec)?
95 | ;
96 | }
97 | }
98 | Ok(())
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/machine.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use std::io;
10 | use std::io::Write;
11 |
12 | use crate::errfmt::ContextualError;
13 |
14 | use crate::filestream::InputSource;
15 | use crate::filestream::FileOut;
16 | use crate::filestream::OutputSink;
17 |
18 | use crate::situation::Horizon;
19 | use crate::situation::Situation;
20 | use crate::situation::Transition;
21 | use crate::situation::COLOR_NORMAL;
22 |
23 | use crate::sitcmd::SitNormal;
24 |
25 | #[derive(Clone)]
26 | #[derive(Copy)]
27 | #[derive(PartialEq)]
28 | pub enum OutputSelector {
29 | Original,
30 | Diff,
31 | Transform,
32 | Check,
33 | }
34 |
35 | pub struct Settings {
36 | pub osel :OutputSelector,
37 | pub syntax :bool,
38 | pub replace :bool,
39 | }
40 |
41 | pub enum Error {
42 | Stdio(std::io::Error),
43 | Syntax(ContextualError),
44 | Check,
45 | }
46 |
47 | pub fn treatfile(path: &std::ffi::OsString, sett: &Settings) -> Result<(), Error> {
48 | let stdin = io::stdin();
49 | let mut fi: InputSource = if path.is_empty() {
50 | InputSource::open_stdin(&stdin)
51 | } else {
52 | InputSource::open_file(path).map_err(Error::Stdio)?
53 | };
54 |
55 | let stdout = io::stdout();
56 | let mut fo: FileOut = if sett.osel == OutputSelector::Check {
57 | FileOut::open_none()
58 | } else if sett.replace && !path.is_empty() {
59 | FileOut::open_soak(fi.size().map_err(Error::Stdio)? * 9 / 8)
60 | } else {
61 | FileOut::open_stdout(&stdout)
62 | };
63 |
64 | let mut color_cur = COLOR_NORMAL;
65 |
66 | let res = treatfile_fallible(&mut fi, &mut fo, &mut color_cur, sett);
67 | if color_cur != COLOR_NORMAL {
68 | write_color(&mut fo, COLOR_NORMAL).map_err(Error::Stdio)?;
69 | }
70 | if res.is_ok() {
71 | fo.commit(path).map_err(Error::Stdio)
72 | } else {
73 | if let OutputSink::Stdout(mut stdout) = fo.sink {
74 | let _ = stdout.write_all(b"\n");
75 | }
76 | res
77 | }
78 | }
79 |
80 | const MAXHORIZON :usize = 128;
81 |
82 | fn treatfile_fallible(
83 | fi: &mut InputSource, fo: &mut FileOut,
84 | color_cur: &mut u32, sett: &Settings,
85 | ) -> Result<(), Error> {
86 | let mut fill :usize = 0;
87 | let mut buf = [0; MAXHORIZON];
88 |
89 | let mut state :Vec> = vec!{Box::new(SitNormal {
90 | end_trigger: 0x100,
91 | end_replace: None,
92 | })};
93 |
94 | loop {
95 | let bytes = fi.read(&mut buf[fill ..]).map_err(Error::Stdio)?;
96 | fill += bytes;
97 | let eof = bytes == 0;
98 | let consumed = stackmachine(
99 | &mut state, fo, color_cur, &buf[0 .. fill], eof, sett
100 | )?;
101 | let remain = fill - consumed;
102 | if eof {
103 | assert!(remain == 0);
104 | break;
105 | }
106 | assert!(remain < MAXHORIZON);
107 | for i in 0 .. remain {
108 | buf[i] = buf[consumed + i];
109 | }
110 | fill = remain;
111 | }
112 | if state.len() != 1 {
113 | return Err(Error::Syntax(ContextualError{
114 | typ: "Unexpected end of file",
115 | ctx: buf[0 .. fill].to_owned(),
116 | pos: fill,
117 | len: 1,
118 | msg: "The file's end was reached without closing all sytactic scopes.\n\
119 | Either, the parser got lost, or the file is truncated or malformed.",
120 | }));
121 | }
122 | Ok(())
123 | }
124 |
125 | fn stackmachine(
126 | state: &mut Vec>,
127 | out: &mut FileOut,
128 | color_cur: &mut u32,
129 | buf: &[u8],
130 | eof: bool,
131 | sett: &Settings,
132 | ) -> Result {
133 | let mut pos :usize = 0;
134 | loop {
135 | let inputhorizon = &buf[pos ..];
136 | let horizon = Horizon {
137 | input: inputhorizon,
138 | is_lengthenable: inputhorizon.len() < MAXHORIZON && !eof,
139 | };
140 | let stacksize_pre = state.len();
141 | let statebox: &mut Box = if let Some(innerstate) = state.last_mut() {
142 | innerstate
143 | } else {
144 | break;
145 | };
146 | let curstate = statebox.as_mut();
147 | let color_pre = if sett.syntax { curstate.get_color() } else { COLOR_NORMAL };
148 | let whatnow = curstate.whatnow(horizon);
149 | let (pre, len, alt) = whatnow.transform;
150 |
151 | if alt.is_some() {
152 | out.change = true;
153 | if sett.osel == OutputSelector::Check {
154 | return Err(Error::Check);
155 | }
156 | }
157 |
158 | write_colored_slice(
159 | out, color_cur, color_pre, &horizon.input[.. pre]
160 | ).map_err(Error::Stdio)?;
161 | let progress = pre + len;
162 | let replaceable = &horizon.input[pre .. progress];
163 |
164 | match (whatnow.transition, eof) {
165 | (Transition::Flush, _) | (Transition::FlushPopOnEof, false) => {
166 | if progress == 0 {
167 | break;
168 | }
169 | }
170 | (Transition::Replace(newstate), _) => {
171 | *statebox = newstate;
172 | }
173 | (Transition::Push(newstate), _) => {
174 | state.push(newstate);
175 | }
176 | (Transition::Pop, _) | (Transition::FlushPopOnEof, true) => {
177 | state.pop();
178 | }
179 | (Transition::Err(e), _) => {
180 | return Err(Error::Syntax(ContextualError{
181 | typ: e.typ,
182 | ctx: buf.to_owned(),
183 | pos: pos + whatnow.transform.0,
184 | len: whatnow.transform.1,
185 | msg: e.msg,
186 | }));
187 | }
188 | }
189 |
190 | let color_trans = if !sett.syntax || state.len() < stacksize_pre {
191 | color_pre
192 | } else {
193 | state.last().unwrap().as_ref().get_color()
194 | };
195 | write_transition(
196 | out, color_cur, color_trans, sett, replaceable, alt
197 | ).map_err(Error::Stdio)?;
198 |
199 | pos += progress;
200 | }
201 | Ok(pos)
202 | }
203 |
204 | fn write_transition(
205 | out: &mut FileOut,
206 | color_cur: &mut u32,
207 | color_trans: u32,
208 | sett: &Settings,
209 | replaceable: &[u8],
210 | alternative: Option<&[u8]>,
211 | ) -> Result<(), std::io::Error> {
212 | match (alternative, sett.osel) {
213 | (Some(replacement), OutputSelector::Diff) => {
214 | write_diff(out, color_cur, color_trans, replaceable, replacement)
215 | }
216 | (Some(replacement), OutputSelector::Transform) => {
217 | write_colored_slice(out, color_cur, color_trans, replacement)
218 | }
219 | (_, _) => {
220 | write_colored_slice(out, color_cur, color_trans, replaceable)
221 | }
222 | }?;
223 | Ok(())
224 | }
225 |
226 | // Edit distance without replacement; greedy, but that suffices.
227 | fn write_diff(
228 | out: &mut FileOut,
229 | color_cur: &mut u32,
230 | color_neutral: u32,
231 | replaceable: &[u8],
232 | replacement: &[u8],
233 | ) -> Result<(), std::io::Error> {
234 | let color_a = 0x10_800000;
235 | let color_b = 0x10_008000;
236 | let remain_a = replaceable;
237 | let mut remain_b = replacement;
238 | for (i, &a) in remain_a.iter().enumerate() {
239 | let color_next;
240 | if let Some(pivot_b) = remain_b.iter().position(|&b| b == a) {
241 | color_next = color_neutral;
242 | write_colored_slice(out, color_cur, color_b, &remain_b[0 .. pivot_b])?;
243 | remain_b = &remain_b[pivot_b+1 ..];
244 | } else {
245 | color_next = color_a;
246 | }
247 | write_colored_slice(out, color_cur, color_next, &remain_a[i ..= i])?;
248 | }
249 | write_colored_slice(out, color_cur, color_b, remain_b)
250 | }
251 |
252 | fn write_colored_slice(
253 | out: &mut FileOut,
254 | color_cur: &mut u32,
255 | color: u32,
256 | slice: &[u8],
257 | ) -> Result<(), std::io::Error> {
258 | if slice.is_empty() {
259 | return Ok(());
260 | }
261 | if *color_cur != color {
262 | write_color(out, color)?;
263 | *color_cur = color;
264 | }
265 | out.write_all(slice)
266 | }
267 |
268 | fn write_color(out :&mut FileOut, code :u32) -> Result<(), std::io::Error> {
269 | let zero = if (code >> 24) & 3 != 0 { "0" } else { "" };
270 | let bold = if (code >> 24) & 1 != 0 { ";1" } else { "" };
271 | let ital = if (code >> 25) & 1 != 0 { ";3" } else { "" };
272 |
273 | if code & 0x00_ffffff == 0 {
274 | return write!(out, "\x1b[{}{}{}m", zero, bold, ital);
275 | }
276 |
277 | let fg = (code >> 28) == 0;
278 | let b = code & 0xff;
279 | let g = (code >> 8) & 0xff;
280 | let r = (code >> 16) & 0xff;
281 | if fg {
282 | write!(out, "\x1b[0{}{};38;2;{};{};{}m", bold, ital, r, g, b)
283 | } else {
284 | write!(out, "\x1b[0;4{}m", (r >> 7) | (g >> 6) | (b >> 5))
285 | }
286 | }
287 |
288 | pub fn expression_tracker(horizon: &[u8], state: Box) -> Result<(bool, usize), ()> {
289 | let mut stack = vec!{state};
290 | let mut color_cur = COLOR_NORMAL;
291 |
292 | match stackmachine(
293 | &mut stack,
294 | &mut FileOut::open_none(),
295 | &mut color_cur,
296 | horizon,
297 | false,
298 | &Settings{
299 | osel: OutputSelector::Original,
300 | syntax: false,
301 | replace: false,
302 | },
303 | ) {
304 | Ok(len) => Ok((stack.is_empty(), len)),
305 | Err(_) => Err(()),
306 | }
307 | }
308 |
--------------------------------------------------------------------------------
/src/main.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | // Color codes are split between flag- and color bits on purpose, such as 0x03_789060.
10 | #![allow(clippy::unusual_byte_groupings)]
11 |
12 | use std::env;
13 | use std::process;
14 | use std::ffi::OsStr;
15 |
16 | mod machine;
17 | use crate::machine::OutputSelector;
18 |
19 | mod errfmt;
20 | mod filestream;
21 | mod situation;
22 |
23 | fn help() {
24 | println!(
25 | "Shellharden: The corrective bash syntax highlighter.\n\
26 | \n\
27 | Usage:\n\
28 | \tshellharden [options] [files]\n\
29 | \tcat files | shellharden [options] ''\n\
30 | \n\
31 | Shellharden is a syntax highlighter and a tool to semi-automate the rewriting\n\
32 | of scripts to ShellCheck conformance, mainly focused on quoting.\n\
33 | \n\
34 | The default mode of operation is like `cat`, but with syntax highlighting in\n\
35 | foreground colors and suggestive changes in background colors.\n\
36 | \n\
37 | Options:\n\
38 | \t--suggest Output a colored diff suggesting changes.\n\
39 | \t--syntax Output syntax highlighting with ANSI colors.\n\
40 | \t--syntax-suggest Diff with syntax highlighting (default mode).\n\
41 | \t--transform Output suggested changes.\n\
42 | \t--check No output; exit with 2 if changes are suggested.\n\
43 | \t--replace Replace file contents with suggested changes.\n\
44 | \t-- Don't treat further arguments as options.\n\
45 | \t-h|--help Show help text.\n\
46 | \t--version Show version.\n\
47 | \n\
48 | The changes suggested by Shellharden inhibits word splitting and indirect\n\
49 | pathname expansion. This will make your script ShellCheck compliant in terms of\n\
50 | quoting. Whether your script will work afterwards is a different question:\n\
51 | If your script was using those features on purpose, it obviously won't anymore!\n\
52 | \n\
53 | Every script is possible to write without using word splitting or indirect\n\
54 | pathname expansion, but it may involve doing things differently.\n\
55 | See the accompanying file how_to_do_things_safely_in_bash.md or online:\n\
56 | https://github.com/anordal/shellharden/blob/master/how_to_do_things_safely_in_bash.md\n\
57 | "
58 | );
59 | }
60 |
61 | fn main() {
62 | let mut args: std::env::ArgsOs = env::args_os();
63 | args.next();
64 |
65 | let mut sett = machine::Settings {
66 | osel: OutputSelector::Diff,
67 | syntax: true,
68 | replace: false,
69 | };
70 |
71 | let mut exit_code: i32 = 0;
72 | let mut opt_trigger: &str = "-";
73 | for arg in args {
74 | if let Some(option) = get_if_opt(&arg, opt_trigger) {
75 | match option {
76 | "--suggest" => {
77 | sett.osel = OutputSelector::Diff;
78 | sett.syntax = false;
79 | sett.replace = false;
80 | }
81 | "--syntax" => {
82 | sett.osel = OutputSelector::Original;
83 | sett.syntax = true;
84 | sett.replace = false;
85 | }
86 | "--syntax-suggest" => {
87 | sett.osel = OutputSelector::Diff;
88 | sett.syntax = true;
89 | sett.replace = false;
90 | }
91 | "--transform" => {
92 | sett.osel = OutputSelector::Transform;
93 | sett.syntax = false;
94 | sett.replace = false;
95 | }
96 | "--check" => {
97 | sett.osel = OutputSelector::Check;
98 | sett.syntax = false;
99 | sett.replace = false;
100 | }
101 | "--replace" => {
102 | sett.osel = OutputSelector::Transform;
103 | sett.syntax = false;
104 | sett.replace = true;
105 | }
106 | "--help" | "-h" => {
107 | help();
108 | }
109 | "--version" => {
110 | println!(env!("CARGO_PKG_VERSION"));
111 | }
112 | "--" => {
113 | opt_trigger = "\x00";
114 | }
115 | _ => {
116 | errfmt::blame_path(&arg, "No such option.");
117 | exit_code = 3;
118 | break;
119 | }
120 | }
121 | }
122 | else if let Err(e) = machine::treatfile(&arg, &sett) {
123 | exit_code = 1;
124 | match (sett.osel, e) {
125 | (_, machine::Error::Stdio(ref fail)) => {
126 | errfmt::blame_path_io(&arg, fail);
127 | }
128 | (OutputSelector::Check, _) | (_, machine::Error::Check) => {
129 | exit_code = 2;
130 | break;
131 | }
132 | (_, machine::Error::Syntax(ref fail)) => {
133 | errfmt::blame_syntax(&arg, fail);
134 | }
135 | };
136 | }
137 | }
138 | process::exit(exit_code);
139 | }
140 |
141 | fn get_if_opt<'a>(arg: &'a OsStr, opt_trigger: &str) -> Option<&'a str> {
142 | if let Some(comparable) = arg.to_str() {
143 | if comparable.starts_with(opt_trigger) {
144 | return Some(comparable);
145 | }
146 | }
147 | None
148 | }
149 |
150 | //------------------------------------------------------------------------------
151 |
152 | #[cfg(test)]
153 | #[macro_use]
154 | mod testhelpers;
155 |
156 | mod commonargcmd;
157 | mod commonstrcmd;
158 | mod microparsers;
159 | mod sitcase;
160 | mod sitcmd;
161 | mod sitcomment;
162 | mod sitextent;
163 | mod sitfor;
164 | mod sitmagic;
165 | mod sitrvalue;
166 | mod sitstrdq;
167 | mod sitstrphantom;
168 | mod sitstrsqesc;
169 | mod sittest;
170 | mod situntilbyte;
171 | mod sitvarbrace;
172 | mod sitvarident;
173 | mod sitvec;
174 |
--------------------------------------------------------------------------------
/src/microparsers.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | pub fn prefixlen(a: &[u8], b: &[u8]) -> usize {
10 | let mut i: usize = 0;
11 | while i < a.len() && i < b.len() && a[i] == b[i] {
12 | i += 1;
13 | }
14 | i
15 | }
16 |
17 | pub fn predlen(pred: impl Fn(u8) -> bool, horizon: &[u8]) -> usize {
18 | let mut i: usize = 0;
19 | while i < horizon.len() && pred(horizon[i]) {
20 | i += 1;
21 | }
22 | i
23 | }
24 |
25 | pub fn is_identifierhead(c: u8) -> bool {
26 | matches!(c, b'a' ..= b'z' | b'A' ..= b'Z' | b'_')
27 | }
28 |
29 | pub fn is_identifiertail(c: u8) -> bool {
30 | matches!(c, b'a' ..= b'z' | b'A' ..= b'Z' | b'0' ..= b'9' | b'_')
31 | }
32 |
33 | pub fn identifierlen(horizon: &[u8]) -> usize {
34 | if !horizon.is_empty() && is_identifierhead(horizon[0]) {
35 | 1 + predlen(is_identifiertail, &horizon[1 ..])
36 | } else {
37 | 0
38 | }
39 | }
40 |
41 | pub fn is_whitespace(c: u8) -> bool {
42 | c <= b' '
43 | }
44 |
45 | pub fn is_lowercase(c: u8) -> bool {
46 | c.is_ascii_lowercase()
47 | }
48 |
49 | pub fn is_word(byte: u8) -> bool {
50 | !matches!(byte, 0 ..= b' ' | b'&' | b'(' | b')' | b';' | b'<' | b'>' | b'`' | b'|')
51 | }
52 |
--------------------------------------------------------------------------------
/src/sitcase.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::sitextent::SitExtent;
13 | use crate::situation::WhatNow;
14 | use crate::situation::flush;
15 | use crate::situation::pop;
16 | use crate::situation::push;
17 | use crate::situation::COLOR_NORMAL;
18 | use crate::situation::COLOR_KWD;
19 |
20 | use crate::microparsers::predlen;
21 | use crate::microparsers::is_lowercase;
22 | use crate::microparsers::is_whitespace;
23 |
24 | use crate::commonargcmd::keyword_or_command;
25 | use crate::commonargcmd::common_expr_quoting_unneeded;
26 |
27 | pub struct SitCase {}
28 |
29 | impl Situation for SitCase {
30 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
31 | for (i, _) in horizon.input.iter().enumerate() {
32 | let len = predlen(is_lowercase, &horizon.input[i..]);
33 | if len == 0 {
34 | if let Some(res) = common_expr_quoting_unneeded(0x100, horizon, i) {
35 | return res;
36 | }
37 | continue;
38 | }
39 | if i + len == horizon.input.len() && (i > 0 || horizon.is_lengthenable) {
40 | return flush(i);
41 | }
42 | let word = &horizon.input[i..i+len];
43 | if word == b"in" {
44 | return become_case_in(i + len);
45 | }
46 | return flush(i + len);
47 | }
48 | flush(horizon.input.len())
49 | }
50 | fn get_color(&self) -> u32 {
51 | COLOR_KWD
52 | }
53 | }
54 |
55 | struct SitCaseIn {}
56 |
57 | impl Situation for SitCaseIn {
58 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
59 | for (i, &a) in horizon.input.iter().enumerate() {
60 | let len = predlen(is_lowercase, &horizon.input[i..]);
61 | if len == 0 {
62 | if a == b')' {
63 | return push((i, 1, None), Box::new(SitCaseArm {}));
64 | }
65 | if let Some(res) = common_expr_quoting_unneeded(0x100, horizon, i) {
66 | return res;
67 | }
68 | continue;
69 | }
70 | if i + len == horizon.input.len() && (i > 0 || horizon.is_lengthenable) {
71 | return flush(i);
72 | }
73 | let word = &horizon.input[i..i+len];
74 | if word == b"esac" {
75 | return pop_kw(i, len);
76 | }
77 | return flush(i + len);
78 | }
79 | flush(horizon.input.len())
80 | }
81 | fn get_color(&self) -> u32 {
82 | COLOR_NORMAL
83 | }
84 | }
85 |
86 | struct SitCaseArm {}
87 |
88 | impl Situation for SitCaseArm {
89 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
90 | for (i, &a) in horizon.input.iter().enumerate() {
91 | if a == b';' {
92 | if i + 1 < horizon.input.len() {
93 | if horizon.input[i + 1] == b';' {
94 | return pop(i, 0, None);
95 | }
96 | } else if i > 0 || horizon.is_lengthenable {
97 | return flush(i);
98 | }
99 | }
100 | if is_whitespace(a) || a == b';' || a == b'|' || a == b'&' || a == b'<' || a == b'>' {
101 | continue;
102 | }
103 | // Premature esac: Survive and rewrite.
104 | let len = predlen(is_lowercase, &horizon.input[i..]);
105 | if i + len != horizon.input.len() || (i == 0 && !horizon.is_lengthenable) {
106 | let word = &horizon.input[i..i+len];
107 | if word == b"esac" {
108 | return pop(i, 0, Some(b";; "));
109 | }
110 | }
111 | return keyword_or_command(0x100, horizon, i);
112 | }
113 | flush(horizon.input.len())
114 | }
115 | fn get_color(&self) -> u32 {
116 | COLOR_NORMAL
117 | }
118 | }
119 |
120 | fn become_case_in(pre: usize) -> WhatNow {
121 | WhatNow{
122 | transform: (pre, 0, None),
123 | transition: Transition::Replace(Box::new(SitCaseIn {})),
124 | }
125 | }
126 |
127 | fn pop_kw(pre: usize, len: usize) -> WhatNow {
128 | WhatNow {
129 | transform: (pre, len, None),
130 | transition: Transition::Replace(Box::new(SitExtent { len: 0, color: COLOR_KWD })),
131 | }
132 | }
133 |
134 | #[cfg(test)]
135 | use crate::testhelpers::*;
136 | #[cfg(test)]
137 | use crate::sitcmd::SitCmd;
138 | #[cfg(test)]
139 | use crate::situation::COLOR_ESC;
140 | #[cfg(test)]
141 | use crate::sitextent::push_extent;
142 |
143 | #[test]
144 | fn test_sit_case() {
145 | sit_expect!(SitCase{}, b"", &flush(0));
146 | sit_expect!(SitCase{}, b" ", &flush(1));
147 | sit_expect!(SitCase{}, b"i\"", &flush(1));
148 | sit_expect!(SitCase{}, b"i", &flush(0), &flush(1));
149 | sit_expect!(SitCase{}, b"in ", &become_case_in(2));
150 | sit_expect!(SitCase{}, b"in", &flush(0), &become_case_in(2));
151 | sit_expect!(SitCase{}, b"inn", &flush(0), &flush(3));
152 | sit_expect!(SitCase{}, b" in", &flush(1));
153 | sit_expect!(SitCase{}, b"fin", &flush(0), &flush(3));
154 | sit_expect!(SitCase{}, b"fin ", &flush(3));
155 | }
156 |
157 | #[test]
158 | fn test_sit_casein() {
159 | sit_expect!(SitCaseIn{}, b"", &flush(0));
160 | sit_expect!(SitCaseIn{}, b" ", &flush(1));
161 | sit_expect!(SitCaseIn{}, b"esa\"", &flush(3));
162 | sit_expect!(SitCaseIn{}, b"esa", &flush(0), &flush(3));
163 | sit_expect!(SitCaseIn{}, b"esac ", &pop_kw(0, 4));
164 | sit_expect!(SitCaseIn{}, b"esac", &flush(0), &pop_kw(0, 4));
165 | sit_expect!(SitCaseIn{}, b"esacs", &flush(0), &flush(5));
166 | sit_expect!(SitCaseIn{}, b" esac", &flush(1));
167 | sit_expect!(SitCaseIn{}, b"besac", &flush(0), &flush(5));
168 | sit_expect!(SitCaseIn{}, b"besac ", &flush(5));
169 | }
170 |
171 | #[test]
172 | fn test_sit_casearm() {
173 | let found_command = push((0, 0, None), Box::new(SitCmd{end_trigger: 0x100}));
174 | let found_the_esac_word = pop(0, 0, Some(b";; "));
175 |
176 | sit_expect!(SitCaseArm{}, b"", &flush(0));
177 | sit_expect!(SitCaseArm{}, b" ", &flush(1));
178 | sit_expect!(SitCaseArm{}, b"\\", &push_extent(COLOR_ESC, 0, 2));
179 | sit_expect!(SitCaseArm{}, b";", &flush(0), &flush(1));
180 | sit_expect!(SitCaseArm{}, b"; ", &flush(2));
181 | sit_expect!(SitCaseArm{}, b" ;", &flush(1));
182 | sit_expect!(SitCaseArm{}, b"esa", &flush(0), &found_command);
183 | sit_expect!(SitCaseArm{}, b"esac ", &found_the_esac_word);
184 | sit_expect!(SitCaseArm{}, b"esac", &flush(0), &found_the_esac_word);
185 | sit_expect!(SitCaseArm{}, b"esacs", &flush(0), &found_command);
186 | sit_expect!(SitCaseArm{}, b" esac", &flush(1));
187 | sit_expect!(SitCaseArm{}, b"besac", &flush(0), &found_command);
188 | sit_expect!(SitCaseArm{}, b"besac ", &found_command);
189 | }
190 |
--------------------------------------------------------------------------------
/src/sitcmd.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::situation::WhatNow;
13 | use crate::situation::flush;
14 | use crate::situation::flush_or_pop;
15 | use crate::situation::pop;
16 | use crate::situation::COLOR_NORMAL;
17 | use crate::situation::COLOR_CMD;
18 |
19 | use crate::microparsers::is_whitespace;
20 |
21 | use crate::commonargcmd::keyword_or_command;
22 | use crate::commonargcmd::common_arg;
23 | use crate::commonargcmd::common_cmd;
24 |
25 | pub struct SitNormal {
26 | pub end_trigger :u16,
27 | pub end_replace :Option<&'static [u8]>,
28 | }
29 |
30 | impl Situation for SitNormal {
31 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
32 | for (i, &a) in horizon.input.iter().enumerate() {
33 | if is_whitespace(a) || a == b';' || a == b'|' || a == b'&' || a == b'<' || a == b'>' {
34 | continue;
35 | }
36 | if u16::from(a) == self.end_trigger {
37 | return pop(i, 1, self.end_replace);
38 | }
39 | return keyword_or_command(self.end_trigger, horizon, i);
40 | }
41 | flush(horizon.input.len())
42 | }
43 | fn get_color(&self) -> u32 {
44 | COLOR_NORMAL
45 | }
46 | }
47 |
48 | pub struct SitCmd {
49 | pub end_trigger :u16,
50 | }
51 |
52 | impl Situation for SitCmd {
53 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
54 | for (i, &a) in horizon.input.iter().enumerate() {
55 | if let Some(res) = common_cmd(self.end_trigger, horizon, i) {
56 | return res;
57 | }
58 | if is_whitespace(a) {
59 | return WhatNow {
60 | transform: (i, 1, None),
61 | transition: Transition::Replace(Box::new(SitArg {
62 | end_trigger: self.end_trigger,
63 | })),
64 | };
65 | }
66 | if a == b'(' {
67 | return pop(i, 0, None);
68 | }
69 | }
70 | flush_or_pop(horizon.input.len())
71 | }
72 | fn get_color(&self) -> u32 {
73 | COLOR_CMD
74 | }
75 | }
76 |
77 | pub struct SitArg {
78 | pub end_trigger :u16,
79 | }
80 |
81 | impl Situation for SitArg {
82 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
83 | for (i, _) in horizon.input.iter().enumerate() {
84 | if let Some(res) = common_arg(self.end_trigger, horizon, i) {
85 | return res;
86 | }
87 | }
88 | flush_or_pop(horizon.input.len())
89 | }
90 | fn get_color(&self) -> u32 {
91 | COLOR_NORMAL
92 | }
93 | }
94 |
95 | #[cfg(test)]
96 | use crate::testhelpers::*;
97 | #[cfg(test)]
98 | use crate::sitmagic::push_magic;
99 | #[cfg(test)]
100 | use crate::sitrvalue::SitLvalue;
101 | #[cfg(test)]
102 | use crate::sitvec::SitVec;
103 | #[cfg(test)]
104 | use crate::sitfor::SitFor;
105 | #[cfg(test)]
106 | use crate::situation::COLOR_HERE;
107 | #[cfg(test)]
108 | use crate::situation::push;
109 | #[cfg(test)]
110 | use crate::situation::COLOR_ESC;
111 | #[cfg(test)]
112 | use crate::sitextent::push_extent;
113 |
114 | #[cfg(test)]
115 | fn mk_assignment(pre: usize) -> WhatNow {
116 | push((pre, 0, None), Box::new(SitLvalue { len: 0, end_trigger: 0 }))
117 | }
118 |
119 | #[cfg(test)]
120 | fn mk_cmd(pre: usize) -> WhatNow {
121 | push((pre, 0, None), Box::new(SitCmd { end_trigger: 0 }))
122 | }
123 |
124 | #[test]
125 | fn test_sit_normal() {
126 | let subj = || {
127 | SitNormal{end_trigger: 0, end_replace: None}
128 | };
129 |
130 | sit_expect!(subj(), b"", &flush(0));
131 | sit_expect!(subj(), b" ", &flush(1));
132 | sit_expect!(subj(), b"\\", &push_extent(COLOR_ESC, 0, 2));
133 | sit_expect!(subj(), b"fo", &flush(0), &mk_cmd(0));
134 | sit_expect!(subj(), b"fo=", &mk_assignment(0));
135 | sit_expect!(subj(), b"for", &flush(0), &push((0, 3, None), Box::new(SitFor {})));
136 | sit_expect!(subj(), b"for=", &mk_assignment(0));
137 | sit_expect!(subj(), b"fork", &flush(0), &mk_cmd(0));
138 | sit_expect!(subj(), b"fork=", &mk_assignment(0));
139 | sit_expect!(subj(), b";fo", &flush(1));
140 | sit_expect!(subj(), b";fo=", &mk_assignment(1));
141 | sit_expect!(subj(), b";for", &flush(1));
142 | sit_expect!(subj(), b";for=", &mk_assignment(1));
143 | sit_expect!(subj(), b";fork", &flush(1));
144 | sit_expect!(subj(), b";fork=", &mk_assignment(1));
145 | sit_expect!(subj(), b"((", &flush(0), &push_magic(0, 1, b')'));
146 | sit_expect!(subj(), b"[[", &flush(0), &push_magic(0, 1, b']'));
147 | }
148 |
149 | #[test]
150 | fn test_sit_arg() {
151 | let found_heredoc = push(
152 | (0, 8, None),
153 | Box::new(SitVec {
154 | terminator: vec![b'\\'],
155 | color: COLOR_HERE,
156 | }),
157 | );
158 | let subj = || {
159 | SitArg{end_trigger: 0}
160 | };
161 |
162 | sit_expect!(subj(), b"", &flush_or_pop(0));
163 | sit_expect!(subj(), b" ", &flush_or_pop(1));
164 | sit_expect!(subj(), b"arg", &flush_or_pop(3));
165 | sit_expect!(subj(), b"<<- \"\\\\\"\n", &found_heredoc);
166 | sit_expect!(subj(), b"a <<- \"\\\\\"", &flush(2));
167 | sit_expect!(subj(), b"a <<- \"\\", &flush(2));
168 | sit_expect!(subj(), b"a <<- ", &flush(2));
169 | sit_expect!(subj(), b"a <", &flush(2));
170 | sit_expect!(subj(), b"a ", &flush_or_pop(2));
171 | }
172 |
--------------------------------------------------------------------------------
/src/sitcomment.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush_or_pop;
13 | use crate::situation::COLOR_CMT;
14 | use crate::situation::pop;
15 |
16 | pub struct SitComment {}
17 |
18 | impl Situation for SitComment {
19 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
20 | for (i, &a) in horizon.input.iter().enumerate() {
21 | if a == b'\n' {
22 | return pop(i, 0, None);
23 | }
24 | }
25 | flush_or_pop(horizon.input.len())
26 | }
27 | fn get_color(&self) -> u32 {
28 | COLOR_CMT
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/sitextent.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush;
13 | use crate::situation::pop;
14 | use crate::situation::push;
15 |
16 | pub struct SitExtent{
17 | pub len: usize,
18 | pub color: u32,
19 | }
20 |
21 | impl Situation for SitExtent {
22 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
23 | if horizon.input.len() >= self.len {
24 | return pop(self.len, 0, None);
25 | }
26 | self.len -= horizon.input.len();
27 | flush(horizon.input.len())
28 | }
29 | fn get_color(&self) -> u32 {
30 | self.color
31 | }
32 | }
33 |
34 | pub fn push_extent(color: u32, pre: usize, len: usize) -> WhatNow {
35 | push((pre, 0, None), Box::new(SitExtent { len, color }))
36 | }
37 |
38 | pub fn push_replaceable(color: u32, pre: usize, len: usize, alt: Option<&'static [u8]>) -> WhatNow {
39 | push((pre, len, alt), Box::new(SitExtent { len: 0, color }))
40 | }
41 |
--------------------------------------------------------------------------------
/src/sitfor.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::situation::WhatNow;
13 | use crate::situation::flush;
14 | use crate::situation::pop;
15 | use crate::situation::push;
16 | use crate::situation::COLOR_KWD;
17 | use crate::situation::COLOR_VAR;
18 | use crate::situation::COLOR_LVAL;
19 | use crate::situation::COLOR_NORMAL;
20 |
21 | use crate::microparsers::identifierlen;
22 | use crate::microparsers::is_identifiertail;
23 | use crate::microparsers::is_whitespace;
24 | use crate::microparsers::predlen;
25 |
26 | use crate::sitextent::push_extent;
27 | use crate::commonargcmd::common_arg;
28 |
29 | pub struct SitFor {}
30 |
31 | impl Situation for SitFor {
32 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
33 | for (i, &a) in horizon.input.iter().enumerate() {
34 | if is_whitespace(a) && a != b'\n' {
35 | continue;
36 | }
37 | let len = identifierlen(&horizon.input[i..]);
38 | if i + len == horizon.input.len() && (i > 0 || horizon.is_lengthenable) {
39 | return flush(i);
40 | }
41 | if len > 0 {
42 | let word = &horizon.input[i..i+len];
43 | if word == b"in" {
44 | return push_forin(i);
45 | }
46 | return push_extent(COLOR_LVAL, i, len);
47 | }
48 | return pop(i, 0, None);
49 | }
50 | flush(horizon.input.len())
51 | }
52 | fn get_color(&self) -> u32 {
53 | COLOR_KWD
54 | }
55 | }
56 |
57 | pub struct SitForIn {}
58 |
59 | impl Situation for SitForIn {
60 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
61 | for (i, &a) in horizon.input.iter().enumerate() {
62 | if a == b'$' {
63 | let candidate = &horizon.input[i+1 ..];
64 | let idlen = identifierlen(candidate);
65 | let candidate = &candidate[idlen ..];
66 | let spacelen = predlen(|x| x == b' ', candidate);
67 | let candidate = &candidate[spacelen ..];
68 | if let Some(end) = candidate.iter().next() {
69 | if idlen >= 1 && matches!(end, b';' | b'\n') {
70 | return become_for_in_necessarily_array(i);
71 | }
72 | } else if i > 0 || horizon.is_lengthenable {
73 | return flush(i);
74 | }
75 | }
76 | if !is_whitespace(a) || a == b'\n' {
77 | return become_for_in_anything_else(i);
78 | }
79 | }
80 | flush(horizon.input.len())
81 | }
82 | fn get_color(&self) -> u32 {
83 | COLOR_NORMAL
84 | }
85 | }
86 |
87 | struct SitVarIdentNecessarilyArray {}
88 |
89 | impl Situation for SitVarIdentNecessarilyArray {
90 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
91 | for (i, &a) in horizon.input.iter().enumerate() {
92 | // An identifierhead is also an identifiertail.
93 | if !is_identifiertail(a) {
94 | return pop(i, 0, Some(b"[@]}\""));
95 | }
96 | }
97 | flush(horizon.input.len())
98 | }
99 | fn get_color(&self) -> u32 {
100 | COLOR_VAR
101 | }
102 | }
103 |
104 | pub struct SitForInAnythingElse {}
105 |
106 | impl Situation for SitForInAnythingElse {
107 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
108 | for (i, _) in horizon.input.iter().enumerate() {
109 | if let Some(res) = common_arg(u16::from(b';'), horizon, i) {
110 | return res;
111 | }
112 | }
113 | flush(horizon.input.len())
114 | }
115 | fn get_color(&self) -> u32 {
116 | COLOR_NORMAL
117 | }
118 | }
119 |
120 | fn push_forin(pre: usize) -> WhatNow {
121 | push((pre, 2, None), Box::new(SitForIn {}))
122 | }
123 |
124 | fn become_for_in_necessarily_array(pre: usize) -> WhatNow {
125 | WhatNow {
126 | transform: (pre, 1, Some(b"\"${")),
127 | transition: Transition::Replace(Box::new(SitVarIdentNecessarilyArray {})),
128 | }
129 | }
130 |
131 | fn become_for_in_anything_else(pre: usize) -> WhatNow {
132 | WhatNow {
133 | transform: (pre, 0, None),
134 | transition: Transition::Replace(Box::new(SitForInAnythingElse {})),
135 | }
136 | }
137 |
138 | #[cfg(test)]
139 | use crate::testhelpers::*;
140 |
141 | #[test]
142 | fn test_sit_for() {
143 | sit_expect!(SitFor{}, b"", &flush(0));
144 | sit_expect!(SitFor{}, b" ", &flush(1));
145 | sit_expect!(SitFor{}, b"\n", &pop(0, 0, None));
146 | sit_expect!(SitFor{}, b";", &pop(0, 0, None));
147 | sit_expect!(SitFor{}, b"_azAZ09\n", &push_extent(COLOR_LVAL, 0, 7));
148 | sit_expect!(SitFor{}, b"_azAZ09;", &push_extent(COLOR_LVAL, 0, 7));
149 | sit_expect!(SitFor{}, b"inn\n", &push_extent(COLOR_LVAL, 0, 3));
150 | sit_expect!(SitFor{}, b"inn;", &push_extent(COLOR_LVAL, 0, 3));
151 | sit_expect!(SitFor{}, b"in\n", &push_forin(0));
152 | sit_expect!(SitFor{}, b"in;", &push_forin(0));
153 | sit_expect!(SitFor{}, b"in ", &push_forin(0));
154 | sit_expect!(SitFor{}, b"in", &flush(0), &push_forin(0));
155 | }
156 |
157 | #[test]
158 | fn test_sit_forin() {
159 | sit_expect!(SitForIn{}, b"", &flush(0));
160 | sit_expect!(SitForIn{}, b" ", &flush(1));
161 | sit_expect!(SitForIn{}, b"a", &become_for_in_anything_else(0));
162 | sit_expect!(SitForIn{}, b" a", &become_for_in_anything_else(1));
163 | sit_expect!(SitForIn{}, b" \n", &become_for_in_anything_else(1));
164 | sit_expect!(SitForIn{}, b" ;", &become_for_in_anything_else(1));
165 | sit_expect!(SitForIn{}, b" $a", &flush(1));
166 | sit_expect!(SitForIn{}, b"$a", &flush(0), &become_for_in_anything_else(0));
167 | sit_expect!(SitForIn{}, b" $a\n", &become_for_in_necessarily_array(1));
168 | sit_expect!(SitForIn{}, b" $a;", &become_for_in_necessarily_array(1));
169 | sit_expect!(SitForIn{}, b" $a $a;", &become_for_in_anything_else(1));
170 | }
171 |
172 | #[test]
173 | fn test_sit_varidentnecessarilyarray() {
174 | let subj = || SitVarIdentNecessarilyArray {};
175 |
176 | sit_expect!(subj(), b"", &flush(0));
177 | sit_expect!(subj(), b"x", &flush(1));
178 | sit_expect!(subj(), b"x\n", &pop(1, 0, Some(b"[@]}\"")));
179 | }
180 |
181 | #[test]
182 | fn test_sit_forinanythingelse() {
183 | let subj = || SitForInAnythingElse {};
184 |
185 | sit_expect!(subj(), b"", &flush(0));
186 | sit_expect!(subj(), b";", &pop(0, 0, None));
187 | sit_expect!(subj(), b"\n", &pop(0, 0, None));
188 | }
189 |
--------------------------------------------------------------------------------
/src/sitmagic.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::situation::WhatNow;
13 | use crate::situation::flush;
14 | use crate::situation::pop;
15 | use crate::situation::COLOR_MAGIC;
16 |
17 | use crate::commonargcmd::common_token_quoting_unneeded;
18 |
19 | // Magic syntax (as opposed to builtin commands)
20 | pub struct SitMagic {
21 | pub end_trigger :u8,
22 | }
23 |
24 | impl Situation for SitMagic {
25 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
26 | for (i, &a) in horizon.input.iter().enumerate() {
27 | if a == b'(' {
28 | return push_magic(i, 1, b')');
29 | }
30 | if a == b'[' {
31 | return push_magic(i, 1, b']');
32 | }
33 | if a == self.end_trigger {
34 | return pop(i, 1, None);
35 | }
36 | if let Some(res) = common_token_quoting_unneeded(0x100, horizon, i) {
37 | return res;
38 | }
39 | }
40 | flush(horizon.input.len())
41 | }
42 | fn get_color(&self) -> u32 {
43 | COLOR_MAGIC
44 | }
45 | }
46 |
47 | pub fn push_magic(pre: usize, len: usize, end_trigger: u8) -> WhatNow {
48 | WhatNow {
49 | transform: (pre, len, None),
50 | transition: Transition::Push(Box::new(SitMagic { end_trigger })),
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/sitrvalue.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2018 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::Transition;
13 | use crate::situation::pop;
14 | use crate::situation::push;
15 | use crate::situation::flush;
16 | use crate::situation::flush_or_pop;
17 | use crate::situation::COLOR_NORMAL;
18 | use crate::situation::COLOR_LVAL;
19 |
20 | use crate::microparsers::is_whitespace;
21 |
22 | use crate::commonargcmd::common_cmd_quoting_unneeded;
23 | use crate::commonargcmd::common_expr;
24 |
25 | pub struct SitLvalue {
26 | pub len :usize,
27 | pub end_trigger :u16,
28 | }
29 |
30 | impl Situation for SitLvalue {
31 | fn whatnow(&mut self, _: Horizon) -> WhatNow {
32 | WhatNow {
33 | transform: (self.len, 1, None),
34 | transition: Transition::Replace(Box::new(SitRvalue{ end_trigger: self.end_trigger })),
35 | }
36 | }
37 | fn get_color(&self) -> u32 {
38 | COLOR_LVAL
39 | }
40 | }
41 |
42 | struct SitRvalue {
43 | end_trigger :u16,
44 | }
45 |
46 | impl Situation for SitRvalue {
47 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
48 | for (i, &a) in horizon.input.iter().enumerate() {
49 | if a == b'(' {
50 | return push((i, 1, None), Box::new(SitArray {}));
51 | }
52 | if let Some(res) = common_cmd_quoting_unneeded(self.end_trigger, horizon, i) {
53 | return res;
54 | }
55 | if is_whitespace(a) {
56 | return pop(i, 1, None);
57 | }
58 | }
59 | flush_or_pop(horizon.input.len())
60 | }
61 | fn get_color(&self) -> u32 {
62 | COLOR_NORMAL
63 | }
64 | }
65 |
66 | struct SitArray {}
67 |
68 | impl Situation for SitArray {
69 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
70 | for (i, _) in horizon.input.iter().enumerate() {
71 | if let Some(res) = common_expr(u16::from(b')'), horizon, i) {
72 | return res;
73 | }
74 | }
75 | flush(horizon.input.len())
76 | }
77 | fn get_color(&self) -> u32 {
78 | COLOR_NORMAL
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/sitstrdq.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2022 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush;
13 | use crate::situation::pop;
14 |
15 | use crate::commonstrcmd::QuotingCtx;
16 | use crate::commonstrcmd::CommonStrCmdResult;
17 | use crate::commonstrcmd::common_str_cmd;
18 |
19 | pub struct SitStrDq {
20 | interpolation_detection: QuotingCtx,
21 | }
22 |
23 | impl SitStrDq {
24 | pub fn new() -> SitStrDq {
25 | SitStrDq{ interpolation_detection: QuotingCtx::Dontneed }
26 | }
27 | }
28 |
29 | impl Situation for SitStrDq {
30 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
31 | for (i, &a) in horizon.input.iter().enumerate() {
32 | if a == b'\"' {
33 | return pop(i, 1, None);
34 | }
35 | match common_str_cmd(horizon, i, self.interpolation_detection) {
36 | CommonStrCmdResult::None => {
37 | self.interpolation_detection = QuotingCtx::Interpolation;
38 | }
39 | CommonStrCmdResult::Some(x) |
40 | CommonStrCmdResult::OnlyWithQuotes(x) => {
41 | let (pre, len, _) = x.transform;
42 | let progress = pre + len;
43 | if progress != 0 {
44 | self.interpolation_detection = QuotingCtx::Interpolation;
45 | }
46 | return x;
47 | }
48 | }
49 | }
50 | flush(horizon.input.len())
51 | }
52 | fn get_color(&self) -> u32 {
53 | 0x00_ff0000
54 | }
55 | }
56 |
57 | #[cfg(test)]
58 | use crate::testhelpers::*;
59 | #[cfg(test)]
60 | use crate::sitcmd::SitNormal;
61 | #[cfg(test)]
62 | use crate::sitextent::push_extent;
63 | #[cfg(test)]
64 | use crate::sitmagic::push_magic;
65 | #[cfg(test)]
66 | use crate::situation::push;
67 | #[cfg(test)]
68 | use crate::situation::COLOR_ESC;
69 |
70 | #[test]
71 | fn test_sit_strdq() {
72 | let found_cmdsub = push(
73 | (0, 2, None),
74 | Box::new(SitNormal {
75 | end_trigger: u16::from(b')'),
76 | end_replace: None,
77 | }),
78 | );
79 | sit_expect!(SitStrDq::new(), b"", &flush(0));
80 | sit_expect!(SitStrDq::new(), b"$", &flush(0), &flush(1));
81 | sit_expect!(SitStrDq::new(), b"$(", &flush(0), &found_cmdsub);
82 | sit_expect!(SitStrDq::new(), b"$( ", &found_cmdsub);
83 | sit_expect!(SitStrDq::new(), b"$((", &push_magic(0, 2, b')'));
84 | sit_expect!(SitStrDq::new(), b"\\", &push_extent(COLOR_ESC, 0, 2));
85 | }
86 |
--------------------------------------------------------------------------------
/src/sitstrphantom.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::situation::WhatNow;
13 | use crate::situation::flush;
14 | use crate::situation::pop;
15 |
16 | use crate::commonstrcmd::QuotingCtx;
17 | use crate::commonstrcmd::CommonStrCmdResult;
18 | use crate::commonstrcmd::common_str_cmd;
19 |
20 | use crate::microparsers::predlen;
21 | use crate::microparsers::is_word;
22 |
23 | use crate::sitstrdq::SitStrDq;
24 |
25 | pub struct SitStrPhantom {
26 | pub cmd_end_trigger: u16,
27 | }
28 |
29 | impl Situation for SitStrPhantom {
30 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
31 | let mouthful = predlen(is_phantomstringfood, horizon.input);
32 | if mouthful == horizon.input.len() {
33 | if horizon.is_lengthenable {
34 | return flush(0);
35 | }
36 | } else if u16::from(horizon.input[mouthful]) != self.cmd_end_trigger {
37 | match horizon.input[mouthful] {
38 | b'\"' => {
39 | return become_real(mouthful);
40 | }
41 | b'$' | b'`' => {
42 | match common_str_cmd(horizon, mouthful, QuotingCtx::Need) {
43 | CommonStrCmdResult::None => {}
44 | CommonStrCmdResult::Some(consult) |
45 | CommonStrCmdResult::OnlyWithQuotes(consult) => {
46 | match consult.transition {
47 | Transition::Flush | Transition::FlushPopOnEof => {
48 | if horizon.is_lengthenable {
49 | return flush(0);
50 | }
51 | }
52 | Transition::Pop | Transition::Replace(_) => {}
53 | Transition::Push(_) | Transition::Err(_) => {
54 | return consult;
55 | }
56 | }
57 | }
58 | }
59 | }
60 | _ => {}
61 | }
62 | }
63 | dutifully_end_the_string()
64 | }
65 | fn get_color(&self) -> u32 {
66 | 0x00_ff0000
67 | }
68 | }
69 |
70 | fn is_phantomstringfood(c: u8) -> bool {
71 | c >= b'+' && is_word(c)
72 | && c != b'?' && c != b'\\'
73 | }
74 |
75 | fn become_real(pre: usize) -> WhatNow {
76 | WhatNow {
77 | transform: (pre, 1, Some(b"")),
78 | transition: Transition::Replace(Box::new(SitStrDq::new())),
79 | }
80 | }
81 |
82 | fn dutifully_end_the_string() -> WhatNow {
83 | pop(0, 0, Some(b"\""))
84 | }
85 |
86 | #[cfg(test)]
87 | use crate::testhelpers::*;
88 | #[cfg(test)]
89 | use crate::sitcmd::SitNormal;
90 | #[cfg(test)]
91 | use crate::sitextent::push_extent;
92 | #[cfg(test)]
93 | use crate::situation::COLOR_VAR;
94 | #[cfg(test)]
95 | use crate::situation::push;
96 |
97 | #[cfg(test)]
98 | fn subject() -> SitStrPhantom {
99 | SitStrPhantom{cmd_end_trigger: 0}
100 | }
101 |
102 | #[test]
103 | fn test_sit_strphantom() {
104 | let cod = dutifully_end_the_string();
105 | let found_cmdsub = push(
106 | (0, 2, None),
107 | Box::new(SitNormal {
108 | end_trigger: u16::from(b')'),
109 | end_replace: None,
110 | }),
111 | );
112 | sit_expect!(subject(), b"", &flush(0), &cod);
113 | sit_expect!(subject(), b"a", &flush(0), &cod);
114 | sit_expect!(subject(), b" ", &cod);
115 | sit_expect!(subject(), b"\\", &cod);
116 | sit_expect!(subject(), b"\'", &cod);
117 | sit_expect!(subject(), b"\"", &become_real(0));
118 | sit_expect!(subject(), b"$", &flush(0), &cod);
119 | sit_expect!(subject(), b"$(", &flush(0), &found_cmdsub);
120 | sit_expect!(subject(), b"a$", &flush(0), &cod);
121 | sit_expect!(subject(), b"a$(", &flush(0), &cod);
122 | sit_expect!(subject(), b"$\'", &cod);
123 | sit_expect!(subject(), b"$\"", &cod);
124 | sit_expect!(subject(), b"$@", &push_extent(COLOR_VAR, 0, 2));
125 | sit_expect!(subject(), b"$*", &push_extent(COLOR_VAR, 0, 2));
126 | sit_expect!(subject(), b"$#", &push_extent(COLOR_VAR, 0, 2));
127 | sit_expect!(subject(), b"$?", &push_extent(COLOR_VAR, 0, 2));
128 | sit_expect!(subject(), b"$-", &push_extent(COLOR_VAR, 0, 2));
129 | sit_expect!(subject(), b"$$", &push_extent(COLOR_VAR, 0, 2));
130 | sit_expect!(subject(), b"$!", &push_extent(COLOR_VAR, 0, 2));
131 | }
132 |
--------------------------------------------------------------------------------
/src/sitstrsqesc.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush;
13 | use crate::situation::pop;
14 | use crate::situation::COLOR_SQESC;
15 | use crate::situation::COLOR_ESC;
16 |
17 | use crate::sitextent::push_extent;
18 |
19 | pub struct SitStrSqEsc {}
20 |
21 | impl Situation for SitStrSqEsc {
22 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
23 | for (i, &a) in horizon.input.iter().enumerate() {
24 | if a == b'\\' {
25 | return push_extent(COLOR_ESC, i, 2);
26 | }
27 | if a == b'\'' {
28 | return pop(i, 1, None);
29 | }
30 | }
31 | flush(horizon.input.len())
32 | }
33 | fn get_color(&self) -> u32 {
34 | COLOR_SQESC
35 | }
36 | }
37 |
38 | #[cfg(test)]
39 | use crate::testhelpers::*;
40 |
41 | #[test]
42 | fn test_sit_strsqesc() {
43 | sit_expect!(SitStrSqEsc{}, b"", &flush(0));
44 | sit_expect!(SitStrSqEsc{}, b"$", &flush(1));
45 | sit_expect!(SitStrSqEsc{}, b"\\", &push_extent(COLOR_ESC, 0, 2));
46 | sit_expect!(SitStrSqEsc{}, b"\'", &pop(0, 1, None));
47 | }
48 |
--------------------------------------------------------------------------------
/src/sittest.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021-2022 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::COLOR_NORMAL;
10 | use crate::situation::Horizon;
11 | use crate::situation::Situation;
12 | use crate::situation::Transition;
13 | use crate::situation::WhatNow;
14 | use crate::situation::flush;
15 | use crate::situation::flush_or_pop;
16 | use crate::situation::push;
17 | use crate::situation::COLOR_CMD;
18 |
19 | use crate::commonargcmd::common_arg;
20 | use crate::commonargcmd::common_token;
21 | use crate::machine::expression_tracker;
22 | use crate::microparsers::is_word;
23 | use crate::microparsers::prefixlen;
24 |
25 | use crate::sitcmd::SitArg;
26 |
27 | pub struct SitTest {
28 | pub end_trigger :u16,
29 | }
30 |
31 | impl Situation for SitTest {
32 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
33 | if horizon.input.len() >= 4 {
34 | let is_emptystringtest = prefixlen(horizon.input, b"-z ") == 3;
35 | let is_nonemptystringtest = prefixlen(horizon.input, b"-n ") == 3;
36 | if is_emptystringtest || is_nonemptystringtest {
37 | let suggest = common_token(self.end_trigger, horizon, 3);
38 | if let Some(ref exciting) = suggest {
39 | if let Transition::Push(_) = &exciting.transition {
40 | let end_replace: &'static [u8] = if is_emptystringtest {
41 | b" = \"\""
42 | } else {
43 | b" != \"\""
44 | };
45 | return push_hiddentest(suggest, end_replace, self.end_trigger);
46 | } else if horizon.is_lengthenable {
47 | return flush(0);
48 | }
49 | }
50 | } else if prefixlen(horizon.input, b"x") == 1 {
51 | if let Some(mut suggest) = common_token(self.end_trigger, horizon, 1) {
52 | if let Transition::Push(_) = &suggest.transition {
53 | let transition = std::mem::replace(&mut suggest.transition, Transition::Flush);
54 | if let Transition::Push(state) = transition {
55 | let (pre, len, _) = suggest.transform;
56 | let progress = pre + len;
57 | if let Ok(found) = find_xyes_comparison(&horizon.input[progress ..], state) {
58 | if found {
59 | return push_xyes(self.end_trigger);
60 | }
61 | if horizon.is_lengthenable {
62 | return flush(0);
63 | }
64 | }
65 | }
66 | } else {
67 | return suggest;
68 | }
69 | }
70 | }
71 | } else if horizon.is_lengthenable {
72 | return flush(0);
73 | }
74 | become_regular(self.end_trigger)
75 | }
76 | fn get_color(&self) -> u32 {
77 | COLOR_CMD
78 | }
79 | }
80 |
81 | fn become_regular(end_trigger :u16) -> WhatNow {
82 | become_regular_with((0, 0, None), end_trigger)
83 | }
84 |
85 | fn become_regular_with(
86 | transform: (usize, usize, Option<&'static [u8]>),
87 | end_trigger :u16,
88 | ) -> WhatNow {
89 | WhatNow {
90 | transform,
91 | transition: Transition::Replace(Box::new(SitArg { end_trigger })),
92 | }
93 | }
94 |
95 | fn push_hiddentest(
96 | inner: Option,
97 | end_replace: &'static [u8],
98 | end_trigger: u16,
99 | ) -> WhatNow {
100 | push(
101 | (0, 3, Some(b"")),
102 | Box::new(SitHiddenTest {
103 | inner,
104 | end_replace,
105 | end_trigger,
106 | }),
107 | )
108 | }
109 |
110 | fn push_xyes(end_trigger: u16) -> WhatNow {
111 | push((0, 1, Some(b"")), Box::new(SitXyes { end_trigger }))
112 | }
113 |
114 | struct SitHiddenTest {
115 | inner: Option,
116 | end_replace: &'static [u8],
117 | end_trigger: u16,
118 | }
119 |
120 | impl Situation for SitHiddenTest {
121 | fn whatnow(&mut self, _horizon: Horizon) -> WhatNow {
122 | let initial_adventure = self.inner.take();
123 | if let Some(mut exciting) = initial_adventure {
124 | exciting.transform.0 = 0;
125 | exciting
126 | } else {
127 | become_regular_with((0, 0, Some(self.end_replace)), self.end_trigger)
128 | }
129 | }
130 | fn get_color(&self) -> u32 {
131 | COLOR_NORMAL
132 | }
133 | }
134 |
135 | struct SitXyes {
136 | end_trigger :u16,
137 | }
138 |
139 | impl Situation for SitXyes {
140 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
141 | for (i, &a) in horizon.input.iter().enumerate() {
142 | if a == b'x' {
143 | let mut replacement: &'static [u8] = b"\"\"";
144 | if i+1 < horizon.input.len() {
145 | if is_word(horizon.input[i+1]) {
146 | replacement = b"";
147 | }
148 | } else if i > 0 || horizon.is_lengthenable {
149 | return flush(i);
150 | }
151 | return become_regular_with((i, 1, Some(replacement)), self.end_trigger);
152 | }
153 | if let Some(res) = common_arg(self.end_trigger, horizon, i) {
154 | return res;
155 | }
156 | }
157 | flush_or_pop(horizon.input.len())
158 | }
159 | fn get_color(&self) -> u32 {
160 | COLOR_NORMAL
161 | }
162 | }
163 |
164 | fn find_xyes_comparison(horizon: &[u8], state: Box) -> Result {
165 | let (found, exprlen) = expression_tracker(horizon, state)?;
166 | let after = &horizon[exprlen ..];
167 |
168 | Ok(found && has_rhs_xyes(after))
169 | }
170 |
171 | fn has_rhs_xyes(horizon: &[u8]) -> bool {
172 | #[derive(Clone)]
173 | #[derive(Copy)]
174 | enum Lex {
175 | Start,
176 | FirstSpace,
177 | Negation,
178 | FirstEq,
179 | SecondEq,
180 | SecondSpace,
181 | }
182 | let mut state = Lex::Start;
183 |
184 | for byte in horizon {
185 | match (state, byte) {
186 | (Lex::Start, b' ') => state = Lex::FirstSpace,
187 | (Lex::FirstSpace, b'=') => state = Lex::FirstEq,
188 | (Lex::FirstSpace, b'!') => state = Lex::Negation,
189 | (Lex::Negation, b'=') => state = Lex::SecondEq,
190 | (Lex::FirstEq, b'=') => state = Lex::SecondEq,
191 | (Lex::FirstEq, b' ') => state = Lex::SecondSpace,
192 | (Lex::SecondEq, b' ') => state = Lex::SecondSpace,
193 | (Lex::SecondSpace, b'x') => return true,
194 | (_, _) => break,
195 | }
196 | }
197 | false
198 | }
199 |
200 | #[cfg(test)]
201 | use crate::testhelpers::*;
202 | #[cfg(test)]
203 | use crate::situation::pop;
204 |
205 | #[test]
206 | fn test_sit_test() {
207 | let subj = || SitTest { end_trigger: 0u16 };
208 |
209 | sit_expect!(subj(), b"", &flush(0), &become_regular(0u16));
210 |
211 | sit_expect!(subj(), b"-f $are ", &become_regular(0u16));
212 | sit_expect!(subj(), b"-z $are ", &push_hiddentest(None, b"", 0u16));
213 | sit_expect!(subj(), b"-n $are ", &push_hiddentest(None, b"", 0u16));
214 | sit_expect!(subj(), b"-z justkidding ", &become_regular(0u16));
215 | sit_expect!(subj(), b"-n justkidding ", &become_regular(0u16));
216 | sit_expect!(subj(), b"-z \"", &push_hiddentest(None, b"", 0u16));
217 | sit_expect!(subj(), b"-n \"", &push_hiddentest(None, b"", 0u16));
218 | sit_expect!(subj(), b"-n \0", &flush(0), &become_regular(0u16));
219 |
220 | sit_expect!(subj(), b"x ", &become_regular(0u16));
221 | sit_expect!(subj(), b"x\0 = x", &pop(1, 0, None));
222 | sit_expect!(subj(), b"x$( ", &flush(0), &become_regular(0u16));
223 | sit_expect!(subj(), b"x\"$(echo)\" = ", &flush(0), &become_regular(0u16));
224 | sit_expect!(subj(), b"x\"$(echo)\" = x", &push_xyes(0u16));
225 | sit_expect!(subj(), b"x$(echo) = x", &push_xyes(0u16));
226 | sit_expect!(subj(), b"x`echo` == x", &push_xyes(0u16));
227 | sit_expect!(subj(), b"x\"$yes\" != x", &push_xyes(0u16));
228 | sit_expect!(subj(), b"x$yes = x", &push_xyes(0x16));
229 | sit_expect!(subj(), b"x$yes = y", &flush(0), &become_regular(0u16));
230 | sit_expect!(subj(), b"$yes = x", &become_regular(0u16));
231 | sit_expect!(subj(), b"x$yes = x$1", &push_xyes(0x16));
232 | sit_expect!(subj(), b"x`$10` = x", &become_regular(0u16));
233 | }
234 |
235 | #[test]
236 | fn test_sit_xyes() {
237 | let subj = || SitXyes { end_trigger: 0u16 };
238 |
239 | sit_expect!(subj(), b" = ", &flush_or_pop(3));
240 | sit_expect!(subj(), b" = x", &flush(3));
241 | sit_expect!(subj(), b"x", &flush(0), &become_regular_with((0, 1, Some(b"\"\"")), 0u16));
242 | sit_expect!(subj(), b" = x ", &become_regular_with((3, 1, Some(b"\"\"")), 0u16));
243 | sit_expect!(subj(), b" = x;", &become_regular_with((3, 1, Some(b"\"\"")), 0u16));
244 | sit_expect!(subj(), b" = xx", &become_regular_with((3, 1, Some(b"")), 0u16));
245 | }
246 |
247 | #[test]
248 | fn test_has_rhs_xyes() {
249 | assert!(has_rhs_xyes(b" = x"));
250 | assert!(has_rhs_xyes(b" != x"));
251 | assert!(has_rhs_xyes(b" == x"));
252 | assert!(!has_rhs_xyes(b" = "));
253 | assert!(!has_rhs_xyes(b" = y"));
254 | assert!(!has_rhs_xyes(b"= x"));
255 | assert!(!has_rhs_xyes(b" =x"));
256 | assert!(!has_rhs_xyes(b" x"));
257 | assert!(!has_rhs_xyes(b" ! x"));
258 | }
259 |
--------------------------------------------------------------------------------
/src/situation.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2024 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | #[derive(Copy)]
10 | #[derive(Clone)]
11 | pub struct Horizon<'a>{
12 | pub input: &'a [u8],
13 | pub is_lengthenable: bool,
14 | }
15 |
16 | pub trait Situation {
17 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow;
18 | fn get_color(&self) -> u32;
19 | }
20 |
21 | pub struct UnsupportedSyntax {
22 | pub typ: &'static str,
23 | pub msg: &'static str,
24 | }
25 |
26 | pub enum Transition {
27 | Flush,
28 | FlushPopOnEof,
29 | Replace(Box),
30 | Push(Box),
31 | Pop,
32 | Err(UnsupportedSyntax),
33 | }
34 |
35 | pub struct WhatNow {
36 | pub transform: (usize, usize, Option<&'static [u8]>), // pre, len, alt
37 | pub transition: Transition,
38 | }
39 |
40 | pub fn flush(pre: usize) -> WhatNow {
41 | WhatNow {
42 | transform: (pre, 0, None),
43 | transition: Transition::Flush,
44 | }
45 | }
46 |
47 | pub fn flush_or_pop(pre: usize) -> WhatNow {
48 | WhatNow {
49 | transform: (pre, 0, None),
50 | transition: Transition::FlushPopOnEof,
51 | }
52 | }
53 |
54 | pub fn pop(pre: usize, len: usize, alt: Option<&'static [u8]>) -> WhatNow {
55 | WhatNow {
56 | transform: (pre, len, alt),
57 | transition: Transition::Pop,
58 | }
59 | }
60 |
61 | pub fn push(transform: (usize, usize, Option<&'static [u8]>), sit: Box) -> WhatNow {
62 | WhatNow {
63 | transform,
64 | transition: Transition::Push(sit),
65 | }
66 | }
67 |
68 | pub fn if_needed(needed: bool, val: T) -> Option {
69 | if needed { Some(val) } else { None }
70 | }
71 |
72 | pub const COLOR_NORMAL: u32 = 0x00_000000;
73 | const COLOR_BOLD : u32 = 0x01_000000;
74 | const COLOR_ITAL : u32 = 0x02_000000;
75 | const COLOR_GOLD : u32 = 0x00_ffcc55;
76 |
77 | pub const COLOR_KWD : u32 = COLOR_BOLD;
78 | pub const COLOR_CMD : u32 = 0x00_c00080;
79 | pub const COLOR_MAGIC : u32 = 0x00_c000c0;
80 | pub const COLOR_VAR : u32 = 0x00_3f7fcf;
81 | pub const COLOR_LVAL : u32 = 0x00_007fff;
82 | pub const COLOR_HERE : u32 = 0x00_802000;
83 | pub const COLOR_CMT : u32 = 0x00_789060 | COLOR_BOLD | COLOR_ITAL;
84 | pub const COLOR_SQ : u32 = COLOR_GOLD;
85 | pub const COLOR_ESC : u32 = 0x00_ff0080 | COLOR_BOLD;
86 | pub const COLOR_SQESC : u32 = 0x00_ff8000;
87 |
--------------------------------------------------------------------------------
/src/situntilbyte.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2016 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush;
13 | use crate::situation::pop;
14 |
15 | use crate::microparsers::predlen;
16 |
17 | pub struct SitUntilByte {
18 | pub until: u8,
19 | pub color: u32,
20 | }
21 |
22 | impl Situation for SitUntilByte {
23 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
24 | let len = predlen(|x| x != self.until, horizon.input);
25 | if len < horizon.input.len() {
26 | pop(len, 1, None)
27 | } else {
28 | flush(len)
29 | }
30 | }
31 | fn get_color(&self) -> u32 {
32 | self.color
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/sitvarbrace.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2021 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::WhatNow;
12 | use crate::situation::flush;
13 | use crate::situation::if_needed;
14 | use crate::situation::pop;
15 | use crate::situation::COLOR_VAR;
16 |
17 | use crate::sitextent::push_replaceable;
18 |
19 | #[derive(Clone)]
20 | #[derive(Copy)]
21 | enum State{
22 | Name,
23 | Index,
24 | Normal,
25 | Dollar,
26 | Escape,
27 | }
28 |
29 | pub struct SitVarBrace {
30 | end_rm: bool,
31 | state: State,
32 | depth: usize,
33 | }
34 |
35 | impl SitVarBrace {
36 | pub fn new(end_rm: bool, replace_s11n: bool) -> SitVarBrace {
37 | SitVarBrace{
38 | end_rm,
39 | state: if replace_s11n { State::Name } else { State::Normal },
40 | depth: 1,
41 | }
42 | }
43 | }
44 |
45 | impl Situation for SitVarBrace {
46 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
47 | for (i, c) in horizon.input.iter().enumerate() {
48 | match (self.state, c) {
49 | (State::Name, b'a' ..= b'z') |
50 | (State::Name, b'A' ..= b'Z') |
51 | (State::Name, b'0' ..= b'9') |
52 | (State::Name, b'_') => {}
53 | (State::Name, b'[') => self.state = State::Index,
54 | (State::Index, b'*') => {
55 | self.state = State::Normal;
56 | return push_replaceable(COLOR_VAR, i, 1, Some(b"@"));
57 | }
58 | (State::Normal, b'$') => self.state = State::Dollar,
59 | (State::Dollar, b'{') => self.depth += 1,
60 | (State::Name | State::Index | State::Normal | State::Dollar, b'}') => {
61 | self.depth -= 1;
62 | if self.depth == 0 {
63 | return pop(i, 1, if_needed(self.end_rm, b""));
64 | }
65 | }
66 | (State::Name, _) => self.state = State::Normal,
67 | (State::Index, _) => self.state = State::Normal,
68 | (State::Normal, b'\\') => self.state = State::Escape,
69 | (State::Normal, _) => {}
70 | (State::Dollar, _) |
71 | (State::Escape, _) => self.state = State::Normal,
72 | }
73 | }
74 | flush(horizon.input.len())
75 | }
76 | fn get_color(&self) -> u32 {
77 | COLOR_VAR
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/src/sitvarident.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::Horizon;
10 | use crate::situation::Situation;
11 | use crate::situation::Transition;
12 | use crate::situation::WhatNow;
13 | use crate::situation::COLOR_VAR;
14 |
15 | use crate::microparsers::predlen;
16 | use crate::microparsers::is_identifiertail;
17 |
18 | pub struct SitVarIdent {
19 | pub end_insert: Option<&'static [u8]>,
20 | }
21 |
22 | impl Situation for SitVarIdent {
23 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
24 | let len = predlen(is_identifiertail, horizon.input);
25 | WhatNow {
26 | transform: (len, 0, self.end_insert),
27 | transition: if len < horizon.input.len() {
28 | Transition::Pop
29 | } else {
30 | Transition::FlushPopOnEof
31 | },
32 | }
33 | }
34 | fn get_color(&self) -> u32 {
35 | COLOR_VAR
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/src/sitvec.rs:
--------------------------------------------------------------------------------
1 | /*
2 | * Copyright 2017 - 2019 Andreas Nordal
3 | *
4 | * This Source Code Form is subject to the terms of the Mozilla Public
5 | * License, v. 2.0. If a copy of the MPL was not distributed with this
6 | * file, You can obtain one at http://mozilla.org/MPL/2.0/.
7 | */
8 |
9 | use crate::situation::flush;
10 | use crate::situation::pop;
11 | use crate::situation::Horizon;
12 | use crate::situation::Situation;
13 | use crate::situation::WhatNow;
14 |
15 | pub struct SitVec {
16 | pub terminator :Vec,
17 | pub color: u32,
18 | }
19 |
20 | impl Situation for SitVec {
21 | fn whatnow(&mut self, horizon: Horizon) -> WhatNow {
22 | if horizon.input.len() < self.terminator.len() {
23 | if horizon.is_lengthenable {
24 | flush(0)
25 | } else {
26 | flush(horizon.input.len())
27 | }
28 | }
29 | else if horizon.input[0 .. self.terminator.len()] == self.terminator[..] {
30 | pop(0, self.terminator.len(), None)
31 | } else {
32 | flush(1)
33 | }
34 | }
35 | fn get_color(&self) -> u32 {
36 | self.color
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/testhelpers.rs:
--------------------------------------------------------------------------------
1 |
2 | use crate::situation::WhatNow;
3 | use crate::situation::Situation;
4 | use crate::situation::Transition;
5 | use crate::situation::Transition::Flush;
6 | use crate::situation::Transition::FlushPopOnEof;
7 | use crate::situation::Transition::Replace;
8 | use crate::situation::Transition::Push;
9 | use crate::situation::Transition::Pop;
10 |
11 | pub fn whatnow_eq(horizon_len: usize, actual: &WhatNow, expected: &WhatNow) -> bool {
12 | assert!(actual.transform.0 + actual.transform.1 <= horizon_len);
13 |
14 | let mut eq = true;
15 | if actual.transform.0 != expected.transform.0 {
16 | eprintln!("WhatNow.pre: {} != {}", actual.transform.0, expected.transform.0);
17 | eq = false;
18 | }
19 | if actual.transform.1 != expected.transform.1 {
20 | eprintln!("WhatNow.len: {} != {}", actual.transform.1, expected.transform.1);
21 | eq = false;
22 | }
23 | if actual.transform.2 != expected.transform.2 {
24 | eprintln!("WhatNow.alt mismatch");
25 | eq = false;
26 | }
27 | transition_eq(&actual.transition, &expected.transition) && eq
28 | }
29 |
30 | fn transition_eq(a: &Transition, b: &Transition) -> bool {
31 | match (a, b) {
32 | (Flush, Flush) => true,
33 | (Flush, _) => {
34 | eprintln!("Transition mismatch; Lhs={}", "Flush");
35 | false
36 | }
37 | (FlushPopOnEof, FlushPopOnEof) => true,
38 | (FlushPopOnEof, _) => {
39 | eprintln!("Transition mismatch; Lhs={}", "FlushPopOnEof");
40 | false
41 | }
42 | (Replace(a), Replace(b)) => sit_eq(a.as_ref(), b.as_ref()),
43 | (Replace(_), _) => {
44 | eprintln!("Transition mismatch; Lhs={}", "Replace");
45 | false
46 | }
47 | (Push(a), Push(b)) => sit_eq(a.as_ref(), b.as_ref()),
48 | (Push(_), _) => {
49 | eprintln!("Transition mismatch; Lhs={}", "Push");
50 | false
51 | }
52 | (Pop, Pop) => true,
53 | (Pop, _) => {
54 | eprintln!("Transition mismatch; Lhs={}", "Pop");
55 | false
56 | }
57 | (Transition::Err(_), Transition::Err(_)) => true,
58 | (Transition::Err(_), _) => {
59 | eprintln!("Transition mismatch; Lhs={}", "Err");
60 | false
61 | }
62 | }
63 | }
64 |
65 | // FIXME: Compare vtable pointers.
66 | fn sit_eq(a: &dyn Situation, b: &dyn Situation) -> bool {
67 | if a.get_color() != b.get_color() {
68 | eprintln!("Situation.color: {} != {}", a.get_color(), b.get_color());
69 | false
70 | } else {
71 | true
72 | }
73 | }
74 |
75 | macro_rules! sit_expect {
76 | ($sit:expr, $inputhorizon:expr, $expect_mid:expr, $expect_eof:expr) => {
77 | assert!(whatnow_eq($inputhorizon.len(), &$sit.whatnow(Horizon{input: $inputhorizon, is_lengthenable: true}), $expect_mid));
78 | assert!(whatnow_eq($inputhorizon.len(), &$sit.whatnow(Horizon{input: $inputhorizon, is_lengthenable: false}), $expect_eof));
79 | };
80 | ($sit:expr, $inputhorizon:expr, $expect_same:expr) => {
81 | assert!(whatnow_eq($inputhorizon.len(), &$sit.whatnow(Horizon{input: $inputhorizon, is_lengthenable: true}), $expect_same));
82 | assert!(whatnow_eq($inputhorizon.len(), &$sit.whatnow(Horizon{input: $inputhorizon, is_lengthenable: false}), $expect_same));
83 | };
84 | }
85 |
--------------------------------------------------------------------------------
/tests/moduletest.rs:
--------------------------------------------------------------------------------
1 | use std::env;
2 | use std::process;
3 | use std::process::Command;
4 |
5 | #[test]
6 | fn moduletest() {
7 | let mut child = Command::new("moduletests/run")
8 | .arg(env!("CARGO_BIN_EXE_shellharden"))
9 | .arg("moduletests")
10 | .spawn()
11 | .expect("moduletests/run: Command not found")
12 | ;
13 | match &child.wait() {
14 | &Ok(waitresult) => {
15 | if let Some(status) = waitresult.code() {
16 | process::exit(status);
17 | }
18 | assert!(false);
19 | }
20 | &Err(_) => assert!(false),
21 | }
22 | }
23 |
--------------------------------------------------------------------------------