2

How do I temporarily capture stdout in Nim?

I would like to have a template with the following signature:

template captureStdout(ident: untyped, body: untyped) = discard

Such that this code (main.nim) runs without error:

var msg = "hello"
echo msg & "1"
var s: string
captureStdout(s):
  echo msg & "2"
  msg = "ciao"
echo msg & "3"
assert s == "hello2\n"

and the output should be:

hello1
ciao3

current efforts

currently I am able to capture stdout using a temporary file, but I am not able to release back to stdout. I do this with the following:

template captureStdout*(ident: untyped, body: untyped) =
  discard reopen(stdout, tmpFile, fmWrite)
  body
  ident = readFile(tmpFile)

with this main.nim runs without assertion error but output is only

hello1

and in tmpFile I see:

hello2
ciao3
pietroppeter
  • 1,433
  • 13
  • 30

1 Answers1

3

When you call reopen you reassign the variable stdout to a File that writes to tmpFile.

In order to print output to the system STDOUT, you need to reassign the variable stdout to a File that writes to your systel STDOUT.

Therefore, the answer differ between Linux and Windows.

For Linux, the way of doing this is to use dup and dup2 C function to duplicate the stdout file descriptor and use a different file (so you can restore stdout). Since, dup and dup2 are not in system/io in Nim we'll need to have bindings to unistd.h.

Here's an example :

#Create dup handles
proc dup(oldfd: FileHandle): FileHandle {.importc, header: "unistd.h".}
proc dup2(oldfd: FileHandle, newfd: FileHandle): cint {.importc,
    header: "unistd.h".}

# Dummy filename
let tmpFileName = "tmpFile.txt"

template captureStdout*(ident: untyped, body: untyped) =
  var stdout_fileno = stdout.getFileHandle()
  # Duplicate stoud_fileno
  var stdout_dupfd = dup(stdout_fileno)
  echo stdout_dupfd
  # Create a new file
  # You can use append strategy if you'd like
  var tmp_file: File = open(tmpFileName, fmWrite)
  # Get the FileHandle (the file descriptor) of your file
  var tmp_file_fd: FileHandle = tmp_file.getFileHandle()
  # dup2 tmp_file_fd to stdout_fileno -> writing to stdout_fileno now writes to tmp_file
  discard dup2(tmp_file_fd, stdout_fileno)
  #
  body
  # Force flush
  tmp_file.flushFile()
  # Close tmp
  tmp_file.close()
  # Read tmp
  ident = readFile(tmpFileName)
  # Restore stdout
  discard dup2(stdout_dupfd, stdout_fileno)

proc main() =
  var msg = "hello"
  echo msg & "1"
  var s: string

  captureStdout(s):
    echo msg & "2"
    msg = "ciao"

  echo msg & "3"
  echo ">> ", s
  assert s == "hello2\n"

when isMainModule:
  main()
  # Check it works twice
  main()
Clonk
  • 2,025
  • 1
  • 11
  • 26
  • 1
    Thanks a lot, this looks great! I was actually getting there looking at https://stackoverflow.com/questions/11042218/c-restore-stdout-to-terminal but this is awesome! I actually tested it on Windows and to my surprise it actually works! I guess this has to do with the fact that dup and dup2 are still available (although deprecated) in Windows (see https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/posix-dup-dup2?view=vs-2019). It should be possible to wrap the non deprecated _dup and _dup2 for windows only (although it probably require some FFI gymnastics)... – pietroppeter Sep 23 '20 at 16:34
  • I can't help you regarding the windows API as I develop entirely in Linux; even when I have windows installed I do all my work in WSL. But It's nice to hear it works :) – Clonk Sep 23 '20 at 16:39
  • 1
    since Nov 2020 the code is available in fusion: https://github.com/nim-lang/fusion/blob/master/src/fusion/ioutils.nim – pietroppeter Mar 08 '21 at 10:19