I have nested directories of pdf files and I would like to extract them to a higher level directory renaming them as follows:

My files are something like:


I want to achieve:


I know I can make a list of the files by doing

find . -type f -name "*.pdf"

And I can imagine a solution using

find . -type f -name "*.pdf" | mv -t ...

But I don't know how to fill in the ... because I don't understand parsing and variable assignment in bash. How does one split the path at the '/' and form a new path and file name as above?

Many thanks in advance!



find . -mindepth 2 -type f -name '*.pdf' -exec bash -c 'f=${1#./}; mv "$1" "./${f//\//_}"' None {} \;

This is safe for all file names, even ones with newlines in their names.

How it works

  • -mindepth 2

    This tells find not to process any files that are already in the current directory.

  • -type f -name '*.pdf'

    This restricts the search to regular files with the pdf extension.

  • -exec bash -c '...' None {} \;

    This runs the command in the quoted string supplying the file name as the first argument, $1.

    For our purposes, the string None is simply a placeholder. It is assigned to $0, under bash conventions, is the name of the command that we are running.

  • f=${1#./}; mv "$1" "./${f//\//_}"

    This (a) removes the prefix ./ from the file name, and (b) moves the file to the desired location with the new name.

    ${1#./} is an example of bash's prefix removal. It returns the strign $1 with ./ removed from the beginning. ${f//\//_} is an example of bash's pattern substitution. It returns the string $f with all / replaced with _. To read more about these features, see the section in man bash entitled Parameter Expansion.

More efficient version

The above version invoke bash for every file found. Alternatively, we can invoke bash just once for several files found. To do this, we wrap our command in a for loop:

find . -mindepth 2 -type f -name '*.pdf' -exec bash -c 'for f in "$@"; do f=${f#./}; echo mv "$f" "./${f//\//_}"; done' None {} +

Alternate problem

Suppose that all the files we want are in a second level directory and we want the moved files to have the order of the directory names reversed so that instead of ./path1_pathA_fileI.pdf, we end up with ./pathA_path1_fileI.pdf. In this case:

for d1 in */; do d1=${d1%/}; for d2 in "$d1"/*/; do d2=${d2%/}; p="${d2#$d1/}_$d1"; for f in "./$d2"/*.pdf; do echo mv "$f" "./${p}_${f#./$d2/}"; done; done; done

Or, for those who prefer their commands spread out over multiple lines:

for d1 in */
    for d2 in "$d1"/*/
         for f in "./$d2"/*.pdf
             echo mv "$f" "./${p}_${f#./$d2/}"
  • Thanks for the step by step breakdown! What if I wanted to also wanted to change the order of the two dir strings so that a final path would be ./pathA_path1_fileI.pdf In this case the pattern substitution ${f//\//_} doesn't work. How could this be done? – Canaryyellow Jun 29 '16 at 5:29
  • @Canaryyellow OK. I added code for that. – John1024 Jun 29 '16 at 5:55

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.