Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Improving CLIs with isatty (jez.io)
84 points by jez on Nov 29, 2019 | hide | past | favorite | 31 comments


Please be careful with this. From the GNU Coding Standards:

"...please don’t make the behavior of a command-line program depend on the type of output device it gets as standard output or standard input."

https://www.gnu.org/prep/standards/standards.html#User-Inter...

Of course you don't have to follow their standards, but their rationale for this particular item is reasonable.

I think a warning as described in the article is perhaps just about OK though.


OK until, of course, you meet me.

And I go around piping things into 'less -RS'

I wouldn't get the warning.

Though of course getting it to some is probably better than none so I'm more than likely with you.

--

Unfortunately many apps do behave differently towards ttys in one key way - colour.

It is somewhat frustrating to be piping into less something like 'apt list --upgradable' and being unable go essuly scan because its a mess without colour.


Instead of a warning it could grep the contents of the current directory. I bet that would work if you were imagining a use case of typing your pipeline on the command line.


What exactly is the rationale though? I could not find a reason in the text for doing so, it only states, "...don't make the behavior of a command-line program depend on the type of output device...". Ok, but why?


Because not breaking a standard abstraction is a logically sound thing to do, even if such abstraction isn't perfect for your use case. The big picture takes into account interoperability and predictability of the behavior of tools when put to work together in unforeseen ways.


Doesn't `ls` behaves like this, printing in multiple columns when the output is a tty?


It does, but it can be argued that this is not in response to a certain type of output file. ls(1) formats filename lists into columns depending on the width of the output viewport. It just so happens that it's not able to determine that width for anything but tty devices, so it falls back to the safe default "single column".

To use an analogy from web browsers, the equivaleny guideline would be asking websites not to detect features by reading the User-Agent, and using feature detection instead.


Nowadays it also adds quotes to filenames with spaces or special characters if the output is a terminal, to make copying and pasting easier.


When I first started using Plan 9 I thought it was great how it (ls)'d give results the same way you asked for them, i.e. if (cd a; ls) would print 'f' then (ls a) would print 'a/f'.


Exactly, this is not an improvement, but makes it worse.


I haven’t figured out a way to do this in native Java, so I end up wrapping my Java CLI tools in a bash wrapper (as a self executing JAR). This wrapper’s main purpose is to set a Java property based on the results of the bash test:

    if [ -t 0 ]; then
    fi
I then use this flag to know if I should do things like export CLI progress bars or other interactive things.


You can call isatty() using a Java FFI library such as JNA or JNR. Some day, Java will come with this out-of-the-box and an external library will not be needed – see https://openjdk.java.net/jeps/191

For JNA (probably JNR too, my personal experience is only with JNA though), just have to add the JAR to your project (or the Maven/Gradle dependency). The JAR includes native libraries for various platforms.

It is non-portable, it won't work on Windows, although you may not care about that, and if you do, Windows has similar APIs (e.g. calling GetConsoleMode() on the file handle will fail if the handle doesn't point to a console.)


Jansi might be worth a look. Provides isatty() and other things that might be helpful for a Java command line app. Works on Windows also. https://github.com/fusesource/jansi


linux-specific hack: read /proc/self/fdinfo/1 and check if its mnt_id (/proc/self/mountinfo) corresponds to a devpts mount.


Allegedly System.console() but of course it's not actually the same and can be wrong in some important cases, leaving you with jni.


when would console() be wrong?


See this code: https://github.com/openjdk/jdk/blob/39c023612dbdd1016ad24b60...

It only works when isatty() returns true on both stdin and stdout. If you redirect stdin from a file, but stdout still goes to tty, isatty() on stdout will return true, but System.console() will return null.

If you are using isatty() on stdout to decide whether to display fancy output, then whether stdin is a tty or not isn't relevant. So in that scenario System.console() doesn't do what you want.


That makes sense, thanks! So it's a bit over-cautious.


The problem with doing this is you end up with a bunch of tools which may or may not have unexpected behavior depending on how you ran them. It's better to have a simple design that just works one way, and you're forced to use it in the one right way. When users invariably use it the wrong way, the reasoning about the failures is simpler, and there are no extra bugs from the extra lines of guard-rail code.

If you really wanted to add this 'protection code', you could make a bash wrapper around every Unix command that has its input or output connected to a tty. Not perfect, but it would eliminate a whole lot of extra tty-checking code from every app, providing a single reliable interface. I imagine you'd turn it off after a while, though.


> the isatty function in the C standard library (man 3 isatty)

It is not in the C standard library, it's in POSIX so isn't available in non-*nix platforms. The same problem applies in other languages using it through cffi.


isatty on which standard file, though, takes some consideration. Zero, any, some, or all of stdin, stdout, and stderr may be redirected independently. Some programs might usefully print differently redirected into a file vs rendered on a tty. And what is one to do about pipes?


no, this only makes any sense at all for stdin


it also makes sense for stdout, e.g. when you're spewing binary data that might corrupt a terminal.


No it doesn't - why would it help at all to precede that with a warning the user wouldn't see?


The implementation in musl libc is a simple ioctl call[0], so really it should be available in all languages without need for a C FFI. I always find this kind of thing interesting.

[0]: https://git.musl-libc.org/cgit/musl/tree/src/unistd/isatty.c


ioctl is inherently non-portable. Even though most Unix-like OS implement it, it is not part of the POSIX / SUS standard and so code which relies on it is a portability problem. The problem is the ioctl command codes and their parameters are not standardised and vary from OS to OS.

(The POSIX standard does standardise ioctl, but only for use with STREAMS, which is an optional feature which more often than not isn't implemented.)


Just to be clear, while ioctl has issues, isatty is part of the POSIX standard: https://pubs.opengroup.org/onlinepubs/9699919799/functions/i...


How would you do an ioctl from Java without an FFI?


The JIT compiler could have support for emitting system calls.


That's not what I meant.

I meant there is no ioctl method in the Java standard library. So how do you call it without using some FFI to import the method? You cannot.


There were some interesting suggestions along these lines in the "12 factor CLI" piece previously linked at HN, in sections 6 and 7:

https://medium.com/@jdxcode/12-factor-cli-apps-dd3c227a0e46#...




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: