Nim: Check if stdin/stdout are associated with terminal or pipe
— Kaushal ModiWhen 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
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 -->"
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 -->"
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 -->
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. ↩︎It doesn’t matter if you use Code Snippet 2 or Code Snippet 3. ↩︎