The inspiration for this post is the Python, Ruby, and Golang: A Command-Line Application Comparison published on Real Python.

I’ve been sticking to my decision to stop writing bash scripts and it has paid off. Python is great for small scripts and cli apps. I do have however some older pet projects in JavaScript (nodeJS) which are a bit large to rewrite in a single day. Additionally, most of the codebase at work is in Java, and context switching can be difficult.

In any case, I thought it’s a nice exercise to write down the common tasks a CLI app has to do and how that can be done in these 3 programming languages.

Command-Line Interface

What is expected from a cli? It should accept arguments in short form (e.g. -n), long form (e.g. --dry-run). It should validate mandatory arguments and it should provide a pretty help message of all the available options. Depending on your needs, you might need sub-command style cli, like git offers.


The command line arguments are passed to the main method. I’m not aware of a built-in CLI builder. With a quick search I found Apache Commons CLI, but I haven’t used it myself. It’s usage page has some detailed examples.

JavaScript (node.js)

Command line arguments are in process.argv. The array will start with ['node', 'script.js'] and the CLI parameters will follow. I don’t think there’s something in the standard library, but I’ve tried the commander dependency, which works good enough.


Command line arguments are in sys.argv. The array will start with the script [''] and the CLI parameters will follow. The standard library has support for a rich parser in the argparse module.

Environment variables

This is a no-brainer and it’s supported out of the box everywhere:

  • Java: System.getenv("VARIABLE").
  • JavaScript: process.env.VARIABLE
  • Python: os.environ['VARIABLE']

A small problem with Java is that System.getenv is a static method and mocking it for unit tests is not that easy.

Recursively listing files (walking a directory)


This depends on the Java version you’re targeting. Java 8 and later has a simpler API:

  .filter(p -> p.getFileName().toString().endsWith(".txt"))

In older versions, you might have to write the recursion yourself using the listFiles() method of a File instance.


node.js does not have a built-in function so you’d need to write it yourself copy paste it from the internet:

const fs = require("fs"),
  path = require("path");

function walkDir(dir, callback) {
  fs.readdirSync(dir).forEach(f => {
    let dirPath = path.join(dir, f);
    let isDirectory = fs.statSync(dirPath).isDirectory();
    isDirectory ? walkDir(dirPath, callback) : callback(path.join(dir, f));


for entry in os.walk('/tmp'):
  dirpath, dirnames, filenames = entry

For every directory, you get an entry which contains:

  • dirpath: the path of the directory
  • dirnames: an array of the sub-directories
  • filenames: an array of the filenames on that directory

I like how Python already splits it into directories and files, which is what you typically want.

File reading / writing


In Java, it can get a bit verbose:

try (
  BufferedReader bufferedReader = new BufferedReader(new FileReader("/tmp/test"));
  BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter("/tmp/test.copy"))
) {
  String line = bufferedReader.readLine();
  while (line != null) {
    line = bufferedReader.readLine();

The library Apache Commons IO can help you out significantly:

List<String> lines = FileUtils.readLines(new File("/tmp/test"), StandardCharsets.UTF_8);
FileUtils.writeLines(new File("/tmp/test.copy"), "utf8", lines);


It’s quite simple:

const contents = fs.readFileSync("/tmp/test", "utf8");
fs.writeFileSync("/tmp/test.copy", contents, "utf8");


Slightly more verbose:

with open('/tmp/test', encoding='utf8') as input_file:
  contents =
with open('/tmp/test.copy', 'w', encoding='utf8') as output_file:

Running a command


The ProcessBuilder class can help out:

var processBuilder = new ProcessBuilder("git", "status")
  .directory(new File("/tmp"));

var process = processBuilder.start();
int exitCode = process.waitFor();
if (exitCode != 0) {
  // the command failed

// get the output of the command to process it
InputStream output = process.getOutputStream();

If we don’t care to process the command’s output, but we want to show it as part of our cli app, then we can call redirectOutputStream(ProcessBuilder.Redirect.INHERIT) on the processBuilder.


nodeJS has the child_process module:

const result = child_process.spawnSync("git", ["status"], {
  cwd: "/tmp",
  encoding: "utf8"
if (result.status) {
  // the command failed

// the output already as a string because of the `encoding` option
const output = result.stdout;

Similar with Java, there is a stdio option which by default is set to pipe and it can be set to inherit if we just want the command’s output to be shown as if it were part of our own.


Python offers the subprocess module in the standard library:

result =['git', 'status'], cwd='/tmp')
if result.returncode != 0:
  // the command failed

Unlike the previous two, the default here is to print the output instead of capture it.

To capture and process the output, we need to pass the stdout=subprocess.PIPE option:

result =['git', 'status'], cwd='/tmp', encoding='utf8', stdout=subprocess.PIPE)
lines = result.stdout.splitlines()

A useful feature of Python is that it has a check=True option which will throw an exception if the command didn’t terminate with a zero exit code. This means that you don’t need to explicitly check if the returncode was zero after each command.

Reading / writing JSON


There are many libraries, I’ve used Jackson. Typically, you’d use it against a pojo that you want to serialize/deserialize. It is possible to use it with a map, although that wouldn’t be the normal case in a strongly typed language like Java:

ObjectMapper objectMapper = new ObjectMapper();
Map<String, String> data = Map.of("hello", "world");

// serialize
String json = objectMapper.writeValueAsString(data);

// deserialize
Map<String, Object> value = objectMapper.readValue(
  new TypeReference<HashMap<String, Object>>() {}

But since the other two languages can serialize/deserialize maps, it’s worth showing that it’s feasible also in Java.


This works in node.js but also in the browser:

JSON.stringify({ hello: "world" });


Python has support for JSON in the standard library json module:

import json
json.dumps({ 'hello': 'world' })

Calling an HTTP API


Java 11 has a new HTTP Client. I didn’t even know about it until 5 minutes ago. I have it a try and it works on my machine:

HttpClient httpClient = HttpClient.newHttpClient();
HttpRequest httpRequest = HttpRequest.newBuilder(URI.create(""))
HttpResponse<String> httpResponse = httpClient.send(httpRequest, HttpResponse.BodyHandlers.ofString());

For external dependencies, if you’re already using Spring, you should probably use the Spring Rest Template. The OkHttp client is also nice and it supports Android too.


The standard https module reminds me why I don’t like asynchronous programming in node.js:

const req = https.request(url, requestOptions, res => {
  const minSuccess = 200;
  const maxSuccess = 300;
  let message = "";
  if (res.statusCode >= minSuccess && res.statusCode < maxSuccess) {
    res.on("data", chunk => {
      message += chunk;

    res.on("end", () => {
      // at this point we have the payload in message
  } else {
    res.on("data", d => {

    // oops error


req.on("error", e => {
  // oops different error

The request external dependency tries to make things prettier. If you want to use Promises and async/await, you have (at least) 3 alternative extra libraries to pick from (which is why I often feel this is a prank).


The standard library has the urllib.request module which is okay:

req = urllib.request.Request(url, headers={
  'Authorization': 'Basic super-secret-credentials-in-base64'

with urllib.request.urlopen(req) as response:
  json_response = json.load(response)

or if you want to post some data:

payload = json.dumps({
  'hello': 'world'

req = urllib.request.Request(url, data=payload, method='POST', headers={
  'Authorization': 'Basic again-it-is-a-secret',
  'Content-Type': 'application/json'

with urllib.request.urlopen(req) as response:
  status = response.status
  if status < 200 or status >= 300:
    raise ValueError(f'Oops: {status}')

Even the documentation of urllib.request mentions that the external dependency Requests package is recommended for a higher-level HTTP client interface. I’ve tried it also, and it is kind of better, but I think that the default is not too bad (and having worked enough with npm I don’t like adding too many dependencies).


How will the users install your app?


To the best of my knowledge, this is missing in the Java world.


You can publish your app on the npm registry. The users can then install it with npm.

e.g. npm i -g eslint will install the eslint cli.

A great feature of npm is the npx variant which allows you to run an app without installing it, e.g. npx eslint will install eslint temporarily, run it, and remove it. It is quite fast too.


Similar with npm, you have pip. You can publish your app on the PyPI registry and the users can install it with pip.

A minor difference for Windows: npm generates cmd scripts while pip generates exe stubs.

Overall remarks


Both the language and the standard library show their age. While the standard library has support for XML and zip files, it doesn’t have built-in support for JSON. The need for backwards compatibility means that there are multiple ways of doing the same thing (e.g. java.time, java.nio, List.of, etc). The programming style is often a bit verbose. Checked exceptions can be annoying (I’ve omitted them from all examples) and don’t play well with newer language features like lambdas and streams.


The standard library is async-first, designed for high performing non-blocking services. This is something of little value for small cli apps. The language itself has 3 different ways of doing async operations (callback hell, promises, async/await), which doesn’t make things easier. All in all, it’s not bad to be honest. I deliberately used synchronous variants in the above examples whenever possible.


The standard library can get you very far without having to install any external dependency. There are options that take into account common use cases, such as validate the success of an external command, which adds extra appreciation points for usability in my book, that feeling that someone went for the extra mile.


De gustibus et coloribus non est disputandum. Pick whatever you like. I like to write less code and having less dependencies to worry about. And remember that there are two kinds of programming languages: the ones people complain about and the ones that nobody uses.