Skip to content

Print the names of affected files #703

Open

Description

@mathiasrw

This is a breaking change.

Today

When RexReplace changes files in place, it prints the name of each changed file to stderr:

> rexreplace foo bar '*.md'
a.md
b.md

The names are the result of the run, but they are treated as a side note. You can only pipe them with 2>&1, and that also includes the errors.

With --output the content of all files is printed right after each other, like cat. A file with no line break at the end is glued onto the next one, and a program can not tell where one file ends.

Change

Print the names of the changed files to stdout, one name on each line. This is how grep -l, gofmt -l and prettier --write do it.

> rexreplace foo bar '**/*.md' | xargs git add
> rexreplace foo bar '**/*.md' | wc -l
  • stderr is then only for errors, --verbose and --debug.
  • --quiet and --quiet-total stop the names from being printed.
  • With --output or --output-match stdout has the result, so the names are printed to stderr. They are the same names as without the flag.
  • Piped data is never named, as there is no file.

Delimiters

One set of delimiter flags for both the names and the content. Content is the content of each file with --output and each match with --output-match.

Flag Default What it does
--name-delimiter a line break Put after the name of each file. The default is a line break, unless the name already ends with one
--name-delimiter-tail on Also put the name delimiter after the last name. Turn it off with --no-name-delimiter-tail or --name-delimiter-tail=false
--content-delimiter a line break Put after the content of each file with --output, or after each match with --output-match. The default is a line break, unless the content already ends with one
--content-delimiter-tail on, off for piped data Also put the content delimiter after the last content
--delimiter Sets both --name-delimiter and --content-delimiter
--null The same as --delimiter '\0'
--tail Sets both --name-delimiter-tail and --content-delimiter-tail. --no-tail turns both off
--nl (-l) off What is printed to stdout and stderr ends with a line break. If it already does, nothing is added
--name-all off Name every file that is checked for a match, also the ones where nothing is changed
> rexreplace foo bar '*.md' --name-delimiter ', ' --no-name-delimiter-tail
a.md, b.md

> rexreplace foo bar '**/*.md' --null | xargs -0 git add

> rexreplace foo bar '*.md' --output --content-delimiter='---\n' 2>/dev/null
bar
---
bar
---
nothing
---

> rexreplace o _ my.file -m --content-delimiter ', ' --no-tail --nl
o, o

> printf foo | rexreplace o x --nl
fxx

Rules:

  • The most specific flag wins: the specific flag, then --delimiter, then --null, then the default. No flags conflict.
    • --delimiter ', ' --null gives , for both.
    • --null --name-delimiter ', ' gives , after the names and NUL between the content.
  • Escapes: the delimiters are treated like the replacement. Escape sequences like \t are understood, and the € and § aliases are used unless --void-euro or --void-section is set.
  • The default line break: without a given delimiter, each name and each content ends with a line break, unless it already ends with one. So --output on files that end with a line break gives the same as cat, and a file without one is never glued onto the next. A given delimiter is used as it is: --content-delimiter '\n' always puts a line break.
  • Tail: the delimiter also comes after the last item, so a while read loop or xargs -0 gets the last name too. The specific tail flags win over --tail.
  • Content is content: --output and --output-match follow the same rule. Use --content-delimiter '' to join the content.
  • Piped data is left as it is: its content tail is off by default. --nl, or a delimiter and --content-delimiter-tail, can add something after it.
  • --nl deals with stdout and stderr. That is the names, the content from --output, --output-match or piped data, and the messages. The messages always end with a line break, so only names or content can need one. It does not change the tail: the tail is printed if it is asked for, and after that a line break is added if stdout does not end with one. Output that ends with more line breaks is kept as it is. If nothing is printed, nothing is added, so empty output stays empty.
  • Short flags: only -l for --nl. -0 is left out for now; only --null is added.

The path of a name

  • By default a name is printed in the way the path was given, like today: what we get is what we print. A relative path is printed as a relative path, and a full path as a full path.

  • --absolute-path (-a): Always print the full path from /.

    > rexreplace foo bar 'docs/*.md'
    docs/a.md
    docs/b.md
    
    > rexreplace foo bar 'docs/*.md' --absolute-path
    /Users/me/project/docs/a.md
    /Users/me/project/docs/b.md
    
    > rexreplace foo bar '/tmp/notes/*.md'
    /tmp/notes/a.md
  • -a is not in use. fd has the same flag for the same thing.

  • grep -l, rg -l and find also print the path in the way it was given.

  • The two forms are already in the code of --replacement-js as file_rel and file.

  • A file given with a full path was not changed. That was a bug of its own: A file given with an absolute path is not changed #726.

What breaks

  • --output adds a line break after a file that does not end with one. Use --no-content-delimiter-tail and --content-delimiter '' to get the content exactly as it is.
  • Scripts that expect stdout to be empty when files are changed in place.
  • Scripts that read the names of the changed files from stderr.
  • Scripts that merge the two streams with --output ... 2>&1 now also get the names.

Order of the names

Files are handled asynchronously by default, so the names come in the order the files are done. This is accepted. --void-async gives the order of the file list.

Good to know

  • A value that starts with - must be given with =, like --content-delimiter='---\n'. yargs reads --content-delimiter '---\n' as an unknown flag. This is the same for all string flags.
  • We will move from yargs to argmate. --no-name-delimiter-tail and --name-delimiter-tail=false work in both. --name-delimiter-tail false (with a space) only works in yargs.
  • argmate reads -A0 as -A with the value 0. This is one reason to leave out -0.
  • For --nl, write() keeps the char code of the last character written to each stream (text.charCodeAt(text.length - 1)), and it is compared with 10 (a line break). Everything is printed with write(), also the messages, so how each stream ends is always known. It is checked once at the end of the run. Reading the last character costs nothing next to writing the text: a benchmark in Bun and Node gave no difference between slice(-1), text[text.length - 1], charCodeAt and endsWith, for 3 bytes up to 10 MB. It is the last character written, not the last content: with -o on the files a\n and an empty file, the last content is empty, but the output a\n\n already ends with a line break.
  • The JS tests capture stdout and stderr in rr() in test/js/helper.js.
  • With Bun as the runtime everything on stderr is red (Bun does this for console.error). The names will no longer be red when they are on stdout.

Reference

This was made before, on a branch that is not merged. Look at it to see how it was done. It can not be copied as it is, as it is written for older code.

Activity

  1. changed the title [-]Print the names of changed files to stdout[/-] [+]Print the names of affected files[/+] on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions