{"id":13493222,"url":"https://github.com/alexpovel/srgn","last_synced_at":"2025-04-11T03:32:55.321Z","repository":{"id":168036121,"uuid":"641495940","full_name":"alexpovel/srgn","owner":"alexpovel","description":"A grep-like tool which understands source code syntax and allows for manipulation in addition to search","archived":false,"fork":false,"pushed_at":"2025-04-01T17:04:27.000Z","size":14651,"stargazers_count":723,"open_issues_count":9,"forks_count":9,"subscribers_count":6,"default_branch":"main","last_synced_at":"2025-04-10T11:08:52.935Z","etag":null,"topics":["abstract-syntax-tree","c","clang","cli","csharp","go","grep","hacktoberfest","hcl","python","regex","rust","rust-lang","sed","tr","tree-sitter","typescript"],"latest_commit_sha":null,"homepage":"https://crates.io/crates/srgn/","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/alexpovel.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE-APACHE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2023-05-16T15:32:07.000Z","updated_at":"2025-04-02T11:01:53.000Z","dependencies_parsed_at":"2023-07-24T20:08:14.953Z","dependency_job_id":"9d8fb9f8-6538-4edd-8ef3-ccf2b8415645","html_url":"https://github.com/alexpovel/srgn","commit_stats":{"total_commits":661,"total_committers":9,"mean_commits":73.44444444444444,"dds":0.1497730711043873,"last_synced_commit":"50bf4c506792a7da40cd61a88e1f3e462e93d988"},"previous_names":["alexpovel/betterletter-rs","alexpovel/betterletters","alexpovel/srgn"],"tags_count":28,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fsrgn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fsrgn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fsrgn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/alexpovel%2Fsrgn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/alexpovel","download_url":"https://codeload.github.com/alexpovel/srgn/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248208690,"owners_count":21065205,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["abstract-syntax-tree","c","clang","cli","csharp","go","grep","hacktoberfest","hcl","python","regex","rust","rust-lang","sed","tr","tree-sitter","typescript"],"created_at":"2024-07-31T19:01:13.305Z","updated_at":"2025-04-11T03:32:55.292Z","avatar_url":"https://github.com/alexpovel.png","language":"Rust","funding_links":[],"categories":["Rust","cli","\u003ca name=\"text-search-replace\"\u003e\u003c/a\u003eText search and replace (alternatives to sed)","c","Text Search"],"sub_categories":[],"readme":"# srgn - a code surgeon\n\nA `grep`-like tool which understands source code syntax and allows for manipulation in\naddition to search.\n\nLike `grep`, regular expressions are a core primitive. Unlike `grep`, additional\ncapabilities allow for **higher precision**, with **options for manipulation**. This\nallows `srgn` to operate along dimensions regular expressions and IDE tooling (*Rename\nall*, *Find all references*, ...) alone cannot, complementing them.\n\n`srgn` is organized around *actions* to take (if any), acting only within precise,\noptionally **language grammar-aware** *scopes*. In terms of existing tools, think of it\nas a mix of\n[`tr`](https://www.gnu.org/software/coreutils/manual/html_node/tr-invocation.html#tr-invocation),\n[`sed`](https://www.gnu.org/software/sed/),\n[ripgrep](https://github.com/BurntSushi/ripgrep) and\n[`tree-sitter`](https://tree-sitter.github.io/tree-sitter/), with a design goal of\n*simplicity*: if you know regex and the basics of the language you are working with, you\nare good to go.\n\n\u003e The answer to \"What if `grep`, `tr`, `sed` and `tree-sitter` got really drunk one\n\u003e night and had a baby?\"\n\u003e\n\u003e -- [Real Python Podcast](https://realpython.com/podcasts/rpp/225/#t=2286)\n\n## Quick walkthrough\n\n\u003e [!TIP]\n\u003e\n\u003e All code snippets displayed here are [verified as part of unit tests](tests/readme.rs)\n\u003e using the actual `srgn` binary. What is showcased here is guaranteed to work.\n\nThe most simple `srgn` usage works [similar to `tr`](#comparison-with-tr):\n\n```bash\n$ echo 'Hello World!' | srgn '[wW]orld' 'there' # replacement\nHello there!\n```\n\nMatches for the regular expression pattern `'[wW]orld'` (the *scope*) are replaced (the\n*action*) by the second positional argument. Zero or more actions can be specified:\n\n```bash\n$ echo 'Hello World!' | srgn '[wW]orld' # zero actions: input returned unchanged\nHello World!\n$ echo 'Hello World!' | srgn --upper '[wW]orld' 'you' # two actions: replacement, afterwards uppercasing\nHello YOU!\n```\n\nReplacement is always performed first and specified positionally. Any [other\nactions](#actions) are applied after and given as command line flags.\n\n### Multiple scopes\n\nSimilarly, more than one scope can be specified: in addition to the regex pattern, a\n[**language grammar-aware**](https://tree-sitter.github.io/tree-sitter/) scope can be\ngiven, which scopes to **syntactical elements of source code** (think, for example, \"all\nbodies of `class` definitions in Python\"). If both are given, the regular expression\npattern is then **only applied *within* that first, language scope**. This enables\nsearch and manipulation at precision not normally possible using plain regular\nexpressions, and serving a dimension different from tools such as *Rename all* in IDEs.\n\nFor example, consider this (pointless) Python source file:\n\n```python file=birds.py\n\"\"\"Module for watching birds and their age.\"\"\"\n\nfrom dataclasses import dataclass\n\n\n@dataclass\nclass Bird:\n    \"\"\"A bird!\"\"\"\n\n    name: str\n    age: int\n\n    def celebrate_birthday(self):\n        print(\"🎉\")\n        self.age += 1\n\n    @classmethod\n    def from_egg(egg):\n        \"\"\"Create a bird from an egg.\"\"\"\n        pass  # No bird here yet!\n\n\ndef register_bird(bird: Bird, db: Db) -\u003e None:\n    assert bird.age \u003e= 0\n    with db.tx() as tx:\n        tx.insert(bird)\n```\n\nwhich can be searched using:\n\n```console\n$ cat birds.py | srgn --python 'class' 'age'\n11:    age: int\n15:        self.age += 1\n```\n\nThe string `age` was sought and found *only* within Python `class` definitions (and not,\nfor example, in function bodies such as `register_bird`, where `age` also occurs and\nwould be nigh impossible to exclude from consideration in vanilla `grep`). By default,\nthis 'search mode' also prints line numbers. **Search mode is entered if no actions are\nspecified**, and a language such as `--python` is given[^3]—think of it like\n'[ripgrep](https://github.com/BurntSushi/ripgrep) but with syntactical language\nelements'.\n\nSearching can also be performed [across\nlines](https://docs.rs/regex/1.10.5/regex/index.html#grouping-and-flags), for example to\nfind methods (aka *`def` within `class`*) lacking docstrings:\n\n```console\n$ cat birds.py | srgn --python 'class' 'def .+:\\n\\s+[^\"\\s]{3}' # do not try this pattern at home\n13:    def celebrate_birthday(self):\n14:        print(\"🎉\")\n```\n\nNote how this does not surface either `from_egg` (has a docstring) or `register_bird`\n(not a method, *`def` outside `class`*).\n\n#### Multiple language scopes\n\nLanguage scopes themselves can be specified multiple times as well. For example, in the\nRust snippet\n\n```rust file=music.rs\npub enum Genre {\n    Rock(Subgenre),\n    Jazz,\n}\n\nconst MOST_POPULAR_SUBGENRE: Subgenre = Subgenre::Something;\n\npub struct Musician {\n    name: String,\n    genres: Vec\u003cSubgenre\u003e,\n}\n```\n\nmultiple items can be surgically drilled down into as\n\n```console\n$ cat music.rs | srgn --rust 'pub-enum' --rust 'type-identifier' 'Subgenre' # AND'ed together\n2:    Rock(Subgenre),\n```\n\nwhere only lines matching *all* criteria are returned, acting like a logical *and*\nbetween all conditions. Note that conditions are evaluated left-to-right, precluding\nsome combinations from making sense: for example, searching for a Python `class` body\n*inside* of Python `doc-strings` usually returns nothing. The inverse works as expected\nhowever:\n\n```console\n$ cat birds.py | srgn --py 'class' --py 'doc-strings' \n8:    \"\"\"A bird!\"\"\"\n19:        \"\"\"Create a bird from an egg.\"\"\"\n```\n\nNo docstrings outside `class` bodies are surfaced!\n\nThe [`-j` flag](#help-output) changes this behavior: from intersecting left-to-right, to\nrunning all queries independently and joining their results, allowing you to search\nmultiple ways at once:\n\n```console\n$ cat birds.py | srgn -j --python 'comments' --python 'doc-strings' 'bird[^s]'\n8:    \"\"\"A bird!\"\"\"\n19:        \"\"\"Create a bird from an egg.\"\"\"\n20:        pass  # No bird here yet!\n```\n\nThe pattern `bird[^s]` was found inside of comments *or* docstrings likewise, not just\n\"docstrings *within* comments\".\n\n#### Working recursively\n\nIf standard input is not given, `srgn` knows how to find relevant source files\nautomatically, for example in this repository:\n\n```console\n$ srgn --python 'class' 'age'\ndocs/samples/birds\n11:    age: int\n15:        self.age += 1\n\ndocs/samples/birds.py\n9:    age: int\n13:        self.age += 1\n```\n\nIt recursively walks its current directory, finding files based on [file\nextensions](docs/samples/birds.py) and [shebang lines](docs/samples/birds), processing\nat very high speed. For example, `srgn --go strings '\\d+'` finds and prints all ~140,000\nruns of digits in literal Go strings inside the [Kubernetes\ncodebase](https://github.com/kubernetes/kubernetes/tree/5639f8f848720329f4a9d53555a228891550cb79)\nof ~3,000,000 lines of Go code within 3 seconds on 12 cores of M3. For more on working\nwith many files, see [below](#run-against-multiple-files).\n\n### Combining actions and scopes\n\nScopes and actions can be combined almost arbitrarily (though many combinations are not\ngoing to be use- or even meaningful). For example, consider this Python snippet (for\nexamples using other supported languages see\n[below](#prepared-queries)):\n\n```python file=gnu.py\n\"\"\"GNU module.\"\"\"\n\ndef GNU_says_moo():\n    \"\"\"The GNU function -\u003e say moo -\u003e ✅\"\"\"\n\n    GNU = \"\"\"\n      GNU\n    \"\"\"  # the GNU...\n\n    print(GNU + \" says moo\")  # ...says moo\n```\n\nagainst which the following command is run:\n\n```bash\ncat gnu.py | srgn --titlecase --python 'doc-strings' '(?\u003c!The )GNU ([a-z]+)' '$1: GNU 🐂 is not Unix'\n```\n\nThe anatomy of that invocation is:\n\n- `--titlecase` (an [action](#character-casing)) will Titlecase Everything Found In\n  Scope\n- `--python 'doc-strings'` (a [scope](#language-grammar-aware-scopes)) will scope to\n  (i.e., only take into consideration) docstrings according to the Python language\n  grammar\n- `'(?\u003c!The )GNU ([a-z]+)'` (a [scope](#scopes)) sees only what was already scoped by\n  the previous option, and will narrow it down further. It can never extend the previous\n  scope. The regular expression scope is applied after any language scope(s).\n\n  \u003c!-- markdownlint-disable MD038 --\u003e\n  `(?\u003c!)` is [negative\n  lookbehind](https://docs.rs/fancy-regex/latest/fancy_regex/#syntax) syntax,\n  demonstrating how this advanced feature is available. Strings of `GNU` prefixed by\n  `The ` will not be considered.\n  \u003c!-- markdownlint-enable MD038 --\u003e\n- `'$1: GNU 🐂 is not Unix'` (an [action](#replacement)) will *replace* each matched\n  occurrence (i.e., each input section found to be in scope) with this string. Matched\n  occurrences are patterns of `'(?\u003c!The )GNU ([a-z]+)'` *only within* Python docstrings.\n  Notably, this replacement string demonstrates:\n\n  - dynamic [variable binding and substitution](#variables) using `$1`, which carries\n  the contents captured by the first capturing regex group. That's `([a-z]+)`, as\n  `(?\u003c!The )` is not capturing.\n  - full Unicode support (🐂).\n\nThe command makes use of multiple scopes (language and regex pattern) and multiple\nactions (replacement and titlecasing). The result then reads\n\n```python file=output-gnu.py\n\"\"\"Module: GNU 🐂 Is Not Unix.\"\"\"\n\ndef GNU_says_moo():\n    \"\"\"The GNU function -\u003e say moo -\u003e ✅\"\"\"\n\n    GNU = \"\"\"\n      GNU\n    \"\"\"  # the GNU...\n\n    print(GNU + \" says moo\")  # ...says moo\n```\n\nwhere the changes are limited to:\n\n```diff\n- \"\"\"GNU module.\"\"\"\n+ \"\"\"Module: GNU 🐂 Is Not Unix.\"\"\"\n\ndef GNU_says_moo():\n    \"\"\"The GNU -\u003e say moo -\u003e ✅\"\"\"\n```\n\n\u003e [!WARNING]\n\u003e\n\u003e While `srgn` is in beta ([major version](https://semver.org/) 0), make sure to only\n\u003e (recursively) process files you can safely\n\u003e [restore](https://git-scm.com/docs/git-restore).\n\u003e\n\u003e Search mode does not overwrite files, so is always safe.\n\nSee [below](#help-output) for the full help output of the tool.\n\n\u003e [!NOTE]\n\u003e\n\u003e Supported languages are\n\u003e\n\u003e - C\n\u003e - C#\n\u003e - Go\n\u003e - HCL (Terraform)\n\u003e - Python\n\u003e - Rust\n\u003e - TypeScript\n\n## Installation\n\n### Prebuilt binaries\n\nDownload a prebuilt binary from the\n[releases](https://github.com/alexpovel/srgn/releases/latest).\n\n### cargo-binstall\n\nThis crate provides its binaries in a format\n[compatible](https://github.com/cargo-bins/cargo-binstall/blob/9cfc0cd5f97300925ae60f67712b74970a380aca/SUPPORT.md#support-for-cargo-binstall)\nwith [`cargo-binstall`](https://github.com/cargo-bins/cargo-binstall):\n\n1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install)\n2. Run `cargo install cargo-binstall` (might take a while)\n3. Run `cargo binstall srgn` (couple seconds, as it downloads [prebuilt\n   binaries](#prebuilt-binaries) from GitHub)\n\nThese steps are guaranteed to work™, as they are [tested in\nCI](./.github/workflows/main.yml). They also work if no prebuilt binaries are available\nfor your platform, as the tool will fall back to [compiling from\nsource](#cargo-compile-from-source).\n\n### Homebrew\n\nA [formula](https://formulae.brew.sh/formula/srgn) is available via:\n\n```text\nbrew install srgn\n```\n\n### Nix\n\nAvailable via [unstable](https://search.nixos.org/packages?channel=unstable\u0026show=srgn\u0026from=0\u0026size=50\u0026sort=relevance\u0026type=packages\u0026query=srgn):\n\n```text\nnix-shell -p srgn\n```\n\n### Arch Linux\n\nAvailable via the [AUR](https://aur.archlinux.org/cgit/aur.git/tree/PKGBUILD?h=srgn).\n\n### MacPorts\n\nA [port](https://ports.macports.org/port/srgn/) is available:\n\n```text\nsudo port install srgn\n```\n\n### CI (GitHub Actions)\n\nAll [GitHub Actions runner\nimages](https://github.com/actions/runner-images/tree/main/images) come with `cargo`\npreinstalled, and `cargo-binstall` provides a convenient [GitHub\nAction](https://github.com/marketplace/actions/install-cargo-binstall):\n\n```yaml\njobs:\n  srgn:\n    name: Install srgn in CI\n    # All three major OSes work\n    runs-on: ubuntu-latest\n    steps:\n      - uses: cargo-bins/cargo-binstall@main\n      - name: Install binary\n        run: \u003e\n          cargo binstall\n          --no-confirm\n          srgn\n      - name: Use binary\n        run: srgn --version\n```\n\nThe above concludes in just [5 seconds\ntotal](https://github.com/alexpovel/srgn/actions/runs/6605290729/job/17940329899), as no\ncompilation is required. For more context, see [`cargo-binstall`'s advise on\nCI](https://github.com/cargo-bins/cargo-binstall#can-i-use-it-in-ci).\n\n### Cargo (compile from source)\n\n1. Install the [Rust toolchain](https://www.rust-lang.org/tools/install)\n2. A C compiler is required:\n   1. On Linux, `gcc` works.\n   2. On macOS, use `clang`.\n   3. On Windows, [MSVC](https://visualstudio.microsoft.com/downloads/) works.\n\n      Select \"Desktop development with C++\" on installation.\n3. Run `cargo install srgn`\n\n### Cargo (as a Rust library)\n\n```text\ncargo add srgn\n```\n\nSee [here](#rust-library) for more.\n\n### Shell completions\n\n[Various\nshells](https://docs.rs/clap_complete/4.5.1/clap_complete/shells/enum.Shell.html#variants)\nare supported for shell completion scripts. For example, append `eval \"$(srgn\n--completions zsh)\"` to `~/.zshrc` for completions in ZSH. An interactive session can\nthen look like:\n\n[![srgn shell completion](./docs/images/interactive-use-shell-completion.gif)](https://asciinema.org/a/673473)\n\n## Walkthrough\n\nThe tool is designed around **scopes** and **actions**. Scopes narrow down the parts of\nthe input to process. Actions then perform the processing. Generally, both scopes and\nactions are composable, so more than one of each may be passed. Both are optional (but\ntaking no action is pointless); specifying no scope implies the entire input is in\nscope.\n\nAt the same time, there is [considerable overlap](#comparison-with-tr) with plain\n[`tr`][tr]: the tool is designed to have close correspondence in the most common use\ncases, and only go beyond when needed.\n\n### Actions\n\nThe simplest action is replacement. It is specially accessed (as an argument, not an\noption) for compatibility with [`tr`][tr], and general ergonomics. All other actions are\ngiven as flags, or options should they take a value.\n\n#### Replacement\n\nFor example, simple, single-character replacements work as in [`tr`][tr]:\n\n```console\n$ echo 'Hello, World!' | srgn 'H' 'J'\nJello, World!\n```\n\nThe first argument is the scope (literal `H` in this case). Anything matched by it is\nsubject to processing (replacement by `J`, the second argument, in this case). However,\nthere is **no direct concept of character classes** as in [`tr`][tr]. Instead, by\ndefault, the scope is a regular expression pattern, so *its*\n[classes](https://docs.rs/regex/1.9.5/regex/index.html#character-classes) can be used to\nsimilar effect:\n\n```console\n$ echo 'Hello, World!' | srgn '[a-z]' '_'\nH____, W____!\n```\n\nThe replacement occurs greedily across the entire match by default (note the [UTS\ncharacter class](https://docs.rs/regex/1.9.5/regex/index.html#ascii-character-classes),\nreminiscent of [`tr`'s\n`[:alnum:]`](https://github.com/coreutils/coreutils/blob/769ace51e8a1129c44ee4e7e209c3b2df2111524/src/tr.c#L322C25-L322C25)):\n\n```console\n$ echo 'ghp_oHn0As3cr3T!!' | srgn 'ghp_[[:alnum:]]+' '*' # A GitHub token\n*!!\n```\n\nAdvanced regex features are\n[supported](https://docs.rs/fancy-regex/0.11.0/fancy_regex/index.html#syntax), for\nexample lookarounds:\n\n```console\n$ echo 'ghp_oHn0As3cr3T' | srgn '(?\u003c=ghp_)[[:alnum:]]+' '*'\nghp_*\n```\n\nTake care in using these safely, as advanced patterns come without certain [safety and\nperformance guarantees](https://docs.rs/regex/latest/regex/#untrusted-input). If they\naren't used, [performance is not\nimpacted](https://docs.rs/fancy-regex/0.11.0/fancy_regex/index.html#).\n\nThe replacement is not limited to a single character. It can be any string, for example\nto fix [this quote](http://regex.info/blog/2006-09-15/247):\n\n```console\n$ echo '\"Using regex, I now have no issues.\"' | srgn 'no issues' '2 problems'\n\"Using regex, I now have 2 problems.\"\n```\n\nThe tool is fully Unicode-aware, with useful support for [certain advanced\ncharacter\nclasses](https://github.com/rust-lang/regex/blob/061ee815ef2c44101dba7b0b124600fcb03c1912/UNICODE.md#rl12-properties):\n\n```console\n$ echo 'Mood: 🙂' | srgn '🙂' '😀'\nMood: 😀\n$ echo 'Mood: 🤮🤒🤧🦠 :(' | srgn '\\p{Emoji_Presentation}' '😷'\nMood: 😷😷😷😷 :(\n```\n\n##### Variables\n\nReplacements are aware of variables, which are made accessible for use through regex\ncapture groups. Capture groups can be numbered, or optionally named. The zeroth capture\ngroup corresponds to the entire match.\n\n```console\n$ echo 'Swap It' | srgn '(\\w+) (\\w+)' '$2 $1' # Regular, numbered\nIt Swap\n$ echo 'Swap It' | srgn '(\\w+) (\\w+)' '$2 $1$1$1' # Use as many times as you'd like\nIt SwapSwapSwap\n$ echo 'Call +1-206-555-0100!' | srgn 'Call (\\+?\\d\\-\\d{3}\\-\\d{3}\\-\\d{4}).+' 'The phone number in \"$0\" is: $1.' # Variable `0` is the entire match\nThe phone number in \"Call +1-206-555-0100!\" is: +1-206-555-0100.\n```\n\nA more advanced use case is, for example, code refactoring using named capture groups\n(perhaps you can come up with a more useful one...):\n\n```console\n$ echo 'let x = 3;' | srgn 'let (?\u003cvar\u003e[a-z]+) = (?\u003cexpr\u003e.+);' 'const $var$var = $expr + $expr;'\nconst xx = 3 + 3;\n```\n\nAs in bash, use curly braces to disambiguate variables from immediately adjacent\ncontent:\n\n```console\n$ echo '12' | srgn '(\\d)(\\d)' '$2${1}1'\n211\n$ echo '12' | srgn '(\\d)(\\d)' '$2$11' # will fail (`11` is unknown)\n$ echo '12' | srgn '(\\d)(\\d)' '$2${11' # will fail (brace was not closed)\n```\n\n#### Beyond replacement\n\nSeeing how the replacement is merely a static string, its usefulness is limited. This is\nwhere [`tr`'s secret sauce](https://maizure.org/projects/decoded-gnu-coreutils/tr.html)\nordinarily comes into play: using its character classes, which are valid in the second\nposition as well, neatly translating from members of the first to the second. Here,\nthose classes are instead regexes, and only valid in first position (the scope). A\nregular expression being a state machine, it is impossible to match onto a 'list of\ncharacters', which in `tr` is the second (optional) argument. That concept is out the\nwindow, and its flexibility lost.\n\nInstead, the offered actions, all of them **fixed**, are used. A peek at [the most\ncommon use cases for `tr`](#use-cases-and-equivalences) reveals that the provided set of\nactions covers virtually all of them! Feel free to file an issue if your use case is not\ncovered.\n\nOnto the next action.\n\n#### Deletion\n\nRemoves whatever is found from the input. Same flag name as in `tr`.\n\n```console\n$ echo 'Hello, World!' | srgn -d '(H|W|!)'\nello, orld\n```\n\n\u003e [!NOTE]\n\u003e As the default scope is to match the entire input, it is an error to specify\n\u003e deletion without a scope.\n\n#### Squeezing\n\nSqueezes repeats of characters matching the scope into single occurrences. Same flag\nname as in `tr`.\n\n```console\n$ echo 'Helloooo Woooorld!!!' | srgn -s '(o|!)'\nHello World!\n```\n\nIf a character class is passed, all members of that class are squeezed into whatever\nclass member was encountered first:\n\n```console\n$ echo 'The number is: 3490834' | srgn -s '\\d'\nThe number is: 3\n```\n\nGreediness in matching is not modified, so take care:\n\n```console\n$ echo 'Winter is coming... 🌞🌞🌞' | srgn -s '🌞+'\nWinter is coming... 🌞🌞🌞\n```\n\n\u003e [!NOTE]\n\u003e The pattern matched the *entire* run of suns, so there's nothing to squeeze. Summer\n\u003e prevails.\n\nInvert greediness if the use case calls for it:\n\n```console\n$ echo 'Winter is coming... 🌞🌞🌞' | srgn -s '🌞+?' '☃️'\nWinter is coming... ☃️\n```\n\n\u003e [!NOTE]\n\u003e Again, as with [deletion](#deletion), specifying squeezing without an *explicit* scope\n\u003e is an error. Otherwise, the entire input is squeezed.\n\n#### Character casing\n\nA good chunk of `tr` usage [falls into this category](#changing-character-casing). It's\nvery straightforward.\n\n```console\n$ echo 'Hello, World!' | srgn --lower\nhello, world!\n$ echo 'Hello, World!' | srgn --upper\nHELLO, WORLD!\n$ echo 'hello, world!' | srgn --titlecase\nHello, World!\n```\n\n#### Normalization\n\nDecomposes input according to [Normalization Form\nD](https://en.wikipedia.org/wiki/Unicode_equivalence#Normal_forms), and then discards\ncode points of the [Mark\ncategory](https://en.wikipedia.org/wiki/Unicode_character_property#General_Category)\n(see [examples](https://www.compart.com/en/unicode/category/Mn)). That roughly means:\ntake fancy character, rip off dangly bits, throw those away.\n\n```console\n$ echo 'Naïve jalapeño ärgert mgła' | srgn -d '\\P{ASCII}' # Naive approach\nNave jalapeo rgert mga\n$ echo 'Naïve jalapeño ärgert mgła' | srgn --normalize # Normalize is smarter\nNaive jalapeno argert mgła\n```\n\nNotice how `mgła` is out of scope for NFD, as it is \"atomic\" and thus not decomposable\n(at least that's what ChatGPT whispers in my ear).\n\n#### Symbols\n\nThis action replaces multi-character, ASCII symbols with appropriate single-code point,\nnative Unicode counterparts.\n\n```console\n$ echo '(A --\u003e B) != C --- obviously' | srgn --symbols\n(A ⟶ B) ≠ C — obviously\n```\n\nAlternatively, if you're only interested in math, make use of scoping:\n\n```console\n$ echo 'A \u003c= B --- More is--obviously--possible' | srgn --symbols '\u003c='\nA ≤ B --- More is--obviously--possible\n```\n\nAs there is a [1:1 correspondence](https://en.wikipedia.org/wiki/Bijection) between an\nASCII symbol and its replacement, the effect is reversible[^1]:\n\n```console\n$ echo 'A ⇒ B' | srgn --symbols --invert\nA =\u003e B\n```\n\nThere is only a limited set of symbols supported as of right now, but more can be added.\n\n#### German\n\nThis action replaces alternative spellings of German special characters (ae, oe, ue, ss)\nwith their native versions (ä, ö, ü, ß)[^2].\n\n```console\n$ echo 'Gruess Gott, Neueroeffnungen, Poeten und Abenteuergruetze!' | srgn --german\nGrüß Gott, Neueröffnungen, Poeten und Abenteuergrütze!\n```\n\nThis action is based on a [word list](./data/word-lists/de.txt) (compile without\n`german` feature if this bloats your binary too much). Note the following features about\nthe above example:\n\n- empty scope and replacement: the entire input will be processed, and no replacement is\n performed\n- `Poeten` remained as-is, instead of being naively and mistakenly converted to `Pöten`\n- as a (compound) word, `Abenteuergrütze` is not going to be found in [any reasonable\n  word list](https://www.duden.de/suchen/dudenonline/Abenteuergr%C3%BCtze), but was\n  handled properly nonetheless\n- while part of a compound word, `Abenteuer` remained as-is as well, instead of being\n  incorrectly converted to `Abenteür`\n- lastly, `Neueroeffnungen` sneakily forms a `ue` element neither constituent word\n  (`neu`, `Eröffnungen`) possesses, but is still processed correctly (despite the\n  mismatched casings as well)\n\nOn request, replacements may be forced, as is potentially useful for names:\n\n```console\n$ echo 'Frau Loetter steht ueber der Mauer.' | srgn --german-naive '(?\u003c=Frau )\\w+'\nFrau Lötter steht ueber der Mauer.\n```\n\nThrough positive lookahead, nothing but the salutation was scoped and therefore changed.\n`Mauer` correctly remained as-is, but `ueber` was not processed. A second pass fixes\nthis:\n\n```console\n$ echo 'Frau Loetter steht ueber der Mauer.' | srgn --german-naive '(?\u003c=Frau )\\w+' | srgn --german\nFrau Lötter steht über der Mauer.\n```\n\n\u003e [!NOTE]\n\u003e\n\u003e Options and flags pertaining to some \"parent\" are prefixed with their parent's name,\n\u003e and will *imply* their parent when given, such that the latter does not need to be\n\u003e passed explicitly. That's why `--german-naive` is named as it is, and `--german`\n\u003e needn't be passed.\n\u003e\n\u003e This behavior might change once `clap` supports [subcommand\n\u003e chaining](https://github.com/clap-rs/clap/issues/2222).\n\nSome branches are undecidable for this modest tool, as it operates without language\ncontext. For example, both `Busse` (busses) and `Buße` (penance) are legal words. By\ndefault, replacements are greedily performed if legal (that's the [whole\npoint](https://en.wikipedia.org/wiki/Principle_of_least_astonishment) of `srgn`,\nafter all), but there's a flag for toggling this behavior:\n\n```console\n$ echo 'Busse und Geluebte 🙏' | srgn --german\nBuße und Gelübte 🙏\n$ echo 'Busse 🚌 und Fussgaenger 🚶‍♀️' | srgn --german-prefer-original\nBusse 🚌 und Fußgänger 🚶‍♀️\n```\n\n### Combining Actions\n\nMost actions are composable, unless doing so were nonsensical (like for\n[deletion](#deletion)). Their order of application is fixed, so the *order* of the flags\ngiven has no influence (piping multiple runs is an alternative, if needed). Replacements\nalways occur first. Generally, the CLI is designed to prevent misuse and\n[surprises](https://en.wikipedia.org/wiki/Principle_of_least_astonishment): it prefers\ncrashing to doing something unexpected (which is subjective, of course). Note that lots\nof combinations *are* technically possible, but might yield nonsensical results.\n\nCombining actions might look like:\n\n```console\n$ echo 'Koeffizienten != Bruecken...' | srgn -Sgu\nKOEFFIZIENTEN ≠ BRÜCKEN...\n```\n\nA more narrow scope can be specified, and will apply to *all* actions equally:\n\n```console\n$ echo 'Koeffizienten != Bruecken...' | srgn -Sgu '\\b\\w{1,8}\\b'\nKoeffizienten != BRÜCKEN...\n```\n\nThe [word boundaries](https://www.regular-expressions.info/wordboundaries.html) are\nrequired as otherwise `Koeffizienten` is matched as `Koeffizi` and `enten`. Note how the\ntrailing periods cannot be, for example, squeezed. The required scope of `\\.` would\ninterfere with the given one. Regular piping solves this:\n\n```console\n$ echo 'Koeffizienten != Bruecken...' | srgn -Sgu '\\b\\w{1,8}\\b' | srgn -s '\\.'\nKoeffizienten != BRÜCKEN.\n```\n\nNote: regex escaping (`\\.`) can be circumvent using [literal scoping](#literal-scope).\nThe specially treated replacement action is also composable:\n\n```console\n$ echo 'Mooood: 🤮🤒🤧🦠!!!' | srgn -s '\\p{Emoji}' '😷'\nMooood: 😷!!!\n```\n\nEmojis are first all replaced, then squeezed. Notice how nothing else is squeezed.\n\n### Scopes\n\nScopes are the second driving concept to `srgn`. In the default case, the main scope is\na regular expression. The [actions](#actions) section showcased this use case in some\ndetail, so it's not repeated here. It is given as a first positional argument.\n\n#### Language grammar-aware scopes\n\n`srgn` extends this through prepared, language grammar-aware scopes, made possible\nthrough the excellent [`tree-sitter`](https://tree-sitter.github.io/tree-sitter/)\nlibrary. It offers a\n[queries](https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax) feature,\nwhich works much like pattern matching against a [tree data\nstructure](https://en.wikipedia.org/wiki/Parse_tree).\n\n`srgn` comes bundled with a handful of the most useful of these queries. Through its\ndiscoverable API (either [as a library](#rust-library) or via CLI, `srgn --help`), one\ncan learn of the supported languages and available, prepared queries. Each supported\nlanguage comes with an escape hatch, allowing you to run your own, custom ad-hoc\nqueries. The hatch comes in the form of `--lang-query \u003cS EXPRESSION\u003e`, where `lang` is a\nlanguage such as `python`. See [below](#custom-queries) for more on this advanced topic.\n\n\u003e [!NOTE]\n\u003e\n\u003e Language scopes are applied *first*, so whatever regex aka main scope you pass, it\n\u003e operates on each matched language construct individually.\n\n##### Prepared queries\n\nThis section shows examples for some of the **prepared queries**.\n\nMost prepared queries are static, e.g. they always scope \"all comments\", for example.\nVarious language elements are naturally *named* however: functions, classes, structs,\nmodules and more are usually not anonymous, but carry names. Some prepared queries\ntherefore have *dynamic* variants, where an optional regular expression pattern can\nadditionally be supplied. The pattern then applies to the name of the item, and will\nonly scope those items whose name matches the pattern. This allows for an extra degree\nof freedom for querying. Examples follow.\n\n###### Finding all structs related to testing (Go)\n\nAn input like\n\n```go file=testing.go\ntype PublicTestInput struct {\n    Name  string\n    Value int\n}\n\ntype privateTestInput struct {\n    name  string\n    value int\n}\n\ntype ActualDomainType struct {\n    Name  string\n    Value int\n}\n```\n\ncan be searched as\n\n```console\n$ cat testing.go | srgn --go 'struct~[tT]est'\n1:type PublicTestInput struct {\n2:    Name  string\n3:    Value int\n4:}\n6:type privateTestInput struct {\n7:    name  string\n8:    value int\n9:}\n```\n\nsurfacing only `struct`s whose name matches `[tT]est`. The `~` character serves as the\nseparator between the language grammar/node type and pattern. This approach allows\nmanipulation of *only these structs*, e.g.\n\n```console\n$ cat testing.go | srgn --go 'struct~[tT]est' '(\\s+)Name' '${1}TestName'\ntype PublicTestInput struct {\n    TestName  string\n    Value int\n}\n\ntype privateTestInput struct {\n    name  string\n    value int\n}\n\ntype ActualDomainType struct {\n    Name  string\n    Value int\n}\n```\n\n###### Finding all `unsafe` code (Rust)\n\nOne advantage of the [`unsafe` keyword in\nRust](https://doc.rust-lang.org/book/ch19-01-unsafe-rust.html) is its \"grepability\".\nHowever, an `rg 'unsafe'` will of course surface *all* string matches (`rg '\\bunsafe\\b'`\nhelps to an extent), not just those in of the actual Rust language keyword. `srgn` helps\nmake this more precise. For example:\n\n```rust file=unsafe.rs\n// Oh no, an unsafe module!\nmod scary_unsafe_operations {\n    pub unsafe fn unsafe_array_access(arr: \u0026[i32], index: usize) -\u003e i32 {\n        // UNSAFE: This function performs unsafe array access without bounds checking\n        *arr.get_unchecked(index)\n    }\n\n    pub fn call_unsafe_function() {\n        let unsafe_numbers = vec![1, 2, 3, 4, 5];\n        println!(\"About to perform an unsafe operation!\");\n        let result = unsafe {\n            // Calling an unsafe function\n            unsafe_array_access(\u0026unsafe_numbers, 10)\n        };\n        println!(\"Result of unsafe operation: {}\", result);\n    }\n}\n```\n\ncan be searched as\n\n```console\n$ cat unsafe.rs | srgn --rs 'unsafe' # Note: no 2nd argument necessary\n3:    pub unsafe fn unsafe_array_access(arr: \u0026[i32], index: usize) -\u003e i32 {\n4:        // UNSAFE: This function performs unsafe array access without bounds checking\n5:        *arr.get_unchecked(index)\n6:    }\n11:        let result = unsafe {\n12:            // Calling an unsafe function\n13:            unsafe_array_access(\u0026unsafe_numbers, 10)\n14:        };\n```\n\nsurfacing only truly `unsafe` items (and not comments, strings etc. merely mentioning\nit).[^4]\n\n###### Replacing `allow` with `expect` for lints (Rust)\n\nTaking advantage of the stabilization of the `expect` lint level in [Rust\n1.81](https://blog.rust-lang.org/2024/09/05/Rust-1.81.0.html#expectlint), one might want\nto migrate from `allow` to `expect` throughout (cf.\n959a692f1d788d071d5f55376e2bb5ff3c2ae15a in this repo):\n\n```rust file=allow.rs\n#[allow(unsafe_code)]\nif let Some(env_value) = env_value {\n    unsafe {\n        env::set_var(DEFAULT_FILTER_ENV, env_value);\n    }\n}\n```\n\ncan be refactored using\n\n```bash\ncat allow.rs | srgn --rust 'attribute' '^allow' 'expect'\n```\n\nwhich will yield\n\n```rust file=output-allow.rs\n#[expect(unsafe_code)]\nif let Some(env_value) = env_value {\n    unsafe {\n        env::set_var(DEFAULT_FILTER_ENV, env_value);\n    }\n}\n```\n\n###### Mass import (module) renaming (Python, Rust)\n\nAs part of a large refactor (say, after an acquisition), imagine all imports of a\nspecific package needed renaming:\n\n```python file=imports.py\nimport math\nfrom pathlib import Path\n\nimport good_company.infra\nimport good_company.aws.auth as aws_auth\nfrom good_company.util.iter import dedupe\nfrom good_company.shopping.cart import *  # Ok but don't do this at home!\n\ngood_company = \"good_company\"  # good_company\n```\n\nAt the same time, a move to [`src/`\nlayout](https://packaging.python.org/en/latest/discussions/src-layout-vs-flat-layout/)\nis desired. Achieve this move with:\n\n```bash\ncat imports.py | srgn --python 'imports' '^good_company' 'src.better_company'\n```\n\nwhich will yield\n\n```python file=output-imports.py\nimport math\nfrom pathlib import Path\n\nimport src.better_company.infra\nimport src.better_company.aws.auth as aws_auth\nfrom src.better_company.util.iter import dedupe\nfrom src.better_company.shopping.cart import *  # Ok but don't do this at home!\n\ngood_company = \"good_company\"  # good_company\n```\n\nNote how the last line remains untouched by this particular operation. To run across\nmany files, see [the `files` option](#run-against-multiple-files).\n\nSimilar import-related edits are supported for other languages as well, for example\nRust:\n\n```rust file=imports.rs\nuse std::collections::HashMap;\n\nuse good_company::infra;\nuse good_company::aws::auth as aws_auth;\nuse good_company::util::iter::dedupe;\nuse good_company::shopping::cart::*;\n\ngood_company = \"good_company\";  // good_company\n```\n\nwhich, using\n\n```bash\ncat imports.rs | srgn --rust 'uses' '^good_company' 'better_company'\n```\n\nbecomes\n\n```rust file=output-imports.rs\nuse std::collections::HashMap;\n\nuse better_company::infra;\nuse better_company::aws::auth as aws_auth;\nuse better_company::util::iter::dedupe;\nuse better_company::shopping::cart::*;\n\ngood_company = \"good_company\";  // good_company\n```\n\n###### Assigning `TODO`s (TypeScript)\n\nPerhaps you're using a system of `TODO` notes in comments:\n\n```typescript file=todo.ts\nclass TODOApp {\n    // TODO app for writing TODO lists\n    addTodo(todo: TODO): void {\n        // TODO: everything, actually 🤷‍♀️\n    }\n}\n```\n\nand *usually* assign people to each note. It's possible to automate assigning yourself\nto every unassigned note (lucky you!) using\n\n```bash\ncat todo.ts | srgn --typescript 'comments' 'TODO(?=:)' 'TODO(@poorguy)'\n```\n\nwhich in this case gives\n\n```typescript file=output-todo.ts\nclass TODOApp {\n    // TODO app for writing TODO lists\n    addTodo(todo: TODO): void {\n        // TODO(@poorguy): everything, actually 🤷‍♀️\n    }\n}\n```\n\nNotice the [positive lookahead](https://www.regular-expressions.info/lookaround.html) of\n`(?=:)`, ensuring an actual `TODO` note is hit (`TODO:`). Otherwise, the other `TODO`s\nmentioned around the comments would be matched as well.\n\n###### Converting `print` calls to proper `logging` (Python)\n\nSay there's code making liberal use of `print`:\n\n```python file=money.py\ndef print_money():\n    \"\"\"Let's print money 💸.\"\"\"\n\n    amount = 32\n    print(\"Got here.\")\n\n    print_more = lambda s: print(f\"Printed {s}\")\n    print_more(23)  # print the stuff\n\nprint_money()\nprint(\"Done.\")\n```\n\nand a move to [`logging`](https://docs.python.org/3/library/logging.html) is desired.\nThat's fully automated by a call of\n\n```bash\ncat money.py | srgn --python 'function-calls' '^print$' 'logging.info'\n```\n\nyielding\n\n```python file=output-money.py\ndef print_money():\n    \"\"\"Let's print money 💸.\"\"\"\n\n    amount = 32\n    logging.info(\"Got here.\")\n\n    print_more = lambda s: logging.info(f\"Printed {s}\")\n    print_more(23)  # print the stuff\n\nprint_money()\nlogging.info(\"Done.\")\n```\n\n\u003e [!NOTE]\n\u003e Note the [anchors](https://www.regular-expressions.info/anchors.html): `print_more` is\n\u003e a function call as well, but `^print$` ensures it's not matched.\n\u003e\n\u003e The regular expression applies *after* grammar scoping, so operates entirely within\n\u003e the already-scoped context.\n\n###### Remove all comments (C#)\n\nOverdone, comments can turn into [smells](https://refactoring.guru/smells/comments). If\nnot tended to, they might very well start lying:\n\n```csharp file=UserService.cs\nusing System.Linq;\n\npublic class UserService\n{\n    private readonly AppDbContext _dbContext;\n\n    /// \u003csummary\u003e\n    /// Initializes a new instance of the \u003csee cref=\"FileService\"/\u003e class.\n    /// \u003c/summary\u003e\n    /// \u003cparam name=\"dbContext\"\u003eThe configuration for manipulating text.\u003c/param\u003e\n    public UserService(AppDbContext dbContext)\n    {\n        _dbContext /* the logging context */ = dbContext;\n    }\n\n    /// \u003csummary\u003e\n    /// Uploads a file to the server.\n    /// \u003c/summary\u003e\n    // Method to log users out of the system\n    public void DoWork()\n    {\n        _dbContext.Database.EnsureCreated(); // Ensure the database schema is deleted\n\n        _dbContext.Users.Add(new User /* the car */ { Name = \"Alice\" });\n\n        /* Begin reading file */\n        _dbContext.SaveChanges();\n\n        var user = _dbContext.Users.Where(/* fetch products */ u =\u003e u.Name == \"Alice\").FirstOrDefault();\n\n        /// Delete all records before proceeding\n        if (user /* the product */ != null)\n        {\n            System.Console.WriteLine($\"Found user with ID: {user.Id}\");\n        }\n    }\n}\n```\n\nSo, should you count purging comments among your fetishes, more power to you:\n\n```bash\ncat UserService.cs | srgn --csharp 'comments' -d '.*' | srgn -d '[[:blank:]]+\\n'\n```\n\nThe result is a tidy, yet taciturn:\n\n```csharp file=output-UserService.cs\nusing System.Linq;\n\npublic class UserService\n{\n    private readonly AppDbContext _dbContext;\n\n    public UserService(AppDbContext dbContext)\n    {\n        _dbContext  = dbContext;\n    }\n\n    public void DoWork()\n    {\n        _dbContext.Database.EnsureCreated();\n        _dbContext.Users.Add(new User  { Name = \"Alice\" });\n\n        _dbContext.SaveChanges();\n\n        var user = _dbContext.Users.Where( u =\u003e u.Name == \"Alice\").FirstOrDefault();\n\n        if (user  != null)\n        {\n            System.Console.WriteLine($\"Found user with ID: {user.Id}\");\n        }\n    }\n}\n```\n\nNote how all\n[different](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/tokens/comments)\n[sorts](https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/xmldoc/) of\ncomments were identified and removed. The second pass removes all leftover dangling\nlines (`[:blank:]` is [tabs and\nspaces](https://docs.rs/regex/latest/regex/#ascii-character-classes)).\n\n\u003e [!NOTE]\n\u003e When deleting (`-d`), for reasons of safety and sanity, a scope is *required*.\n\n###### Upgrade VM size (Terraform)\n\nSay you'd like to upgrade the instance size you're using:\n\n```hcl file=ec2.tf\ndata \"aws_ec2_instance_type\" \"tiny\" {\n  instance_type = \"t2.micro\"\n}\n\nresource \"aws_instance\" \"main\" {\n  ami           = \"ami-022f20bb44daf4c86\"\n  instance_type = data.aws_ec2_instance_type.tiny.instance_type\n}\n```\n\nwith\n\n```bash\ncat ec2.tf | srgn --hcl 'strings' '^t2\\.(\\w+)$' 't3.$1' | srgn --hcl 'data-names' 'tiny' 'small'\n```\n\nwill give\n\n```hcl file=output-ec2.tf\ndata \"aws_ec2_instance_type\" \"small\" {\n  instance_type = \"t3.micro\"\n}\n\nresource \"aws_instance\" \"main\" {\n  ami           = \"ami-022f20bb44daf4c86\"\n  instance_type = data.aws_ec2_instance_type.small.instance_type\n}\n```\n\n###### Rename function (C)\n\nYou can rename a function:\n\n```c file=function.c\nvoid old_function_name(void) {\n    ///\n    int variable_in_old_function_name;\n    ///\n}\n\nint main(void) {\n    old_function_name();\n}\n```\n\nusing\n\n```bash\ncat function.c | srgn --c 'function' 'old_function_name' 'new_function_name'\n```\n\nwhich will give\n\n```c file=output-function.c\nvoid new_function_name(void) {\n    ///\n    int variable_in_old_function_name;\n    ///\n}\n\nint main(void) {\n    new_function_name();\n}\n```\n\n##### Custom queries\n\nCustom queries allow you to create ad-hoc scopes. These might be useful, for example, to\ncreate small, ad-hoc, tailor-made linters, for example to catch code such as:\n\n```python file=cond.py\nif x:\n    return left\nelse:\n    return right\n```\n\nwith an invocation of\n\n```bash\ncat cond.py | srgn --python-query '(if_statement consequence: (block (return_statement (identifier))) alternative: (else_clause body: (block (return_statement (identifier))))) @cond' --fail-any # will fail\n```\n\nto hint that the code can be more idiomatically rewritten as `return left if x else\nright`. Another example, this one in Go, is ensuring sensitive fields are not\nserialized:\n\n```go file=sensitive.go\npackage main\n\ntype User struct {\n    Name     string `json:\"name\"`\n    Token string `json:\"token\"`\n}\n```\n\nwhich can be caught as:\n\n```bash\ncat sensitive.go | srgn --go-query '(field_declaration name: (field_identifier) @name tag: (raw_string_literal) @tag (#match? @name \"[tT]oken\") (#not-eq? @tag \"`json:\\\"-\\\"`\"))' --fail-any # will fail\n```\n\n##### Custom queries from file\n\nTyping out tree-sitter queries at the CLI can be unwieldy. To mitigate this you can read queries from [file](docs/python_cond_query.scm).\n\nBelow we use the same Python file from the previous section with an invocation of\n\n```bash\ncat cond.py | srgn --python-query-file 'docs/python_cond_query.scm'\n1:if x:\n2:    return left\n3:else:\n4:    return right\n```\n\n###### Ignoring parts of matches\n\nOccassionally, parts of a match need to be ignored, for example when no suitable\ntree-sitter node type is available. For example, say we'd like to replace the `error`\nwith `wrong` inside the string of the macro body:\n\n```rust file=wrong.rs\nfn wrong() {\n    let wrong = \"wrong\";\n    error!(\"This went error\");\n}\n```\n\nLet's assume there's a node type for matching *entire* macros (`macro_invocation`) and\none to match macro *names* (`((macro_invocation macro: (identifier) @name))`), but\n*none* to match macro *contents* (this is wrong, tree-sitter offers this in the form of\n`token_tree`, but let's imagine...). To match just `\"This went error\"`, the entire macro\nwould need to be matched, with the name part ignored. Any capture name starting with\n`_SRGN_IGNORE` will provide just that:\n\n```bash\ncat wrong.rs | srgn --rust-query '((macro_invocation macro: (identifier) @_SRGN_IGNORE_name) @macro)' 'error' 'wrong'\n```\n\n```rust file=output-wrong.rs\nfn wrong() {\n    let wrong = \"wrong\";\n    error!(\"This went wrong\");\n}\n```\n\nIf it weren't ignored, the result would read `wrong!(\"This went wrong\");`.\n\n###### Further reading\n\nThese matching expressions are a mouthful. A couple resources exist for getting started\nwith your own queries:\n\n- the [official docs on\n  querying](https://tree-sitter.github.io/tree-sitter/using-parsers#pattern-matching-with-queries)\n- the great [official playground](https://tree-sitter.github.io/tree-sitter/playground)\n  for interactive use, which makes developing queries a breeze. For example, the above\n  Go sample looks like:\n\n  ![tree-sitter playground go\n  example](./docs/images/tree-sitter-playground-go-example.png)\n- [*How to write a linter using tree-sitter in an\n  hour*](https://siraben.dev/2022/03/22/tree-sitter-linter.html), a great introduction\n  to the topic in general\n- the official [`tree-sitter`\n  CLI](https://github.com/tree-sitter/tree-sitter/blob/master/cli/README.md)\n- using `srgn` with high verbosity (`-vvvv`) is supposed to grant detailed insights into\n  what's happening to your input, including a [representation of the parsed\n  tree](https://docs.rs/tree-sitter/latest/tree_sitter/struct.Node.html#method.to_sexp)\n\n#### Run against multiple files\n\nUse the `--glob` option to run against multiple files, in-place. This option accepts a\n[glob pattern](https://docs.rs/glob/0.3.1/glob/struct.Pattern.html). The glob is\nprocessed *within `srgn`*: it must be quoted to prevent premature shell interpretation.\nThe `--glob` option takes precedence over the heuristics of language scoping. For\nexample,\n\n\u003c!-- markdownlint-disable MD010 --\u003e\n```console\n$ srgn --go 'comments' --glob 'tests/langs/go/fizz*.go' '\\w+'\ntests/langs/go/fizzbuzz.go\n5:// fizzBuzz prints the numbers from 1 to a specified limit.\n6:// For multiples of 3, it prints \"Fizz\" instead of the number,\n7:// for multiples of 5, it prints \"Buzz\", and for multiples of both 3 and 5,\n8:// it prints \"FizzBuzz\".\n25:\t// Run the FizzBuzz function for numbers from 1 to 100\n```\n\u003c!-- markdownlint-enable MD010 --\u003e\n\nfinds only what's matched by the (narrow) glob, even though `--go` queries by themselves\nwould match much more.\n\n`srgn` will process results fully parallel, using all available threads. For example,\n**[450k lines of Python](./benches/django/) are processed in about a second**, altering\nover 1000 lines across a couple hundred files:\n\n![hyperfine benchmarks for files option](./docs/images/files-benchmarks.png)\n\nRun the [benchmarks](./benches/bench-files.sh) too see performance for your own system.\n\n#### Explicit failure for (mis)matches\n\nAfter all scopes are applied, it might turn out no matches were found. The default\nbehavior is to silently succeed:\n\n```console\n$ echo 'Some input...' | srgn --delete '\\d'\nSome input...\n```\n\nThe output matches the specification: all digits are removed. There just happened to be\nnone. No matter how many actions are applied, **the input is returned unprocessed** once\nthis situation is detected. Hence, no unnecessary work is done.\n\nOne might prefer receiving explicit feedback (exit code other than zero) on failure:\n\n```bash\necho 'Some input...' | srgn --delete --fail-none '\\d'  # will fail\n```\n\nThe inverse scenario is also supported: **failing if anything matched**. This is useful\nfor checks (for example, in CI) against \"undesirable\" content. This works much like a\ncustom, ad-hoc linter.\n\nTake for example \"old-style\" Python code, where type hints are not yet [surfaced to the\nsyntax-level](https://docs.python.org/3/library/typing.html):\n\n```python file=oldtyping.py\ndef square(a):\n    \"\"\"Squares a number.\n\n    :param a: The number (type: int or float)\n    \"\"\"\n\n    return a**2\n```\n\nThis style can be checked against and \"forbidden\" using:\n\n```bash\ncat oldtyping.py | srgn --python 'doc-strings' --fail-any 'param.+type'  # will fail\n```\n\n#### Literal scope\n\nThis causes whatever was passed as the regex scope to be interpreted literally. Useful\nfor scopes containing lots of special characters that otherwise would need to be\nescaped:\n\n```console\n$ echo 'stuff...' | srgn -d --literal-string '.'\nstuff\n```\n\n### Help output\n\nFor reference, the full help output with all available options is given below. As with\nall other snippets, the output is validated for correctness as part of [unit\ntests](./tests/readme.rs). Checkout git tags to view help output of specific versions.\n\n```console\n$ srgn --help\nA grep-like tool which understands source code syntax and allows for manipulation in\naddition to search\n\nUsage: srgn [OPTIONS] [SCOPE] [REPLACEMENT]\n\nArguments:\n  [SCOPE]\n          Scope to apply to, as a regular expression pattern.\n          \n          If string literal mode is requested, will be interpreted as a literal\n          string.\n          \n          Actions will apply their transformations within this scope only.\n          \n          The default is the global scope, matching the entire input. Where that\n          default is meaningless or dangerous (e.g., deletion), this argument is\n          required.\n          \n          [default: .*]\n\nOptions:\n      --completions \u003cSHELL\u003e\n          Print shell completions for the given shell.\n          \n          [possible values: bash, elvish, fish, powershell, zsh]\n\n  -h, --help\n          Print help (see a summary with '-h')\n\n  -V, --version\n          Print version\n\nComposable Actions:\n  -u, --upper\n          Uppercase anything in scope.\n          \n          [env: UPPER=]\n\n  -l, --lower\n          Lowercase anything in scope.\n          \n          [env: LOWER=]\n\n  -t, --titlecase\n          Titlecase anything in scope.\n          \n          [env: TITLECASE=]\n\n  -n, --normalize\n          Normalize (Normalization Form D) anything in scope, and throw away marks.\n          \n          [env: NORMALIZE=]\n\n  -g, --german\n          Perform substitutions on German words, such as 'Abenteuergruesse' to\n          'Abenteuergrüße', for anything in scope.\n          \n          ASCII spellings for Umlauts (ae, oe, ue) and Eszett (ss) are replaced by\n          their respective native Unicode (ä, ö, ü, ß).\n          \n          Arbitrary compound words are supported.\n          \n          Words legally containing alternative spellings are not modified.\n          \n          Words require correct spelling to be detected.\n\n  -S, --symbols\n          Perform substitutions on symbols, such as '!=' to '≠', '-\u003e' to '→', on\n          anything in scope.\n          \n          Helps translate 'ASCII art' into native Unicode representations.\n\n  [REPLACEMENT]\n          Replace anything in scope with this value.\n          \n          Variables are supported: if a regex pattern was used for scoping and\n          captured content in named or numbered capture groups, access these in the\n          replacement value using `$1` etc. for numbered, `$NAME` etc. for named\n          capture groups.\n          \n          This action is specially treated as a positional argument for ergonomics and\n          compatibility with `tr`.\n          \n          If given, will run before any other action.\n          \n          [env: REPLACE=]\n\nStandalone Actions (only usable alone):\n  -d, --delete\n          Delete anything in scope.\n          \n          Cannot be used with any other action: there is no point in deleting and\n          performing any other processing. Sibling actions would either receive empty\n          input or have their work wiped.\n\n  -s, --squeeze\n          Squeeze consecutive occurrences of scope into one.\n          \n          [env: SQUEEZE=]\n          [aliases: squeeze-repeats]\n\nOptions (global):\n  -G, --glob \u003cGLOB\u003e\n          Glob of files to work on (instead of reading stdin).\n          \n          If actions are applied, they overwrite files in-place.\n          \n          For supported glob syntax, see:\n          \u003chttps://docs.rs/glob/0.3.1/glob/struct.Pattern.html\u003e\n          \n          Names of processed files are written to stdout.\n\n      --fail-no-files\n          Fail if working on files (e.g. globbing is requested) but none are found.\n          \n          Processing no files is not an error condition in itself, but might be an\n          unexpected outcome in some contexts. This flag makes the condition explicit.\n\n      --dry-run\n          Do not destructively overwrite files, instead print rich diff only.\n          \n          The diff details the names of files which would be modified, alongside all\n          changes inside those files which would be performed outside of dry running.\n          It is similar to git diff with word diffing enabled.\n\n  -i, --invert\n          Undo the effects of passed actions, where applicable.\n          \n          Requires a 1:1 mapping between replacements and original, which is currently\n          available only for:\n          \n          - symbols: '≠' \u003c-\u003e '!=' etc.\n          \n          Other actions:\n          \n          - german: inverting e.g. 'Ä' is ambiguous (can be 'Ae' or 'AE')\n          \n          - upper, lower, deletion, squeeze: inversion is impossible as information is\n            lost\n          \n          These may still be passed, but will be ignored for inversion and applied\n          normally.\n          \n          [env: INVERT=]\n\n  -L, --literal-string\n          Do not interpret the scope as a regex. Instead, interpret it as a literal\n          string. Will require a scope to be passed.\n          \n          [env: LITERAL_STRING=]\n\n      --fail-any\n          If anything at all is found to be in scope, fail.\n          \n          The default is to continue processing normally.\n\n      --fail-none\n          If nothing is found to be in scope, fail.\n          \n          The default is to return the input unchanged (without failure).\n\n  -j, --join-language-scopes\n          Join (logical 'OR') multiple language scopes, instead of intersecting them.\n          \n          The default when multiple language scopes are given is to intersect their\n          scopes, left to right. For example, `--go func --go strings` will first\n          scope down to `func` bodies, then look for strings only within those. This\n          flag instead joins (in the set logic sense) all scopes. The example would\n          then scope any `func` bodies, and any strings, anywhere. Language scopers\n          can then also be given in any order.\n          \n          No effect if only a single language scope is given. Also does not affect\n          non-language scopers (regex pattern etc.), which always intersect.\n\n  -H, --hidden\n          Do not ignore hidden files and directories.\n\n      --gitignored\n          Do not ignore `.gitignore`d files and directories.\n\n      --sorted\n          Process files in lexicographically sorted order, by file path.\n          \n          In search mode, this emits results in sorted order. Otherwise, it processes\n          files in sorted order.\n          \n          Sorted processing disables parallel processing.\n\n      --threads \u003cTHREADS\u003e\n          Number of threads to run processing on, when working with files.\n          \n          If not specified, will default to available parallelism. Set to 1 for\n          sequential, deterministic (but not sorted) output.\n\n  -v, --verbose...\n          Increase log verbosity level.\n          \n          The base log level to use is read from the `RUST_LOG` environment variable\n          (if unspecified, defaults to 'error'), and increased according to the number\n          of times this flag is given, maxing out at 'trace' verbosity.\n\nLanguage scopes:\n      --c \u003cC\u003e\n          Scope C code using a prepared query.\n          \n          [env: C=]\n\n          Possible values:\n          - comments:        Comments (single- and multi-line)\n          - strings:         Strings\n          - includes:        Includes\n          - type-def:        Type definitions\n          - enum:            `enum` definitions\n          - struct:          `struct` type definitions\n          - variable:        Variable definitions\n          - function:        All functions usages (declarations and calls)\n          - function-def:    Function definitions\n          - function-decl:   Function declaration\n          - switch:          `switch` blocks\n          - if:              `if` blocks\n          - for:             `for` blocks\n          - while:           `while` blocks\n          - do:              `do` blocks\n          - union:           `union` blocks\n          - identifier:      Identifier\n          - declaration:     Declaration\n          - call-expression: Call expression\n\n      --c-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope C code using a custom tree-sitter query.\n          \n          [env: C_QUERY=]\n\n      --c-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope C code using a custom tree-sitter query from file.\n          \n          [env: C_QUERY_FILE=]\n\n      --csharp \u003cCSHARP\u003e\n          Scope C# code using a prepared query.\n          \n          [env: CSHARP=]\n          [aliases: cs]\n\n          Possible values:\n          - comments:             Comments (including XML, inline, doc comments)\n          - strings:              Strings (incl. verbatim, interpolated; incl. quotes,\n            except for interpolated)\n          - usings:               `using` directives (including periods)\n          - struct:               `struct` definitions (in their entirety)\n          - enum:                 `enum` definitions (in their entirety)\n          - interface:            `interface` definitions (in their entirety)\n          - class:                `class` definitions (in their entirety)\n          - method:               Method definitions (in their entirety)\n          - variable-declaration: Variable declarations (in their entirety)\n          - property:             Property definitions (in their entirety)\n          - constructor:          Constructor definitions (in their entirety)\n          - destructor:           Destructor definitions (in their entirety)\n          - field:                Field definitions on types (in their entirety)\n          - attribute:            Attribute names\n          - identifier:           Identifier names\n\n      --csharp-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope C# code using a custom tree-sitter query.\n          \n          [env: CSHARP_QUERY=]\n\n      --csharp-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope C# code using a custom tree-sitter query from file.\n          \n          [env: CSHARP_QUERY_FILE=]\n\n      --go \u003cGO\u003e\n          Scope Go code using a prepared query.\n          \n          [env: GO=]\n\n          Possible values:\n          - comments:            Comments (single- and multi-line)\n          - strings:             Strings (interpreted and raw; excluding struct tags)\n          - imports:             Imports\n          - expression:          Expressions (all of them!)\n          - type-def:            Type definitions\n          - type-alias:          Type alias assignments\n          - struct:              `struct` type definitions\n          - struct~\u003cPATTERN\u003e:    Like struct, but only considers items whose name matches\n            PATTERN.\n          - interface:           `interface` type definitions\n          - interface~\u003cPATTERN\u003e: Like interface, but only considers items whose name\n            matches PATTERN.\n          - const:               `const` specifications\n          - var:                 `var` specifications\n          - func:                `func` definitions\n          - func~\u003cPATTERN\u003e:      Like func, but only considers items whose name matches\n            PATTERN.\n          - method:              Method `func` definitions (`func (recv Recv) SomeFunc()`)\n          - free-func:           Free `func` definitions (`func SomeFunc()`)\n          - init-func:           `func init()` definitions\n          - type-params:         Type parameters (generics)\n          - defer:               `defer` blocks\n          - select:              `select` blocks\n          - go:                  `go` blocks\n          - switch:              `switch` blocks\n          - labeled:             Labeled statements\n          - goto:                `goto` statements\n          - struct-tags:         Struct tags\n\n      --go-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope Go code using a custom tree-sitter query.\n          \n          [env: GO_QUERY=]\n\n      --go-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope Go code using a custom tree-sitter query from file.\n          \n          [env: GO_QUERY_FILE=]\n\n      --hcl \u003cHCL\u003e\n          Scope HashiCorp Configuration Language code using a prepared query.\n          \n          [env: HCL=]\n\n          Possible values:\n          - variable:       `variable` blocks (in their entirety)\n          - resource:       `resource` blocks (in their entirety)\n          - data:           `data` blocks (in their entirety)\n          - output:         `output` blocks (in their entirety)\n          - provider:       `provider` blocks (in their entirety)\n          - terraform:      `terraform` blocks (in their entirety)\n          - locals:         `locals` blocks (in their entirety)\n          - module:         `module` blocks (in their entirety)\n          - variables:      Variable declarations and usages\n          - resource-names: `resource` name declarations and usages\n          - resource-types: `resource` type declarations and usages\n          - data-names:     `data` name declarations and usages\n          - data-sources:   `data` source declarations and usages\n          - comments:       Comments\n          - strings:        Literal strings\n\n      --hcl-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope HashiCorp Configuration Language code using a custom tree-sitter query.\n          \n          [env: HCL_QUERY=]\n\n      --hcl-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope HashiCorp Configuration Language code using a custom tree-sitter query\n          from file.\n          \n          [env: HCL_QUERY_FILE=]\n\n      --python \u003cPYTHON\u003e\n          Scope Python code using a prepared query.\n          \n          [env: PYTHON=]\n          [aliases: py]\n\n          Possible values:\n          - comments:             Comments\n          - strings:              Strings (raw, byte, f-strings; interpolation not\n            included)\n          - imports:              Module names in imports (incl. periods; excl.\n            `import`/`from`/`as`/`*`)\n          - doc-strings:          Docstrings (not including multi-line strings)\n          - function-names:       Function names, at the definition site\n          - function-calls:       Function calls\n          - class:                Class definitions (in their entirety)\n          - def:                  Function definitions (*all* `def` block in their\n            entirety)\n          - async-def:            Async function definitions (*all* `async def` block in\n            their entirety)\n          - methods:              Function definitions inside `class` bodies\n          - class-methods:        Function definitions decorated as `classmethod` (excl.\n            the decorator)\n          - static-methods:       Function definitions decorated as `staticmethod` (excl.\n            the decorator)\n          - with:                 `with` blocks (in their entirety)\n          - try:                  `try` blocks (in their entirety)\n          - lambda:               `lambda` statements (in their entirety)\n          - globals:              Global, i.e. module-level variables\n          - variable-identifiers: Identifiers for variables (left-hand side of\n            assignments)\n          - types:                Types in type hints\n          - identifiers:          Identifiers (variable names, ...)\n\n      --python-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope Python code using a custom tree-sitter query.\n          \n          [env: PYTHON_QUERY=]\n\n      --python-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope Python code using a custom tree-sitter query from file.\n          \n          [env: PYTHON_QUERY_FILE=]\n\n      --rust \u003cRUST\u003e\n          Scope Rust code using a prepared query.\n          \n          [env: RUST=]\n          [aliases: rs]\n\n          Possible values:\n          - comments:         Comments (line and block styles; excluding doc comments;\n            comment chars incl.)\n          - doc-comments:     Doc comments (comment chars included)\n          - uses:             Use statements (paths only; excl. `use`/`as`/`*`)\n          - strings:          Strings (regular, raw, byte; includes interpolation parts in\n            format strings!)\n          - attribute:        Attributes like `#[attr]`\n          - struct:           `struct` definitions\n          - struct~\u003cPATTERN\u003e: Like struct, but only considers items whose name matches\n            PATTERN.\n          - priv-struct:      `struct` definitions not marked `pub`\n          - pub-struct:       `struct` definitions marked `pub`\n          - pub-crate-struct: `struct` definitions marked `pub(crate)`\n          - pub-self-struct:  `struct` definitions marked `pub(self)`\n          - pub-super-struct: `struct` definitions marked `pub(super)`\n          - enum:             `enum` definitions\n          - enum~\u003cPATTERN\u003e:   Like enum, but only considers items whose name matches\n            PATTERN.\n          - priv-enum:        `enum` definitions not marked `pub`\n          - pub-enum:         `enum` definitions marked `pub`\n          - pub-crate-enum:   `enum` definitions marked `pub(crate)`\n          - pub-self-enum:    `enum` definitions marked `pub(self)`\n          - pub-super-enum:   `enum` definitions marked `pub(super)`\n          - enum-variant:     Variant members of `enum` definitions\n          - fn:               Function definitions\n          - fn~\u003cPATTERN\u003e:     Like fn, but only considers items whose name matches\n            PATTERN.\n          - impl-fn:          Function definitions inside `impl` blocks (associated\n            functions/methods)\n          - priv-fn:          Function definitions not marked `pub`\n          - pub-fn:           Function definitions marked `pub`\n          - pub-crate-fn:     Function definitions marked `pub(crate)`\n          - pub-self-fn:      Function definitions marked `pub(self)`\n          - pub-super-fn:     Function definitions marked `pub(super)`\n          - const-fn:         Function definitions marked `const`\n          - async-fn:         Function definitions marked `async`\n          - unsafe-fn:        Function definitions marked `unsafe`\n          - extern-fn:        Function definitions marked `extern`\n          - test-fn:          Function definitions with attributes containing `test`\n            (`#[test]`, `#[rstest]`, ...)\n          - trait:            `trait` definitions\n          - trait~\u003cPATTERN\u003e:  Like trait, but only considers items whose name matches\n            PATTERN.\n          - impl:             `impl` blocks\n          - impl-type:        `impl` blocks for types (`impl SomeType {}`)\n          - impl-trait:       `impl` blocks for traits on types (`impl SomeTrait for\n            SomeType {}`)\n          - mod:              `mod` blocks\n          - mod~\u003cPATTERN\u003e:    Like mod, but only considers items whose name matches\n            PATTERN.\n          - mod-tests:        `mod tests` blocks\n          - type-def:         Type definitions (`struct`, `enum`, `union`)\n          - identifier:       Identifiers\n          - type-identifier:  Identifiers for types\n          - closure:          Closure definitions\n          - unsafe:           `unsafe` keyword usages (`unsafe fn`, `unsafe` blocks,\n            `unsafe Trait`, `unsafe impl Trait`)\n\n      --rust-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope Rust code using a custom tree-sitter query.\n          \n          [env: RUST_QUERY=]\n\n      --rust-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope Rust code using a custom tree-sitter query from file.\n          \n          [env: RUST_QUERY_FILE=]\n\n      --typescript \u003cTYPESCRIPT\u003e\n          Scope TypeScript code using a prepared query.\n          \n          [env: TYPESCRIPT=]\n          [aliases: ts]\n\n          Possible values:\n          - comments:       Comments\n          - strings:        Strings (literal, template)\n          - imports:        Imports (module specifiers)\n          - function:       Any `function` definitions\n          - async-function: `async function` definitions\n          - sync-function:  Non-`async function` definitions\n          - method:         Method definitions\n          - constructor:    `constructor` method definitions\n          - class:          `class` definitions\n          - enum:           `enum` definitions\n          - interface:      `interface` definitions\n          - try-catch:      `try`/`catch`/`finally` blocks\n          - var-decl:       Variable declarations (`let`, `const`, `var`)\n          - let:            `let` variable declarations\n          - const:          `const` variable declarations\n          - var:            `var` variable declarations\n          - type-params:    Type (generic) parameters\n          - type-alias:     Type alias declarations\n          - namespace:      `namespace` blocks\n          - export:         `export` blocks\n\n      --typescript-query \u003cTREE-SITTER-QUERY-VALUE\u003e\n          Scope TypeScript code using a custom tree-sitter query.\n          \n          [env: TYPESCRIPT_QUERY=]\n\n      --typescript-query-file \u003cTREE-SITTER-QUERY-FILENAME\u003e\n          Scope TypeScript code using a custom tree-sitter query from file.\n          \n          [env: TYPESCRIPT_QUERY_FILE=]\n\nOptions (german):\n      --german-prefer-original\n          When some original version and its replacement are equally legal, prefer the\n          original and do not modify.\n          \n          For example, \"Busse\" (original) and \"Buße\" (replacement) are equally legal\n          words: by default, the tool would prefer the latter.\n          \n          [env: GERMAN_PREFER_ORIGINAL=]\n\n      --german-naive\n          Always perform any possible replacement ('ae' -\u003e 'ä', 'ss' -\u003e 'ß', etc.),\n          regardless of legality of the resulting word\n          \n          Useful for names, which are otherwise not modifiable as they do not occur in\n          dictionaries. Called 'naive' as this does not perform legal checks.\n          \n          [env: GERMAN_NAIVE=]\n```\n\n## Rust library\n\nWhile this tool is CLI-first, it is library-very-close-second, and library usage is\ntreated as a first-class citizen just the same. See the [library\ndocumentation](https://docs.rs/srgn) for more, library-specific details.\n\nNote that the binary takes precedence though, which with the crate currently being both\na library *and* binary, [creates\nproblems](https://blog.axo.dev/2024/03/its-a-lib-and-a-bin). This might be fixed in the\nfuture.\n\n### Status and stats\n\n[![docs.rs](https://img.shields.io/docsrs/srgn)](https://docs.rs/srgn/)\n[![codecov](https://codecov.io/gh/alexpovel/srgn/graph/badge.svg?token=IPU7L9BWMV)](https://codecov.io/gh/alexpovel/srgn)\n[![crates](https://img.shields.io/crates/v/srgn.svg)](https://crates.io/crates/srgn)\n[![dependency status](https://deps.rs/repo/github/alexpovel/srgn/status.svg)](https://deps.rs/repo/github/alexpovel/srgn)\n[![Lines of Code](https://tokei.rs/b1/github/alexpovel/srgn?category=code)](https://github.com/XAMPPRocky/tokei#badges)\n[![Hits-of-Code](https://hitsofcode.com/github/alexpovel/srgn?branch=main)](https://hitsofcode.com/github/alexpovel/srgn/view?branch=main)\n\nNote: these apply to the entire repository, including the [binary](./src/main.rs).\n\n#### Code coverage icicle graph\n\nThe code is currently structured as (color indicates coverage):\n\n[![Code coverage icile graph](https://codecov.io/gh/alexpovel/srgn/graphs/icicle.svg?token=IPU7L9BWMV)](https://codecov.io/gh/alexpovel/srgn/graphs/icicle.svg?token=IPU7L9BWMV)\n\nHover over the rectangles for file names.\n\n## Contributing\n\nTo see how to build, refer to [compiling from source](#cargo-compile-from-source).\nOtherwise, refer to the [guidelines](CONTRIBUTING.md).\n\n## Similar tools\n\nAn unordered list of similar tools you might be interested in.\n\n- [GritQL](https://github.com/getgrit/gritql) (very similar)\n- [`ast-grep`](https://github.com/ast-grep/ast-grep) (very similar)\n- [Semgrep](https://semgrep.dev/)\n- [`pss`](https://github.com/eliben/pss)\n- [`fastmod`](https://github.com/facebookincubator/fastmod)\n- [`prefactor`](https://github.com/banga/prefactor)\n- [`grep-ast`](https://github.com/paul-gauthier/grep-ast)\n- [`amber`](https://github.com/dalance/amber)\n- [`sd`](https://github.com/chmln/sd)\n- [`ripgrep`](https://github.com/BurntSushi/ripgrep)\n- [`ripgrep-structured`](https://github.com/orf/ripgrep-structured)\n- [tree-sitter CLI](https://github.com/tree-sitter/tree-sitter/blob/master/cli/README.md)\n- [`tree-sitter-grep`](https://crates.io/crates/tree-sitter-grep)\n- [`gron`](https://github.com/tomnomnom/gron)\n- [Ruleguard](https://go-ruleguard.github.io/) (quite different, but useful for custom\n  linting)\n- [Coccinelle for Rust](https://rust-for-linux.com/coccinelle-for-rust)\n\n## Comparison with `tr`\n\n`srgn` is inspired by `tr`, and in its simplest form behaves similarly, but not\nidentically. In theory, `tr` is quite flexible. In practice, it is commonly used mainly\nacross a couple specific tasks. Next to its two positional arguments ('arrays of\ncharacters'), one finds four flags:\n\n1. `-c`, `-C`, `--complement`: complement the first array\n2. `-d`, `--delete`: delete characters in the first first array\n3. `-s`, `--squeeze-repeats`: squeeze repeats of characters in the first array\n4. `-t`, `--truncate-set1`: truncate the first array to the length of the second\n\nIn `srgn`, these are implemented as follows:\n\n1. is not available directly as an option; instead, negation of regular expression\n   classes can be used (e.g., `[^a-z]`), to much more potent, flexible and well-known\n   effect\n2. available (via regex)\n3. available (via regex)\n4. not available: it's inapplicable to regular expressions, not commonly used and, if\n   used, often misused\n\nTo show how uses of `tr` found in the wild can translate to `srgn`, consider the\nfollowing section.\n\n### Use cases and equivalences\n\nThe following sections are the approximate categories much of `tr` usage falls into.\nThey were found using [GitHub's code search](https://cs.github.com). The corresponding\nqueries are given. Results are from the first page of results at the time. The code\nsamples are links to their respective sources.\n\nAs the stdin isn't known (usually dynamic), some representative samples are used and the\ntool is exercised on those.\n\n#### Identifier Safety\n\nMaking inputs safe for use as identifiers, for example as variable names.\n\n[Query](https://github.com/search?type=code\u0026q=%22tr+-c%22)\n\n1. [`tr -C '[:alnum:]_\\n'\n   '_'`](https://github.com/grafana/grafana/blob/9328fda8ea8384e8cfcf1c78d1fe95d92bbad786/docs/make-docs#L234)\n\n    Translates to:\n\n    ```console\n    $ echo 'some-variable? 🤔' | srgn '[^[:alnum:]_\\n]' '_'\n    some_variable___\n    ```\n\n    Similar examples are:\n\n    - [`tr -C \"[:alnum:]\"\n      '-'`](https://github.com/elastic/go-elasticsearch/blob/594de0c207ef5c4804615ebedd043a789ef3ce75/.buildkite/functions/imports.sh#L38)\n    - [`tr -C \"A-Za-z0-9\"\n      \"_\"`](https://github.com/Homebrew/brew/blob/b2cf50bbe10ab996a1e3365545fadabf36df777a/Library/Homebrew/cmd/update.sh#L104)\n    - [`tr -c '[a-zA-Z0-9-]'\n      '-'`](https://github.com/xamarin/xamarin-macios/blob/c14f9ff7c7693ab060c4b84c78075ff975ea7c64/Make.config#L69)\n    - [`tr -C 'a-zA-Z0-9@_'\n      '_'`](https://github.com/openzfsonwindows/openzfs/blob/61f4ce826122f19a0a0c734efb4c2469b2aa367b/autogen.sh#L22)\n\n2. [`tr -c '[:alnum:]'\n   _`](https://github.com/freebsd/freebsd-src/blob/9dc0c983b0931f359c2ff10d47ad835ef74e929a/libexec/rc/rc.d/jail#L413)\n\n    Translates to:\n\n    ```console\n    $ echo 'some  variablê' | srgn '[^[:alnum:]]' '_'\n    some__variabl_\n    ```\n\n3. [`tr -c -s '[:alnum:]'\n   '-'`](https://github.com/weaviate/weaviate/blob/169381df70852ef687528ebf81e27869b3017403/ci/push_docker.sh#L26)\n\n    Translates to:\n\n    ```console\n    $ echo '🙂 hellö???' | srgn -s '[^[:alnum:]]' '-'\n    -hell-\n    ```\n\n#### Literal-to-literal translation\n\nTranslates a *single*, literal character to another, for example to clean newlines.\n\n[Query](https://github.com/search?q=%22%7C+tr++%22+%28path%3A*.sh+OR+path%3A*.yml+OR+path%3A*.yaml%29\u0026type=code\u0026ref=advsearch)\n\n1. [`tr  \" \"\n   \";\"`](https://github.com/facebook/react-native/blob/d31d16b19cecb893a388fcb141602e8abad4aa76/packages/react-native/sdks/hermes-engine/utils/build-hermes-xcode.sh#L32)\n\n    Translates to:\n\n    ```console\n    $ echo 'x86_64 arm64 i386' | srgn ' ' ';'\n    x86_64;arm64;i386\n    ```\n\n    Similar examples are:\n\n    - [`tr ' '\n      '-'`](https://github.com/eyedol/tools/blob/e940fe4484b486aa8d42a76d9305a9227bea7552/backup.sh#L11)\n    - [`tr '-'\n      '_'`](https://github.com/rerun/rerun/blob/aa5ad6360780ddbbb11835654e8f49b3827f15cd/modules/stubbs/lib/functions.sh#L147)\n\n2. [`tr '.'\n   \"\\n\"`](https://github.com/SDA-SE/cluster-image-scanner/blob/a769be53eae423f57a7f34c429cfa3a2770a859e/images/scan/syft/build.sh#L16):\n\n    Translates to:\n\n    ```console\n    $ echo '3.12.1' | srgn --literal-string '.' '\\n'  # Escape sequence works\n    3\n    12\n    1\n    $ echo '3.12.1' | srgn '\\.' '\\n'  # Escape regex otherwise\n    3\n    12\n    1\n    ```\n\n3. [`tr '\\n'\n   ','`](https://github.com/gtoubassi/dqn-atari/blob/513b307039f4c28b5b517cd542ad625b41f0ef50/logstats.sh#L43)\n\n    Translates to:\n\n    ```console\n    $ echo -ne 'Some\\nMulti\\nLine\\nText' | srgn --literal-string '\\n' ','\n    Some,Multi,Line,Text\n    ```\n\n    If escape sequences remain uninterpreted (`echo -E`, the default), the scope's\n    escape sequence will need to be turned into a literal `\\` and `n` as well, as it is\n    otherwise interpreted by the tool as a newline:\n\n    ```console\n    $ echo -nE 'Some\\nMulti\\nLine\\nText' | srgn --literal-string '\\\\n' ','\n    Some,Multi,Line,Text\n    ```\n\n    Similar examples are:\n\n    - [`tr '\\n' '\n      '`](https://github.com/mhassan2/splunk-n-box/blob/a721af8b8ae6103a7b274651206d4812d37db398/scripts/viz.sh#L427)\n    - [`tr \"\\n\" \"\n      \"`](https://github.com/ministryofjustice/modernisation-platform-configuration-management/blob/6a1dd9f31a62d68d796ae304165eed1fcb1b822e/ansible/roles/nomis-weblogic/tasks/patch-weblogic.yml#L13)\n\n#### Removing a character class\n\nVery useful to remove whole categories in one fell swoop.\n\n[Query](https://github.com/search?q=%22%7C+tr++%22+%28path%3A*.sh+OR+path%3A*.yml+OR+path%3A*.yaml%29\u0026type=code\u0026ref=advsearch)\n\n1. [`tr -d\n   '[:punct:]'`](https://github.com/CNMAT/OpenSoundControl.org/blob/fb7b3b48ba9ac64eae030e3333f9a980f4f8fd59/build-implementations.sh#L98)\n   which they [describe\n   as](https://github.com/CNMAT/OpenSoundControl.org/blob/fb7b3b48ba9ac64eae030e3333f9a980f4f8fd59/build-implementations.sh#L94):\n\n    \u003e Omit all punctuation characters\n\n    translates to:\n\n    ```console\n    $ echo 'Lots... of... punctuation, man.' | srgn -d '[[:punct:]]'\n    Lots of punctuation man\n    ```\n\nLots of use cases also call for **inverting**, then removing a character class.\n\n[Query](https://github.com/search?type=code\u0026q=%22tr+-c%22)\n\n1. [`tr\n    -cd a-z`](https://github.com/git/git/blob/d6c51973e4a0e889d1a426da08f52b9203fa1df2/t/lib-credential.sh#L542)\n\n    Translates to:\n\n    ```console\n    $ echo 'i RLY love LOWERCASING everything!' | srgn -d '[^[:lower:]]'\n    iloveeverything\n    ```\n\n2. [`tr -cd\n   'a-zA-Z0-9'`](https://github.com/gitlabhq/gitlabhq/blob/e74bf51e817ee50e85b1bbdc34f0443d1088fd68/doc/user/project/service_desk/configure.md?plain=1#L553)\n\n    Translates to:\n\n    ```console\n    $ echo 'All0wed ??? 💥' | srgn -d '[^[:alnum:]]'\n    All0wed\n    ```\n\n3. [`tr -cd\n   '[[:digit:]]'`](https://github.com/coredns/coredns/blob/b5e6291115d1e60fed561c64d70341b354e69504/Makefile.release#L94)\n\n   Translates to:\n\n    ```console\n    $ echo '{\"id\": 34987, \"name\": \"Harold\"}' | srgn -d '[^[:digit:]]'\n    34987\n    ```\n\n#### Remove literal character(s)\n\nIdentical to replacing them with the empty string.\n\n[Query](https://github.com/search?q=%22%7C+tr+%22\u0026type=code\u0026ref=advsearch)\n\n1. [`tr -d \".\"`](https://github.com/ohmyzsh/ohmyzsh/blob/079dbff2c4f22935a71101c511e2285327d8ab68/themes/gallois.zsh-theme#L82)\n\n    Translates to:\n\n    ```console\n    $ echo '1632485561.123456' | srgn -d '\\.'  # Unix timestamp\n    1632485561123456\n    ```\n\n    Similar examples are:\n\n    - [`tr -d '\\`'`](https://github.com/jlevy/the-art-of-command-line/blob/6b50745d2e788add2e8f1ed29010e72659a9a074/README.md?plain=1#L22)\n    - [`tr -d ' '`](https://github.com/hfg-gmuend/openmoji/blob/9782be9d240513a3d609a4bd6f1176f2d7e1b804/helpers/lib/optimize-build.sh#L77)\n    - [`tr -d ' '`](https://github.com/PaNOSC-ViNYL/SimEx/blob/0ca295ec57864c0e468eba849d3f44f992c59634/Docker/simex_devel/simex_install.sh#L44)\n2. [`tr -d '\\r\\n'`](https://github.com/kubernetes/kubernetes/blob/37cf2638c975080232990d2fc2c24d0f40c38074/cluster/gce/util.sh#L1690)\n\n    Translates to:\n\n    ```console\n    $ echo -e 'DOS-Style\\r\\n\\r\\nLines' | srgn -d '\\r\\n'\n    DOS-StyleLines\n    ```\n\n    Similar examples are:\n\n    - [`tr -d '\\r'`](https://github.com/mhassan2/splunk-n-box/blob/a721af8b8ae6103a7b274651206d4812d37db398/scripts/viz.sh#L427)\n    - [`tr -d '\\r'`](https://github.com/Cidaas/cidaas-shopware-connect-plugin/blob/519e21a9a385b26803ec442e2a8b59918a948f77/.gitlab-ci.yml#L43)\n\n#### Squeeze whitespace\n\nRemove repeated whitespace, as it often occurs when slicing and dicing text.\n\n[Query](https://github.com/search?type=code\u0026q=%22+tr+-s%22)\n\n1. [`tr -s '[:space:]'`](https://github.com/ohmyzsh/ohmyzsh/blob/bbda81fe4b338f00bbf7c7f33e6d1b12d067dc05/plugins/alias-finder/alias-finder.plugin.zsh#L26)\n\n    Translates to:\n\n    ```console\n    $ echo 'Lots   of  space !' | srgn -s '[[:space:]]'  # Single space stays\n    Lots of space !\n    ```\n\n    Similar examples are:\n\n   - [`tr -s \" \"`](https://github.com/facebookresearch/fastText/blob/166ce2c71a497ff81cb62ec151be5b569e1f1be6/.circleci/pull_data.sh#L18)\n   - [`tr -s\n     [:blank:]`](https://github.com/Atmosphere-NX/Atmosphere/blob/4fe9a89ab8ed958a3e080d7ee11767bef9cb2d57/atmosphere.mk#L11)\n     (`blank` is `\\t` and space)\n   - [`tr\n     -s`](https://github.com/kovidgoyal/kitty/blob/863adb3e8d8e7229610c6b0e6bc8d48db9becda5/kitty/rc/get_colors.py#L28)\n     (no argument: this will error out; presumably space was meant)\n   - [`tr -s ' '`](https://github.com/google/jax/blob/bf40f75bd59501d66a0e500d255daca3f9f2895e/build/rocm/build_rocm.sh#L65)\n   - [`tr -s ' '`](https://github.com/chromium/chromium/blob/ffed2601d91f4413ca4672a6027c2c05d49df815/docs/linux/minidump_to_core.md?plain=1#L111)\n   - [`tr -s '[:space:]'`](https://github.com/ceph/ceph/blob/6c387554d8e104727b3e448a1def4f1991be1ff7/src/stop.sh#L188)\n   - [`tr -s ' '`](https://github.com/PyO3/pyo3/blob/8f4a26a66ecee4cfa473ada8cb27a57c4533d04f/.netlify/build.sh#L7)\n2. [`tr -s ' '\n   '\\n'`](https://github.com/doocs/leetcode/blob/4f89e08ed45f7d5e1047767071001073ffe4d32b/solution/0100-0199/0192.Word%20Frequency/README.md?plain=1#L54)\n   (squeeze, *then replace*)\n\n    Translates to:\n\n    ```console\n    $ echo '1969-12-28    13:37:45Z' | srgn -s ' ' 'T'  # ISO8601\n    1969-12-28T13:37:45Z\n    ```\n\n3. [`tr -s '[:blank:]' ':'`](https://github.com/cockroachdb/cockroach/blob/985662236d7bf273b93a7b5e32def8e2d1043640/docs/generated/http/BUILD.bazel#L76)\n\n    Translates to:\n\n    ```console\n    $ echo -e '/usr/local/sbin \\t /usr/local/bin' | srgn -s '[[:blank:]]' ':'\n    /usr/local/sbin:/usr/local/bin\n    ```\n\n#### Changing character casing\n\nA straightforward use case. Upper- and lowercase are often used.\n\n[Query](https://github.com/search?q=%22%7C+tr++%22+%28path%3A*.sh+OR+path%3A*.yml+OR+path%3A*.yaml%29\u0026type=code\u0026ref=advsearch)\n\n1. [`tr A-Z\n   a-z`](https://github.com/golang/go/blob/a742ae493ff59a71131706500ce53f85477897f0/src/encoding/xml/xml.go#L1874)\n   (lowercasing)\n\n    Translates to:\n\n    ```console\n    $ echo 'WHY ARE WE YELLING?' | srgn --lower\n    why are we yelling?\n    ```\n\n    Notice the default scope. It can be refined to lowercase only long words, for\n    example:\n\n    ```console\n    $ echo 'WHY ARE WE YELLING?' | srgn --lower '\\b\\w{,3}\\b'\n    why are we YELLING?\n    ```\n\n    Similar examples are:\n\n    - [`tr 'A-Z' 'a-z'`](https://github.com/nwchemgit/nwchem/blob/aad4ecd5657055b085b57115314e4d56271ad749/travis/guess_simd.sh#L13)\n    - [`tr '[A-Z]' '[a-z]'`](https://github.com/XIMDEX/xcms/blob/4dd3c055de5cb0eebed28f1e9da87ed731a44a99/bin/lib/util.sh#L47)\n    - [`tr '[A-Z]' '[a-z]'`](https://github.com/varunjampani/video_prop_networks/blob/4f4a39842bd9112932abe40bad746c174a242bf6/lib/davis/configure.sh#L30)\n    - [`tr '[:upper:]' '[:lower:]'`](https://github.com/PaNOSC-ViNYL/SimEx/blob/0ca295ec57864c0e468eba849d3f44f992c59634/Docker/simex_devel/simex_install.sh#L44)\n    - [`tr \"[:upper:]\" \"[:lower:]\"`](https://github.com/tst-labs/esocial/blob/b678f59bba883a63e91be79d7f2853a57156cf7b/src/esocial-esquemas/generate-java-from-xsd.sh#L11)\n2. [`tr '[a-z]'\n   '[A-Z]'`](https://github.com/henrikpersson/potatis/blob/63feb9de28781e4e9c62bd091bd335b87b474cb1/nes-android/install.sh#L10)\n   (uppercasing)\n\n    Translates to:\n\n    ```console\n    $ echo 'why are we not yelling?' | srgn --upper\n    WHY ARE WE NOT YELLING?\n    ```\n\n    Similar examples are:\n\n    - [`tr '[a-z]' '[A-Z]'`](https://github.com/basho/riak-zabbix/blob/423e21c31821a345bf59ec4b2baba06d532a7f30/build_templates.sh#L40)\n    - [`tr \"[:lower:]\" \"[:upper:]\"`](https://github.com/Fivium/Oracle-Backup-and-Sync/blob/036aace4a8eb45ab7e6e226ddceb08f35c46b9f3/dbsync/scripts/dbsync.sh#L122)\n    - [`tr \"[:lower:]\" \"[:upper:]\"`](https://github.com/jorgeazevedo/xenomai-lab/blob/a2ce85a86f37fd9762905026ce4a1542684c714b/data/.xenomailab/blocks/template/rename.sh#L5)\n\n## License\n\nThis project is licensed under either of\n\n- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or\n  \u003chttp://www.apache.org/licenses/LICENSE-2.0\u003e)\n- MIT license ([LICENSE-MIT](LICENSE-MIT) or \u003chttp://opensource.org/licenses/MIT\u003e)\n\nat your option.\n\n[tr]: https://www.gnu.org/software/coreutils/manual/html_node/tr-invocation.html\n[^1]: Currently, reversibility is not possible for any other action. For example,\n    lowercasing is not the inverse of uppercasing. Information is lost, so it cannot be\n    undone. Structure (imagine mixed case) was lost. Something something entropy...\n[^2]: Why is such a bizzare, unrelated feature included? As usual, historical reasons.\n    The original, core version of `srgn` was merely a Rust rewrite of [a previous,\n    existing tool](https://github.com/alexpovel/betterletter), which was *only*\n    concerned with the *German* feature. `srgn` then grew from there.\n[^3]: With zero actions and no language scoping provided, `srgn` becomes 'useless', and\n    other tools such as ripgrep are much more suitable. That's why an error is emitted\n    and input is returned unchanged.\n[^4]: Combined with `--fail-any`, the invocation could be used to fail if any `unsafe`\n    code is found, like a low-budget linter. In reality, for this case, just use\n    [`#![forbid(unsafe_code)]`](https://doc.rust-lang.org/nomicon/safe-unsafe-meaning.html)\n    though.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpovel%2Fsrgn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falexpovel%2Fsrgn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falexpovel%2Fsrgn/lists"}