Treat the code that implements a CLI utility to the highest standards.
Let's step back for a moment. In any graphical application, there always is a chunk of code to deal with the interface of the tool and there is another chunk of code to do the "real work" behind the interface, possibly split into multiple logical layers. This basic split of presentation vs. logic is essential for the maintainability of the code. The same is equally true for CLI tools: the code that interacts with the console is the presentation layer, and anything else is part of the backend.
In other words: writing a CLI tool, no matter how simple, is no excuse to write a bunch of spaghetti code riddled with global variables, lack of data structures, full of side effects and without tests.
The guideline to determine what is part of the presentation and what is not is simple: any code that accepts input from the user — be it via command-line arguments or flags or via stdin — and any code that formats data and writes to stdout and stderr has to be considered part of the frontend. Everything else belongs in the backend. And, in your code structure, it should be very clear which parts are the frontend and which are the backend.
This makes more sense if you consider that the majority of CLI-based tools follow this sequential pattern:
- Gather user input, be it in the form of arguments and flags or data fed through stdin.
- Do something with the gathered data like querying a database to retrieve a list of students, fetching a file from a remote server, or performing a computation.
- Display the results of the computation on stdout or display any errors on stderr.
What I'd like you to notice here is that the first and third steps belong in the frontend, and the middle step —which will usually be the most complex— belongs in the backend or your program logic. If you write code with these three phases clearly separated and are strict to not mix presentation with program logic, you will end up with code that is easier to maintain, easier to understand and, better yet, easier to test!
But what if your application does not follow this sequential pattern? For example: what if you want your algorithm to report status to the console as it processes input? That's fine: there are ways to still keep the conceptual separation of the code. One approach would be to make your backend code receive a "hooks" object or a callback function and use those to notify the caller via asynchronous events.
One specific thing to avoid: never write to stdout or stderr from backend code, specially if that code belongs in a shared library.
And if none of this has convinced you, think about it this way: if you were to add a GUI to your application, how would you maximize the amount of code that you can share between the two interfaces?