Assembling these things with the basic Keyboard Maestro building blocks has real advantages of simplicity and maintainability.
If you are going to do any part of it with Execute Script
actions you probably need both a motive and a means:
- Motive - something that doesn't seem to be included yet among the pre-fabricated blocks, or that you want more fine control with, and
- Means - pre-fabricated scripting building blocks, so that you don't serve too many years in the wheel-reinvention business.
In this particular case, once you have read two variables, let's call them:
nameListPath
, and
nameMaybeNew
into the script, (using the Keyboard Maestro Engine getvariable function), you might, depending on division of labour between Execute Script Action and other KM actions, need a readFile function, to read your name list with.
JavaScript for Automation and Applescript versions might look something like:
JXA
// readFile :: FilePath -> IO String
const readFile = strPath => {
var error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
ObjC.wrap(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return typeof error.code !== 'string' ? (
str
) : 'Could not read ' + strPath;
};
Applescript
-- readFile :: FilePath -> IO String
on readFile(strPath)
set ca to current application
set str to unwrap(ca's NSString's stringWithContentsOfFile:(wrap(strPath)'s ¬
stringByStandardizingPath()) encoding:(ca's NSUTF8StringEncoding) |error|:(missing value))
if str is missing value then
"Could not read file at " & strPath
else
str
end if
end readFile
then, a lines function to split your names text into a list or array of lines:
JXA
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
Applescript
-- lines :: String -> [String]
on |lines|(xs)
paragraphs of xs
end |lines|
next, checking for a matching name in a list of names would need some kind of find function, and perhaps an isPrefixOf function to go with it, so that you can define the kind of match that you are looking for:
JXA
// find :: (a -> Bool) -> [a] -> Maybe a
const find = (p, xs) => {
for (var i = 0, lng = xs.length; i < lng; i++) {
var x = xs[i];
if (p(x)) return just(x);
}
return nothing('Not found');
};
// isPrefixOf :: [a] -> [a] -> Bool
const isPrefixOf = (xs, ys) => {
const pfx = (xs, ys) => xs.length ? (
ys.length ? xs[0] === ys[0] && pfx(
xs.slice(1), ys.slice(1)
) : false
) : true;
return typeof xs !== 'string' ? pfx(xs, ys) : ys.startsWith(xs);
};
AppleScript
-- find :: (a -> Bool) -> [a] -> Maybe a
on find(p, xs)
tell mReturn(p)
set lng to length of xs
repeat with i from 1 to lng
if |λ|(item i of xs) then return {nothing:false, just:item i of xs}
end repeat
{nothing:true}
end tell
end find
-- isPrefixOf :: [a] -> [a] -> Bool
-- isPrefixOf :: String -> String -> Bool
on isPrefixOf(xs, ys)
if class of xs is string then
(offset of xs in ys) = 1
else
if length of xs = 0 then
true
else
if length of ys = 0 then
false
else
set {x, xt} to uncons(xs)
set {y, yt} to uncons(ys)
(x = y) and isPrefixOf(xt, yt)
end if
end if
end if
end isPrefixOf
-- Lift 2nd class handler function into 1st class script wrapper
-- mReturn :: First-class m => (a -> b) -> m (a -> b)
on mReturn(f)
if class of f is script then
f
else
script
property |λ| : f
end script
end if
end mReturn
Finally, if the name wasn't found, and you wanted to add it to your list, you could just append it to the existing file, but your example shows sorted names, so you might want to do two things:
- first add the new name anywhere in the existing list, perhaps at the beginning (a cons function, as in construct an expanded list from a new atom and an existing list), and then
- apply a sort function.
JXA
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x].concat(xs);
// sort :: Ord a => [a] -> [a]
const sort = xs => xs.slice()
.sort();
AppleScript
-- cons :: a -> [a] -> [a]
on cons(x, xs)
{x} & xs
end cons
-- sort :: Ord a => [a] -> [a]
on sort(xs)
((current application's NSArray's arrayWithArray:xs)'s ¬
sortedArrayUsingSelector:"compare:") as list
end sort
and to get the expanded and sorted version back into a text file, you would need to first convert the list back to a continuous string (an unlines function), and then write it out (a writeFile function)
JXA
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// writeFile :: FilePath -> String -> IO ()
const writeFile = (strPath, strText) =>
$.NSString.alloc.initWithUTF8String(strText)
.writeToFileAtomicallyEncodingError(
ObjC.wrap(strPath)
.stringByStandardizingPath, false,
$.NSUTF8StringEncoding, null
);
Applescript
-- unlines :: [String] -> String
on unlines(xs)
intercalate(linefeed, xs)
end unlines
-- intercalate :: String -> [String] -> String
on intercalate(s, xs)
set {dlm, my text item delimiters} to {my text item delimiters, s}
set str to xs as text
set my text item delimiters to dlm
return str
end intercalate
-- writeFile :: FilePath -> String -> IO ()
on writeFile(strPath, strText)
set ca to current application
(ca's NSString's stringWithString:strText)'s ¬
writeToFile:(stringByStandardizingPath of ¬
(ca's NSString's stringWithString:strPath)) atomically:true ¬
encoding:(ca's NSUTF8StringEncoding) |error|:(missing value)
end writeFile
but before you overwrite the old file with writeFile you should certainly back up the original file somewhere, perhaps with a derived (e.g. time-stamped) name. Now we need splitFileName and showCurrentTime functions to build the name of our backup file with ...
JXA
// splitFileName :: FilePath -> (String, String)
const splitFileName = strPath =>
strPath !== '' ? (
strPath[strPath.length - 1] !== '/' ? (() => {
const
xs = strPath.split('/'),
stem = xs.slice(0, -1);
return stem.length > 0 ? (
[stem.join('/') + '/', xs.slice(-1)[0]]
) : ['./', xs.slice(-1)[0]];
})() : [strPath, '']
) : ['./', ''];
// showCurrentTime :: () -> String
const showCurrentTime = () =>
(new Date())
.toISOString();
Applescript
-- splitFileName :: FilePath -> (String, String)
on splitFileName(strPath)
if strPath ≠ "" then
if last character of strPath ≠ "/" then
set xs to splitOn("/", strPath)
set stem to text 1 thru -2 of xs
if stem ≠ {} then
{intercalate("/", stem) & "/", item -1 of xs}
else
{"./", item -1 of xs}
end if
else
{strPath, ""}
end if
else
{"./", ""}
end if
end splitFileName
-- showCurrentTime :: () -> String
on showCurrentTime()
set dteNow to current date
tell (dteNow - (time to GMT)) to set {y, m, d, t} to ¬
{its year, its month as integer, its day, its time}
intercalate("-", {y, m, d}) & "T" & ¬
intercalate(":", {t div 3600, (t mod 3600) div 60, t mod 60}) & "Z"
end showCurrentTime
-- splitOn :: String -> String -> [String]
on splitOn(strDelim, strMain)
set {dlm, my text item delimiters} to {my text item delimiters, strDelim}
set xs to text items of strMain
set my text item delimiters to dlm
return xs
end splitOn
We're probably almost done, but by now, Peter's drag and drop building blocks may well be looking easier to find, assemble and maintain, and more thoroughly tested too : -)
Still, you could pull it all together into a script, though you still might need to paste a couple more generic helper functions.
And unless your scripting languages are in fairly well-practiced shape, it might well take more time (and certainly need more testing), than with the KM building blocks.
For reference, a couple of preliminary (non-production) sketches:
JXA
Ensure that list contains name (thru JavaScript for Automation).kmmacros (23.2 KB)
Applescript
Ensure that list contains name (Applescript).kmmacros (24.1 KB)