If you don't have rename (it's a really short Perl script) or your rename is the more simplistic util-linux version or you simply want to know how to make your command work: you just need to send it to the shell for execution:
find spec -name "*_test.rb" -exec sh -c 'echo mv "
(echo "$1" | sed s/test.rb\$/spec.rb/)"' _ {} \;
Answer from Dennis Williamson on serverfault.comIf you don't have rename (it's a really short Perl script) or your rename is the more simplistic util-linux version or you simply want to know how to make your command work: you just need to send it to the shell for execution:
find spec -name "*_test.rb" -exec sh -c 'echo mv "
(echo "$1" | sed s/test.rb\$/spec.rb/)"' _ {} \;
Too complicated. If you have the rename command available, you could try the following:
find . -name "*_test.rb" -print0 | xargs -0 rename "s/_test/_spec/"
This finds the relevant files, sends them to xargs which in turn uses the rename tool (which renames files according to the given perl regex).
To solve it in a way most close to the original problem would be probably using xargs "args per command line" option:
find . -name "*_test.rb" | sed -e "p;s/test/spec/" | xargs -n2 mv
It finds the files in the current working directory recursively, echoes the original file name (p) and then a modified name (s/test/spec/) and feeds it all to mv in pairs (xargs -n2). Beware that in this case the path itself shouldn't contain a string test.
This happens because sed receives the string {} as input, as can be verified with:
find . -exec echo `echo "{}" | sed 's/./foo/g'` \;
which prints foofoo for each file in the directory, recursively. The reason for this behavior is that the pipeline is executed once, by the shell, when it expands the entire command.
There is no way of quoting the sed pipeline in such a way that find will execute it for every file, since find doesn't execute commands via the shell and has no notion of pipelines or backquotes. The GNU findutils manual explains how to perform a similar task by putting the pipeline in a separate shell script:
#!/bin/sh
echo "$1" | sed 's/_test.rb$/_spec.rb/'
(There may be some perverse way of using sh -c and a ton of quotes to do all this in one command, but I'm not going to try.)
bash - Using sed to mass rename files - Stack Overflow
How to rename multiple files with sed?
shell - How can I recursively replace a string in file and directory names? - Unix & Linux Stack Exchange
sed - how to rename multiple files by replacing string in file name? this string contains a "#" - Unix & Linux Stack Exchange
Videos
You could do:
find . -name '*\[M]\[@0]\[0].dav.avi' -type f -exec sh -c '
for file do
echo mv "$file" "${file%"[M][@0][0].dav.avi"}.avi"
done' sh {} +
(remove echo if that looks all right).
Or if you want to rename all the .dav.avi to .avi, but also remove a [M][@0][0] if there:
find . -name '*.dav.avi' -type f -exec sh -c '
for file do
new=${file%.*.*}
new=${new%"[M][@0][0]"}.avi
echo mv "$file" "$new"
done' sh +
Those are POSIX syntax and also work with busybox utilities so should work on synology.
A few notes (based on observations of your own and other answers):
- the pattern passed to
find -namemust be quoted, otherwise it could be expanded by the shell. - you can't use
xargson file names unless you can guarantee that those file names won't contain blanks or newlines or quotes or backslashes. - Use
printfinstead ofechoto output arbitrary data. - Leaving a variable expansion unquoted in list context has a very special meaning. You don't want to do it here.
- The
.character is a special regular expression operator, you need to escape it in regexps if you want it to be treated literally. - The syntax to read a line with
readisIFS= read -r line, but again there's no guarantee that a file path be made of only one line. - It's often better to do
(cd -P -- "$dir" && find . ... -exec)thanfind -- "$dir" ... -execbecause it avoids problems with$dirvalues that happen to befindpredicates or start with-and because it shortens the file paths passed to-execand thus allows to pass more.
Here is a quick solution using sed to rename the files:
echo "10.01.07-10.01.48[M][@0][0].dav.avi" | sed 's/\[.*dav//'
10.01.07-10.01.48.avi
I know nothing about the Synology shell though, but I hope that sed is available!
Use (for example) in a loop:
#!/bin/sh
for file in *avi; do
newName=$(echo "$file" | sed 's/\[.*dav//')
mv -v "$file" "$newName"
done
Before:
ls
09.01.07-13.11.48[M][@0][0].dav.avi
110.01.07-13.11.48[M][@0][0].dav.avi
12.01.07-13.11.48[M][@0][0].dav.avi
Running the script:
./renameAvi.sh
โ09.01.07-13.11.48[M][@0][0].dav.aviโ -> โ09.01.07-13.11.48.aviโ
โ110.01.07-13.11.48[M][@0][0].dav.aviโ -> โ110.01.07-13.11.48.aviโ
โ12.01.07-13.11.48[M][@0][0].dav.aviโ -> โ12.01.07-13.11.48.aviโ
After:
ls
09.01.07-13.11.48.avi 12.01.07-13.11.48.avi
110.01.07-13.11.48.avi
Edit:
If you want to use find one way to implement is like in this oneliner:
find . -name "*.avi" | while read fileName; do newName=$(echo "$fileName" | sed 's/\[.*dav//'); mv -v "$fileName" "$newName"; done
โ./01.01.07-13.11.38[M][@0][0].dav.aviโ -> โ./01.01.07-13.11.38.aviโ
โ./01.01.07-13.11.39[M][@0][0].dav.aviโ -> โ./01.01.07-13.11.39.aviโ
sed --version
sed (GNU sed) 4.2.2
find --version
find (GNU findutils) 4.4.2
First, I should say that the easiest way to do this is to use the prename or rename commands.
On Ubuntu, OSX (Homebrew package rename, MacPorts package p5-file-rename), or other systems with perl rename (prename):
Copyrename s/0000/000/ F0000*
or on systems with rename from util-linux-ng, such as RHEL:
Copyrename 0000 000 F0000*
That's a lot more understandable than the equivalent sed command.
But as for understanding the sed command, the sed manpage is helpful. If you run man sed and search for & (using the / command to search), you'll find it's a special character in s/foo/bar/ replacements.
Copy s/regexp/replacement/
Attempt to match regexp against the pattern space. If successโ
ful, replace that portion matched with replacement. The
replacement may contain the special character & to refer to that
portion of the pattern space which matched, and the special
escapes \1 through \9 to refer to the corresponding matching
sub-expressions in the regexp.
Therefore, \(.\) matches the first character, which can be referenced by \1.
Then . matches the next character, which is always 0.
Then \(.*\) matches the rest of the filename, which can be referenced by \2.
The replacement string puts it all together using & (the original
filename) and \1\2 which is every part of the filename except the 2nd
character, which was a 0.
This is a pretty cryptic way to do this, IMHO. If for some reason the rename command was not available and you wanted to use sed to do the rename (or perhaps you were doing something too complex for rename?), being more explicit in your regex would make it much more readable. Perhaps something like:
Copyls F00001-0708-*|sed 's/F0000\(.*\)/mv & F000\1/' | sh
Being able to see what's actually changing in the s/search/replacement/ makes it much more readable. Also it won't keep sucking characters out of your filename if you accidentally run it twice or something.
You've had your sed explanation. Now you can use just the shell. No need for external commands.
Copyfor file in F0000*
do
echo mv "$file" "${file/#F0000/F000}"
# ${file/#F0000/F000} means replace the pattern that starts at beginning of string
done
Note that this snippet runs echo as a safety measure that prints what the mv command will do without doing it. To actually perform the mv, you need to remove echo.
Sample files as follow; I just want to rename
-tmpto.tmp
$ ls -1 *tmp file-3-tmp file-4-tmp
2. 1st attempt with sed
$ for i in *tmp; do echo $i | sed 's/-t/.t/'; done file-3.tmp file-4.tmp
Looks promising, but this is just echo, not the actual file. Same output as step 1. So I use sed -i but still didn't work
$ for i in *tmp; do sed -i 's/-t/.t/' $i; done $ ls -1 *tmp file-3-tmp file-4-tmp
Let me know the right way to do this. Thanks
You can use the rename command (see edit 1).
Solution 1
For a reasonable number of files/directory, by setting bash 4 option globstar (not works on recursive name, see edit 3):
shopt -s globstar
rename -n 's/etckeeper/userkeeper/g' **
Solution 2
For a big number of files/directories using rename and find in two steps to prevent failed rename on files in just renamed directories (see edit 2):
find . -type f -exec rename 's/etckeeper/userkeeper/g' {} \;
find . -type d -exec rename 's/etckeeper/userkeeper/g' {} \;
EDIT 1:
There are two different rename commands. This answer uses the Perl-based rename command, available in Debian-based distros. To have it on a not Debian based distro you can install from cpan or grab it around.
EDIT 2:
As pointed out by Jeff Schaller in the comments the -depth find option Process each directory's contents before the directory itself so only an "ordered" find by -depth option would be enough:
find . -depth -exec rename 's/etckeeper/userkeeper/g' {} \;
EDIT 3
Solution 1 doesn't work for recursive rename targets, (es. etckeeper/etckeeper/etckeeper) becasue outer levels are processed before inner levels and pointer to inner levels become useless. (After the first rename etckeeper/etckeeper/etckeeper will be named usrkeeper/etckeeper/etckeeper so the rename for etckeeper/etckeeper/ and etckeeper/etckeeper/etckeeper will fail). The same problem fixed in find by -depth options.
EDIT4
As pointed out in the comments by cas, I'd use {} + rather than {} \; - forking a perl script like rename multiple times (once per file/dir) is expensive.
Your original question is actually pretty easy to answer: xargs (at least on OS X) has an -I option, too, which does not require that the replacement string be a unique argument. So, the invocation simply becomes:
% find . -path '*etckeeper*' -print0 | xargs -0 -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'
Easy peasy. Now, let's rename in the correct order. As it turns out, find will print directories first (because it has to process them before it descends into them), so all we need to do is reverse the order of the filenames:
% find . -path '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(echo % | sed "s/etckeeper/usrkeeper/g" )'
Note that I've switched find -print0 | xargs -0 for find -print | xargs. Why? Because tail -r reverses based on newlines, not null characters. If you didn't switch it, tail -r would print out the same thing you piped into it! So the script is more correct now, but also it will break on filenames that contain e.g. newlines.
Note that if you don't have tail -r, you should try tac. See: How can I reverse the order of lines in a file? on Stack Overflow.
Now, the issue is that the sed command is too aggressive. For example, with a tree like:
.
โโโ etckeeper-foo
โ โโโ etckeeper-bar.md
โโโ some-other-directory
โโโ etckeeper-baz.md
You'll get mv invocations that look like:
mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./usrkeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo
The first one's fine, but the second one's clearly not - since we haven't yet done mv ./etckeeper-foo ./usrkeeper-foo!
Let's only replace in the last pathname component. We can do this by using basename and dirname:
% find . -name '*etckeeper*' -print | tail -r | xargs -n 1 -I % bash -c 'echo mv % $(dirname %)/$(basename % | sed "s/etckeeper/usrkeeper/g" )'
Note that the other change I've made is using find -name, not find -path.
This produces the correct mv invocations:
mv ./some-other-directory/etckeeper-baz.md ./some-other-directory/usrkeeper-baz.md
mv ./etckeeper-foo/etckeeper-bar.md ./etckeeper-foo/usrkeeper-bar.md
mv ./etckeeper-foo ./usrkeeper-foo
Finally. Remember, once again, that this will fail on esoteric filenames. Note also that if your filenames are particularly long, xargs will not work properly because the arguments to mv become too long. You can (at least on OS X) pass -x to xargs to tell it to immediately fail if this is the case.
To replace # by somethingelse for filenames in the current directory (not recursive) you can use the (Perl-)rename utility:
rename 's/#/somethingelse/' *
Characters like - must be escaped with a \.
For your case, you would want to use
rename 's/#U00a9/safe/g' *
Note that if you only want to operate on a certain selection of files, e.g., only *.jpg, adjust the final input to match that selection:
rename 's/#U00a9/safe/g' *.jpg
To perform a test before actually changing filenames, use the -n flag:
demo/> ls
Lucky-#U00a9NBC-125x125.jpg
Lucky-#U00a9NBC-150x150.jpg
demo/> rename -n 's/#U00a9/safe/g' *.jpg
rename(Lucky-#U00a9NBC-125x125.jpg, Lucky-safeNBC-125x125.jpg)
rename(Lucky-#U00a9NBC-150x150.jpg, Lucky-safeNBC-150x150.jpg)
For OS X, rename can be installed using homebrew: brew install rename.
This is not hard, simply make sure to escape the octothorpe (#) in the name by prepending a reverse-slash (\).
find . -type f -name 'Lucky-*' | while read FILE ; do
newfile="$(echo ${FILE} |sed -e 's/\\#U00a9/safe/')" ;
mv "${FILE}" "${newfile}" ;
done
rename 's/ACDC/AC-DC/' *.xxx
from man rename
DESCRIPTION
"rename" renames the filenames supplied according to the rule specified as the
first argument. The perlexpr argument is a Perl expression which is expected to modify the
$_ string in Perl for at least some of the filenames specified. If a given filename is not
modified by the expression, it will not be renamed. If no filenames are given on
the command line, filenames will be read via standard input.
For example, to rename all files matching "*.bak" to strip the extension, you might say
rename 's/\.bak$//' *.bak
To translate uppercase names to lower, you'd use
rename 'y/A-Z/a-z/' *
This answer contains the good parts from all other answers, while leaving out such heresy as ls | while read.
Current directory:
for file in ACDC*.xxx; do
mv "$file" "${file//ACDC/AC-DC}"
done
Including subdirectories:
find . -type f -name "ACDC*" -print0 | while read -r -d '' file; do
mv "$file" "${file//ACDC/AC-DC}"
done
Newline characters are really unlikely to be in filenames, so this can be simpler while still working with names containing spaces:
find . -type f -name "ACDC*" | while read -r file; do
mv "$file" "${file//ACDC/AC-DC}"
done
find . -type f -print0 | xargs -0 sed -i 's/href=\"1\//href=\"\/1\//g'
Per https://stackoverflow.com/a/5130044/833771, if the target directory is a Git or SVN root, you should use:
find . -not \( -name .svn -prune -o -name .git -prune \) -type f -print0 | xargs -0 sed -i 's/href=\"1\//href=\"\/1\//g'
Using Perl's rename (usable in any OS):
rename -n 's/TEST/PROD/g' ./*TEST*.XML
Remove -n switch, aka dry-run when your attempts are satisfactory to rename for real.
Which rename are you using? With
$ rename --version
rename from util-linux 2.39.3
Starting with:
$ ls -1
PAGES_TEST_SART1.XML
PAGES_TEST_SART2.XML
PAGES_TEST_SART3.XML
PAGES_TEST_SART4.XML
PAGES_TEST_SART5.XML
PAGES_TEST_SART6.XML
PAGES_TEST_SART7.XML
PAGES_TEST_SART8.XML
PAGES_TEST_SART9.XML
You can do:
$ rename TEST PROD *
and get:
$ ls -1
PAGES_PROD_SART1.XML
PAGES_PROD_SART2.XML
PAGES_PROD_SART3.XML
PAGES_PROD_SART4.XML
PAGES_PROD_SART5.XML
PAGES_PROD_SART6.XML
PAGES_PROD_SART7.XML
PAGES_PROD_SART8.XML
PAGES_PROD_SART9.XML
You can use find to find all matching files recursively:
find . -iname "*dbg*" -exec rename _dbg.txt .txt '{}' \;
EDIT: what the '{}' and \; are?
The -exec argument makes find execute rename for every matching file found. '{}' will be replaced with the path name of the file. The last token, \; is there only to mark the end of the exec expression.
All that is described nicely in the man page for find:
-exec utility [argument ...] ;
True if the program named utility returns a zero value as its
exit status. Optional arguments may be passed to the utility.
The expression must be terminated by a semicolon (``;''). If you
invoke find from a shell you may need to quote the semicolon if
the shell would otherwise treat it as a control operator. If the
string ``{}'' appears anywhere in the utility name or the argu-
ments it is replaced by the pathname of the current file.
Utility will be executed from the directory from which find was
executed. Utility and arguments are not subject to the further
expansion of shell patterns and constructs.
For renaming recursively I use the following commands:
find -iname \*.* | rename -v "s/ /-/g"
Cannot find anything long and ugly about find:
find . -regex 'search_regex' -exec rename 's/old/new/g' {} +
You can make rename recursive if that's the syntax you prefer:
shopt -s globstar
rename 's/old/new/' **/*
Bash's globstar option makes ** match 0 or more directories or files recursively, so **/* will recurse down the entire directory tree. You just need to write your search pattern as a glob instead of a regular expression. For example, to rename all files whose name contains foo, you would do:
shopt -s globstar
rename 's/old/new/' **/*foo*
If your structure only has two levels, you don't need to use recursion.
Don't parse filenames with sed. If you want to use regex and sed style syntax to rename files, use rename. If you're using Ubuntu 17.10, you need to install it
sudo apt install rename
Then you can use pa4080's answer.
With rename, use the -n option for testing.
rename -n 'expression' file(s)
You could also just use the mv command. As a one-line command:
for d in ./*/; do mv -v "$d" "${d/Edition/Volume}"; done; for f in ./*/*; do mv -v "$f" "${f/Edition/Volume}"; done
You can use echo for testing with mv, ie echo mv -v source dest, but it gives inaccurate results here, unless you test and then run the loops separately.
As a script:
#!/bin/bash
# rename directories first
for d in ./*/; do
mv -v "$d" "${d/Edition/Volume}"
done
# now rename files
for f in ./*/*; do
mv -v "$f" "${f/Edition/Volume}"
done
mv and rename recognise -- to indicate the end of options. Since all paths begin with ./ in this example, we do not need to use that, but if paths may begin with -, then use -- at the end of options, eg rename -n -- 's/foo/bar/' *, to prevent those paths being interpreted as options.
You could use the command rename two times to accomplish this task:
rename 's/Edition/Volume/' */ # rename the directories
rename 's/Edition/Volume/' */*.pdf # rename the PDF files inside
Here are two similar questions:
- Recursive bash script
- Explaining a shell script to recursively print full directory tree
Note that you're using the Perl script called rename distributed by Debian and derivatives (Ubuntu, Mint, โฆ). Other Linux distributions ship a completely different, and considerably less useful, command called rename.
y/A-Z/a-z/ translates each character in the range A through Z into the corresponding character in the range a through z, i.e. ASCII uppercase letters to the corresponding lowercase letter. To perform the opposite translation, use y/a-z/A-Z/. Another way to write the same command is rename '$_ = uc($_)' * โ uc is the uppercase function, and the rename command renames files based on the transformation made to the $_ variable.
rename 'โฆ' * only renames files in the current directory, because that's what * matches. Dot files (files whose name begins with .) are skipped, too.
If you want to rename files in the current directory and in subdirectories recursively, you can use the find command to traverse the current directory recursively. There is a difficulty here: if you call rename, this renames both the directory and the base name part. If you call rename on a directory before recursing into it (find -exec rename โฆ {} \;), find gets confused because it's found a directory but that directory no longer exists by the time it tries to descend into it. You can work around this by telling find to traverse a directory before acting on it, but then you end up attempting to rename foo/bar to FOO/BAR but the directory FOO doesn't exist.
A simple way to avoid this difficulty is to make the renaming command act only on the base name part of the path. The regular expression ([^/]*\Z) matches the final part of the path that doesn't contain a /.
find . -depth -exec rename 's!([^/]*\Z)!uc($1)!e' {} +
The shell zsh provides more convenient features for renaming โ even more cryptic than Perl, but terser and often easier to compose.
The function zmv renames files based on patterns. Run autoload -U zmv once to activate it (put this line in your .zshrc).
In the first argument to zmv (the pattern to replace), you can use zsh's powerful wildcard patterns. In the second argument to zmv (the replacement text), you can use its parameter expansion features, including history modifiers.
zmv -w '**/*' '$1$2:u'
Explanation:
-wโ automatic assign numeric variables to each wildcard pattern**/*โ all files in subdirectories, recursively (**/matches 0, 1 or more levels of subdirectories)$1โ the first numeric variable, here matching the directory part of each path$2:uโ the second numeric variable, here matching the base name part of each path, with the:umodifier to convert the value to uppercase
As an added bonus, this respects the ambient locale settings.
If you aren't sure about a zmv command you wrote, you can pass the -n option to print what the command would do and not change anything. Check the output, and if it does what you want, re-run the command without -n to actually act.
I'd like to direct anyone who's still being linked to this answer to the excellent answer Guiles Quernot gave to this question which doesn't require find.
The resulting command would be:
shopt -s globstar
rename -n 'y/a-z/A-Z/' **
But before running please read the answer linked for caveats regarding old bash versions.
Finally in case someone is wondering what does the y/// command does in perl regex. Here's a link to the relevant documentation.