Mastodon hachyterm.io

Create a command-line tool which downloads a README template for your coding projects

Why Nim?

Nim is a statically typed systems programming language.

Nim generates small, native dependency-free executables. The language combines a Python-like syntax with powerful features like meta-programming.

Nim supports macOS, Linux, BSD, and Windows. The language is open-source and has no corporate affiliation.

Nim compiles to multiple backends, for example, C, C++, or JavaScript.

The ecosystem and community are small, but the language has reached its first stable release.

If you’re interested in a low-level language, then you should take a look at Nim. It’s easier to learn than languages like C++ or Rust, but can be a decent replacement for those languages.

More about Nim on the Nim forum: Why use Nim?


In this blog post, I will show you how to create a command-line tool with Nim.

You will learn how to:

  • connect to the internet with an HTTP client
  • parse command-line options
  • create a file on your system (IO)
  • compile a Nim application and execute it

Install Nim

First, install Nim on your operating system.

I like to use choosenim. choosenim is a tool that allows you to install Nim and its toolchain easily. You can manage multiple Nim installations on your machine.

Here’s how to install Nim with choosenim:

  • Windows:

Get the latest release and run the runme.bat script.

  • Unix (macOS, Linux):
curl https://nim-lang.org/choosenim/init.sh -sSf | sh

(Optional) Nim Basics

Nim offers splendid tutorials and community resources to get started.

I recommend taking a look at Learn Nim in Y minutes or Nim By Example to get a sense of the language.

Let’s Create Our First Program

The Goal

I often need a README template for my coding projects.

My favorite template is Best-README-Template.
But there also other good examples, e.g.:

We want to create a command-line utility which downloads such a template as README.md to the current folder.

You could achieve that goal by using a library like curl. But what happens if your system doesn’t have curl?

Our Nim utility will compile to a stand-alone C binary that will seamlessly run on your system without any dependencies like curl or wget.
And we’ll learn a bit Nim along the way.

1. Connect to the Internet

Create a new file called readme_template_downloader.nim:

import httpClient
var client = newHttpClient() ## mutable variable `var`
echo client.getContent("https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/README.md")

These lines import the httpclient library and create a new instance of the HTTP client.

getContent is an inbuilt procedure (function) that connects to the URL and returns the content of a GET request. For now, we use echo to write to standard output.

Save the file. We’ll now compile it to a C binary.

nim c -d:ssl readme_template_downloader.nim

c stands for compile, -d:ssl is a flag that allows us to use the OpenSSL library.

Now you can run the application. Here’s the command for Unix:

./readme_template_downloader

You should now see the result of the README template in your terminal.

You can also compile and run the program in a single step:

nim c -d:ssl -r readme_template_downloader

2. Create a Procedure

Procedures in Nim are what most other languages call functions. Let’s adjust our file:

import httpClient

var url = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/README.md"

proc downloadTemplate(link: string) =
  var client = newHttpClient()
  echo client.getContent(link)

when isMainModule:
  downloadTemplate(url)

The when statement is a compile-time statement. If you import the file, Nim won’t run the downloadTemplate procedure.
Here the file represents our main module and Nim will invoke the procedure.

In the downloadTemplate procedure, we define the input parameter (link is of type string), but we allow Nim to infer the type of the output.

Don’t forget to re-compile and to rerun the application:

nim c -d:ssl -r readme_template_downloader

3. Write to a File (IO)

We’re able to get the content of the URL, but we haven’t saved it to a file yet.

We’ll use the io module, part of the standard library, for that. We don’t have to import anything, it works out of the box.

import httpClient

var url = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/README.md"

proc downloadTemplate(link: string) =
  var client = newHttpClient()
  try: ## (A)
    var file = open("README.md", fmWrite)  ## (B)
    defer: file.close()
    file.write(client.getContent(link))
    echo("Success - downloaded template to `README.md`.")
  except IOError as err:  ## (C)
    echo("Failed to download template: " & err.msg)

when isMainModule:
  downloadTemplate(url)

On line (A), we use a try statement. It’s the same as in Python. With a try statement, you can handle an exception.

On line (B), we use open to create a new file in write mode. If the file does not exist, Nim will create it. If it already exists, Nim will overwrite it.

defer works like a context manager in Python. It makes sure that Nim closes the file after the operation finishes.

With file.write Nim will save the result of the HTTP GET request to the file.

On line (C), we handle the exception. We can append the message of the IOError to the string that we’ll write to standard output.

For example, if we provide an invalid URL for the HTTP client, the CLI program will output a line like this:

Failed to download template: 404 Bad Request

4. Let’s Code the CLI Interaction

When we run the program with --help or -h, we want some information about the application. Something like this:

nim_template -h

README Template Downloader 0.1.0 (download a README Template)

  Allowed arguments:
  - h | --help     : show help
  - v | --version  : show version
  - d | --default  : dowloads "BEST-README-Template"
  - t | --template : download link for template ("RAW")

Add these lines to the readme_template_downloader.nim file:

proc writeHelp() =
  echo """
  README Template Downloader 0.1.0 (download a README Template)

  Allowed arguments:

  - h | --help : show help
  - v | --version : show version
  - d | --default : dowloads "BEST-README-Template"
  - t | --template : download link for template ("RAW")
    """

proc writeVersion() =
  echo "README Template Downloader 0.1.0"

5. Let’s Write the CLI Command

We’ll write a procedure as the entry point of the script. We’ll move the initialization of the url variable into the procedure, too.

import httpclient, os

## previous code

proc cli() =
  var url: string = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/BLANK_README.md"

  if paramCount() == 0:
    writeHelp()
    quit(0) ## exits program with exit status 0

when isMainModule:
  cli()

Add os to the list of imports at the top of the file for paramCount to work.
paramCount returns the number of command-line arguments given to the application.

In our case, we want to show the output of writeHelp and exit the program if we don’t give any option.

Here’s the whole program so far:

import httpClient, os

proc downloadTemplate(link: string) =
  var client = newHttpClient()
  try:
    var file = open("README.md", fmWrite)
    defer: file.close()
    file.write(client.getContent(link))
    echo("Success - downloaded template to `README.md`.")
  except IOError as err:
    echo("Failed to download template: " & err.msg)

proc writeHelp() =
  echo """
  README Template Downloader 0.1.0 (download a README Template)

  Allowed arguments:

  - h | --help : show help
  - v | --version : show version
  - d | --default : dowloads "BEST-README-Template"
  - t | --template : download link for template ("RAW")
    """

proc writeVersion() =
  echo "README Template Downloader 0.1.0"

proc cli() =
  var url: string = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/BLANK_README.md"

  if paramCount() == 0:
    writeHelp()
    quit(0)

when isMainModule:
  cli()

Compile and run. You should see the help information in your terminal.

5.1. Parse Command-Line Options

Now we need a way to parse the command-line options that the program supports: -v, --default, etc.

Nim provides a getopt iterator in the parseopt module.

Add import parseopt to the top of the file.

import httpclient, os, parseopt

## previous code

proc cli() =
  var url: string = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/BLANK_README.md"

  if paramCount() == 0:
    writeHelp()
    quit(0)

  for kind, key, val in getopt():    ## (A)
    case kind
    of cmdLongOption, cmdShortOption:
      case key
      of "help", "h":
        writeHelp()
        quit()
      of "version", "v":
        writeVersion()
        quit()
      of "d", "default": discard    ## (B)
      of "t", "template": url = val ## (C)
      else:       ## (D)
        discard
    else:
      discard     ## (D)

  downloadTemplate(url) ## (E)

The iterator (line A) checks for the long form of the option (--help) and the short form (-h). The case statement is a multi-branch control-flow construct. See case statement in the Nim Tutorial. The case statement works like the switch/case statement from JavaScript.

--help and -h invoke the writeHelp procedure. --version and -v invoke the writeVersion procedure we defined earlier.

--default or -d is for the default option (see line B). If we don’t provide any arguments, our application will give us the help information. Thus, we have to provide a command-line argument for downloading the default README template. We can discard the value provided to the -d option, because we’ll invoke the downloadTemplate procedure with the default URL later (line E).

The -t or --template (line C) change the value of the url variable.
Let’s say we run the Nim program like this:

./readme_template_downloader -t="https://gist.githubusercontent.com/PurpleBooth/109311bb0361f32d87a2/raw/8254b53ab8dcb18afc64287aaddd9e5b6059f880/README-Template.md"

Now Nim will overwrite the default url variable with the provided option in -t.

We’ll discard everything else (lines D), because we can ignore any other options we provide to our Nim program.

You can find the complete script as a GitHub Gist:

import httpclient, parseopt, os

proc downloadTemplate(link: string) =
  var client = newHttpClient()
  try:
    var file = open("README.md", fmWrite)
    defer: file.close()
    file.write(client.getContent(link))
    echo("Success - downloaded template to `README.md`.")
  except IOError as err:
    echo("Failed to download template: " & err.msg)

proc writeHelp() =
  echo """
  README Template Downloader 0.1.0 (download a README Template)

  Allowed arguments:
  - h | --help     : show help
  - v | --version  : show version
  - d | --default  : dowloads "BEST-README-Template"
  - t | --template : download link for template ("RAW")
  """

proc writeVersion() =
  echo "README Template Downloader 0.1.0"

proc cli() =
  var url: string = "https://raw.githubusercontent.com/othneildrew/Best-README-Template/master/BLANK_README.md"

  if paramCount() == 0:
    writeHelp()
    quit(0)

  for kind, key, val in getopt():
    case kind
    of cmdLongOption, cmdShortOption:
      case key
      of "help", "h":
        writeHelp()
        quit()
      of "version", "v":
        writeVersion()
        quit()
      of "d", "default": discard
      of "t", "template": url = val
      else:
        discard
    else:
      discard

  downloadTemplate(url)

when isMainModule:
  cli()

Don’t forget to re-compile the finished application.

Recap

In this blog post, you learned how to create a Nim utility that downloads a file from the internet to the current folder on your machine.
You learned how to create an HTTP Client, how to write to a file, and how to parse command-line options.
Along the way, you gained a basic understanding of the Nim language: how to use variables, procedures (functions), how to handle exceptions.

To learn more about Nim, see Learn Nim.

Acknowledgments

Credits go to xmonader for his Nim Days repository.