all repos — pls @ b3fded352e4ea8c9afcda2ad94ba3697164cc8b8

A polite but determined task runner.

Initial commit (porting from nifty)
Fabio Cevasco fabio.cevasco@it.abb.com
Fri, 01 Oct 2021 10:14:18 +0200
commit

b3fded352e4ea8c9afcda2ad94ba3697164cc8b8

parent

b74e6a27d09f1b977c909d9945e27561c43f9d91

M .gitignore.gitignore

@@ -1,3 +1,5 @@

nimcache/ nimblecache/ htmldocs/ +pls.exe +pls
A pls.nimble

@@ -0,0 +1,63 @@

+import + ospaths + +template thisModuleFile: string = instantiationInfo(fullPaths = true).filename + +when fileExists(thisModuleFile.parentDir / "src/plspkg/config.nim"): + # In the git repository the Nimble sources are in a ``src`` directory. + import src/plspkg/config +else: + # When the package is installed, the ``src`` directory disappears. + import plspkg/config + +# Package + +version = pkgVersion +author = pkgAuthor +description = pkgDescription +license = "MIT" +bin = @["pls"] +srcDir = "src" +installExt = @["nim"] + +# Dependencies + +requires "nim >= 0.19.0" + +const compile = "nim c -d:release" +const linux_x64 = "--cpu:amd64 --os:linux -o:pls" +const windows_x64 = "--cpu:amd64 --os:windows -o:pls.exe" +const macosx_x64 = "-o:pls" +const program = "pls" +const program_file = "src/pls.nim" +const zip = "zip -X" + +proc shell(command, args: string, dest = "") = + exec command & " " & args & " " & dest + +proc filename_for(os: string, arch: string): string = + return "pls" & "_v" & version & "_" & os & "_" & arch & ".zip" + +task windows_x64_build, "Build pls for Windows (x64)": + shell compile, windows_x64, program_file + +task linux_x64_build, "Build pls for Linux (x64)": + shell compile, linux_x64, program_file + +task macosx_x64_build, "Build pls for Mac OS X (x64)": + shell compile, macosx_x64, program_file + +task release, "Release pls": + echo "\n\n\n WINDOWS - x64:\n\n" + windows_x64_buildTask() + shell zip, filename_for("windows", "x64"), program & ".exe" + shell "rm", program & ".exe" + echo "\n\n\n LINUX - x64:\n\n" + linux_x64_buildTask() + shell zip, filename_for("linux", "x64"), program + shell "rm", program + echo "\n\n\n MAC OS X - x64:\n\n" + macosx_x64_buildTask() + shell zip, filename_for("macosx", "x64"), program + shell "rm", program + echo "\n\n\n ALL DONE!"
A src/pls.nim

@@ -0,0 +1,198 @@

+import + json, + os, + parseopt, + logging, + algorithm, + strutils, + sequtils + +import + plspkg/plslogger + +newPlsLogger().addHandler() +setLogFilter(lvlInfo) + +import + plspkg/config, + plspkg/project, + plspkg/messaging + +let usage* = """ $1 v$2 - $3 + (c) 2021 $4 + + Usage: + pls <command> [<target>] Executes <command> (on <target>). + + => For more information on available commands, run: pls help + + Options: + --log, -l Specifies the log level (debug|info|notice|warn|error|fatal). + Default: info + --force, -f Do not ask for confirmation when executing the specified command. + --help, -h Displays this message. + --version, -h Displays the version of the application. +""" % [pkgTitle, pkgVersion, pkgDescription, pkgAuthor] + +var force = false + +# Helper Methods + +proc addProperty(parentObj: JsonNode, name = ""): tuple[key: string, value: JsonNode] = + var done = false + while (not done): + if name == "": + result.key = editValue("Name") + elif name == "name": + warn "Property identifier 'name' cannot be modified." + else: + printValue(" Name", name) + result.key = name + var ok = false + while (not ok): + var value = "" + if parentObj.hasKey(result.key): + value = $parentObj[result.key] + try: + result.value = editValue("Value", value).parseJson + if (result.value == newJNull()): + ok = confirm("Remove property '$1'?" % result.key) + done = true + else: + ok = true + except: + warn("Please enter a valid JSON value.") + done = done or confirm("OK?") + +proc addProperties(obj: var JsonNode) = + var done = false + while (not done): + let prop = addProperty(obj) + obj[prop.key] = prop.value + done = not confirm("Do you want to add/remove more properties?") + +proc changeValue(oldv: tuple[label: string, value: JsonNode], newv: tuple[label: string, value: JsonNode]): bool = + if oldv.value != newJNull(): + printDeleted(oldv.label, $oldv.value) + if newv.value != newJNull(): + printAdded(newv.label, $newv.value) + return confirm("Confirm change?") + +### MAIN ### + +var args = newSeq[string](0) + +for kind, key, val in getopt(): + case kind: + of cmdArgument: + args.add key + of cmdLongOption, cmdShortOption: + case key: + of "force", "f": + force = true + of "log", "l": + var val = val + setLogLevel(val) + of "help", "h": + echo usage + quit(0) + of "version", "v": + echo pkgVersion + quit(0) + else: + discard + else: + discard + +var prj = newPlsProject(getCurrentDir()) + +if args.len == 0: + echo usage + quit(0) +case args[0]: + of "init": + if prj.configured: + fatal "Project already configured." + quit(2) + prj.init() + notice "Project initialized." + of "def": + if args.len < 2: + fatal "No target specified." + quit(3) + let alias = args[1] + var props = newJObject() + prj.load + if prj.targets.hasKey(alias): + notice "Redefining existing target: " & alias + warn "Specify properties for target '$1':" % alias + props = prj.targets[alias] + for k, v in props.mpairs: + if k == "name": + continue + let prop = addProperty(props, k) + props[prop.key] = prop.value + if confirm "Do you want to add/remove more properties?": + addProperties(props) + else: + notice "Mapping new target: " & alias + warn "Specify properties for target '$1':" % alias + addProperties(props) + prj.def(alias, props) + of "undef": + if args.len < 2: + fatal "No target specified." + quit(3) + let alias = args[1] + prj.load + if not prj.targets.hasKey(alias): + fatal "Target '$1' not defined." % [alias] + quit(4) + if force or confirm("Remove definition for target '$1'?" % alias): + prj.undef(alias) + of "list": + prj.load + # TODO + echo "NOT IMPLEMENTED" + of "info": + if args.len < 2: + fatal "No target specified." + quit(3) + prj.load + let alias = args[1] + if not prj.targets.hasKey(alias): + fatal "Target '$1' not defined." % [alias] + quit(4) + let data = prj.targets[alias] + for k, v in data.pairs: + echo "$1:\t$2" % [k, $v] + of "help": + echo "" + if args.len < 2: + var sortedKeys = toSeq(prj.help.keys) + sortedKeys.sort(cmp[string]) + for k in sortedKeys: + printGreen " pls $1" % prj.help[k]["_syntax"].getStr + echo "\n $1\n" % prj.help[k]["_description"].getStr + else: + let cmd = args[1] + let help = prj.help[cmd] + if not prj.help.hasKey(cmd): + fatal "Command '$1' is not defined." % cmd + quit(5) + printGreen " pls " & help["_syntax"].getStr + echo "\n $1\n" % help["_description"].getStr + else: + if args.len < 1: + echo usage + quit(1) + if args.len < 2: + prj.load + var targets = toSeq(prj.targets.pairs) + if targets.len == 0: + warn "No targets defined - nothing to do." + quit(0) + for key, val in prj.targets.pairs: + prj.executeRec(args[0], key) + else: + prj.executeRec(args[0], args[1])
A src/pls.nim.cfg

@@ -0,0 +1,12 @@

+# https://blog.filippo.io/easy-windows-and-linux-cross-compilers-for-macos/ + + +# https://gist.github.com/Drakulix/9881160 +amd64.windows.gcc.path = "/usr/local/bin" +amd64.windows.gcc.exe = "x86_64-w64-mingw32-gcc" +amd64.windows.gcc.linkerexe = "x86_64-w64-mingw32-gcc" + +# http://crossgcc.rts-software.org/doku.php?id=compiling_for_linux +amd64.linux.gcc.path = "/usr/local/bin" +amd64.linux.gcc.exe = "x86_64-linux-musl-gcc" +amd64.linux.gcc.linkerexe = "x86_64-linux-musl-gcc"
A src/plspkg/config.nim

@@ -0,0 +1,5 @@

+const + pkgTitle* = "pls" + pkgVersion* = "1.0.0" + pkgAuthor* = "Fabio Cevasco" + pkgDescription* = "A nifty script runner."
A src/plspkg/help.json

@@ -0,0 +1,26 @@

+{ + "help": { + "_syntax": "help [<command>]", + "_description": "Display help on the specified command (or all commands)." + }, + "info": { + "_syntax": "info <target>", + "_description": "Displays information on <target>." + }, + "init": { + "_syntax": "init [<storage-dir>]", + "_description": "Initializes a project in the current directory (using <storage-dir> as storage directory)." + }, + "list": { + "_syntax": "list", + "_description": "Lists all dependencies (recursively) of the current project." + }, + "def": { + "_syntax": "def (command|target) <id>", + "_description": "Configures a new or existing command or target <id>." + }, + "undef": { + "_syntax": "undef (command|target) <id>", + "_description": "Unmaps the previously-mapped command or target <id>." + } +}
A src/plspkg/messaging.nim

@@ -0,0 +1,103 @@

+import + terminal, + strutils, + sequtils + +import + minimline + +type + TreeNode* = object + label: string + nodes: seq[TreeNode] + +proc foreground(str: string, color: ForegroundColor) = + stdout.setForegroundColor(color) + stdout.write(str) + resetAttributes() + +proc printGreen*(str: string) = + foreground(str, fgGreen) + +proc printRed*(str: string) = + foreground(str, fgRed) + +proc printYellow*(str: string) = + foreground(str, fgYellow) + +proc printBlue*(str: string) = + foreground(str, fgBlue) + +proc confirm*(q: string): bool = + printYellow("(!) " & q & " [y/n]: ") + var ed = initEditor() + let answer = ed.readLine().toLowerAscii[0] + if answer == 'y': + return true + return false + +proc printValue*(key, value: string) = + printBlue(" -> $1: " % key) + printGreen(value) + resetAttributes() + stdout.write("\n") + +proc editValue*(key: string, value = ""): string = + printBlue(" -> $1: " % key) + var ed = initEditor() + result = ed.edit(value) + +proc printDeleted*(label, value: string) = + printRed("--- ") + echo label & ": " & value + +proc printAdded*(label, value: string) = + printGreen("+++ ") + echo label & ": " & value + +when defined(windows): + proc ch(s: string): string = + case s: + of "└": + return $(192.chr) + of "├": + return $(195.chr) + of "─": + return $(196.chr) + of "┬": + return $(194.chr) + of "│": + return $(179.chr) +else: + proc ch(s: string): string = + return s + +proc newTreeNode*(label: string): TreeNode = + result.label = label + result.nodes = newSeq[TreeNode]() + +proc add*(x: var TreeNode, node: TreeNode) = + x.nodes.add(node) + +proc tree*(node: TreeNode, prefix = ""): string = + let splitterPart = if node.nodes.len > 0: ch("│") else: "" + let splitter = "\n" & prefix & splitterPart & "" + return prefix & [node.label].join(splitter) & "\n" & node.nodes.map(proc(x: TreeNode): string = + let ix = node.nodes.find(x) + let last = node.nodes.len-1 == ix + let more = x.nodes.len > 0 + let prefixPart = if last: " " else: ch("│") + let newPrefix = prefix & prefixPart & " " + let lastPart = if last: ch("└") else: ch("├") + let morePart = if more: ch("┬") else: ch("─") + let rec = tree(x, newPrefix) + var offset = if ch("└").len > 1: 3 else: 1 + var endSpace = "" + if lastPart == ch("└"): + offset = 2 + endSpace = " " + return prefix & lastPart & ch("─") & morePart & endSpace & rec[prefix.len+offset .. rec.len-1] + ).join("") + + +
A src/plspkg/minimline.nim

@@ -0,0 +1,297 @@

+import + critbits, + terminal, + std/exitprocs + +exitprocs.addExitProc(resetAttributes) + +when defined(windows): + proc getchr*(): cint {.header: "<conio.h>", importc: "_getch".} + proc putchr*(c: cint): cint {.discardable, header: "<conio.h>", importc: "_putch".} +else: + proc putchr*(c: cint) = + stdout.write(c.chr) + + proc getchr*(): cint = + return getch().ord.cint + +# Types + +type + Key* = int + KeySeq* = seq[Key] + KeyCallback* = proc(ed: var LineEditor) + LineError* = ref Exception + LineEditorError* = ref Exception + Line = object + text: string + position: int + LineEditor* = object + line: Line + +# Internal Methods + +proc empty(line: Line): bool = + return line.text.len <= 0 + +proc full(line: Line): bool = + return line.position >= line.text.len + +proc first(line: Line): int = + if line.empty: + raise LineError(msg: "Line is empty!") + return 0 + +proc last(line: Line): int = + if line.empty: + raise LineError(msg: "Line is empty!") + return line.text.len-1 + +proc fromStart(line: Line): string = + if line.empty: + return "" + return line.text[line.first..line.position-1] + +proc toEnd(line: Line): string = + if line.empty: + return "" + return line.text[line.position..line.last] + +proc back*(ed: var LineEditor, n=1) = + if ed.line.position <= 0: + return + stdout.cursorBackward(n) + ed.line.position = ed.line.position - n + +proc forward*(ed: var LineEditor, n=1) = + if ed.line.full: + return + stdout.cursorForward(n) + ed.line.position += n + +# Public API + +proc deletePrevious*(ed: var LineEditor) = + if ed.line.position <= 0: + return + if not ed.line.empty: + if ed.line.full: + stdout.cursorBackward + putchr(32) + stdout.cursorBackward + ed.line.position.dec + ed.line.text = ed.line.text[0..ed.line.last-1] + else: + let rest = ed.line.toEnd & " " + ed.back + for i in rest: + putchr i.ord.cint + ed.line.text = ed.line.fromStart & ed.line.text[ed.line.position+1..ed.line.last] + stdout.cursorBackward(rest.len) + +proc deleteNext*(ed: var LineEditor) = + if not ed.line.empty: + if not ed.line.full: + let rest = ed.line.toEnd[1..^1] & " " + for c in rest: + putchr c.ord.cint + stdout.cursorBackward(rest.len) + ed.line.text = ed.line.fromStart & ed.line.toEnd[1..^1] + +proc printChar*(ed: var LineEditor, c: int) = + if ed.line.full: + putchr(c.cint) + ed.line.text &= c.chr + ed.line.position += 1 + else: + putchr(c.cint) + let rest = ed.line.toEnd + ed.line.text.insert($c.chr, ed.line.position) + ed.line.position += 1 + for j in rest: + putchr(j.ord.cint) + ed.line.position += 1 + ed.back(rest.len) + +proc print*(ed: var LineEditor, str: string) = + for c in str: + ed.printChar(c.ord) + +# Character sets +const + CTRL* = {0 .. 31} + DIGIT* = {48 .. 57} + LETTER* = {65 .. 122} + UPPERLETTER* = {65 .. 90} + LOWERLETTER* = {97 .. 122} + PRINTABLE* = {32 .. 126} +when defined(windows): + const + ESCAPES* = {0, 22, 224} +else: + const + ESCAPES* = {27} + + +# Key Mappings +var KEYMAP*: CritBitTree[KeyCallBack] + +KEYMAP["backspace"] = proc(ed: var LineEditor) = + ed.deletePrevious() +KEYMAP["delete"] = proc(ed: var LineEditor) = + ed.deleteNext() +KEYMAP["down"] = proc(ed: var LineEditor) = + discard +KEYMAP["up"] = proc(ed: var LineEditor) = + discard +KEYMAP["left"] = proc(ed: var LineEditor) = + ed.back() +KEYMAP["right"] = proc(ed: var LineEditor) = + ed.forward() +KEYMAP["ctrl+c"] = proc(ed: var LineEditor) = + quit(0) +KEYMAP["ctrl+d"] = proc(ed: var LineEditor) = + quit(0) + +# Key Names +var KEYNAMES*: array[0..31, string] +KEYNAMES[1] = "ctrl+a" +KEYNAMES[2] = "ctrl+b" +KEYNAMES[3] = "ctrl+c" +KEYNAMES[4] = "ctrl+d" +KEYNAMES[5] = "ctrl+e" +KEYNAMES[6] = "ctrl+f" +KEYNAMES[7] = "ctrl+g" +KEYNAMES[8] = "ctrl+h" +KEYNAMES[9] = "ctrl+i" +KEYNAMES[9] = "tab" +KEYNAMES[10] = "ctrl+j" +KEYNAMES[11] = "ctrl+k" +KEYNAMES[12] = "ctrl+l" +KEYNAMES[13] = "ctrl+m" +KEYNAMES[14] = "ctrl+n" +KEYNAMES[15] = "ctrl+o" +KEYNAMES[16] = "ctrl+p" +KEYNAMES[17] = "ctrl+q" +KEYNAMES[18] = "ctrl+r" +KEYNAMES[19] = "ctrl+s" +KEYNAMES[20] = "ctrl+t" +KEYNAMES[21] = "ctrl+u" +KEYNAMES[22] = "ctrl+v" +KEYNAMES[23] = "ctrl+w" +KEYNAMES[24] = "ctrl+x" +KEYNAMES[25] = "ctrl+y" +KEYNAMES[26] = "ctrl+z" + +# Key Sequences +var KEYSEQS*: CritBitTree[KeySeq] + +when defined(windows): + KEYSEQS["up"] = @[224, 72] + KEYSEQS["down"] = @[224, 80] + KEYSEQS["right"] = @[224, 77] + KEYSEQS["left"] = @[224, 75] + KEYSEQS["insert"] = @[224, 82] + KEYSEQS["delete"] = @[224, 83] +else: + KEYSEQS["up"] = @[27, 91, 65] + KEYSEQS["down"] = @[27, 91, 66] + KEYSEQS["right"] = @[27, 91, 67] + KEYSEQS["left"] = @[27, 91, 68] + KEYSEQS["home"] = @[27, 91, 72] + KEYSEQS["end"] = @[27, 91, 70] + KEYSEQS["insert"] = @[27, 91, 50, 126] + KEYSEQS["delete"] = @[27, 91, 51, 126] + +proc readLine*(ed: var LineEditor, prompt="", hidechars = false, reset = true): string = + stdout.write(prompt) + if reset: + ed.line = Line(text: "", position: 0) + var c = -1 # Used to manage completions + var esc = false + while true: + var c1: int + if c > 0: + c1 = c + c = -1 + else: + c1 = getchr() + if esc: + esc = false + continue + elif c1 in {10, 13}: + stdout.write("\n") + return ed.line.text + elif c1 in {8, 127}: + KEYMAP["backspace"](ed) + elif c1 in PRINTABLE: + if hidechars: + putchr('*'.ord.cint) + ed.line.text &= c1.chr + ed.line.position.inc + else: + ed.printChar(c1) + elif c1 in ESCAPES: + var s = newSeq[Key](0) + s.add(c1) + let c2 = getchr() + s.add(c2) + if s == KEYSEQS["left"]: + KEYMAP["left"](ed) + elif s == KEYSEQS["right"]: + KEYMAP["right"](ed) + elif s == KEYSEQS["up"]: + KEYMAP["up"](ed) + elif s == KEYSEQS["down"]: + KEYMAP["down"](ed) + elif s == KEYSEQS["home"]: + KEYMAP["home"](ed) + elif s == KEYSEQS["end"]: + KEYMAP["end"](ed) + elif s == KEYSEQS["delete"]: + KEYMAP["delete"](ed) + elif s == KEYSEQS["insert"]: + KEYMAP["insert"](ed) + elif c2 == 91: + let c3 = getchr() + s.add(c3) + if s == KEYSEQS["right"]: + KEYMAP["right"](ed) + elif s == KEYSEQS["left"]: + KEYMAP["left"](ed) + elif s == KEYSEQS["up"]: + KEYMAP["up"](ed) + elif s == KEYSEQS["down"]: + KEYMAP["down"](ed) + elif s == KEYSEQS["home"]: + KEYMAP["home"](ed) + elif s == KEYSEQS["end"]: + KEYMAP["end"](ed) + elif c3 in {50, 51}: + let c4 = getchr() + s.add(c4) + if c4 == 126 and c3 == 50: + KEYMAP["insert"](ed) + elif c4 == 126 and c3 == 51: + KEYMAP["delete"](ed) + elif KEYMAP.hasKey(KEYNAMES[c1]): + KEYMAP[KEYNAMES[c1]](ed) + else: + # Assuming unhandled two-values escape sequence; do nothing. + if esc: + esc = false + continue + else: + esc = true + continue + +proc password*(ed: var LineEditor, prompt=""): string = + return ed.readLine(prompt, true) + +proc initEditor*(): LineEditor = + result.line.text = "" + result.line.position = 0 + +proc edit*(ed: var LineEditor, value: string): string = + ed.print value + return ed.readLine("", false, false)
A src/plspkg/pls.json

@@ -0,0 +1,4 @@

+{ + "commands": {}, + "targets": {} +}
A src/plspkg/plslogger.nim

@@ -0,0 +1,72 @@

+import + logging, + strutils, + terminal, + std/exitprocs + +exitprocs.addExitProc(resetAttributes) + +type + PlsLogger* = ref object of Logger + +proc logPrefix*(level: Level): tuple[msg: string, color: ForegroundColor] = + case level: + of lvlDebug: + return ("---", fgMagenta) + of lvlInfo: + return ("(i)", fgCyan) + of lvlNotice: + return (" ", fgWhite) + of lvlWarn: + return ("(!)", fgYellow) + of lvlError: + return ("(!)", fgRed) + of lvlFatal: + return ("(x)", fgRed) + else: + return (" ", fgWhite) + +method log*(logger: PlsLogger; level: Level; args: varargs[string, `$`]) = + var f = stdout + if level >= getLogFilter() and level >= logger.levelThreshold: + if level >= lvlWarn: + f = stderr + let ln = substituteLog(logger.fmtStr, level, args) + let prefix = level.logPrefix() + f.setForegroundColor(prefix.color) + f.write(prefix.msg) + f.write(ln) + resetAttributes() + f.write("\n") + if level in {lvlError, lvlFatal}: flushFile(f) + +proc newPlsLogger*(levelThreshold = lvlAll; fmtStr = " "): PlsLogger = + new result + result.fmtStr = fmtStr + result.levelThreshold = levelThreshold + +proc getLogLevel*(): string = + return LevelNames[getLogFilter()].toLowerAscii + +proc setLogLevel*(val: var string): string {.discardable.} = + var lvl: Level + case val: + of "debug": + lvl = lvlDebug + of "info": + lvl = lvlInfo + of "notice": + lvl = lvlNotice + of "warn": + lvl = lvlWarn + of "error": + lvl = lvlError + of "fatal": + lvl = lvlFatal + of "none": + lvl = lvlNone + else: + val = "warn" + lvl = lvlWarn + setLogFilter(lvl) + return val
A src/plspkg/project.nim

@@ -0,0 +1,159 @@

+import + os, + json, + logging, + strutils, + sequtils, + pegs + +type + PlsProject* = object + dir*: string + commands*: JsonNode + targets*: JsonNode + tasklists*: JsonNode + +const plsTpl* = "pls.json".slurp +const systemHelp = "help.json".slurp + +let placeholder = peg"'{{' {[^}]+} '}}'" + +proc newPlsProject*(dir: string): PlsProject = + result.dir = dir + +proc configFile*(prj: PlsProject): string = + return prj.dir/"pls.json" + +proc configured*(prj: PlsProject): bool = + return fileExists(prj.configFile) + +proc init*(prj: var PlsProject) = + var o = parseJson(plsTpl) + prj.configFile.writeFile(o.pretty) + +proc load*(prj: var PlsProject) = + if not prj.configFile.fileExists: + fatal "Project not initialized - configuration file not found." + quit(10) + let cfg = prj.configFile.parseFile + prj.commands = cfg["commands"] + prj.targets = cfg["targets"] + if cfg.hasKey("dir"): + prj.dir = cfg["dir"].getStr + +proc help*(prj: var PlsProject): JsonNode = + result = systemHelp.parseJson + if prj.configured: + prj.load + for k, v in prj.tasklists.pairs: + let syntax = "$$$1" % k + let description = "Executes: $1" % v.elems.mapIt(it.getStr).join(", ") + result["$"&k] = (""" + { + "_syntax": "$1", + "_description": "$2" + } + """ % [syntax, description]).parseJson + for k, v in prj.commands.pairs: + if v.hasKey("_syntax") and v.hasKey("_description"): + result[k] = (""" + { + "_syntax": "$1", + "_description": "$2" + } + """ % [v["_syntax"].getStr, v["_description"].getStr]).parseJson + +proc save*(prj: PlsProject) = + var o = newJObject() + o["commands"] = %prj.commands + o["targets"] = %prj.targets + prj.configFile.writeFile(o.pretty) + +proc def*(prj: var PlsProject, alias: string, props: var JsonNode) = + for k, v in props.mpairs: + if v == newJNull(): + props.delete(k) + prj.load + if not prj.targets.hasKey alias: + notice "Adding target '$1'..." % alias + prj.targets[alias] = newJObject() + prj.targets[alias]["name"] = %alias + else: + notice "Updating target '$1'..." % alias + prj.targets[alias] = newJObject() + for key, val in props.pairs: + prj.targets[alias][key] = val + notice " $1: $2" % [key, $val] + prj.save + notice "Target '$1' saved." % alias + +proc undef*(prj: var PlsProject, alias: string) = + prj.load + prj.targets.delete(alias) + prj.save + notice "Target '$1' removed." % alias + +proc lookupCommand(prj: PlsProject, command: string, props: seq[string], cmd: var JsonNode): bool = + if not prj.commands.hasKey command: + warn "Command '$1' not found" % command + return + var cmds = prj.commands[command] + var score = 0 + # Cycle through command definitions + for key, val in cmds: + if key == "_syntax" or key == "_description": + continue + var params = key.split("+") + # Check if all params are available + var match = params.all do (x: string) -> bool: + props.contains(x) + if match and params.len > score: + score = params.len + cmd = val + return score > 0 + +proc execute*(prj: var PlsProject, command, alias: string): int = + prj.load + if not prj.targets.hasKey alias: + warn "Package definition '$1' not found within $2. Nothing to do." % [alias, prj.dir] + return + notice "$1: $2" % [command, alias] + let target = prj.targets[alias] + var keys = newSeq[string](0) + for key, val in target.pairs: + keys.add key + var res: JsonNode + var cmd: string + var pwd = prj.dir + if target.hasKey("dir"): + pwd = target["dir"].getStr + if prj.lookupCommand(command, keys, res): + cmd = res["cmd"].getStr.replace(placeholder) do (m: int, n: int, c: openArray[string]) -> string: + return target[c[0]].getStr + if res.hasKey("pwd"): + pwd = res["pwd"].getStr.replace(placeholder) do (m: int, n: int, c: openArray[string]) -> string: + return target[c[0]].getStr + pwd = prj.dir/pwd + notice "Executing: $1" % cmd + pwd.createDir() + pwd.setCurrentDir() + result = execShellCmd cmd + else: + debug "Command '$1' not available for target '$2'" % [command, alias] + setCurrentDir(prj.dir) + +proc executeRec*(prj: var PlsProject, command, alias: string) = + prj.load + let pwd = getCurrentDir() + var dir = alias + if (execute(prj, command, alias) != 0): + return + if prj.targets[alias].hasKey("dir"): + dir = prj.targets[alias]["dir"].getStr + var childProj = newPlsProject(pwd/prj.dir/dir) + if childProj.configured: + childProj.load() + setCurrentDir(childProj.dir) + for key, val in childProj.targets.pairs: + childProj.executeRec(command, key) + setCurrentDir(pwd)