7
\$\begingroup\$

I often make changes on my code-base and manually using sed, awk or perl -pi -e is not always as efficient as having a simple alias: git pie.

Furthermore, because I am working on Cygwin, I cannot do real in-place replacement without a temporary file that I have to manually suppress afterwards.

Last but not least, sed, awk and perl -pi -e touch all the files that make them new from the point of view of make. I rather want to touch only the file with real changes so I wrote this Git alias:

[alias]

    # Replace
    pie = "!f() {                                            \
        git ls-files -z | xargs -0 perl -e '                 \
            my $eval = shift @ARGV;                          \
            foreach my $file (@ARGV) {                       \
                next unless -f $file;                        \
                open my $fd, \"<\", $file;                   \
                my $content = do { local $/; <$fd>};         \
                close $fd;                                   \
                my $new = do { $_ = $content; eval $eval; }; \
                if ($content ne $new) {                      \
                    open my $fd, \">\", $file;               \
                    print $fd $_;                            \
                    close $fd;                               \
                }                                            \
            }                                                \
        ' \"$1\"; }; f" 

I am sure some simplifications can be made on this piece of code, so I ask for your opinion.

The usage is:

$ git pie s/foo/bar/g

I decided to slurp the whole file in memory in order to do multiline regexes which is very convenient sometime despite the performance.

There is perhaps a missing feature: the ability to exclude or include some file extensions only or simply allow this call:

$ git ls-files | grep -P '[.][ch]$' | xargs git pie s/foo/bar/ 

I still didn't figure out how I want to do it. One possible way is:

$ git pie 's/foo/bar/ if $file =~ /\.(ch)$/'
\$\endgroup\$
  • \$\begingroup\$ Why did you put it into a shell function instead of running the git | xargs directly in the alias? \$\endgroup\$ – chicks Mar 27 '17 at 14:07
  • \$\begingroup\$ How would you put the replacement pattern in the correct place with echo 1 2 3 | xargs echo ? You need to write it as follow: f() {echo 1 2 3 | xargs echo $1;}; f. Do you have a better option? \$\endgroup\$ – nowox Mar 27 '17 at 14:12
  • 1
    \$\begingroup\$ This looks pretty cool. In fact I added to my aliases now, thanks. The only thing I would change is drop the \"<\" when opening file for reading, as that's the default mode anyway. \$\endgroup\$ – janos Apr 20 '17 at 17:49
  • 1
    \$\begingroup\$ nice, and by re-opening the file for write the original file isn't being replaced by a new file with a completely different inode as happens (on unix at least, don't know about windows) with the -i options of perl & sed etc. The only thing I'd change is to make pie a standalone perl script in your $PATH (because it could be useful standalone and because reading and editing a perl script inside a couple of levels of quoting is harder than it needs to be, and all those EOL backslashes don't help). then just run the pie script with xargs in your git alias. \$\endgroup\$ – cas Jan 13 '18 at 11:03
  • 1
    \$\begingroup\$ also, IMO the 3 form of open FH, MODE, EXPR is fine. it's a good habit to use it even when you don't need to. \$\endgroup\$ – cas Jan 13 '18 at 11:05
2
\$\begingroup\$

As written, $new will contain the return value of s///, which is 1 (true) or the empty string (false). This will (almost) never match $content and you'll be rewriting every file unconditionally.

You can inject $1 directly without the pass-and-eval. This will stop the Perl script altogether in the event of an error, which is probably desirable.

While my and lexical open etc. are good habits, they're at odds with the design constraints of a one-liner that needs to survive multiple levels of quoting. For example, print FD prints $_ to handle FD, while print $fd needs an explicit $_ argument, else it prints the string representation of that handle to stdout. Just switching to a bareword-open saves you five characters.

Perl has a mechanism to read whole files specified on command-line and you can shorten your script further by employing it.

Tying all this together, your one-liner can fit in one line with some whitespace to spare. In 128 columns (plus four spaces of indent):

[alias]
    pie = "!f(){ git ls-files -z | xargs -0 perl -0777ne'$old=$_; '\"$1\"'; $_ ne $old and open FD, q|>|, $ARGV and print FD';} ;f"
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.