What does sort “by location” do?

I have a massive table of contents made of Groups. It is very deep (6 levels in some places). Peppered inside a few of these are RTFs.

Now, I want to display just the contained RTFs in a Smart Group, but have them sorted in the original order. That’s my goal.

So if the Groups in the ToC are named (1), (2), (3), etc., and (1) contains RTFs A and B, (2) is empty, and (3) contains RTF C, I want the Smart Group to list the replicants in their original order inside the ToC, like so:

A
B
C

I have never even noticed the “by location” sort option. I had hoped that it would let me reach my goal above. It does not.

So my questions are:

Q1: Is it possible to make the Smart Group described above?
Q2: What does sorting “by location” actually do?

I guess that this is in a document? If not, where do you “have” this TOC? In which format?

So supposing that you’re talking about a document containing the TOC in an hereto unspecified format: you’d need a smart rule that would read that document. I don’t quite see that happening (and I don’t know why you’d want a smart rule to operate on only one document).

Where are “the replicants” suddenly coming from?

Still assuming that the TOC is in some kind of (hopefully text) document, I’d simply do a grep kind of command on them like

(() => {
  const app=Application("DEVONThink 3");
  app.includeStandardAdditions = true;
  const path = app.selectedRecords()[0];
  const result = app.doShellScript(`grep .rtf "${path}`);
  app.displayDialog(result);
} 

But all this is just a wild guess given that your description of the task is lacking relevant information.

Hello, and thanks for the response. The ToC is just a massive set of Groups. Imagine a list of folders: G1, G2, …, G100. And each of these has sub-folders. A massive outline structure, made entirely of folders. Some of these folders have text documents in them. I want my Smart Group to collect just the text files, but then SORT them in the same order as the outline order of the Groups.

So imagine 90 Groups, many nested, and inside of these are 8 text files. Let G1 contain A, B, and C; G2 contain D; G2-sub-1 contain E, G2-sub-2 contain F, G10 contain G, G90 contain H—then I want the Smart Group to sort the text files in the (vertical) order: A, B, C, D, E, F, G, and H.

What Sort setting do I use to reproduce the original order of the text files inside the outline/ToC/structure of Groups?

Do you mean

  • I have a record that contains a set of groups
    or
  • I see a set of groups in DT and I consider this to be a TOC?

In the latter case: You do not have a TOC. You have a view of a database. Although that might seem picky, it makes a huge difference. In the first case, you’d just go through a text file (as I assumed in my previous post). In the latter … well, I don’t know.

I’m not sure that I understand what “original” means here. At least here, there is nothing “original” about the sort order: When I click on “Name”, they get sorted by name, and when I click on “Creation Date”, they get sorted by creation date. Same with tags.

A Smart Group does not “sort” text files. At least I don’t know how it would do that. It can show you the records matching your current criteria. And in this view you can click on “Name” to sort by name, on “Tags” to sort by tags and so on. Sort order is dynamic.

What you apparently (I may be wrong) want to achieve is to sort the text files by name of the immediate parent group. But also that is not clear. A group containing other groups is just a type of record containing other records of this (and possibly other) types. There’s no sorting implied here.

Smart rules don’t even provide access to the parent group(s). Also, records can be part of several groups (think replicants). So… No, I do not think that what you want is possible with smart groups.

But what is it that you do want to achieve? I don’t mean “I want to see the text files ordered by parent group name”, but rather “I need this kind of order to do …” Maybe there’d be another way to reach that goal.

There is no sort method that would accomplish such a sort automatically. The only option would be to use the Unsorted option and manually put the files in the desired order.

Hello! Well, I’ve come up with a workaround that I’d like to share here, for posterity.

  1. I wrote an AppleScript that prepends numbers in the usual “##. ”-type format. I then selected all the Groups in the outline structure and prepend numbers to them.

  2. Then, to eliminate the Groups from the SmartGroup, I give them a color label.

  3. Then I set the container of the Groups-outline as the Smart Group search scope, and use NOT that label as a filter, and use by Location as the sort rule.

Result: Any items inside the outline structure are shown—sans the Groups that hid them—in their original (intra-outline) order! … except for ones inside a single numbered Group, which receive the default alphabetic secondary sort. I can then preserve these as unsorted replicants somewhere and run the AppleScript again to remove the prepended numbers from the Groups-outline.

If it works and makes sense to you, that’s awesome! :slight_smile:

Sorry that it doesn’t make sense. I was being brief and unclear.

The takeaway is that anybody who has a complex group structure and who wants to extract all their contents and display them in an unconcealed and flat format that duplicates their order in the original structure can now do so.

So it turns out that by Location really is the solution for reproducing unsorted orders in a Smart Group after all. It just needed a way to capture and reproduce the original unsorted order. That way is prepending numbers. Doing so makes the unsorted order alphabetic, which in turn lets by Location do its magic inside the Smart Group.

To make clear what I wanted to do …

Here are 170 nested groups. A few of them contain RTF files:

And here is my goal, accomplished: those RTF files unconcealed and in their original outline order:

Here’s an AppleScript for prepending numbers and removing them:

CSH_Prepend_Number___Cmd-Ctrl-Alt-O.scpt.zip (7.2 KB)

I didn’t say it didn’t make sense to me (though I personally have no requirement for it).
I was encouraging you for creating a solution that fits your mindset and need.

Hi @chrillek

I am trying to get this to work as an example to run JXA from Keyboard Maestro

I think there was a mising ) at the end and a missing closing quote but still I am not getting any output when I select records in DT3 and then run this in KM - any ideas what is wrong?

(() => {

  const app=Application("DEVONThink 3");

  app.includeStandardAdditions = true;

  const path = app.selectedRecords()[0];

  const result = app.doShellScript(`grep .rtf "${path}"`);

  app.displayDialog(result);

} )

image

Result is:

image

You forgot the pair of parenthesis at the end:

...
}}()

They take care of running the anonymous function defined just before them. Otherwise, you’ll just have an anonymous function object returned by the script, which is never executed.

But: I’m not sure if KM will run this as expected. It might be (check the documentation) that KM wants to call a JS function with a fixed name (like DT’s onperformsmartrule function). In that case, you have to write a function with exactly that name like so

function KMsDesiredFunctionName() {
 // code goes here
}

not (!) followed by a pair of parenthesis at the end.

Apart from this:
const path = app.selectedRecords()[0];
will give you an Object Reference to the first selected record (which is not a “path” of any kind). You have to derefences its path property for what you plan to do:

const path = app.selectedRecords()[0].path() (for example, one could of course write that a bit less compressed).

An alternative to using doShellScript with grep on the record’s path would be to use a Regular Expression like /(\w\.rtf)\W/ on the records plainTextand assemble all matches into a string fordisplayDialog`. But that is for another time :wink:

Thanks - Probably closer but still not what we are seeking:

function testfunction() {

  const app=Application("DEVONThink 3");

  app.includeStandardAdditions = true;

  const path = app.selectedRecords()[0].path();

  const result = app.doShellScript(`grep .rtf "${path}"`);

  app.displayDialog(result);

}

testfunction;

image

image

You have to call the function: testfunction()
(I know, parenthesis are scary)

Fair enough re: parentheses

Why do I get a privilege error - Devonthink 3 was designated and it is open with records selected. Is this related to having to designate Terminal to run the shell script?

2021-11-14 18:07:37 Execute a JavaScript For Automation failed with script error: text-script: execution error: Error: Error: A privilege violation occurred. (-10004). Macro “JXA Demo” cancelled (while executing Execute JavaScript For Automation).

I tried this:

function testfunction() {

  const app=Application("DEVONThink 3");

const app2=Application("Terminal");

  app.includeStandardAdditions = true;

  const path = app.selectedRecords()[0].path();

  const result = app2.doShellScript(`grep .rtf "${path}"`);

  app.displayDialog(result);

}

testfunction();

And then get this:


2021-11-14 18:12:35 Execute a JavaScript For Automation failed with script error: text-script: execution error: Error: Error: Message not understood. (-1708). Macro “JXA Demo” cancelled (while executing Execute JavaScript For Automation).

You can call doShellScript only on the current application’s Application object, not on “DEVONthink 3” nor on “Terminal”. And you have to include standard aditions for the current application’s object, too.

So this should (!) work:

function testfunction() {
  const app=Application("DEVONThink 3");
  app.includeStandardAdditions = true;
  curApp = Application.currentApplication();
  curApp.includeStandardAdditions = true;

  const path = app.selectedRecords()[0].path();
  const result = curApp.doShellScript(`grep .rtf "${path}"`);
  app.displayDialog(result);
}
testfunction();

See also

Much appreciated @chrillek - when I try that exact script I get:

2021-11-15 04:52:49 Execute a JavaScript For Automation failed with script error: text-script: execution error: Error: Error: The command exited with a non-zero status. (1). Macro “JXA Demo” cancelled (while executing Execute JavaScript For Automation).
2021-11-15 04:52:57 Running application query took a while (3399 us)

I see the same error when the file in question does not contain the string you’re looking for. In that case grep returns an error which is then reported back by doShellScript. So let’s catch the error:

function testfunction() {
  const app=Application("DEVONThink 3");
  app.includeStandardAdditions = true;
  curApp = Application.currentApplication();
  curApp.includeStandardAdditions = true;

  const path = app.selectedRecords()[0].path();
  let result="String not found";
  try {
  result = curApp.doShellScript(`grep .rtf "${path}"`);
  } catch (error) {
  /* Print error to JavaScript console, will appear in Script Editor */
  console.log(error.errorNumber);
  }
  app.displayDialog(result);
}
testfunction();

Details on trycatch

1 Like

That works - huge thanks!

Question - if I want to modify this to also search inside PDF/text files (which grep does not search) what route do you suggest? [I’ll do the learning/work to edit it - just hoping you can point me in the right direction.]

search inside PDF/text files (which grep does not search)

Actually the -a argument treats all files as ASCII, so you technically can grep a PDF.