How to get POSIX path of the current script's folder in JavaScript for Automation?
Asked Answered
B

7

7

In AppleScript it’s possible to get the the POSIX path of the folder the current script is located in using this line:

POSIX path of ((path to me as text) & "::")

Example result: /Users/aaron/Git/test/

What’s the JavaScript equivalent?

Barfield answered 27/2, 2015 at 19:54 Comment(0)
G
5

Here's a way [NOTE: I NO LONGER RECOMMEND THIS METHOD. SEE EDIT, BELOW]:

app = Application.currentApplication();
app.includeStandardAdditions = true;
path = app.pathTo(this);
app.doShellScript('dirname \'' + path + '\'') + '/';

note the single quotes surrounding path to work with paths with spaces, etc., in the doShellScript

EDIT After being slapped on the hand by @foo for using a fairly unsafe path-quoting method, I'd like to amend this answer with:

ObjC.import("Cocoa");
app = Application.currentApplication();
app.includeStandardAdditions = true;
thePath = app.pathTo(this);

thePathStr = $.NSString.alloc.init;
thePathStr = $.NSString.alloc.initWithUTF8String(thePath);
thePathStrDir = (thePathStr.stringByDeletingLastPathComponent);

thePathStrDir.js + "/";

If you're going to use this string, of course, you still have to deal with whether or not it has questionable characters in it. But at least at this stage this is not an issue. This also demonstrates a few concepts available to the JXA user, like using the ObjC bridge and .js to get the string "coerced" to a JavaScript string (from NSString).

Graduation answered 27/2, 2015 at 20:54 Comment(8)
Nice! I wasn’t aware of this.Barfield
"note the single quotes surrounding path to work with paths with spaces" ...which completely blows up if the path string contains single quotes instead. Never generate code without fully sanitizing your inputs: it's totally unsafe. When working with do shell script, always use function quotedForm(s) {return "'"+s.replace("'","'\\''")+"'"} to correctly single-quote your arbitrary text before concatenating it, e.g. app.doShellScript('dirname ' + quotedForm(path)).Spondaic
thank you, @Spondaic - I've changed (added to) my answer.Graduation
@Graduation Thanks for the edit! Great that we got rid of the shell call. I’ve created slightly changed version of your code—please see my answer below.Barfield
@foo: great tip, but it should be function quotedForm(s) { return "'" + s.replace(/'/g, "'\\''") + "'" }, to ensure that all embedded ' instances are "escaped".Outpoint
@Outpoint Amazing that the first comment correcting this mistake came nearly two years later... two seconds of testing would have caught this mistake.Nolpros
Looks like all I needed is app.pathTo(this) from all that. Thanks!Deguzman
app.pathTo(this) points to the Script Editor app, not the script itself.Wolsky
G
9

Pure JXA code without ObjC involved:

App = Application.currentApplication()
App.includeStandardAdditions = true
SystemEvents = Application('System Events')

var pathToMe = App.pathTo(this)
var containerPOSIXPath = SystemEvents.files[pathToMe.toString()].container().posixPath()
Gaona answered 12/7, 2015 at 14:38 Comment(1)
Kudos for figuring out a pure JXA method. A side benefit is that SystemEvents.files[pathToMe.toString()].container() gives you access to a full-fledged object with properties reporting information about the folder. A potential down-side - which probably won't matter much for a single call - is that this method is much slower than using the ObjC bridge via $(App.pathTo(this).toString()).stringByDeletingLastPathComponent.js (based on my informal tests, around 5-6 times slower).Outpoint
G
5

Here's a way [NOTE: I NO LONGER RECOMMEND THIS METHOD. SEE EDIT, BELOW]:

app = Application.currentApplication();
app.includeStandardAdditions = true;
path = app.pathTo(this);
app.doShellScript('dirname \'' + path + '\'') + '/';

note the single quotes surrounding path to work with paths with spaces, etc., in the doShellScript

EDIT After being slapped on the hand by @foo for using a fairly unsafe path-quoting method, I'd like to amend this answer with:

ObjC.import("Cocoa");
app = Application.currentApplication();
app.includeStandardAdditions = true;
thePath = app.pathTo(this);

thePathStr = $.NSString.alloc.init;
thePathStr = $.NSString.alloc.initWithUTF8String(thePath);
thePathStrDir = (thePathStr.stringByDeletingLastPathComponent);

thePathStrDir.js + "/";

If you're going to use this string, of course, you still have to deal with whether or not it has questionable characters in it. But at least at this stage this is not an issue. This also demonstrates a few concepts available to the JXA user, like using the ObjC bridge and .js to get the string "coerced" to a JavaScript string (from NSString).

Graduation answered 27/2, 2015 at 20:54 Comment(8)
Nice! I wasn’t aware of this.Barfield
"note the single quotes surrounding path to work with paths with spaces" ...which completely blows up if the path string contains single quotes instead. Never generate code without fully sanitizing your inputs: it's totally unsafe. When working with do shell script, always use function quotedForm(s) {return "'"+s.replace("'","'\\''")+"'"} to correctly single-quote your arbitrary text before concatenating it, e.g. app.doShellScript('dirname ' + quotedForm(path)).Spondaic
thank you, @Spondaic - I've changed (added to) my answer.Graduation
@Graduation Thanks for the edit! Great that we got rid of the shell call. I’ve created slightly changed version of your code—please see my answer below.Barfield
@foo: great tip, but it should be function quotedForm(s) { return "'" + s.replace(/'/g, "'\\''") + "'" }, to ensure that all embedded ' instances are "escaped".Outpoint
@Outpoint Amazing that the first comment correcting this mistake came nearly two years later... two seconds of testing would have caught this mistake.Nolpros
Looks like all I needed is app.pathTo(this) from all that. Thanks!Deguzman
app.pathTo(this) points to the Script Editor app, not the script itself.Wolsky
O
4

To complement the helpful existing answers by providing self-contained utility functions:

// Return the POSIX path of the folder hosting this script / app.
// E.g., from within '/foo/bar.scpt', returns '/foo'.
function myPath() {
    var app = Application.currentApplication(); app.includeStandardAdditions = true
    return $(app.pathTo(this).toString()).stringByDeletingLastPathComponent.js
}

// Return the filename root (filename w/o extension) of this script / app.
// E.g., from within '/foo/bar.scpt', returns 'bar'.
// (Remove `.stringByDeletingPathExtension` if you want to retain the extension.)
function myName() {
    var app = Application.currentApplication(); app.includeStandardAdditions = true
    return $(app.pathTo(this).toString()).lastPathComponent.stringByDeletingPathExtension.js
}

Note: These functions make use of the shortcut syntax forms for the ObjC bridge ($(...) for ObjC.wrap() and .js for ObjC.unwrap(), and also take advantage of the fact that the Foundation framework's symbols are available by default - see the OS X 10.10 JXA release notes.


It's easy to generalize these functions to provide the equivalents of the POSIX dirname and basename utilities:

// Returns the parent path of the specified filesystem path.
// A trailing '/' in the input path is ignored.
// Equivalent of the POSIX dirname utility.
// Examples:
//    dirname('/foo/bar') // -> '/foo'
//    dirname('/foo/bar/') // ditto
function dirname(path) {
  return $(path.toString()).stringByDeletingLastPathComponent.js
}

// Returns the filename component of the specified filesystem path.
// A trailing '/' in the input path is ignored.
// If the optional <extToStrip> is specified:
//   - If it it is a string, it is removed from the result, if it matches at
//     the end (case-sensitively) - do include the '.'
//   - Otherwise (Boolean or number), any truthy value causes any extension
//     (suffix) present to be removed.
// Equivalent of the POSIX basename utility; the truthy semantics of the
// 2nd argument are an extension.
// Examples:
//    basename('/foo/bar') // -> 'bar'
//    basename('/foo/bar/') // ditto
//    basename('/foo/bar.scpt', 1) // -> 'bar'
//    basename('/foo/bar.scpt', '.scpt') // -> 'bar'
//    basename('/foo/bar.jxa', '.scpt') // -> 'bar.jxa'
function basename(path, extToStrip) {
  path = path.toString()
  if (path[path.length-1] === '/') { path = path.slice(0, -1) }
  if (typeof extToStrip === 'string') {
    return path.slice(-extToStrip.length) === extToStrip ? $(path).lastPathComponent.js.slice(0, -extToStrip.length) : $(path).lastPathComponent.js    
  } else { // assumed to be numeric: if truthy, strip any extension
    return extToStrip ? $(path).lastPathComponent.stringByDeletingPathExtension.js : $(path).lastPathComponent.js    
  }
}
Outpoint answered 13/10, 2015 at 3:30 Comment(0)
B
3

So to sum up what I’m doing now, I’m answering my own question. Using @foo’s and @CRGreen’s excellent responses, I came up with the following:

ObjC.import('Foundation');
var app, path, dir;

app = Application.currentApplication();
app.includeStandardAdditions = true;

path = app.pathTo(this);
dir = $.NSString.alloc.initWithUTF8String(path).stringByDeletingLastPathComponent.js + '/';

This is quite close to @CRGreen’s response, however, it’s a bit more terse and I’m importing Foundation instead of Cocoa. Plus, I’m declaring the variables I’m using to avoid accidental globals.

Barfield answered 1/3, 2015 at 11:22 Comment(3)
I've read up a bit about the advantages of using Foundation, but perhaps you can chime in here as to your reasons for using it, @BarfieldGraduation
@Graduation I’m no Mac or iOS developer but my naïve assumption was that Foundation is probably smaller than Cocoa which consists of Foundation Kit, Application Kit, Core Data, and others.Barfield
Nicely done; @CRGreen: To quote from the OSX 10.10 JXA release notes: "The symbols from the Foundation framework are available by default in JavaScript for Automation". In other words: no need for an explicit Objc.import('Foundation'). Also, using the shortcut syntax for wrapping JS objects, we can simplify to dir = $(path).stringByDeletingLastPathComponent.js + '/'Outpoint
S
2

Use -[NSString stringByDeletingLastPathComponent], which already knows how to remove the last path segment safely:

ObjC.import('Foundation')

path = ObjC.unwrap($(path).stringByDeletingLastPathComponent)

Alternatively, if you prefer the more dangerous life, you could use a regular expression to strip the last path segment from a POSIX path string. Off the top of my head (caveat emptor, etc):

path = path.replace(/\/[^\/]+\/*$/,'').replace(/^$/,'/')

(Note that the second replace() is required to process paths with <2 parts correctly.)

Spondaic answered 28/2, 2015 at 14:12 Comment(0)
F
2

I think I found a simpler way to get the parent folder without invoking ObjC.

var app = Application.currentApplication();
app.includeStandardAdditions = true;
thePath = app.pathTo(this);

Path(thePath + '/../../')
Feld answered 16/3, 2015 at 22:6 Comment(2)
Nice, but to get the folder in which the script is located you just need Path(thePath + '/..') (only one level up).Outpoint
Actually, normalization of the path (resolution of the /..) only appears to happen and seems to be an artifact of the implicit result printed by Script Editor; e.g., for script /foo/dir/script, letting Script Editor print the result of Path(thePath + '/..') implicitly shows /foo/dir, but if you inspect the value with console.log or perform string concatenation, you'll get /foo/dir/script/.., i.e., the non-normalized path.Outpoint
D
-2

Lots of interesting solutions above.

Here's mine, which does NOT require ObjC, and returns an object with properties likely to be needed.

'use strict';
var oScript = getScriptProp(this);

/*oScript Properties
    Path
    Name
    ParentPath
    Folder
*/

oScript.ParentPath;

//~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

function getScriptProp(pRefObject) {

  var app = Application.currentApplication()
  app.includeStandardAdditions = true

  var pathScript = app.pathTo(pRefObject).toString();
  var pathArr = pathScript.split("/")

  var oScript = {
    Path: pathScript,
    Name: pathArr[pathArr.length - 1],
    ParentPath: pathArr.slice(0, pathArr.length - 1).join("/"),
    Folder: pathArr[pathArr.length - 2]
  };

  return oScript
}
Durbar answered 1/5, 2017 at 3:6 Comment(1)
app.pathTo(this) points to the Script Editor app, not the script bundle itself.Wolsky

© 2022 - 2024 — McMap. All rights reserved.