├── .gitignore ├── .travis.yml ├── README.md ├── examples └── nimbootstrap │ ├── nimbootstrap.nim │ ├── nimbootstrap.nim.cfg │ └── nimbootstrap.nimble ├── nimshell.nimble ├── src ├── nimshell.nim └── nimshell │ └── private │ └── utils.nim └── tests ├── nim.cfg ├── tnimshell.nim └── tutils.nim /.gitignore: -------------------------------------------------------------------------------- 1 | nimcache/ 2 | *.swp 3 | *.exe 4 | 5 | /examples/*/bin 6 | 7 | # Nimble puts test executables with the files! 8 | /tests/* 9 | !/tests/*.nim 10 | !/tests/*.cfg 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: c 2 | 3 | env: 4 | # Nim versions to test against 5 | - CHOOSENIM_CHOOSE_VERSION=devel 6 | - CHOOSENIM_CHOOSE_VERSION=0.19.4 7 | - CHOOSENIM_CHOOSE_VERSION=0.18.0 8 | - CHOOSENIM_CHOOSE_VERSION=0.17.2 9 | matrix: 10 | allow_failures: 11 | # devel branch is often broken 12 | - env: CHOOSENIM_CHOOSE_VERSION=devel 13 | 14 | install: 15 | - curl https://nim-lang.org/choosenim/init.sh -sSf | sh -s -- -y 16 | - export PATH=~/.nimble/bin:$PATH 17 | - nimble update 18 | - nimble install -d -y 19 | 20 | before_script: 21 | - set -e 22 | - export PATH=~/.nimble/bin:$PATH 23 | - export CHOOSENIM_NO_ANALYTICS=1 24 | script: 25 | - nimble test 26 | # Make sure the example compiles. 27 | - nimble develop 28 | - cd examples/nimbootstrap && nimble build 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nimshell [![nimble](https://raw.githubusercontent.com/yglukhov/nimble-tag/master/nimble.png)](https://github.com/yglukhov/nimble-tag) 2 | 3 | [![Build Status](https://travis-ci.org/vegansk/nimshell.svg?branch=master)](https://travis-ci.org/vegansk/nimshell) 4 | 5 | Nim shell scripting library 6 | -------------------------------------------------------------------------------- /examples/nimbootstrap/nimbootstrap.nim: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env nimrunner 2 | import nimshell, os 3 | 4 | let usageHelp = """ 5 | Nim bootstrap script 6 | 7 | Usage: 8 | nimboostrap [] 9 | 10 | Options: 11 | Nim installation path 12 | Nimble installation path 13 | """ 14 | 15 | if(paramCount() < 1 or paramCount() > 3): 16 | quit usageHelp 17 | 18 | if not ?(which "git"): quit "Git must be installed!" 19 | 20 | let NIM_DIR = paramStr(1) 21 | let NIMBLE_DIR = if paramCount() > 1: paramStr(2) else: "" 22 | 23 | # First install nim 24 | createDir NIM_DIR 25 | >>! cmd"git clone -b devel --depth 1 git://github.com/vegansk/Nim.git $NIM_DIR" 26 | setCurrentDir NIM_DIR 27 | >>! cmd"git clone -b devel --depth 1 git://github.com/nim-lang/csources" 28 | setCurrentDir "csources" 29 | >>! sh("." / "build") 30 | setCurrentDir ParDir 31 | >>! ("bin" / exe"nim" & " c koch.nim") 32 | >>! ("." / exe"koch" & " boot -d:release") 33 | 34 | echo "Don't forget to add " & (NIM_DIR / "bin") & " to your PATH" 35 | 36 | if NIMBLE_DIR == "": 37 | quit() 38 | 39 | # Modify path to install nimble 40 | putEnv("PATH", (NIM_DIR / "bin") & $PathSep & getEnv("PATH")) 41 | 42 | # Then install nimble 43 | createDir NIMBLE_DIR 44 | setCurrentDir NIMBLE_DIR 45 | when defined(windows): 46 | >>! cmd"git clone --depth 1 https://github.com/nim-lang/nimble.git $NIMBLE_DIR" 47 | >>! "nim c -r src/nimble -y install" 48 | 49 | # Modify path to use nimble 50 | putEnv("PATH", (getHomeDir() / ".nimble" / "bin") & $PathSep & getEnv("PATH")) 51 | 52 | # Install nimrunner 53 | let NIMRUNNER_DIR = mktemp() 54 | block: 55 | defer: 56 | try: 57 | removeDir NIMRUNNER_DIR 58 | except: discard 59 | >>! cmd"git clone https://github.com/vegansk/nimshell $NIMRUNNER_DIR" 60 | setCurrentDir NIMRUNNER_DIR 61 | >>! cmd"nimble -y install" 62 | setCurrentDir NIMRUNNER_DIR / "examples" / "nimrunner" 63 | >>! cmd"nimble -y install" 64 | 65 | echo "Don't forget to add " & (getHomeDir() / ".nimble" / "bin") & " to your PATH" 66 | -------------------------------------------------------------------------------- /examples/nimbootstrap/nimbootstrap.nim.cfg: -------------------------------------------------------------------------------- 1 | passC:"-static" 2 | passL:"-static" 3 | define:release 4 | -------------------------------------------------------------------------------- /examples/nimbootstrap/nimbootstrap.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.1.0" 4 | author = "Anatoly Galiulin" 5 | description = "Download and install script for nim language" 6 | license = "MIT" 7 | bin = @["nimbootstrap"] 8 | binDir = "bin" 9 | 10 | # Deps 11 | 12 | requires "nim >= 0.10.2", "nimshell" 13 | -------------------------------------------------------------------------------- /nimshell.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | version = "0.0.3" 3 | author = "Anatoly Galiulin" 4 | description = "Library for shell scripting in nim" 5 | license = "MIT" 6 | 7 | srcDir = "src" 8 | 9 | # Deps 10 | requires "nim >= 0.17.2" 11 | -------------------------------------------------------------------------------- /src/nimshell.nim: -------------------------------------------------------------------------------- 1 | {.push warnings:off hints:off.} 2 | import os, osproc, macros, parseutils, sequtils, streams, strutils, options, oids 3 | {.pop.} 4 | 5 | import ./nimshell/private/utils 6 | 7 | type 8 | Command* = ref object 9 | value: string 10 | process: Option[Process] 11 | stdout: Option[Stream] 12 | 13 | proc newCommand*(cmd: string): Command = 14 | new(result) 15 | result.value = cmd 16 | result.process = none(Process) 17 | result.stdout = none(Stream) 18 | 19 | proc close*(cmd: Command) = 20 | map(cmd.process, close) 21 | 22 | var 23 | lastExitCode {.threadvar.}: int 24 | 25 | proc `>>?`*(c: Command): int 26 | 27 | proc exitCode*(c: Command): int = 28 | if isNone(c.process): 29 | lastExitCode = >>? c 30 | else: 31 | lastExitCode = waitForExit(c.process.get()) 32 | result = lastExitCode 33 | 34 | proc `$?`*(): int = lastExitCode 35 | 36 | macro cmd*(text: string{lit}): Command = 37 | var nodes: seq[NimNode] = @[] 38 | for k, v in text.strVal.interpolatedFragments: 39 | if k == ikStr or k == ikDollar: 40 | nodes.add(newLit(v)) 41 | else: 42 | nodes.add(parseExpr("$(" & v & ")")) 43 | var str = newNimNode(nnkStmtList).add( 44 | foldr(nodes, a.infix("&", b))) 45 | result = newCall(bindSym"newCommand", str) 46 | 47 | when not defined(shellNoImplicits): 48 | converter stringToCommand*(s: string): Command = newCommand(s) 49 | converter commandToBool*(c: Command): bool = c.exitCode() == 0 50 | 51 | proc `&>`*(c: Command, s: Stream): Command = 52 | assert isNone(c.process) 53 | c.stdout = some(s) 54 | result = c 55 | 56 | proc execCommand*(c: Command, options: set[ProcessOption] = {}) = 57 | assert isNone(c.process) 58 | var opt = options 59 | var line = c.value 60 | if isNone(c.stdout): 61 | opt = opt + {poParentStreams} 62 | when defined(windows): 63 | line = "cmd /q /d /c " & line 64 | c.process = some(startProcess(line, "", [], nil, opt + {poEvalCommand, poUsePath, poStdErrToStdOut})) 65 | if isSome(c.stdout): 66 | c.process.get().outputStream().copyStream(c.stdout.get()) 67 | 68 | proc `>>?`*(c: Command): int = 69 | if isNone(c.process): 70 | execCommand(c) 71 | result = c.exitCode() 72 | 73 | proc `>>`*(c: Command) = 74 | discard >>? c 75 | 76 | proc `>>!`*(c: Command) = 77 | let res = >>? c 78 | if res != 0: 79 | write(stderr, "Error code " & $res & " while executing command: " & c.value & "\n") 80 | quit(res) 81 | 82 | proc devNull*(): Stream {.inline.} = newDevNullStream() 83 | 84 | template SCRIPTDIR*: string = 85 | parentDir(instantiationInfo(0, true).filename) 86 | 87 | proc `$`*(c: Command): string = 88 | let sout = newStringStream() 89 | >> (c &> sout) 90 | result = sout.data.strip 91 | 92 | proc `$$`*(c: Command): seq[string] = 93 | result = ($c).splitLines() 94 | if result[0] == "": 95 | result = @[] 96 | 97 | #################################################################################################### 98 | # Helpers 99 | 100 | proc mktemp*(): string = 101 | result = getTempDir() / $genOid() 102 | createDir(result) 103 | 104 | proc `?`*(s: string): bool = not (s.strip == "") 105 | 106 | proc findInPath(name: string): string = 107 | if existsFile name: 108 | return name 109 | for p in getEnv("PATH").split(PathSep): 110 | if existsFile(p / name): 111 | return (p / name) 112 | return "" 113 | 114 | when defined(windows): 115 | proc which*(name: string): string = 116 | result = findInPath name 117 | if not ?result: 118 | result = findInPath(name & ".exe") 119 | 120 | proc sh*(name: string): string = name & ".bat" 121 | proc exe*(name: string): string = name & ".exe" 122 | 123 | elif defined(posix): 124 | proc which*(name: string): string = findInPath name 125 | 126 | proc sh*(name: string): string = name & ".sh" 127 | proc exe*(name: string): string = name 128 | -------------------------------------------------------------------------------- /src/nimshell/private/utils.nim: -------------------------------------------------------------------------------- 1 | import streams 2 | 3 | type 4 | DevNullStreamObj = object of StreamObj 5 | DevNullStream* = ref DevNullStreamObj 6 | 7 | when defined(windows): 8 | const 9 | IO_BUFF_SIZE = 1 10 | else: 11 | const 12 | IO_BUFF_SIZE = 4096 13 | 14 | 15 | proc nullClose(s: Stream) = discard 16 | proc nullAtEnd(s: Stream): bool = true 17 | proc nullSetPosition(s: Stream; pos: int) = discard 18 | proc nullGetPosition(s: Stream): int = -1 19 | proc nullReadData(s: Stream; buffer: pointer; bufLen: int): int = 0 20 | proc nullWriteData(s: Stream; buffer: pointer; bufLen: int) = discard 21 | proc nullFlush(s: Stream) = discard 22 | 23 | proc newDevNullStream*(): DevNullStream = 24 | new(result) 25 | result.closeImpl = nullClose 26 | result.atEndImpl = nullAtEnd 27 | result.setPositionImpl = nullSetPosition 28 | result.getPositionImpl = nullGetPosition 29 | result.readDataImpl = nullReadData 30 | result.writeDataImpl = nullWriteData 31 | result.flushImpl = nullFlush 32 | 33 | proc copyStream*(sin: Stream, sout: Stream) = 34 | var arr: array[0..IO_BUFF_SIZE-1, uint8] 35 | while not sin.atEnd: 36 | let len = sin.readData(addr(arr), arr.len) 37 | if len > 0: 38 | sout.writeData(addr(arr), len) 39 | -------------------------------------------------------------------------------- /tests/nim.cfg: -------------------------------------------------------------------------------- 1 | --path:"../src/" -------------------------------------------------------------------------------- /tests/tnimshell.nim: -------------------------------------------------------------------------------- 1 | import options, sequtils, strutils, unittest 2 | 3 | # Include since we test private APIs 4 | include ./nimshell 5 | 6 | suite "nimshell": 7 | when defined(windows): 8 | test "Windows tests": 9 | var v = cmd"""dir ${($$"dir /b /ad c:\\").mapIt(string, "\"c:\\" & it & "\"").join(" ")}""" 10 | >> v 11 | check: true == ?v.process 12 | 13 | for v in $$"dir /b c:\\": 14 | echo "\"" & v & "\"" 15 | 16 | elif defined(posix): 17 | test "POSIX tests": 18 | var v = cmd"""ls ${($$"ls /").mapIt(string, "/" & it).join(" ")}""" 19 | >> v 20 | check: isSome(v.process) 21 | 22 | for v in $$"ls -lah /": 23 | echo "\"" & v & "\"" 24 | 25 | test "common tests": 26 | check: 0 != >>? ("execInvalidCommand" &> devNull()) 27 | check: "Hello, world!" == $cmd"echo Hello, world!" 28 | 29 | check: true == (cmd"exit 0" and cmd"exit 0") 30 | check: false == (cmd"exit 0" and cmd"exit 123") 31 | check: `$?`() == 123 32 | 33 | check: true == (cmd"exit 1" or cmd"exit 0") 34 | check: false == (cmd"exit 1" or cmd"exit 3") 35 | check: `$?`() == 3 36 | 37 | echo(which "sh") 38 | -------------------------------------------------------------------------------- /tests/tutils.nim: -------------------------------------------------------------------------------- 1 | import unittest, streams 2 | import ./nimshell/private/utils 3 | 4 | suite "copyStream": 5 | test "correctly copies data": 6 | let 7 | input = "Hello, world!" 8 | sin = newStringStream("Hello, world!") 9 | sout = newStringStream() 10 | 11 | sin.copyStream(sout) 12 | 13 | let output = sout.data 14 | check: output == input 15 | --------------------------------------------------------------------------------