Remove all non-numeric characters in a file name - macos

I have a large number of files that are named in the format: ABC_XYZ_123.jpg.
I want to rename them in bulk so that I can get the format: 123.jpg.
How can I do this on a Mac?
Thanks!

Using find in terminal:
find . -type f -name "*_*_*.jpg" -execdir bash -c 'mv "$0" "${0##*_}"' {} \;
Would result in:
ABC_XYZ_123.jpg -> 123.jpg

Because I can never remember the command lines of the stools that specialize on this kind of stuff (like rename) I create a bash script that does the task:
$ ls > x.sh
$ vi x. sh
Within VI:
:%s/^[a-z_]*\(.*\)$/mv \0 \1/`
and then:
$ source x.sh
rename can do the likes, but, as I said, I've never bothered to learn it.
$ brew install rename

Related

Shell Script for Bulk renaming of files

I want to recursively rename all files in directory path by changing their prefix.
For Example
XYZMyFile.h
XYZMyFile.m
XYZMyFile1.h
XYZMyFile1.m
XYZMyFile2.h
XYZMyFile2.m
TO
ABCMyFile.h
ABCMyFile.m
ABCMyFile1.h
ABCMyFile1.m
ABCMyFile2.h
ABCMyFile2.m
These files are under a directory structure with many layers. Can someone help me with a shell script for this bulk task?
A different approach maybe:
ls *.{h,m} | while read a; do n=ABC$(echo $a | sed -e 's/^XYZ//'); mv $a $n; done
Description:
ls *.{h,m} --> Find all files with .h or .m extension
n=ABC --> Add a ABC prefix to the file name
sed -e 's/^XYZ//' --> Removes the XYZ prefix from the file name
mv $a $n --> Performs the rename
Set globstar first and then use rename like below:
# shopt -s globstar # This will cause '**' to expand to each and everything
# ls -R
.:
nXYZ1.c nXYZ2.c nXYZ3.c subdir XYZ1.m XYZ2.m XYZ3.m
nXYZ1.h nXYZ2.h nXYZ3.h XYZ1.c XYZ2.c XYZ3.c
nXYZ1.m nXYZ2.m nXYZ3.m XYZ1.h XYZ2.h XYZ3.h
./subdir:
nXYZ1.c nXYZ1.m nXYZ2.h nXYZ3.c nXYZ3.m XYZ1.h XYZ2.c XYZ2.m XYZ3.h
nXYZ1.h nXYZ2.c nXYZ2.m nXYZ3.h XYZ1.c XYZ1.m XYZ2.h XYZ3.c XYZ3.m
# rename 's/^XYZ(.*.[mh])$/ABC$1/;s/^([^\/]*\/)XYZ(.*.[mh])$/$1ABC$2/' **
# ls -R
.:
ABC1.h ABC2.m nXYZ1.c nXYZ2.c nXYZ3.c subdir XYZ3.c
ABC1.m ABC3.h nXYZ1.h nXYZ2.h nXYZ3.h XYZ1.c
ABC2.h ABC3.m nXYZ1.m nXYZ2.m nXYZ3.m XYZ2.c
./subdir:
ABC1.h ABC2.h ABC3.h nXYZ1.c nXYZ1.m nXYZ2.h nXYZ3.c nXYZ3.m XYZ2.c
ABC1.m ABC2.m ABC3.m nXYZ1.h nXYZ2.c nXYZ2.m nXYZ3.h XYZ1.c XYZ3.c
# shopt -u globstar # Unset gobstar
This may be the simplest way to achieve your objective.
Note1 : Here I am not changing nXYZ to nABC as you have noticed. If they are meant to be changed the simplified rename command would be
rename 's/XYZ(.*.[mh])$/ABC$1/' **
Note2 : The question has mentioned nothing about multiple occurrences of XYZ. So nothing done in this regard.
Easy find and rename (the binary in /usr/bin, not the Perl function mentioned)
Yes, there is a command to do this non-recursive already.
rename XYZ ABC XYZ*
rename --help
Usage:
rename [options] expression replacement file...
Options:
-v, --verbose explain what is being done
-s, --symlink act on symlink target
-h, --help display this help and exit
-V, --version output version information and exit
For more details see rename(1).
edit: missed the "many layers of directory" part of the question, b/c it's a little messy. Adding the find.
Easiest to remember:
find . -type f -name "*.pdf" -exec rename XYZ ABC {} \;
Probably faster to finish:
find . -type d -not -path "*/\.*" -not -name ".*" -exec rename XYZ ABC {}/*.pdf \;
I'm not sure how to get easier than one command line of code.
For non-recursive, you can use rename which is a perl script:
rename -v -n 's/^.+(?=MyFile)/what-you-want/' *.{h,m}
test:
dir > ls | cat -n
1 XYZMyFile1.h
2 XYZMyFile1.m
3 XYZMyFile.h
4 XYZMyFile.m
dir >
dir > rename -v -n 's/^.+(?=MyFile)/what-you-want/' *.{h,m}
rename(XYZMyFile1.h, what-you-wantMyFile1.h)
rename(XYZMyFile1.m, what-you-wantMyFile1.m)
rename(XYZMyFile.h, what-you-wantMyFile.h)
rename(XYZMyFile.m, what-you-wantMyFile.m)
dir >
and for recursive,use find + this command
If you do not have access to rename, you can use perl directly like so:
perl -le '($old=$_) && s/^xzy/abc/g && rename($old,$_) for <*.[mh]>'
and here is a screen-shot
and with renrem, a CLI I developed using C++, specifically for renaming

Finding files using find and brace expansion

I am trying to trying to find some scripts in bash.
FOLDERS='one,two'
eval find "{$FOLDERS}/*.sh"
Of course I want to do this without eval. But removing eval simply gives:
find: {one,two}/*.sh: No such file or directory
How can I make find accept a variable set of folders using something like brace expansion, and without using a loop?
Use an array, then you can expand the array elements directly.
folders=(one two)
find "${folders[#]}" -name '*.sh'
The nice thing about this is it'll work even if the folders have whitespace, commas, or other special characters.
folders=('comma,separated,name' 'My Documents')
A working command using brace expansion would look like:
$ find {one,two} -name "*.sh"
Demonstration:
$ cd /tmp
$ mkdir {one,two}
$ touch {one,two}/{a.sh,b.sh,c.txt}
$ ls one/
a.sh b.sh c.txt
$ ls two/
a.sh b.sh c.txt
$ find {one,two} -name "*.sh"
one/b.sh
one/a.sh
two/b.sh
two/a.sh

open vi with passed file name

I usually use like this
$ find -name testname.c
./dir1/dir2/testname.c
$ vi ./dir1/dir2/testname.c
it's to annoying to type file name with location again.
how can I do this with only one step?
I've tried
$ find -name testname.c | xargs vi
but I failed.
Use the -exec parameter to find.
$ find -name testname.c -exec vi {} \;
If your find returns multiple matches though, the files will be opened sequentially. That is, when you close one, it will open the next. You won't get them all queued up in buffers.
To get them all open in buffers, use:
$ vi $(find -name testname.c)
Is this really vi, by the way, and not Vim, to which vi is often aliased nowadays?
You can do it with the following commands in bash:
Either use
vi `find -name testname.c`
Or use
vi $(!!)
if you have already typed find -name testname.c
Edit: possible duplication: bash - automatically capture output of last executed command into a variable
The problem is xargs takes over all of vi's input there (and, having no other recourse, then passes on /dev/null to vi because the alternative is passing the rest of the file list), leaving no way for you to interact with it. You probably want to use a subcommand instead:
$ vi $(find -name testname.c)
Sadly there's no simple fc or r invocation that can do this for you easily after you've run the initial find, although it's easy enough to add the characters to both ends of the command after the fact.
My favorite solution is to use vim itself:
:args `find -name testname.c`
Incidentally, VIM has extended shell globbing builtin, so you can just say
:args **/testname.c
which will find recursively in the sub directory tree.
Not also, that VIM has filename completion on the commandline, so if you know you are really looking for a single file, try
:e **/test
and then press Tab (repeatedly) to cycle between any matchin filenames in the subdirectory tree.
For something a bit more robust than vi $(find -name testname.c) and the like, the following will protect against file names with whitespace and other interpreted shell characters (if you have newlines embedded in your file names, god help you). Inject this function into your shell environment:
# Find a file (or files) by name and open with vi.
function findvi()
{
declare -a fnames=()
readarray -t fnames < <(find . -name "$1" -print)
if [ "${#fnames[#]}" -gt 0 ]; then
vi "${fnames[#]}"
fi
}
Then use like
$ findvi Classname.java

Using an alias in find -exec

I have a very long command in bash, which I do not want to type all the time, so I put an alias in my .profile
alias foo='...'
Now I want to execute this alias using find -exec
find . -exec foo '{}' \;
but find cannot find foo:
find: foo: No such file or directory
Is it possible to use an alias in find?
find itself doesn't know anything about aliases, but your shell does. If you are using a recent enough version of bash (I think 4.0 added this feature), you can use find . -exec ${BASH_ALIASES[foo]} {} \; to insert the literal content of the alias at that point in the command line.
Nope, find doesn't know anything about your aliases. Aliases are not like environment variables in that they aren't "inherited" by child processes.
You can create a shell script with the same commands, set +x permissions and have it in your path. This will work with find.
Another way of calling an alias when processing the results of find is to use something like this answer
so the following should work:
alias ll="ls -al"
find . -type d | while read folder; do ll $folder; done
I am using the ll commonly know alias for this example but you may use your alias instead, just replace ll in the following line with your alias (foo) and it should work:
find . -exec `alias ll | cut -d"'" -f2` {} \;
your case:
find . -exec `alias foo | cut -d"'" -f2` {} \;
Note it assumes your alias is quoted using the following syntax:
alias foo='your-very-long-command'
It's not possible (or difficult / error-prone) to use aliases in the find command.
An easier way to achieve the desired result is putting the contents of the alias in a shellscript and run that shellscript:
alias foo | sed "s/alias foo='//;s/'$/ \"\$#\"/" > /tmp/foo
find -exec bash /tmp/foo {} \;
The sed command removes the leading alias foo=' and replaces the trailing ' by "$#" which will contain the arguments passed to the script.
You can use the variable instead.
So instead of:
alias foo="echo test"
use:
foo="echo test"
then execute it either by command substitution or eval, for instance:
find . -type f -exec sh -c "eval $foo" \;
or:
find . -type f -exec sh -c "echo `$foo`" \;
Here is real example which is finding all non-binary files:
IS_BINARY='import sys; sys.exit(not b"\x00" in open(sys.argv[1], "rb").read())'
find . -type f -exec bash -c "python -c '$IS_BINARY' {} || echo {}" \;
I ran into the same thing and pretty much implemented skjaidev's solution.
I created a bash script called findVim.sh with the following contents:
[ roach#sepsis:~ ]$ cat findVim.sh #!/bin/bash
find . -iname $1 -exec vim '{}' \;
Then I added the the .bashrc alias as:
[ roach#sepsis:~ ]$ cat ~/.bashrc | grep fvim
alias fvim='sh ~/findVim.sh'
Finally, I reloaded .bashrc with source ~/.bashrc.
Anyways long story short I can edit arbitrary script files slightly faster with:
$ fvim foo.groovy

How do I rename the extension for a bunch of files?

In a directory, I have a bunch of *.html files. I'd like to rename them all to *.txt
How can I do that? I use the bash shell.
If using bash, there's no need for external commands like sed, basename, rename, expr, etc.
for file in *.html
do
mv "$file" "${file%.html}.txt"
done
For an better solution (with only bash functionality, as opposed to external calls), see one of the other answers.
The following would do and does not require the system to have the rename program (although you would most often have this on a system):
for file in *.html; do
mv "$file" "$(basename "$file" .html).txt"
done
EDIT: As pointed out in the comments, this does not work for filenames with spaces in them without proper quoting (now added above). When working purely on your own files that you know do not have spaces in the filenames this will work but whenever you write something that may be reused at a later time, do not skip proper quoting.
rename 's/\.html$/\.txt/' *.html
does exactly what you want.
This worked for me on OSX from .txt to .txt_bak
find . -name '*.txt' -exec sh -c 'mv "$0" "${0%.txt}.txt_bak"' {} \;
You want to use rename :
rename -S <old_extension> <new_extension> <files>
rename -S .html .txt *.html
This does exactly what you want - it will change the extension from .html to .txt for all files matching *.html.
Note: Greg Hewgill correctly points out this is not a bash builtin; and is a separate Linux command. If you just need something on Linux this should work fine; if you need something more cross-platform then take a look at one of the other answers.
On a Mac...
Install rename if you haven't: brew install rename
rename -S .html .txt *.html
For Ubuntu Users :
rename 's/\.html$/\.txt/' *.html
This is the slickest solution I've found that works on OSX and Linux, and it works nicely with git too!
find . -name "*.js" -exec bash -c 'mv "$1" "${1%.js}".tsx' - '{}' \;
and with git:
find . -name "*.js" -exec bash -c 'git mv "$1" "${1%.js}".tsx' - '{}' \;
This question explicitly mentions Bash, but if you happen to have ZSH available it is pretty simple:
zmv '(*).*' '$1.txt'
If you get zsh: command not found: zmv then simply run:
autoload -U zmv
And then try again.
Thanks to this original article for the tip about zmv.
Here is an example of the rename command:
rename -n ’s/\.htm$/\.html/’ *.htm
The -n means that it's a test run and will not actually change any files. It will show you a list of files that would be renamed if you removed the -n. In the case above, it will convert all files in the current directory from a file extension of .htm to .html.
If the output of the above test run looked ok then you could run the final version:
rename -v ’s/\.htm$/\.html/’ *.htm
The -v is optional, but it's a good idea to include it because it is the only record you will have of changes that were made by the rename command as shown in the sample output below:
$ rename -v 's/\.htm$/\.html/' *.htm
3.htm renamed as 3.html
4.htm renamed as 4.html
5.htm renamed as 5.html
The tricky part in the middle is a Perl substitution with regular expressions, highlighted below:
rename -v ’s/\.htm$/\.html/’ *.htm
One line, no loops:
ls -1 | xargs -L 1 -I {} bash -c 'mv $1 "${1%.*}.txt"' _ {}
Example:
$ ls
60acbc4d-3a75-4090-85ad-b7d027df8145.json ac8453e2-0d82-4d43-b80e-205edb754700.json
$ ls -1 | xargs -L 1 -I {} bash -c 'mv $1 "${1%.*}.txt"' _ {}
$ ls
60acbc4d-3a75-4090-85ad-b7d027df8145.txt ac8453e2-0d82-4d43-b80e-205edb754700.txt
The command mmv seems to do this task very efficiently on a huge number of files (tens of thousands in a second). For example, to rename all .xml files to .html files, use this:
mmv ";*.xml" "#1#2.html"
the ; will match the path, the * will match the filename, and these are referred to as #1 and #2 in the replacement name.
Answers based on exec or pipes were either too slow or failed on a very large number of files.
In Linux or window git bash or window's wsl, try below command to change every file's extension in current directory or sub-directories or even their sub-directories with just one line of code
find . -depth -name "*.html" -exec sh -c 'mv "$1" "${1%.html}.txt"' _ {} \;
Try this
rename .html .txt *.html
usage:
rename [find] [replace_with] [criteria]
After someone else's website crawl, I ended up with thousands of files missing the .html extension, across a wide tree of subdirectories.
To rename them all in one shot, except the files already having a .html extension (most of them had none at all), this worked for me:
cd wwwroot
find . -xtype f \! -iname *.html -exec mv -iv "{}" "{}.html" \; # batch rename files to append .html suffix IF MISSING
In the OP's case I might modify that slightly, to only rename *.txt files, like so:
find . -xtype f -iname *.txt -exec filename="{}" mv -iv ${filename%.*}.{txt,html} \;
Broken down (hammertime!):
-iname *.txt
- Means consider ONLY files already ending in .txt
mv -iv "{}.{txt,html}"
- When find passes a {} as the filename, ${filename%.*} extracts its basename without any extension to form the parameters to mv. bash takes the {txt,html} to rewrite it as two parameters so the final command runs as: mv -iv "filename.txt" "filename.html"
Fix needed though: dealing with spaces in filenames
This is a good way to modify multiple extensions at once:
for fname in *.{mp4,avi}
do
mv -v "$fname" "${fname%.???}.mkv"
done
Note: be careful at the extension size to be the same (the ???)
Rename file extensions for all files under current directory and sub directories without any other packages (only use shell script):
Create a shell script rename.sh under current directory with the following code:
#!/bin/bash
for file in $(find . -name "*$1"); do
mv "$file" "${file%$1}$2"
done
Run it by ./rename.sh .old .new.
Eg. ./rename.sh .html .txt
A bit late to the party. You could do it with xargs:
ls *.html | xargs -I {} sh -c 'mv $1 `basename $1 .html`.txt' - {}
Or if all your files are in some folder
ls folder/*.html | xargs -I {} sh -c 'mv $1 folder/`basename $1 .html`.txt' - {}
Similarly to what was suggested before, this is how I did it:
find . -name '*OldText*' -exec sh -c 'mv "$0" "${0/OldText/NewText}"' {} \;
I first validated with
find . -name '*OldText*' -exec sh -c 'echo mv "$0" "${0/OldText/NewText}"' {} \;
Nice & simple!
find . -iname *.html -exec mv {} "$(basename {} .html).text" \;
If you prefer PERL, there is a short PERL script (originally written by Larry Wall, the creator of PERL) that will do exactly what you want here:
tips.webdesign10.com/files/rename.pl.txt.
For your example the following should do the trick:
rename.pl 's/html/txt/' *.html
The easiest way is to use rename.ul it is present in most of the Linux distro
rename.ul -o -v [oldFileExtension] [newFileExtension] [expression to search for file to be applied with]
rename.ul -o -v .oldext .newext *.oldext
Options:
-o: don't overwrite preexisting .newext
-v: verbose
-n: dry run
Unfortunately it's not trivial to do portably. You probably need a bit of expr magic.
for file in *.html; do echo mv -- "$file" "$(expr "$file" : '\(.*\)\.html').txt"; done
Remove the echo once you're happy it does what you want.
Edit: basename is probably a little more readable for this particular case, although expr is more flexible in general.
Here is what i used to rename .edge files to .blade.php
for file in *.edge; do mv "$file" "$(basename "$file" .edge).blade.php"; done
Works like charm.
You can also make a function in Bash, add it to .bashrc or something and then use it wherever you want.
change-ext() {
for file in *.$1; do mv "$file" "$(basename "$file" .$1).$2"; done
}
Usage:
change-ext css scss
Source of code in function: https://stackoverflow.com/a/1224786/6732111
Here is a solution, using AWK. Make sure the files are present in the working directory. Else, cd to the directory where the html files are located and then execute the below command:
for i in $(ls | grep .html); do j=$(echo $i | grep -oh "^\w*." | awk '{print $1"txt"}'); mv $i $j; done
I wrote this code in my .bashrc
alias find-ext='read -p "Path (dot for current): " p_path; read -p "Ext (unpunctured): " p_ext1; find $p_path -type f -name "*."$p_ext1'
alias rename-ext='read -p "Path (dot for current): " p_path; read -p "Ext (unpunctured): " p_ext1; read -p "Change by ext. (unpunctured): " p_ext2; echo -en "\nFound files:\n"; find $p_path -type f -name "*.$p_ext1"; find $p_path -type f -name "*.$p_ext1" -exec sh -c '\''mv "$1" "${1%.'\''$p_ext1'\''}.'\''$p_ext2'\''" '\'' _ {} \;; echo -en "\nChanged Files:\n"; find $p_path -type f -name "*.$p_ext2";'
In a folder like "/home/<user>/example-files" having this structure:
/home/<user>/example-files:
file1.txt
file2.txt
file3.pdf
file4.csv
The commands would behave like this:
~$ find-text
Path (dot for current): example-files/
Ext (unpunctured): txt
example-files/file1.txt
example-files/file2.txt
~$ rename-text
Path (dot for current): ./example-files
Ext (unpunctured): txt
Change by ext. (unpunctured): mp3
Found files:
./example-files/file1.txt
./example-files/file1.txt
Changed Files:
./example-files/file1.mp3
./example-files/file1.mp3
~$
You could use a tool designed for renaming files in bulk, e.g. renamer.
To rename all file extensions in the current folder:
$ renamer --find ".html" --replace ".txt" --dry-run *
Many more usage examples here.

Resources