0

Problem statement:

I have a number of directories, corresponding to series names, and random filename underneath each, as follows:

Title.S01E01/1234abc.mkv
Title.S01E02/5678def.mkv
Title.S01E03/10101qpr.mkv
....etc

Each directory is either empty or contains at most one file.

I need to rename files underneath each directory with the directory name, keeping the extension.

Research:

I tried the usual find/rename combination, but I couldn't exactly figure out the regex necessary to extract the directory name from the list of found files, and to appropriately pass it to the rename commend.

2
  • Can we assume there is one main directory with only subdirectories (no regular files) directly in it? Can we assume each and every file in the directory should be processed (no matter the depth)? Or should we mind the depth (i.e. always main-dir/sub-dir/file)? Or is this depth the only depth that occurs? Nov 17, 2020 at 0:55
  • Thanks for your response @KamilMaciorowski. One main directory. Assume depth is 1. Thanks again.
    – prof.hell
    Nov 17, 2020 at 1:22

1 Answer 1

0

Please read the whole answer before you run the code.

Run this in the parent directory of Title.S01E01/, Title.S01E02/ etc.:

find . -path '*/*/*' -type f -print -exec sh -c '
   f="$1"
   d="${f%/*}"
   cd "$d" || exit 1
   d="${d##*/}"
   f="${f##*/}"
   e="${f##*.}"
   if [ "$e" = "$f" ]; then
      e=""
   else
      e=".$e"
   fi
   mv -i -- "$f" "$d$e"
  ' find-sh {} \;

Notes:

  • This does not fulfill the "with Linux rename" request from the title; rename is never used. Still the code does the job.

  • AFAIK the code is portable. Keep reading to learn about some non-portable improvements.

  • The solution works recursively, as the tile requests.

  • -print is only to show you what file is being processed.

  • -path '*/*/*' is a portable (semi-)equivalent of -mindepth 2 of GNU find. The point is to leave all files (if any) in the starting directory intact. The shell code does not retrieve the real name of the directory holding the currently processed file; it gets the name from find where the starting directory is referred to as . (because we said so: find . …). This is not the name you would want for a file.

    Technically the first * in -path '*/*/*' will always match at least our .. -path './*/*' will work as well, as long as the starting directory is specified as . or ./ in find invocation.

  • If you need to protect files in subdirectories in subdirectories of the starting directory then you need -maxdepth 2 of GNU find or equivalent(ish) solution. Poor man's portable solution seems to be ! -path '*/*/*/*'; a better portable solution includes -prune but it gets complicated.

  • The above link also elaborates on possible invalid character issues when one uses * with -path. In GNU find definitely prefer -mindepth 2 (and -maxdepth 2 if needed) instead of -path ….

  • The shell code mainly manipulates strings to get the directory name, the file name and the "extension".

    I write "extension" in quotes because in Linux what you call extension is just a part of the name. The distinction exists more in users' minds than in Linux filesystems or OS design (while it does exist in DOS/Windows filesystems and OS design).

  • Files without "extension" are supported; they are supported even if there's a dot in the directory name (but then expect the file to gain an "extension" from the directory name).

  • A hidden file (with name starting with a dot) may get unhidden; an unhidden file may get hidden. It depends only on the name of the respective directory. The code does not try to preserve a leading dot nor a lack of it.

  • If the target name happens to be a directory (i.e. yet another, deeper subdirectory) then the file will be moved into it because this is how mv works.

  • You said there is "at most one file" per directory. My code uses mv -i anyway, so in case you made an honest mistake an extra file will not automatically overwrite anything. If you get prompted then think, investigate, abort maybe (Ctrl+c).

  • It should be safe to run the code for the second time (e.g. after aborting), the only side effect will be 'something.foo' and 'something.foo' are the same file messages from mv for files already renamed. Except in a case where something.foo is the name of the directory. E.g. if the original file is bar (without "extension") and the first run renames it to something.foo then the second run will rename it to a stable name something.foo.foo. If all the files are originally named with "extensions" then you will never experience this quirk.

  • find-sh is explained here: What is the second sh in sh -c 'some shell code' sh?

  • Anyone who wants to modify my code should not forget how to use find -exec sh -c safely.

You must log in to answer this question.

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