start

package module
v0.4.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jun 28, 2021 License: BSD-3-Clause Imports: 10 Imported by: 0

README

Start

Start Go command line apps with ease

Build Status BSD 3-clause License Godoc Reference goreportcard

Flag

Executive Summary

The start package for Go provides two basic features for command line applications:

  1. Read your application settings transparently from either

    • command line flags,
    • environment variables,
    • config file entries,
    • or hardcoded defaults
      in this order. You are free to decide where to put your configuration, start will find it!
  2. Parse commands and subcommands. This includes:

    • Mapping commands and subcommands to functions.
    • Providing an auto-generated help command for every command and subcommand.

Motivation

I built the start package mainly because existing flag packages did not provide any option for getting default values from environment variables or from a config file (let alone in a transparent way). And I decided to include command and subcommand parsing as well, making this package a complete "starter kit".

Status

(start uses Semantic Versioning 2.0.0.)

Version Release

Basic functionality is implemented.
Unit tests pass but no real-world tests were done yet.

Tested with:

  • Go 1.13.5 darwin/amd64 on macOS Catalina
  • Go 1.8.0 darwin/amd64 on macOS Sierra
  • Go 1.7.0 darwin/amd64 on macOS Sierra
  • Go 1.6.3 darwin/amd64 on macOS Sierra
  • Go 1.6.0 darwin/amd64 on Mac/OSX El Capitan
  • Go 1.5.1 darwin/amd64 on Mac/OSX El Capitan
  • Go 1.4.2 darwin/amd64 on Mac/OSX Yosemite
  • Go 1.4.2 linux/arm on a Banana Pi running Bananian OS 15.01 r01
  • Go 1.4.2 win32/amd64 on Windows 7

Usage

import (
	"github.com/christophberger/start"
)
Define application settings:

Define your application settings using pflag:

var ip *int = flag.IntP("intname", "n", 1234, "help message")
var sp *string = flag.StringP("strname", "s", "default", "help message")
var bp *bool = flag.BoolP("boolname", "b", "help message") // default is false if boolean flag is missing

var flagvar int
flag.IntVarP(&flagvar, "flagname", "f" 1234, "help message")

...you know this already from the standard flag package - almost no learning curve here. The pflag package adds POSIX compatibility: --help and -h instead of -help. See the pflag readme for details.

Then (optionally, if not using commands as well) call

start.Parse()

(instead of pflag.Parse()) to initialize each variable from these sources, in the given order:

  1. From a commandline flag of the long or short name.
  2. From an environment variable named <APPNAME>_<LONGNAME>, if the commandline flag does not exist. (<APPNAME> is the executable's name (without extension, if any), and <LONGNAME> is the flag's long name.) [1]
  3. From an entry in the config file, if the environment variable does not exist.
  4. From the default value if the config file entry does not exist.

This way, you are free to decide whether to use a config file, environment variables, flags, or any combination of these. For example, let's assume you want to implement an HTTP server. Some of the settings will depend on the environment (development, test, or production), such as the HTTP port. Using environment variables, you can define, for example, port 8080 on the test server, and port 80 on the production server. Other settings will be the same across environments, so put them into the config file. And finally, you can overwrite any default setting at any time via command line flags.

And best of all, each setting has the same name in the config file, for the environment variable, and for the command line flag (but the latter can also have a short form).

[1] NOTE: If your executable's name contains characters other than a-zA-Z0-9_, then <APPLICATION> must be set to the executable's name with all special characters replaced by an underscore. For example: If your executable is named "start.test", then the environment variable is expected to read START_TEST_CFGPATH.

Define commands:

Use Add() to define a new command. Pass the name, a short and a long help message, optionally a list of command-specific flag names, and the function to call.

start.Add(&start.Command{
		Name:  "command",
		Short: "short help message",
		Long:  "long help for 'help command'",
		Flags: []string{"socket", "port"},
		Cmd:   func(cmd *start.Command) error {
				fmt.Println("Done.")
		}
})

The Cmd function receives its Command struct. It can get the command line via the cmd.Args slice.

Define subcommands in the same way but add the name of the parent command:

start.Add(&start.Command{
		Name:  "command",
		Parent: "parentcmd"
		Short: "short help message",
		Long:  "long help for 'help command'",
		Flags: []string{"socket", "port"},
		Cmd:   func(cmd *start.Command) error {
				fmt.Println("Done.")
		}
})

The parent command's Cmd is then optional. If you specify one, it will only be invoked if no subcommand is used.

For evaluating the command line, call

start.Up()

This method calls start.Parse() and then executes the given command. The command receives its originating Command as input can access cmd.Args (a string array) to get all parameters (minus the flags)

Notes about the config file

By default, start looks for a configuration file in the following places:

  • In the path defined through the environment variable <APPNAME>_CFGPATH
  • In the working directory
  • In the user's config dir:
    • in $XDG_CONFIG_HOME (if defined)
    • in the .config/<appname> directory
    • for Windows, in %LOCALAPPDATA%

The name of the configuration file is either <appname>.toml except if the file is located in $HOME/.config/<appname>; in this case the name is config.toml.

You can also set a custom name:

start.UseConfigFile("<your_config_file>")

start then searches for this file name in the places listed above.

You may as well specify a full path to your configuration file:

start.UseConfigFile("<path_to_your_config_file>")

The above places do not get searched in this case.

Or simply set <APPNAME>_CFGPATH to a path of your choice. If this path does not end in ".toml", start assumes that the path is a directory and tries to find <appname>.toml inside this directory.

The configuration file is a TOML file. By convention, all of the application's global variables are top-level "key=value" entries, outside any section. Besides this, you can include your own sections as well. This is useful if you want to provide defaults for more complex data structures (arrays, tables, nested settings, etc). Access the parsed TOML document directly if you want to read values from TOML sections.

start uses toml-go for parsing the config file. The parsed contents are available via a property named "CfgFile", and you can use toml-go methods for accessing the contents (after having invoked start.Parse()or start.Up()):

langs := start.CfgFile.GetArray("colors")
langs := start.CfgFile.GetDate("publish")

(See the toml-go project for all avaialble methods.)

Example

For this example, let's assume you want to build a fictitious application for translating text. We will go through the steps of setting up a config file, environment variables, command line flags, and commands.

First, set up a config file consisting of key/value pairs:

targetlang = bavarian
sourcelang = english_us
voice = Janet

Set an environment variable. Let's assume your executable is named "gotranslate":

$ export GOTRANSLATE_VOICE = Sepp

Define the global variables in your code, just as you would do with the pflag package:

tl := flag.StringP("targetlang", "t", "danish", "The language to translate into")
var sl string
flag.StringVarP("sourcelang", "s", "english_uk", "The language to translate from")
v := flag.StringP("voice", "v", "Homer", "The voice used for text-to-speech")
speak := flag.BoolP("speak", "p", false, "Speak out the translated string")

Define and implement some commands:

func main() {
	start.Add(&start.Command{
		Name: "translate",
		OwnFlags: []string{"voice", "speak"}, // voice and speak make only sense for the translate command
		Short: "translate [<options>] <string>",
		Long: "Translate a string from a source language into a target language, optionally speaking it out",
		Cmd: translate,
	})

	start.Add(&start.Command{
		Name: "check",
		Short: "check [style|spelling]",
		Long: "Perform various checks",
	})

	start.Add(&start.Command{
		Parent: "check"
		Name: "style",
		Short: "check style <string>",
		Long: "Check the string for slang words or phrases",
		Cmd: checkstyle,
	})

	start.Add("check", &start.Command{
		Parent: "check"
		Name: "spelling",
		Short: "check spelling <string>",
		Long: "Check the string for spelling errors",
		Cmd: checkspelling,
	})

	start.Up()
}


func translate(cmd *start.Command) error {
	source := cmd.Args[0]
	target := google.Translate(sl, source, tl)  // this API method is completely made up

	if speak {
		apple.VoiceKit.SpeakOutString(target).WithVoice(v)  // this also
	}
	return nil
}

func checkstyle(cmd *start.Command) error  {
	// real-life code should check for len(cmd.Args) first
	source := cmd.Args[0]
	stdout.Println(office.StyleChecker(source))  // also made up
	return nil
}

func checkspelling(cmd *start.Command) error {
	source := cmd.Args[0]
	stdout.Println(aspell.Check(source))  // just an imaginary method
	return nil
}

TODO

  • Add predefined "version" and "help" commands OR flags.
  • Factor out most of this large README into [[Wiki|TOC]] pages.
  • Change the mock-up code from the Example section into executable code.

Change Log

See CHANGES.md for details.

About the name

For this package, I chose the name "start" for three reasons:

  1. The package is all about starting a Go application: Read preferences, fetch environment variables, parse the command line.
  2. The package helps starting a new commandline application project quickly.
  3. Last not least this is my starter project on GitHub, and at the same time my first public Go project. (Oh, I am sooo excited!)

Documentation

Overview

Package start combines four common tasks for setting up an commandline application:

* Reading settings from a configuration file * Reading environment variables * Reading command line flags * Defining commands and subcommands

See the file README.md about usage of the start package.

Copyright (c) Christoph Berger. All rights reserved. Use of this source code is governed by the BSD (3-Clause) License that can be found in the LICENSE.txt file.

This source code may import third-party source code whose licenses are provided in the respective license files.

Index

Constants

This section is empty.

Variables

View Source
var (

	// Commands is the global command list.
	Commands = CommandMap{}

	// Note: I do explicitly make use of my right to use package-global variables.
	// First, this package acts like a Singleton. No accidental reuse can happen.
	// Second, these variables do not pollute the global name spaces, as they are
	// package variables and private.
	// These variables might get refactored into a struct at a later time.
	App string // app name

)

Functions

func Add

func Add(cmd *Command) error

Add adds a command to either the global Commands map, or, if the command has a parent value, to its parent command as a subcommand.

func ConfigFilePath

func ConfigFilePath() string

ConfigFilePath returns the path of the config file that has been read in. Use after calling Up() or Parse(). Returns an empty path if no config file was found.

func ConfigFileToml

func ConfigFileToml() toml.Document

ConfigFileToml returns the toml document created from the config file. Useful for fetching additional content from the config file than the one used by the flags.

func External added in v0.4.2

func External() func(cmd *Command) error

External defines an external command to execute via os/exec. The external command's name follows Git subcommmand naming convention: "mycmd do" invokes the external command "mycmd-do".

func GetUserConfigDir added in v0.4.2

func GetUserConfigDir() (dir string, exists bool)

GetUserConfigDir finds the user's config directory in an OS-independent way. "OS-independent" means compatible with most Unix-like operating systems as well as with Microsoft Windows(TM). The boolean return value indicates if the directory exists at the location determined via environment variables.

func Parse

func Parse() error

Parse initializes all flag variables from command line flags, environment variables, configuration file entries, or default values. After this, each flag variable has a value either - - from a command line flag, or - from an environment variable, if the flag is not set, or - from an entry in the config file, if the environment variable is not set, or - from its default value, if there is no entry in the config file. Note: For better efficiency, Parse reads the config file and environment variables only once. Subsequent calls only parse the flags again, so you can call Parse() from multiple places in your code without actually repeating the complete parse process. Use Reparse() if you must execute the full parse process again. This behavior diverges from the behavior of flag.Parse(), which parses always.

func Reparse

func Reparse() error

Reparse is the same as Parse but parses always.

func SetConfigFile

func SetConfigFile(fn string)

SetConfigFile allows to set a custom file name and/or path. Call this before Parse() or Up(), respectively. Afterwards it has of course no effect.

func SetDescription

func SetDescription(descr string)

SetDescription sets a description of the app. It receives a string containing a brief description of the application. If a user runs the application with no arguments, or if the user invokes the help command, Usage() will print this description string and list the available commands.

func SetInitFunc

func SetInitFunc(initf func() error)

SetInitFunc sets a function that is called after parsing the variables but before calling the command. Useful for global initialization that affects all commands alike.

func SetVersion

func SetVersion(ver string)

SetVersion sets the version number of the application. Used by the pre-defined version command.

func Up

func Up()

Up parses all flags and then evaluates and executes the command line.

func Usage

func Usage(cmd *Command) error

Usage prints a description of the application and the short help string of every command, when called with a nil argument. When called with a command as parameter, Usage prints this command's long help string as well as the short help strings of the available subcommands. Parse() or Up() must be called before invoking Usage().

Types

type Command

type Command struct {
	Name   string
	Parent string
	Flags  []string
	Short  string
	Long   string
	Cmd    func(cmd *Command) error
	Args   []string
	Path   string
	// contains filtered or unexported fields
}

Command defines a command or a subcommand. Flags is a list of flag names that the command accepts. If a flag is passed to the command that the command does not accept, and if that flag is not among the global flags available for all commands, then Up() returns an error. If Flags is empty, all global flags are allowed. ShortHelp contains a short help string that is used in --help. LongHelp contains a usage description that is used in --help <command>. Cmd contains the function to execute. It receives the list of arguments (without the flags, which are parsed already). For commands with child commands, Cmd can be left empty. Args gets filled with all arguments, excluding flags. Path is an optional path to external executables that reside outside $PATH. To be used with the External() function.

func (*Command) Add

func (cmd *Command) Add(subcmd *Command) error

Add for Command adds a subcommand to a command.

type CommandMap

type CommandMap map[string]*Command // TODO: Make struct with Usage command

CommandMap represents a list of Command objects.

func (*CommandMap) Add

func (c *CommandMap) Add(cmd *Command) error

Add for CommandMap adds a command to a list of commands.

Directories

Path Synopsis
examples

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL