1

I have a collection of directories that all contain a particular file that is named the same in all the directories.

dir1/dirA/file.txt
dir1/dirB/file.txt
dir1/dirC/file.txt
....
dir4/dirX/file.txt
dir4/dirY/file.txt
dir4/dirZ/file.txt

I want to copy these files into another directory, such that each file's name is its parent directory's name:

all_files/
    dirA.txt
    dirB.txt
    dirC.txt
    ....
    dirX.txt
    dirY.txt
    dirZ.txt

While I understand these examples could cause overlap, it's not an issue for me -- I know all the target directories have unique names.

I'm using bash in OS X. I tried doing several things like this (see here and here). My thought would be to split the string on the slashes, get the second to the last one, and use that, but I couldn't get it to work (based on this and others):

for name in `ls */*/file`
    directory=${DIRS[${#DIRS[@]} - 2]}
    mv name ../all_files/${directory}.txt
done

However, I couldn't get it to work.

1
  • Had to use this again today... thanks former self!
    – dantiston
    Dec 19, 2016 at 23:23

2 Answers 2

3
for name in ./*/*/file.txt
do
    mv "$name" "../all_files/$(basename -- "$(dirname -- "$name")").txt"
done

Some comments:

  • Never do this or anything like this:

    for name in `ls */*/file`
    

    If the file names have spaces or other difficult characters, the above will fail. Instead use:

    for name in ./*/*/file
    

    The shell will handle the above correctly no matter how strange the file name is.

  • This suffers from multiple issues:

    mv name ../all_files/${directory}.txt
    

    Most importantly, this will move file literally named name. You want instead to move the file that the name variable refers to. For this use "$name" rather than name. Note the use of double-quotes here. That protects name from word splitting so that this will work even if the file name has spaces or other difficult characters.

    Similarly, the destination location should have double quotes around it.

1

I have thought about this a bit and figured out two solutions. The first uses basename and dirname:

for name in */*/file
    do cp $name ../all_files/$(basename -- "$(dirname -- "$name")")
done

This one uses string splitting (as I originally wanted to do)

for name in */*/choices
    do parts=(${name//\// })
    directory=${parts[${#parts[@]} - 2]}
    cp $name all_files/${directory}.txt
done

Hope this helps someone!

2
  • +1 for using -- after basename and dirname. That feature is not documented on my (linux) system but I see that it works.
    – John1024
    Sep 10, 2014 at 23:58
  • @John1024 at the time, I wasn't 100% sure about what the -- did either, I found it in an example. I just came across this post again today and decided to figure it out. It looks like it's terminating the positional arguments, which is handy for a function like basename and dirname which behave differently if they receive no arguments. unix.stackexchange.com/questions/11376/…
    – dantiston
    Mar 31, 2016 at 16:46

You must log in to answer this question.

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