80 comments

[ 3.7 ms ] story [ 149 ms ] thread
[flagged]
I use Helix, which doesn't have find and replace built in!
(comment deleted)
[flagged]
(comment deleted)
[flagged]
(comment deleted)
[flagged]
Not affiliated, I just built a little tool to make my life easier and thought I'd share
I think it's cool. Thanks for sharing
It's great and clearly the community appreciates it! I'll put Show HN in the title since that's the convention for sharing one's projects on HN (https://news.ycombinator.com/showhn.html).

Btw, do you want to include some text giving the backstory of how you came to work on this, and explaining what's different about it? that's also the convention. If you post it in a reply to this comment, I'll move your text to the top of the thread.

(comment deleted)
Can you please not post shallow dismissals of other people's work? This is in the site guidelines: https://news.ycombinator.com/newsguidelines.html.

It's important, when people share something they've made on HN, that they don't run into this sort of bilious internet putdown.

Edit - these are other examples of the same thing (i.e. the thing we don't want in HN threads, and which we'd appreciate if you'd not do any more of):

https://news.ycombinator.com/item?id=41810426

https://news.ycombinator.com/item?id=41224056

I'm using this quickly put-together shell script called replace

    #!/usr/bin/env bash
    
    # Function to escape special characters for sed
    escape_sed_string() {
        printf '%s\n' "$1" | gsed -e 's/[]\/$*.^[]/\\&/g'
    }
    
    help() {
      gum style --foreground cyan --italic "\
    Usage (everything optional, you will be prompted):\n\
    $0\n\
      --ext .js --ext .ts\n\
      --from \"source string\"\n\
      --to \"replacement string\"\n\
      --dir somePath"
    }
    
    # Parse command line arguments
    while [[ "$#" -gt 0 ]]; do
        case $1 in
            -h)
                help
                exit 0
                ;;
            --help)
                help
                exit 0
                ;;
            --ext) EXTENSIONS+=("$2"); shift ;;
            --from) REPLACE_FROM="$2"; shift ;;
            --to) REPLACE_TO="$2"; shift ;;
            --dir) DIRECTORY="$2"; shift ;;
            *) gum style --foreground red --bold "Unknown parameter: $1"; exit 1 ;;
        esac
        shift
    done
    
    # Check for missing parameters and prompt using gum
    if [ -z "${EXTENSIONS+set}" ]; then
        EXTENSIONS=($(gum choose \
            --no-limit \
            --selected .ts,.mts,.tsx,.vue,.js,.cjs,.mjs \
            .ts .mts .tsx .vue .js .cjs .mjs .txt .md .html .json))
    fi
    
    # Exit if no extension is selected
    if [ ${#EXTENSIONS[@]} -eq 0 ]; then
        gum style --foreground red --bold " Error: No extensions selected. Exiting."
        exit 1
    fi
    
    if [ -z "${REPLACE_FROM+set}" ]; then
        REPLACE_FROM=$(gum input --placeholder "Search string:")
        if [ -z "${REPLACE_FROM}" ]; then
            echo "No replace from string, exiting"
            exit 1
        fi
    fi
    if [ -z "${REPLACE_TO+set}" ]; then
        REPLACE_TO=$(gum input --placeholder "Replace string:")
    fi
    if [ -z "${DIRECTORY+set}" ]; then
        DIRECTORY="."
    fi
    
    # Escape strings for sed
    ESCAPED_FROM=$(escape_sed_string "$REPLACE_FROM")
    ESCAPED_TO=$(escape_sed_string "$REPLACE_TO")
    
    # Run the replacement
    for ext in "${EXTENSIONS[@]}"; do
        gum style --foreground blue " Replacing ${ext} files..."
        find "$DIRECTORY" -type f -name "*$ext" ! -path "*/node_modules/*" -exec gsed -i "s/$ESCAPED_FROM/$ESCAPED_TO/g" {} \;
    done
    
    gum style --foreground green --bold " Replacement complete."
(comment deleted)
A useful feature of bash and zsh is the "edit command". The standard shortcut is "ctrl-x ctrl-e".

It opens the current command line in $EDITOR, which often defaults to vim.

(comment deleted)
That is very useful. What does it have to do with this?
If you want to search and replace a command line, there's tools to do it in your favourite editor.
Ah, so you didn't click through and actually see what this tool is, you just read the title.
I did click through, but misinterpreted what it was doing. Apologies, I'm "multitasking".
Cool.

I assumed it uses ripgrep (or the underlying walkdir) because that's the established high-performance tool for this. But apparently not.

[flagged]
Why not just ask politely instead?
Also see the excellent https://github.com/your-tools/ruplacer.

For more advanced needs, I have a custom thing called greprep that let's you make changes using your favorite editor. Workflow is like this:

  1. $ rg -n .... > /tmp/lines.txt
  2. (edit lines.txt in vscode)
  3. $ greprep /tmp/lines.txt to apply the changes
(comment deleted)
In Emacs, there is [helm-ag-edit](https://github.com/emacsorphanage/helm-ag) (but uses ripgrep if present). It's almost identical to your workflow, but all done inside the same app.

1. helm-ag <pattern> # the search results are updated as you type 2. helm-ag-edit # edit the search result as regular text. Use multi-cursors, macros, whatever. 3. helm-ag-edit-save # commits the changes to the affected files

All those commands have keybindings, so it's pretty fast. I'll often open up Emacs just to do that and then go back to my JetBrains IDE.

nice! Find and replace across a codebase is one of the few times I open an IDE.

Being able to interactively ignore instances for replacement is great!

Am I alone in initially thinking this was specifically for the fish shell because of this tool's name?
Perhaps. I as a fish user thought “oh, like `string replace`”
Very nice, it might be a good alternative when I can't use vscode remote connections.
You could also use vim in a loop. Say you want to replace "hello" in all files in the current dir with "world" and confirm every replace, then you would do:

    for f in $(grep -l 'hello' *); do vim -c ':%s/hello/world/gc | wq' "$f"; done
Or if you want to use some more vim magic, this simpler command will do the same:

    vim -c "argdo %s/hello/world/gce | update" -c "qall" *
"argdo" will do the replace command for each file, the additional e modifier in gce will ignore files that do not contain the search string and the second command "qall" is to quit vim after the work is done.
(comment deleted)
Feels like we just keep making tools that already exist in Emacs.
I dunno, seems reasonable to me that we might have nice things without requiring everyone to use emacs. (And for those who do use emacs, I guess you're ahead of the curve?)
On the other hand, it seems reasonable that we should be able to have nice things without giving up our editors. I know I’ve been spoiled by Kakoune’s cursors, but this feels like a tool that should work by spawning $EDITOR in the middle of its execution (or perhaps just having two phases and a control file). I don’t know if that’s actually possible with the current capabilities of $EDITORs (which are not Emacs). I just feel, in the darkest hour of the night which I spend reflecting on UIs, like it should be.
Nonsense, lots of people are doing text editors.
Similarly, on bash/ksh: set -o vi Ctrl-[ v (or ESC) set -o emacs Ctrl-x e
There was another comment about the difficulty in installing scooter and in the issues section, there are some requests to add more installation options.

https://github.com/thomasschafer/scooter/issues/6

Not everyone has the Rust toolchain installed on their machine. The `cargo install` installation directive needs to be discouraged.

Neat ... but I'd probably just do this by opening a "grep -l" list in nano for interactive replacement directly instead. Easy peasy.
We're losing the art of bash ``` find -type f -iname '*.go' | xargs -r -n1 sed -i 's,foo,foobar,g' ```
Something that frequently trips me up, mostly when helping colleagues, is the arguments to both find and xargs differ substantially between GNU and the FreeBSD-derived ones that ship on macOS.
Why use `xargs` instead of `-exec`? And if you do need `xargs` (for example, for parallel processing with `-P`), it is recommended to use `-print0` with `find` and `-0` with `xargs` to avoid issues due to filenames.
xargs passes many inputs to one script invocation, so even with a single thread there is often a dramatic speedup.

(and agree re -print0/-0, it's absolutely essential)

`find` can do that as well with `{} +` (at least, the `GNU` implementation and it'll automatically add more invocations if there are just way too many files).

In any case, OP was using `-n1` which means one file per invocation.

The problem is... if you use these on rare occasions it gets frustrating, because you have to read the manual or google everything or ask the llm again and again.

noone can remember these abbreviations

https://atuin.sh/ is such a huge productivity booster in these scenarios. I remember that I used find + xargs command sometime in the last 6 months on of my computers and with Atuin I can quickly find it and then slightly modify it to fit my current need.
You can save useful snippets in a file for later use.
yes, but...

noone except your shell! Fish is perfect and very very often can guess what I want using pwd, file existence, current prompt, etc...

Yeah it's great! Difficult to remember Bash pipelines are being replaced with modern tools with good UX.
I disagree, They are not difficult to remember.

you'll never build good UX and a powerful tool. You're either a master of your tool or you're not.

You get all this and more with a direct perl one liner. Without the interactivity. I'd argue that if it is a lot of files, interactivity would be a pain. Also, since the original is preserved as a .bak file, one can be fearless about trying

   #change xxx to yyy in all html
   bash> perl -pi.bak -e 's/xxx/yyy/g' *.html

   #change xxx10 (say) to yyy10 in all html
   bash> perl -pi.bak -e 's/xxx(\d+)/yyy$1/g' *.html

   # Change x4 to yyyy, where the number of y's equals to the number after x.
   bash> perl -pi.bak -e 's/x(\d+)/"y" x $1/ge' *.
The last example shows the /e operator, which evaluates an expression and uses the result as substitute, instead of a simple string.

And finally, to exclude files, one can use a subshell. For example, suppose you want to change all html, but exclude undesirable.html..

   perl -pi.bak -e 's/x/y/g'  $(ls *.html | egrep -v undesirable)