Here is a macro for creating or updating a folder structure in the macOS file system.
Create folder structure from text outline.kmmacros (30.4 KB)
USE
- Specify a path to an 'anchor' folder, in which the new directory structure will be either created or found and updated. (the anchor folder will be created if it doesn't yet exist)
- Provide a tab-indented plain text outline (directly as a Keyboard Maestro text variable, or perhaps read in from a plain text file.
And run the macro, to see a report tree on which every node is annoted as either 'Found' or 'Created'.
For example, given an anchor folder path:
and a tab-indented outline:
we might obtain, on first run:
and if we run it again, all the nodes will be marked as 'Found' (already existing)
JS Source of the Execute a JavaScript for Automation action
/*
Copyright 2018 Rob Trew
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE
OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
// Create folder structure, at a given 'anchor' filePath,
// based on a supplied plain text outline.
// (must be consistently tab or 4-space indented)
// Ver 0.01
(() => {
'use strict';
// main :: IO String
const main = () => {
const
kme = Application('Keyboard Maestro Engine'),
fpAnchor = kme.getvariable('anchorFolderPath'),
strOutline = kme.getvariable('folderOutline');
return drawTree(
fmapTree(
fp => {
const lr = createDirectoryIfMissingLR(
true, fp
);
return lr.Left || lr.Right;
},
filePathTree(
fpAnchor,
treesFromLineIndents(
lineIndents(strOutline)
)
)
)
);
};
// GENERIC FUNCTIONS --------------------------------------
// Left :: a -> Either a b
const Left = x => ({
type: 'Either',
Left: x
});
// Node :: a -> [Tree a] -> Tree a
const Node = (v, xs) => ({
type: 'Node',
root: v, // any type of value (but must be consistent across tree)
nest: xs || []
});
// Right :: b -> Either a b
const Right = x => ({
type: 'Either',
Right: x
});
// Tuple (,) :: a -> b -> (a, b)
const Tuple = (a, b) => ({
type: 'Tuple',
'0': a,
'1': b,
length: 2
});
// append (++) :: [a] -> [a] -> [a]
// append (++) :: String -> String -> String
const append = (xs, ys) => xs.concat(ys);
// concatMap :: (a -> [b]) -> [a] -> [b]
const concatMap = (f, xs) => [].concat.apply([], xs.map(f));
// cons :: a -> [a] -> [a]
const cons = (x, xs) => [x].concat(xs);
// createDirectoryIfMissingLR :: Bool -> FilePath ->
// Either String String
const createDirectoryIfMissingLR = (blnParents, fp) =>
doesPathExist(fp) ? (
Right(`Found: '${fp}'`)
) : (() => {
const
e = $(),
blnOK = $.NSFileManager.defaultManager[
'createDirectoryAtPath' +
'WithIntermediateDirectoriesAttributesError'
]($(fp)
.stringByStandardizingPath,
blnParents, undefined, e
);
return blnOK ? (
Right(`Created: '${fp}'`)
) : Left(e.localizedDescription);
})();
// doesPathExist :: FilePath -> IO Bool
const doesPathExist = strPath =>
$.NSFileManager.defaultManager
.fileExistsAtPath(
$(strPath).stringByStandardizingPath
);
// draw :: Tree String -> [String]
const draw = node => {
// shift :: String -> String -> [String] -> [String]
const shift = (first, other, xs) =>
zipWith(
append,
cons(first, replicate(xs.length - 1, other)),
xs
);
// drawSubTrees :: [Tree String] -> [String]
const drawSubTrees = xs => {
const lng = xs.length;
return lng > 0 ? (
lng > 1 ? append(
cons(
'β',
shift('ββ ', 'β ', draw(xs[0]))
),
drawSubTrees(xs.slice(1))
) : cons('β', shift('ββ ', ' ', draw(xs[0])))
) : [];
};
return append(
lines(node.root),
drawSubTrees(node.nest)
);
};
// drawTree :: Tree String -> String
const drawTree = tree =>
unlines(draw(tree));
// filePathTree :: filePath -> [Tree String] -> Tree filePath
const filePathTree = (fpAnchor, trees) => {
const go = fp => tree => {
const path = `${fp}/${tree.root}`;
return Node(
path,
tree.nest.map(go(path))
);
};
return Node(fpAnchor, trees.map(go(fpAnchor)));
};
// filter :: (a -> Bool) -> [a] -> [a]
const filter = (f, xs) => xs.filter(f);
// fmapTree :: (a -> b) -> Tree a -> Tree b
const fmapTree = (f, tree) => {
const go = node => Node(
f(node.root),
node.nest.map(go)
);
return go(tree);
};
// foldl :: (a -> b -> a) -> a -> [b] -> a
const foldl = (f, a, xs) => xs.reduce(f, a);
// foldl1 :: (a -> a -> a) -> [a] -> a
const foldl1 = (f, xs) =>
xs.length > 1 ? xs.slice(1)
.reduce(f, xs[0]) : xs[0];
// isNull :: [a] -> Bool
// isNull :: String -> Bool
const isNull = xs =>
Array.isArray(xs) || typeof xs === 'string' ? (
xs.length < 1
) : undefined;
// iterateUntil :: (a -> Bool) -> (a -> a) -> a -> [a]
const iterateUntil = (p, f, x) => {
let vs = [x],
h = x;
while (!p(h))(h = f(h), vs.push(h));
return vs;
};
// levels :: Tree a -> [[a]]
const levels = tree =>
map(xs => map(x => x.root, xs),
iterateUntil(
xs => xs.length < 1,
xs => concatMap(x => x.nest, xs), [tree]
)
);
// lines :: String -> [String]
const lines = s => s.split(/[\r\n]/);
// map :: (a -> b) -> [a] -> [b]
const map = (f, xs) => xs.map(f);
// 'The mapAccumL function behaves like a combination of map and foldl;
// it applies a function to each element of a list, passing an accumulating
// parameter from left to right, and returning a final value of this
// accumulator together with the new list.' (See Hoogle)
// mapAccumL :: (acc -> x -> (acc, y)) -> acc -> [x] -> (acc, [y])
const mapAccumL = (f, acc, xs) =>
xs.reduce((a, x, i) => {
const pair = f(a[0], x, i);
return Tuple(pair[0], a[1].concat(pair[1]));
}, Tuple(acc, []));
// minimum :: Ord a => [a] -> a
const minimum = xs =>
xs.length > 0 ? (
foldl1((a, x) => x < a ? x : a, xs)
) : undefined;
// readFile :: FilePath -> IO String
const readFile = strPath => {
let error = $(),
str = ObjC.unwrap(
$.NSString.stringWithContentsOfFileEncodingError(
$(strPath)
.stringByStandardizingPath,
$.NSUTF8StringEncoding,
error
)
);
return Boolean(error.code) ? (
ObjC.unwrap(error.localizedDescription)
) : str;
};
// replicate :: Int -> a -> [a]
const replicate = (n, x) =>
Array.from({
length: n
}, () => x);
// snd :: (a, b) -> b
const snd = tpl => tpl[1];
// unlines :: [String] -> String
const unlines = xs => xs.join('\n');
// zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
const zipWith = (f, xs, ys) =>
Array.from({
length: Math.min(xs.length, ys.length)
}, (_, i) => f(xs[i], ys[i], i));
// TREES FROM INDENTED TEXT
// treesFromLineIndents :: [(Int, String)] -> [Tree String]
const treesFromLineIndents = xs =>
foldl((levels, tpl) => {
const
indent = tpl[0],
iNext = indent + 1,
iMax = levels.length - 1,
node = Node(tpl[1]);
return (
levels[
indent < iMax ? (
indent
) : iMax
].nest.push(node),
iNext > iMax ? (
levels.push(node)
) : levels[iNext] = node,
levels
);
}, [Node(undefined, [])],
xs
)[0].nest;
// lineIndents :: String -> [(Int, String)]
const lineIndents = strIndented => {
const
rgxIndent = /^(\s*)(.*)$/,
xs = concatMap(
s => {
const
lr = s.match(rgxIndent).slice(1),
w = lr[1];
return isNull(w) ? (
[]
) : [Tuple(lr[0].length, w)];
},
lines(strIndented)
),
indents = concatMap(x => {
const n = x[0];
return n > 0 ? [n] : [];
}, xs),
margin = minimum(indents),
// Smallest non-zero indent difference
// between any two adjacent lines
unit = minimum(
filter(
x => x > 0,
snd(mapAccumL(
(a, x) => Tuple(x, Math.abs(x - a)),
0, indents
))
)
);
return unit > 1 ? map(
x => Tuple(
Math.floor((x[0] - margin) / unit),
x[1]
),
xs
) : xs;
};
// MAIN ---
return main();
})();