2

Prepend user-defined string to all files and folders recursively using find and rename.

I’d like to prepend “x “ (no quotes) to a directory and all of its contents down through all subdirectories. I’m a beginner using macOS Mojave 10.14.6 and Terminal. I downloaded rename using Homebrew for this purpose.

Example:

/Old Project  
/Old Project/Abstract.rtf  
/Old Project/Manuscript.docx  
/Old Project/Data Analysis  
/Old Project/Data Analysis/Working Syntax.sps  
/Old Project/Data Analysis/Working Data.sav  
/Old Project/Data Analysis/Cleaned Data.sav  
/Old Project/Data Analysis/Figures  
/Old Project/Data Analysis/Figures/Figure 1.png  
/Old Project/Data Analysis/Figures/Figure 2.png   
/Old Project/Data Analysis/Raw Data  
/Old Project/Data Analysis/Raw Data/2020-06-26.csv  
/Old Project/Ethics 
/Old Project/Ethics/Application.pdf  
/Old Project/Ethics/Approval.pdf  
/Old Project/Ethics/Informed Consent.docx   

Desired Result:

/x Old Project  
/x Old Project/x Abstract.rtf  
/x Old Project/x Manuscript.docx  
/x Old Project/x Data Analysis  
/x Old Project/x Data Analysis/x Working Syntax.sps  
/x Old Project/x Data Analysis/x Working Data.sav  
/x Old Project/x Data Analysis/x Cleaned Data.sav  
/x Old Project/x Data Analysis/x Figures  
/x Old Project/x Data Analysis/x Figures/x Figure 1.png  
/x Old Project/x Data Analysis/x Figures/x Figure 2.png   
/x Old Project/x Data Analysis/x Raw Data  
/x Old Project/x Data Analysis/x Raw Data/x 2020-06-26.csv  
/x Old Project/x Ethics 
/x Old Project/x Ethics/x Application.pdf  
/x Old Project/x Ethics/x Approval.pdf  
/x Old Project/x Ethics/x Informed Consent.docx 

What I Have So Far:

find . -depth (-execdir OR -exec) rename -n ’s/^/x /‘ {} +

find . List all files and directories recursively within the current working directory. Will output a list of filenames that include the path.

-depth Directs find to start at the lowest depth (at the bottom of the subdirectories) so you don’t run into the problem whereby an un-renamed file in a renamed directory cannot be found because that path no longer exists. (How do I get this find and rename command to work with subdirectories?)

-exec Find will execute the named command (rename) on each item in the list.

-execdir Find will execute the named command (rename) on each item in the list, with one difference - it will first enter each subdirectory then pass only the filename to the rename command (no path).

rename Rename command that uses Perl regular expressions. It cannot handle recursive file renaming on its own, which is why it needs find. Apparently it is a standard command on some systems while there’s another rename command that is standard on other systems, leading to some confusion.

-n Directs rename to show what will happen and not actually run it.

’s///‘ Tells rename to do a substitution, with the first section replaced by the second section. In my syntax (’s/^/x /‘) to replace ^ (marker for the beginning of the filename) with x .

{} Directs rename to the list of files from find.

+ Tells find the command is over.


-exec versus -execdir

-exec passes along the full file path. Rename acts upon the full file path as outlined in the documentation for rename and in the answer to a similar question:

“Note that rename will then operate on the entire path, not just the filename.” (http://plasmasturm.org/code/rename/)

“Temporary note: there is something wrong - the rename pattern does not handle filenames with >path; I'm working on a fix” (https://unix.stackexchange.com/a/153489)

So, if I use -exec, then I would get "x /Old Project/Data Analysis/Figures/Figure 1.png" instead of "/Old Project/Data Analysis/Figures/x Figure 1.png", for example. To solve this, I believe I would have to write a complex regular expression to somehow capture just the filename portion as outlined in this answer to a similar question:

“If you only want to modify the last component, you can anchor your regexp at (\A|?<=/), and make sure that it doesn't match any / and only matches at the last /.” (https://unix.stackexchange.com/a/166886)

I tried the regular expression given in this answer, but it resulted in an error (“Quantier follows nothing in regex…”) and I’m not actually sure it is for my version of rename.

execdir passes along the only the file name, which is promising. In fact, when I dry run the command, all the planned changes look perfect. However, the actual result is not - it renames files and folders in the main directory but fails to find all other files and folders. It says that they do not exist.

I eventually found this answer:

“find -execdir | rename

This would be the best way to do it if it weren't for the relative path madness, as it avoids Perl regex fu to only act on the basename:

PATH="$(echo "$PATH" | sed -E 's/(^|:)[^\/][^:]*//g')" \
find a -depth -execdir rename 's/(.*)/\L$1/' '{}' \;
-execdir first cds into the directory before executing only on the basename.

Unfortunately, I can't get rid of that PATH hacking part, find -execdir refuses to do anything if you have a relative path in PATH…” (Lowercasing all directories under a directory)

So, as I understand it, the command works in theory, which is why it works in the dry run, but, in practice, find refuses to actually go into each subdirectory for the rename command.


My Questions:

  1. For using exec: Is there a way to isolate the filename from the full file path for rename?
  2. For using execdir: Is there a way to ask find to use or to get absolute path names?

Notes

I'm very new to programming.

I found this very thorough answer (https://stackoverflow.com/a/54163971/13821837) but the syntax doesn't match what works for my system.

1

1 Answer 1

2

Using tools:

First install GNU find via

brew install findutils

Then:

gfind . -depth -exec rename -n 's@(?<=/)[\s\w\.-]+$@x $&@' {} \;

With perl rename.

Remove -n switch when the output looks good.

Note

  • -depth here is very important, it traverse first files from directories before renaming directories themselves. (descending order, mandatory here).
  • the -n from renamestands for dry-run

Check regex explanations.

The replacement part x $& means a literal x + space and the last full match from the left of the substitution s///

Local test:

./Old Project
./Old Project/Manuscript.docx
./Old Project/Data Analysis
./Old Project/Data Analysis/Working Syntax.sps
./Old Project/Data Analysis/Raw data
./Old Project/Data Analysis/Working Data.sav
./Old Project/Data Analysis/Figures
./Old Project/Data Analysis/Figures/Figure 2.png
./Old Project/Data Analysis/Figures/Figure 1.png
./Old Project/Data Analysis/Raw Data
./Old Project/Data Analysis/Raw Data/2020-06-26.csv
./Old Project/Data Analysis/Cleaned Data.sav
./Old Project/Ethics
./Old Project/Ethics/Informed Consent.docx
./Old Project/Ethics/Application.pdf
./Old Project/Ethics/Approval.pdf
./Old Project/Abstract.rtf

After processing:

./x Old Project
./x Old Project/x Manuscript.docx
./x Old Project/x Data Analysis
./x Old Project/x Data Analysis/x Cleaned Data.sav
./x Old Project/x Data Analysis/x Figures
./x Old Project/x Data Analysis/x Figures/x Figure 2.png
./x Old Project/x Data Analysis/x Figures/x Figure 1.png
./x Old Project/x Data Analysis/x Raw Data
./x Old Project/x Data Analysis/x Raw Data/x 2020-06-26.csv
./x Old Project/x Data Analysis/x Raw data
./x Old Project/x Data Analysis/x Working Data.sav
./x Old Project/x Data Analysis/x Working Syntax.sps
./x Old Project/x Ethics
./x Old Project/x Ethics/x Application.pdf
./x Old Project/x Ethics/x Approval.pdf
./x Old Project/x Ethics/x Informed Consent.docx
./x Old Project/x Abstract.rtf
5
  • Don't change the rules from comments. 'Old Project' is renamed for me Jun 29, 2020 at 18:51
  • Because GNU find, by default, output the first dir like ./dir. It's required here Jun 29, 2020 at 18:54
  • My post is 100% conform from your sample input. I repeat; the leading / is required from gfind output, or ./. This is far more than a start Jun 29, 2020 at 19:26
  • When yon run gfind without rename, do you have the leading / like ./x Old Project ? This is the normal behavior of find, and the regex need the starting / Jun 29, 2020 at 19:40
  • 1
    I understand. I need to figure out how to get gfind or find to list the current directory with a leading / (i.e. ./) otherwise, the regular expression can't find it.
    – thinkblot
    Jun 29, 2020 at 20:14

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .