Skip to content

Replace String within Multiple Files #229

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
sam-mininberg opened this issue May 6, 2025 · 7 comments
Open

Replace String within Multiple Files #229

sam-mininberg opened this issue May 6, 2025 · 7 comments

Comments

@sam-mininberg
Copy link

sam-mininberg commented May 6, 2025

Hi! I started using script recently, which I've found very useful. One pattern I've found myself using is making in-place replacements across multiple files like this:

script.ListFiles("*.txt").ExecForEach("sed -i 's/tpo/typo/g' {{.}}").Wait()

I think it would be nice to have a "ReplaceInEach" function so that I could replace all sed calls with script equivalents. For example:

script.ListFiles("*.txt").ReplaceInEach("tpo", "typo")

Please let me know what you think. Thanks!

@bitfield
Copy link
Owner

bitfield commented May 7, 2025

Thanks @sam-mininberg, great suggestion!

I can see where this would be useful, but I'm not sure how well it fits into the existing model of "everything is a pipe" that script uses. There's no output from ReplaceInEach that could be used by subsequent pipeline stages, as far as I can work out.

This might be better implemented as a custom Filter function, I think.

@sam-mininberg
Copy link
Author

Thanks for your quick reply, @bitfield!

You make a good point about the output from ReplaceInEach. Perhaps it could return the number of replacements made in each file, in the same format as Freq? That way one could verify that the expected replacements were made. A subsequent Filter could then be applied, for example, to get the list of files in which at least 1 replacement was made.

No worries if ReplaceInEach doesn't make sense for script right now. I like your idea of implementing this with Filter! I'll work on that and follow up here.

@sam-mininberg
Copy link
Author

Here's what I came up with:

// ReplaceInEach reads paths from the pipe, one per line, and replaces within
// each file all occurences of the string search with the string replace.
// Returns the paths prefixed with the number of replacements made in each.
func (p *Pipe) ReplaceInEach(search, replace string) *Pipe {
	return p.Filter(func(r io.Reader, w io.Writer) error {
		scanner := newScanner(r)
		for scanner.Scan() {
			file := scanner.Text()
			s, err := File(file).String()
			if err != nil {
				return err
			}
			count := strings.Count(s, search)
			_, err = Echo(strings.ReplaceAll(s, search, replace)).WriteFile(file)
			if err != nil {
				return err
			}
			_, err = fmt.Fprintf(w, "%d %s\n", count, file)
			if err != nil {
				return err
			}
		}
		return scanner.Err()
	})
}

Please let me know what you think, thanks!

@bitfield
Copy link
Owner

Nice! Could you use FilterLine instead to save having to write the scanner loop?

@sam-mininberg
Copy link
Author

sam-mininberg commented May 13, 2025

Thanks for the feedback @bitfield! Sure, here's my updated implementation:

// ReplaceInEach reads paths from the pipe, one per line, and replaces within
// each file all occurences of the string search with the string replace.
// Returns the paths along with the number of replacements made in each.
func (p *Pipe) ReplaceInEach(search, replace string) *Pipe {
	return p.FilterLine(func(file string) string {
		s, err := File(file).String()
		if err != nil {
			return fmt.Sprintf("%s %s", file, err)
		}
		count := strings.Count(s, search)
		_, err = Echo(strings.ReplaceAll(s, search, replace)).WriteFile(file)
		if err != nil {
			return fmt.Sprintf("%s %s", file, err)
		}
		return fmt.Sprintf("%s %d", file, count)
	})
}

Since FilterLine doesn't return an error like Filter does, I put any error that occurs in the output of the Pipe. As such, I swapped the places of the file name and the count/error (I think [file] [error] is easier to parse than [error] [file]).

Please let me know what you think. Thanks!

@bitfield
Copy link
Owner

Nice! I think this is sufficiently simple and elegant that the library itself doesn't really need a built-in ReplaceInEach function: your implementation is just fine, and people can customise it to their specific needs without being constrained to a "one size fits all" set of choices.

Would you like to produce a PR to add this example to the README?

@sam-mininberg
Copy link
Author

That sounds good! I'll start working on that.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants