├── .github └── workflows │ └── ci.yml ├── README.org ├── changelog.org ├── docs ├── dochack.js ├── index.html ├── nimdoc.out.css ├── shell.html └── shell.idx ├── shell.nim ├── shell.nimble └── tests ├── anotherDir ├── runAnotherTest.nims └── tCorrectDir.nims ├── config.nims ├── tException.nim ├── tExpect.nim ├── tExpect.sh ├── tNimScript.nims └── tShell.nim /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: shell CI 2 | on: 3 | push: 4 | paths: 5 | - 'tests/**' 6 | - '.github/workflows/ci.yml' 7 | pull_request: 8 | paths: 9 | - 'tests/**' 10 | - '.github/workflows/ci.yml' 11 | 12 | jobs: 13 | build: 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | branch: [version-1-2, version-1-4, devel] 18 | target: [linux, macos, windows] 19 | include: 20 | - target: linux 21 | builder: ubuntu-18.04 22 | - target: macos 23 | builder: macos-10.15 24 | - target: windows 25 | builder: windows-2019 26 | name: '${{ matrix.target }} (${{ matrix.branch }})' 27 | runs-on: ${{ matrix.builder }} 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v2 31 | with: 32 | path: shell 33 | 34 | - name: Setup Nim 35 | uses: alaviss/setup-nim@0.1.0 36 | with: 37 | path: nim 38 | version: ${{ matrix.branch }} 39 | 40 | - name: Run tests 41 | shell: bash 42 | run: | 43 | cd shell 44 | nimble test 45 | 46 | - name: Build docs 47 | if: ${{ matrix.docs == 'true' && matrix.target == 'linux' }} 48 | shell: bash 49 | run: | 50 | cd shell 51 | branch=${{ github.ref }} 52 | branch=${branch##*/} 53 | nimble doc --project --outdir:docs \ 54 | '--git.url:https://github.com/${{ github.repository }}' \ 55 | '--git.commit:${{ github.sha }}' \ 56 | "--git.devel:$branch" \ 57 | shell.nim 58 | # Ignore failures for older Nim 59 | cp docs/{the,}index.html || true 60 | 61 | - name: Publish docs 62 | if: > 63 | github.event_name == 'push' && github.ref == 'refs/heads/master' && 64 | matrix.target == 'linux' && matrix.branch == 'devel' 65 | uses: crazy-max/ghaction-github-pages@v1 66 | with: 67 | build_dir: shell/docs 68 | env: 69 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 70 | -------------------------------------------------------------------------------- /README.org: -------------------------------------------------------------------------------- 1 | * shell 2 | [[https://travis-ci.org/Vindaar/shell][https://travis-ci.org/Vindaar/shell.svg?branch=master]] 3 | 4 | A mini Nim DSL to execute shell commands more conveniently. 5 | 6 | ** Usage 7 | With this macro you can simply write 8 | #+BEGIN_SRC nim 9 | shell: 10 | touch foo 11 | mv foo bar 12 | rm bar 13 | #+END_SRC 14 | which is then rewritten to something equivalent to: 15 | #+BEGIN_SRC nim 16 | execShell("touch foo") 17 | execShell("mv foo bar") 18 | execShell("rm bar") 19 | #+END_SRC 20 | where =execShell= is a proc around =startProcess= for normal 21 | compilation and =gorgeEx= when using NimScript. 22 | 23 | Note: When using =NimScript= the given command is prepended by 24 | #+BEGIN_SRC 25 | &"cd {getCurrentDir()} && " 26 | #+END_SRC 27 | in order to switch the evaluation into the directory of the =shell= 28 | call. 29 | The same is achieved on the compiled backend by the =poEvalCommand= 30 | argument to =startProcess=. 31 | 32 | See [[Full expansion of the macro]] below for more details and how to read 33 | the exit code of executed commands. 34 | 35 | Most simple things should work as expected. See below for some known 36 | quirks. 37 | 38 | ** ~one~ and ~pipe~ 39 | 40 | By default each line in the =shell= macro will be handled by a 41 | different call to =execShell=. If you need several commands, which 42 | depend on the state of the previous, you may do so via the =one= 43 | command like so: 44 | #+BEGIN_SRC nim 45 | shell: 46 | one: 47 | mkdir foo 48 | cd foo 49 | touch bar 50 | cd ".." 51 | rm foo/bar 52 | #+END_SRC 53 | 54 | Similar to the =one= command, the =pipe= command exists. This concats 55 | the command via the shell pipe =|=: 56 | #+BEGIN_SRC nim 57 | shell: 58 | pipe: 59 | cat test.txt 60 | head -3 61 | #+END_SRC 62 | will produce: 63 | #+BEGIN_SRC nim 64 | execShell("cat test.txt | head -3") 65 | #+END_SRC 66 | 67 | Both of these can even be combined! 68 | #+BEGIN_SRC nim 69 | shell: 70 | one: 71 | mkdir foo 72 | pushd foo 73 | echo "Hallo\nWorld" > test.txt 74 | pipe: 75 | cat test.txt 76 | grep H 77 | popd 78 | rm foo/test.txt 79 | rmdir foo 80 | #+END_SRC 81 | will work just as expected, echoing =Hallo= in the shell. 82 | 83 | ** Handling programs that require user input 84 | 85 | Some terminal commands will require user input. Starting from version 86 | =v0.5.0=, basic support for a =expect= / =send= feature is 87 | available. It allows for functionality similar to the 88 | [[https://linux.die.net/man/1/expect][=expect(1)= program]]. 89 | 90 | Let's consider two simple examples. Assuming we have a script that 91 | reads from =stdin= such as: 92 | 93 | =tExpect.sh=: 94 | #+begin_src sh 95 | #!/bin/sh 96 | 97 | echo "Hello world. Your name?" 98 | read foo 99 | echo "Your name is" $foo 100 | #+end_src 101 | 102 | calling this script using the =shell= macro would normally 103 | #+begin_src nim 104 | import shell 105 | shell: 106 | ./tExpect.sh 107 | #+end_src 108 | cause the nim program to simply stop upon encountering the =read= 109 | line. 110 | 111 | Now we can do just this: 112 | #+begin_src nim 113 | shell: 114 | ./tExpect.sh 115 | send: "BaFu" 116 | #+end_src 117 | 118 | This goes full rogue mode and just sends "BaFu", regardless what the question is. 119 | 120 | 121 | You can also specifiy what answer you send to which question. 122 | By using =expect= / =send= we can handle it this way: 123 | 124 | #+begin_src nim 125 | import shell 126 | shell: 127 | ./tExpect.sh 128 | expect: "Your name?" 129 | send: "BaFu" 130 | #+end_src 131 | As you notice the =expect= line only contains the second part of the 132 | last printed line of the shell script. That is because the =expect= 133 | functionality tries to match the shell output line by line using one 134 | of the following: 135 | - the =expect= line matches exactly 136 | - the line starts with the =expect= line 137 | - the line ends with the =expect= line (useful for long outputs that 138 | end with ="y/N"= like constructs) 139 | (this may become configurable in the future) 140 | 141 | Another example would be installing a nimble package and dealing with 142 | the possibility of having to upgrade / overwrite package: 143 | #+begin_src nim 144 | import shell 145 | shell: 146 | nimble install foo 147 | expect: "Overwrite? [y/N]" 148 | send: "y" 149 | #+end_src 150 | which handles such situations programatically. 151 | 152 | *Note*: You may have multiple =expect= / =send= constructs in a single 153 | =shell= call. But keep in mind that every =expect= must have a =send=. 154 | 155 | ** Nim symbol quoting 156 | 157 | *NOTE:* In a previous version this was done via accented quotes 158 | =`=. For the old behavior compile with =-d:oldQuote=. 159 | 160 | Another important feature to make this library useful is quoting of 161 | Nim symbols. 162 | 163 | This is handled via parenthesis =()= (if you need to run something in 164 | a subshell unfortunately that will have to be done with an explicit 165 | string now). Any tree in =()= is subject to quoting. That means if an 166 | identifier within =()= is preceded by a =$=, the symbol is 167 | unquoted. Note however that for the moment only a single variable may 168 | be quoted in each =()=. 169 | 170 | The simplest case would be: 171 | #+BEGIN_SRC nim 172 | let name = "Vindaar" 173 | shell: 174 | echo Hello from ($name) 175 | #+END_SRC 176 | which will perform the call: 177 | #+BEGIN_SRC nim 178 | execShell(&"echo Hello from {name}!") 179 | #+END_SRC 180 | and after the call to =strformat.&=: 181 | #+BEGIN_SRC nim 182 | execShell("echo Hello from Vindaar!") 183 | #+END_SRC 184 | 185 | *** Appending to a Nim identifier 186 | 187 | Assuming we have a filename identifier and we want to convert some 188 | image from =png= to =jpg= with image magick. The simplest command 189 | should look like: 190 | #+BEGIN_SRC sh 191 | convert myimage.png myimage.jpg 192 | #+END_SRC 193 | 194 | This can be done in several ways. 195 | 196 | **** Using dot expressions and no string literals: 197 | #+BEGIN_SRC nim 198 | let fname = "myimage" 199 | shell: 200 | convert ($fname).png ($fname).jpg 201 | #+END_SRC 202 | Note that this is a special case. Continuing after a =()= quote 203 | without literal strings will only work for dot expressions. For 204 | instance: 205 | #+BEGIN_SRC nim 206 | let fname = "myimage" 207 | shell: 208 | convert ($fname)".png" ($fname)".jpg" 209 | #+END_SRC 210 | will wrongly be converted to: 211 | #+BEGIN_SRC sh 212 | convert myimage .png myimage .jpg 213 | #+END_SRC 214 | which is obviously not what one would expect. 215 | 216 | **** Using string literals: 217 | #+BEGIN_SRC nim 218 | let fname = "myimage" 219 | shell: 220 | convert ($fname".pdf") ($fname".png") 221 | #+END_SRC 222 | In contrast to the wrong example shown above, this will work as 223 | expected. 224 | 225 | This is especially useful for cases without dot expressions after the 226 | quoted nim identifier. 227 | 228 | *** Appending a Nim identifier to a string literal 229 | 230 | The other example would be appending a Nim identifier to a literal 231 | string. For instance in case we have a filename, which we create at 232 | run time and we wish to hand it to some command which takes an 233 | argument, which is must be given without a space like: 234 | #+BEGIN_SRC sh 235 | ./myBin input --out=output 236 | #+END_SRC 237 | 238 | In this case one of the following ways works: 239 | 240 | **** using =()= after a string literal: 241 | #+BEGIN_SRC nim 242 | let outfile = "myoutput.txt" 243 | shell: 244 | ./myBin input "--out="($outfile) 245 | #+END_SRC 246 | If the =()= appears after the literal we can correctly generate the 247 | string without a space (in comparison to the case presented above when 248 | a string literal follows a =()=). 249 | 250 | **** For more predictable behavior, put the string literal also into 251 | =()=: 252 | #+BEGIN_SRC nim 253 | let outfile = "myoutput.txt" 254 | shell: 255 | ./myBin input ("--out="$outfile) 256 | #+END_SRC 257 | 258 | *** General remark on predictability 259 | 260 | *NOTE:* previously this section said to handle quoting + concatenation 261 | with strings both in the case of with and without space with =()= for 262 | the most predictable behavior. But that was a bad idea from my side! 263 | If you need spaces, simply put it outside the =()= and use a space! 264 | 265 | The =doAssert= below is to be understood in the context of the =shell= 266 | macro. To summarize the above then: 267 | #+BEGIN_SRC nim 268 | let outfile = "myoutput.txt" 269 | doAssert ("--out="$outfile) == &"--out={outfile}" # <- without space, ident after 270 | doAssert "--out" ($outfile) == &"--out {outfile}" # <- with space, ident after 271 | let fname = "myimage" 272 | doAssert ($outfile".jpg") == &"{fname}.jpg" # <- without space, ident first 273 | doAssert ($outfile) "image2" == &"{outfile} image2" # <- without space, ident first 274 | #+END_SRC 275 | 276 | *NOTE 2:* For the moment however, the =()= usage is restricted to a 277 | single string literal (or something that is convertible to a string 278 | via the =stringify= proc) and a single Nim identifier! This 279 | restriction will maybe be removed in the future. 280 | 281 | This syntax also works for more complicated Nim expressions than a 282 | simple identifier: 283 | #+BEGIN_SRC nim 284 | const t = (a: "name", b: 5.5) 285 | doAssert ("--out="$(t.a)) 286 | doAssert ("--out="$t.a) 287 | #+END_SRC 288 | both work. Of course =t= needn't be a tuple. It can also be an object 289 | or even a function call, like for instance extracting a filename 290 | within a call: 291 | #+BEGIN_SRC nim 292 | import os, shell 293 | let path = "/some/user/path/toAFile.txt" 294 | shell: 295 | ./myBin ("--inputFile="$(path.extractFilename)) 296 | #+END_SRC 297 | should produce: 298 | #+BEGIN_SRC sh 299 | ./myBin --inputFile=toAFile.txt 300 | #+END_SRC 301 | 302 | ** Accented quotes 303 | 304 | *NOTE*: In a previous version accented quotes were also used to quote 305 | Nim identifiers. That use case is now handled via parentheses. For the 306 | old behavior compile with =-d:oldQuote=. 307 | 308 | Accented quotes allow you to hand raw strings. 309 | 310 | Note: this has the downside of disallowing =`= as a token to be handed 311 | to the shell. If you want to use the shell's =`=, you need to put the 312 | appropriate command into quotation marks. 313 | 314 | *** Raw strings 315 | If you want to hand a literal string to the shell, you may do so by 316 | putting it into accented quotes: 317 | #+BEGIN_SRC nim 318 | echo `hello` 319 | #+END_SRC 320 | will be rewritten to 321 | #+BEGIN_SRC nim 322 | execShell("echo \"hello\"") 323 | #+END_SRC 324 | 325 | For a string consisting of multiple commands / words, put quotation 326 | marks around it: 327 | #+BEGIN_SRC sh 328 | echo `"Hello from Nim!"` 329 | #+END_SRC 330 | which will then also be rewritten to: 331 | #+BEGIN_SRC nim 332 | execShell("echo \"Hello from Nim!\"") 333 | #+END_SRC 334 | 335 | 336 | ** Assignment of results to Nim variables 337 | 338 | Also useful is assignment of the result of a shell call to a Nim 339 | string. This can be done with the =shellAssign= macro. It is a little 340 | special compared to the =shell= and =shellEcho= macros. It only 341 | supports a single statement (*), which needs to be an assignment of a 342 | shell call of the syntax presented above to a Nim variable, such as: 343 | #+BEGIN_SRC nim 344 | var name = "" 345 | shellAssign: 346 | name = echo Araq 347 | assert name == "Araq" 348 | #+END_SRC 349 | Here the left =name= is the Nim variable (note: this is an exception 350 | of the Nim symbol quoting mentioned above!), whereas the right hand 351 | side is an arbitrary shell call, in this case a simple call to 352 | =echo=. The Nim variable will be assigned the result of the shell 353 | call, by being rewritten to: 354 | #+BEGIN_SRC nim 355 | var name = "" 356 | name = asgnShell("echo Araq") 357 | assert name == "Araq" 358 | #+END_SRC 359 | =asgnShell= is internally called by =execShell= mentioned 360 | above. =asgnShell= itself performs the calls to =execCmdEx= (or =exec= 361 | for NimScript). 362 | 363 | (*): a single statement is not entirely precise, because the =one= and 364 | =pipe= operators can be used in combination with the assignment! For 365 | example the following is also possible: 366 | #+BEGIN_SRC nim 367 | var res = "" 368 | shellAssign: 369 | res = pipe: 370 | seq 0 1 10 371 | tail -3 372 | assert res == "8\n9\n10" 373 | #+END_SRC 374 | 375 | 376 | ** NimScript & Nim compile time (~nimvm~) 377 | 378 | This macro can also be used in NimScript as well as at regular Nim 379 | compile time (i.e. in ~nimvm~ contexts). It uses ~gorgeEx~ in this 380 | case. Some features may not work perfectly. Instead of using the 381 | current working directory, it always starts at the path of the current 382 | project being compiled! So you may need to prepend your own ~cd ~ 383 | depending on where you need to run your commands. 384 | 385 | ** Known issues 386 | 387 | Certain things unfortunately *have* to go into quotation marks. As 388 | seen in the =one= example above, the simple =..= is not allowed. 389 | 390 | Variable assignments in the shell need to be handed via a string 391 | literal: 392 | #+BEGIN_SRC nim 393 | shell: 394 | one: 395 | "a=`echo hello`" 396 | echo $a 397 | #+END_SRC 398 | 399 | Also if you need assignment via `:`, you need to also put it into quotation 400 | marks, as the Nim tree is not unambiguous enough to properly parse the 401 | resulting command. Say you wish to compile a Nim program, you might want to do: 402 | #+BEGIN_SRC nim 403 | shell: 404 | nim c "--out:noTest" test.nim 405 | #+END_SRC 406 | 407 | A slightly different case is assignment via `=`. A single `=` 408 | assignment in a command is possible, but not more. That is due to a 409 | Nim parser limitation (as you cannot have "nested" `nnkAsgn` nodes). 410 | 411 | So this: 412 | #+begin_src sh 413 | shel: 414 | foo --bar --option=value --more 415 | #+end_src 416 | is fine, but this: 417 | #+begin_src sh 418 | shell: 419 | foo --bar --option=value --secondOption=value2 420 | #+end_src 421 | is *not* valid Nim syntax. Fortunately, in most cases the `=` sign is 422 | optional anyway and you may just drop (one or both): 423 | #+begin_src sh 424 | shel: 425 | foo --bar --option value --secondOption value2 426 | #+end_src 427 | is valid and means the same for most programs. 428 | 429 | In general, if in doubt you can just write strings or triple string 430 | (to pass a ="= to the shell). 431 | 432 | ** Full expansion of the macro 433 | 434 | As mentioned at the top of the README, the expansion shown is 435 | simplified (as a matter of fact it was as simple once, but has since 436 | become more complex). 437 | 438 | The full expansion of the first example is: 439 | #+BEGIN_SRC nim 440 | discard block: 441 | var outputStr381052 = "" 442 | var exitCode381051: int 443 | if exitCode381051 == 444 | 0: 445 | let tmp381063 = execShell("touch foo") 446 | outputStr381052 = outputStr381052 & 447 | tmp381063[0] 448 | exitCode381051 = tmp381063[1] 449 | else: 450 | echo "Skipped command `" & "touch foo" & 451 | "` due to failure in previous command!" 452 | if exitCode381051 == 453 | 0: 454 | let tmp381064 = execShell("mv foo bar") 455 | outputStr381052 = outputStr381052 & 456 | tmp381064[0] 457 | exitCode381051 = tmp381064[1] 458 | else: 459 | echo "Skipped command `" & "mv foo bar" & 460 | "` due to failure in previous command!" 461 | if exitCode381051 == 462 | 0: 463 | let tmp381065 = execShell("rm bar") 464 | outputStr381052 = outputStr381052 & 465 | tmp381065[0] 466 | exitCode381051 = tmp381065[1] 467 | else: 468 | echo "Skipped command `" & "rm bar" & 469 | "` due to failure in previous command!" 470 | (outputStr381052, exitCode381051) 471 | #+END_SRC 472 | 473 | As can be seen from the expansion above, successive commands are only 474 | run, if the exit code of the previous command was 0, while the output 475 | is appended to the previous command's output. 476 | 477 | The normal =shell= command discards the return value of the block. If 478 | you want to keep it, use the =shellVerbose= macro: 479 | #+BEGIN_SRC nim 480 | let res = shellVerbose: 481 | someCommand 482 | #+END_SRC 483 | where =res= will be of type =tuple[output: string, exitCode: string]= 484 | according to the expansion above. 485 | 486 | ** Debugging 487 | In order to see what's going on, you can either compile your program 488 | with the =-d:debugShell= flag, which will then echo the rewritten 489 | commands during compilation. 490 | Alternatively in order to avoid calling the commands immediately, you 491 | may use the =shellEcho= macro instead. It simply echoes the commands 492 | that would otherwise be run. 493 | 494 | ** Error reporting 495 | 496 | By default ~shell~ prints output messages to stdout: 497 | 498 | #+BEGIN_SRC nim :exports both :results scalar 499 | import shell 500 | 501 | shell: 502 | ls 503 | #+END_SRC 504 | 505 | #+RESULTS: 506 | : shellCmd: ls 507 | : shell> nim.cfg 508 | : shell> README.org 509 | : shell> shell 510 | : shell> shell.nim 511 | : shell> shell.nim.bin 512 | : shell> shell.nimble 513 | : shell> tests 514 | 515 | What is printed to stdout can be configured by using defines: 516 | 517 | - ~shellNoDebugOutput~ :: Do not print command output 518 | - ~shellNoDebugError~ :: Do not print error output 519 | - ~shellNoDebugCommand~ :: Do not print command being executed 520 | - ~shellNoDebugRuntime~ :: When error occurs do not print failed command 521 | 522 | By default these are disabled - to enable use either 523 | ~-d:shellNoDebug*~ or use the ~{.define(shellNoDebug*).}~ pragma 524 | 525 | #+BEGIN_SRC nim :exports both :results scalar 526 | {.define(shellNoDebugOutput).} 527 | 528 | import shell 529 | 530 | shell: 531 | ls 532 | #+END_SRC 533 | 534 | #+RESULTS: 535 | : shellCmd: ls 536 | 537 | The default ~shellVerbose~ command combines stderr and stdout into 538 | single result. To get =stdout=, =stderr= and the return code 539 | separately use ~shellVerboseErr~. Both of these templates have an 540 | overload that takes ~set[DebugOutputKind]~ to control printing 541 | settings: 542 | 543 | #+BEGIN_SRC nim :exports both :results scalar 544 | import shell 545 | 546 | let (res, err, code) = shellVerboseErr {dokCommand}: 547 | echo "test" 548 | 549 | echo "Returned string: '", res, "' with exit code ", code 550 | 551 | #+END_SRC 552 | 553 | #+RESULTS: 554 | : shellCmd: echo test 555 | : Returned string: 'test' with exit code 0 556 | 557 | Printing errors directly into stdout is good solution for most of the 558 | use cases, but sometimes it is necessary to provide more sophisticated 559 | error handing - throwing an exception when the command failed. To 560 | switch to exceptions use ~-d:shellThrowException~. It will 561 | automatically disable all other output types in the default 562 | configuration. 563 | 564 | #+begin_src nim :exports both :results scalar 565 | {.define(shellThrowException).} 566 | 567 | import shell, strutils 568 | 569 | try: 570 | shell: 571 | ls -l 572 | ls -z 573 | except ShellExecError: 574 | let e = cast[ShellExecError](getCurrentException()) 575 | echo e.msg # Error message describing what happened 576 | echo "command was: ", e.cmd # Original command string 577 | assert e.cmd == "ls -z" 578 | echo "return code: ", e.retcode # Return code 579 | echo "regular out: ", e.outstr # Stdout from command 580 | echo "error outpt: " 581 | for l in e.errstr.split('\n'): # Stderr from the command 582 | echo " ", l 583 | #+end_src 584 | 585 | #+RESULTS: 586 | : Command ls -z exited with non-zero code 587 | : command was: ls -z 588 | : return code: 2 589 | : regular out: 590 | : error outpt: 591 | : ls: invalid option -- 'z' 592 | : Try 'ls --help' for more information. 593 | 594 | On command failure ~ShellExecError~ is raised. 595 | 596 | Note that some commands output error messages into ~stdout~ rather 597 | than into ~stderr~ - it might be necessary to check both. In this 598 | particular example content of the ~stderr~ is largely meaningless: 599 | actual reason for error was printed into ~stdout~. 600 | 601 | #+begin_src nim :exports both :results scalar 602 | {.define(shellThrowException).} 603 | 604 | import shell, strutils 605 | 606 | try: 607 | shell: 608 | ngspice -b "/tmp/ngpsice-simulation/zzz.netkRs8jE" 609 | except ShellExecError: 610 | let e = cast[ShellExecError](getCurrentException()) 611 | echo e.msg # Error message describing what happened 612 | echo "command was: ", e.cmd # Original command string 613 | echo "exec direct: ", e.cwd # 614 | echo "return code: ", e.retcode # Return code 615 | echo "regular out: \n====\n", e.outstr # Stdout from command 616 | echo "====\nerror outpt: \n====\n", e.errstr # Stderr from the command 617 | #+end_src 618 | 619 | #+RESULTS: 620 | #+begin_example 621 | Command ngspice -b /tmp/ngpsice-simulation/zzz.netkRs8jE exited with non-zero code 622 | command was: ngspice -b /tmp/ngpsice-simulation/zzz.netkRs8jE 623 | exec direct: /home/test/workspace/git-sandbox/shell 624 | return code: 1 625 | regular out: 626 | ==== 627 | 628 | ==== 629 | error outpt: 630 | ==== 631 | /tmp/ngpsice-simulation/zzz.netkRs8jE: No such file or directory 632 | #+end_example 633 | -------------------------------------------------------------------------------- /changelog.org: -------------------------------------------------------------------------------- 1 | * v0.6.0 2 | - print process ID for the spawned process by default. Can be disabled 3 | by compiling with ~-d:PrintPid=false~. 4 | * v0.5.2 5 | - add preliminary support for ~shell~ in macros (i.e. in ~nimvm~ contexts) 6 | * v0.5.1 7 | - ~send~ can now be used without an explicit ~expect~ 8 | * v0.5.0 9 | - add =expect= / =send= construct to DSL 10 | * v0.4.4 11 | - allow (single) `nnkAsgn` in macro, e.g. to write 12 | =foo --option=value --more=. Note that only a single assignment is valid. 13 | * v0.4.3 14 | - replace travis CI by Github Actions (now includes Windows and OSX 15 | testing) 16 | - windows support is only partial. =one= and =pipe= appear to be 17 | broken 18 | - change =shellVerboseImpl=, =shellVerbose= internals 19 | - allow to customize the process options, which are handed to 20 | =startProcess= 21 | - command not found on windows is an =OSError=, which we catch and 22 | turn into an error code 23 | - do not close error stream manually anymore (should not be done 24 | according to Nim docs of =osproc.errorStream=) 25 | 26 | * v0.4.2 27 | - fix =shellAssign= to allow quoting of Nim variables 28 | * v0.4.1 29 | - improve handling of complicated quoting expressions 30 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | Index 21 | 22 | 23 | 24 | 25 | 62 | 63 | 64 | 65 |
66 | 146 |
147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /docs/nimdoc.out.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stylesheet for use with Docutils/rst2html. 3 | 4 | See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to 5 | customize this style sheet. 6 | 7 | Modified from Chad Skeeters' rst2html-style 8 | https://bitbucket.org/cskeeters/rst2html-style/ 9 | 10 | Modified by Boyd Greenfield and narimiran 11 | */ 12 | 13 | :root { 14 | --primary-background: #fff; 15 | --secondary-background: ghostwhite; 16 | --third-background: #e8e8e8; 17 | --info-background: #50c050; 18 | --warning-background: #c0a000; 19 | --error-background: #e04040; 20 | --border: #dde; 21 | --text: #222; 22 | --anchor: #07b; 23 | --anchor-focus: #607c9f; 24 | --input-focus: #1fa0eb; 25 | --strong: #3c3c3c; 26 | --hint: #9A9A9A; 27 | --nim-sprite-base64: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAN4AAAA9CAYAAADCt9ebAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTEyLTAzVDAxOjAzOjQ4KzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0xMi0wM1QwMjoyODo0MSswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0xMi0wM1QwMjoyODo0MSswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDozMzM0ZjAxYS0yMDExLWE1NGQtOTVjNy1iOTgxMDFlMDFhMmEiIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6MzMzNGYwMWEtMjAxMS1hNTRkLTk1YzctYjk4MTAxZTAxYTJhIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6MzMzNGYwMWEtMjAxMS1hNTRkLTk1YzctYjk4MTAxZTAxYTJhIj4gPHhtcE1NOkhpc3Rvcnk+IDxyZGY6U2VxPiA8cmRmOmxpIHN0RXZ0OmFjdGlvbj0iY3JlYXRlZCIgc3RFdnQ6aW5zdGFuY2VJRD0ieG1wLmlpZDozMzM0ZjAxYS0yMDExLWE1NGQtOTVjNy1iOTgxMDFlMDFhMmEiIHN0RXZ0OndoZW49IjIwMTktMTItMDNUMDE6MDM6NDgrMDE6MDAiIHN0RXZ0OnNvZnR3YXJlQWdlbnQ9IkFkb2JlIFBob3Rvc2hvcCAyMS4wIChXaW5kb3dzKSIvPiA8L3JkZjpTZXE+IDwveG1wTU06SGlzdG9yeT4gPC9yZGY6RGVzY3JpcHRpb24+IDwvcmRmOlJERj4gPC94OnhtcG1ldGE+IDw/eHBhY2tldCBlbmQ9InIiPz4PsixkAAAJ5klEQVR4nO2dfbBUZR3HP3vvxVD0zo0ACXxBuQMoQjJ1DfMl0NIhNcuSZqQhfGt6UWtK06xJexkrmywVRTQlHCIdtclC0zBJvYIvvEUgZpc3XyC7RVbKlQu1/fHdbc+uu2fPOfs85+y55/nMnBl2z+5zfnc5v/M8z+8119XVRYroAG4HfgvMT1YUR4MMAa4HLkhakCRoSVqAELwLeBY4C7gF+D6QS1QiR1ROAJ4Dzk9akKQwoXhtwL4GxvHjU8AKoNPz3leAu4HBFq+bAyZZHD9rDAK+BywDDklYlkQxoXhfAtYAEw2MVckQYBHwU6or99nA08BBFq49GngUeBIYaWH8rNEJdAOXA60Jy5I4jSreSOBKYDzwBPCJhiUqcSjwe2BWnc9NLnxuvMFrnwqsAqYBBwBfNzh2FpmNfs9jkhakWcg1aFxZiH5UL3cDnwf+Xue7BwFjgFHAOwuv24tyob3cO0LIshP4EbCn8Pq/wKvA9sLxMvCvOmPsA1yDZnHv/nEv2mM+F0IeR4m8z7lM7tMbUbzj0CxX7YfbAXwaWFJ4PRrNIu9FS9KJyEIZN68CG4DnkRJtLBw7gHHAYuDdNb77EDAjBhkHIk7xKoiqeK3IwjilzuceQJvoZjdQ/AMZaeoZiWYgBXSEwyleBW0Rv3cR9ZUO4LSI48fN2wN+bi5wJNBvUZaBSCaVy48oxpVhwDdMC5ISxpJRh6/DLGEUrxXt29YBQ+2IkwquR76ofZIWxJFegireNLSnm48skFmmDfmiVgJHJyuKI620ADOpbWEcDPwYOZKD7OmyxCTkXL+wzueOiEEWR8poQb60V4A7kLm/yFjgKeALuM1xLfYDbkX+zEGe98cAX0Oui6viF8vR7OS6urragW2UZr21wK+Aiwlu7XPoN3sYOAd4H6WH1SnA0qSEcjQnRT/e1bgnsw16kGPez4/lyCBF48oNwL+TFGSAsgCndI4qFBVvJ0owdZhjL3CnxfHzBo8+YBMyol0CHBijrKbHS/LoA7Yio9sPgJNr/QHekLGR6MffL+KP4SjnHmQxtoXNmbQP+CHyV75hYDzTIWNpWkU8iR5mq71vVsZqXgtcFqNQ/wG2IOtfD8oi6AX+Ujj+isKz8sBrnu+1okyGdmD/wnEgcDClTIdRyJRvI1cvCMciq7At4rj5eoCPAusbHCfLigda/VyKgi+AtyreMGAzykGzQQ/wO+BxSlkCuy1dq8hw5OieUjimYT+x9bHCdWwS1823Ez1EXmhgjKwrXpHzkduuanbCtzGX+NkPPAj8GincNkPjNkIO5dadUjiOB95m+BonopQpm8R58/0JJbHWy2eshVM8sRvdbyurKV4Hmoka2WA/iwwLP6d+QmzSdKC92GzK/W9R+Q3woQbHCELcN991wJcjftcpXolngKm18vFmoVonYcgDv0Qz5pqGREuOTuA8lPYUZbndh0LJNpkUqgZx33xvomim7RG+6xSvnOm1gqQXoyiMoKxFs8VZpFfpQHvQK4HDUPnAsBa9bxGP0tUjF+IYCkxFew+/G3owdq20pgjzt3uPRscs/o43IaOhH2f4ZaAPRyZQP6vgbuCbyGext87F0sgIZFI/N8BnlwBnolovcWAjq/uzwM0+55cBJ0UYN84ZL+rfbnLMM4FfUDv7Z1XlCe8FetETbleNL7+CZrnvMjCVDuTOOA84Hf+96ga0PC8qXY50FQsuMg+41+d8p885R4n7gdt8zo+qvDkmUF4fZQXwEbS+99KDMhlWkw0eALqQglXyDDCdcovf+4lv5jPNXJ9zWc/FDMMdPudGVCreRlTWwVtWbynwYVQQCFSp61Q042WJLUjB1nneuw8tvXo97x1Lugvg+j1Mo9boySLVHtJFWqsthx5GlbSGeN5bigrHdqPl52Zj4qWLXvTQWY4KOX2ccgPMBLRcuy9+0YzhguXN4GuYq2Zc2R/NZg+hfYt3/9ZCepdQthmB4vIWIYOTbWyWzGt2Y0izG1fqjlltxnsdpbPMRMmd3lqTTumqMw7FZY5G5mSHw5dalreiRWYGWjbZ7gYUlFa0xOtIWA4vk1E6zWEoI+FvyYrjSAO1FG8DCmQGKd+DJFsGogWVVFiP/GWbga9Svg9NgtPQvnd04fUNCcriSBF+vqZ5nn9PQ+Xs4q401oI6EP0R+BkyXoAeAtcgBfwidnvkVaMVFTO6n1JoWTfqiONw1MVP8e6l3GVwOPJZXW5VItGGiuduAu5CZdOrMQJ1CHqpIFccS+LxaD/3Hcr7vF0Xw7UdAwQ/xduLGkJ6aUMhVAuwU006B3wM+ZLmozJ5QRhWkGs9yjKw1fhwDsq8eE/F+y+i1CeHIxD1wppupXrA5xyUOjQHMzU3cyjTeS2aaaN2Fzoc1bhch3xspuqBTkDulQVUz1q4mYEbNuewQD3FexGFS1VjOLoRHwOOinj9HAooXY2CSidHHKeSI5GFcRWNdSxqR7VH1iHHeTV24R+X53C8hSCBvPPqnD8B+AOygn6OYAm0ORSGthLl8B0d4DtRmIKsoMsJF1U/Hi1dt6DusIN8PrsIlUdwOAITpDFlC6q3MTbgmHm011qGepOvQSXPipyOCujW6rxqk0dRWYsVFe8PRSn5JxWOoEvdfOGzfnF5tnCRK+bGi33MoB1hL0U5d1H5J5oVD6A5mp8sQS6KSWh5e0jEcR4BPmhKqJA4xTM3XuxjBlW8DuRacDU3y0myNbNTPHPjxT5m0GTN15A/zVFiI+HKYzgc/ydMlrRfgmQWuYn0F91xJEQYxVuDnMcOrQAWJi2EI72ErQviwqLEQpQ+5XBEIqzi3YWLwF+BMiMcjshEqYR1Gdk1KmxBsaR9SQviSDdRFK8fxVU+YliWZmcbcq7vSFoQR/qJWvuxD0WgLDYoSzPzAqowtjVhORwDhEaKru4GPoliGgcyy4Hj0DLT4TBCo9WO88jQ8Bns97lLghvRTOfqqDiMYqrM+HyUYdBtaLykeRmlK12C9rQOh1FM1vd/HqUIzaT5e+LVoh/VxByHShs6HFaw0VjjHhTxP5d0LT+fRnu5q3HuAodlbHW02Q5cDByM+sw1642cRylCx6PeZiuTFScUFxK+f19QovaRS+t4tsasxhvABbZbSfUCV6CM7qtQl6Fm4E1U22UqcAYqvZ42fgJMxH6vdYc5nkBlSW6Pq4fbS6hb6jg0u9yGug7FyS5U1+UcVBbwbFSuMM1sQ1bXK4A9CcviqM0e9H80HdUxCpwIa4McygA/GfgAcCJqmGKKXUixupEv7nHsLc2agWNQ0d9OzC+PHNHIo1XeLCoe8kkqXiUtwKFoWXoEKqk3BpWLaC8cXsV8HT1J+tFTZKvn+DMqFZi1knvtyKg1O2lBHADcCVxEedNSAP4HJcsr0NNWHVUAAAAASUVORK5CYII="); 28 | 29 | --keyword: #5e8f60; 30 | --identifier: #222; 31 | --comment: #484a86; 32 | --operator: #155da4; 33 | --punctuation: black; 34 | --other: black; 35 | --escapeSequence: #c4891b; 36 | --number: #252dbe; 37 | --literal: #a4255b; 38 | --raw-data: #a4255b; 39 | } 40 | 41 | [data-theme="dark"] { 42 | --primary-background: #171921; 43 | --secondary-background: #1e202a; 44 | --third-background: #2b2e3b; 45 | --info-background: #008000; 46 | --warning-background: #807000; 47 | --error-background: #c03000; 48 | --border: #0e1014; 49 | --text: #fff; 50 | --anchor: #8be9fd; 51 | --anchor-focus: #8be9fd; 52 | --input-focus: #8be9fd; 53 | --strong: #bd93f9; 54 | --hint: #7A7C85; 55 | --nim-sprite-base64: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAARMAAABMCAYAAABOBlMuAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFFmlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS42LWMxNDggNzkuMTY0MDM2LCAyMDE5LzA4LzEzLTAxOjA2OjU3ICAgICAgICAiPiA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPiA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIiB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIgeG1wOkNyZWF0b3JUb29sPSJBZG9iZSBQaG90b3Nob3AgMjEuMCAoV2luZG93cykiIHhtcDpDcmVhdGVEYXRlPSIyMDE5LTEyLTAzVDAxOjE4OjIyKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAxOS0xMi0wM1QwMToyMDoxMCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAxOS0xMi0wM1QwMToyMDoxMCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHBob3Rvc2hvcDpJQ0NQcm9maWxlPSJzUkdCIElFQzYxOTY2LTIuMSIgeG1wTU06SW5zdGFuY2VJRD0ieG1wLmlpZDplZGViMzU3MC1iNmZjLWQy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"); 56 | 57 | --keyword: #ff79c6; 58 | --identifier: #f8f8f2; 59 | --comment: #6272a4; 60 | --operator: #ff79c6; 61 | --punctuation: #f8f8f2; 62 | --other: #f8f8f2; 63 | --escapeSequence: #bd93f9; 64 | --number: #bd93f9; 65 | --literal: #f1fa8c; 66 | --raw-data: #8be9fd; 67 | } 68 | 69 | .theme-switch-wrapper { 70 | display: flex; 71 | align-items: center; 72 | } 73 | 74 | .theme-switch-wrapper em { 75 | margin-left: 10px; 76 | font-size: 1rem; 77 | } 78 | 79 | .theme-switch { 80 | display: inline-block; 81 | height: 22px; 82 | position: relative; 83 | width: 50px; 84 | } 85 | 86 | .theme-switch input { 87 | display: none; 88 | } 89 | 90 | .slider { 91 | background-color: #ccc; 92 | bottom: 0; 93 | cursor: pointer; 94 | left: 0; 95 | position: absolute; 96 | right: 0; 97 | top: 0; 98 | transition: .4s; 99 | } 100 | 101 | .slider:before { 102 | background-color: #fff; 103 | bottom: 4px; 104 | content: ""; 105 | height: 13px; 106 | left: 4px; 107 | position: absolute; 108 | transition: .4s; 109 | width: 13px; 110 | } 111 | 112 | input:checked + .slider { 113 | background-color: #66bb6a; 114 | } 115 | 116 | input:checked + .slider:before { 117 | transform: translateX(26px); 118 | } 119 | 120 | .slider.round { 121 | border-radius: 17px; 122 | } 123 | 124 | .slider.round:before { 125 | border-radius: 50%; 126 | } 127 | 128 | html { 129 | font-size: 100%; 130 | -webkit-text-size-adjust: 100%; 131 | -ms-text-size-adjust: 100%; } 132 | 133 | body { 134 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 135 | font-weight: 400; 136 | font-size: 1.125em; 137 | line-height: 1.5; 138 | color: var(--text); 139 | background-color: var(--primary-background); } 140 | 141 | /* Skeleton grid */ 142 | .container { 143 | position: relative; 144 | width: 100%; 145 | max-width: 1050px; 146 | margin: 0 auto; 147 | padding: 0; 148 | box-sizing: border-box; } 149 | 150 | .column, 151 | .columns { 152 | width: 100%; 153 | float: left; 154 | box-sizing: border-box; 155 | margin-left: 1%; 156 | } 157 | 158 | .column:first-child, 159 | .columns:first-child { 160 | margin-left: 0; } 161 | 162 | .three.columns { 163 | width: 22%; 164 | } 165 | 166 | .nine.columns { 167 | width: 77.0%; } 168 | 169 | .twelve.columns { 170 | width: 100%; 171 | margin-left: 0; } 172 | 173 | @media screen and (max-width: 860px) { 174 | .three.columns { 175 | display: none; 176 | } 177 | .nine.columns { 178 | width: 98.0%; 179 | } 180 | body { 181 | font-size: 1em; 182 | line-height: 1.35; 183 | } 184 | } 185 | 186 | cite { 187 | font-style: italic !important; } 188 | 189 | 190 | /* Nim search input */ 191 | div#searchInputDiv { 192 | margin-bottom: 1em; 193 | } 194 | input#searchInput { 195 | width: 80%; 196 | } 197 | 198 | /* 199 | * Some custom formatting for input forms. 200 | * This also fixes input form colors on Firefox with a dark system theme on Linux. 201 | */ 202 | input { 203 | -moz-appearance: none; 204 | background-color: var(--secondary-background); 205 | color: var(--text); 206 | border: 1px solid var(--border); 207 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 208 | font-size: 0.9em; 209 | padding: 6px; 210 | } 211 | 212 | input:focus { 213 | border: 1px solid var(--input-focus); 214 | box-shadow: 0 0 3px var(--input-focus); 215 | } 216 | 217 | select { 218 | -moz-appearance: none; 219 | background-color: var(--secondary-background); 220 | color: var(--text); 221 | border: 1px solid var(--border); 222 | font-family: "Lato", "Helvetica Neue", "HelveticaNeue", Helvetica, Arial, sans-serif; 223 | font-size: 0.9em; 224 | padding: 6px; 225 | } 226 | 227 | select:focus { 228 | border: 1px solid var(--input-focus); 229 | box-shadow: 0 0 3px var(--input-focus); 230 | } 231 | 232 | /* Docgen styles */ 233 | /* Links */ 234 | a { 235 | color: var(--anchor); 236 | text-decoration: none; 237 | } 238 | 239 | a span.Identifier { 240 | text-decoration: underline; 241 | text-decoration-color: #aab; 242 | } 243 | 244 | a.reference-toplevel { 245 | font-weight: bold; 246 | } 247 | 248 | a.toc-backref { 249 | text-decoration: none; 250 | color: var(--text); } 251 | 252 | a.link-seesrc { 253 | color: #607c9f; 254 | font-size: 0.9em; 255 | font-style: italic; } 256 | 257 | a:hover, 258 | a:focus { 259 | color: var(--anchor-focus); 260 | text-decoration: underline; } 261 | 262 | a:hover span.Identifier { 263 | color: var(--anchor); 264 | } 265 | 266 | 267 | sub, 268 | sup { 269 | position: relative; 270 | font-size: 75%; 271 | line-height: 0; 272 | vertical-align: baseline; } 273 | 274 | sup { 275 | top: -0.5em; } 276 | 277 | sub { 278 | bottom: -0.25em; } 279 | 280 | img { 281 | width: auto; 282 | height: auto; 283 | max-width: 100%; 284 | vertical-align: middle; 285 | border: 0; 286 | -ms-interpolation-mode: bicubic; } 287 | 288 | @media print { 289 | * { 290 | color: black !important; 291 | text-shadow: none !important; 292 | background: transparent !important; 293 | box-shadow: none !important; } 294 | 295 | a, 296 | a:visited { 297 | text-decoration: underline; } 298 | 299 | a[href]:after { 300 | content: " (" attr(href) ")"; } 301 | 302 | abbr[title]:after { 303 | content: " (" attr(title) ")"; } 304 | 305 | .ir a:after, 306 | a[href^="javascript:"]:after, 307 | a[href^="#"]:after { 308 | content: ""; } 309 | 310 | pre, 311 | blockquote { 312 | border: 1px solid #999; 313 | page-break-inside: avoid; } 314 | 315 | thead { 316 | display: table-header-group; } 317 | 318 | tr, 319 | img { 320 | page-break-inside: avoid; } 321 | 322 | img { 323 | max-width: 100% !important; } 324 | 325 | @page { 326 | margin: 0.5cm; } 327 | 328 | h1 { 329 | page-break-before: always; } 330 | 331 | h1.title { 332 | page-break-before: avoid; } 333 | 334 | p, 335 | h2, 336 | h3 { 337 | orphans: 3; 338 | widows: 3; } 339 | 340 | h2, 341 | h3 { 342 | page-break-after: avoid; } 343 | } 344 | 345 | 346 | p { 347 | margin-top: 0.5em; 348 | margin-bottom: 0.5em; 349 | } 350 | 351 | small { 352 | font-size: 85%; } 353 | 354 | strong { 355 | font-weight: 600; 356 | font-size: 0.95em; 357 | color: var(--strong); 358 | } 359 | 360 | em { 361 | font-style: italic; } 362 | 363 | h1 { 364 | font-size: 1.8em; 365 | font-weight: 400; 366 | padding-bottom: .25em; 367 | border-bottom: 6px solid var(--third-background); 368 | margin-top: 2.5em; 369 | margin-bottom: 1em; 370 | line-height: 1.2em; } 371 | 372 | h1.title { 373 | padding-bottom: 1em; 374 | border-bottom: 0px; 375 | font-size: 2.5em; 376 | text-align: center; 377 | font-weight: 900; 378 | margin-top: 0.75em; 379 | margin-bottom: 0em; 380 | } 381 | 382 | h2 { 383 | font-size: 1.3em; 384 | margin-top: 2em; } 385 | 386 | h2.subtitle { 387 | text-align: center; } 388 | 389 | h3 { 390 | font-size: 1.125em; 391 | font-style: italic; 392 | margin-top: 1.5em; } 393 | 394 | h4 { 395 | font-size: 1.125em; 396 | margin-top: 1em; } 397 | 398 | h5 { 399 | font-size: 1.125em; 400 | margin-top: 0.75em; } 401 | 402 | h6 { 403 | font-size: 1.1em; } 404 | 405 | 406 | ul, 407 | ol { 408 | padding: 0; 409 | margin-top: 0.5em; 410 | margin-left: 0.75em; } 411 | 412 | ul ul, 413 | ul ol, 414 | ol ol, 415 | ol ul { 416 | margin-bottom: 0; 417 | margin-left: 1.25em; } 418 | 419 | ul.simple > li { 420 | list-style-type: circle; 421 | } 422 | 423 | ul.simple-boot li { 424 | list-style-type: none; 425 | margin-left: 0em; 426 | margin-bottom: 0.5em; 427 | } 428 | 429 | ol.simple > li, ul.simple > li { 430 | margin-bottom: 0.2em; 431 | margin-left: 0.4em } 432 | 433 | ul.simple.simple-toc > li { 434 | margin-top: 1em; 435 | } 436 | 437 | ul.simple-toc { 438 | list-style: none; 439 | font-size: 0.9em; 440 | margin-left: -0.3em; 441 | margin-top: 1em; } 442 | 443 | ul.simple-toc > li { 444 | list-style-type: none; 445 | } 446 | 447 | ul.simple-toc-section { 448 | list-style-type: circle; 449 | margin-left: 0.8em; 450 | color: #6c9aae; } 451 | 452 | ul.nested-toc-section { 453 | list-style-type: circle; 454 | margin-left: -0.75em; 455 | color: var(--text); 456 | } 457 | 458 | ul.nested-toc-section > li { 459 | margin-left: 1.25em; 460 | } 461 | 462 | 463 | ol.arabic { 464 | list-style: decimal; } 465 | 466 | ol.loweralpha { 467 | list-style: lower-alpha; } 468 | 469 | ol.upperalpha { 470 | list-style: upper-alpha; } 471 | 472 | ol.lowerroman { 473 | list-style: lower-roman; } 474 | 475 | ol.upperroman { 476 | list-style: upper-roman; } 477 | 478 | ul.auto-toc { 479 | list-style-type: none; } 480 | 481 | 482 | dl { 483 | margin-bottom: 1.5em; } 484 | 485 | dt { 486 | margin-bottom: -0.5em; 487 | margin-left: 0.0em; } 488 | 489 | dd { 490 | margin-left: 2.0em; 491 | margin-bottom: 3.0em; 492 | margin-top: 0.5em; } 493 | 494 | 495 | hr { 496 | margin: 2em 0; 497 | border: 0; 498 | border-top: 1px solid #aaa; } 499 | 500 | blockquote { 501 | font-size: 0.9em; 502 | font-style: italic; 503 | padding-left: 0.5em; 504 | margin-left: 0; 505 | border-left: 5px solid #bbc; 506 | } 507 | 508 | .pre { 509 | font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; 510 | font-weight: 500; 511 | font-size: 0.85em; 512 | color: var(--text); 513 | background-color: var(--third-background); 514 | padding-left: 3px; 515 | padding-right: 3px; 516 | border-radius: 4px; 517 | } 518 | 519 | pre { 520 | font-family: "Source Code Pro", Monaco, Menlo, Consolas, "Courier New", monospace; 521 | color: var(--text); 522 | font-weight: 500; 523 | display: inline-block; 524 | box-sizing: border-box; 525 | min-width: 100%; 526 | padding: 0.5em; 527 | margin-top: 0.5em; 528 | margin-bottom: 0.5em; 529 | font-size: 0.85em; 530 | white-space: pre !important; 531 | overflow-y: hidden; 532 | overflow-x: visible; 533 | background-color: var(--secondary-background); 534 | border: 1px solid var(--border); 535 | -webkit-border-radius: 6px; 536 | -moz-border-radius: 6px; 537 | border-radius: 6px; } 538 | 539 | .pre-scrollable { 540 | max-height: 340px; 541 | overflow-y: scroll; } 542 | 543 | 544 | /* Nim line-numbered tables */ 545 | .line-nums-table { 546 | width: 100%; 547 | table-layout: fixed; } 548 | 549 | table.line-nums-table { 550 | border-radius: 4px; 551 | border: 1px solid #cccccc; 552 | background-color: ghostwhite; 553 | border-collapse: separate; 554 | margin-top: 15px; 555 | margin-bottom: 25px; } 556 | 557 | .line-nums-table tbody { 558 | border: none; } 559 | 560 | .line-nums-table td pre { 561 | border: none; 562 | background-color: transparent; } 563 | 564 | .line-nums-table td.blob-line-nums { 565 | width: 28px; } 566 | 567 | .line-nums-table td.blob-line-nums pre { 568 | color: #b0b0b0; 569 | -webkit-filter: opacity(75%); 570 | text-align: right; 571 | border-color: transparent; 572 | background-color: transparent; 573 | padding-left: 0px; 574 | margin-left: 0px; 575 | padding-right: 0px; 576 | margin-right: 0px; } 577 | 578 | 579 | table { 580 | max-width: 100%; 581 | background-color: transparent; 582 | margin-top: 0.5em; 583 | margin-bottom: 1.5em; 584 | border-collapse: collapse; 585 | border-color: var(--third-background); 586 | border-spacing: 0; 587 | font-size: 0.9em; 588 | } 589 | 590 | table th, table td { 591 | padding: 0px 0.5em 0px; 592 | border-color: var(--third-background); 593 | } 594 | 595 | table th { 596 | background-color: var(--third-background); 597 | border-color: var(--third-background); 598 | font-weight: bold; } 599 | 600 | table th.docinfo-name { 601 | background-color: transparent; 602 | text-align: right; 603 | } 604 | 605 | table tr:hover { 606 | background-color: var(--third-background); } 607 | 608 | 609 | /* rst2html default used to remove borders from tables and images */ 610 | .borderless, table.borderless td, table.borderless th { 611 | border: 0; } 612 | 613 | table.borderless td, table.borderless th { 614 | /* Override padding for "table.docutils td" with "! important". 615 | The right padding separates the table cells. */ 616 | padding: 0 0.5em 0 0 !important; } 617 | 618 | .admonition { 619 | padding: 0.3em; 620 | background-color: var(--secondary-background); 621 | border-left: 0.4em solid #7f7f84; 622 | margin-bottom: 0.5em; 623 | -webkit-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); 624 | -moz-box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); 625 | box-shadow: 0 5px 8px -6px rgba(0,0,0,.2); 626 | } 627 | .admonition-info { 628 | border-color: var(--info-background); 629 | } 630 | .admonition-info-text { 631 | color: var(--info-background); 632 | } 633 | .admonition-warning { 634 | border-color: var(--warning-background); 635 | } 636 | .admonition-warning-text { 637 | color: var(--warning-background); 638 | } 639 | .admonition-error { 640 | border-color: var(--error-background); 641 | } 642 | .admonition-error-text { 643 | color: var(--error-background); 644 | } 645 | 646 | .first { 647 | /* Override more specific margin styles with "! important". */ 648 | margin-top: 0 !important; } 649 | 650 | .last, .with-subtitle { 651 | margin-bottom: 0 !important; } 652 | 653 | .hidden { 654 | display: none; } 655 | 656 | blockquote.epigraph { 657 | margin: 2em 5em; } 658 | 659 | dl.docutils dd { 660 | margin-bottom: 0.5em; } 661 | 662 | object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { 663 | overflow: hidden; } 664 | 665 | 666 | div.figure { 667 | margin-left: 2em; 668 | margin-right: 2em; } 669 | 670 | div.footer, div.header { 671 | clear: both; 672 | text-align: center; 673 | color: #666; 674 | font-size: smaller; } 675 | 676 | div.footer { 677 | padding-top: 5em; 678 | } 679 | 680 | div.line-block { 681 | display: block; 682 | margin-top: 1em; 683 | margin-bottom: 1em; } 684 | 685 | div.line-block div.line-block { 686 | margin-top: 0; 687 | margin-bottom: 0; 688 | margin-left: 1.5em; } 689 | 690 | div.topic { 691 | margin: 2em; } 692 | 693 | div.search_results { 694 | background-color: var(--third-background); 695 | margin: 3em; 696 | padding: 1em; 697 | border: 1px solid #4d4d4d; 698 | } 699 | 700 | div#global-links ul { 701 | margin-left: 0; 702 | list-style-type: none; 703 | } 704 | 705 | div#global-links > simple-boot { 706 | margin-left: 3em; 707 | } 708 | 709 | hr.docutils { 710 | width: 75%; } 711 | 712 | img.align-left, .figure.align-left, object.align-left { 713 | clear: left; 714 | float: left; 715 | margin-right: 1em; } 716 | 717 | img.align-right, .figure.align-right, object.align-right { 718 | clear: right; 719 | float: right; 720 | margin-left: 1em; } 721 | 722 | img.align-center, .figure.align-center, object.align-center { 723 | display: block; 724 | margin-left: auto; 725 | margin-right: auto; } 726 | 727 | .align-left { 728 | text-align: left; } 729 | 730 | .align-center { 731 | clear: both; 732 | text-align: center; } 733 | 734 | .align-right { 735 | text-align: right; } 736 | 737 | /* reset inner alignment in figures */ 738 | div.align-right { 739 | text-align: inherit; } 740 | 741 | p.attribution { 742 | text-align: right; 743 | margin-left: 50%; } 744 | 745 | p.caption { 746 | font-style: italic; } 747 | 748 | p.credits { 749 | font-style: italic; 750 | font-size: smaller; } 751 | 752 | p.label { 753 | white-space: nowrap; } 754 | 755 | p.rubric { 756 | font-weight: bold; 757 | font-size: larger; 758 | color: maroon; 759 | text-align: center; } 760 | 761 | p.topic-title { 762 | font-weight: bold; } 763 | 764 | pre.address { 765 | margin-bottom: 0; 766 | margin-top: 0; 767 | font: inherit; } 768 | 769 | pre.literal-block, pre.doctest-block, pre.math, pre.code { 770 | margin-left: 2em; 771 | margin-right: 2em; } 772 | 773 | pre.code .ln { 774 | color: grey; } 775 | 776 | /* line numbers */ 777 | pre.code, code { 778 | background-color: #eeeeee; } 779 | 780 | pre.code .comment, code .comment { 781 | color: #5c6576; } 782 | 783 | pre.code .keyword, code .keyword { 784 | color: #3B0D06; 785 | font-weight: bold; } 786 | 787 | pre.code .literal.string, code .literal.string { 788 | color: #0c5404; } 789 | 790 | pre.code .name.builtin, code .name.builtin { 791 | color: #352b84; } 792 | 793 | pre.code .deleted, code .deleted { 794 | background-color: #DEB0A1; } 795 | 796 | pre.code .inserted, code .inserted { 797 | background-color: #A3D289; } 798 | 799 | span.classifier { 800 | font-style: oblique; } 801 | 802 | span.classifier-delimiter { 803 | font-weight: bold; } 804 | 805 | span.option { 806 | white-space: nowrap; } 807 | 808 | span.problematic { 809 | color: #b30000; } 810 | 811 | span.section-subtitle { 812 | /* font-size relative to parent (h1..h6 element) */ 813 | font-size: 80%; } 814 | 815 | span.DecNumber { 816 | color: var(--number); } 817 | 818 | span.BinNumber { 819 | color: var(--number); } 820 | 821 | span.HexNumber { 822 | color: var(--number); } 823 | 824 | span.OctNumber { 825 | color: var(--number); } 826 | 827 | span.FloatNumber { 828 | color: var(--number); } 829 | 830 | span.Identifier { 831 | color: var(--identifier); } 832 | 833 | span.Keyword { 834 | font-weight: 600; 835 | color: var(--keyword); } 836 | 837 | span.StringLit { 838 | color: var(--literal); } 839 | 840 | span.LongStringLit { 841 | color: var(--literal); } 842 | 843 | span.CharLit { 844 | color: var(--literal); } 845 | 846 | span.EscapeSequence { 847 | color: var(--escapeSequence); } 848 | 849 | span.Operator { 850 | color: var(--operator); } 851 | 852 | span.Punctuation { 853 | color: var(--punctuation); } 854 | 855 | span.Comment, span.LongComment { 856 | font-style: italic; 857 | font-weight: 400; 858 | color: var(--comment); } 859 | 860 | span.RegularExpression { 861 | color: darkviolet; } 862 | 863 | span.TagStart { 864 | color: darkviolet; } 865 | 866 | span.TagEnd { 867 | color: darkviolet; } 868 | 869 | span.Key { 870 | color: #252dbe; } 871 | 872 | span.Value { 873 | color: #252dbe; } 874 | 875 | span.RawData { 876 | color: var(--raw-data); } 877 | 878 | span.Assembler { 879 | color: #252dbe; } 880 | 881 | span.Preprocessor { 882 | color: #252dbe; } 883 | 884 | span.Directive { 885 | color: #252dbe; } 886 | 887 | span.Command, span.Rule, span.Hyperlink, span.Label, span.Reference, 888 | span.Other { 889 | color: var(--other); } 890 | 891 | /* Pop type, const, proc, and iterator defs in nim def blocks */ 892 | dt pre > span.Identifier, dt pre > span.Operator { 893 | color: var(--identifier); 894 | font-weight: 700; } 895 | 896 | dt pre > span.Keyword ~ span.Identifier, dt pre > span.Identifier ~ span.Identifier, 897 | dt pre > span.Operator ~ span.Identifier, dt pre > span.Other ~ span.Identifier { 898 | color: var(--identifier); 899 | font-weight: inherit; } 900 | 901 | /* Nim sprite for the footer (taken from main page favicon) */ 902 | .nim-sprite { 903 | display: inline-block; 904 | width: 51px; 905 | height: 14px; 906 | background-position: 0 0; 907 | background-size: 51px 14px; 908 | -webkit-filter: opacity(50%); 909 | background-repeat: no-repeat; 910 | background-image: var(--nim-sprite-base64); 911 | margin-bottom: 5px; } 912 | 913 | span.pragmadots { 914 | /* Position: relative frees us up to make the dots 915 | look really nice without fucking up the layout and 916 | causing bulging in the parent container */ 917 | position: relative; 918 | /* 1px down looks slightly nicer */ 919 | top: 1px; 920 | padding: 2px; 921 | background-color: var(--third-background); 922 | border-radius: 4px; 923 | margin: 0 2px; 924 | cursor: pointer; 925 | font-size: 0.8em; 926 | } 927 | 928 | span.pragmadots:hover { 929 | background-color: var(--hint); 930 | } 931 | span.pragmawrap { 932 | display: none; 933 | } 934 | 935 | span.attachedType { 936 | display: none; 937 | visibility: hidden; 938 | } 939 | -------------------------------------------------------------------------------- /docs/shell.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | shell 21 | 22 | 23 | 24 | 25 | 62 | 63 | 64 | 65 |
66 |
67 |

shell

68 |
69 |
70 |
71 | 75 |     Dark Mode 76 |
77 | 84 |
85 | Search: 87 |
88 |
89 | Group by: 90 | 94 |
95 | 152 | 153 |
154 |   Source 157 |   Edit 158 | 159 |
160 |
161 | 162 |

163 |
164 |

Types

165 |
166 | 167 |
DebugOutputKind = enum
168 |   dokCommand, dokError, dokOutput, dokRuntime
169 |
170 | 171 | 172 |   Source 175 |   Edit 176 | 177 |
178 | 179 |
ShellExecError = ref object of CatchableError
180 |   cmd*: string               ## Command that returned non-zero exit code
181 |   cwd*: string               ## Absolute path of initial command execution directory
182 |   retcode*: int              ## Exit code
183 |   errstr*: string            ## Stderr for command
184 |   outstr*: string            ## Stdout for command
185 |   
186 |
187 | 188 | 189 |   Source 192 |   Edit 193 | 194 |
195 | 196 |
197 |
198 |

Procs

199 |
200 | 201 |
proc asgnShell(cmd: string;
202 |                debugConfig: set[DebugOutputKind] = defaultDebugConfig;
203 |                options: set[ProcessOption] = defaultProcessOptions): tuple[
204 |     output, error: string, exitCode: int] {...}{.
205 |     raises: [OSError, Exception, IOError], tags: [ExecIOEffect, ReadEnvEffect,
206 |     RootEffect, ReadIOEffect, TimeEffect, WriteIOEffect].}
207 |
208 | 209 | wrapper around execCmdEx, which returns the output of the shell call as a string (stripped of n) 210 |   Source 213 |   Edit 214 | 215 |
216 | 217 |
proc execShell(cmd: string;
218 |                debugConfig: set[DebugOutputKind] = defaultDebugConfig;
219 |                options: set[ProcessOption] = defaultProcessOptions): tuple[
220 |     output, error: string, exitCode: int] {...}{.
221 |     raises: [OSError, Exception, IOError], tags: [ExecIOEffect, ReadEnvEffect,
222 |     RootEffect, ReadIOEffect, TimeEffect, WriteIOEffect].}
223 |
224 | 225 | wrapper around asgnShell, which calls the commands and handles return values. 226 |   Source 229 |   Edit 230 | 231 |
232 | 233 |
234 |
235 |

Macros

236 |
237 | 238 |
macro shellVerbose(args: varargs[untyped]): untyped
239 |
240 | 241 |

See the shell macro below for a general explanation.

242 |

This macro differs from shell in as such that it

243 |
  1. returns a tuple of
  2. 244 |
245 |

  • output: string <- output of the shell command to stdout
  • 246 |
  • exitCode: int <- the exit code as an integer
  • 247 |
248 |

249 |
  1. allows to customize the error output behavior by handing the
  2. 250 |
251 |

argument debugConfig (see below) as well as the process options with which startProcess is called by using the options argument.

252 |

As you notice the macro signature is args: varargs[untyped]. This macro parses the given arguments manually (to allow multiple named arguments in an untyped macro). If the arguments are not named, they are expected in the order as shown below. combineOutAndErr has to be named!

253 |

The following arguments are possible:

254 |
  • debug, debugConfig: a set of DebugOutputKind
  • 255 |
  • options, processOptions: a set of ProcessOption (see stdlib.osproc)
  • 256 |
  • combineOutAndErr: a static bool to decide if the macro should return a 2 tuple of (output: string, errCode: int) (stderr is appended to stdout) or a 3 tuple of (output, outerr: string, errCode: int) (stderr separate) The latter can also be had by using the shellVerboseErr overload below.
  • 257 |
258 | 259 |

Example:

260 |
let (res, code) = shellVerbose(debug = {dokCommand}, options = {poEvalCommand},
261 |                                combineOutAndErr = true):
262 |   echo "test"
263 | 
264 | assert res == "test"
265 | assert code == 0
266 |   Source 269 |   Edit 270 | 271 |
272 | 273 |
macro shellVerboseErr(args: varargs[untyped]): untyped
274 |
275 | 276 | Run shell command, return (stdout, stderr, code). This is an overload of shellVerbose with combineOutAndErr = false by default. 277 |

Example:

278 |
let (res, err, code) = shellVerboseErr {dokCommand}:
279 |   echo "test"
280 | 
281 | assert res == "test"
282 | assert code == 0
283 |   Source 286 |   Edit 287 | 288 |
289 | 290 |
macro shell(cmds: untyped): untyped
291 |
292 | 293 |

A mini DSL to write shell commands in Nim. Some constructs are not implemented. If in doubt, put (parts of) the command into " ".

294 |

The command is echoed before it is run. It is prefixed by

295 |

296 | shellCmd:
297 | 
If there is output, the output is echoed. Each successive line of the output is prefixed by

298 |

299 | shell>
300 | 

301 |

If multiple commands are run in succession (i.e. multiple statements in the macro body) and one command returns a non-zero exit code, the following commands will not be run. Instead a warning message will be shown.

302 |

For usage with NimScript the output can only be echoed after the call has finished.

303 |

The exit code of the command is dropped. If you wish to inspect the exit code, use shellVerbose above.

304 | 305 |   Source 308 |   Edit 309 | 310 |
311 | 312 |
macro shellEcho(cmds: untyped): untyped
313 |
314 | 315 | a helper macro around the proc that generates the shell commands to check whether the commands are as expected It echoes the commands at compile time (the representation of the command) and also the resulting string (taking into account potential) Nim symbol quoting at run time 316 |   Source 319 |   Edit 320 | 321 |
322 | 323 |
macro checkShell(cmds: untyped; exp: untyped): untyped
324 |
325 | 326 | a wrapper around the shell macro, which can calls unittest.check to check whether construction of the commands works as expected 327 |   Source 330 |   Edit 331 | 332 |
333 | 334 |
macro shellAssign(cmd: untyped): untyped
335 |
336 | 337 | 338 |   Source 341 |   Edit 342 | 343 |
344 | 345 |
346 | 347 |
348 |
349 | 350 |
351 | 356 |
357 |
358 |
359 | 360 | 361 | 362 | -------------------------------------------------------------------------------- /docs/shell.idx: -------------------------------------------------------------------------------- 1 | dokCommand shell.html#dokCommand DebugOutputKind.dokCommand 2 | dokError shell.html#dokError DebugOutputKind.dokError 3 | dokOutput shell.html#dokOutput DebugOutputKind.dokOutput 4 | dokRuntime shell.html#dokRuntime DebugOutputKind.dokRuntime 5 | DebugOutputKind shell.html#DebugOutputKind shell: DebugOutputKind 6 | ShellExecError shell.html#ShellExecError shell: ShellExecError 7 | asgnShell shell.html#asgnShell,string,set[DebugOutputKind],set[ProcessOption] shell: asgnShell(cmd: string; debugConfig: set[DebugOutputKind] = defaultDebugConfig;\n options: set[ProcessOption] = defaultProcessOptions): tuple[\n output, error: string, exitCode: int] 8 | execShell shell.html#execShell,string,set[DebugOutputKind],set[ProcessOption] shell: execShell(cmd: string; debugConfig: set[DebugOutputKind] = defaultDebugConfig;\n options: set[ProcessOption] = defaultProcessOptions): tuple[\n output, error: string, exitCode: int] 9 | shellVerbose shell.html#shellVerbose.m,varargs[untyped] shell: shellVerbose(args: varargs[untyped]): untyped 10 | shellVerboseErr shell.html#shellVerboseErr.m,varargs[untyped] shell: shellVerboseErr(args: varargs[untyped]): untyped 11 | shell shell.html#shell.m,untyped shell: shell(cmds: untyped): untyped 12 | shellEcho shell.html#shellEcho.m,untyped shell: shellEcho(cmds: untyped): untyped 13 | checkShell shell.html#checkShell.m,untyped,untyped shell: checkShell(cmds: untyped; exp: untyped): untyped 14 | shellAssign shell.html#shellAssign.m,untyped shell: shellAssign(cmd: untyped): untyped 15 | -------------------------------------------------------------------------------- /shell.nim: -------------------------------------------------------------------------------- 1 | import macros, options 2 | when not defined(NimScript): 3 | import osproc, streams, os 4 | export osproc 5 | else: 6 | type 7 | # dummy enum, which has no effect on NimScript 8 | ProcessOption* = enum 9 | poEchoCmd, poUsePath, poEvalCommand, poStdErrToStdOut, poParentStreams, 10 | poInteractive, poDaemon 11 | import strutils, strformat 12 | export strformat 13 | 14 | type 15 | InfixKind = enum 16 | ifSlash = "/" 17 | ifBackSlash = "\\" 18 | ifGreater = ">" 19 | ifSmaller = "<" 20 | ifDash = "-" 21 | ifPipe = "|" 22 | ifAnd = "&&" 23 | 24 | DebugOutputKind* = enum 25 | dokCommand 26 | dokError 27 | dokOutput 28 | dokRuntime 29 | 30 | type 31 | ShellExecError* = ref object of CatchableError 32 | cmd*: string ## Command that returned non-zero exit code 33 | cwd*: string ## Absolute path of initial command execution directory 34 | retcode*: int ## Exit code 35 | errstr*: string ## Stderr for command 36 | outstr*: string ## Stdout for command 37 | 38 | Expect* = object 39 | init*: bool # object was initialized 40 | expect*: Option[string] # the string we expect 41 | send*: string # the command we send as a response 42 | 43 | ShellCmd* = object 44 | cmds*: seq[string] 45 | expects*: seq[Expect] 46 | 47 | proc initExpect(init: bool): Expect = Expect(init: init) 48 | 49 | proc add(s: var ShellCmd, cmd: string) = 50 | s.cmds.add cmd 51 | 52 | proc `[]`(s: ShellCmd, idx: int): string = s.cmds[idx] 53 | 54 | proc len(s: ShellCmd): int = s.cmds.len 55 | 56 | iterator items(s: ShellCmd): string = 57 | for cmd in s.cmds: 58 | yield cmd 59 | 60 | const defaultDebugConfig: set[DebugOutputKind] = 61 | block: 62 | var config: set[DebugOutputKind] = { 63 | dokOutput, dokError, dokCommand, dokRuntime 64 | } 65 | 66 | when defined shellNoDebugOutput: 67 | config = config - {dokOutput} 68 | 69 | when defined shellNoDebugError: 70 | config = config - {dokError} 71 | 72 | when defined shellNoDebugCommand: 73 | config = config - {dokCommand} 74 | 75 | when defined shellNoDebugRuntime: 76 | config = config - {dokRuntime} 77 | 78 | when defined shellThrowException: 79 | config = {} 80 | 81 | config 82 | 83 | when defined(windows): 84 | const defaultProcessOptions: set[ProcessOption] = {poStdErrToStdOut, poEvalCommand, poDaemon, poUsePath} 85 | else: 86 | const defaultProcessOptions: set[ProcessOption] = {poStdErrToStdOut, poEvalCommand} 87 | # this default is used for `shellVerboseErr` where we do ``not`` want to combine stdout 88 | # and stderr. 89 | const defaultProcessOptionsErr: set[ProcessOption] = {poEvalCommand} 90 | 91 | proc stringify(cmd: NimNode): string 92 | proc iterateTree(cmds: NimNode, joinBy = " "): string 93 | 94 | proc replaceInfixKind(ifKind: InfixKind): string = 95 | case ifKind 96 | of ifSlash, ifBackSlash: 97 | result = $ifKind 98 | else: 99 | result = " " & $ifKind & " " 100 | 101 | proc handleInfix(n: NimNode): NimNode = 102 | ## reorder the tree of the infix 103 | ## TODO: we could just use `unpackInfix` ? 104 | result = nnkIdentDefs.newTree() 105 | result.add n[1] 106 | result.add n[0] 107 | result.add n[2] 108 | 109 | proc handleDotExpr(n: NimNode): string = 110 | ## string value for a dot expr 111 | var stmts = nnkIdentDefs.newTree() 112 | stmts.add n[0] 113 | stmts.add ident(".") 114 | stmts.add n[1] 115 | for el in stmts: 116 | result.add iterateTree(nnkIdentDefs.newTree(el)) 117 | 118 | proc recurseInfix(n: NimNode): string = 119 | ## replace infix tree by an identDefs tree in correct order 120 | ## and a string node in place of the previous "infixed" symbol 121 | var m = copy(n) 122 | let ifKind = parseEnum[InfixKind](m[0].strVal) 123 | # replace the infix symbol 124 | m[0] = newLit(replaceInfixKind(ifKind)) 125 | let inTree = handleInfix(m) 126 | for el in inTree: 127 | result.add iterateTree(nnkIdentDefs.newTree(el)) 128 | 129 | proc handlePrefix(n: NimNode): string = 130 | ## handle `nnkPrefix` 131 | var m = copy(n) 132 | result = m[0].strVal 133 | m.del(0) 134 | result.add iterateTree(m) 135 | 136 | proc handleVarTy(n: NimNode): string = 137 | ## varTy replaces our `out` with a `var`. Replace manually 138 | result = "out" 139 | if n.len > 0: 140 | result.add " " & iterateTree(nnkIdentDefs.newTree(n[0])) 141 | 142 | proc rawString(n: NimNode): string = 143 | ## converts an identifier that is given in accented quotes to 144 | ## a raw string literal in quotation marks 145 | expectKind n, nnkAccQuoted 146 | result = "\"" & n[0].strVal & "\"" 147 | 148 | proc handleQuote(n: NimNode): NimNode = 149 | ## reconstruct a tree with `$` prefix / infix replaced by 150 | ## curly of repr 151 | proc addArg(res: var NimNode, n: NimNode) = 152 | let arg = handleQuote(n) 153 | case arg.kind 154 | of nnkIdentDefs, nnkCommand: 155 | res.add nnkCurly.newTree(arg[0]) 156 | res.add arg[1] 157 | else: 158 | res.add nnkCurly.newTree(arg) 159 | 160 | if n.len == 0: 161 | result = n # keep node 162 | else: 163 | # if tree, modify from existing form replacing `$` calls by `nnkCurly` 164 | case n.kind 165 | of nnkPrefix: 166 | if eqIdent(n[0], "$"): # if `$` prefix, rewrite to reordered nnkIdentDefs 167 | result = nnkIdentDefs.newTree() 168 | result.addArg(n[1]) 169 | else: 170 | result = n 171 | of nnkCallStrLit: 172 | # `$foo"someString"` rewrite to ident defs 173 | result = nnkIdentDefs.newTree(handleQuote(n[0]), handleQuote(n[1])) 174 | of nnkCommand: 175 | # `"someString"$foo` rewrite to ident defs 176 | result = nnkCommand.newTree(handleQuote(n[0]), handleQuote(n[1])) 177 | of nnkInfix: 178 | if eqIdent(n[0], "$"): 179 | # reorder infix and rewrite as nnkIdentDefs 180 | result = nnkIdentDefs.newTree() 181 | result.add handleQuote(n[1]) 182 | result.addArg(n[2]) 183 | else: 184 | result = n 185 | else: 186 | # keep as is, possibly replace children 187 | result = newTree(n.kind) 188 | for ch in n: 189 | result.add handleQuote(ch) 190 | 191 | proc parensUnquotePrefix(n: NimNode): string = 192 | let res = handleQuote(n) 193 | result = stringify(res) 194 | 195 | proc nimSymbol(n: NimNode, useParens: static bool): string = 196 | ## converts the identifier given in accented quotes to a Nim symbol 197 | ## quoted in `{}` using strformat 198 | when declared(oldQuote): 199 | expectKind n, nnkAccQuoted 200 | if eqIdent(n[0], "$"): 201 | result = "{" & n[1].strVal & "}" 202 | else: 203 | error("Unsupported symbol in accented quotes: " & $n.repr) 204 | else: 205 | expectKind n, nnkPar 206 | result = parensUnquotePrefix(n[0]) 207 | 208 | proc handleCall(n: NimNode): string = 209 | ## converts the given `NimNode` representing a call to a string. The call 210 | ## corresponds to usage of `()`, thus a quoting of a nim identifier. 211 | ## Specifically, this corresponds to the case in which some identifier 212 | ## or string literal appears right before a quoted nim identifier, so that 213 | ## the value of the quoted identifier is placed right after the first 214 | ## argument. 215 | ## Assuming `outname` defines a string with value `test.h5`, then: 216 | ## Call 217 | ## StrLit "--out=" 218 | ## Prefix 219 | ## Ident "$" 220 | ## Ident "outname" 221 | ## -> "--out=test.h5" 222 | expectKind n[1], nnkPrefix 223 | result = stringify(n[0]) & parensUnquotePrefix(n[1]) 224 | 225 | proc stringify(cmd: NimNode): string = 226 | ## Handles the stringification of a single `NimNode` according to its 227 | ## `NimNodeKind`. 228 | case cmd.kind 229 | of nnkCommand: 230 | result = iterateTree(cmd) 231 | of nnkCall: 232 | # call may appear when quoting with `()` without space after previous 233 | # element 234 | result = handleCall(cmd) 235 | of nnkPrefix: 236 | result = handlePrefix(cmd) 237 | of nnkIdent: 238 | result = cmd.strVal 239 | of nnkDotExpr: 240 | result = handleDotExpr(cmd) 241 | of nnkStrLit, nnkTripleStrLit, nnkRStrLit: 242 | result = cmd.strVal 243 | of nnkCallStrLit: 244 | result = stringify(cmd[0]) & stringify(cmd[1]) 245 | of nnkIntLit, nnkFloatLit: 246 | result = cmd.repr 247 | of nnkVarTy, nnkMutableTy: 248 | # `nnkMutableTy` required for https://github.com/nim-lang/Nim/issues/15751 249 | result = handleVarTy(cmd) 250 | of nnkInfix: 251 | result = recurseInfix(cmd) 252 | of nnkCurly: 253 | doAssert cmd.len == 1 254 | result = "{" & cmd[0].repr & "}" 255 | of nnkIdentDefs: 256 | result = iterateTree(cmd, joinBy = "") 257 | of nnkAccQuoted: 258 | # handle accented quotes. Allows to either have the content be put into 259 | # a raw string literal, or if prefixed by `$` assumed to be a Nim symbol 260 | case cmd.len 261 | of 1: 262 | result = rawString(cmd) 263 | of 2: 264 | when declared(oldQuote): 265 | result = nimSymbol(cmd, useParens = false) 266 | else: 267 | error("API change: for quoting use ()! Compile with -d:oldQuote for grace period." & 268 | "Offending command: " & cmd.repr) 269 | else: 270 | error("Unsupported quoting: " & $cmd.kind & " for command " & cmd.repr) 271 | of nnkPar: 272 | when not declared(oldQuote): 273 | result = nimSymbol(cmd, useParens = true) 274 | else: 275 | error("Quoting via () only allowed if compiled without -d:oldQuote!" & 276 | "Relevant command: " & cmd.repr) 277 | of nnkStmtList: 278 | doAssert cmd.len == 1, "nnkStmtList should only appear in the context of `expect` or `send` and must " & 279 | "only have a single argument. Blocks of commands are not supported." 280 | result = stringify(cmd[0]) 281 | else: 282 | error("Unsupported node kind: " & $cmd.kind & " for command " & cmd.repr & 283 | ". Consider putting offending part into \" \".") 284 | 285 | proc iterateTree(cmds: NimNode, joinBy = " "): string = 286 | ## main proc which iterates over tree and assigns assigns the correct 287 | ## strings to `subCmds` depending on NimNode kind 288 | var subCmds: seq[string] 289 | for cmd in cmds: 290 | subCmds.add stringify(cmd) 291 | result = subCmds.join(joinBy) 292 | 293 | proc concatCmds(cmds: seq[string], sep = " && "): string = 294 | ## concat commands to single string, by default via `&&` 295 | result = cmds.join(sep) 296 | 297 | proc concatCmds(cmd: ShellCmd, sep = " && "): string = 298 | ## concat commands to single string, by default via `&&` 299 | result = cmd.cmds.join(sep) 300 | 301 | proc getPrompt(pid: int): string = 302 | const PrintPid {.booldefine.} = true 303 | when PrintPid: 304 | result = "shell " & $pid & "> " 305 | else: 306 | result = "shell> " 307 | 308 | proc asgnShell*( 309 | cmd: string, 310 | expects: var seq[Expect], 311 | debugConfig: set[DebugOutputKind] = defaultDebugConfig, 312 | options: set[ProcessOption] = defaultProcessOptions 313 | ): tuple[output, error: string, exitCode: int] = 314 | ## wrapper around `execCmdEx`, which returns the output of the shell call 315 | ## as a string (stripped of `\n`) 316 | when nimvm: 317 | block: 318 | # prepend the NimScript called command by current directory 319 | let nscmd = &"cd {getProjectPath()} && {cmd}" 320 | let (res, code) = gorgeEx(nscmd, "", "") 321 | result.output = res 322 | result.exitCode = code 323 | else: 324 | when not defined(NimScript): 325 | when defined(windows): 326 | var prcs: Process 327 | try: 328 | prcs = startProcess(cmd, options = options) 329 | except OSError as e: 330 | let exitCode = 1 331 | let err = e.msg 332 | return (output: "", error: err, exitCode: exitCode) 333 | else: 334 | let prcs = startProcess(cmd, options = options) 335 | 336 | let pid = prcs.processId 337 | let outStream = prcs.outputStream 338 | let inStream = prcs.inputStream 339 | var line = "" 340 | var res = "" 341 | var exp = if expects.len > 0: expects.pop else: initExpect(init = false) 342 | while prcs.running: 343 | try: 344 | let streamRes = outStream.readLine(line) 345 | if streamRes: 346 | if dokOutput in debugConfig: 347 | echo getPrompt(pid), line 348 | res = res & "\n" & line 349 | # now check if we expect a line and this line matches 350 | if exp.init and # if any 351 | (exp.expect.isNone or # either if we don't expect, just `send` 352 | (exp.expect.isSome and # or if expect text 353 | (line == exp.expect.get or # as long as it matches in some sense 354 | line.startsWith(exp.expect.get) or 355 | line.endsWith(exp.expect.get)))): 356 | inStream.write(exp.send & "\n") 357 | inStream.flush() 358 | exp = if expects.len > 0: expects.pop else: initExpect(init = false) 359 | else: 360 | # should mean stream is finished, i.e. process stoped 361 | sleep 10 362 | doAssert not prcs.running 363 | break 364 | except IOError, OSError: 365 | # outstream died on us? 366 | doAssert outStream.isNil 367 | break 368 | 369 | if not outStream.atEnd(): 370 | if dokOutput in debugConfig: 371 | let rem = outStream.readAll() 372 | res &= rem 373 | for line in rem.split("\n"): 374 | echo getPrompt(pid), line 375 | else: 376 | res &= outStream.readAll() 377 | 378 | if exp.init: # if `exp` is still initialized, it means it wasn't consumed 379 | expects.insert(exp, 0) 380 | 381 | let exitCode = prcs.peekExitCode 382 | 383 | # Zero exit code does not guarantee that there will be nothing in 384 | # stderr. 385 | let err = prcs.errorStream 386 | let errorText = err.readAll() 387 | 388 | if exitCode != 0: 389 | if dokRuntime in debugConfig: 390 | echo "Error when executing: ", cmd 391 | 392 | if dokError in debugConfig: 393 | for line in errorText.split("\n"): 394 | echo "err> ", line 395 | 396 | prcs.close() 397 | result = (output: res, error: errorText, exitCode: exitCode) 398 | 399 | else: 400 | # prepend the NimScript called command by current directory 401 | let nscmd = &"cd {getCurrentDir()} && " & cmd 402 | let (res, code) = gorgeEx(nscmd, "", "") 403 | result.output = res 404 | result.exitCode = code 405 | result.output = result.output.strip(chars = {'\n'}) 406 | result.error = result.error.strip(chars = {'\n'}) 407 | 408 | proc execShell*( 409 | cmd: string, 410 | expects: var seq[Expect], ## mutable as we pop each element if we encounter the `expect` 411 | debugConfig: set[DebugOutputKind] = defaultDebugConfig, 412 | options: set[ProcessOption] = defaultProcessOptions 413 | ): tuple[output, error: string, exitCode: int] = 414 | ## wrapper around `asgnShell`, which calls the commands and handles 415 | ## return values. 416 | if dokCommand in debugConfig: 417 | echo "shellCmd: ", cmd 418 | 419 | result = asgnShell(cmd, expects, debugConfig, options) 420 | if dokOutput in debugConfig: 421 | when nimvm: 422 | if result[0].len > 0: 423 | for line in splitLines(result[0]): 424 | echo "shell> ", line 425 | else: 426 | when defined(NimScript): 427 | # output of child process is already echoed on the fly for non NimScript 428 | # usage 429 | if result[0].len > 0: 430 | for line in splitLines(result[0]): 431 | echo "shell> ", line 432 | 433 | when defined shellThrowException: 434 | let cwd = getCurrentDir() 435 | if result.exitCode != 0: 436 | raise ShellExecError( 437 | msg: "Command " & cmd & " exited with non-zero code", 438 | cmd: cmd, 439 | cwd: cwd, 440 | retcode: result.exitCode, 441 | errstr: result.error, 442 | outstr: result.output 443 | ) 444 | 445 | proc flattenCmds(cmds: NimNode): NimNode = 446 | ## removes nested StmtLists, if any 447 | case cmds.kind 448 | of nnkStmtList: 449 | if cmds.len == 1 and cmds[0].kind == nnkStmtList: 450 | result = flattenCmds(cmds[0]) 451 | else: 452 | result = cmds 453 | else: 454 | result = cmds 455 | 456 | proc genShellCmds(cmds: NimNode): ShellCmd = 457 | ## the proc that actually generates the shell commands 458 | ## from the given statements 459 | # first strip potential nested StmtLists from input 460 | let flatCmds = flattenCmds(cmds) 461 | var exp = initExpect(init = false) 462 | 463 | # iterate over all commands in the command list 464 | for cmd in flatCmds: 465 | case cmd.kind 466 | of nnkCall: 467 | if eqIdent(cmd[0], "one"): 468 | # in this case call this proc on content 469 | let oneCmd = genShellCmds(cmd[1]) 470 | # and concat them to a valid concat of shell calls 471 | result.add concatCmds(oneCmd) 472 | elif eqIdent(cmd[0], "pipe"): 473 | # in this case call this proc on content 474 | let pipeCmd = genShellCmds(cmd[1]) 475 | # and concat them to a valid string of piped commands 476 | result.add concatCmds(pipeCmd, sep = " | ") 477 | elif eqIdent(cmd[0], "expect"): 478 | exp = initExpect(init = true) 479 | exp.expect = some(stringify(cmd[1])) 480 | elif eqIdent(cmd[0], "send"): 481 | if exp.expect.isNone: # case without explicit `expect` 482 | exp.init = true 483 | exp.send = stringify(cmd[1]) 484 | result.expects.add exp 485 | exp = initExpect(init = false) # reset the `exp` 486 | of nnkCommand: 487 | result.add iterateTree(cmd) 488 | of nnkIdent, nnkStrLit, nnkTripleStrLit: 489 | result.add cmd.strVal 490 | of nnkPrefix, nnkAccQuoted: 491 | result.add iterateTree(nnkIdentDefs.newTree(cmd)) 492 | of nnkPar: 493 | result.add cmd.stringify 494 | of nnkAsgn: 495 | # handle first child, then second 496 | let lhs = stringify(cmd[0]) 497 | let rhs = stringify(cmd[1]) 498 | result.add concatCmds(@[lhs, rhs], sep = "=") 499 | else: 500 | error("Unsupported node kind: " & $cmd.kind & " for command " & cmd.repr & 501 | ". Consider putting offending part into \" \".") 502 | 503 | proc nilOrQuote(cmd: string): NimNode = 504 | ## either returns a string literal node if the given command does 505 | ## not contain curly brackets (indicating a Nim symbol is quoted) 506 | ## or prefix a `&` to call strformat 507 | if "{" in cmd and "}" in cmd: 508 | result = nnkPrefix.newTree(ident"&", newLit(cmd)) 509 | else: 510 | result = newLit(cmd) 511 | 512 | 513 | from std / algorithm import reversed 514 | proc shellVerboseImpl(debugConfig: NimNode, 515 | options: NimNode, 516 | combineOutAndErr: bool, 517 | cmds: NimNode): NimNode = 518 | ## This is the compile time proc, which creates the actual code of the shell macro. 519 | ## Depending on `combineOutAndErr` it returns either a 2 tuple or a 3 tuple. 520 | expectKind cmds, nnkStmtList 521 | result = newStmtList() 522 | let shCmds = genShellCmds(cmds) 523 | 524 | # we use two temporary variables. One to store total output of all commands 525 | # and the other to store the last exitCode. 526 | let exCodeSym = genSym(nskVar, "exitCode") 527 | let outputSym = genSym(nskVar, "outputStr") 528 | let outerrSym = genSym(nskVar, "outerrStr") 529 | result.add quote do: 530 | var `outputSym` = "" 531 | var `exCodeSym`: int 532 | var `outerrSym` = "" 533 | 534 | var expects = shCmds.expects.reversed # reverse so that `pop` gives us first element 535 | for cmd in shCmds: 536 | let qCmd = nilOrQuote(cmd) 537 | let expId = genSym(nskVar, "expects") 538 | result.add quote do: 539 | # use the exit code to determine if next command should be run 540 | if `exCodeSym` == 0: 541 | var `expId` = @`expects` 542 | let tmp = execShell(`qCmd`, `expId`, `debugConfig`, `options`) 543 | `outputSym` = `outputSym` & tmp[0] 544 | `outerrSym` = tmp[1] 545 | `exCodeSym` = tmp[2] 546 | else: 547 | if dokRuntime in `debugConfig`: 548 | echo "Skipped command `" & 549 | `qCmd` & 550 | "` due to failure in previous command!" 551 | 552 | var resBody: NimNode = newStmtList() 553 | resBody.add result 554 | if combineOutAndErr: 555 | # possibly combine stdout & stderr by appending the latter and return 2 tuple 556 | resBody.add quote do: 557 | ## TODO: this should not be necessary if we hand `poStdErrToStdOut` no? 558 | `outputSym` = `outputSym` & `outerrSym` 559 | (`outputSym`, `exCodeSym`) 560 | else: 561 | # define the tuple 3 tuple we return in case we keep error 562 | resBody.add quote do: 563 | (`outputSym`, `outerrSym`, `exCodeSym`) 564 | result = quote do: 565 | block: 566 | `resBody` 567 | 568 | when defined(debugShell): 569 | echo result.repr 570 | 571 | template parseTmpl(procName, enumType, name: untyped): untyped = 572 | proc `procName`(n: NimNode): NimNode = 573 | case n.kind 574 | of nnkIdent, nnkSym: result = n 575 | of nnkCurly: 576 | for ch in n: 577 | # check if this can be parsed as `DebugOutputKind` 578 | case ch.kind 579 | of nnkIdent: 580 | discard parseEnum[enumType](ch.strVal) 581 | of nnkCall: 582 | doAssert ch[0].strVal == astToStr(enumType) 583 | of nnkSym: 584 | # we trust that this is a valid symbol for an enum field 585 | discard 586 | else: 587 | error("Argument to `" & $name & "` in curly must be an identifier corresponding " & 588 | "to a field of `" & astToStr(enumType) & "`! Argument is: " & 589 | $(ch.repr) & " of kind " & $(ch.kind)) 590 | result = n 591 | else: 592 | error("Invalid node kind for `" & $name & "`: " & $n.kind & "!") 593 | 594 | parseTmpl(parseDebugConfig, DebugOutputKind, "debug") 595 | parseTmpl(parseProcessOptions, ProcessOption, "options") 596 | 597 | proc parseCombineAndErr(n: NimNode): bool = 598 | case n.kind 599 | of nnkIdent, nnkSym: result = parseBool(n.strVal) 600 | else: 601 | error("Invalid argument to `combineOutAndErr` in `shellVerbose`! Requires a " & 602 | "bool literal!") 603 | 604 | proc parseShellVerboseArgs(combineOutAndErrDefault: bool, 605 | args: NimNode): (NimNode, NimNode, bool, NimNode) = 606 | var 607 | cmds: NimNode 608 | cfgArg: NimNode 609 | optArg: NimNode 610 | combineOutAndErr: bool = combineOutAndErrDefault # default is true 611 | expectKind(args, nnkArglist) 612 | expectKind(args[args.len - 1], nnkStmtList) 613 | var idx = 0 614 | for arg in args: 615 | case arg.kind 616 | of nnkExprEqExpr: 617 | let argStr = arg[0].strVal 618 | const allowedArgs = ["debug", "debugConfig", "options", "processOptions", "combineOutAndErr"] 619 | case argStr 620 | of "debug", "debugConfig": 621 | cfgArg = parseDebugConfig(arg[1]) 622 | of "options", "processOptions": 623 | optArg = parseProcessOptions(arg[1]) 624 | of "combineOutAndErr": 625 | combineOutAndErr = parseCombineAndErr(arg[1]) 626 | else: 627 | error("Invalid named argument to `shellVerbose`: " & $argStr & ". Allowed arguments: " & $allowedArgs) 628 | of nnkCurly: 629 | if idx == 0: 630 | cfgArg = parseDebugConfig(arg) 631 | elif idx == 1: 632 | optArg = parseProcessOptions(arg) 633 | else: 634 | error("Invalid unnamed argument at index " & $idx & "!") 635 | of nnkStmtList, nnkIdent: 636 | cmds = arg 637 | else: 638 | error("Invalid node kind encountered while parsing args of `shellVerbose`! " & 639 | "Node is " & $(arg.repr) & " of kind " & $(arg.kind)) 640 | inc idx 641 | if cfgArg.isNil: 642 | cfgArg = newLit defaultDebugConfig 643 | if optArg.isNil: 644 | if combineOutAndErrDefault: 645 | optArg = newLit defaultProcessOptions 646 | else: 647 | optArg = newLit defaultProcessOptionsErr 648 | result = (cfgArg, optArg, combineOutAndErr, cmds) 649 | 650 | macro shellVerbose*(args: varargs[untyped]): untyped = 651 | ## See the `shell` macro below for a general explanation. 652 | ## 653 | ## This macro differs from `shell` in as such that it 654 | ## 1. returns a tuple of 655 | ## - `output: string` <- output of the shell command to stdout 656 | ## - `exitCode: int` <- the exit code as an integer 657 | ## 2. allows to customize the error output behavior by handing the 658 | ## argument `debugConfig` (see below) as well as the process options 659 | ## with which `startProcess` is called by using the `options` argument. 660 | ## 661 | ## As you notice the macro signature is `args: varargs[untyped]`. This 662 | ## macro parses the given arguments manually (to allow multiple named arguments 663 | ## in an untyped macro). If the arguments are not named, they are expected 664 | ## in the order as shown below. `combineOutAndErr` has to be named! 665 | ## 666 | ## The following arguments are possible: 667 | ## - `debug`, `debugConfig`: a set of `DebugOutputKind` 668 | ## - `options`, `processOptions`: a set of `ProcessOption` (see `stdlib.osproc`) 669 | ## - `combineOutAndErr`: a static bool to decide if the macro should return a 670 | ## 2 tuple of `(output: string, errCode: int)` (`stderr` is appended to `stdout`) 671 | ## or a 3 tuple of `(output, outerr: string, errCode: int)` (`stderr` separate) 672 | ## The latter can also be had by using the `shellVerboseErr` overload below. 673 | runnableExamples: 674 | let (res, code) = shellVerbose(debug = {dokCommand}, options = {poEvalCommand}, 675 | combineOutAndErr = true): 676 | echo "test" 677 | 678 | assert res == "test" 679 | assert code == 0 680 | 681 | let (cfgArg, optArg, combineOutAndErr, cmds) = parseShellVerboseArgs( 682 | combineOutAndErrDefault = true, 683 | args 684 | ) 685 | result = shellVerboseImpl(cfgArg, optArg, combineOutAndErr = combineOutAndErr, cmds) 686 | 687 | macro shellVerboseErr*(args: varargs[untyped]): untyped = 688 | ## Run shell command, return `(stdout, stderr, code)`. This is an overload 689 | ## of `shellVerbose` with `combineOutAndErr = false` by default. 690 | runnableExamples: 691 | let (res, err, code) = shellVerboseErr {dokCommand}: 692 | echo "test" 693 | 694 | assert res == "test" 695 | assert code == 0 696 | 697 | let (cfgArg, optArg, combineOutAndErr, cmds) = parseShellVerboseArgs( 698 | combineOutAndErrDefault = false, 699 | args 700 | ) 701 | result = shellVerboseImpl(cfgArg, optArg, combineOutAndErr = combineOutAndErr, cmds) 702 | 703 | macro shell*(cmds: untyped): untyped = 704 | ## A mini DSL to write shell commands in Nim. Some constructs are not 705 | ## implemented. If in doubt, put (parts of) the command into `" "`. 706 | ## 707 | ## The command is echoed before it is run. It is prefixed by 708 | ## ``` 709 | ## shellCmd: 710 | ## ``` 711 | ## If there is output, the output is echoed. Each successive line of the 712 | ## output is prefixed by 713 | ## ``` 714 | ## shell> 715 | ## ``` 716 | ## 717 | ## If multiple commands are run in succession (i.e. multiple statements in 718 | ## the macro body) and one command returns a non-zero exit code, the following 719 | ## commands will not be run. Instead a warning message will be shown. 720 | ## 721 | ## For usage with NimScript the output can only be echoed after the 722 | ## call has finished. 723 | ## 724 | ## The exit code of the command is dropped. If you wish to inspect 725 | ## the exit code, use `shellVerbose` above. 726 | ## 727 | ## Within the DSL a few extra commands exist. 728 | ## 729 | ## ```nim 730 | ## shell: 731 | ## one: 732 | ## cmd 1 733 | ## cmd 2 ... 734 | ## ``` 735 | ## 736 | ## `one` can be used to run multiple commands in the same invocation. Each command 737 | ## combined via `&&` in the shell. 738 | ## 739 | ## ```nim 740 | ## shell: 741 | ## pipe: 742 | ## cmd 1 743 | ## cmd 2 ... 744 | ## ``` 745 | ## 746 | ## `pipe` can be used to pipe together multiple commands. Each command is combined 747 | ## `|` in the shell. 748 | ## 749 | ## Finally, there is an `expect` / `send` feature, somewhat similar to the `expect(1)` 750 | ## program. 751 | ## 752 | ## ```nim 753 | ## shell: 754 | ## commandThatNeedsInput 755 | ## expect: "Some text to match" 756 | ## send: "Some text to answer" 757 | ## ``` 758 | ## 759 | ## The code tries to match output line after `commandThatNeedsInput` is run by the 760 | ## argument to `expect`. Currently it simply tries to match 761 | ## - the whole line 762 | ## - only the beginning 763 | ## - only the end 764 | ## (this will probably become configurable in the future) 765 | ## Upon a match, the `send` argument will be sent to the process. 766 | result = quote do: 767 | discard shellVerbose(debug = defaultDebugConfig, options = defaultProcessOptions): 768 | `cmds` 769 | 770 | macro shellEcho*(cmds: untyped): untyped = 771 | ## a helper macro around the proc that generates the shell commands 772 | ## to check whether the commands are as expected 773 | ## It echoes the commands at compile time (the representation of the 774 | ## command) and also the resulting string (taking into account potential) 775 | ## Nim symbol quoting at run time 776 | expectKind cmds, nnkStmtList 777 | result = newStmtList() 778 | let shCmds = genShellCmds(cmds) 779 | for cmd in shCmds: 780 | let qCmd = nilOrQuote(cmd) 781 | # echo representation at compile time 782 | echo qCmd.repr 783 | # and echo 784 | result.add quote do: 785 | echo `qCmd` 786 | 787 | macro shellCmd*(cmds: untyped): untyped = 788 | ## a helper macro around the proc that generates the shell commands 789 | ## to get the generated command as a runtime string. 790 | expectKind cmds, nnkStmtList 791 | let shCmds = genShellCmds(cmds) 792 | doAssert shCmds.len == 1, "Can only handle a single command at this time" 793 | var res = "" 794 | #for cmd in shCmds: 795 | # let qCmd = nilOrQuote(cmd) 796 | # # echo representation at compile time 797 | # echo qCmd.repr 798 | # # and echo 799 | # res.add qCmd.str 800 | result = nilOrQuote(shCmds[0]) 801 | 802 | macro checkShell*(cmds: untyped, exp: untyped): untyped = 803 | ## a wrapper around the shell macro, which can calls `unittest.check` to 804 | ## check whether construction of the commands works as expected 805 | expectKind cmds, nnkStmtList 806 | 807 | let shCmds = genShellCmds(cmds) 808 | 809 | if exp.kind == nnkStmtList: 810 | let checkCommand = nilOrQuote(shCmds[0]) 811 | when nimvm: 812 | result = quote do: 813 | doAssert `checkCommand` == `exp[0]` 814 | else: 815 | when not defined(NimScript): 816 | result = quote do: 817 | check `checkCommand` == `exp[0]` 818 | else: 819 | result = quote do: 820 | doAssert `checkCommand` == `exp[0]` 821 | when defined(debugShell): 822 | echo result.repr 823 | 824 | macro shellAssign*(cmd: untyped): untyped = 825 | expectKind cmd, nnkStmtList 826 | expectKind cmd[0], nnkAsgn 827 | doAssert cmd[0].len == 2, "Only a single assignment is allowed!" 828 | 829 | ## in this case assume node 0 is Nim identifier to which we wish 830 | ## to assign value of rest of the nodes 831 | let nimSym = cmd[0][0] 832 | # node 1 is the shell call we make 833 | let cmds = nnkIdentDefs.newTree(cmd[0][1]) 834 | let shCmd = genShellCmds(cmds)[0] 835 | let qCmd = nilOrQuote(shCmd) 836 | 837 | # now get possible expects separately 838 | var expects: seq[Expect] = @[] 839 | if cmd.len > 1: 840 | expects = genShellCmds(cmd).expects.reversed 841 | 842 | let expId = genSym(nskVar, "expects") 843 | result = quote do: 844 | block: 845 | var `expId` = @`expects` 846 | `nimSym` = asgnShell(`qCmd`, `expId`)[0] 847 | 848 | when defined(debugShell): 849 | echo result.repr 850 | -------------------------------------------------------------------------------- /shell.nimble: -------------------------------------------------------------------------------- 1 | # Package 2 | 3 | version = "0.6.0" 4 | author = "Vindaar" 5 | description = "A Nim mini DSL to execute shell commands" 6 | license = "MIT" 7 | 8 | 9 | # Dependencies 10 | 11 | requires "nim >= 0.19.0" 12 | 13 | task test, "executes the tests": 14 | exec "nim c -d:debugShell -r tests/tShell.nim" 15 | exec "nim c -r tests/tException.nim" 16 | # execute using NimScript as well 17 | exec "nim e -d:debugShell -r tests/tNimScript.nims" 18 | # and execute PWD test, by running the nims file in another dir, 19 | # which itself calls the test 20 | when not defined(windows): 21 | exec "cd tests/anotherDir && nim e -r runAnotherTest.nims" 22 | exec "nim c -r tests/tExpect.nim" 23 | 24 | task travis, "executes the tests on travis": 25 | exec "nim c -d:debugShell -d:travisCI -r tests/tShell.nim" 26 | # execute using NimScript as well 27 | exec "nim e -d:debugShell -d:travisCI -r tests/tNimScript.nims" 28 | when not defined(windows): 29 | exec "cd tests/anotherDir && nim e -d:travisCI -r runAnotherTest.nims" 30 | -------------------------------------------------------------------------------- /tests/anotherDir/runAnotherTest.nims: -------------------------------------------------------------------------------- 1 | exec "pwd && nim e tCorrectDir.nims" 2 | -------------------------------------------------------------------------------- /tests/anotherDir/tCorrectDir.nims: -------------------------------------------------------------------------------- 1 | import ../../shell 2 | import strutils 3 | 4 | block: 5 | # check that current working directory is indeed the one we call from 6 | var res = "" 7 | shellAssign: 8 | res = pwd 9 | # result should be the directory "anotherDir" and ``not!`` ``shell``, since 10 | # the test code is run from this directory via the `runAnotherTest.nims` file 11 | doAssert res.endsWith("anotherDir") 12 | -------------------------------------------------------------------------------- /tests/config.nims: -------------------------------------------------------------------------------- 1 | switch("path", "$projectDir/../src") -------------------------------------------------------------------------------- /tests/tException.nim: -------------------------------------------------------------------------------- 1 | {.define(shellThrowException).} 2 | 3 | import ../shell 4 | import unittest 5 | import strutils 6 | 7 | when not defined(windows): 8 | suite "[shell]": 9 | test "[exception] throw on invalid command": 10 | try: 11 | shell: 12 | ls -l 13 | ls -z 14 | except ShellExecError: 15 | let e = cast[ShellExecError](getCurrentException()) 16 | echo e.msg 17 | echo "command was: ", e.cmd 18 | assert e.cmd == "ls -z" 19 | echo "executed in directory:", e.cwd 20 | echo "return code: ", e.retcode 21 | echo "error outpt: " 22 | for l in e.errstr.split('\n'): 23 | echo " ", l 24 | except: 25 | assert false, "Execution must throw exception `ShellExecError`" 26 | -------------------------------------------------------------------------------- /tests/tExpect.nim: -------------------------------------------------------------------------------- 1 | import ../shell 2 | import std / unittest 3 | 4 | # only run on linux due to the tExpect shell script (and not any platform bindings 5 | # of the `expect` support) 6 | when defined(linux): 7 | block: 8 | var res = "" 9 | shellAssign: 10 | res = "./tests/tExpect.sh" 11 | expect: "Your name?" 12 | send: "Vindaar" 13 | check res == """Hello world. Your name? 14 | Your name is Vindaar""" 15 | 16 | block: 17 | # now try without the `expect` 18 | var res = "" 19 | shellAssign: 20 | res = "./tests/tExpect.sh" 21 | send: "Vindaar" 22 | check res == """Hello world. Your name? 23 | Your name is Vindaar""" 24 | -------------------------------------------------------------------------------- /tests/tExpect.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | echo "Hello world. Your name?" 4 | read foo 5 | echo "Your name is" $foo 6 | -------------------------------------------------------------------------------- /tests/tNimScript.nims: -------------------------------------------------------------------------------- 1 | import ../shell 2 | import strutils 3 | 4 | checkShell: 5 | cd "Analysis/ingrid" 6 | do: 7 | "cd Analysis/ingrid" 8 | 9 | checkShell: 10 | cd Analysis/ingrid/stuff 11 | do: 12 | "cd Analysis/ingrid/stuff" 13 | 14 | checkShell: 15 | run test.h5 16 | do: 17 | "run test.h5" 18 | 19 | checkShell: 20 | "cd Analysis" 21 | do: 22 | "cd Analysis" 23 | 24 | checkShell: 25 | nimble develop 26 | do: 27 | "nimble develop" 28 | 29 | checkShell: 30 | ./reconstruction "Run123" "--out" "test.h5" 31 | do: 32 | "./reconstruction Run123 --out test.h5" 33 | 34 | checkShell: 35 | ./reconstruction Run123 "--out" "test.h5" 36 | do: 37 | "./reconstruction Run123 --out test.h5" 38 | 39 | checkShell: 40 | ./reconstruction Run123 "--out" test.h5 41 | do: 42 | "./reconstruction Run123 --out test.h5" 43 | 44 | checkShell: 45 | ./reconstruction Run123 --out test.h5 46 | do: 47 | "./reconstruction Run123 --out test.h5" 48 | 49 | checkShell: 50 | ./reconstruction Run123 --out 51 | do: 52 | "./reconstruction Run123 --out" 53 | 54 | 55 | checkShell: 56 | ./reconstruction Run123 """--out="test.h5"""" 57 | do: 58 | "./reconstruction Run123 --out=\"test.h5\"" 59 | 60 | checkShell: 61 | echo """"test file"""" > test.txt 62 | do: 63 | "echo \"test file\" > test.txt" 64 | 65 | checkShell: 66 | cat test.txt | grep """"file"""" 67 | do: 68 | "cat test.txt | grep \"file\"" 69 | 70 | checkShell: 71 | mkdir foo && rmdir foo 72 | do: 73 | "mkdir foo && rmdir foo" 74 | 75 | checkShell: 76 | echo `Hallo` 77 | do: 78 | "echo \"Hallo\"" 79 | 80 | checkShell: 81 | echo `"Hello World!"` 82 | do: 83 | "echo \"Hello World!\"" 84 | 85 | checkShell: 86 | "a=`echo Hallo`" 87 | do: 88 | "a=`echo Hallo`" 89 | 90 | shellEcho: 91 | ./reconstruction Run123 --out test.h5 92 | 93 | checkShell: 94 | one: 95 | mkdir foo 96 | cd foo 97 | touch bar 98 | cd ".." 99 | do: 100 | "mkdir foo && cd foo && touch bar && cd .." 101 | 102 | checkShell: 103 | pipe: 104 | cat tests/tShell.nim 105 | grep test 106 | head -3 107 | do: 108 | "cat tests/tShell.nim | grep test | head -3" 109 | 110 | let name = "Vindaar" 111 | checkShell: 112 | echo "Hello from" ($name) 113 | do: 114 | &"echo Hello from {name}" 115 | 116 | let dir = "testDir" 117 | checkShell: 118 | tar -czf ($dir).tar.gz 119 | do: 120 | &"tar -czf {dir}.tar.gz" 121 | 122 | block: 123 | # "[shell] quoting a Nim symbol and appending to it using dotExpr": 124 | let dir = "testDir" 125 | checkShell: 126 | tar -czf ($dir).tar.gz 127 | do: 128 | &"tar -czf {dir}.tar.gz" 129 | 130 | block: 131 | # "[shell] unintuitive: quoting a Nim symbol (), appending string": 132 | ## This is a rather unintuitive side effect of the way the Nim parser works. 133 | ## Unfortunately appending a string literal to a quote via `()` will result 134 | ## in a space between the quoted identifier and the string literal. 135 | ## See the test case below, which quotes everything via `()`. 136 | let dir = "testDir" 137 | checkShell: 138 | tar -czf ($dir)".tar.gz" 139 | do: 140 | &"tar -czf {dir} .tar.gz" 141 | 142 | block: 143 | # "[shell] quoting a Nim symbol () and appending string inside the ()": 144 | let dir = "testDir" 145 | checkShell: 146 | tar -czf ($dir".tar.gz") 147 | do: 148 | &"tar -czf {dir}.tar.gz" 149 | 150 | block: 151 | # "[shell] quoting a Nim expression () and appending string inside the ()": 152 | let pdf = "test.pdf" 153 | checkShell: 154 | pdfcrop "--margins '5 5 5 5'" ($pdf) ($(pdf.replace(".pdf",""))"_cropped.pdf") 155 | do: 156 | &"pdfcrop --margins '5 5 5 5' {pdf} {pdf.replace(\".pdf\",\"\")}_cropped.pdf" 157 | 158 | block: 159 | # "[shell] quoting a Nim symbol and appending it to a string without space": 160 | let outname = "test.h5" 161 | checkShell: 162 | ./test "--out="($outname) 163 | do: 164 | &"./test --out={outname}" 165 | 166 | block: 167 | # "[shell] quoting a Nim symbol and appending it within `()`": 168 | let outname = "test.h5" 169 | checkShell: 170 | ./test ("--out="$outname) 171 | do: 172 | &"./test --out={outname}" 173 | 174 | block: 175 | # "[shell] quoting a Nim symbol and appending it within `()` with a space": 176 | ## NOTE: while this works, it is not the recommended way for clarity! 177 | let outname = "test.h5" 178 | checkShell: 179 | ./test ("--out" $outname) 180 | do: 181 | &"./test --out {outname}" 182 | 183 | block: 184 | # "[shell] quoting a Nim symbol and appending it to a string with space": 185 | let outname = "test.h5" 186 | checkShell: 187 | ./test "--out" ($outname) 188 | do: 189 | &"./test --out {outname}" 190 | 191 | block: 192 | # "[shell] quoting a Nim symbol with tuple fields": 193 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 194 | checkShell: 195 | ./test "--in" ($run.name) "--out" ($(run.outName)) 196 | do: 197 | &"./test --in {run.name} --out {run.outName}" 198 | 199 | block: 200 | # "[shell] quoting a Nim symbol with tuple fields, appending to string": 201 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 202 | checkShell: 203 | ./test ("--in="$(run.name)) ("--out="$(run.outName)) 204 | do: 205 | &"./test --in={run.name} --out={run.outName}" 206 | 207 | block: 208 | # "[shell] quoting a Nim symbol with tuple fields, appending to string without parens": 209 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 210 | checkShell: 211 | ./test ("--in="$run.name) ("--out="$run.outName) 212 | do: 213 | &"./test --in={run.name} --out={run.outName}" 214 | 215 | block: 216 | # "[shell] quoting a Nim expression with obj fields": 217 | type 218 | TestObj = object 219 | name: string 220 | val: float 221 | let obj = TestObj(name: "test", val: 5.5) 222 | checkShell: 223 | ./test ("--in="$obj.name) ("--val="$(obj.val)) 224 | do: 225 | &"./test --in={obj.name} --val={(obj.val)}" 226 | 227 | block: 228 | # "[shell] quoting a Nim expression with proc call": 229 | # sometimes calling a function on an identifier is useful, e.g. to extract 230 | # a filename 231 | proc extractFilename(s: string): string = 232 | result = s[^11 .. ^1] 233 | let path = "/some/user/path/toAFile.txt" 234 | checkShell: 235 | ./test ("--in="$(path.extractFilename)) 236 | do: 237 | &"./test --in={path.extractFilename}" 238 | 239 | when not defined(windows): 240 | shell: 241 | touch test1234567890.txt 242 | mv test1234567890.txt bar1234567890.txt 243 | rm bar1234567890.txt 244 | 245 | block: 246 | # "[shell] quoting a Nim expression without anything else": 247 | let myCmd = "runMe" 248 | checkShell: 249 | ($myCmd) 250 | do: 251 | $myCmd 252 | 253 | 254 | block: 255 | var res = "" 256 | shellAssign: 257 | res = echo `hello` 258 | doAssert res == "hello" 259 | 260 | block: 261 | var res = "" 262 | shellAssign: 263 | res = pipe: 264 | seq 0 1 10 265 | tail -3 266 | when not defined(travisCI): 267 | doAssert res == "8\n9\n10" 268 | 269 | block: 270 | let ret = shellVerbose: 271 | "for f in 1 2 3; do echo $f; sleep 1; done" 272 | doAssert ret[0] == "1\n2\n3", "was " & $ret[0] 273 | 274 | block: 275 | var toContinue = true 276 | template tc(cmd: untyped): untyped {.dirty.} = 277 | if toContinue: 278 | toContinue = cmd 279 | 280 | template shellCheck(actions: untyped): untyped = 281 | tc: 282 | let res = shellVerbose: 283 | actions 284 | res[1] == 0 285 | 286 | shellCheck: 287 | one: 288 | "f=hallo" 289 | echo $f 290 | doAssert toContinue 291 | 292 | block: 293 | let res = shellVerbose: 294 | echo runBrokenCommand 295 | thisCommandDoesNotExistOnYourSystemOrThisTestWillFail 296 | echo Hello 297 | doAssert res[1] != 0 298 | echo res[0] 299 | doAssert res[0].startsWith("runBrokenCommand") 300 | 301 | echo "All tests passed using NimScript!" 302 | -------------------------------------------------------------------------------- /tests/tShell.nim: -------------------------------------------------------------------------------- 1 | import unittest 2 | import ../shell 3 | import strutils 4 | 5 | suite "[shell]": 6 | test "[shell] single cmd w/ StrLit": 7 | checkShell: 8 | cd "Analysis/ingrid" 9 | do: 10 | "cd Analysis/ingrid" 11 | 12 | test "[shell] single cmd w/ InFix": 13 | checkShell: 14 | cd Analysis/ingrid/stuff 15 | do: 16 | "cd Analysis/ingrid/stuff" 17 | 18 | test "[shell] single cmd w/ InFix via filename": 19 | checkShell: 20 | run test.h5 21 | do: 22 | "run test.h5" 23 | 24 | test "[shell] single as StrLit": 25 | checkShell: 26 | "cd Analysis" 27 | do: 28 | "cd Analysis" 29 | 30 | test "[shell] single cmd w/ two idents": 31 | checkShell: 32 | nimble develop 33 | do: 34 | "nimble develop" 35 | 36 | test "[shell] single cmd w/ prefix and StrLit": 37 | checkShell: 38 | ./reconstruction "Run123" "--out" "test.h5" 39 | do: 40 | "./reconstruction Run123 --out test.h5" 41 | 42 | test "[shell] single cmd w/ prefix and ident and StrLit": 43 | checkShell: 44 | ./reconstruction Run123 "--out" "test.h5" 45 | do: 46 | "./reconstruction Run123 --out test.h5" 47 | 48 | test "[shell] single cmd w/ prefix, ident, StrLit and InFix": 49 | checkShell: 50 | ./reconstruction Run123 "--out" test.h5 51 | do: 52 | "./reconstruction Run123 --out test.h5" 53 | 54 | test "[shell] single cmd w/ prefix, ident, InFix and VarTy": 55 | checkShell: 56 | ./reconstruction Run123 --out test.h5 57 | do: 58 | "./reconstruction Run123 --out test.h5" 59 | 60 | test "[shell] single cmd w/ prefix, ident and VarTy at the end": 61 | checkShell: 62 | ./reconstruction Run123 --out 63 | do: 64 | "./reconstruction Run123 --out" 65 | 66 | test "[shell] single cmd w/ tripleStrLit to escape \" ": 67 | checkShell: 68 | ./reconstruction Run123 """--out="test.h5"""" 69 | do: 70 | "./reconstruction Run123 --out=\"test.h5\"" 71 | 72 | test "[shell] single cmd with `nnkAsgn`": 73 | checkShell: 74 | ./reconstruction Run123 --out=test.h5 --foo 75 | do: 76 | "./reconstruction Run123 --out=test.h5 --foo" 77 | 78 | test "[shell] command with a redirect": 79 | checkShell: 80 | echo """"test file"""" > test.txt 81 | do: 82 | "echo \"test file\" > test.txt" 83 | 84 | test "[shell] command with a pipe": 85 | checkShell: 86 | cat test.txt | grep """"file"""" 87 | do: 88 | "cat test.txt | grep \"file\"" 89 | 90 | test "[shell] command with a manual &&": 91 | checkShell: 92 | mkdir foo && rmdir foo 93 | do: 94 | "mkdir foo && rmdir foo" 95 | 96 | test "[shell] command with literal string from single word": 97 | checkShell: 98 | echo `Hallo` 99 | do: 100 | "echo \"Hallo\"" 101 | 102 | test "[shell] command with literal string of multiple words": 103 | checkShell: 104 | echo `"Hello World!"` 105 | do: 106 | "echo \"Hello World!\"" 107 | 108 | test "[shell] command with accent quotes for the shell": 109 | checkShell: 110 | "a=`echo Hallo`" 111 | do: 112 | "a=`echo Hallo`" 113 | 114 | test "[shell] view output": 115 | shellEcho: 116 | ./reconstruction Run123 --out test.h5 117 | check true 118 | 119 | when not defined(windows): 120 | ## this test does not work on windows, since the commands don't exist 121 | test "[shell] multiple commands": 122 | shell: 123 | touch test1234567890.txt 124 | mv test1234567890.txt bar1234567890.txt 125 | rm bar1234567890.txt 126 | check true 127 | 128 | test "[shell] multiple commands in one shell call": 129 | checkShell: 130 | one: 131 | mkdir foo 132 | cd foo 133 | touch bar 134 | cd ".." 135 | do: 136 | "mkdir foo && cd foo && touch bar && cd .." 137 | 138 | test "[shell] combine several commands via pipe": 139 | checkShell: 140 | pipe: 141 | cat tests/tShell.nim 142 | grep test 143 | head -3 144 | do: 145 | "cat tests/tShell.nim | grep test | head -3" 146 | 147 | test "[shell] quoting a Nim symbol": 148 | let name = "Vindaar" 149 | checkShell: 150 | echo "Hello from" ($name) 151 | do: 152 | &"echo Hello from {name}" 153 | 154 | test "[shell] quoting a Nim symbol and appending to it using dotExpr": 155 | let dir = "testDir" 156 | checkShell: 157 | tar -czf ($dir).tar.gz 158 | do: 159 | &"tar -czf {dir}.tar.gz" 160 | 161 | test "[shell] unintuitive: quoting a Nim symbol (), appending string": 162 | ## This is a rather unintuitive side effect of the way the Nim parser works. 163 | ## Unfortunately appending a string literal to a quote via `()` will result 164 | ## in a space between the quoted identifier and the string literal. 165 | ## See the test case below, which quotes everything via `()`. 166 | let dir = "testDir" 167 | checkShell: 168 | tar -czf ($dir)".tar.gz" 169 | do: 170 | &"tar -czf {dir} .tar.gz" 171 | 172 | test "[shell] quoting a Nim symbol () and appending string inside the ()": 173 | let dir = "testDir" 174 | checkShell: 175 | tar -czf ($dir".tar.gz") 176 | do: 177 | &"tar -czf {dir}.tar.gz" 178 | 179 | test "[shell] quoting a Nim expression () and appending string inside the ()": 180 | let pdf = "test.pdf" 181 | checkShell: 182 | pdfcrop "--margins '5 5 5 5'" ($pdf) ($(pdf.replace(".pdf",""))"_cropped.pdf") 183 | do: 184 | &"pdfcrop --margins '5 5 5 5' {pdf} {pdf.replace(\".pdf\",\"\")}_cropped.pdf" 185 | 186 | test "[shell] quoting a Nim symbol and appending it to a string without space": 187 | let outname = "test.h5" 188 | checkShell: 189 | ./test "--out="($outname) 190 | do: 191 | &"./test --out={outname}" 192 | 193 | test "[shell] quoting a Nim symbol and appending it within `()`": 194 | let outname = "test.h5" 195 | checkShell: 196 | ./test ("--out="$outname) 197 | do: 198 | &"./test --out={outname}" 199 | 200 | test "[shell] quoting a Nim symbol and appending it within `()` with a space": 201 | ## NOTE: while this works, it is not the recommended way for clarity! 202 | let outname = "test.h5" 203 | checkShell: 204 | ./test ("--out" $outname) 205 | do: 206 | &"./test --out {outname}" 207 | 208 | test "[shell] quoting a Nim symbol and appending it to a string with space": 209 | let outname = "test.h5" 210 | checkShell: 211 | ./test "--out" ($outname) 212 | do: 213 | &"./test --out {outname}" 214 | 215 | test "[shell] quoting a Nim symbol with tuple fields": 216 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 217 | checkShell: 218 | ./test "--in" ($run.name) "--out" ($(run.outName)) 219 | do: 220 | &"./test --in {run.name} --out {run.outName}" 221 | 222 | test "[shell] quoting a Nim symbol with tuple fields, appending to string": 223 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 224 | checkShell: 225 | ./test ("--in="$(run.name)) ("--out="$(run.outName)) 226 | do: 227 | &"./test --in={run.name} --out={run.outName}" 228 | 229 | test "[shell] quoting a Nim symbol with tuple fields, appending to string without parens": 230 | const run = (name: "Run_240_181021-14-54", outName: "run_240.h5") 231 | checkShell: 232 | ./test ("--in="$run.name) ("--out="$run.outName) 233 | do: 234 | &"./test --in={run.name} --out={run.outName}" 235 | 236 | test "[shell] quoting a Nim expression with obj fields": 237 | type 238 | TestObj = object 239 | name: string 240 | val: float 241 | let obj = TestObj(name: "test", val: 5.5) 242 | checkShell: 243 | ./test ("--in="$obj.name) ("--val="$(obj.val)) 244 | do: 245 | &"./test --in={obj.name} --val={(obj.val)}" 246 | 247 | test "[shell] quoting a Nim expression with proc call": 248 | # sometimes calling a function on an identifier is useful, e.g. to extract 249 | # a filename 250 | proc extractFilename(s: string): string = 251 | result = s[^11 .. ^1] 252 | let path = "/some/user/path/toAFile.txt" 253 | checkShell: 254 | ./test ("--in="$(path.extractFilename)) 255 | do: 256 | &"./test --in={path.extractFilename}" 257 | 258 | test "[shell] quoting a Nim expression, prepending and appending to it": 259 | let name = "foo" 260 | checkShell: 261 | Rscript -e ("rmarkdown::render('"$name"')") 262 | do: 263 | &"Rscript -e rmarkdown::render('foo')" 264 | 265 | test "[shell] quoting a Nim expression, prepending and appending to it, literal string": 266 | let name = "foo" 267 | checkShell: 268 | Rscript -e ("\"rmarkdown::render('"$name"""')"""") 269 | do: 270 | &"Rscript -e \"rmarkdown::render('foo')\"" 271 | 272 | test "[shell] quoting a Nim expression without anything else": 273 | let myCmd = "runMe" 274 | checkShell: 275 | ($myCmd) 276 | do: 277 | $myCmd 278 | 279 | ## these tests don't work on windows, since the commands don't exist 280 | test "[shellAssign] assigning output of a shell call to a Nim var": 281 | var res = "" 282 | shellAssign: 283 | res = echo `hello` 284 | check res == "hello" 285 | 286 | when not defined(windows): 287 | ## `pipe` does not work on windows 288 | test "[shellAssign] assigning output of a shell pipe to a Nim var": 289 | var res = "" 290 | shellAssign: 291 | res = pipe: 292 | seq 0 1 10 293 | tail -3 294 | when not defined(travisCI): 295 | # test is super flaky on travis. Often thee 10 is missing?! 296 | check res.multiReplace([("\n", "")]) == "8910" 297 | 298 | test "[shellAssign] assigning output from shell to a variable while quoting a Nim var": 299 | var res = "" 300 | let name1 = "Lucian" 301 | let name2 = "Markus" 302 | shellAssign: 303 | res = echo "Hello " ($name1) "and" ($name2) 304 | check res == "Hello Lucian and Markus" 305 | 306 | test "[shell] real time output": 307 | shell: 308 | "for f in 1 2 3; do echo $f; sleep 1; done" 309 | 310 | test "[shellVerbose] check for exit code of wrong command": 311 | let res = shellVerbose: 312 | thisCommandDoesNotExistOnYourSystemOrThisTestWillFail 313 | check res[1] != 0 314 | 315 | test "[shellVerbose] compare output of command using shellVerbose": 316 | let res = shellVerbose: 317 | echo "Hello world!" 318 | check res[0] == "Hello world!" 319 | check res[1] == 0 320 | 321 | when not defined(windows): 322 | ## `one` command does not work on windows 323 | test "[shellVerbose] remove nested StmtLists": 324 | var toContinue = true 325 | template tc(cmd: untyped): untyped {.dirty.} = 326 | if toContinue: 327 | toContinue = cmd 328 | 329 | template shellCheck(actions: untyped): untyped = 330 | tc: 331 | let res = shellVerbose: 332 | actions 333 | res[1] == 0 334 | 335 | shellCheck: 336 | one: 337 | "f=hallo" 338 | echo $f 339 | check toContinue 340 | 341 | test "[shellVerbose] check commands are not run after failure": 342 | let res = shellVerbose: 343 | echo runBrokenCommand 344 | thisCommandDoesNotExistOnYourSystemOrThisTestWillFail 345 | echo Hello 346 | check res[1] != 0 347 | check res[0].startsWith("runBrokenCommand") 348 | 349 | when not defined(windows): 350 | ## stderr redirect does not work on windows? 351 | test "[shellVerboseErr] check stderr output": 352 | let test = "test" 353 | let (res, err, _) = shellVerboseErr: 354 | echo ($test) 355 | echo ($test) >&2 356 | 357 | doAssert test == res 358 | doAssert test == err 359 | 360 | test "[shellVerboseErr] setting debug config works": 361 | let test = "test" 362 | let (res, err, _) = shellVerboseErr {dokOutput}: 363 | echo ($test) 364 | 365 | doAssert test == res 366 | 367 | test "[shellVerbose] change process options": 368 | let (res, err) = shellVerbose(options = {poEvalCommand}): 369 | echo "Hello World" 370 | check res == "Hello World" 371 | --------------------------------------------------------------------------------