Emacs, scripting and anything text oriented.

Nim: Check if stdin/stdout are associated with terminal or pipe

Kaushal Modi

When writing bash scripts, I often need to know if the script is receiving input from the terminal, or some piped process. I would also need to know if the script is sending output to the terminal, or to another piped process.

As I am learning Nim and trying to write new scripts using that, I need to know how to do the same in Nim.

Today, I came across this r/nim thread where a user needed to know if the Nim-compiled binary was receiving input from stdin.

That reminded me of this technique that I had used in my bash project eless.. (by checking [[ -t 0 ]] and [[ -t 1 ]]).

Bash #

Here’s a similar bash snippet if you want to try it out:

#!/usr/bin/env bash

# How to detect whether input is from keyboard, a file, or another process.
# Useful for writing a script that can read from standard input, or prompt the
# user for input if there is none.

# https://gist.github.com/davejamesmiller/1966557
if [[ -t 0 ]] # Script is called normally - Terminal input (keyboard) - interactive
then
    # eless foo
    # eless foo | cat -
    echo "--> Input from terminal"
else # Script is getting input from pipe or file - non-interactive
    # echo bar | eless foo
    # echo bar | eless foo | cat -
    echo "--> Input from PIPE/FILE"
fi

# https://stackoverflow.com/a/911213/1219634
if [[ -t 1 ]] # Output is going to the terminal
then
    # eless foo
    # echo bar | eless foo
    echo "    Output to terminal -->"
else # Output is going to a pipe, file?
    # eless foo | cat -
    # echo bar | eless foo | cat -
    echo "    Output to a PIPE -->"
fi
Code Snippet 1: Checking stdin and stdout in bash

So using that as the basis, I went down the path of figuring out how to do the same in Nim.

Nim #

I remembered seeing the variables stdin and stdout defined in the implicitly1 imported system module. So I started with trying to get the value of those variables and see if they helped me figure out how they are associated with the terminal.

Using os.getFileInfo #

From the docs of stdin and stdout, I learned that the type of both of those is File. So while searching for “File”, I ended up on os.FileInfo, whose value I can get using os.getFileInfo.

Based on the structure of that FileInfo object:

FileInfo = object
  id*: tuple[device: DeviceId, file: FileId]
  kind*: PathComponent
  size*: BiggestInt
  permissions*: set[FilePermission]
  linkCount*: BiggestInt
  lastAccessTime*: times.Time
  lastWriteTime*: times.Time
  creationTime*: times.Time

I guessed that the id field in there might hold the information I needed.. and indeed it did!

By printing out the value of getFileInfo(stdin).id.file, and running the Nim-compiled binary by itself (./binary) vs providing it the output of another process (echo foo | ./binary), I learned that its value is 35 if the binary is not receiving input from another process/pipe.

Similarly the value of getFileInfo(stdout).id.file is also 35 if the binary is not sending the output to another process/pipe.

Based on that deduction, this worked!

# Figuring out if input is coming from a pipe and if output is going to a pipe.
import std/[os, strutils]

# https://nim-lang.org/docs/os.html#FileInfo
if getFileInfo(stdin).id.file==35:
  # ./stdin_stdout foo
  # ./stdin_stdout foo | cat
  echo "--> Input from terminal"
else:
  # echo bar | ./stdin_stdout
  # echo bar | ./stdin_stdout | cat
  echo "--> Input from a PIPE/FILE: `" & readAll(stdin).strip() & "'"

if getFileInfo(stdout).id.file==35:
  # ./stdin_stdout foo
  # echo bar | ./stdin_stdout foo
  echo "    Output to terminal -->"
else:
  # ./stdin_stdout | cat
  # echo bar | ./stdin_stdout | cat
  echo "    Output to a PIPE -->"
Code Snippet 2: Using os.getFileInfo to check stdin and stdout

But there’s a catch .. That value of 35 might not be the same on all POSIX systems. So instead, use the isatty() based approach that I show next.

Using terminal.isatty #

In the same reddit thread, /u/bpbio from Reddit provides a better, concise answer—using the isatty proc from the terminal module:

proc isatty(f: File): bool {.raises: [], tags: [].}

Returns true if f is associated with a terminal device.

From my brief testing, I saw that isatty(stdin) is equivalent to getFileInfo(stdin).id.file==35, and the same for stdout too.

So my above snippet can be rewritten as:

# Figuring out if input is coming from a pipe and if output is going to a pipe.
import std/[terminal, strutils]

if isatty(stdin):
  # ./stdin_stdout foo
  # ./stdin_stdout foo | cat
  echo "--> Input from terminal"
else:
  # echo bar | ./stdin_stdout
  # echo bar | ./stdin_stdout | cat
  echo "--> Input from a PIPE/FILE: `" & readAll(stdin).strip() & "'"

if isatty(stdout):
  # ./stdin_stdout foo
  # echo bar | ./stdin_stdout foo
  echo "    Output to terminal -->"
else:
  # ./stdin_stdout | cat
  # echo bar | ./stdin_stdout | cat
  echo "    Output to a PIPE -->"
Code Snippet 3: Using terminal.isatty to check stdin and stdout

Result #

Assuming that the Nim-compiled binary of the above code2 is stdin_stdout, you get this output:

> ./stdin_stdout
--> Input from terminal
    Output to terminal -->
> ./stdin_stdout | cat
--> Input from terminal
    Output to a PIPE -->
> echo foo | ./stdin_stdout
--> Input from a PIPE/FILE: `foo'
    Output to terminal -->
> echo foo | ./stdin_stdout | cat
--> Input from a PIPE/FILE: `foo'
    Output to a PIPE -->

  1. Nim has the concept of implictly and explicitly imported modules. You do not need to manually import the former using the import keyword, while you need to do that for the latter. ↩︎

  2. It doesn’t matter if you use Code Snippet 2 or Code Snippet 3↩︎


Versions used: nim 0.18.1
Webmentions
 1