13 Commits
0.4.1 ... 0.8.0

5 changed files with 255 additions and 173 deletions

View File

@ -1,179 +1,17 @@
import docopt, json, osproc, posix, nre, streams, strtabs, terminal, unicode import nre, terminal, strtabs, tables, unicode
import os except sleep
import strutils except toUpper, toLower import strutils except toUpper, toLower
import ./cliutils/config
import ./cliutils/daemonize
import ./cliutils/procutil
export config
export daemonize
export procutil
type type
CombinedConfig* = object
docopt*: Table[string, Value]
json*: JsonNode
TermColor = ForegroundColor or BackgroundColor TermColor = ForegroundColor or BackgroundColor
proc getVal*(cfg: CombinedConfig, key, default: string): string =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper)
if cfg.docopt[argKey]: return $cfg.docopt[argKey]
elif existsEnv(envKey): return getEnv(envKey)
elif cfg.json.hasKey(jsonKey):
let node = cfg.json[jsonKey]
case node.kind
of JString: return node.getStr
of JInt: return $node.getInt
of JFloat: return $node.getFloat
of JBool: return $node.getBool
of JNull: return ""
of JObject: return $node
of JArray: return $node
else: return default
proc loadEnv*(): StringTableRef =
result = newStringTable()
for k, v in envPairs():
result[k] = v
## Process execution
type HandleProcMsgCB* = proc (outMsg: TaintedString,
errMsg: TaintedString, cmd: string): void
proc sendMsg*(h: HandleProcMsgCB, outMsg: TaintedString,
errMsg: TaintedString = nil, cmd: string = ""): void =
if h != nil: h(outMsg, errMsg, cmd)
proc makeProcMsgHandler*(outSink, errSink: File, prefixCmd: bool = true): HandleProcMsgCB =
result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} =
let prefix = if cmd == nil or not prefixCmd: "" else: cmd & ": "
if outMsg != nil: outSink.writeLine(prefix & outMsg)
if errMsg != nil: errSink.writeLine(prefix & errMsg)
proc makeProcMsgHandler*(outSink, errSink: Stream, prefixCmd: bool = true): HandleProcMsgCB =
result = proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} =
let prefix = if cmd == nil or not prefixCmd: "" else: cmd & ": "
if outMsg != nil: outSink.writeLine(prefix & outMsg)
if errMsg != nil: errSink.writeLine(prefix & errMsg)
proc combineProcMsgHandlers*(a, b: HandleProcMsgCB): HandleProcMsgCB =
if a == nil: result = b
elif b == nil: result = a
else:
result = proc(cmd: string, outMsg, errMsg: TaintedString): void =
a(cmd, outMsg, errMsg)
b(cmd, outMsg, errMsg)
proc waitFor*(p: Process, msgCB: HandleProcMsgCB, procCmd: string = ""): int =
var pout = outputStream(p)
var perr = errorStream(p)
var line = newStringOfCap(120).TaintedString
while true:
if pout.readLine(line):
msgCB.sendMsg(line, nil, procCmd)
elif perr.readLine(line):
msgCB.sendMsg(nil, line, procCmd)
else:
result = peekExitCode(p)
if result != -1: break
close(p)
proc exec*(command: string, workingDir: string = "",
args: openArray[string] = [], env: StringTableRef = nil,
options: set[ProcessOption] = {poUsePath},
msgCB: HandleProcMsgCB = nil): int
{.tags: [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} =
var p = startProcess(command, workingDir, args, env, options)
result = waitFor(p, msgCB, command)
proc execWithOutput*(command: string, workingDir:string = "",
args: openArray[string] = [], env: StringTableRef = nil,
options: set[ProcessOption] = {poUsePath},
msgCB: HandleProcMsgCB = nil):
tuple[output: TaintedString, error: TaintedString, exitCode: int] =
result = (TaintedString"", TaintedString"", -1)
var outSeq, errSeq: seq[TaintedString]
outSeq = @[]; errSeq = @[]
var outputCollector = combineProcMsgHandlers(msgCB,
proc(outMsg, errMsg: TaintedString, cmd: string): void {.closure.} =
if outMsg != nil: outSeq.add(outMsg)
if errMsg != nil: errSeq.add(errMsg))
result[2] = exec(command, workingDir, args, env, options, outputCollector)
result[0] = outSeq.join("\n")
result[1] = errSeq.join("\n")
## Daemonize
var
pidFileInner: string
fi, fo, fe: File
proc onStop(sig: cint) {.noconv.} =
close(fi)
close(fo)
close(fe)
removeFile(pidFileInner)
quit(QuitSuccess)
proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid =
if fileExists(pidfile):
raise newException(IOError, "pidfile " & pidfile & " already exists, daemon already running?")
let pid1 = fork()
# Are we the child process?
if pid1 == 0:
# Yes, so let's get ready to execute the main code given to us.
discard chdir("/")
discard setsid()
discard umask(0)
# Fork again... but I'm not sure why.
let pid2 = fork()
# We don't need the intermediate process, so if we are not the child this
# time, lets just quit
if pid2 > 0: quit(QuitSuccess)
# If we are the grandchild let's set up our environment.
flushFile(stdout)
flushFile(stderr)
if not si.isNil and si != "":
fi = open(si, fmRead)
discard dup2(getFileHandle(fi), getFileHandle(stdin))
if not so.isNil and so != "":
fo = open(so, fmAppend)
discard dup2(getFileHandle(fo), getFileHandle(stdout))
if not se.isNil and so != "":
fe = open(se, fmAppend)
discard dup2(getFileHandle(fe), getFileHandle(stderr))
pidFileInner = pidfile
# Add hooks to cleanup after ourselves when we're asked to die.
signal(SIGINT, onStop)
signal(SIGTERM, onStop)
# Find out what our actual PID is and save it
let childPid = getpid()
writeFile(pidfile, $childPid)
# Finally, execute our main
daemonMain()
return pid1
const termReset* = "\e[0;m" const termReset* = "\e[0;m"
proc termColor*(color: TermColor, bright, bold = false): string = proc termColor*(color: TermColor, bright, bold = false): string =
@ -183,3 +21,30 @@ proc termColor*(color: TermColor, bright, bold = false): string =
proc withColor*(str: string, color: TermColor, bright, bold = false): string = proc withColor*(str: string, color: TermColor, bright, bold = false): string =
return termColor(color, bright, bold) & str return termColor(color, bright, bold) & str
proc stripAnsi*(str: string): string =
let STRIP_ANSI_REGEX = re"\x1B\[([0-9]{1,2}(;[0-9]{1,2})?)?[m|K]"
return str.replace(STRIP_ANSI_REGEX, "")
proc queryParamsToCliArgs*(queryParams: Table[string, string]): seq[string] =
result = @[]
for k,v in queryParams:
# support ?arg1=val1&arg2=val2 -> cmd val1 val2
if k.startsWith("arg"): result.add(v)
else :
result.add("--" & k)
if v != "true": result.add(v) # support things like ?verbose=true -> cmd --verbose
proc queryParamsToCliArgs*(queryParams: StringTableRef): seq[string] =
result = @[]
for k,v in queryParams:
# support ?arg1=val1&arg2=val2 -> cmd val1 val2
if k.startsWith("arg"): result.add(v)
else :
result.add("--" & k)
if v != "true": result.add(v) # support things like ?verbose=true -> cmd --verbose

View File

@ -1,11 +1,11 @@
# Package # Package
version = "0.4.1" version = "0.8.0"
author = "Jonathan Bernard" author = "Jonathan Bernard"
description = "Helper functions for writing command line interfaces." description = "Helper functions for writing command line interfaces."
license = "MIT" license = "MIT"
# Dependencies # Dependencies
requires @["nim >= 0.18.0", "docopt"] requires @["nim >= 1.6.0", "docopt >= 0.6.8"]

78
cliutils/config.nim Normal file
View File

@ -0,0 +1,78 @@
import docopt, json, nre, os, strtabs, strutils
type
CombinedConfig* = object
docopt*: Table[string, Value]
json*: JsonNode
template walkFieldDefs*(t: NimNode, body: untyped) =
let tTypeImpl = t.getTypeImpl
var nodeToItr: NimNode
if tTypeImpl.typeKind == ntyObject: nodeToItr = tTypeImpl[2]
elif tTypeImpl.typeKind == ntyTypeDesc: nodeToItr = tTypeImpl.getType[1].getType[2]
else: error $t & " is not an object or type desc (it's a " & $tTypeImpl.typeKind & ")."
for fieldDef {.inject.} in nodeToItr.children:
# ignore AST nodes that are not field definitions
if fieldDef.kind == nnkIdentDefs:
let fieldIdent {.inject.} = fieldDef[0]
let fieldType {.inject.} = fieldDef[1]
body
elif fieldDef.kind == nnkSym:
let fieldIdent {.inject.} = fieldDef
let fieldType {.inject.} = fieldDef.getType
body
# TODO: Generic config loader
# macro loadConfig*(filePath: string, cfgType: typed, args: Table[string, docopt.Value): untyped =
# result = newNimNode(nnkObjConstr).(cfgType)
#
# var idx = 0
# cfgType.walkFieldDefs:
# let valLookup = quote do: `getVal`(
proc getVal*(cfg: CombinedConfig, key: string): string =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper)
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return $cfg.docopt[argKey]
elif existsEnv(envKey): return getEnv(envKey)
elif cfg.json.hasKey(jsonKey):
let node = cfg.json[jsonKey]
case node.kind
of JString: return node.getStr
of JInt: return $node.getInt
of JFloat: return $node.getFloat
of JBool: return $node.getBool
of JNull: return ""
of JObject: return $node
of JArray: return $node
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
proc getVal*(cfg: CombinedConfig, key, default: string): string =
try: return getVal(cfg, key)
except: return default
proc getJson*(cfg: CombinedConfig, key: string): JsonNode =
let argKey = "--" & key
let envKey = key.replace('-', '_').toUpper
let jsonKey = key.replace(re"(-\w)", proc (m: RegexMatch): string = ($m)[1..1].toUpper)
if cfg.docopt.contains(argKey) and cfg.docopt[argKey]: return parseJson($cfg.docopt[argKey])
elif existsEnv(envKey): return parseJson(getEnv(envKey))
elif cfg.json.hasKey(jsonKey): return cfg.json[jsonKey]
else: raise newException(ValueError, "cannot find a configuration value for \"" & key & "\"")
proc getJson*(cfg: CombinedConfig, key: string, default: JsonNode): JsonNode =
try: return getJson(cfg, key)
except: return default
proc loadEnv*(): StringTableRef =
result = newStringTable()
for k, v in envPairs():
result[k] = v

68
cliutils/daemonize.nim Normal file
View File

@ -0,0 +1,68 @@
import posix
import os except sleep
## Daemonize
var
pidFileInner: string
fi, fo, fe: File
proc onStop(sig: cint) {.noconv.} =
close(fi)
close(fo)
close(fe)
removeFile(pidFileInner)
quit(QuitSuccess)
proc daemonize*(pidfile, si, so, se: string, daemonMain: proc(): void): Pid =
if fileExists(pidfile):
raise newException(IOError, "pidfile " & pidfile & " already exists, daemon already running?")
let pid1 = fork()
# Are we the child process?
if pid1 == 0:
# Yes, so let's get ready to execute the main code given to us.
discard chdir("/")
discard setsid()
discard umask(0)
# Fork again... but I'm not sure why.
let pid2 = fork()
# We don't need the intermediate process, so if we are not the child this
# time, lets just quit
if pid2 > 0: quit(QuitSuccess)
# If we are the grandchild let's set up our environment.
flushFile(stdout)
flushFile(stderr)
if si.len > 0:
fi = open(si, fmRead)
discard dup2(getFileHandle(fi), getFileHandle(stdin))
if so.len > 0:
fo = open(so, fmAppend)
discard dup2(getFileHandle(fo), getFileHandle(stdout))
if se.len > 0:
fe = open(se, fmAppend)
discard dup2(getFileHandle(fe), getFileHandle(stderr))
pidFileInner = pidfile
# Add hooks to cleanup after ourselves when we're asked to die.
signal(SIGINT, onStop)
signal(SIGTERM, onStop)
# Find out what our actual PID is and save it
let childPid = getpid()
writeFile(pidfile, $childPid)
# Finally, execute our main
daemonMain()
return pid1

71
cliutils/procutil.nim Normal file
View File

@ -0,0 +1,71 @@
import osproc, streams, strtabs, strutils
## Process execution
type HandleProcMsgCB* = proc (outMsg: string, errMsg: string, cmd: string): void
proc sendMsg*(h: HandleProcMsgCB, outMsg: string,
errMsg: string = "", cmd: string = ""): void =
if h != nil: h(outMsg, errMsg, cmd)
proc makeProcMsgHandler*(outSink, errSink: File, prefixCmd: bool = true): HandleProcMsgCB =
result = proc(outMsg, errMsg: string, cmd: string): void {.closure.} =
let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": "
if outMsg.len > 0: outSink.writeLine(prefix & outMsg)
if errMsg.len > 0: errSink.writeLine(prefix & errMsg)
proc makeProcMsgHandler*(outSink, errSink: Stream, prefixCmd: bool = true): HandleProcMsgCB =
result = proc(outMsg, errMsg: string, cmd: string): void {.closure.} =
let prefix = if cmd.len == 0 or not prefixCmd: "" else: cmd & ": "
if outMsg.len > 0: outSink.writeLine(prefix & outMsg)
if errMsg.len > 0: errSink.writeLine(prefix & errMsg)
proc combineProcMsgHandlers*(a, b: HandleProcMsgCB): HandleProcMsgCB =
if a == nil: result = b
elif b == nil: result = a
else:
result = proc(cmd: string, outMsg, errMsg: string): void =
a(cmd, outMsg, errMsg)
b(cmd, outMsg, errMsg)
proc waitFor*(p: Process, msgCB: HandleProcMsgCB, procCmd: string = ""): int =
var pout = outputStream(p)
var perr = errorStream(p)
var line = newStringOfCap(120)
while true:
if pout.readLine(line):
msgCB.sendMsg(line, "", procCmd)
elif perr.readLine(line):
msgCB.sendMsg("", line, procCmd)
else:
result = peekExitCode(p)
if result != -1: break
close(p)
proc exec*(command: string, workingDir: string = "",
args: openArray[string] = [], env: StringTableRef = nil,
options: set[ProcessOption] = {poUsePath},
msgCB: HandleProcMsgCB = nil): int
{.tags: [ExecIOEffect, ReadIOEffect, RootEffect], gcsafe.} =
var p = startProcess(command, workingDir, args, env, options)
result = waitFor(p, msgCB, command)
proc execWithOutput*(command: string, workingDir:string = "",
args: openArray[string] = [], env: StringTableRef = nil,
options: set[ProcessOption] = {poUsePath},
msgCB: HandleProcMsgCB = nil):
tuple[output, error: string, exitCode: int] =
result = ("", "", -1)
var outSeq, errSeq: seq[string]
outSeq = @[]; errSeq = @[]
let outputCollector = combineProcMsgHandlers(msgCB,
proc(outMsg, errMsg: string, cmd: string): void {.closure.} =
if outMsg.len > 0: outSeq.add(outMsg)
if errMsg.len > 0: errSeq.add(errMsg))
result[2] = exec(command, workingDir, args, env, options, outputCollector)
result[0] = outSeq.join("\n")
result[1] = errSeq.join("\n")