Match Up to a Sequence of Characters in a Comma-Separated String Array

Hello All,

My question is the following:

I have a comma-separated array of strings, which I got from reading Pro Tools' AudioSuite menu with AppleScript. It looks like this (I extracted a few lines from the FabFilter block):

FabFilter Pro-Q 3__EQ,FabFilter Pro-C 2__Dynamics,FabFilter Pro-MB__Dynamics,FabFilter Pro-R__Reverb,FabFilter Pro-DS__Dynamics,FabFilter Pro-L 2__Dynamics,FabFilter Timeless 3__Delay,FabFilter Pro-C 2_Harmonic

And so on and so forth.

The array format is basically Plugin Name__Tag (which is actually a submenu, but Tag sounds more intuitive). A plugin name may have one or more tags, one per string.

At this point, I would like to group all tags bound to a plugin name, and make a single string out of those: Plugin Name_Tag1_Tag2_Tag3 etcetera.

I was thinking of using a "For Each" block in Keyboard Maestro, grouping all substrings by plugin name (reading strings up to the comma character), and build a new composite array.

In other words, RegEx for each string in the array should do the following:

  1. Match everything up to "__";
  2. For each match in 1), match everything after "__", until comma, and build the new string.

RegExes for 1) and 2) are

.+?(?=__)

and

[^,]+

Is there a way to combine the two RegEx expressions? Or should I keep two separate loops, one for plugin names and one for their tags?

Thank you for your help!

Jan

It might actually be easier to do this in the AppleScript you use to collect them, then return a return-delimited list to KM. When you read the menu in AS, are you getting a list back (rather than a string)?

One option (JS works quite well for splits, accumulations and joins), might be something like:

Each plugin name with all associated tags.kmmacros (2.7 KB)

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    const
        accumulation = Application(
            "Keyboard Maestro Engine"
        )
        .getvariable("csvSource")
        .split(",")
        .reduce(
            (a, x) => {
                const [plug, tag] = x.split("__");

                return Object.assign(
                    a,
                    {
                        [plug]: (a[plug] || [])
                        .concat(tag)
                    }
                );
            }, {}
        );

    return Object.keys(accumulation)
    .map(k => [k, ...accumulation[k]].join("_"))
    .join("\n");
})();
4 Likes

Hello Nige_S, I'm trying to make it simple...
Through AppleScript I get a list of menu items, structured like this:

menu item "FabFilter Pro-Q 3" of menu "EQ" of menu item "EQ" of menu "AudioSuite" of menu bar item "AudioSuite" of menu bar 1 of application process "Pro Tools"

The first "EQ" is actually a plug-in submenu, or "tag" (EQ, Dynamics, Reverb, Delay etc. etc.).
Then there's the "vendor" submenu, in this case FabFilter (not in the above example).
Then there's redundant infos.

I already simplified the list and kept only what I really need.
And I already filtered all tags, so that the vendor name comes right after the plugin name:
FabFilter Pro-Q 3__FabFilter__Tag1__Tag2 etc.

The double underscore is totally arbitrary.
I was wondering about the quickest way of getting the Tag1__Tag2 part.

Thank you,
J

Very nice and workable solution, ComplexPoint!!!

I need to figure it out a bit more, but it's very close to what I'm looking for.

Thank you!
J

You're not going to understand that anytime soon, unless you're fairly familiar with JavaScript.

I'm a novice with JS and get the gist, but I still don't quite understand all the working parts.

Here's an AppleScript – see if it does what you need.


Download: Consolicate Tags v1.00.kmmacros (7.5 KB)

Macro-Image

Keyboard Maestro Export

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.

System Information
  • macOS 10.14.6
  • Keyboard Maestro v10.2

2 Likes

Hello ccstone,
that "figure it out a bit more" was indeed an understatement :joy::joy::joy:

I vaguely remember some C#, but Javascript is smoother, IMO.

Yours solution works flawlessy, too!!
And I've indeed got nice code to study!!

Thank you guys!!!
J

1 Like

The only place in the code where I'm "cheating" is where I sort the list with AppleScriptObjC and that lets me take just one pass through the list.

From there it's all just concatenation of text.

My first crack at this was overcomplicated, and although it seemed to work – further inspection showed otherwise.

You can, incidentally add a sort in the penultimate line:

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    // Ver 0.02

    // Sorting AZ by plugin name.

    const
        accumulation = Application(
            "Keyboard Maestro Engine"
        )
        .getvariable("csvSource")
        .split(",")
        .reduce(
            (a, x) => {
                const [plug, tag] = x.split("__");

                return Object.assign(
                    a,
                    {
                        [plug]: (a[plug] || [])
                        .concat(tag)
                    }
                );
            }, {}
        );

    return Object.keys(accumulation)
    .map(k => [k, ...accumulation[k]].join("_"))
    .sort()
    .join("\n");
})();
2 Likes

That sounds like my cue! I can do overcomplicated... Plus, very rusty with AS records :wink:

This would go at the end of the AppleScript that collects the menu items and assumes they are being presented as a list (as in the first line):

AppleScript
set theList to {"FabFilter Pro-Q 3__EQ", "FabFilter Pro-C 2__Dynamics", "FabFilter Pro-MB__Dynamics", "FabFilter Pro-R__Reverb", "FabFilter Pro-DS__Dynamics", "FabFilter Pro-L 2__Dynamics", "FabFilter Timeless 3__Delay", "FabFilter Pro-C 2__Harmonic"}

set theListofRecords to {{|name|:null, tag:null}}
set AppleScript's text item delimiters to "__"

repeat with eachItem in theList
	set tmpName to text item 1 of contents of eachItem
	set tmpTag to text item 2 of contents of eachItem
	set foundFlag to false
	repeat with eachRecord in theListofRecords
		if eachRecord contains {|name|:tmpName} then
			set tag of eachRecord to (tag of eachRecord & "__" & tmpTag)
			set foundFlag to true
			exit repeat
		end if
	end repeat
	if not foundFlag then copy {|name|:tmpName, tag:tmpTag} to end of theListofRecords
end repeat

set theListofRecords to rest of theListofRecords

set outText to {}
repeat with eachRecord in theListofRecords
	copy (|name| of eachRecord & "__" & tag of eachRecord) to end of outText
end repeat

set AppleScript's text item delimiters to linefeed
return outText as text

You could easily sort the output in KM with a "Filter" action -- downside is that the tags themselves aren't sorted, though you could fix that.

All of which made me wonder -- can we do it in similar fashion in KM? And sort the tags? I do keep promising to try and understand Dictionaries... And so:

Plugins List.kmmacros (9.8 KB)

Image

Definitely a newb's first attempt -- looking forward to suggested improvements!

2 Likes

Welcome to ask about the first part that looks opaque :slight_smile:

1 Like

FWIW, a Haskell solution using Hutton's parser:

Download Macro(s): Plug-in names with associated tags.kmmacros (13 KB)

Macro-Image

Macro-Notes
  • Macros are always disabled when imported into the Keyboard Maestro Editor.
    • The user must ensure the macro is enabled.
    • The user must also ensure the macro's parent macro-group is enabled.
System Information
  • macOS 13.1
  • Keyboard Maestro v10.2
Expand disclosure triangle to see "haskell" source
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Main where

import Control.Applicative
import Control.Monad
import Data.Char
import Data.List
import Data.List.Extra (groupSortOn)

between :: (Monad m) => m a1 -> m a2 -> m b -> m b
between open close p = open >> p >>= \v -> close >> pure v

-- Based on functional parsing library from chapter 13
-- of Programming in Haskell,
-- Graham Hutton, Cambridge University Press, 2016.

newtype Parser a
  = P (String -> [(a, String)])

parse :: Parser a -> String -> [(a, String)]
parse (P p) = p

item :: Parser Char
item =
  P
    ( \case
        [] -> []
        (x : xs) -> [(x, xs)]
    )

instance Functor Parser where
  -- fmap :: (a -> b) -> Parser a -> Parser b
  fmap f p =
    P
      ( \inp ->
          case parse p inp of
            [] -> []
            [(v, out)] -> [(f v, out)]
      )

instance Applicative Parser where
  -- pure :: a -> Parser a
  pure v = P (\inp -> [(v, inp)])

  -- <*> :: Parser (a -> b) -> Parser a -> Parser b
  (<*>) pg px =
    P
      ( \inp ->
          case parse pg inp of
            [] -> []
            [(g, out)] -> parse (fmap g px) out
      )

instance Monad Parser where
  -- (>>=) :: Parser a -> (a -> Parser b) -> Parser b
  p >>= f =
    P
      ( \inp ->
          case parse p inp of
            [] -> []
            [(v, out)] -> parse (f v) out
      )

-- Making choices
instance Alternative Parser where
  -- empty :: Parser a
  empty = P (const [])

  -- (<|>) :: Parser a -> Parser a -> Parser a
  p <|> q =
    P
      ( \inp ->
          case parse p inp of
            [] -> parse q inp
            [(v, out)] -> [(v, out)]
      )

-- Derived primitives
-- sepBy1 p sep = liftM2 (:) p (many (sep >> p))

sepBy :: (Alternative f) => f a -> f [a]
sepBy p = (:) <$> p <*> many p

sepBy1 :: (Alternative f, Monad f) => f a1 -> f a2 -> f [a1]
sepBy1 p sep = (:) <$> p <*> many (sep >> p)

sepBy2 p = (:) <$> p <*> many p

commaSep :: Parser a -> Parser [a]
commaSep p = sepBy1 p (string ",")

tagSep p = sepBy1 p (string " @")

count :: Int -> Parser a -> Parser [a]
count = replicateM

satisfy :: (Char -> Bool) -> Parser Char
satisfy p = item >>= go
  where
    go x
      | p x = pure x
      | otherwise = empty

digit :: Parser Char
digit = satisfy isDigit

digits :: Parser [Char]
digits = some digit

lower :: Parser Char
lower = satisfy isLower

upper :: Parser Char
upper = satisfy isUpper

letter :: Parser Char
letter = satisfy isAlpha

alphanum :: Parser Char
alphanum = satisfy isAlphaNum

char :: Char -> Parser Char
char x = satisfy (== x)

noneOf :: String -> Parser Char
noneOf cs = satisfy (`notElem` cs)

oneOf :: String -> Parser Char
oneOf cs = satisfy (`elem` cs)

string :: String -> Parser String
string [] = pure []
string (x : xs) = char x >> string xs >> pure (x : xs)

ident :: Parser String
ident = lower >>= \x -> many alphanum >>= \xs -> pure (x : xs)

constructor :: Parser String
constructor = upper >>= \x -> many alphanum >>= \xs -> pure (x : xs)

subscriptedIdentifier :: Parser String
subscriptedIdentifier = lower >>= \x -> many (alphanum <|> char '_' <|> char '\'') >>= \xs -> pure (x : xs)

operator :: Parser String
operator = some (oneOf "/=-+!*%<>&|^?•$")

nat :: Parser Int
nat = read <$> some digit

int :: Parser Int
int = (char '-' >> nat >>= \n -> pure (-n)) <|> nat

-- Handling spacing
space :: Parser ()
space = void $ many (satisfy isSpace)

digitString :: Parser String
digitString = many (satisfy isDigit)

naturalNumber :: Parser Int
naturalNumber = read <$> digitString

token :: Parser a -> Parser a
token p = space >> p >>= \v -> space >> pure v

identifier :: Parser String
identifier = token ident

method :: Parser String
method = char '.' >>= \x -> many alphanum >>= \xs -> pure (x : xs)

natural :: Parser Int
natural = token nat

integer :: Parser Int
integer = token int

symbol :: String -> Parser String
symbol xs = token (string xs)

s = "FabFilter Pro-Q 3__EQ,FabFilter Pro-C 2__Dynamics,FabFilter Pro-MB__Dynamics,FabFilter Pro-R__Reverb,FabFilter Pro-DS__Dynamics,FabFilter Pro-L 2__Dynamics,FabFilter Timeless 3__Delay,FabFilter Pro-C 2__Harmonic,FabFilter Pro-Q 3__DSR"

data PlugIn = PlugIn
  { plug :: String,
    tag :: String
  }
  deriving (Show)

plugInParser :: Parser [PlugIn]
plugInParser =
  commaSep $
    PlugIn
      <$> many (satisfy (/= '_'))
      <*> (string "__" >> many alphanum)

f =
  unlines
    . map (\xs -> intercalate "_" $ plug (head xs) : map tag xs)
    . groupSortOn plug
    . fst
    <=< parse plugInParser

interact' :: (String -> String) -> IO ()
interact' f = putStr . f =<< readFile =<< getContents

s1 = "A \"quoted\" string to test"

main :: IO ()
main =
  interact' $
    unlines
      . map (\xs -> intercalate "_" $ plug (head xs) : map tag xs)
      . groupSortOn plug
      . fst
      <=< parse plugInParser
3 Likes

I'll definitely delve deeper into JavaScript soon... :smiley:
Is there also a way to keep only unique lines in the array, and get rid of all the duplicates?

I already do that through sort | uniq and a text file, but having it all in the JS script would be the icing on the cake...

Thank you!
J

Google: javascript array unique

First hit:

1 Like

Duplicates should, I think, fall away automatically if we define the accumulation in terms of key:value dictionaries (JS Objects) rather than ordered lists (JS Arrays),
seeding .reduce with {} rather than with [].

So, perhaps, for example:

Plugin names with associated tags (sorted- no duplicates).kmmacros (2.8 KB)

Expand disclosure triangle to view JS source
(() => {
    "use strict";

    const
        accumulation = Application(
            "Keyboard Maestro Engine"
        )
        .getvariable("csvSource")
        .split(",")
        .reduce(
            (a, x) => {
                const [plug, tag] = x.split("__");

                return Object.assign(
                    a,
                    {
                        [plug]: Object.assign(
                            a[plug] || {},
                            {[tag]: true}
                        )
                    }
                );
            }, {}
        );

    return Object.keys(accumulation).sort()
    .map(
        k => [k, ...Object.keys(accumulation[k]).sort()]
        .join("_")
    )
    .join("\n");
})();
2 Likes

But if that doesn't work with your data – could you perhaps give us a bigger data sample for testing ?

(Including some duplicates in the sense that you have in mind, and want to exclude)

Works like a charm, no need to change anything!!
Of course the array gets used in a "Prompt with list" action.
Thank you @ComplexPoint and all for your help and commitment!!!

Jan