Tutorial #
Welcome to the Docli Tutorial! Here you’ll be introduced to the key Docli concepts. If you get stuck at any point in the process, feel free to download a working example of the completed CLI app.
Confirming that Docli is installed #
Before starting the tutorial, let’s make sure that you have Docli installed. Go ahead and create a main.go
file and paste the following content inside:
package main
import (
"github.com/alecthomas/repr"
"github.com/celicoo/docli/v2"
)
func main() {
args := docli.Args()
repr.Println(args)
}
Now build and run it:
$ go build
$ ./main
docli.args{
}
Note: the output is the Abstract Syntax Structure of the command-line arguments, and it’s empty because you didn’t pass any arguments. If you run it again passing arguments you should see a different output.
Creating a new CLI app #
Directory structure #
If you like Cobra, you’ll feel at home with this. While you’re welcome to provide your own organization, typically a Docli-based app will follow the following directory structure:
▾ app/
▾ cmd/
root.go
main.go
Go ahead and create the above structure, but instead of app
, let’s name the root directory to git
.
app/ #
This is the root of our app. The main.go
file goes into this directory and it’s responsible for calling the function that initializes Docli:
package main
import "<path>/git"
func main() {
cmd.Execute()
}
Note: Make sure to replace the <path>
placeholder.
cmd/ #
This is where the commands are stored. The root.go
is responsible for the logic of the root command and the Execute()
function:
package cmd
const version = "0.0.1"
type Git struct {}
func (g *Git) Doc() string {
return ""
}
func (g *Git) Run() {
}
func (g *Git) Help() {
}
func (g *Git) Error(err error) {
}
func Execute() {
}
The Git
struct represents the root command in our CLI app and it must implement the docli.command interface.
Writing the docstring #
Like YAML or Python, the docstring is a line-oriented language that uses indentation to define structure. Lines beginning with either spaces or tabs are used to register arguments and commands (commands are explained a little later in the tutorial). These arguments and commands can have letters of any language, numbers, and dashes.
Let’s go back to the root.go file and replace the Doc() method of our Git struct with:
func (g *Git) Doc() string {
return `usage: git <command>
commands:
c, clone clone a repository into a new directory
arguments:
v, --version print version
Use "git <command> help" for more information about the <command>.`
}
By convention, dashes are used in front of arguments, but not commands. You can use this convention with Docli, but it’s not necessary.
Accessing command-line argument values #
Now that we’ve registered the arguments and commands in the docstring, we’ll need to define them as fields in the Git
struct, so we can access their values. Fields that represent arguments will be primitive types and those that represent commands will be user-defined types that implement the docli.command interface, just like the root command does.
Regardless of how many aliases your arguments or commands have , you’ll only need to (and only should) define one field in the struct. We suggest using the longer identifier, but you can use whichever you’d like.
In our example, we have one command and one argument, and each has two identifiers. Let’s add these to our Git
struct:
type Git struct {
Clone Clone
Version bool
}
For every command, you’ll need to create a file to hold the type that represents the command. In our example, the Clone field represents the clone
command, so we need to create a file that holds this type. Let’s go ahead and create the clone.go
file inside the cmd
directory and paste the following code inside:
package cmd
import (
"fmt"
"log"
"github.com/alecthomas/repr"
)
type Clone struct {
Repository,
Directory string
Verbose,
Quiet,
Progress,
NoCheckout,
Bare,
Mirror,
Local,
NoHardlinks,
Shared bool
RecurseSubmodules string
Jobs int
Template,
Reference,
ReferenceIfAble string
Dissociate bool
Origin,
Branch,
UploadPack string
Depth int
ShallowSince,
ShallowExclude string
SingleBranch,
NoTags,
ShallowSubmodules bool
SeparateGitDir,
Config string
Ipv4,
Ipv6 bool
Filter string
}
func (c *Clone) Doc() string {
return `usage: git clone [<arguments>] --repository=<url> --directory=<directory>
arguments:
-r, --repository repository to clone
-d, --directory path to directory
-v, --verbose be more verbose
-q, --quiet be more quiet
--progress force progress reporting
-n, --no-checkout don't create a checkout
--bare create a bare repository
--mirror create a mirror repository (implies bare)
-l, --local to clone from a local repository
--no-hardlinks don't use local hardlinks, always copy
-s, --shared setup as shared repository
--recurse-submodules=<pathspec> initialize submodules in the clone
-j, --jobs=<n> number of submodules cloned in parallel
--template=<template-directory> directory from which templates will be used
--reference=<repo> reference repository
--reference-if-able=<repo> reference repository
--dissociate use --reference only while cloning
-o, --origin=<name> use <name> instead of 'origin' to track upstream
-b, --branch=<branch> checkout <branch> instead of the remote's HEAD
-u, --upload-pack=<path> path to git-upload-pack on the remote
--depth=<depth> create a shallow clone of that depth
--shallow-since=<time> create a shallow clone since a specific time
--shallow-exclude=<revision> deepen history of shallow clone, excluding rev
--single-branch clone only one branch, HEAD or --branch
--no-tags don't clone any tags, and make later fetches not to follow them
--shallow-submodules any cloned submodules will be shallow
--separate-git-dir=<gitdir> separate git dir from working tree
-c, --config=<key=value> set config inside the new repository
-4, --ipv4 use IPv4 addresses only
-6, --ipv6 use IPv6 addresses only
--filter=<args> object filtering
`
}
func (c *Clone) Run() {
repr.Println(c)
}
func (c *Clone) Help() {
fmt.Println(c.Doc())
}
func (c *Clone) Error(err error) {
log.Fatal(err)
}
In this tutorial, we won’t go into detail about the logic of the Clone command because its functionality is pretty straightforward.
Writing the Methods’ Logic #
A specific method will run when a certain condition is met.
Error method #
The Error method will run when the user passes an argument or command that is not registered in the docstring. Usually, you’ll just print the error message to stderr
and exit the program. In our example, we’ll ignore invalid arguments and force the Run method to run, otherwise we’ll print the error message to stderr
and exit the program:
func (g *Git) Error(err error) {
switch err.(type) {
case *args.InvalidArgumentError:
// Ignore InvalidArgumentError.
g.Run()
default:
log.Fatal(err)
}
}
The InvalidArgumentError
is within the args
package, so make sure to add "github.com/celicoo/docli/v2/args"
to
the import declarations.
Help method #
The Help method will run when the user passes the help
argument. Usually, you’ll just print the docstring to stdout
; that’s what we’ll do in our example:
func (g *Git) Help() {
fmt.Println(g.Doc())
}
Run method #
The Run method will run when no error is found and an argument other than help
is called. In our example we’ll print the version if the --version
(or -v
) command-line argument is passed, otherwise we’ll just print “Hello, world!":
func (g *Git) Run() {
if g.Version {
fmt.Println(version)
return
}
fmt.Println("Hello, world!")
}
Initializing Docli #
We created a CLI app with one command and one argument. But if you try to execute it, it won’t run. There is one piece missing: initializing Docli.
func Execute() {
var g Git
args := docli.Args()
args.Bind(&g)
}
Testing the CLI app #
The very last step is to make sure it’s actually working. To do that, we need to build and run it:
$ go build
$ ./git
Hello, world!
Unlike other packages, Docli doesn’t allow values to be assigned to arguments without using the =
operator. You need to use the =
operator, otherwise the internal parser will think you’re passing an argument and, as a result, the Error
method of the struct that represents the command that you’re executing will be called. The clone
command was added to help you understand this, so go ahead and play with it.
Congratulations! You’ve completed the tutorial. Thank you for reading, and I hope you enjoyed it.
If you have any question, please don’t hesitate to open an issue.