Problem With JXA ObjC Code in Shell Script in Plugin Action

Here's some example code, but please note: This code doesn't actually do anything of relevance, other than demonstrate the error I'm trying to solve. In fact, the code in question isn't even invoked in the JXA script - its mere presence causes problems.

#!/bin/bash
osascript -l JavaScript <<JXA_END 2>/dev/null
(function() {
    'use strict';

    // NOTE: This never gets called
    function readTextFile(strPath) {
        var error;
        var str = ObjC.unwrap(
            $.NSString.stringWithContentsOfFileEncodingError(
                $(strPath).stringByStandardizingPath,
                $.NSUTF8StringEncoding,
                error
            )
        );
        if (error)
            throw Error('Could not read file "' + strPath + '"');
        return str;
    }

    return "Hello World";
})()
JXA_END

This code, minus the osascript wrapper of course, runs just fine in Script Editor or from Atom. But execute this script in a Terminal window, or in a KM Plugin, and I get the following error (under El Cap 10.11.6):

line 2: strPath: command not found

I'm pretty sure the error message is misleading. I think the error has to do with the ObjC code. I've tried various Objc.import( ) statements, but haven't found anything to help.

Anyone got any ideas? @ComplexPoint - I found a post of yours from way back when that kind-of alluded to a similar issue, and I wonder if you ever figured out what the issue was/is?

Thanks.

PS: I know there are other ways to read a file. This is just one example of ObjC code that doesn't work; some of the other ObjC things I might want to do can't be as easily replaced with other methods.

Havenā€™t tested, but at first sight I think that the dollars are perhaps being read as Bash variable prefixes.

One of the useful things about the <<JXA_END ā€¦ JXA_END kind of embedding is that you can still use dollar-prefixed Bash variables inside it.

Here, however, there are contending interpretations of the dollar. Perhaps experimentation will yield some kind of escaping of them ?

This seems to work here:

(Escaping any dollar character in the JS code with a backslash, to prevent it being read by Bash as the start of a Bash variable reference)

osascript -l JavaScript <<JXA_END 2>/dev/null

(function() {
    // readFile :: FilePath -> maybe String
    let readFile = strPath => {
        var error = \$(),
            str = ObjC.unwrap(
                \$.NSString.stringWithContentsOfFileEncodingError(
                    \$(strPath)
                    .stringByStandardizingPath,
                    \$.NSUTF8StringEncoding,
                    error
                )
            );
        return error.code ? error.localizedDescription : str;
    }

    return readFile('/Users/houthakker/Desktop/scratch.txt')

    return "Hello World";
})()
JXA_END
1 Like

Now that you mention it, the answer seems so obvious. But my mind went in a different direction, and I couldnā€™t see it.

Thanks!!

1 Like

Dan, for the benefit of the rest of us, could you please explicitly state the resolution?

Thanks.

Rob actually explained it well. Here's a quote:

One of the useful things about the <<JXA_END .... JXA_END kind of embedding is that you can still use dollar-prefixed Bash variables inside it.

So instead of repeating what Rob said, I'll give some examples of what he's talking about. I'm sure I'll use a wrong term here or there. But the concept should be correct, regardless.


#example #1 - Bash Variables

Consider this Bash shell script:

1  #!/bin/bash
2  Document_Name="My Test Document"
3  echo $Document_Name

The result is this:

My Test Document

In line # 2, we set a variable named Document_Name to the value "My Test Document".

The key point here is in line # 3, which shows that we can have Bash insert the value of a variable anywhere within a Bash script, by using a dollar sign followed by the name of the variable.


#example #2 - Bash variables in JavaScript

Consider this Bash shell script:

1  #!/bin/bash
2  Document_Name="My Test Document"
3  osascript -l JavaScript <<JXA_END 2>/dev/null
4  (function() {
5
6      return "$Document_Name";
7
8  }) ()
9  JXA_END

The result is this:

My Test Document

Why doesn't the JavaScript code (line # 6) result in displaying the actual characters "$Document_Name"?

Because in a Bash shell script, the dollar sign is always interpreted as a Bash command - even when it resides inside an osascript segment.

Well, almost always.


#example #3 - Escaping the Dollar Sign in JavaScripts inside a Bash Script

Consider this Bash shell script:

1  #!/bin/bash
2  Document_Name="My Test Document"
3  osascript -l JavaScript <<JXA_END 2>/dev/null
4  (function() {
5
6      return "\$Document_Name";
7
8  }) ()
9  JXA_END

The result is this:

$Document_Name

Why is this different from example # 2? Because in line 6, we use a backslash to escape the dollar sign, telling the Bash script to use a literal dollar sign, instead of interpreting it as a Bash variable.


#example #4: Using ObjC references in JavaScript inside Bash Scripts

Consider this shortened version of the script in my original post:

 1  #!/bin/bash
 2  osascript -l JavaScript <<JXA_END 2>/dev/null
 3  (function() {
 4      'use strict';
 5
 6      var error;
 7      var str = ObjC.unwrap(
 8          $.NSString.stringWithContentsOfFileEncodingError(
 9              $(strPath).stringByStandardizingPath,
10              $.NSUTF8StringEncoding,
11              error
12          )
13      );
14
15  })()
16  JXA_END

The above JavaScript code will run just fine (without the osascript wrapper) in Script Editor. (Actually, since it's not a complete example, it will get a JavaScript error, but ignore that for now.) But from Bash, it will generate an error.

If you've been following along, you already know why. Line # 9 uses the JavaScript dollar sign notation. Bash interprets the dollar sign as a Bash command, and we get an error because $(strPath) isn't a valid Bash command.

(I don't know why line # 8 doesn't generate an error, but even without generating an error, I doubt it would end up working right.)

So the solution to getting the above code to work correctly is to escape each dollar sign with a backslash. Problem solved.

I hope this helps.

3 Likes

See background under unix shell 'Here document"

1 Like

Thanks, Dan. That was very helpful.

OK, so then the solution is to always precede a ObjC $ with a backslash, correct?
So then the code would be:

 8          \$.NSString.stringWithContentsOfFileEncodingError(
 9              \$(strPath).stringByStandardizingPath,
10              \$.NSUTF8StringEncoding,

This seems very unfortunate, since now I can't test code in Script Editor, and then use it directly in a shell script. I suppose the same thing would be true if I run JXA ObjC code embedded in a shell script, all run from AppleScript? (don't know why I'd ever want to do this)

But why would you want to use a shell script to execute a JXA function?
Why not just run it directly as a JXA script?

I'm thrilled you asked that question. I almost included the answer in my previous post, but it was getting long, and I wanted to save that for a separate post.

Attribution: I learned the following from @ComplexPoint's code in some of his plugins, so the credit goes to him.


Consider this JavaScript script, which is kind of useless, but illustrates my point:

(function(documentName) {

    return documentName;

})
("Test Document");

Here we have a JavaScript function that takes one parameter, which we've named "documentName". I can easily test this in Script Editor (or Atom), and it runs just fine.

But suppose this JavaScript code is actually going to be used in a KM plugin. And suppose we want "documentName" to be supplied with the value of a plugin parameter called "Document Name".

Since KM stores plugin parameters as Environment variables, we need to get the value of the environment variable "KMPARAM_Document_Name". (KM names the environment variable by taking the plugin parameter's name, replacing spaces with underscores, and prefixing it with "KMPARAM_").

So now we have a choice. We can write JXA code to extract the environment variable. The problem with that is, we can't easily test it in Script Editor (or Atom), because the environment variable hasn't been set.

You may have already guessed the answer, but if not, here's the Bash script for the above code, which goes in the plugin:

#!/bin/bash
osascript -l JavaScript <<JXA_END 2>/dev/null
(function(documentName) {

    return documentName;

})
("$KMPARAM_Document_Name");
JXA_END

The JavaScript portion is exactly the same, except for the final line, which passes the value for documentName.

Instead of passing a hard-coded value like "Test Document", we want to pass in the value of our plugin's parameter. So we do it the easy way, using the aforementioned dollar-sign feature of Bash, and we don't have to change the function itself at all.


So, for testing, I work with a standard JavaScript or JXA script, and pass in hard-coded parameters. When I'm ready to use it in the plugin, I just copy the JavaScript into the Bash shell script, and make sure the last line passes in the KM environment variable(s) that contain the plugin parameter(s).

I hope this makes sense. It's actually quite ingenious.

I'm sure you're right, but it seems to be over my head.

Since you can get KM Variables (and I'm assuming KM plugin parameters) directly in a shell script and you can do the same with AppleScript and JXA, what is the need to put JXA code in a shell script?

The only way to get the value of plugin parameters is through environment variables. You canā€™t communicate with KM and ask for their values, like you can with normal variables. Thatā€™s the whole point of the previous exercise. Perhaps I should have mentioned that. :blush:

So, since you must access environment variables when writing plugins, you have two choices.

One is to write code, in your JXA script, that specifically reads the environment variables. Totally doable, but you canā€™t really test it from Script Editor (or Atom).

The other way is the method Iā€™ve just shown.

Iā€™m willing to bet money, after youā€™ve worked with both methods, youā€™ll find Robā€™s method the easiest.

And hereā€™s the cool part: When I wrote my first plugin in JavaScript, I just cannibalized an existing plugin, and it happened to be one of Robā€™s. I had no idea what I was doing, as is the case for most of us when we steal someone elseā€™s code. I just changed it and it worked.

It was many months before I actually appreciated the beauty of how Rob does it.

So I guess my bottom line is ā€œtry it - youā€™ll like it!ā€ :smile:

1 Like

So, just to make sure I understand this, this issue is only an issue when you are writing a KM Plugin, correct?

Well, you would if you were using ObjC $ commands, right?
Test in SE/Atom, and then add prefix of "\" when you put the JXA function in a shell script.

Do you happen to have some example code for doing this?
If not, please don't go to any trouble to gen some.

It just seems like to me that you could write a JXA function one time for your library, that gets the KM parameters in the shell environment. Then just call this function whenever you need it.

For testing, you could just use a default value:

var pluginParam = getKMPluginParam("Document_Name") || "Default Doc Name";

I'm sure you're right, but for me it may be āˆž since I'm not writing any plugins, and don't have any plans to do so.

Sorry for the distraction, but maybe it will help others interested in plugin.

I suppose there might be some other outlier cases, but I would think that, yes, the main issue is when writing plugins.

Well, you would if you were using ObjC $ commands, right?

Yes. Although up until this point, I haven't written any plugins that require the use of ObjC. In the case of the plugin I'm currently working on, I need to write to disk, and the routine I had handy uses ObjC. But there's also Application extensions that will do the job, without the use of the dollar sign.

It just seems like to me that you could write a JXA function one time for your library, that gets the KM parameters in the shell environment. Then just call this function whenever you need it.

For testing, you could just use a default value:

var pluginParam = getKMPluginParam("Document_Name") || "Default Doc Name";

Sure, that would work, unless the parameter is optional and might legitimately be missing.

I'm sure you're right, but for me it may be āˆž since I'm not writing any plugins, and don't have any plans to do so. Sorry for the distraction, but maybe it will help others interested in plugin.

LOL. No worries. Trust me, if I can't "defend" why I'm doing something a certain way, then I need to rethink things. And I regularly get stuck along one line of thinking, so I appreciate the opportunity to think my decisions through.

1 Like

@ComplexPoint OK, so ObjC uses the dollar sign, but is that a requirement? Isnā€™t the dollar sign just a shortcut for something else?

For instance, if JQuery is being used, isnā€™t the syntax $( ) just a shortcut for JQuery( ), or something like that?

In other words, somewhere the dollar sign is being assigned to something, when used with ObjC, and I wonder if whatever it represents could be used instead?

Does that make any sense?

Automation.ObjC.$ an object reference exported to the global environment of the JS interpreter from the Automation object.

All imported ObjC methods are accessible as children of it (see under $ in the JXA release notes).

You could define an alias for it at the head of a script.

(function (){
    'use strict';

    ObjC.import('Cocoa')

    var dollar = Automation.ObjC.$;
    
    
    dollar.NSBeep();
    
})();
1 Like

Or, of course, as $ is defined in the global name space, you could define your alias outside the module, where it will persist between module runs until the interpreter session ends.

var dollar = dollar || Automation.ObjC.$;

(function () {
    'use strict';
    
    ObjC.import('Cocoa');

    dollar.NSBeep();

})();
1 Like

I discovered a problem with passing in Bash variables as parameters to the function. If the text contains multiple lines, it wonā€™t work. And now that I think about it, it probably wonā€™t work correctly if it contains quotes either.

Thoughts?

Perhaps those are cases where the application object interface is a more natural or effortless instrument than the use of bash dollar strings ?

(Though there will probably be some routes around the quotes and multiple lines)

#!/bin/bash

MSG="this 'is' multilined\nand seems to work\nwith 'single quotes'\nthough \\\"double\\\" need triple escaping"

osascript -l JavaScript <<JXA_END 2>/dev/null
((strMsg) =>{
    'use strict';

    return strMsg;

})("$MSG");

JXA_END

OK, ready for a good laugh? As often happens, I disagree with @JMichaelTX, do it my own way, and end up agreeing with Jim.

This is another case of that, although the reason I ended up agreeing with Jim isnā€™t entirely because of his arguments, but because, in the long run, it solves a problem and performance is (possibly) better.

###First issue: Parameters with multiple lines, or quotes, etc.

Using the "$KMPARAM_Variable_Name" method, the script will fail if the parameter contains multiple lines, or embedded quotes, and possibly other special characters.

###Second issue: Performance

If I donā€™t use the "$KMPARAM_Variable_Name" method, then I donā€™t have to run it in a shell script. Instead, I can compile the JXA script (using Script Editor), and have the plugin run the compiled script directly.

NOTE: I havenā€™t measured the speed difference, but it seems reasonable that running a compiled script will execute faster than using a shell script containing uncompiled JavaScript. I could be wrong, but it doesnā€™t really matter, because the first issue is the most relevant.


So, hereā€™s an example of how Iā€™m coding it now:

 1 (function(inDesignMode) {
 2     'use strict';
 3     ObjC.import('stdlib');
 4 
 5     function getPluginParameter(name) {
 6         var envName = "KMPARAM_" + name.replace(/ /g, "_");
 7         var result;
 8 
 9         if (!inDesignMode) {
10             result = $.getenv(envName);
11             return result ? result.trim() : "";
12         }
13 
14         var designingParams = {
15             KMPARAM_Sort_Order: "newest first",
16             KMPARAM_Timestamp_Format: "date + time + duration"
17         };
18 
19         result = designingParams[envName];
20         if (result === undefined)
21             throw Error("Unknown Plugin Parameter Name '" + name + "' while in designing mode");
22         return result;
23     }
24 
25     function execute() {
26         var sortOrder = getPluginParameter("Sort Order");
27         var timestampFormat = getPluginParameter("Timestamp Format");
28 
29         return "Sort Order: '" + sortOrder + "'\nTimestamp Format: '" + timestampFormat + "'";
30     }
31 
32     if (inDesignMode) {
33         return execute();
34     } else {
35         try {
36             return execute();
37         } catch (e) {
38             return e.message;
39         }
40     }
41 })(true);

Click here for version without line numbers

###Point of Interest # 1:

Notice that Iā€™m passing a parameter to the main function indicating whether Iā€™m running in design mode or not. See lines 1 and 41. This allows me to change how the code operates when Iā€™m testing it in Script Editor or Atom, compared to how it runs when the plugin executes it

###Point of Interest # 2:
The function ā€œgetPluginParameterā€, lines 5-23.

I call this passing the plugin parameter name, as it is specified in the pluginā€™s plist file. The function converts it to the environment variable name. This is merely a design choice on my part, but it allows me to easily use the parameter name when throwing exceptions. I suppose this might add some execution time, but I doubt itā€™s measurable.

The function either returns the environment variable value or a hard-coded test value, depending on whether inDesignMode is true or not (see point # 1, above).

You may wonder why I coded lines 14-22 the way I did, instead of just using a switch statement. The reason is that, as you can see in lines 14-17, that the test values are easily identifiable, and color-coded to make it easy to pick out the values vs. the names.

1 Like

Dan, one of your great strengths is that you always evaluate things based on objective criteria, and you are open to change when the criteria so indicates.

I am glad, if in some small way, I helped you arrive at a better solution.
But most of it is your on doing.