Command line interface guidelines (2021)
clig.dev
clig.dev
This is true today, and it was true as well "in the 1980s", to use the same time frame as TFA. The difference is that today there are more people than ever who know what the command line is, and who can use it. At least an order of magnitude more people; maybe two. We can certainly say that we live in the CLI golden age!
If you want to judge the shift in quality in a thing that grew in quantity, then you should look at percentages — otherwise you end up creating statements that are true, but don't say anything meaningful.
E.g. there are probably more absolute listeners of Jazz music today than back at the height of the cultural bloom of the genre. But that isn't because Jazz is more popular today than it once was, but because there are more absolute listeners of any kind of music. Would you say that Jazz in the US is now more important, influential etc. than it was at its peak?
Whether from that quantity new qualities emerged is a different question. E.g. the new quantity of diverse jazz listeners probably has lead to an explosion of new sub-genres, that might have never emerged otherwise.
Still, just because there is more different Jazz now does not mean Jazz has become more relevant overall. This was the factor we discussed btw.
If done right it can also encourage Functional Core, Imperative Shell, because a sensible Unix-philosophy command line needs lots of actions without side effects and a few with. You can write a little command that generates and dumps out the system state just before a (bad) decision is made, and do so against production systems with virtual impunity. And that means you can hand these tools to someone who you need to become part of a bus number, even if they are otherwise hesitant to do so.
> do_thing.py | dry-run
--
Think of it as "explain your work, step by step" as one would prompt...
also, its a food-for-thought you muppets.
---
@jasonjmcghee ; ( $ ) . ( $ ) great justice.
But at that point you could just handle —dry-run directly
You can do that. Possibly not from all languages but for anything that can call functions in the c standard library, that’s what isatty() is for (among other uses). It takes a file descriptor and returns whether it is a terminal or not. If you do this with stdout, this tells you whether it goes to a terminal or whether it is redirected in a way.
As the parent suspects, though, this won’t tell you anything about what is on the other side of the redirection.
-
What if there wasa 'deep-pipe' '||' which would be based on a set env/docker/blah - which would launch an env and execute your '||'d code in it, and output some log/metrics/whatever?
The output sent to tee is usually not the same as the output from the command to the terminal, so you are getting something different than most human users expect from original command... the reason is that terminal escape codes and other formatting for humans may need to be omitted from output to a pipe. You do this by asking the OS, "is this thing a terminal?".
Python example
"terminal" if sys.stdout.isatty() else "something else"
C is very similar:
if (isatty (1)) fprintf (stdout, "Terminal."); else fprintf (stdout, "Not Terminal.");
(printf works, too).
Even ls outputs a tabular format by default when it's on a terminal and a list one file/dir per line when it's not on a terminal (if it's piped to cat for example or why ls | wc -l correctly counts the entries).
But the (essential) behavior of the command remains the same. ls still lists files/dirs... scp still copies files, etc.
Of course. A command needs to do it's defined function.
You'll find some programs that are quite a bit different when invoked from outside the terminal vs inside the terminal. Developers need to take into account both situations, which is really the point the original post.
One approach could be something like “set -x” after setting a confirmation with “trap” command.
confirm_execution() {
echo -n "Execute $BASH_COMMAND? [y/N] "
read response
if [[ $response != [yY] ]]; then
echo "Skipped."
return 1
fi
}
trap 'confirm_execution' DEBUG
set -x
user_command
set +x
But that’s a wrapper scriptdraw_heart() { cat << EOF <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="100" height="100"> <path fill="red" d="M12 21.35l-1.45-1.32C5.4 15.36 2 12.28 2 8.5 2 5.42 4.42 3 7.5 3c1.74 0 3.41.81 4.5 2.09C13.09 3.81 14.76 3 16.5 3 19.58 3 22 5.42 22 8.5c0 3.78-3.4 6.86-8.55 11.54L12 21.35z"/> </svg> EOF }
trap 'confirm_execution' DEBUG set -x draw_heart set +x
[1]: https://prettier.io
I'll go further. PowerShell covers a lot of the concerns in the OP out of the box. It is extremely well thought out and is one of my favorite cli models to buy into.
I use MacOS on my personal machine and Linux for various shellboxes and I switched to Powershell years ago and haven't looked back. Occasionally I invoke bash as a language runtime for checking shell script stuff, the way I would any other language's REPL, but for a shell? Powershell is strictly better.
The one actual problem with Powershell in this area is quoting for external commands. It's solvable in scripts by replacing certain things with double-quoted equivalents but not really interactively, and it is an occasional pain (though I still think it's overall less of a problem than quoting in general in bash and its ilk).
I feel it’s safer for scripts that have irreversible (or difficult to reverse) actions.
Instead of
myprog # see what would happen
myprog --commit # alright, do it
You do myprog # see what would happen
myprog | sh # alright, do it
But if you want to change something: myprog > x
vi x
cat x | sh
And if you just want to run everything in parallel: myprog | parallel -j 16If you call an API to retrieve data, how can that be a dry run? Are you suppose to give fake examples with fake output?
Calling an API to retrieve data is not really the type of program that requires a dry-run flag. It's mainly useful for commands that change the state of something in ways that could potentially be destructive, unwanted and/or hard to revert.
https://docs.aws.amazon.com/cli/latest/userguide/cli-usage-h...
Never display animations in stdout! I quite liked TFA in general, but I was skimming around looking for where they were going to advise on the difference between stderr & stdout until I saw that and realised they weren't.
stderr should be all (not just 'errors') of your logging, informational type stuff, the bits that maybe you might animate (and some people will hate) if tty, etc.
stdout should be the useful output - which you may or may not have - regardless of whether tty or not, primarily because an inconsistency like that is just confusing.
e.g.
echo foo | mysed 's/oo/aa/' | cat
# mysed should:
# stdout: faa
# stderr: mysed version 1 here hello\nfound oo\nprinting aa (or whatever)
I don't want to have to fight your tool with grep to get the 'actual' output lines. And I don't want to struggle to debug it because if I remove `| cat` above (as a silly example) it behaves differently than with it.If we could go back in time and make it stdin, stdout, and stdext (because UNIX so six letters, but standard_extra, or standard_extended), we might have a prayer of getting people to follow that convention.
But it's called stderr, so devs think, quite reasonably, that it should be used for error reporting, and conversely, if it isn't an error, it goes in stdout.
But I agree with you that this is the better way to structure a program. You might confuse more people with it, but they'll be able to do more useful things with its output, so that's a win.
(Ok, sure, I'd probably change the name too!)
To add a small tweak to this rule... "stdout should be what you ask for". If I ask for --help, that should go to stdout. If I ask for logs, they should go to stdout. If I don't ask for it, stderr.
for the love of god please don't. the yubikey-agent example provided exemplifies everything i dislike about github READMEs and whimsical user interfaces.
on the technical side, symbols and emojis can render inconsistently among terminals, leading to potentially confusing messaging. on the artistic side, personal tolerances towards whimsy and playfulness vary wildly and should only be used very sparingly and ONLY if you know what you're doing (if you have to ask, you probably don't)
Not everyone is like that, and that's ok. I don't expect my whims to be catered to, and you shouldn't either.
1. Use a vocabulary of at most two emojis in output (e.g. one for success, one for failure).
2. Any information conveyed by an emoji should be redundantly conveyed by text.
Grepping through a large list of options is also painful, seems ther needs to be a balance here.
There is a web analogy for this in how people organize FAQs. Some have a list of section links, and you have to click on a section to get the FAQs for that topic. Others just put everything on one giant page.
Here's the problem scenario with splitting things up into section pages: You think you see the appropriate section, but then you don't see your concern answered. There are two possibilities: either the organization was counter-intuitive and your concern was answered in one of the other sections, or your concern wasn't answered anywhere. And what's the only way to be sure? Visit every single section page and search through all of them.
Much, much less painful to just have it all on one page and search it.
e.g. `git stash <sub> --help` just reports `git stash --help` which includes each `<sub>`
So it's scriptable and useful by multiple levels of skill.
Not quite. They were primarily intended for interactive use within a login shell. There are the programs which generate output on stdout (ls, cat, find, tty, who, date), and there are the "silent" text filters (tr, grep, cut, uniq, sort, wc). A one-liner would enable you to do basic computing tasks in that era. Any complex program would be written in C. After the appearance of DSLs like sed and AWK, certain string-heavy programs were offloaded to the shell.
The shell is not a sane programming environment and was never intended as such.
There are 10kloc C programs that could be 10 lines of shell and there are 1kloc shell programs that could've been 100 lines of C.
Both kinds are nowadays probably better done in Python or Lua, but the shell and C are what's most universally available.
Only when the shell calls other external C programs. Ten lines of calling ffmpeg or curl is not shell programming.
> there are 1kloc shell programs that could've been 100 lines of C
The 1kloc shell programs are fragile spaghetti that breaks in weird ways. Any invocation of an external program can fail for a variety of reasons, and the shell doesn't provide adequate mechanisms for dealing with it, apart from exit codes and filtering error text output.
Ten lines of calling ffmpeg or curl is shell programming in precisely the same sense that 100 lines of C that `#include <sys/socket.h>` are C programming.
If your code isn't standing on the shoulders of giants, you're probably wasting everyone's time.
100% wrong. this is what the shell was designed to do and where it is at it's best.
often shell "scripts" are used like "macros" or power-tools, shortcuts to save off a complex invocation or workflow. error handling isn't as important in a one-off and "adequate" is whatever gets the job done for the user, which it does.
it's rare that 1kloc shell script is the best engineering choice vs. (in the ancient days) Perl or (today) Python. e.g. "real" programming languages. you mostly should not write large programs in shell. and you really should not glue together pipelines of external programs using, for example, Python or C, which is onerous.
ah, the HN crowd: where everything is either black or white, great or terrible. how about "each to his own" and "use the right tool for the job"
I get that in a dev shop that's not the case, but most businesses employ 0 devs. So people end up being forced into shell scripting because it's the only approved option.
if your management can't allow even a python script, but it can allow bash? then it's not an engineering problem. as you even allude - your business does not have a software engineering culture.
a good engineering culture does not depend on job titles or budget or degree either. but it can't exist without healthy management.
True, handling multiple sub-processes is difficult in most Unix shells. This is a function where Powershell could have done so much better, but didn't. It is somewhat better than bash&co, but could have been much more so. Python does it much better than Powershell does.
To me it makes total sense to think of the standard POSIX toolkit as the standard library of the various shell languages, that seems basically correct in fact.
Why is it incredible useful?
Just imagine how long it would take to write the following in C or Rust:
curl -sS https://go.dev/doc/devel/release |
html2text |
grep -o -P '\bgo\d+\.\d+\.\d+\b' |
sort -V |
uniq |
tail -1
Why is it broken by design?Read this: https://news.ycombinator.com/item?id=29747034
The problem: a command line interface must be human readable and machine readable at the same time. There is no canonical way to solve this problem.
I think your example self-explains why it's broken by design. It's a good example.
> a command line interface must be human readable and machine readable at the same time. There is no canonical way to solve this problem.
And you know, there could be one. Apple has Human Interface Guidelines to reify the meaning of the visual abstractions in its desktop UI. The problem is that the command line didn't come from people who think like Apple designers; it came from people who think "How can I express what I want using the least code possible, because laziness, impatience, and hubris are virtues?" And they weren't wrong for the time (especially because every byte matters), but the design decisions they made got baked into tooling that can't now be moved.
I think at this point we'd have to punt the POSIX toolchain to get something better; it's hard for me to imagine how we'd build discoverable, conceptually-consistent UX atop what we currently have.
Great for discoverability though, and that doesn't require graphics, just context.
There should be a button I can push in my shell that lets me ask "what does the token at cursor mean," and a button that lets me type a plain language search string that wires down to a contextual search (i.e. I'm in the middle of typing out "grep" I should be able to ask "how do I search folders?").
We didn't have the tools to build this when grep was invented; we have them now.
The snarky answer is "By writing it in source code, in a GUI text editor", a thing I do frequently. But the problem is that I have no idea what you're getting at in the first place, so that's just an attempt to recover some meaning from what you wrote.
By and large, GUIs do not. There is no such thing as a universal shell for GUIs, and attempts to layer automation atop the GUI abstraction are generally spotty and unreliable (certainly when compared to CLI and shells). This is both for technical reasons (i.e. it's much easier to clearly delineate the two ends of a pipe than to clearly delineate "I want to click on the red square inside the 'diagram' window inside the drawing app") and for ecosystem reasons (since GUIs aren't thought of as automatable, GUI designers are free to move pieces around version-to-version of software, making it extremely challenging to describe a GUI structurally).
I've seen some neat attempts at GUI automation (Sikuli is my favorite) but it's never been a core feature like it is in the CLIs-glued-together-by-shells world.
Yeah, sometimes there's functionality stuck behind a button or menu select which I want exposed in a more textual way, in macOS that's when you break out Automator, Alfred, or Hammerspoon (the only one I've ever used fwiw), Linux and Windows have their own equivalents.
I don't think the distinction you're pointing to is nearly so stark or clear-cut as it's often made out to be. Ecosystems converge towards the tasks which are amenable to batching and pipelining being equipped to do so.
The specific kind of task you described is frequently exposed as macros in programs complex enough to deserve it.
I mean... "no-code development" is an entire category of product offerings. People are positively thirsty for being able to do that sort of thing without having to learn to love staring at giant blobs of text all day.
Posix could potentially do this. It already has a bit about conventions and could be expanded. Problem is getting things to adhere to it, plus I doubt the posix authors could be convinced to add a lot more to it.
https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...
Since the computer is there to serve us, ultimately the solution must be for the machine to read as well as humans.
Sure, but that will just be one aspect of an integrated and wholistic AI, which can configure the parameters input to the automated theorem proving component, and act on the results.
That is only true for a very narrow notion of "better machines". Automated theorem proving is still light years away from being applicable in the majority of software projects. Don't get me wrong I am aware of the progress made in the last decades. Yet, it will be some time until someone writing the next iOS app will reach routinely for an automated theorem prover to lower the defect rate.
And even then, the questions remains whether fulfilling a formal specification is at all correlated with "better machines" or "better software". There are domains where this is conceivable: os kernels (L4), certifying compilers (CompCert), and others. But how does a theorem prover, automated or not, help with improving the next generations of video codecs? That is an intrinsically subjective problem -- the quality axis, less so the performance axis. How does theorem proving help with neuronal networks? How does it help with capturing the right business process to actually improve business outcomes and not just introducing new bureaucracy?
* Powershell has entered the chat
It's not really the "same time". Usually, human and machine use the same command, but at different times. And there are many ways to enable different outputs, even at the same time. But this all depends on having some standard which everyone follows. And that's where it becomes complicated.
> Since very few implementations of ls allow you to terminate filenames with NUL characters instead of newlines
In fact the solution to this issue seems to be so obvious that I might be missing something ? (Rejection by shell interfaces for some reason ??)
html2text(get("https://go.dev/doc/devel/release")).find_all("\bgo\d+\.\d+\.\d+\b").sorted().unique().last()
It's just that Rust is designed to be more robust in exchange for stricter Compiler time checks.> Secrets should only be accepted via credential files, pipes, AF_UNIX sockets, secret management services, or another IPC mechanism.
Which one of these is the most convenient and portable to use?
Do you use secret management services for work only, or do you use them in your personal projects too?
See my post https://smallstep.com/blog/command-line-secrets/ for a bit more of a deep dive about using secrets on the command line.
Credential files are a good, simple, portable option. Files have permissions already. They don't depend on an external service or a proprietary API.
And, if your program accepts a credential file, it will be compatible with systemd credentials. systemd credentials offer more security than an unencrypted credential file. They are encrypted and can be TPM-bound, but they don't require the software using the credential to have native TPM support.
Command Line Interface Guidelines - https://news.ycombinator.com/item?id=38053692 - Oct 2023 (1 comment)
Command Line Interface Guidelines - https://news.ycombinator.com/item?id=31651161 - June 2022 (1 comment)
Command Line Interface Guidelines - https://news.ycombinator.com/item?id=25492119 - Dec 2020 (5 comments)
https://en.wikipedia.org/wiki/Bracketed-paste
Put in .inputrc:
bind 'set enable-bracketed-paste on'https://www.goodreads.com/book/show/104745.The_Art_of_UNIX_P...
https://www.catb.org/esr/writings/taoup/
It has been a while since I read that book, but after skimming through clig.dev I gather opinions have changed over time quite a bit.
Example: Something I did a few months back ago for a tiny personal project @ https://github.com/hiAndrewQuinn/finstem was implement `--format CSV`, `TSV` and `JSON` flags. I haven't had need for any of these myself, but they exist so any future people who want to use `csvkit`, `awk` and `jq` respectively to wrap around my program have easy ways to do so. That's not stuff I would have had the instincts to do if I wasn't myself a user of all 3 of those programs.
I wish people wouldn't conflate CLIs with terminals. I run (shell) command lines all day, but try hard to avoid terminals / apps that emulate terminals.
(To run command lines, I use Emacs, which I never run inside a terminal.)
Like, why does tar get to be so special that it takes command line flags with or without leading dashes?
... but we didn't, so now you have to memorize whether recursion is capital or lowercase R, restart is capital or lowercase R, poweroff is capital or lowercase P, etc.
Please avoid the use of short arguments in scripts. It makes the least sense there. The short arguments (along with aliases, abbreviations, and whatnot) are a convenience for human usage, to reduce the amount of manual typing. In scripts you can be explicit with minimal cost (and you also should, considering the ratio of writes vs. reads).
now we got lots of mega-cli programs, each one with its own distinct option language.
Examples: kubectl, docker, openssl, git - (git got two command line languages: plumbing and porcelain)
https://danluu.com/cli-complexity/
... ls had 11 command line options in 1979, in 2017 it got 58.
I find that guis have worse discoverability and cli better. It's pretty hard to search for a gui affordance. Plus they are essentially unscriptable.
https://www.gnu.org/software/libc/manual/html_node/Getopt.html
That said.
I think CLI programs are user friendly for professionals. Because they support input-process-output. Where input is STDOUT read by human, process is thinking by human and output is keyboard input to STDIN by human.UIs for the general audience? TUIs! TUIs are easily to parse, succinct in organization and fast input - all for humans. Humans can parse TUIs. And they can make up a mental modal.
GUIs fail often with a lack of organization, information overflow and distractions by weird metaphors. The Windows 95 desktop metaphor is an example. It doesn’t make sense. Same for Windows 11 and its file-browser which makes it hard to recognize the filesystem or even just the home-directory. Now open Nautilus on Linux, it opens by default your home-directory (in most cases the place to be).
I like the CLI but TUIs are my love. GUIs are okay if are like a TUI.
See the manual page's [1] "STANDARDS" section, which reads:
getopt()
POSIX.1-2008.
getopt_long()
getopt_long_only()
GNU.
The use of '+' and '-' in optstring is a GNU extension.
[1]: https://www.man7.org/linux/man-pages/man3/getopt.3.htmlIt is probably a feature which isn’t necessary for the languages itself but Linux/POSIX.
TUIs have the same drawbacks as GUIs and then some. Their big advantage is to work seamlessly over SSH but that’s pretty much it. They are not more discoverable and they are not more efficient than GUIs. There is nothing preventing you from having a decent GUI along the same lines as Midnight Commander to have a file manager without the metaphors you dislike, for example (as a matter of fact, there are several).
When designing TUIs I found this to be limiting in a creative sense -- I have to really think about how I arrange the TUI elements and information because I cannot put as many elements as I can on a GUI in the same screen space.
Also, TUIs seem to be mostely unaffected by the trend to make every GUI element “touch-friendly” large which is an advantage for me as a Desktop user.
Full rant here: <https://masysma.net/37/why_terminal.xhtml>
TUIs are ideal (sometimes) where a command needs to be interactive. Many commands lend themselves well to batch processing or require no interactivity at all. In many cases, a script piped into a text editor (which is a TUI) is all that is needed, sparing apps from having to embed a text editor and deal with all of the design choices. Other times GUI will work a lot better.
I still find it a rather shocking order of priority, honestly.
So you just have to design the UI perfectly on the first try. That’s possible for small tools but what about larger ones? Past a certain point it becomes a truism that do-it-once perfectly without iteration is impossible.
Then those terminal programs get upgraded on the next system update because hey, you’re supposed to the get latest version right?
Terminal programs can do the same (`-v1`) in principle. Few do.
- $package-manager install $program
- which $program
- cp $program ~/my/own/directories
- $package-manager uninstall $program
done. It won't ever change by accident.This would be analogical, if commands always included their version. For example:
rm-2.44.287-SNAPSHOT -r /My biggest beef is total lack of CLI exit code.
Makes it useless for bash programming with systemd utils.
A good option package should support long options being both command line flags and environment variables. The really good ones support them in init files as well (I'm partial to the JSON-ish HOCON format, though .INI works too).
I always spec (later overrides previous settings):
- system default (/etc/foo.conf)
- user defaults (~/.foo.conf)
- local defaults (${CWD}/foo.conf)
- user-specified init file (overrides local default)
- environment vars (FOO_OUTPUT=JSON)
- command line var (--output=json)
As well as --no-init and --no-envWriting a replacement utility seems a bit like overkill.
# pizza --cheese
Ordering...
Success.
#
Let some legacy programs like ls and sync keep their single-hyphen combinations. Most new programs should just accept separate flags, and allow either single or double hyphens.
Edit: Yeah, this opinion collects downvotes, doesn’t it? Y’all love it when you accidentally type -recursive instead of --recursive, and it turns out that it means the same thing as -r -e -c -u -s -i -v? That never made any sense to me, for the vast majority of tools out there. It sucks, to be honest.
You must like `find`.
I think the answer to that is just to stop designing argument parsers that way—if you want -a 0 then maybe -a 0 is acceptable, maybe -a=0 is acceptable, but -a0 should not be. I don’t see how this part is controversial.
For the same reason, -all should not be parsed as -a -l -l.
>I favour everything being --long --anyway --to-make-it=really-clear.
Yeah—I think it would be equally clear to write it like -long -anyway -to-make-it=really-clear, if we decided to make more parsers that worked that way. Some parsers do work that way.
>You must like `find`.
I have to assume that this is just sarcasm. The `find` command gives you a DSL for writing queries, and for some reason, the tokens in that DSL are option flags starting with -. Bizarre. I don’t think anybody wants to design something like that, and I don’t think there’s really anything to learn from find except maybe “sometimes, for historical reasons, the command-line arguments for standardized tools just plain suck.”