├── .github └── workflows │ ├── ci.yaml │ └── release.yml ├── .gitignore ├── CHANGELOG.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src ├── lib.rs ├── main.rs ├── opts.rs └── util.rs └── tests ├── file-test.rs └── resources ├── after ├── ac.rs ├── ac │ ├── ac2.rs │ └── ac2 │ │ └── ac3.rs ├── interrupt.rs └── lib.rs └── small-lib-before.rs /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: [ staging, trying, main, workflow-test ] 4 | pull_request: 5 | 6 | name: CI 7 | 8 | jobs: 9 | check: 10 | name: Cargo check 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | TARGET: [x86_64-unknown-linux-gnu, x86_64-pc-windows-msvc] 15 | 16 | steps: 17 | - uses: actions/checkout@v4 18 | 19 | - uses: dtolnay/rust-toolchain@stable 20 | with: 21 | targets: ${{ matrix.TARGET }} 22 | 23 | - name: Cache Dependencies 24 | uses: Swatinem/rust-cache@v2 25 | with: 26 | key: ${{ matrix.TARGET }} 27 | 28 | - run: cargo check --target ${{ matrix.TARGET }} 29 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | on: 3 | push: 4 | branches: 5 | - main 6 | tags: 7 | - v*.*.* 8 | workflow_dispatch: 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | matrix: 14 | include: 15 | - { target: x86_64-unknown-linux-gnu, os: ubuntu-latest, suffix: .gz } 16 | - { target: x86_64-apple-darwin, os: macos-latest, suffix: .gz } 17 | - { target: x86_64-pc-windows-msvc, os: windows-latest, suffix: .zip } 18 | runs-on: ${{ matrix.os }} 19 | steps: 20 | - uses: actions/checkout@v4 21 | - uses: dtolnay/rust-toolchain@stable 22 | with: 23 | targets: ${{ matrix.target }} 24 | - name: Cache Dependencies 25 | uses: Swatinem/rust-cache@v2 26 | with: 27 | key: ${{ matrix.target }} 28 | - run: cargo build --target ${{ matrix.target }} --release 29 | 30 | - name: (Not Windows) Move executables and compress 31 | if: ${{ matrix.os != 'windows-latest' }} 32 | run: gzip -c target/${{ matrix.target }}/release/form > form-${{ matrix.target }}${{ matrix.suffix }} 33 | 34 | - name: (Windows) Move executables and compress 35 | if: ${{ matrix.os == 'windows-latest' }} 36 | run: Compress-Archive -Path target\${{ matrix.target }}\release\form.exe -DestinationPath form-${{ matrix.target }}${{ matrix.suffix }} 37 | 38 | - uses: actions/upload-artifact@v4 39 | with: 40 | name: form-${{ matrix.target }} 41 | path: form-${{ matrix.target }}${{ matrix.suffix }} 42 | 43 | release: 44 | name: release 45 | runs-on: ubuntu-latest 46 | needs: [build] 47 | permissions: 48 | contents: write 49 | steps: 50 | - uses: actions/checkout@v4 51 | - uses: actions/download-artifact@v4 52 | with: 53 | path: artifacts 54 | - run: ls -R ./artifacts 55 | 56 | - name: Set current date as environment variable 57 | run: echo "CURRENT_DATE=$(date +'%Y-%m-%d')" >> $GITHUB_ENV 58 | 59 | - id: changelog-reader 60 | uses: mindsers/changelog-reader-action@v2 61 | with: 62 | version: ${{ (github.ref_type == 'tag' && github.ref_name) || 'Unreleased' }} 63 | 64 | - uses: softprops/action-gh-release@v2 65 | with: 66 | tag_name: ${{ steps.changelog-reader.outputs.version }} 67 | name: ${{ (github.ref_type == 'tag' && steps.changelog-reader.outputs.version) || format('Prereleased {0}', env.CURRENT_DATE) }} 68 | body: ${{ steps.changelog-reader.outputs.changes }} 69 | prerelease: ${{ steps.changelog-reader.outputs.status == 'unreleased' }} 70 | token: ${{ secrets.GITHUB_TOKEN }} 71 | files: | 72 | artifacts/**/* 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target/ 2 | **/*.rs.bk 3 | .idea/ 4 | *.iml 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/). 7 | 8 | ## [Unreleased] 9 | 10 | ## [v0.13.0] - 2025-05-07 11 | * Update `syn` so it supports edition 2024, run `cargo update` for the other deps. 12 | 13 | ## [v0.12.1] - 2024-04-02 14 | * Format result files with `prettyplease` (when `-f` option passed), update `syn` 15 | 16 | ## [v0.11.1] - 2024-01-03 17 | * Bump env_logger to 0.10.1 which changes: `Resolved soundness issue by switching from atty to is-terminal` 18 | 19 | ## [v0.11.0] - 2023-10-29 20 | * Files and directories whose name excluding extensions is a Windows reserved name are renamed by appending an underscore. The module structure and the directory structure remain intact. This is a BREAKING change to the modules that are produced if they have names that now get underscores appended. 21 | 22 | ## [v0.10.0] - 2022-07-27 23 | * Update deps including security advisories and switching failure to anyhow 24 | 25 | ## [v0.9.0] - 2022-07-26 26 | * Add GHA CI 27 | 28 | [Unreleased]: https://github.com/djmcgill/form/compare/v0.13.0...HEAD 29 | [v0.13.0]: https://github.com/djmcgill/form/compare/v0.12.1...v0.13.0 30 | [v0.12.1]: https://github.com/djmcgill/form/compare/v0.11.1...v0.12.1 31 | [v0.11.1]: https://github.com/djmcgill/form/compare/v0.11.0...v0.11.1 32 | [v0.11.0]: https://github.com/djmcgill/form/compare/v0.10.0...v0.11.0 33 | [v0.10.0]: https://github.com/djmcgill/form/compare/v0.9.0...v0.10.0 34 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "aho-corasick" 7 | version = "1.1.3" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 10 | dependencies = [ 11 | "memchr", 12 | ] 13 | 14 | [[package]] 15 | name = "anyhow" 16 | version = "1.0.98" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" 19 | 20 | [[package]] 21 | name = "bitflags" 22 | version = "2.9.0" 23 | source = "registry+https://github.com/rust-lang/crates.io-index" 24 | checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" 25 | 26 | [[package]] 27 | name = "cfg-if" 28 | version = "1.0.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 31 | 32 | [[package]] 33 | name = "env_logger" 34 | version = "0.10.2" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580" 37 | dependencies = [ 38 | "humantime", 39 | "is-terminal", 40 | "log", 41 | "regex", 42 | "termcolor", 43 | ] 44 | 45 | [[package]] 46 | name = "errno" 47 | version = "0.3.11" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "976dd42dc7e85965fe702eb8164f21f450704bdde31faefd6471dba214cb594e" 50 | dependencies = [ 51 | "libc", 52 | "windows-sys", 53 | ] 54 | 55 | [[package]] 56 | name = "fastrand" 57 | version = "2.3.0" 58 | source = "registry+https://github.com/rust-lang/crates.io-index" 59 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 60 | 61 | [[package]] 62 | name = "form" 63 | version = "0.13.0" 64 | dependencies = [ 65 | "anyhow", 66 | "env_logger", 67 | "getopts", 68 | "log", 69 | "prettyplease", 70 | "proc-macro2", 71 | "quote", 72 | "syn", 73 | "tempfile", 74 | ] 75 | 76 | [[package]] 77 | name = "getopts" 78 | version = "0.2.21" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 81 | dependencies = [ 82 | "unicode-width", 83 | ] 84 | 85 | [[package]] 86 | name = "getrandom" 87 | version = "0.3.2" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0" 90 | dependencies = [ 91 | "cfg-if", 92 | "libc", 93 | "r-efi", 94 | "wasi", 95 | ] 96 | 97 | [[package]] 98 | name = "hermit-abi" 99 | version = "0.5.1" 100 | source = "registry+https://github.com/rust-lang/crates.io-index" 101 | checksum = "f154ce46856750ed433c8649605bf7ed2de3bc35fd9d2a9f30cddd873c80cb08" 102 | 103 | [[package]] 104 | name = "humantime" 105 | version = "2.2.0" 106 | source = "registry+https://github.com/rust-lang/crates.io-index" 107 | checksum = "9b112acc8b3adf4b107a8ec20977da0273a8c386765a3ec0229bd500a1443f9f" 108 | 109 | [[package]] 110 | name = "is-terminal" 111 | version = "0.4.16" 112 | source = "registry+https://github.com/rust-lang/crates.io-index" 113 | checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" 114 | dependencies = [ 115 | "hermit-abi", 116 | "libc", 117 | "windows-sys", 118 | ] 119 | 120 | [[package]] 121 | name = "libc" 122 | version = "0.2.172" 123 | source = "registry+https://github.com/rust-lang/crates.io-index" 124 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 125 | 126 | [[package]] 127 | name = "linux-raw-sys" 128 | version = "0.9.4" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" 131 | 132 | [[package]] 133 | name = "log" 134 | version = "0.4.27" 135 | source = "registry+https://github.com/rust-lang/crates.io-index" 136 | checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" 137 | 138 | [[package]] 139 | name = "memchr" 140 | version = "2.7.4" 141 | source = "registry+https://github.com/rust-lang/crates.io-index" 142 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 143 | 144 | [[package]] 145 | name = "once_cell" 146 | version = "1.21.3" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" 149 | 150 | [[package]] 151 | name = "prettyplease" 152 | version = "0.2.32" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "664ec5419c51e34154eec046ebcba56312d5a2fc3b09a06da188e1ad21afadf6" 155 | dependencies = [ 156 | "proc-macro2", 157 | "syn", 158 | ] 159 | 160 | [[package]] 161 | name = "proc-macro2" 162 | version = "1.0.95" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 165 | dependencies = [ 166 | "unicode-ident", 167 | ] 168 | 169 | [[package]] 170 | name = "quote" 171 | version = "1.0.40" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 174 | dependencies = [ 175 | "proc-macro2", 176 | ] 177 | 178 | [[package]] 179 | name = "r-efi" 180 | version = "5.2.0" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" 183 | 184 | [[package]] 185 | name = "regex" 186 | version = "1.11.1" 187 | source = "registry+https://github.com/rust-lang/crates.io-index" 188 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 189 | dependencies = [ 190 | "aho-corasick", 191 | "memchr", 192 | "regex-automata", 193 | "regex-syntax", 194 | ] 195 | 196 | [[package]] 197 | name = "regex-automata" 198 | version = "0.4.9" 199 | source = "registry+https://github.com/rust-lang/crates.io-index" 200 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 201 | dependencies = [ 202 | "aho-corasick", 203 | "memchr", 204 | "regex-syntax", 205 | ] 206 | 207 | [[package]] 208 | name = "regex-syntax" 209 | version = "0.8.5" 210 | source = "registry+https://github.com/rust-lang/crates.io-index" 211 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 212 | 213 | [[package]] 214 | name = "rustix" 215 | version = "1.0.7" 216 | source = "registry+https://github.com/rust-lang/crates.io-index" 217 | checksum = "c71e83d6afe7ff64890ec6b71d6a69bb8a610ab78ce364b3352876bb4c801266" 218 | dependencies = [ 219 | "bitflags", 220 | "errno", 221 | "libc", 222 | "linux-raw-sys", 223 | "windows-sys", 224 | ] 225 | 226 | [[package]] 227 | name = "syn" 228 | version = "2.0.101" 229 | source = "registry+https://github.com/rust-lang/crates.io-index" 230 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 231 | dependencies = [ 232 | "proc-macro2", 233 | "quote", 234 | "unicode-ident", 235 | ] 236 | 237 | [[package]] 238 | name = "tempfile" 239 | version = "3.19.1" 240 | source = "registry+https://github.com/rust-lang/crates.io-index" 241 | checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" 242 | dependencies = [ 243 | "fastrand", 244 | "getrandom", 245 | "once_cell", 246 | "rustix", 247 | "windows-sys", 248 | ] 249 | 250 | [[package]] 251 | name = "termcolor" 252 | version = "1.4.1" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 255 | dependencies = [ 256 | "winapi-util", 257 | ] 258 | 259 | [[package]] 260 | name = "unicode-ident" 261 | version = "1.0.18" 262 | source = "registry+https://github.com/rust-lang/crates.io-index" 263 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 264 | 265 | [[package]] 266 | name = "unicode-width" 267 | version = "0.1.14" 268 | source = "registry+https://github.com/rust-lang/crates.io-index" 269 | checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" 270 | 271 | [[package]] 272 | name = "wasi" 273 | version = "0.14.2+wasi-0.2.4" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" 276 | dependencies = [ 277 | "wit-bindgen-rt", 278 | ] 279 | 280 | [[package]] 281 | name = "winapi-util" 282 | version = "0.1.9" 283 | source = "registry+https://github.com/rust-lang/crates.io-index" 284 | checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" 285 | dependencies = [ 286 | "windows-sys", 287 | ] 288 | 289 | [[package]] 290 | name = "windows-sys" 291 | version = "0.59.0" 292 | source = "registry+https://github.com/rust-lang/crates.io-index" 293 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 294 | dependencies = [ 295 | "windows-targets", 296 | ] 297 | 298 | [[package]] 299 | name = "windows-targets" 300 | version = "0.52.6" 301 | source = "registry+https://github.com/rust-lang/crates.io-index" 302 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 303 | dependencies = [ 304 | "windows_aarch64_gnullvm", 305 | "windows_aarch64_msvc", 306 | "windows_i686_gnu", 307 | "windows_i686_gnullvm", 308 | "windows_i686_msvc", 309 | "windows_x86_64_gnu", 310 | "windows_x86_64_gnullvm", 311 | "windows_x86_64_msvc", 312 | ] 313 | 314 | [[package]] 315 | name = "windows_aarch64_gnullvm" 316 | version = "0.52.6" 317 | source = "registry+https://github.com/rust-lang/crates.io-index" 318 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 319 | 320 | [[package]] 321 | name = "windows_aarch64_msvc" 322 | version = "0.52.6" 323 | source = "registry+https://github.com/rust-lang/crates.io-index" 324 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 325 | 326 | [[package]] 327 | name = "windows_i686_gnu" 328 | version = "0.52.6" 329 | source = "registry+https://github.com/rust-lang/crates.io-index" 330 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 331 | 332 | [[package]] 333 | name = "windows_i686_gnullvm" 334 | version = "0.52.6" 335 | source = "registry+https://github.com/rust-lang/crates.io-index" 336 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 337 | 338 | [[package]] 339 | name = "windows_i686_msvc" 340 | version = "0.52.6" 341 | source = "registry+https://github.com/rust-lang/crates.io-index" 342 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 343 | 344 | [[package]] 345 | name = "windows_x86_64_gnu" 346 | version = "0.52.6" 347 | source = "registry+https://github.com/rust-lang/crates.io-index" 348 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 349 | 350 | [[package]] 351 | name = "windows_x86_64_gnullvm" 352 | version = "0.52.6" 353 | source = "registry+https://github.com/rust-lang/crates.io-index" 354 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 355 | 356 | [[package]] 357 | name = "windows_x86_64_msvc" 358 | version = "0.52.6" 359 | source = "registry+https://github.com/rust-lang/crates.io-index" 360 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 361 | 362 | [[package]] 363 | name = "wit-bindgen-rt" 364 | version = "0.39.0" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" 367 | dependencies = [ 368 | "bitflags", 369 | ] 370 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "form" 3 | version = "0.13.0" 4 | authors = ["David McGillicuddy "] 5 | description = "A small script to move inline modules into the proper directory structure" 6 | license = "MIT" 7 | repository = "https://github.com/djmcgill/form" 8 | edition = "2021" 9 | 10 | [dependencies] 11 | quote = "1.0.20" 12 | proc-macro2 = "1.0.42" 13 | getopts = "0.2.21" 14 | log = "0.4.17" 15 | env_logger = "0.10.1" 16 | anyhow = "1.0.58" 17 | prettyplease = "0.2.19" 18 | 19 | [dependencies.syn] 20 | version = "2.0.101" 21 | features = ["full", "fold", "printing", "extra-traits"] 22 | 23 | [[bin]] 24 | name = "form" 25 | test = false 26 | doc = false 27 | 28 | [dev-dependencies] 29 | tempfile = "3.10.1" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 David McGillicuddy 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![crates.io](https://img.shields.io/crates/d/form.svg)](https://crates.io/crates/form) 2 | [![crates.io](https://img.shields.io/crates/v/form.svg)](https://crates.io/crates/form) 3 | [![CircleCI](https://circleci.com/gh/djmcgill/form/tree/main.svg?style=svg)](https://circleci.com/gh/djmcgill/form/tree/main) 4 | [![CI](https://github.com/djmcgill/form/workflows/CI/badge.svg?branch=main)](https://github.com/djmcgill/form) 5 | 6 | # Form 7 | 8 | A library for splitting apart a large file with multiple modules into the idiomatic rust directory structure, intended for use with svd2rust. 9 | Creates a lib.rs as well as a subdirectory structure in the target directory. It does NOT create the cargo project or the cargo manifest file. 10 | 11 | It's advised (but not necessary) to use rustfmt afterwards, or you can pass `-f` to use `prettyplease`. 12 | ## Usage: 13 | Arguments: 14 | ``` 15 | -i, --input FILE OPTIONAL: input file to read, defaults to stdin 16 | -o, --outdir DIR set output directory 17 | -h, --help print this help menu 18 | -v, --version print version information 19 | -f format files with `prettyplease` 20 | ``` 21 | 22 | 23 | Intended usage (using `svd2rust` 0.12.1 and before): 24 | ```bash 25 | svd2rust -i FOO.svd | form -o ~/private/code/form/test/src 26 | ``` 27 | Usage with `svd2rust` 0.13.0 and later can be found in [svd2rust's documentation](https://docs.rs/svd2rust/). 28 | 29 | Advanced usage: 30 | ```bash 31 | cargo install form 32 | export RUST_LOG=form=debug 33 | export RUST_BACKTRACE=1 34 | form -i ~/private/code/form/resources/full-lib.rs -o ~/private/code/form/test/src 35 | ``` 36 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | pub mod util; 4 | pub use crate::util::create_directory_structure; 5 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![recursion_limit = "1024"] 2 | 3 | use anyhow::{Context, Result}; 4 | use env_logger::Env; 5 | use log::trace; 6 | 7 | mod opts; 8 | use crate::opts::FormOpts; 9 | 10 | mod util; 11 | use crate::util::create_directory_structure; 12 | 13 | const NAME: &str = "form"; 14 | const VERSION: &str = env!("CARGO_PKG_VERSION"); 15 | 16 | fn main() -> Result<()> { 17 | env_logger::Builder::from_env(Env::default().default_filter_or("info")) 18 | .try_init() 19 | .context("could not initialise env_logger")?; 20 | 21 | trace!("logging initialised"); 22 | 23 | let try_parsed_args = 24 | FormOpts::from_args().context("could not parse the command line arguments")?; 25 | // if None, we've already printed a help text or version and have nothing more to do 26 | if let Some(opts) = try_parsed_args { 27 | create_directory_structure(opts.output_dir, &opts.input, opts.format_output)?; 28 | println!("Completed successfully"); 29 | } 30 | Ok(()) 31 | } 32 | -------------------------------------------------------------------------------- /src/opts.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{anyhow, Result}; 2 | use getopts::Options; 3 | use std::{ 4 | env, 5 | fs::File, 6 | io::{self, Read}, 7 | path::Path, 8 | }; 9 | 10 | pub struct FormOpts { 11 | pub input: String, 12 | pub output_dir: String, 13 | pub format_output: bool, 14 | } 15 | 16 | impl FormOpts { 17 | pub fn from_args() -> Result> { 18 | const SHORT_INPUT: &str = "i"; 19 | const SHORT_OUTDIR: &str = "o"; 20 | const SHORT_FMT: &str = "f"; 21 | const SHORT_HELP: &str = "h"; 22 | const SHORT_VERSION: &str = "v"; 23 | 24 | let args: Vec = env::args().collect(); 25 | let program = args[0].clone(); 26 | let mut opts = Options::new(); 27 | opts.optopt( 28 | SHORT_INPUT, 29 | "input", 30 | "input file to read instead of stdin", 31 | "FILE", 32 | ); 33 | opts.optopt(SHORT_OUTDIR, "outdir", "set output directory", "DIR"); 34 | opts.optflag( 35 | "f", 36 | "format-output", 37 | "format result with prettyplease formatter", 38 | ); 39 | opts.optflag(SHORT_HELP, "help", "print this help menu"); 40 | opts.optflag(SHORT_VERSION, "version", "print the version"); 41 | 42 | let matches = opts.parse(&args[1..]).unwrap(); 43 | let format_output = matches.opt_present(SHORT_FMT); 44 | if matches.opt_present(SHORT_HELP) { 45 | print_usage(&program, opts); 46 | return Ok(None); 47 | } 48 | if matches.opt_present(SHORT_VERSION) { 49 | print_version(); 50 | return Ok(None); 51 | } 52 | let output_dir = matches 53 | .opt_str(SHORT_OUTDIR) 54 | .ok_or_else(|| anyhow!("Output directory missing"))?; 55 | let input = read_input(matches.opt_str(SHORT_INPUT))?; 56 | 57 | Ok(Some(FormOpts { 58 | output_dir, 59 | input, 60 | format_output, 61 | })) 62 | } 63 | } 64 | 65 | fn print_usage(program: &str, opts: Options) { 66 | let brief = format!("Usage: {} [options]", program); 67 | println!("{}", opts.usage(&brief)); 68 | } 69 | 70 | fn print_version() { 71 | println!("{} {}", crate::NAME, crate::VERSION); 72 | } 73 | 74 | fn read_input>(input_file: Option

) -> Result { 75 | let mut input = String::new(); 76 | match input_file { 77 | Some(file_name) => { 78 | let mut file = File::open(file_name)?; 79 | file.read_to_string(&mut input)?; 80 | } 81 | None => { 82 | io::stdin().read_to_string(&mut input)?; 83 | } 84 | } 85 | Ok(input) 86 | } 87 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use anyhow::{Context, Result}; 2 | use log::{debug, info, trace}; 3 | use quote::ToTokens; 4 | use std::{ 5 | fs::{DirBuilder, File}, 6 | io::Write, 7 | path::{Path, PathBuf}, 8 | }; 9 | use syn::{fold::*, parse_quote, Ident, Item}; 10 | 11 | pub fn create_directory_structure>( 12 | base_dir: P, 13 | string_contents: &str, 14 | fmt: bool, 15 | ) -> Result<()> { 16 | info!("Started parsing the input as Rust. This can take a minute or two."); 17 | let parsed_crate = syn::parse_file(string_contents).context("failed to parse crate")?; 18 | info!("Finished parsing"); 19 | 20 | let base_dir = base_dir.as_ref(); 21 | let mut dir_builder = DirBuilder::new(); 22 | dir_builder 23 | .recursive(true) 24 | .create(&base_dir) 25 | .with_context(|| format!("unable to create the directory {}", base_dir.display()))?; 26 | info!("Prepared target directory {}", base_dir.display()); 27 | 28 | let mut folder = FileIntoMods { 29 | current_dir: &base_dir, 30 | current_mod_fs_name: None, 31 | has_path_attr: false, 32 | fmt, 33 | }; 34 | 35 | // Why doesn't syn::Fold::fold handle errors again? 36 | // TODO: catch panics? 37 | let new_contents = folder.fold_file(parsed_crate); 38 | trace!("transformed module contents"); 39 | 40 | let lib_file_path = base_dir.join("lib.rs"); 41 | 42 | let mut file = File::create(&lib_file_path) 43 | .with_context(|| format!("Unable to create the file {}", lib_file_path.display()))?; 44 | debug!("Writing to file {}", lib_file_path.display()); 45 | write_file(&new_contents, &mut file, fmt).context("unable to write to lib.rs")?; 46 | Ok(()) 47 | } 48 | 49 | #[derive(Debug)] 50 | struct FileIntoMods + Send + Sync> { 51 | current_dir: P, 52 | current_mod_fs_name: Option, 53 | has_path_attr: bool, 54 | fmt: bool, 55 | } 56 | impl + Send + Sync> FileIntoMods

{ 57 | fn sub_mod(&self, path: String, has_path_attr: bool) -> FileIntoMods { 58 | FileIntoMods { 59 | current_dir: self.current_dir.as_ref().join(&path), 60 | current_mod_fs_name: Some(path), 61 | has_path_attr, 62 | fmt: self.fmt, 63 | } 64 | } 65 | } 66 | 67 | impl + Send + Sync> FileIntoMods

{ 68 | fn fold_sub_mod( 69 | &mut self, 70 | mod_name: Ident, 71 | mod_fs_name: String, 72 | mod_has_path_attr: bool, 73 | mod_file: syn::File, 74 | ) -> Result<()> { 75 | let mod_name = mod_name.to_string(); 76 | trace!("Folding over module {}", mod_name); 77 | 78 | if !self.current_dir.as_ref().exists() { 79 | let mut dir_builder = DirBuilder::new(); 80 | info!("Creating directory {}", self.current_dir.as_ref().display()); 81 | dir_builder 82 | .recursive(true) 83 | .create(self.current_dir.as_ref()) 84 | .unwrap_or_else(|err| { 85 | panic!( 86 | "building {} failed with {}", 87 | self.current_dir.as_ref().display(), 88 | err 89 | ) 90 | }); 91 | } 92 | 93 | let mut sub_self = self.sub_mod(mod_fs_name.clone(), mod_has_path_attr); 94 | let folded_mod = fold_file(&mut sub_self, mod_file); 95 | let file_name = self.current_dir.as_ref().join(mod_fs_name + ".rs"); 96 | trace!( 97 | "Writing contents of module {} to file {}", 98 | mod_name, 99 | file_name.display() 100 | ); 101 | write_mod_file(folded_mod, &file_name, self.fmt) 102 | .unwrap_or_else(|err| panic!("writing to {} failed with {}", file_name.display(), err)); 103 | Ok(()) 104 | } 105 | } 106 | 107 | impl + Send + Sync> Fold for FileIntoMods

{ 108 | fn fold_item(&mut self, mut item: Item) -> Item { 109 | if let Some((mod_name, mod_fs_name, mod_has_path_attr, mod_file)) = 110 | extract_mod(&mut item, &self.current_mod_fs_name, self.has_path_attr) 111 | { 112 | self.fold_sub_mod(mod_name, mod_fs_name, mod_has_path_attr, mod_file) 113 | .unwrap(); 114 | } 115 | fold_item(self, item) 116 | } 117 | } 118 | 119 | fn write_mod_file(item_mod: syn::File, file_name: &Path, fmt: bool) -> Result<()> { 120 | trace!("Opening file {}", file_name.display()); 121 | let mut file = File::create(&file_name) 122 | .with_context(|| format!("unable to create file {}", file_name.display()))?; 123 | trace!("Successfully opened file {}", file_name.display()); 124 | debug!("Writing to file {}", file_name.display()); 125 | write_file(&item_mod, &mut file, fmt) 126 | } 127 | 128 | fn extract_mod( 129 | node: &mut Item, 130 | parent_fs_name: &Option, 131 | parent_has_path_attr: bool, 132 | ) -> Option<(Ident, String, bool, syn::File)> { 133 | let top_level = parent_fs_name.is_none(); 134 | if let Item::Mod(mod_item) = &mut *node { 135 | if let Some(item_content) = mod_item.content.take() { 136 | let items = item_content.1; 137 | 138 | let mod_name = mod_item.ident.clone(); 139 | let mod_name_str = mod_name.to_string(); 140 | 141 | let mod_name_is_reserved = is_name_reserved(&mod_name_str); 142 | let mod_has_path_attr = parent_has_path_attr || mod_name_is_reserved; 143 | let mod_fs_name = if mod_name_is_reserved { 144 | xform_reserved_name(&mod_name_str) 145 | } else { 146 | mod_name_str 147 | }; 148 | 149 | if mod_has_path_attr { 150 | let path_attr_val = match parent_fs_name { 151 | Some(parent_fs_name) => format!("{parent_fs_name}/{mod_fs_name}.rs"), 152 | None => format!("{mod_fs_name}.rs"), 153 | }; 154 | mod_item 155 | .attrs 156 | .push(parse_quote! { #[path = #path_attr_val] }); 157 | } 158 | 159 | Some((mod_name, mod_fs_name, mod_has_path_attr, make_file(items))) 160 | } else if top_level { 161 | None 162 | } else { 163 | panic!("Moving nested non-inline `mod` declarations not currently supported.") 164 | } 165 | } else { 166 | None 167 | } 168 | } 169 | 170 | fn make_file(items: Vec) -> syn::File { 171 | syn::File { 172 | shebang: None, 173 | attrs: vec![], 174 | items, 175 | } 176 | } 177 | 178 | fn write_file(piece: &syn::File, writer: &mut W, fmt: bool) -> Result<()> { 179 | let string = if fmt { 180 | prettyplease::unparse(piece) 181 | } else { 182 | let mut new_tokens = proc_macro2::TokenStream::new(); 183 | piece.to_tokens(&mut new_tokens); 184 | new_tokens.to_string() 185 | }; 186 | trace!("Written string for tokens, now writing"); 187 | writer 188 | .write_all(string.as_bytes()) 189 | .context("unable to write the tokens to the file")?; 190 | trace!("Successfully wrote token string"); 191 | Ok(()) 192 | } 193 | 194 | const RESERVED_NAMES: &[&str] = &[ 195 | "CON", "PRN", "AUX", "NUL", "COM0", "COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", 196 | "COM8", "COM9", "LPT0", "LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9", 197 | ]; 198 | 199 | fn is_name_reserved(name: &str) -> bool { 200 | RESERVED_NAMES 201 | .iter() 202 | .any(|&reserved_name| name.eq_ignore_ascii_case(reserved_name)) 203 | } 204 | 205 | fn xform_reserved_name(reserved_name: &str) -> String { 206 | format!("{reserved_name}_") 207 | } 208 | -------------------------------------------------------------------------------- /tests/file-test.rs: -------------------------------------------------------------------------------- 1 | #![cfg(test)] 2 | use form::create_directory_structure; 3 | use std::{fs::File, io::Read, path::Path}; 4 | use syn::parse_file; 5 | use tempfile::tempdir; 6 | 7 | #[test] 8 | fn test_from_reference_files() { 9 | let before_file = std::str::from_utf8(include_bytes!("resources/small-lib-before.rs")).unwrap(); 10 | 11 | let expected_lib = include_bytes!("resources/after/lib.rs"); 12 | let expected_interrupt = include_bytes!("resources/after/interrupt.rs"); 13 | let expected_ac = include_bytes!("resources/after/ac.rs"); 14 | let expected_ac2 = include_bytes!("resources/after/ac/ac2.rs"); 15 | let expected_ac3 = include_bytes!("resources/after/ac/ac2/ac3.rs"); 16 | 17 | let lib_dir = tempdir().unwrap(); 18 | create_directory_structure(lib_dir.path(), before_file, false).unwrap(); 19 | 20 | compare_to_expected(expected_lib, lib_dir.path().join("lib.rs")); 21 | compare_to_expected(expected_interrupt, lib_dir.path().join("interrupt.rs")); 22 | compare_to_expected(expected_ac, lib_dir.path().join("ac.rs")); 23 | compare_to_expected(expected_ac2, lib_dir.path().join("ac/ac2.rs")); 24 | compare_to_expected(expected_ac3, lib_dir.path().join("ac/ac2/ac3.rs")); 25 | } 26 | 27 | #[test] 28 | fn test_from_reference_files_with_format() { 29 | let before_file = std::str::from_utf8(include_bytes!("resources/small-lib-before.rs")).unwrap(); 30 | 31 | let expected_lib = include_bytes!("resources/after/lib.rs"); 32 | let expected_interrupt = include_bytes!("resources/after/interrupt.rs"); 33 | let expected_ac = include_bytes!("resources/after/ac.rs"); 34 | let expected_ac2 = include_bytes!("resources/after/ac/ac2.rs"); 35 | let expected_ac3 = include_bytes!("resources/after/ac/ac2/ac3.rs"); 36 | 37 | let lib_dir = tempdir().unwrap(); 38 | create_directory_structure(lib_dir.path(), before_file, true).unwrap(); 39 | 40 | compare_to_expected(expected_lib, lib_dir.path().join("lib.rs")); 41 | compare_to_expected(expected_interrupt, lib_dir.path().join("interrupt.rs")); 42 | compare_to_expected(expected_ac, lib_dir.path().join("ac.rs")); 43 | compare_to_expected(expected_ac2, lib_dir.path().join("ac/ac2.rs")); 44 | compare_to_expected(expected_ac3, lib_dir.path().join("ac/ac2/ac3.rs")); 45 | } 46 | 47 | fn compare_to_expected>(expected: &[u8], path: P) { 48 | let expected = parse_file(std::str::from_utf8(expected).unwrap()).unwrap(); 49 | 50 | let mut found_string = String::new(); 51 | let mut found_file = File::open(path).unwrap(); 52 | found_file.read_to_string(&mut found_string).unwrap(); 53 | let found = parse_file(&found_string).unwrap(); 54 | assert_eq!(expected, found) 55 | } 56 | -------------------------------------------------------------------------------- /tests/resources/after/ac.rs: -------------------------------------------------------------------------------- 1 | pub mod ac2; 2 | pub const AC: AC = AC; 3 | -------------------------------------------------------------------------------- /tests/resources/after/ac/ac2.rs: -------------------------------------------------------------------------------- 1 | pub const AC2: AC = AC; 2 | pub mod ac3; 3 | -------------------------------------------------------------------------------- /tests/resources/after/ac/ac2/ac3.rs: -------------------------------------------------------------------------------- 1 | pub const AC3: AC = AC; 2 | 3 | -------------------------------------------------------------------------------- /tests/resources/after/interrupt.rs: -------------------------------------------------------------------------------- 1 | pub const INTERRUPT_CONST: u8 = 3; 2 | -------------------------------------------------------------------------------- /tests/resources/after/lib.rs: -------------------------------------------------------------------------------- 1 | # ! [ cfg_attr ( feature = "rt" , feature ( asm ) ) ] 2 | 3 | extern crate cortex_m_rt ; 4 | 5 | use core::ops::Deref; 6 | 7 | use bare_metal::Peripheral; 8 | pub const NVIC_PRIO_BITS: u8 = 2; 9 | 10 | #[doc(hidden)] 11 | pub mod interrupt; 12 | 13 | pub use cortex_m::peripheral::TPIU; 14 | pub struct AC; 15 | pub mod ac; 16 | 17 | mod foo; -------------------------------------------------------------------------------- /tests/resources/small-lib-before.rs: -------------------------------------------------------------------------------- 1 | # ! [ cfg_attr ( feature = "rt" , feature ( asm ) ) ] 2 | 3 | extern crate cortex_m_rt ; 4 | 5 | use core::ops::Deref; 6 | 7 | use bare_metal::Peripheral; 8 | pub const NVIC_PRIO_BITS: u8 = 2; 9 | 10 | #[doc(hidden)] 11 | pub mod interrupt { 12 | pub const INTERRUPT_CONST: u8 = 3; 13 | } 14 | 15 | pub use cortex_m::peripheral::TPIU; 16 | pub struct AC; 17 | pub mod ac { 18 | pub mod ac2 { 19 | pub const AC2: AC = AC; 20 | pub mod ac3 { 21 | pub const AC3: AC = AC; 22 | } 23 | } 24 | pub const AC: AC = AC; 25 | } 26 | 27 | mod foo; --------------------------------------------------------------------------------