├── .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 | [![Build and test status](https://github.com/anordal/shellharden/workflows/build-and-tests/badge.svg?branch=master)](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 | ![real-world example](img/ex-realworld.png) 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 | ![artificial example](img/ex-artificial.png) 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 | [![Packaging status](https://repology.org/badge/vertical-allrepos/shellharden.svg)](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 | --------------------------------------------------------------------------------