Perl from command line: substitute regex only once in a file - regex

I'm trying to replace a line in a configuration file. The problem is I only want to replace only one occurence. Part of the file looks like this. It is the gitolite default config file:
# -----------------------------------------------------------------
# suggested locations for site-local gitolite code (see cust.html)
# this one is managed directly on the server
# LOCAL_CODE => "$ENV{HOME}/local",
# or you can use this, which lets you put everything in a subdirectory
# called "local" in your gitolite-admin repo. For a SECURITY WARNING
# on this, see http://gitolite.com/gitolite/cust.html#pushcode
# LOCAL_CODE => "$rc{GL_ADMIN_BASE}/local",
# ------------------------------------------------------------------
I would like to set LOCAL_CODE to something else from the command line. I thought I might do it in perl to get pcre convenience. I'm new to perl though and can't get it working.
I found this:
perl -i.bak -p -e’s/old/new/’ filename
The problem is -p seems to have it loop over the file line by line, and so a 'o' modifier won't any have effect. However without the -p option it doesn't seem to work...

A compact way to do this is
perl -i -pe '$done ||= s/old/new/' filename

Yet another one-liner:
perl -i.bak -p -e '$i = s/old/new/ if !$i' filename

There are probably a large number of perl one liners that will do this, but here is one.
perl -i.bak -p -e '$x++ if $x==0 && s/old/new/;' filename

Related

How can I use perl to delete files matching a regex

Due to a Makefile mistake, I have some fake files in my git repo...
$ ls
=0.1.1 =4.8.0 LICENSE
=0.5.3 =5.2.0 Makefile
=0.6.1 =7.1.0 pyproject.toml
=0.6.1, all_commands.txt README_git_workflow.md
=0.8.1 CHANGES.md README.md
=1.2.0 ciscoconfparse/ requirements.txt
=1.7.0 configs/ sphinx-doc/
=2.0 CONTRIBUTING.md tests/
=2.2.0 deploy_docs.py tutorial/
=22.2.0 dev_tools/ utils/
=22.8.0 do.py
=2.7.0 examples/
$
I tried this, but it seems that there may be some more efficient means to accomplish this task...
# glob "*" will list all files globbed against "*"
foreach my $filename (grep { /\W\d+\.\d+/ } glob "*") {
my $cmd1 = "rm $filename";
`$cmd1`;
}
Question:
I want a remove command that matches against a pcre.
What is a more efficient perl solution to delete the files matching this perl regex: /\W\d+\.\d+/ (example filename: '=0.1.1')?
Fetch a wider set of files and then filter through whatever you want
my #files_to_del = grep { /^\W[0-9]+\.[0-9]+/ and not -d } glob "$dir/*";
I added an anchor (^) so that the regex can only match a string that begins with that pattern, otherwise this can blow away files other than intended. Reconsider what exactly you need.
Altogether perhaps (or see a one-liner below †)
use warnings;
use strict;
use feature 'say';
use File::Glob ':bsd_glob'; # for better glob()
use Cwd qw(cwd); # current-working-directory
my $dir = shift // cwd; # cwd by default, or from input
my $re = qr/^\W[0-9]+\.[0-9]+/;
my #files_to_del = grep { /$re/ and not -d } glob "$dir/*";
say for #files_to_del; # please inspect first
#unlink or warn "Can't unlink $_: $!" for #files_to_del;
where that * in glob might as well have some pre-selection, if suitable. In particular, if the = is a literal character (and not an indicator printed by the shell, see footnote)‡ then glob "=*" will fetch files starting with it, and then you can pass those through a grep filter.
I exclude directories, identified by -d filetest, since we are looking for files (and to not mix with some scary language about directories from unlink, thanks to brian d foy comment).
If you'd need to scan subdirectories and do the same with them, perhaps recursively -- what doesn't seem to be the case here? -- then we could employ this logic in File::Find::find (or File::Find::Rule, or yet others).
Or read the directory any other way (opendir+readdir, libraries like Path::Tiny), and filter.
† Or, a quick one-liner ... print (to inspect) what's about to get blown away
perl -wE'say for grep { /^\W[0-9]+\.[0-9]+/ and not -d } glob "*"'
and then delete 'em
perl -wE'unlink or warn "$_: $!" for grep /^\W[0-9]+\.[0-9]+/ && !-d, glob "*"'
(I switched to a more compact syntax just so. Not necessary)
If you'd like to be able to pass a directory to it (optionally, or work in the current one) then do
perl -wE'$d = shift//q(.); ...' dirpath (relative path fine. optional)
and then use glob "$d/*" in the code. This works the same way as in the script above -- shift pulls the first element from #ARGV, if anything was passed to the script on the command line, or if #ARGV is empty it returns undef and then // (defined-or) operator picks up the string q(.).
‡ That leading = may be an "indicator" of a file type if ls has been aliased with ls -F, what can be checked by running ls with suppressed aliases, one way being \ls (or check alias ls).
If that is so, the = stands for it being a socket, what in Perl can be tested for by the -S filetest.
Then that \W in the proposed regex may need to be changed to \W? to allow for no non-word characters preceding a digit, along with a test for a socket. Like
my $re = qr/^\W? [0-9]+ \. [0-9]+/x;
my #files_to_del = grep { /$re/ and -S } glob "$dir/*";
Why not just:
$ rm =*
Sometimes, shell commands are the best option.
In these cases, I use perl to merely filter the list of files:
ls | perl -ne 'print if /\A\W\d+\.\d+/a' | xargs rm
And, when I do that, I feel guilty for not doing something simpler with an extended pattern in grep:
ls | grep -E '^\W\d+\.\d+' | xargs rm
Eventually I'll run into a problem where there's a directory so I need to be more careful about the file list:
find . -type f -maxdepth 1 | grep -E '^\./\W\d+\.\d+' | xargs rm
Or I need to allow rm to remove directories too should I want that:
ls | grep -E '^\W\d+\.\d+' | xargs rm -r
Here you go.
unlink( grep { /\W\d+\.\d+/ && !-d } glob( "*" ) );
This matches the filename, and excludes directories.
To delete filenames matching this: /\W\d+\.\d+/ pcre, use the following one-liners...
1> $fn is a filename... I'm also removing the my keywords since the one-liner doesn't have to worry about perl lexical scopes:
perl -e 'foreach $fn (grep { /\W\d+\.\d+/ } glob "*") {$cmd1="rm $fn";`$cmd1`;}'
2> Or as Andy Lester responded, perhaps his answer is as efficient as we can make it...
perl -e 'unlink(grep { /\W\d+\.\d+/ } glob "*");'

Execute module-qualified function inside perl regex substitution?

I have the following perl one-liner to convert /path/to/file.txt to /path/to/
echo "/path/to/file.txt" | perl -pe 's{(.*)}{File::Basename->dirname($1)}ge'
but I'm missing something in my invocation of File::Basename->dirname(), causing the following error:
Can't locate object method "dirname" via package "File::Basename" (perhaps you forgot to load "File::Basename"?) at -e line 1, <> line 1.
What am I missing?
(I know I can just use dirname from bash but I'm trying to do something more complicated with perl than what this stripped down example shows).
Error #1:
Like the message suggests (perhaps you forgot to load "File::Basename"?), you need to load File::Basename.
perl -pe'use File::Basename; ...'
or
perl -MFile::Basename -pe'...'
Error #2:
dirname is not a method, so File::Basename->dirname is incorrect. It needs to be called as File::Basename::dirname.
perl -MFile::Basename -pe's{(.*)}{File::Basename::dirname($1)}ge'
You could also import dirname.
perl -MFile::Basename=dirname -pe's{(.*)}{dirname($1)}ge'
Fortunately, File::Basename exports dirname by default, so you can simply use
perl -MFile::Basename -pe's{(.*)}{dirname($1)}ge'
Load a module with -MModName=func
perl -MFile::Basename=dirname -pe 's{(.*)}{dirname($1)}ge'
The File::Basename module exports all its functions by default so you don't need =dirname above. But this varies between modules, and mostly you do need to import symbols. For more on how to do that in a one-liner, find the -M switch in Command Switches in perlrun.
The problem was that you had <button> in the replacement string, but you've removed that now so it should work
The replacement string must be a valid Perl expression if you're using the /e modifier, and
<button>File::Basename->dirname($1)
isn't valid Perl
The correct command would be:
echo "/path/to/file.txt" | perl -pe 'use File::Basename 'dirname'; s{([^\n]+)}{dirname($1)}ge'

Regex match for file and rename + overwrite old file

Im trying to make a bash script to rename some files wich match my regex, if they match i want to rename them using the regex and overwrite an old existing file.
I want to do this because on computer 1 i have a file, on computer 2 i change the file. Later i go back to computer 1 and it gives an example conflict so it saves them both.
Example file:
acl_cam.MYI
Example file after conflict:
acl_cam (Example conflit with .... on 2015-08-20).MYI
I tried a lot of thinks like rename, mv and couple other scripts but it didn't work.
the regex i should use in my opinion:
(.*)/s\(.*\)\.(.*)
then rename it to value1 . value2 and replace the old file (acl_cam.MYI) and do this for all files/directories from where it started
can you guys help me with this one?
The issue you have, if I understand your question correctly, is two part. (1) What is the correct regex that will match the error string and produce a filename?; and (2) how to use the returned filename to move/remove the offending file?
If the sting at issue is:
acl_cam (Example conflit with .... on 2015-08-20).MYI
and you need to return the MySQL file name, then a regex similar to the following will work:
[ ][(].*[)]
The stream editor sed is about as good as anything else to return the filename from your string. Example:
$ printf "acl_cam (Example conflit with .... on 2015-08-20).MYI\n" | \
sed -e 's/[ ][(].*[)]//'
acl_cam.MYI
(shown with line continuation above)
Then it is up to you how you move or delete the file. The remaining question is where is the information (the error string) currently stored and how do you have access to it? If you have a file full of these errors, then you could do something like the following:
while read -r line; do
victim=$( printf "%s\n" "$line" | sed -e 's/[ ][(].*[)]//' )
## to move the file to /path/to/old
[ -e "$victim" ] && mv "$victim" /path/to/old
done <$myerrorfilename
(you could also feed the string to sed as a here-string, but omitted for simplicity)
You could also just delete the file if that suits your purpose. However, more information is needed to clarify how/where that information is stored and what exactly you want to do with it to provide any more specifics. Let me know if you have further questions.
Final solution for this question for people who are interested:
for i in *; do
#Wildcar check if current file containt (Exemplaar
if [[ $i == *"(Exemplaar"* ]]
then
#Rename the file to the original name (without Exemplaar conflict)
NewFileName=$(echo "$i" | sed -E -e 's/[ ][(].*[)]//')
#Remove the original file
rm $NewFileName;
#Copy the conflict file as the original file name
cp -a "$i" $NewFileName;
#Delete the conflict file
rm "$i";
echo "Removed file: $NewFileName with: $i";
fi
done
I used this code to replace my database conflict files created by dropbox sync with different computers.

Need Regex For Enter mark content

The below content having enter mark i want capture that in regex please help.
I try \s* and \t* and \r and \n ..etc, but cant get.
dslkjflkds
lksdfds
slkdjlkds
sdlkdslkjhgfsd
slkdfjhldsjfds
slkjdsjhlsd
sldfldsjf
sldfjhldsjhflds
If I understand you correctly, you want to clear empty lines... If so,
I copied your text and tried to find more than one enter marks.
with this
(\n){2,}
works for me. Hope this helps
There are many ways that you can do this in Perl. One fairly simply way, it from the command line, using the -n, -i, and -e flags described in perlrun:
perl -ni -e 'print unless /^$/' <filename>
-n assumes this while loop around your statement:
while (<>) {
# statements passed to -e go here
}
-e executes a statement
-i instructs that you want the operation to be performed in-place (modifying the original file). You can specify -i.bak if you wish to preserve the original file in a .bak file.
Is this what you expect?
$ perl -p00e';' file.txt
dslkjflkds
lksdfds
slkdjlkds
sdlkdslkjhgfsd
slkdfjhldsjfds
slkjdsjhlsd
sldfldsjf
sldfjhldsjhflds

Uncommenting a config line based on regex with string replace

I have a config file with a bunch of URLs for repos that are commented out. I need to uncomment a specific one and thought sed would make it easy to match a regex then doing a string replace on that line.
I was wondering if my regex in correct for sed syntax or if the sed command is not correct?
mirrorRegex="^# http.*vendor.*distroARCH-1.1\/"
sed '/$mirrorRegex/s/# //' /etc/repos
Before:
# ftp://mirrors.example.com/distro/distroARCH-1.1/
# http://mirrors.example.com/distro/distroARCH-1.1/
# ftp://packages.vendor.org/distro/distro/distroARCH-1.1/
# http://packages.vendor.org/distro/distro/distroARCH-1.1/
# http://mirror.school.edu/pub/distro/distroARCH-1.1/
# http://system.site3.com/distroARCH-1.1/
After: What is expected.
# ftp://mirrors.example.com/distro/distroARCH-1.1/
# http://mirrors.example.com/distro/distroARCH-1.1/
# ftp://packages.vendor.org/distro/distro/distroARCH-1.1/
http://packages.vendor.org/distro/distro/distroARCH-1.1/
# http://mirror.school.edu/pub/distro/distroARCH-1.1/
# http://system.site3.com/distroARCH-1.1/
You need to use double quotes in order to expand shell variables:
sed "/$mirrorRegex/s/# //"
You can use awk like this to do the same:
awk '$0~var {sub(/^# /,x)}1' var="$mirrorRegex" file
sed 's|^#[[:blank:]]*\(http.*vendor.*distroARCH-1.1/.*\)|\1|' YourFile
use of other separator than default / (| in this case) will help
and with variable version
Content='http.*vendor.*distroARCH-1.1/'
sed "s|^#[[:blank:]]*\(${Content}.*\)|\1|" YourFile