The Complete Beginner's Guide to CLI Apps in V #18051
hungrybluedev
started this conversation in
Blog
Replies: 2 comments 4 replies
-
Great example! But why we need big enums converting code here? We can check strings without enum-string mapping. Even in --shape argument we are checking map keys but not enums. |
Beta Was this translation helpful? Give feedback.
3 replies
-
There are issue with enums. See #18365 |
Beta Was this translation helpful? Give feedback.
1 reply
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
Goal
Our goal is to make a simple CLI application that prints various geometric shapes to the screen depending on the options provided. It will be able to recognise command-line arguments with the help of the
flag
module.Here's a sample output of our application:
Interested? Let's get started...
Prerequisites
This tutorial assumes that you have a basic understanding of how the command-line works. If you don't, you can learn how to do so from this excellent article by Flavio Copes.
You should have V installed on your system. Ideally, you should also know the basic syntax of V. If you need help with these, refer to this guide.
Once you have V installed, make sure you have the latest version of V by doing v up. It should automatically update your V installation. In case of any technical difficulties, you can get in touch with us on the V Discord Server.
The source code for this project is available at this repository.
Initialising the Project
Navigate to a directory that you own. For example,
~/Projects
for Linux/MacOS users andC:\Projects
for Windows users. Then type the following command:The tool will prompt you for the project name, description, version, and license. Fill in the details as you see fit. Here is an example of what we can enter.
Let us look at the files that were created by
v new
. In the folder, we find av.mod
file and ageo.v
file.The
v.mod
file looks like this:And this is what
geo.v
contains:V's scaffolding was updated, which puts default applications with a
main
function in thesrc
directory. This is to allow for clean code organisation.Note that the original geo repository has the
geo.v
file in the root directory, which still works (and is recommended for third-party module/package development).Let us make sure that we can run the project out of the box by typing the command
v run .
:Accepting Command-Line Arguments
Let us start with how we want to accept command-line arguments. We can make use of the built-in
flag
andos
modules to do this. In order to use these modules in our code, we will need toimport
them.We will start simple and put everything in the
main
function, which is the entry point for the application. By default, all variables defined in V are immutable. To make a variable mutable we need to use themut
keyword. We will make use of this when we define a newFlagParser
struct using theflag.new_flag_parser()
function.The
FlagParser
can then be modified to add details about the application. Additionally, we define the various flags that we want to accept using functions such asbool(...)
,int(...)
,string(...)
, etc. When the function is called, it will match pieces of the command line arguments to the flags we defined and we store the values in variables that will be used later.Right now, we can just print out the values we've parsed to the terminal. And when we're done, we need to call
finalize()
to finish the parsing, and obtain the rest of the arguments provided by the user.Note that the flag module automatically adds
-h/--help
and--version
whenfinalize()
is called.The code should now look like this:
We define our parser in line 7. We initialize it with details such as the name of the application, version, description, etc. Next, we define the various flags that we want to use. The general syntax is
fp.<type>('<name>', `<short_name>`, <default_value>, '<description>')
.Note that we use backticks to represent a single character, which is also known as a rune. Also, we use the
to_lower()
function to convert the shape input to lowercase to simplify the processing down the road. We also make sure that we protect against zero-length strings for the specified symbol using anor {...}
block. If the user inputs something invalid, it will default to*
.After we're done processing the flags, we call
finalize()
to obtain the remaining arguments. We do nothing with them, so we list them as ignored. In your application, you may use them to get the path of a file, or the name of a directory, and so on. Note the trailing!
at the end. This is to specify that we do not want to handle any errors that might occur while callingfinalize()
and we do not mind if the program crashes. If you want to handle the error, use andor {...}
block.Finally, we print out the values of the flags to the screen.
To run the code, we can use the
v run
command:Alternatively, we can build the executable and then run the application:
Now we test the different command-line arguments:
The
flag
module automatically adds a--version
flag when we callfp.finalize()
. Here's how to use it:The same is also true for the
--help
or-h
flag:If you want to customise the output that is generated by the
--help
and--version
flags, you can redefine them and customise the behaviour.Working with Modules
We can start structuring our application so that we stay organised.
The recommended way to organise V code is with modules/subdirectories. In our case, we want to create a module called
geometry
that contains the relevant code not concerned with command-line argument processing. There are two things we need do to make this module.geometry
. The source root is either the root directory if your root directory has your main V file, or thesrc
directory (in newer projects).geometry
module as long as they have the linemodule geometry
at the top.Now we can make a file
options.v
inside the geometry module folder. The directory structure now looks like this:In this
options.v
file, we will start defining some structs, enums, and functions that we will call from themain
function ingeo.v
. We start by defining anenum
of the various shapes that we plan to support:Next, we define the configuration struct that we will store our user inputs in to pass to the appropriate functions:
ShapeOptions
stores the shape type, size, and symbol. We also define a functionare_valid()
on the ShapeOptions type that checks if the configuration provided is valid. Note that we apply a default value of*
to the symbol. So if it is left uninitialised, it will defaultto
*
. V initialises every field in a struct with a zero-based value if no further information is provided. In case of enums, the default value is the first value specified in the enum.Next, we define a map that makes it easy to convert strings to the
GeometricShapeKind
enum. We also store the string keys to be used later when we want to see if the user shape inputs are valid.Let's put in a temporary implementation of the
generate_shape()
functionwhich accepts a
ShapeOptions
variable:Putting everything together, here is the complete source for
options.v
so far:Going back to
main
ingeo.v
, we make a few changes to properly validate the user input for the shape. We use theallowed_shapes
list of strings to restrict the user input to the ones we will implement. We also check that the size is greater than zero and that
the shape is either not specified, or that it is one of the allowed ones.
We use
'none'
as a dummy shape. If the user does not specify a shape, we continuously ask for input from the user until they enter a valid one or they quit by pressingCtrl-C
.Finally, we can now call the
generate_shape()
function and pass in the details for theShapeOptions
configuration struct.If you are familiar with Python, this syntax is reminiscent of Python's named arguments.
The updated source code for
geo.v
is therefore:We can run the project and make sure everything is working as it should:
Generating the Shapes
Now that we have the pipeline in place, we are ready to return proper shapes as per the user input! We will return the shapes as a list of strings, where each string is a line of the shape.
We add two new files to the
geometry
module:triangle.v
- for containing source code relevant to generation of triangular shapes.quadrilateral.v
- the same but for quadrilateral shapes.The project structure will now be:
The logic to generate the shapes is rather straightforward. We will verify that the options are valid, and if they are, we will return the
appropriate shape as an array of strings.
Here is the source code for the
triangle.v
file:And here is the source code for the
quadrilateral.v
file:All we are doing here is simple manipulations of arrays using nesting and loops with interesting indices. Sometimes, we reuse the previous lines and copy them over to simplify the shape generation, like we do in the
generate_diamond
function.In every function, we make use of
options.are_valid()
to check if the options are valid. If they are, we return the shape. If they arenot, we return an empty array.
Finally, we can modify the
generate_shape
function to use the appropriate functions and return the shapes as requested.Now when we run the project, we see the following outputs:
Final Touches
For future maintainability, we moved the application information to the
geometry/metadata.v
file:Now we are directly extracting the name, version, description from the
v.mod
file. So we only need to keep the information updated in one place. We can modify the relevant part of thegeo.v
file as well:Now, when we want to bump the version number for the project, we can just do
v bump --patch
,v bump --minor
orv bump --major
at the root of the repository.Demonstration
We can now run the application with the following commands:
v run . --shape left-triangle --size 7 --symbol "x"
v .
orv -prod .
which would produce thegeo
executable. Then./geo --shape pyramid
or something similar to run the final executable with the command line options.--help
command should also work.Some sample runs are:
Wrapping Up
The full source code for the project is available here.
It's very easy to make CLI applications really quickly in V. Especially considering you not needing to validate the command-line flags yourself. The user only needs to focus on building the business logic for the application. In this tutorial, we built a sample application of modest size that shows how to use the
flag
module to simplify command-line argument processing, and we also learned how to structure a V application into modules.It is important to note that structs, functions and constants not declared as
pub
are all accessible to all files in the current module. In order to access them outside the module, thepub
keyword is necessary. So we neededpub
forgenerate_shape
,GeometricShape
, etc, for us to use them in the externalmain
module (geo.v
).Hopefully, this tutorial has been helpful to you. You can get in touch with the V Discord community if you have questions or suggestions. Commenting below this post also helps!
Acknowledgements
The idea for this demo project was provided by @SheatNoisette who also helped add the unit tests.
I would also like to thank @spytheman and @JalonSolov, both of whom are active on the V Discord Server, and have provided valuable feedback and suggestions.
Beta Was this translation helpful? Give feedback.
All reactions