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.
Top comments (3)
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.
Thanks for pointing out this library!
Awesome.
Would be great to have you joined on below group.
linkedin.com/groups/10546099