├── .github └── funding.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── Makefile ├── README.md └── src ├── cli ├── command │ ├── command.rs │ └── mod.rs ├── controller │ ├── controller.rs │ └── mod.rs └── mod.rs ├── main.rs └── usecases ├── backup.rs ├── dry_run.rs ├── find.rs ├── ignore_case.rs ├── mod.rs ├── regex.rs └── replace.rs /.github/funding.yml: -------------------------------------------------------------------------------- 1 | github: ibilalkayy 2 | buy_me_a_coffee: ibilalkayy 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | -------------------------------------------------------------------------------- /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 = "anstream" 16 | version = "0.6.18" 17 | source = "registry+https://github.com/rust-lang/crates.io-index" 18 | checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" 19 | dependencies = [ 20 | "anstyle", 21 | "anstyle-parse", 22 | "anstyle-query", 23 | "anstyle-wincon", 24 | "colorchoice", 25 | "is_terminal_polyfill", 26 | "utf8parse", 27 | ] 28 | 29 | [[package]] 30 | name = "anstyle" 31 | version = "1.0.10" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" 34 | 35 | [[package]] 36 | name = "anstyle-parse" 37 | version = "0.2.6" 38 | source = "registry+https://github.com/rust-lang/crates.io-index" 39 | checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" 40 | dependencies = [ 41 | "utf8parse", 42 | ] 43 | 44 | [[package]] 45 | name = "anstyle-query" 46 | version = "1.1.2" 47 | source = "registry+https://github.com/rust-lang/crates.io-index" 48 | checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" 49 | dependencies = [ 50 | "windows-sys", 51 | ] 52 | 53 | [[package]] 54 | name = "anstyle-wincon" 55 | version = "3.0.8" 56 | source = "registry+https://github.com/rust-lang/crates.io-index" 57 | checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" 58 | dependencies = [ 59 | "anstyle", 60 | "once_cell_polyfill", 61 | "windows-sys", 62 | ] 63 | 64 | [[package]] 65 | name = "bitflags" 66 | version = "2.9.1" 67 | source = "registry+https://github.com/rust-lang/crates.io-index" 68 | checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" 69 | 70 | [[package]] 71 | name = "cfg-if" 72 | version = "1.0.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 75 | 76 | [[package]] 77 | name = "clap" 78 | version = "4.5.39" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "fd60e63e9be68e5fb56422e397cf9baddded06dae1d2e523401542383bc72a9f" 81 | dependencies = [ 82 | "clap_builder", 83 | "clap_derive", 84 | ] 85 | 86 | [[package]] 87 | name = "clap_builder" 88 | version = "4.5.39" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "89cc6392a1f72bbeb820d71f32108f61fdaf18bc526e1d23954168a67759ef51" 91 | dependencies = [ 92 | "anstream", 93 | "anstyle", 94 | "clap_lex", 95 | "strsim", 96 | ] 97 | 98 | [[package]] 99 | name = "clap_derive" 100 | version = "4.5.32" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "09176aae279615badda0765c0c0b3f6ed53f4709118af73cf4655d85d1530cd7" 103 | dependencies = [ 104 | "heck", 105 | "proc-macro2", 106 | "quote", 107 | "syn", 108 | ] 109 | 110 | [[package]] 111 | name = "clap_lex" 112 | version = "0.7.4" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" 115 | 116 | [[package]] 117 | name = "colorchoice" 118 | version = "1.0.3" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" 121 | 122 | [[package]] 123 | name = "dirs" 124 | version = "6.0.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e" 127 | dependencies = [ 128 | "dirs-sys", 129 | ] 130 | 131 | [[package]] 132 | name = "dirs-sys" 133 | version = "0.5.0" 134 | source = "registry+https://github.com/rust-lang/crates.io-index" 135 | checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab" 136 | dependencies = [ 137 | "libc", 138 | "option-ext", 139 | "redox_users", 140 | "windows-sys", 141 | ] 142 | 143 | [[package]] 144 | name = "fara" 145 | version = "0.1.21" 146 | dependencies = [ 147 | "clap", 148 | "dirs", 149 | "regex", 150 | ] 151 | 152 | [[package]] 153 | name = "getrandom" 154 | version = "0.2.16" 155 | source = "registry+https://github.com/rust-lang/crates.io-index" 156 | checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" 157 | dependencies = [ 158 | "cfg-if", 159 | "libc", 160 | "wasi", 161 | ] 162 | 163 | [[package]] 164 | name = "heck" 165 | version = "0.5.0" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" 168 | 169 | [[package]] 170 | name = "is_terminal_polyfill" 171 | version = "1.70.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" 174 | 175 | [[package]] 176 | name = "libc" 177 | version = "0.2.172" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "d750af042f7ef4f724306de029d18836c26c1765a54a6a3f094cbd23a7267ffa" 180 | 181 | [[package]] 182 | name = "libredox" 183 | version = "0.1.3" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 186 | dependencies = [ 187 | "bitflags", 188 | "libc", 189 | ] 190 | 191 | [[package]] 192 | name = "memchr" 193 | version = "2.7.4" 194 | source = "registry+https://github.com/rust-lang/crates.io-index" 195 | checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" 196 | 197 | [[package]] 198 | name = "once_cell_polyfill" 199 | version = "1.70.1" 200 | source = "registry+https://github.com/rust-lang/crates.io-index" 201 | checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" 202 | 203 | [[package]] 204 | name = "option-ext" 205 | version = "0.2.0" 206 | source = "registry+https://github.com/rust-lang/crates.io-index" 207 | checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" 208 | 209 | [[package]] 210 | name = "proc-macro2" 211 | version = "1.0.95" 212 | source = "registry+https://github.com/rust-lang/crates.io-index" 213 | checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" 214 | dependencies = [ 215 | "unicode-ident", 216 | ] 217 | 218 | [[package]] 219 | name = "quote" 220 | version = "1.0.40" 221 | source = "registry+https://github.com/rust-lang/crates.io-index" 222 | checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" 223 | dependencies = [ 224 | "proc-macro2", 225 | ] 226 | 227 | [[package]] 228 | name = "redox_users" 229 | version = "0.5.0" 230 | source = "registry+https://github.com/rust-lang/crates.io-index" 231 | checksum = "dd6f9d3d47bdd2ad6945c5015a226ec6155d0bcdfd8f7cd29f86b71f8de99d2b" 232 | dependencies = [ 233 | "getrandom", 234 | "libredox", 235 | "thiserror", 236 | ] 237 | 238 | [[package]] 239 | name = "regex" 240 | version = "1.11.1" 241 | source = "registry+https://github.com/rust-lang/crates.io-index" 242 | checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" 243 | dependencies = [ 244 | "aho-corasick", 245 | "memchr", 246 | "regex-automata", 247 | "regex-syntax", 248 | ] 249 | 250 | [[package]] 251 | name = "regex-automata" 252 | version = "0.4.9" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" 255 | dependencies = [ 256 | "aho-corasick", 257 | "memchr", 258 | "regex-syntax", 259 | ] 260 | 261 | [[package]] 262 | name = "regex-syntax" 263 | version = "0.8.5" 264 | source = "registry+https://github.com/rust-lang/crates.io-index" 265 | checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" 266 | 267 | [[package]] 268 | name = "strsim" 269 | version = "0.11.1" 270 | source = "registry+https://github.com/rust-lang/crates.io-index" 271 | checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 272 | 273 | [[package]] 274 | name = "syn" 275 | version = "2.0.101" 276 | source = "registry+https://github.com/rust-lang/crates.io-index" 277 | checksum = "8ce2b7fc941b3a24138a0a7cf8e858bfc6a992e7978a068a5c760deb0ed43caf" 278 | dependencies = [ 279 | "proc-macro2", 280 | "quote", 281 | "unicode-ident", 282 | ] 283 | 284 | [[package]] 285 | name = "thiserror" 286 | version = "2.0.12" 287 | source = "registry+https://github.com/rust-lang/crates.io-index" 288 | checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" 289 | dependencies = [ 290 | "thiserror-impl", 291 | ] 292 | 293 | [[package]] 294 | name = "thiserror-impl" 295 | version = "2.0.12" 296 | source = "registry+https://github.com/rust-lang/crates.io-index" 297 | checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" 298 | dependencies = [ 299 | "proc-macro2", 300 | "quote", 301 | "syn", 302 | ] 303 | 304 | [[package]] 305 | name = "unicode-ident" 306 | version = "1.0.18" 307 | source = "registry+https://github.com/rust-lang/crates.io-index" 308 | checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" 309 | 310 | [[package]] 311 | name = "utf8parse" 312 | version = "0.2.2" 313 | source = "registry+https://github.com/rust-lang/crates.io-index" 314 | checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" 315 | 316 | [[package]] 317 | name = "wasi" 318 | version = "0.11.0+wasi-snapshot-preview1" 319 | source = "registry+https://github.com/rust-lang/crates.io-index" 320 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 321 | 322 | [[package]] 323 | name = "windows-sys" 324 | version = "0.59.0" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" 327 | dependencies = [ 328 | "windows-targets", 329 | ] 330 | 331 | [[package]] 332 | name = "windows-targets" 333 | version = "0.52.6" 334 | source = "registry+https://github.com/rust-lang/crates.io-index" 335 | checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" 336 | dependencies = [ 337 | "windows_aarch64_gnullvm", 338 | "windows_aarch64_msvc", 339 | "windows_i686_gnu", 340 | "windows_i686_gnullvm", 341 | "windows_i686_msvc", 342 | "windows_x86_64_gnu", 343 | "windows_x86_64_gnullvm", 344 | "windows_x86_64_msvc", 345 | ] 346 | 347 | [[package]] 348 | name = "windows_aarch64_gnullvm" 349 | version = "0.52.6" 350 | source = "registry+https://github.com/rust-lang/crates.io-index" 351 | checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" 352 | 353 | [[package]] 354 | name = "windows_aarch64_msvc" 355 | version = "0.52.6" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" 358 | 359 | [[package]] 360 | name = "windows_i686_gnu" 361 | version = "0.52.6" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" 364 | 365 | [[package]] 366 | name = "windows_i686_gnullvm" 367 | version = "0.52.6" 368 | source = "registry+https://github.com/rust-lang/crates.io-index" 369 | checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" 370 | 371 | [[package]] 372 | name = "windows_i686_msvc" 373 | version = "0.52.6" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" 376 | 377 | [[package]] 378 | name = "windows_x86_64_gnu" 379 | version = "0.52.6" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" 382 | 383 | [[package]] 384 | name = "windows_x86_64_gnullvm" 385 | version = "0.52.6" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" 388 | 389 | [[package]] 390 | name = "windows_x86_64_msvc" 391 | version = "0.52.6" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" 394 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "fara" 3 | description = "Find And Replace App — a fast and flexible command-line tool to search and replace text across files and folders." 4 | repository = "https://github.com/ibilalkayy/fara" 5 | version = "0.1.21" 6 | edition = "2024" 7 | license = "Apache-2.0" 8 | readme = "README.md" 9 | categories = ["command-line-utilities"] 10 | keywords = ["file", "search", "find", "replace"] 11 | exclude = ["target/*"] 12 | 13 | [dependencies] 14 | clap = { version = "4.5.37", features = ["derive"] } 15 | dirs = "6.0.0" 16 | regex = "1.11.1" 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | install: 2 | cargo add clap --features derive 3 | cargo add regex 4 | cargo add dirs 5 | 6 | build: 7 | cargo build 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 🛠️ fara — Find And Replace App 2 | 3 | **fara** stands for **Find And Replace App** — a fast and flexible command-line tool to search and replace text across files and folders. 4 | 5 | --- 6 | 7 | ## ✨ Features 8 | 9 | * 🔍 **Find** text in files or entire folders. 10 | * ✏️ **Replace** text easily and quickly. 11 | * 🎯 **Target** specific files, directories, or use glob patterns like `**/*.rs`. 12 | * 🔡 **Case-insensitive** search with `--ignore-case`. 13 | * 📝 Optional **regex** support for advanced matching. 14 | * 💾 Backup files with `--backup` (if needed). 15 | * 🚫 **Dry run** option to preview changes. 16 | 17 | --- 18 | 19 | ## 🧱 Installation 20 | 21 | You can install `fara` using [Cargo](https://doc.rust-lang.org/cargo/): 22 | 23 | ```bash 24 | cargo install fara 25 | ``` 26 | 27 | Alternatively, clone the repository and build it manually: 28 | 29 | ```bash 30 | git clone https://github.com/ibilalkayy/fara.git 31 | cd fara 32 | cargo build && cargo install --path . 33 | ``` 34 | 35 | --- 36 | 37 | ## 🚀 Usage 38 | 39 | ### Basic Example 40 | 41 | ```bash 42 | fara --find "Foo" --replace "Bar" --target "./src/**/*.rs" 43 | ``` 44 | 45 | This will search for `"Foo"` in all `.rs` files under `./src/` and replace it with `"Bar"`. 46 | 47 | --- 48 | 49 | ### 🔧 Options 50 | 51 | | Option | Description | 52 | | ------------------------ | --------------------------------------------------------- | 53 | | `-f`, `--find ` | The text to search for | 54 | | `-r`, `--replace ` | The new text to replace the matched text | 55 | | `-t`, `--target ` | The file or folder to search in (supports glob patterns) | 56 | | `-e`, `--regex ` | Use a regular expression for advanced matching | 57 | | `--ignore-case` | Ignore case when matching text | 58 | | `--confirm` | Ask for confirmation before replacing | 59 | | `--dry-run` | Show the changes without modifying the file (coming soon) | 60 | | `--output ` | Write the changed text to a different output file | 61 | | `--backup ` | Create a backup of matching files | 62 | | `-h`, `--help` | Show help information | 63 | | `-V`, `--version` | Show version information | 64 | 65 | --- 66 | 67 | ## 📂 Example Commands 68 | 69 | Replace `"Hello"` with `"Hi"` in all `.txt` files in a folder: 70 | 71 | ```bash 72 | fara --find "Hello" --replace "Hi" --target "./notes/**/*.txt" --confirm 73 | ``` 74 | 75 | Use regex to replace digits with a dash: 76 | 77 | ```bash 78 | fara --regex "\d+" --replace "-" --target "./data/*.csv" --confirm 79 | ``` 80 | 81 | Case-insensitive replace: 82 | 83 | ```bash 84 | fara --find "welcome" --replace "Hello" --target "main.rs" --ignore-case 85 | ``` 86 | 87 | --- 88 | 89 | ## 🤝 Contributing 90 | 91 | We welcome contributions! 92 | If you find a bug, want to request a feature, or contribute code, feel free to [open an issue](https://github.com/ibilalkayy/fara/issues) or submit a pull request. 93 | 94 | --- 95 | 96 | ## 📄 License 97 | 98 | Licensed under the [Apache-2.0 License](LICENSE). 99 | -------------------------------------------------------------------------------- /src/cli/command/command.rs: -------------------------------------------------------------------------------- 1 | use clap::Parser; 2 | 3 | #[derive(Debug, Parser)] 4 | #[clap( 5 | author = "Bilal Khan", 6 | version, 7 | about = "fara stands for Find And Replace Application — a fast, flexible command-line tool to search and replace text across files and folders." 8 | )] 9 | pub struct Fara { 10 | /// Find the text to be changed 11 | #[clap(short, long)] 12 | pub find: Option, 13 | 14 | /// Use expressions for finding the text 15 | #[clap(short = 'e', long)] 16 | pub regex: Option, 17 | 18 | /// Write the text to be replaced with 19 | #[clap(short, long)] 20 | pub replace: Option, 21 | 22 | /// Mention the target path to find the text there 23 | #[clap(short, long)] 24 | pub target: Option, 25 | 26 | /// Include the files that are matching the globe pattern 27 | #[clap(long)] 28 | pub backup: Option, 29 | 30 | /// Ignore the case while finding the data 31 | #[clap(long)] 32 | pub ignore_case: bool, 33 | 34 | /// Assure the text before replacing it 35 | #[clap(long)] 36 | pub confirm: bool, 37 | 38 | /// Show the replaced text before writing it 39 | #[clap(long)] 40 | pub dry_run: bool, 41 | 42 | /// Store the modified text into another file 43 | #[clap(long)] 44 | pub output: Option, 45 | } 46 | -------------------------------------------------------------------------------- /src/cli/command/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | -------------------------------------------------------------------------------- /src/cli/controller/controller.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | cli::command::command::Fara, 3 | usecases::{ 4 | backup::file_backup, dry_run::dry_run_text, find::find_text, ignore_case::ignore_case, 5 | regex::find_regex, replace::replace_text, 6 | }, 7 | }; 8 | 9 | impl Fara { 10 | pub fn control_args(self) { 11 | if self.backup.is_some() 12 | && (self.find.is_some() 13 | || self.regex.is_some() 14 | || self.replace.is_some() 15 | || self.dry_run 16 | || self.confirm 17 | || self.output.is_some()) 18 | { 19 | eprintln!("Err: --backup cannot be used with the given flags except --target"); 20 | return; 21 | } else if self.backup.is_some() && self.target.is_none() { 22 | eprintln!("Err: --backup requires the --target flag"); 23 | return; 24 | } 25 | 26 | if self.confirm && (self.dry_run || self.output.is_some() || self.ignore_case) { 27 | eprintln!("Err: --confirm cannot be used with --dry-run, --ignore-case or --output"); 28 | return; 29 | } 30 | 31 | if self.output.is_some() && self.dry_run { 32 | eprintln!("Err: --output cannot be used with --dry-run"); 33 | return; 34 | } 35 | 36 | if self.output.is_some() && self.ignore_case { 37 | eprintln!("Err: --output cannot be used with --ignore-case"); 38 | return; 39 | } 40 | 41 | if self.confirm || self.dry_run || self.output.is_some() { 42 | if self.find.is_none() && self.regex.is_none() { 43 | eprintln!( 44 | "Err: --confirm, --dry-run or --output requires either --find or --regex" 45 | ); 46 | return; 47 | } 48 | if self.replace.is_none() { 49 | eprintln!("Err: --confirm, --dry-run or --output requires --replace"); 50 | return; 51 | } 52 | 53 | if self.target.is_none() { 54 | eprintln!("Err: --confirm, --dry-run or --output requires --target"); 55 | return; 56 | } 57 | 58 | if let Some(target) = &self.target { 59 | self.finding_options(target); 60 | return; 61 | } else { 62 | eprintln!("Err: --replace, --dry-run, --output or --backup requires --target"); 63 | return; 64 | } 65 | } else { 66 | if self.backup.is_some() { 67 | if let Some(backup_file) = &self.backup { 68 | if let Some(target) = &self.target { 69 | file_backup(backup_file, target); 70 | return; 71 | } 72 | } 73 | } else if self.ignore_case { 74 | if self.find.is_none() && self.regex.is_none() { 75 | eprintln!("Err: --ignore-case requires either --find or --regex"); 76 | return; 77 | } 78 | if self.replace.is_none() { 79 | eprintln!("Err: --ignore-case requires --replace"); 80 | return; 81 | } 82 | 83 | if self.target.is_none() { 84 | eprintln!("Err: --ignore-case requires --target"); 85 | return; 86 | } 87 | 88 | if let Some(find) = &self.find { 89 | if let Some(target) = &self.target { 90 | if let Some(replace) = &self.replace { 91 | let data = ignore_case(find, target); 92 | replace_text(target, target, &data, replace); 93 | return; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | eprintln!("Err: no valid operation selected. Write 'fara --help' for more info"); 100 | } 101 | 102 | fn finding_options(&self, path: &String) { 103 | if let Some(find) = &self.find { 104 | let text_found = find_text(find, path); 105 | if text_found { 106 | self.actions(path, find); 107 | } else { 108 | eprintln!("Err: '{}' text is not found in a given file", find); 109 | } 110 | } else if let Some(regex) = &self.regex { 111 | if let Some(regex_text) = find_regex(regex, path) { 112 | self.actions(path, ®ex_text); 113 | } else { 114 | eprintln!( 115 | "Err: text with this expression '{}' is not found in a given file", 116 | regex 117 | ); 118 | } 119 | } 120 | } 121 | 122 | fn actions(&self, inner_path: &String, find_text: &String) { 123 | let replace_txt = self.replace.as_ref().unwrap(); 124 | if self.confirm { 125 | replace_text(inner_path, inner_path, find_text, replace_txt); 126 | } else if self.dry_run { 127 | self.handle_dry_run(replace_txt); 128 | } else if let Some(outer_path) = &self.output { 129 | replace_text(inner_path, outer_path, find_text, replace_txt); 130 | } 131 | } 132 | 133 | fn handle_dry_run(&self, replace_txt: &String) { 134 | if self.target.is_some() { 135 | if let Some(target) = &self.target { 136 | if let Some(find) = &self.find { 137 | dry_run_text(target, find, replace_txt); 138 | } else if let Some(regex) = &self.regex { 139 | if let Some(regex_found) = find_regex(®ex, target) { 140 | dry_run_text(target, ®ex_found, replace_txt); 141 | } else { 142 | eprintln!("Err: '{}' is not found in the given file", regex); 143 | } 144 | } else { 145 | eprintln!("Err: use either --find or --regex flag to find the text"); 146 | } 147 | } else { 148 | eprintln!("Err: failed to get the target"); 149 | } 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/cli/controller/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod controller; 2 | -------------------------------------------------------------------------------- /src/cli/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod command; 2 | pub mod controller; 3 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod cli; 2 | mod usecases; 3 | 4 | use clap::Parser; 5 | use cli::command::command::Fara; 6 | 7 | fn main() { 8 | let args = Fara::parse(); 9 | args.control_args(); 10 | } 11 | // Hello world this is it 12 | -------------------------------------------------------------------------------- /src/usecases/backup.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::{self, File}, 3 | io::{Read, Write}, 4 | }; 5 | 6 | pub fn file_backup(backup_file: &str, target: &str) { 7 | let home_dir = dirs::home_dir().expect("Err: failed to get the home directory"); 8 | let joined_dir = home_dir.join("fara"); 9 | let merge_path = joined_dir.join(backup_file); 10 | 11 | if !joined_dir.exists() { 12 | fs::create_dir(&joined_dir).expect("Err: failed to create the home directory"); 13 | } 14 | 15 | if !merge_path.exists() { 16 | let mut data_file = File::create_new(&merge_path).expect("Err: failed to create the file"); 17 | let mut data_result = File::open(target).expect("Err: failed to open the file"); 18 | let mut file_content = String::new(); 19 | 20 | data_result.read_to_string(&mut file_content).unwrap(); 21 | data_file 22 | .write(file_content.as_bytes()) 23 | .expect("Err: failed to write into the file"); 24 | 25 | println!( 26 | "Backup data is successfully saved in the {:?} file", 27 | merge_path 28 | ); 29 | } else { 30 | eprintln!("Err: {:?} file already exists", merge_path); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/usecases/dry_run.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Read}; 2 | 3 | pub fn dry_run_text(taken_path: &str, from: &str, to: &str) { 4 | let mut content = String::new(); 5 | File::open(taken_path) 6 | .expect("Err: file opening failed") 7 | .read_to_string(&mut content) 8 | .expect("Err: file reading failed"); 9 | 10 | let new_content = content.replace(from, to); 11 | println!("{}", new_content); 12 | } 13 | -------------------------------------------------------------------------------- /src/usecases/find.rs: -------------------------------------------------------------------------------- 1 | use std::{fs::File, io::Read}; 2 | 3 | pub fn find_text(text: &str, path: &str) -> bool { 4 | let mut file_path = File::open(path).expect("Err: failed to open the file"); 5 | let mut file_content = String::new(); 6 | 7 | file_path.read_to_string(&mut file_content).unwrap(); 8 | if file_content.contains(text) { 9 | return true; 10 | } else { 11 | return false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/usecases/ignore_case.rs: -------------------------------------------------------------------------------- 1 | use std::{ 2 | fs::File, 3 | io::{BufRead, BufReader}, 4 | }; 5 | 6 | pub fn ignore_case(find_data: &str, path: &str) -> String { 7 | let file_text = File::open(path).expect("Err: failed to open the file"); 8 | let reader = BufReader::new(file_text); 9 | 10 | for line in reader.lines() { 11 | let line = line.expect("Err: failed to read the line"); 12 | 13 | for word in line.split_whitespace() { 14 | if word.eq_ignore_ascii_case(find_data) { 15 | return word.to_string(); 16 | } 17 | } 18 | } 19 | return String::new(); 20 | } 21 | -------------------------------------------------------------------------------- /src/usecases/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod backup; 2 | pub mod dry_run; 3 | pub mod find; 4 | pub mod ignore_case; 5 | pub mod regex; 6 | pub mod replace; 7 | -------------------------------------------------------------------------------- /src/usecases/regex.rs: -------------------------------------------------------------------------------- 1 | use regex::Regex; 2 | use std::{fs::File, io::Read}; 3 | 4 | pub fn find_regex(expression: &str, path: &str) -> Option { 5 | let re = Regex::new(&expression).unwrap(); 6 | let mut file_path = File::open(path).expect("Err: failed to open the file"); 7 | let mut file_content = String::new(); 8 | 9 | file_path.read_to_string(&mut file_content).unwrap(); 10 | if let Some(m) = re.find(&file_content) { 11 | return Some(m.as_str().to_string()); 12 | } 13 | None 14 | } 15 | -------------------------------------------------------------------------------- /src/usecases/replace.rs: -------------------------------------------------------------------------------- 1 | use std::fs::{File, OpenOptions}; 2 | use std::io::{Read, Write}; 3 | 4 | pub fn replace_text(from_path: &str, to_path: &str, from: &str, to: &str) { 5 | let mut content = String::new(); 6 | File::open(from_path) 7 | .expect("Err: file opening failed") 8 | .read_to_string(&mut content) 9 | .expect("Err: file reading failed"); 10 | 11 | let new_content = content.replace(from, to); 12 | 13 | let mut file = OpenOptions::new() 14 | .create(true) 15 | .write(true) 16 | .truncate(true) 17 | .open(to_path) 18 | .expect("Err: file opening for writing failed"); 19 | 20 | match file.write_all(new_content.as_bytes()) { 21 | Ok(_) => println!("'{}' is successfully replaced with '{}'", from, to), 22 | Err(error) => eprintln!("Failure occurred while replacing the text: {}", error), 23 | } 24 | } 25 | --------------------------------------------------------------------------------