I should start by saying I'm a coding noob. The script I'm working on is really pushing my limited skills to the limit. Please forgive the length of the post. I have tried to keep it short but I think the level of detail is necessary.

I also just give a very high level overview of what I'm having problems with.

Basically, I need my script to create hardlinks and name / number them based on several criteria but I'm not sure whether I'm going about things the right way and/or using the right tools.

(Too Many) Details.

I'm writing a bash script that will automate the process of creating and naming hardlinks from torrent files. I'm using a loop to set variables based on strings that are extracted from both the torrent folder path and the name of the individual files within the torrent folder. I then use the variables to name the hardlinks according a pattern (that complies with Plex's naming scheme).

The problem is though, that I need to create hardlinks from multiple folders/torrents. Here's how each torrent (Dir's A, B,and C) numbers their files any how I need to number them ($HardlinkDir).

 $DirA             $DirB          $DirC                           $HardlinkDir
   │                 │              │                                  │
01.*.mp4             │              │              The Series Name - s01e01 - Episode Name.mp4
02.*.mp4             │              │              The Series Name - s01e02 - Episode Name.mp4
03.*.mp4             │              │              The Series Name - s01e03 - Episode Name.mp4
04.*.mp4             │              │              The Series Name - s01e04 - Episode Name.mp4
05.*.mp4             │              │              The Series Name - s01e05 - Episode Name.mp4
06.*.mp4             │              │              The Series Name - s01e06 - Episode Name.mp4
07.*.mp4             │              │              The Series Name - s01e07 - Episode Name.mp4
                ┌ 01.*.mkv          │              The Series Name - s01e08 - Episode Name.mkv
08.foo.mp4 ─────┼ 02.*.mkv          │              The Series Name - s01e09 - Episode Name.mkv
                └ 03.*.mkv          │              The Series Name - s01e10 - Episode Name.mkv
09.*.mp4                            │              The Series Name - s01e11 - Episode Name.mp4
                               ┌ 01.*.mkv          The Series Name - s01e12 - Episode Name.mkv
10.bar.mp4 ────────────────────┼ 02.*.mkv          The Series Name - s01e13 - Episode Name.mkv
                               └ 03.*.mkv          The Series Name - s01e14 - Episode Name.mkv
11.*.mp4                                           The Series Name - s01e15 - Episode Name.mp4

The names of the hardlinks in $HardlinkDir can be set almost entirely from variables defined before or during the loop. But as the "e##" numbers deviate from e08 onward, the numbers have to be set according to a handful of rules.

A new version of TorrentA is uploaded every few weeks. With it, the number of files preceding the one that contains the string foo, can vary. As a result, the number within the filename where the string foo appears can also vary. As too does the number in filename where the string bar appears. Sometimes, there's more than one file between the files that contain the strings foo, and bar... to make things even more complicated, sometimes the foo and bar files don't have a file numbered between them.

The number of files before, between, and after the files with strings foo, and bar, can vary, as can the numbers in the filenames. But the strings foo. and bar are always present.

Torrents / Dirs A and B always have three files, and they're always numbered and naming the exact same thing from week to week.

One last thing to make things that little bit more complicated... TorrentA (DirA) is periodically updated, so version 1 might only have the first four files. Then TorrentB might be uploaded before the new version of TorrentA is released. So the script might try creating hardlinks for DirB based on information derived from filenames in DirA that don't yet exist (it'd be a rare scenario though).

So as far as I can figure, what I need to script to do (roughly in order) is-

  1. set the name of the file containing the string foo as a variable... $foo.
  2. set the name of the file containing the string bar as a variable... $bar

if either the foo or bar files don't exist, the variable would not be set.

  1. When the loop begins, the script needs to check if the name of the file being processed ($filename) matches either $foo or $bar
  2. if it does match, the script skips the file altogether.
  3. if it doesn't match $foo or $bar, the script extracts the two digit number using awk and/or sed.

That should work for files 01 to 07. 08 would get skipped, then 09 would have to have other arguments.

  1. if the number in the $filename equals the number in $foo +1, but $filename doesn't match $bar then add +2. In other words, if the number in $filename is 09, add 2 to make it 11.

In order to account for the possibility of multiple files being between the foo and bar files, I'm guessing the number set during step 6 needs to iterate as the loop runs, and that the script also needs to check if the number in the filename is higher than that in $bar. For example....

  1. if the number in the $filename equals the number in $foo +1, but $filename doesn't match $bar or have a number higher that $bar then add +3. So a theoretical file 10.*.mkv would become The Series Name - s01e11 - Episode Name.mkv.
  2. if the $filename matches $bar, skip it.
  3. (I'm a little shaky on this one) if the $filename (in DirA remember) has a number greater that $bar, then... I'm not sure what. The number used when naming the hardlink needs to be higher than the numbers that were previously set. Perhaps I could use some kind of counter or output the numbers already in use to a tmp file?

[note] As for the files in DirB and DirC, their numbers would be set in a similar fashion but using the strings within the names of the DirA and DirB files (if $filename == 01.*.mkv ; then etc. etc.). I could also implement checks based on files that already exist in $hardlinkDir.


Q1. Is my basic workflow of how the script could work roughly correct?

Q2. Am I overcomplicating things?

Q3. In regards to step 9., would the possibility of using a counter for setting file numbers after $bar be worth exploring?... I just can't think of how to derive the numbers when the number of files after $foo, and between $bar, and then after $bar might vary... or not exist at all.

  • is '08.foo.mp4' the name of dirB ? Feb 1, 2021 at 4:18
  • I gave a solution (but may be incorrect. just edited it as I forgot to increment the episode_number ... Feb 1, 2021 at 4:59

1 Answer 1


basically : you need a script that takes N arguments, the first can be the resulting HardlinkDir, then a file with the episode names, and the rest can be dirA, dirB, dirC, etc.

to me the simplest is to 1) delete the content of the HardlinkDir dir, then for each files seen alphabetically in dirA, dirB dirC : ln that file. The titles could come from a separate file giving the name you want the Nth file to have ?


a file "series_name.txt" where you put all the episode names in order, 1 per line. for exemple :

 s01e01 - Episode Name
 s01e02 - Episode Name

And a script to which you give the arguments: "HardlinkDir" "series_name.txt" "dirA" "dirB" "dirC" (...)

a simple solution with bash (calling a awk script to do the heavy lifting) is then:

#!/usr/bin/env bash
_usage() {
  cat <<'EOF'
   this script needs at least those arguments:
     # $1=HardLinkDir, $2=series_name.txt # containing 1 line per episode name
     # $3 to $n : the directory, in order, with the *.mp4 and *.mkv in them, in order
   it will then EMPTY the $1 directory, and re-create hard links (ln) in it.
   for msg in "$@"; do printf "Erreur: %s\n" "$msg" ; done
   exit 1

if [ "$#" -lt 3 ]; then
   _usage "We need at least 3 args : HardLinkDir series_name.txt dirA ... "
lndir="$1" ; 
episode_list="$2" ; series_name="${2/.txt/}" ; 
[ -d "$lndir" ] || _usage "arg1 : lndir=$lndir : is not a directory"
[ -f "$episode_list" ] || _usage "arg2 : episode_list=$episode_list : is not a file"
shift 2 # we "absorbed" the first 2 args : the rest are the dirs with the movie files
for dir in "$@"; do 
  arg=$((arg + 1))
  [ -d "$dir" ] || _usage "arg${arg}: '$dir' : is not a directory."

rm "${lndir:-__must_be_nonvoid__}"/*  # delete first the files in that dir, if any

for dir in "$@"; do
  ls "${dir}"/*.???
done | awk -v series_name="${series_name}" -v lndir="${lndir}" '
 BEGIN { after_first=0 }
 (NR == FNR) { rem="we read the first file... so the episode list"

 ((NR != FNR) && ( after_first==0 )) { after_first=1; episode_number=0 }

 ((NR != FNR) && ( $0 ~ /..mp4|.mkv/ ) { 
   rem="NR>FNR, so we read the 2nd file, -, stdin..."
   rem="we also match .mp4 or .mkv, so we have a video file fullname as input"
   fullfilename=$0; ext=fullfilename ; sub(".*[.]","",ext)
   destname = lndir "/" series_name " - " episode[episode_number] "." ext
   cmd="ln \"" fullfilename "\" \"" destname "\""
   printf("executing: %s\n",cmd)
   system(cmd); close(cmd)
' "$episode_list" "-"  # first file is the episode_list file, then stdin

Please note: I just wrote this without testing it at all, as I can't right now test it... Maybe replace "rm" and "ln" above with: "echo rm" and "echo ln", respectively, to see what would be done... I also use only regular awk thinkgs ( hence the weird : ext=fullname; sub(....,ext) )

  • note that the name of the file with the episode_list is also the series_name Feb 1, 2021 at 4:51
  • usually I dislike doing the loop & ls | something ... but here it simplified the awk, imo... It would be cleaner to do everything (parsing, etc) within awk ... but then it would be much easier in perl (but less portable). Here it may choke if a filename has a newline, for exemple... I believe this will not be the case Feb 1, 2021 at 4:56
  • just add -s to ln to create symlinks instead (usefull if the hardlinkdir is not on the same fs as the movie files) Feb 1, 2021 at 5:12

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.