Skip to content
How to create a CLI in V

How to create a CLI in V

Published: at 13:37

As some of you may know, I’m a fan of V, a programming language inspired by Go but trying to do better on many fronts, with great tooling and native libraries. You can learn more in its documentation, using its playground or watching this quickie session from Devoxx France 2024.

I’ve already covered some aspects of V in a previous article detailing how to create a tiny web server, which leads me to publish tVeb. More recently, I decided to explore the cli module of V, whose aim is to provide a simple way to create application with commands, flags, help, man, etc.

So I decided to create a simple CLI using another great included module: compress. As you may have guessed, it compresses data. In my case, files.

To follow this tutorial you’ll need V and a file editor, nothing more.

Compressing files with V

Let’s create a new folder and a main.v file. For this first step, it creates a text file and compress it with gzip. Here we’ll assume the folder is empty, so we don’t have to check if the file already exists for example:

import os
import compress.gzip

fn main() {
    // Define the file names
    filename := 'test.txt'
    compressed_filename := '${filename}.gz'

    // Create a simple text file
    // The `!` is a way to ignore errors
    os.write_file(filename, 'Hello, world!')!

    // Read the file, in bytes to pass to the compressor
    content := os.read_bytes(filename)!
    compressed_content := gzip.compress(content)!

    // Create the compressed file, or open it if it already exists
    // Here there is an error handling, change `create` by `open` to test it
    mut file_compressed := os.create(compressed_filename) or {
        eprintln('Impossible to access $compressed_filename')
        exit(1)
    }

    // Write the compressed content to the file
    file_compressed.write(compressed_content)!
    file_compressed.close()
    println("✅ File compressed to '$compressed_filename'!")
}

Run it with v run main.v. You should see a test.txt.gz file in the same folder. You can check it has been compressed correctly with :

$ gzip -d test.txt.gz -c
Hello, world!

Zstd and its parameters

The compress module provides a wrapper around Zstandard (learn more with Hubert). It allows to natively define a compression level and how many CPU threads to use. To do so, just edit some lines of the code above:

...
import compress.zstd
...
compressed_filename := '${filename}.zst'
...
compressed_content := zstd.compress(
  content,
  compression_level: 8,
  nb_threads: 4)!
...

You should see a test.txt.zst file in the same folder. If zstd is installed on your system, you can check it has been compressed correctly with:

$ zstd -d test.txt.zst -c
Hello, world!

Let’s start to CLI

What if we want to define compression level and number of threads as parameters and not hardcode them? It’s where the cli module helps.

It allows to define an application, its name, version, description, add commands and flags to it. Here we’ll just define a main command to compress a file and add flags to define compression level and number of threads:

import os
import runtime
import cli { Command, Flag }

fn main() {
    mut app_cli := Command{
        name: 'Compressor'
        description: 'A tiny CLI to compress files with Zstandard'
        version: '0.1.0'
        execute: compress
    }

    app_cli.add_flag(
        Flag{
            flag: cli.FlagType.int
            name: 'level'
            abbrev: 'l'
            description: 'Compression level'
            default_value: ['8']
            required: false
        }
    )

    app_cli.add_flag(
        Flag{
            flag: cli.FlagType.int
            name: 'threads'
            abbrev: 't'
            description: 'Number of threads'
            default_value: [runtime.nr_cpus().str()]
            required: false
        }
    )
    app_cli.setup()
	app_cli.parse(os.args)
}

As you can see, we define short aliases for flags (abbrev), set default values, what’s required or not, etc. In the case of the threads flag, we set the default value to the maximum supported by the CPU with runtime.nr_cpus(). Some flags are automatically added, like help, man or version.

Then, we define the compress function, called when the command is executed. As V allows us, we’ll do it in a dedicated file, named compress.v:

import os
import cli { Command, Flag }
import compress.zstd

// The calling command is passed as a parameter
// The return is void, with no error handling (`!`)
fn compress(cmd cli.Command) ! {
    // Define the file names
    filename := 'test.txt'
    compressed_filename := '${filename}.zst'

    // Create a simple text file
    // The `!` is a way to ignore errors
    os.write_file(filename, 'Hello, world!')!

    level := cmd.flags.get_int('level')!
    threads := cmd.flags.get_int('threads')!

    println('Algorithm: Zstandard, level: $level, threads: $threads')

    // Read the file, in bytes to pass to the compressor
    content := os.read_bytes(filename)!
    compressed_content := zstd.compress(
        content,
        compression_level: level,
        nb_threads: threads
    )!

    // Create the compressed file, it will be open if it already exists
    // Here there is an error handling, change create by open to test it
    mut file_compressed := os.create(compressed_filename) or {
        eprintln('Impossible to access $compressed_filename')
        exit(1)
    }

    // Write the compressed content to the file
    file_compressed.write(compressed_content)!
    file_compressed.close()
    println("✅ File compressed to '$compressed_filename'!")
}

As for main.v it will be considered as included in the main module used by default. You can also add module main at the beginning of both files if you prefer to be explicit. To run the project across files, use:

v run .

Now it’s time to compile and check flags are working:

$ v -prod . -o compressor

$ ./compressor version
Compressor version 0.1.0

$ ./compressor -help
Usage: Compressor [flags] [commands]

A tiny CLI to compress files with Zstandard

Flags:
 -l  -level          Compression level
 -t  -threads        Number of threads
     -help           Prints help information.
     -version        Prints version information.
     -man            Prints the auto-generated manpage.

Commands:
 help                Prints help information.
 version             Prints version information.
 man                 Prints the auto-generated manpage.

$ ./compressor -l 22 -t 2
Algorithm: Zstandard, level: 22, threads: 2
 File compressed to 'test.txt.zst'!

Of course, you can go further: add file management, error handling, more commands, options, etc. Just take a look at the cli module documentation. You can also find a more complete version of this tool in this repository.