DEV Community

Cover image for Learn Nim: Create a README Template Downloader
Sophia Brandt
Sophia Brandt

Posted on • Originally published at rockyourcode.com on

Learn Nim: Create a README Template Downloader

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
Enter fullscreen mode Exit fullscreen mode

(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")
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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"

Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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.

Links

Top comments (3)

Collapse
 
cblake profile image
c-blake

Readers of this article might also be interested in github.com/c-blake/cligen which can greatly reduce CLI boilerplate code (at the cost of slightly less flexibility Re help messages) using Nim's metaprogramming and static introspection capabilities.

Collapse
 
sophiabrandt profile image
Sophia Brandt

Thanks for pointing out this library!

Collapse
 
adityapadwal profile image
Aditya Padwal

Awesome.
Would be great to have you joined on below group.

linkedin.com/groups/10546099