No internet connection
  1. Home
  2. How to

Duplicate and increment a file in Finder

By Andrew Sherman @Andrew_Sherman
    2021-02-24 21:50:56.708Z

    My workflow involves duplicating project files in Finder and incrementing the number in the name each time there is a significant new version. This workflow is the same regardless of whether the extension is .ptx .mp4 .prproj etc

    Example filenames
    ProjectName-Mix-v1.ptx
    ProjectName-Mix-v2.ptx

    My script works OK so far, but it gives errors sometimes. Specifically sometimes it doesn't find the "Paste" menu item, and sometimes the script seems to happen too quickly and Finder gets the numbers wrong, or opens the old file instead of the new one. I have some manual "waits" in there but I was wondering if there was a better, more elegant and robust way of doing it.

    Also I'm looking for a way to take my original file, and move it into a folder called "Old", and then to re-select the newly incremented file and open it (end of script).

    And lastly currently it doesn't work if the file does not yet have a version number; not sure if there's a way around that.

    Here's the script so far.

    
    // Get name of selected file
    var sourceFilePath = sf.ui.finder.firstSelectedPath;
    var sourceFileName = sourceFilePath.replace(/^.*[\\\/]/, '');
    var sourceFileNameQuoted = 'Copy “' + sourceFileName + '”';
    var sourceFileNameNoExtension = sourceFileName.replace(/\.[^/.]+$/, "");
    
    //Get and Increment Last Number
    function getAndIncrementLastNumber(str) {
        return str.replace(/\d+$/, function (s) {
            return ++s;
        });
    }
    
    var destinationFileName = getAndIncrementLastNumber(sourceFileNameNoExtension);
    
    // Copy selected file
    sf.ui.finder.menuClick({
        menuPath: ["Edit",sourceFileNameQuoted],
    });
    
    sf.wait({
        intervalMs: 300,
    });
    
    // Paste file
    sf.ui.finder.menuClick({
        menuPath: ["Edit","Paste Item"],
    });
    
    sf.wait({
        intervalMs: 300,
    });
    
    // Rename
    sf.ui.frontmostApp.menuClick({
        menuPath: ["File","Rename"],
    });
    
    sf.keyboard.type({
        text: destinationFileName,
    });
    
    sf.keyboard.press({
        keys: "return",
    });
    
    // Open new file with default application
    sf.ui.finder.menuClick({
        menuPath: ["File","Open"],
    });
    
    // Move original source file to folder called "Old" (in the background)
    // Create folder if it doesn't exist
    
    Solved in post #21, click to view
    • 25 replies

    There are 25 replies. Estimated reading time: 14 minutes

    1. Chris Shaw @Chris_Shaw2021-02-25 01:20:56.309Z2021-02-25 15:44:15.396Z

      The key here is to use the sf.ui.file' actions to move and copy files. This way you're not relying on finder clicks.

      here you go:

      // Get paths and names of selected file
      var fullPath = decodeURIComponent(sf.appleScript.finder.selection.getItem(0).asItem.path);
      var directory = fullPath.split('/').slice(0, -1).join('/');
      var filename = fullPath.split('/').slice(-1)[0];
      var filenameWithoutExtension = filename.split('.').slice(0, -1).join('.');
      var extension = filename.split('.').slice(-1)[0];
      
      
      //Create a new name (get the name and suffixed number - if your session name was 'My Session 42' then save 'My Session ' in grps[1] and '42' in grps[2])
      var grps = filenameWithoutExtension.match(/^(.+?)(\d+)$/);
      
      //Construct a new name based on the prefix and the number (+1), followed by the '.ptx' suffix. If there was no number, just add ' 2'
      // This next line will add a suffix if no number is found at end of file (in this case " - V1")
      var newNumber = !grps || grps.length === 0 || isNaN(Number(grps[2])) ? ' - V1' : ((Number(grps[2]) + 1) + '');
      var newFile = (grps ? grps[1] : (filenameWithoutExtension)) + newNumber + '.ptx';
      
      // Save copy of session with new name
      sf.file.copy({
          sourcePath: fullPath,
          destinationPath: newFile,
      });
      
      // Move original source file to folder called "Old" (in the background)
      // Create folder if it doesn't exist
      
      // This will create 'Old Sessions' if it doesn't exist.
      sf.file.directoryCreate({
          path: directory + '/' + "Old Sessions",
      });
      
      sf.file.move({
          sourcePath: fullPath,
          destinationPath: directory + '/' + "Old Sessions/" + filename,
      });
      
      // Open new file
      
      sf.file.open({
          path: newFile,
      });
      
      ``
      1. Dustin Harris @Dustin_Harris
          2021-02-25 01:28:55.973Z

          Bah you beat me to it Chris! (and with way more elegant code!)🤟

          1. It's elegant because it's mostly @chrscheuer 's code🙂…

            //CS//

            1. In reply toDustin_Harris:

              That first chunk of code that splits the file path is in my code library. So useful.

              1. AAndrew Sherman @Andrew_Sherman
                  2021-02-25 08:20:20.700Z

                  How do you manage / save items in your code library? What do you use?

                  1. Dustin Harris @Dustin_Harris
                      2021-02-25 12:04:38.181Z

                      My ‘library’ is just a package that I’ve called ‘*Library’, which contains a bunch of scripts that have functions I reuse often, so instead of rewriting the same code over and over, I copy it from my ‘library’ script and paste it into the script I’m working on :)

                      1. AAndrew Sherman @Andrew_Sherman
                          2021-02-25 12:14:45.346Z

                          That one's going into the Ideas forum...

                  2. In reply toChris_Shaw:
                    AAndrew Sherman @Andrew_Sherman
                      2021-02-25 08:30:38.470Z

                      It looks like I need to change a permission on my hard drive for this to work, I'm getting this error when i try to run the command: IOException in file.copy: You can’t save the file “VideoTest-01.mov” because the volume “Macintosh HD” is read only..

                      Do you know how I can resolve this? Is it something to do with system access for sf.ui.file?

                      1. This is likely caused by macOS having forgotten to ask you for permission to have SoundFlow access that folder. This can happen on Catalina+ due to the hardened runtime.

                        Go to System Preferences -> Security & Privacy -> Files & Folders and make sure SoundFlow has access:

                        1. AAndrew Sherman @Andrew_Sherman
                            2021-02-25 12:06:21.845Z

                            Thanks Christian, I thought this might be the case so I checked, but I gave Soundflow permissions for Full Disk Access as well as Files and Folders. Does it make a difference whether it's the Soundflow editor or the background app?

                            1. Does it make a difference whether it's the Soundflow editor or the background app?

                              We call everything SoundFlow, so ideally you'll never have to make this distinction yourself. Did you do anything manual since you asked this?

                              1. AAndrew Sherman @Andrew_Sherman
                                  2021-02-26 14:45:59.431Z

                                  It wasn't working with Soundflow having Files and Folders permissions, so I added Full Disk Access to see if it would work. Still not working.

                                • In reply toAndrew_Sherman:

                                  I'd say, it' also pretty likely that you may be trying to save at the root of the drive, which would always give you an error. Make sure to double check the path you're trying to save at :)

                                  1. AAndrew Sherman @Andrew_Sherman
                                      2021-02-26 14:43:54.259Z

                                      Hi Christian, no that's not it; I've been testing it in different locations as well es external drives, and it gives the same error for each location.

                                      Error log:
                                      IOException in file.copy: You can’t save the file “FreshBurrito-02.prproj” because the volume “MP-005” is read only. (Increment 2: Line 19)

                                      1. That's odd. The file.copy action generally works (it's used by a lot of customers every day) so there is something specific either in your script or in your volume setup that causes this.

                                        Try to boil it down to a single line of code with string values while debugging, to verify that indeed the paths are correct:

                                        sf.file.copy({
                                            sourcePath: '/Users/chr/Desktop/sourcefile.txt',
                                            destinationPath: '/Volumes/MP-005/blabla/blabla',
                                        });
                                        

                                        Once you have your paths as static strings - provided that you're still seeing issues, once we have that, we can have you try a couple of Terminal commands to use instead while testing/isolating the issue.

                                        Please let us know how the code looks with static paths when you're still seeing the issue.

                                        1. AAndrew Sherman @Andrew_Sherman
                                            2021-03-02 11:48:28.524Z

                                            Hi Christian, I've tried this but I can't get copy working:

                                            sf.file.copy({
                                                sourcePath: '/Users/andrewsherman/VideoTest-01.mov',
                                                destinationPath: '/Users/andrewsherman/Movies',
                                            });
                                            

                                            OException in file.copy: “VideoTest-01.mov” couldn’t be copied to “andrewsherman” because an item with the same name already exists.

                                            Note: there is no file in the "Movies" folder

                                            1. What's missing here is the inclusion of the file name in the destinationPath, like so:

                                              sf.file.copy({
                                                  sourcePath: '/Users/andrewsherman/VideoTest-01.mov',
                                                  destinationPath: '/Users/andrewsherman/Movies/VideoTest-01.mov',
                                              });
                                              
                                              1. AAndrew Sherman @Andrew_Sherman
                                                  2021-03-02 16:47:09.015Z

                                                  Aha, thank you. I'll use that and try to debug the bigger script.

                                                  1. AAndrew Sherman @Andrew_Sherman
                                                      2021-04-12 09:53:00.053Z2021-04-12 12:16:12.662Z

                                                      So I finally got round to debugging this one and I've got it working now, thanks to the amazing Soundflow community help! For people who are struggling with a script, if you use a "log" at various points in your script you can see what Soundflow is computing for that variable, and in that way I went through it from top to bottom and found a few places where my paths were missing elements.

                                                      I do have one more question: how to I get it to add a leading zero to my filename version number? For example FileName-03.ptx

                                                      
                                                      // Get paths and names of selected file in Finder
                                                      var fullPath = decodeURIComponent(sf.appleScript.finder.selection.getItem(0).asItem.path);
                                                      var directory = fullPath.split('/').slice(0, -1).join('/');
                                                      var filename = fullPath.split('/').slice(-1)[0];
                                                      var filenameWithoutExtension = filename.split('.').slice(0, -1).join('.');
                                                      var extension = filename.split('.').slice(-1)[0];
                                                      
                                                      // Create a new name (get the name and suffixed number - if your session name was 'My Session 42' then save 'My Session ' in grps[1] and '42' in grps[2])
                                                      var grps = filenameWithoutExtension.match(/^(.+?)(\d+)$/);
                                                      
                                                      // Construct a new name based on the prefix and the number (+1), followed by the extension suffix. If there was no number, just add ' 2'
                                                      // This next line will add a suffix if no number is found at end of file (in this case " - V1")
                                                      var newNumber = !grps || grps.length === 0 || isNaN(Number(grps[2])) ? '-V01' : ((Number(grps[2]) + 1) + '').padStart(2, '0');
                                                      var newFile = (grps ? grps[1] : (filenameWithoutExtension)) + newNumber + '.' + extension;
                                                      
                                                      // Save copy of session with new name
                                                      sf.file.copy({
                                                          sourcePath: fullPath,
                                                          destinationPath: directory + '/' + newFile,
                                                      });
                                                      
                                                      // Create folder called "Old" if it doesn't exist
                                                      
                                                      sf.file.directoryCreate({
                                                          path: directory + '/Old',
                                                      });
                                                      
                                                      // Move original source file to folder called "Old"
                                                      
                                                      sf.file.move({
                                                          sourcePath: fullPath,
                                                          destinationPath: directory + '/Old/' + filename,
                                                      });
                                                      
                                                      // Open new file with default application
                                                      
                                                      sf.file.open({
                                                          path: directory + '/' + newFile,
                                                      });
                                                      
                                                      Reply1 LikeSolution
                                                      1. Awesome!

                                                        Change the newNumber declaration line to:

                                                        var newNumber = !grps || grps.length === 0 || isNaN(Number(grps[2])) ? '-V01' : ((Number(grps[2]) + 1) + '').padStart(2, '0');
                                                        
                                                        1. AAndrew Sherman @Andrew_Sherman
                                                            2021-04-12 10:08:56.580Z

                                                            Excellent, thanks Christian

                                    • A
                                      In reply toAndrew_Sherman:
                                      Andrew Sherman @Andrew_Sherman
                                        2021-02-25 08:18:55.511Z

                                        Wow, thanks Chris. I'm going to have to get my head around what's happening in this one, I can see the sf.ui.file actions will be useful.

                                        1. Hey guys, quick question on this - like I said before, amazing little script. Really helps my workflow! I had an idea and was wondering if there was a way to modify the code to incorporate when there is a modifier at the end of a session file. For Instance, Song1_V2_Clean, to make that change to Song1_V3_Clean, rather than Song1_V2_Clean-V1. This would be really helpful for when I stick a modifier name at the end of a session file and want to keep it at the end and not in the middle.

                                          Thanks guys!
                                          DB

                                          1. AAndrew Sherman @Andrew_Sherman
                                              2023-06-27 18:12:16.784Z

                                              Hi Daniel, I'm not sure if you use ChatGPT or something similar, but I've found it quite helpful for Javascript code like this. It requires some trial and error, but I've found the best results taking a script one step at a time and building its functionality bit by bit. For someone like me who doesn't know a whole lot of Javascript that can be quite helpful. I have not tested this, but this is what I got from GPT regarding your question - you may need to modify it or add it to the script in the relevant place.

                                              let str = 'Song1_V2_Clean';
                                              
                                              let parts = str.split('_'); // split the string into parts
                                              
                                              // extract version number (without the 'V')
                                              let versionNumber = parseInt(parts[1].substring(1));
                                              
                                              // increment version number
                                              versionNumber++;
                                              
                                              // construct new version string
                                              parts[1] = 'V' + versionNumber;
                                              
                                              let newStr = parts.join('_'); // join the parts back together
                                              
                                              console.log(newStr); // output: Song1_V3_Clean
                                              
                                          2. D
                                            In reply toAndrew_Sherman:
                                            Daniel Braunstein @Daniel_Braunstein
                                              2023-06-05 20:27:22.206Z

                                              Guys - thank you for this treasure. What an amazing script. I've been digging for hours trying to find a way to do it with Mac automator to no avail... This just saved me a lot of clicks and a lot of forgetting to save a new version!!!