Finder

Automating Finder

Finder is probably the most-used program on a Mac with a comprehensive scripting dictionary. The examples here illustrate how to get to a file object from a path and how to rename a selection of files or folders using a regular expression.

Get a file object from its name#

File is of course one of the central elements of Finder. However, you can’t get a file object directly from its complete name.

Using an absolute path#

If your file is given by a complete path, i.e. one beginning with ‘/’, you have to drill down through the complete folder hierarchy:

function fileFromPath(fullname) {
  const finderApp = Application("Finder");
  /* Split the full name to the file at folder separators '/' 
     Throw away the first, empty element with splice
  */
  const components = fullname.split('/').slice(1);
  /* The name of the file proper is the last element of
     components, i.e. the part following the last '/' in fullname 
     Remove it from components and save in filename */
  const filename = components.pop(); 
  /* components now contain all folders from the root to the one
    containing filename. Drill down to find the last folder. */
  let container = finderApp.startupDisk();
  components.forEach(dir => {
    container = container.folders[dir];
  })
  const file = container.files[filename];
  return file;
}

Call this function like const fileĀ = fileFromPath('/Users/me/Desktop/folder1/file.text') and file will be a File object from Finder. So file.comment() would give you the Finder comment for it etc.

Using a relative path#

If your filename is specified relative to your current location, i.e. it does not begin with ‘/’, you have to first determine the current directory like shown in Marrying JXA with ObjC. Then you can simply prepend it to the file name and pass it to fileFromPath:
const file = fileFromPath(`${currentDir}/${filename}`)

Get a folder object from its name#

Getting a folder from its name is very similar to getting a file. In fact, you can modify the code above slightly to work with folders, too:

function pathToFinderObject(fullpath) {
ObjC.import("Foundation");
const fileManager = $.NSFileManager.defaultManager;
const isDir = Ref();
const exists = fileManager.fileExistsAtPathIsDirectory($(fullpath),isDir);
if (!exists) {
  throw `File ${fullpath} not found`;
}
 const finderApp = Application("Finder");
 const components = fullpath.split('/').slice(1);
 const filename = isDir[0] ? undefined :  components.pop(); 
 let container = finderApp.startupDisk();
  components.forEach(dir => {
    container = container.folders[dir];
  })
  const file = isDir[0] ? container.files[filename] : 
  return file;
}

This function relies on the JXA-Objective-C bridge.

Batch rename files using Regular Expression#

Similarly to a script for DEVONthink 3, the following code allows the user to batch rename the currently selected files using a regular expression.

(() => {
  const app = Application("Finder");
  app.includeStandardAdditions = true;
  const selection = app.selection();
  if (selection.length > 0) {
    const suffix = selection.length > 1 ? "s" : "";

    /* Set title to display # of selected records */

    const title = 
      `Change name${suffix} for ${selection.length} file${suffix}`;

    /* Ask user for RegExp, stop if empty string */
    const searchFor = app.displayDialog("Search for RE", {
      withTitle: title,
      defaultAnswer: "",
    });
    if (searchFor.textReturned === "") return;

     /* 
      * Ask user for replacement, 
      * showing the RegExp they entered 
      */
    const replaceWith = app.displayDialog(
      `Replace "${searchFor.textReturned}" ` +
      `by: (use $1, $2 etc. for groups)`,
      { withTitle: title, defaultAnswer: "" }
    );
    const re = new RegExp(searchFor.textReturned);
    const reText = replaceWith.textReturned;

    const inform = selection.map(
      (s) => `${s.name()} => ${s.name().replace(re, reText)}`
    );
    app.displayDialog(inform, { withTitle: "Filename to change" });
    selection.forEach((s) => {
      s.name = s.name().replace(re, reText);
    });
  }
})();