add vendor
This commit is contained in:
parent
70291554e1
commit
499d829860
|
@ -0,0 +1,51 @@
|
||||||
|
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
|
||||||
|
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "570b54cabe6b8eb0bc2dfce68d964677d63b5260"
|
||||||
|
version = "v1.5.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gosuri/uilive"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "ac356e6e42cd31fcef8e6aec13ae9ed6fe87713e"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/jawher/mow.cli"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0e80ee9f63156ea1954dc2375c33a1c7e752c25c"
|
||||||
|
version = "v1.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-colorable"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "167de6bfdfba052fa6b2d3664c8f5272e23c9072"
|
||||||
|
version = "v0.0.9"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-isatty"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "0360b2af4f38e8d38c7fce2a9f4e702702d73a39"
|
||||||
|
version = "v0.0.3"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
name = "github.com/mattn/go-sqlite3"
|
||||||
|
packages = ["."]
|
||||||
|
revision = "6c771bb9887719704b210e87e934f08be014bdb1"
|
||||||
|
version = "v1.6.0"
|
||||||
|
|
||||||
|
[[projects]]
|
||||||
|
branch = "master"
|
||||||
|
name = "golang.org/x/sys"
|
||||||
|
packages = ["unix"]
|
||||||
|
revision = "2c42eef0765b9837fbdab12011af7830f55f88f0"
|
||||||
|
|
||||||
|
[solve-meta]
|
||||||
|
analyzer-name = "dep"
|
||||||
|
analyzer-version = 1
|
||||||
|
inputs-digest = "1a1f721ab61fd1976d54ba03bad816beaefd926604a77b24c318305d4082f779"
|
||||||
|
solver-name = "gps-cdcl"
|
||||||
|
solver-version = 1
|
|
@ -0,0 +1,37 @@
|
||||||
|
# Gopkg.toml example
|
||||||
|
#
|
||||||
|
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
|
||||||
|
# for detailed Gopkg.toml documentation.
|
||||||
|
#
|
||||||
|
# required = ["github.com/user/thing/cmd/thing"]
|
||||||
|
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project"
|
||||||
|
# version = "1.0.0"
|
||||||
|
#
|
||||||
|
# [[constraint]]
|
||||||
|
# name = "github.com/user/project2"
|
||||||
|
# branch = "dev"
|
||||||
|
# source = "github.com/myfork/project2"
|
||||||
|
#
|
||||||
|
# [[override]]
|
||||||
|
# name = "github.com/x/y"
|
||||||
|
# version = "2.4.0"
|
||||||
|
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/fatih/color"
|
||||||
|
version = "1.5.0"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
branch = "master"
|
||||||
|
name = "github.com/gosuri/uilive"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/jawher/mow.cli"
|
||||||
|
version = "1.0.3"
|
||||||
|
|
||||||
|
[[constraint]]
|
||||||
|
name = "github.com/mattn/go-sqlite3"
|
||||||
|
version = "1.6.0"
|
|
@ -0,0 +1,5 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.6
|
||||||
|
- tip
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2013 Fatih Arslan
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||||
|
this software and associated documentation files (the "Software"), to deal in
|
||||||
|
the Software without restriction, including without limitation the rights to
|
||||||
|
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
|
||||||
|
the Software, and to permit persons to whom the Software is furnished to do so,
|
||||||
|
subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||||
|
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||||
|
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
|
||||||
|
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,177 @@
|
||||||
|
# Color [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/color) [![Build Status](http://img.shields.io/travis/fatih/color.svg?style=flat-square)](https://travis-ci.org/fatih/color)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Color lets you use colorized outputs in terms of [ANSI Escape
|
||||||
|
Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It
|
||||||
|
has support for Windows too! The API can be used in several ways, pick one that
|
||||||
|
suits you.
|
||||||
|
|
||||||
|
|
||||||
|
![Color](http://i.imgur.com/c1JI0lA.png)
|
||||||
|
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go get github.com/fatih/color
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the `vendor` folder is here for stability. Remove the folder if you
|
||||||
|
already have the dependencies in your GOPATH.
|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
### Standard colors
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Print with default helper functions
|
||||||
|
color.Cyan("Prints text in cyan.")
|
||||||
|
|
||||||
|
// A newline will be appended automatically
|
||||||
|
color.Blue("Prints %s in blue.", "text")
|
||||||
|
|
||||||
|
// These are using the default foreground colors
|
||||||
|
color.Red("We have red")
|
||||||
|
color.Magenta("And many others ..")
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
### Mix and reuse colors
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a new color object
|
||||||
|
c := color.New(color.FgCyan).Add(color.Underline)
|
||||||
|
c.Println("Prints cyan text with an underline.")
|
||||||
|
|
||||||
|
// Or just add them to New()
|
||||||
|
d := color.New(color.FgCyan, color.Bold)
|
||||||
|
d.Printf("This prints bold cyan %s\n", "too!.")
|
||||||
|
|
||||||
|
// Mix up foreground and background colors, create new mixes!
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
|
||||||
|
boldRed := red.Add(color.Bold)
|
||||||
|
boldRed.Println("This will print text in bold red.")
|
||||||
|
|
||||||
|
whiteBackground := red.Add(color.BgWhite)
|
||||||
|
whiteBackground.Println("Red text with white background.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Use your own output (io.Writer)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use your own io.Writer output
|
||||||
|
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
|
||||||
|
|
||||||
|
blue := color.New(color.FgBlue)
|
||||||
|
blue.Fprint(writer, "This will print text in blue.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom print functions (PrintFunc)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create a custom print function for convenience
|
||||||
|
red := color.New(color.FgRed).PrintfFunc()
|
||||||
|
red("Warning")
|
||||||
|
red("Error: %s", err)
|
||||||
|
|
||||||
|
// Mix up multiple attributes
|
||||||
|
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
|
||||||
|
notice("Don't forget this...")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Custom fprint functions (FprintFunc)
|
||||||
|
|
||||||
|
```go
|
||||||
|
blue := color.New(FgBlue).FprintfFunc()
|
||||||
|
blue(myWriter, "important notice: %s", stars)
|
||||||
|
|
||||||
|
// Mix up with multiple attributes
|
||||||
|
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
|
||||||
|
success(myWriter, "Don't forget this...")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Insert into noncolor strings (SprintFunc)
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Create SprintXxx functions to mix strings with other non-colorized strings:
|
||||||
|
yellow := color.New(color.FgYellow).SprintFunc()
|
||||||
|
red := color.New(color.FgRed).SprintFunc()
|
||||||
|
fmt.Printf("This is a %s and this is %s.\n", yellow("warning"), red("error"))
|
||||||
|
|
||||||
|
info := color.New(color.FgWhite, color.BgGreen).SprintFunc()
|
||||||
|
fmt.Printf("This %s rocks!\n", info("package"))
|
||||||
|
|
||||||
|
// Use helper functions
|
||||||
|
fmt.Println("This", color.RedString("warning"), "should be not neglected.")
|
||||||
|
fmt.Printf("%v %v\n", color.GreenString("Info:"), "an important message.")
|
||||||
|
|
||||||
|
// Windows supported too! Just don't forget to change the output to color.Output
|
||||||
|
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### Plug into existing code
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Use handy standard colors
|
||||||
|
color.Set(color.FgYellow)
|
||||||
|
|
||||||
|
fmt.Println("Existing text will now be in yellow")
|
||||||
|
fmt.Printf("This one %s\n", "too")
|
||||||
|
|
||||||
|
color.Unset() // Don't forget to unset
|
||||||
|
|
||||||
|
// You can mix up parameters
|
||||||
|
color.Set(color.FgMagenta, color.Bold)
|
||||||
|
defer color.Unset() // Use it in your function
|
||||||
|
|
||||||
|
fmt.Println("All text will now be bold magenta.")
|
||||||
|
```
|
||||||
|
|
||||||
|
### Disable color
|
||||||
|
|
||||||
|
There might be a case where you want to disable color output (for example to
|
||||||
|
pipe the standard output of your app to somewhere else). `Color` has support to
|
||||||
|
disable colors both globally and for single color definition. For example
|
||||||
|
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
|
||||||
|
the color output with:
|
||||||
|
|
||||||
|
```go
|
||||||
|
|
||||||
|
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
|
||||||
|
|
||||||
|
if *flagNoColor {
|
||||||
|
color.NoColor = true // disables colorized output
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
It also has support for single color definitions (local). You can
|
||||||
|
disable/enable color output on the fly:
|
||||||
|
|
||||||
|
```go
|
||||||
|
c := color.New(color.FgCyan)
|
||||||
|
c.Println("Prints cyan text")
|
||||||
|
|
||||||
|
c.DisableColor()
|
||||||
|
c.Println("This is printed without any color")
|
||||||
|
|
||||||
|
c.EnableColor()
|
||||||
|
c.Println("This prints again cyan...")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Todo
|
||||||
|
|
||||||
|
* Save/Return previous values
|
||||||
|
* Evaluate fmt.Formatter interface
|
||||||
|
|
||||||
|
|
||||||
|
## Credits
|
||||||
|
|
||||||
|
* [Fatih Arslan](https://github.com/fatih)
|
||||||
|
* Windows support via @mattn: [colorable](https://github.com/mattn/go-colorable)
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details
|
||||||
|
|
|
@ -0,0 +1,600 @@
|
||||||
|
package color
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// NoColor defines if the output is colorized or not. It's dynamically set to
|
||||||
|
// false or true based on the stdout's file descriptor referring to a terminal
|
||||||
|
// or not. This is a global option and affects all colors. For more control
|
||||||
|
// over each color block use the methods DisableColor() individually.
|
||||||
|
NoColor = os.Getenv("TERM") == "dumb" ||
|
||||||
|
(!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd()))
|
||||||
|
|
||||||
|
// Output defines the standard output of the print functions. By default
|
||||||
|
// os.Stdout is used.
|
||||||
|
Output = colorable.NewColorableStdout()
|
||||||
|
|
||||||
|
// colorsCache is used to reduce the count of created Color objects and
|
||||||
|
// allows to reuse already created objects with required Attribute.
|
||||||
|
colorsCache = make(map[Attribute]*Color)
|
||||||
|
colorsCacheMu sync.Mutex // protects colorsCache
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color defines a custom color object which is defined by SGR parameters.
|
||||||
|
type Color struct {
|
||||||
|
params []Attribute
|
||||||
|
noColor *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute defines a single SGR Code
|
||||||
|
type Attribute int
|
||||||
|
|
||||||
|
const escape = "\x1b"
|
||||||
|
|
||||||
|
// Base attributes
|
||||||
|
const (
|
||||||
|
Reset Attribute = iota
|
||||||
|
Bold
|
||||||
|
Faint
|
||||||
|
Italic
|
||||||
|
Underline
|
||||||
|
BlinkSlow
|
||||||
|
BlinkRapid
|
||||||
|
ReverseVideo
|
||||||
|
Concealed
|
||||||
|
CrossedOut
|
||||||
|
)
|
||||||
|
|
||||||
|
// Foreground text colors
|
||||||
|
const (
|
||||||
|
FgBlack Attribute = iota + 30
|
||||||
|
FgRed
|
||||||
|
FgGreen
|
||||||
|
FgYellow
|
||||||
|
FgBlue
|
||||||
|
FgMagenta
|
||||||
|
FgCyan
|
||||||
|
FgWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Foreground Hi-Intensity text colors
|
||||||
|
const (
|
||||||
|
FgHiBlack Attribute = iota + 90
|
||||||
|
FgHiRed
|
||||||
|
FgHiGreen
|
||||||
|
FgHiYellow
|
||||||
|
FgHiBlue
|
||||||
|
FgHiMagenta
|
||||||
|
FgHiCyan
|
||||||
|
FgHiWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background text colors
|
||||||
|
const (
|
||||||
|
BgBlack Attribute = iota + 40
|
||||||
|
BgRed
|
||||||
|
BgGreen
|
||||||
|
BgYellow
|
||||||
|
BgBlue
|
||||||
|
BgMagenta
|
||||||
|
BgCyan
|
||||||
|
BgWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// Background Hi-Intensity text colors
|
||||||
|
const (
|
||||||
|
BgHiBlack Attribute = iota + 100
|
||||||
|
BgHiRed
|
||||||
|
BgHiGreen
|
||||||
|
BgHiYellow
|
||||||
|
BgHiBlue
|
||||||
|
BgHiMagenta
|
||||||
|
BgHiCyan
|
||||||
|
BgHiWhite
|
||||||
|
)
|
||||||
|
|
||||||
|
// New returns a newly created color object.
|
||||||
|
func New(value ...Attribute) *Color {
|
||||||
|
c := &Color{params: make([]Attribute, 0)}
|
||||||
|
c.Add(value...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the given parameters immediately. It will change the color of
|
||||||
|
// output with the given SGR parameters until color.Unset() is called.
|
||||||
|
func Set(p ...Attribute) *Color {
|
||||||
|
c := New(p...)
|
||||||
|
c.Set()
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unset resets all escape attributes and clears the output. Usually should
|
||||||
|
// be called after Set().
|
||||||
|
func Unset() {
|
||||||
|
if NoColor {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(Output, "%s[%dm", escape, Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set sets the SGR sequence.
|
||||||
|
func (c *Color) Set() *Color {
|
||||||
|
if c.isNoColorSet() {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(Output, c.format())
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) unset() {
|
||||||
|
if c.isNoColorSet() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
Unset()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) setWriter(w io.Writer) *Color {
|
||||||
|
if c.isNoColorSet() {
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, c.format())
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) unsetWriter(w io.Writer) {
|
||||||
|
if c.isNoColorSet() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if NoColor {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintf(w, "%s[%dm", escape, Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add is used to chain SGR parameters. Use as many as parameters to combine
|
||||||
|
// and create custom color objects. Example: Add(color.FgRed, color.Underline).
|
||||||
|
func (c *Color) Add(value ...Attribute) *Color {
|
||||||
|
c.params = append(c.params, value...)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) prepend(value Attribute) {
|
||||||
|
c.params = append(c.params, 0)
|
||||||
|
copy(c.params[1:], c.params[0:])
|
||||||
|
c.params[0] = value
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprint formats using the default formats for its operands and writes to w.
|
||||||
|
// Spaces are added between operands when neither is a string.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprint(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
c.setWriter(w)
|
||||||
|
defer c.unsetWriter(w)
|
||||||
|
|
||||||
|
return fmt.Fprint(w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Print formats using the default formats for its operands and writes to
|
||||||
|
// standard output. Spaces are added between operands when neither is a
|
||||||
|
// string. It returns the number of bytes written and any write error
|
||||||
|
// encountered. This is the standard fmt.Print() method wrapped with the given
|
||||||
|
// color.
|
||||||
|
func (c *Color) Print(a ...interface{}) (n int, err error) {
|
||||||
|
c.Set()
|
||||||
|
defer c.unset()
|
||||||
|
|
||||||
|
return fmt.Fprint(Output, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintf formats according to a format specifier and writes to w.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprintf(w io.Writer, format string, a ...interface{}) (n int, err error) {
|
||||||
|
c.setWriter(w)
|
||||||
|
defer c.unsetWriter(w)
|
||||||
|
|
||||||
|
return fmt.Fprintf(w, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Printf formats according to a format specifier and writes to standard output.
|
||||||
|
// It returns the number of bytes written and any write error encountered.
|
||||||
|
// This is the standard fmt.Printf() method wrapped with the given color.
|
||||||
|
func (c *Color) Printf(format string, a ...interface{}) (n int, err error) {
|
||||||
|
c.Set()
|
||||||
|
defer c.unset()
|
||||||
|
|
||||||
|
return fmt.Fprintf(Output, format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fprintln formats using the default formats for its operands and writes to w.
|
||||||
|
// Spaces are always added between operands and a newline is appended.
|
||||||
|
// On Windows, users should wrap w with colorable.NewColorable() if w is of
|
||||||
|
// type *os.File.
|
||||||
|
func (c *Color) Fprintln(w io.Writer, a ...interface{}) (n int, err error) {
|
||||||
|
c.setWriter(w)
|
||||||
|
defer c.unsetWriter(w)
|
||||||
|
|
||||||
|
return fmt.Fprintln(w, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Println formats using the default formats for its operands and writes to
|
||||||
|
// standard output. Spaces are always added between operands and a newline is
|
||||||
|
// appended. It returns the number of bytes written and any write error
|
||||||
|
// encountered. This is the standard fmt.Print() method wrapped with the given
|
||||||
|
// color.
|
||||||
|
func (c *Color) Println(a ...interface{}) (n int, err error) {
|
||||||
|
c.Set()
|
||||||
|
defer c.unset()
|
||||||
|
|
||||||
|
return fmt.Fprintln(Output, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprint is just like Print, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprint(a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprint(a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintln is just like Println, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprintln(a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprintln(a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sprintf is just like Printf, but returns a string instead of printing it.
|
||||||
|
func (c *Color) Sprintf(format string, a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
|
||||||
|
// FprintFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprint().
|
||||||
|
func (c *Color) FprintFunc() func(w io.Writer, a ...interface{}) {
|
||||||
|
return func(w io.Writer, a ...interface{}) {
|
||||||
|
c.Fprint(w, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Print().
|
||||||
|
func (c *Color) PrintFunc() func(a ...interface{}) {
|
||||||
|
return func(a ...interface{}) {
|
||||||
|
c.Print(a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FprintfFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprintf().
|
||||||
|
func (c *Color) FprintfFunc() func(w io.Writer, format string, a ...interface{}) {
|
||||||
|
return func(w io.Writer, format string, a ...interface{}) {
|
||||||
|
c.Fprintf(w, format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintfFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Printf().
|
||||||
|
func (c *Color) PrintfFunc() func(format string, a ...interface{}) {
|
||||||
|
return func(format string, a ...interface{}) {
|
||||||
|
c.Printf(format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FprintlnFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Fprintln().
|
||||||
|
func (c *Color) FprintlnFunc() func(w io.Writer, a ...interface{}) {
|
||||||
|
return func(w io.Writer, a ...interface{}) {
|
||||||
|
c.Fprintln(w, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintlnFunc returns a new function that prints the passed arguments as
|
||||||
|
// colorized with color.Println().
|
||||||
|
func (c *Color) PrintlnFunc() func(a ...interface{}) {
|
||||||
|
return func(a ...interface{}) {
|
||||||
|
c.Println(a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SprintFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprint(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output, example:
|
||||||
|
//
|
||||||
|
// put := New(FgYellow).SprintFunc()
|
||||||
|
// fmt.Fprintf(color.Output, "This is a %s", put("warning"))
|
||||||
|
func (c *Color) SprintFunc() func(a ...interface{}) string {
|
||||||
|
return func(a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprint(a...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SprintfFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprintf(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output.
|
||||||
|
func (c *Color) SprintfFunc() func(format string, a ...interface{}) string {
|
||||||
|
return func(format string, a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprintf(format, a...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SprintlnFunc returns a new function that returns colorized strings for the
|
||||||
|
// given arguments with fmt.Sprintln(). Useful to put into or mix into other
|
||||||
|
// string. Windows users should use this in conjunction with color.Output.
|
||||||
|
func (c *Color) SprintlnFunc() func(a ...interface{}) string {
|
||||||
|
return func(a ...interface{}) string {
|
||||||
|
return c.wrap(fmt.Sprintln(a...))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// sequence returns a formated SGR sequence to be plugged into a "\x1b[...m"
|
||||||
|
// an example output might be: "1;36" -> bold cyan
|
||||||
|
func (c *Color) sequence() string {
|
||||||
|
format := make([]string, len(c.params))
|
||||||
|
for i, v := range c.params {
|
||||||
|
format[i] = strconv.Itoa(int(v))
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(format, ";")
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap wraps the s string with the colors attributes. The string is ready to
|
||||||
|
// be printed.
|
||||||
|
func (c *Color) wrap(s string) string {
|
||||||
|
if c.isNoColorSet() {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.format() + s + c.unformat()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) format() string {
|
||||||
|
return fmt.Sprintf("%s[%sm", escape, c.sequence())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) unformat() string {
|
||||||
|
return fmt.Sprintf("%s[%dm", escape, Reset)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DisableColor disables the color output. Useful to not change any existing
|
||||||
|
// code and still being able to output. Can be used for flags like
|
||||||
|
// "--no-color". To enable back use EnableColor() method.
|
||||||
|
func (c *Color) DisableColor() {
|
||||||
|
c.noColor = boolPtr(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
// EnableColor enables the color output. Use it in conjunction with
|
||||||
|
// DisableColor(). Otherwise this method has no side effects.
|
||||||
|
func (c *Color) EnableColor() {
|
||||||
|
c.noColor = boolPtr(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) isNoColorSet() bool {
|
||||||
|
// check first if we have user setted action
|
||||||
|
if c.noColor != nil {
|
||||||
|
return *c.noColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// if not return the global option, which is disabled by default
|
||||||
|
return NoColor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equals returns a boolean value indicating whether two colors are equal.
|
||||||
|
func (c *Color) Equals(c2 *Color) bool {
|
||||||
|
if len(c.params) != len(c2.params) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, attr := range c.params {
|
||||||
|
if !c2.attrExists(attr) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Color) attrExists(a Attribute) bool {
|
||||||
|
for _, attr := range c.params {
|
||||||
|
if attr == a {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func boolPtr(v bool) *bool {
|
||||||
|
return &v
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCachedColor(p Attribute) *Color {
|
||||||
|
colorsCacheMu.Lock()
|
||||||
|
defer colorsCacheMu.Unlock()
|
||||||
|
|
||||||
|
c, ok := colorsCache[p]
|
||||||
|
if !ok {
|
||||||
|
c = New(p)
|
||||||
|
colorsCache[p] = c
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorPrint(format string, p Attribute, a ...interface{}) {
|
||||||
|
c := getCachedColor(p)
|
||||||
|
|
||||||
|
if !strings.HasSuffix(format, "\n") {
|
||||||
|
format += "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(a) == 0 {
|
||||||
|
c.Print(format)
|
||||||
|
} else {
|
||||||
|
c.Printf(format, a...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func colorString(format string, p Attribute, a ...interface{}) string {
|
||||||
|
c := getCachedColor(p)
|
||||||
|
|
||||||
|
if len(a) == 0 {
|
||||||
|
return c.SprintFunc()(format)
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.SprintfFunc()(format, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Black is a convenient helper function to print with black foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Black(format string, a ...interface{}) { colorPrint(format, FgBlack, a...) }
|
||||||
|
|
||||||
|
// Red is a convenient helper function to print with red foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Red(format string, a ...interface{}) { colorPrint(format, FgRed, a...) }
|
||||||
|
|
||||||
|
// Green is a convenient helper function to print with green foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Green(format string, a ...interface{}) { colorPrint(format, FgGreen, a...) }
|
||||||
|
|
||||||
|
// Yellow is a convenient helper function to print with yellow foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func Yellow(format string, a ...interface{}) { colorPrint(format, FgYellow, a...) }
|
||||||
|
|
||||||
|
// Blue is a convenient helper function to print with blue foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Blue(format string, a ...interface{}) { colorPrint(format, FgBlue, a...) }
|
||||||
|
|
||||||
|
// Magenta is a convenient helper function to print with magenta foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func Magenta(format string, a ...interface{}) { colorPrint(format, FgMagenta, a...) }
|
||||||
|
|
||||||
|
// Cyan is a convenient helper function to print with cyan foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func Cyan(format string, a ...interface{}) { colorPrint(format, FgCyan, a...) }
|
||||||
|
|
||||||
|
// White is a convenient helper function to print with white foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func White(format string, a ...interface{}) { colorPrint(format, FgWhite, a...) }
|
||||||
|
|
||||||
|
// BlackString is a convenient helper function to return a string with black
|
||||||
|
// foreground.
|
||||||
|
func BlackString(format string, a ...interface{}) string { return colorString(format, FgBlack, a...) }
|
||||||
|
|
||||||
|
// RedString is a convenient helper function to return a string with red
|
||||||
|
// foreground.
|
||||||
|
func RedString(format string, a ...interface{}) string { return colorString(format, FgRed, a...) }
|
||||||
|
|
||||||
|
// GreenString is a convenient helper function to return a string with green
|
||||||
|
// foreground.
|
||||||
|
func GreenString(format string, a ...interface{}) string { return colorString(format, FgGreen, a...) }
|
||||||
|
|
||||||
|
// YellowString is a convenient helper function to return a string with yellow
|
||||||
|
// foreground.
|
||||||
|
func YellowString(format string, a ...interface{}) string { return colorString(format, FgYellow, a...) }
|
||||||
|
|
||||||
|
// BlueString is a convenient helper function to return a string with blue
|
||||||
|
// foreground.
|
||||||
|
func BlueString(format string, a ...interface{}) string { return colorString(format, FgBlue, a...) }
|
||||||
|
|
||||||
|
// MagentaString is a convenient helper function to return a string with magenta
|
||||||
|
// foreground.
|
||||||
|
func MagentaString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgMagenta, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CyanString is a convenient helper function to return a string with cyan
|
||||||
|
// foreground.
|
||||||
|
func CyanString(format string, a ...interface{}) string { return colorString(format, FgCyan, a...) }
|
||||||
|
|
||||||
|
// WhiteString is a convenient helper function to return a string with white
|
||||||
|
// foreground.
|
||||||
|
func WhiteString(format string, a ...interface{}) string { return colorString(format, FgWhite, a...) }
|
||||||
|
|
||||||
|
// HiBlack is a convenient helper function to print with hi-intensity black foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiBlack(format string, a ...interface{}) { colorPrint(format, FgHiBlack, a...) }
|
||||||
|
|
||||||
|
// HiRed is a convenient helper function to print with hi-intensity red foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiRed(format string, a ...interface{}) { colorPrint(format, FgHiRed, a...) }
|
||||||
|
|
||||||
|
// HiGreen is a convenient helper function to print with hi-intensity green foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiGreen(format string, a ...interface{}) { colorPrint(format, FgHiGreen, a...) }
|
||||||
|
|
||||||
|
// HiYellow is a convenient helper function to print with hi-intensity yellow foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func HiYellow(format string, a ...interface{}) { colorPrint(format, FgHiYellow, a...) }
|
||||||
|
|
||||||
|
// HiBlue is a convenient helper function to print with hi-intensity blue foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiBlue(format string, a ...interface{}) { colorPrint(format, FgHiBlue, a...) }
|
||||||
|
|
||||||
|
// HiMagenta is a convenient helper function to print with hi-intensity magenta foreground.
|
||||||
|
// A newline is appended to format by default.
|
||||||
|
func HiMagenta(format string, a ...interface{}) { colorPrint(format, FgHiMagenta, a...) }
|
||||||
|
|
||||||
|
// HiCyan is a convenient helper function to print with hi-intensity cyan foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiCyan(format string, a ...interface{}) { colorPrint(format, FgHiCyan, a...) }
|
||||||
|
|
||||||
|
// HiWhite is a convenient helper function to print with hi-intensity white foreground. A
|
||||||
|
// newline is appended to format by default.
|
||||||
|
func HiWhite(format string, a ...interface{}) { colorPrint(format, FgHiWhite, a...) }
|
||||||
|
|
||||||
|
// HiBlackString is a convenient helper function to return a string with hi-intensity black
|
||||||
|
// foreground.
|
||||||
|
func HiBlackString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgHiBlack, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiRedString is a convenient helper function to return a string with hi-intensity red
|
||||||
|
// foreground.
|
||||||
|
func HiRedString(format string, a ...interface{}) string { return colorString(format, FgHiRed, a...) }
|
||||||
|
|
||||||
|
// HiGreenString is a convenient helper function to return a string with hi-intensity green
|
||||||
|
// foreground.
|
||||||
|
func HiGreenString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgHiGreen, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiYellowString is a convenient helper function to return a string with hi-intensity yellow
|
||||||
|
// foreground.
|
||||||
|
func HiYellowString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgHiYellow, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiBlueString is a convenient helper function to return a string with hi-intensity blue
|
||||||
|
// foreground.
|
||||||
|
func HiBlueString(format string, a ...interface{}) string { return colorString(format, FgHiBlue, a...) }
|
||||||
|
|
||||||
|
// HiMagentaString is a convenient helper function to return a string with hi-intensity magenta
|
||||||
|
// foreground.
|
||||||
|
func HiMagentaString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgHiMagenta, a...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HiCyanString is a convenient helper function to return a string with hi-intensity cyan
|
||||||
|
// foreground.
|
||||||
|
func HiCyanString(format string, a ...interface{}) string { return colorString(format, FgHiCyan, a...) }
|
||||||
|
|
||||||
|
// HiWhiteString is a convenient helper function to return a string with hi-intensity white
|
||||||
|
// foreground.
|
||||||
|
func HiWhiteString(format string, a ...interface{}) string {
|
||||||
|
return colorString(format, FgHiWhite, a...)
|
||||||
|
}
|
|
@ -0,0 +1,342 @@
|
||||||
|
package color
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Testing colors is kinda different. First we test for given colors and their
|
||||||
|
// escaped formatted results. Next we create some visual tests to be tested.
|
||||||
|
// Each visual test includes the color name to be compared.
|
||||||
|
func TestColor(t *testing.T) {
|
||||||
|
rb := new(bytes.Buffer)
|
||||||
|
Output = rb
|
||||||
|
|
||||||
|
NoColor = false
|
||||||
|
|
||||||
|
testColors := []struct {
|
||||||
|
text string
|
||||||
|
code Attribute
|
||||||
|
}{
|
||||||
|
{text: "black", code: FgBlack},
|
||||||
|
{text: "red", code: FgRed},
|
||||||
|
{text: "green", code: FgGreen},
|
||||||
|
{text: "yellow", code: FgYellow},
|
||||||
|
{text: "blue", code: FgBlue},
|
||||||
|
{text: "magent", code: FgMagenta},
|
||||||
|
{text: "cyan", code: FgCyan},
|
||||||
|
{text: "white", code: FgWhite},
|
||||||
|
{text: "hblack", code: FgHiBlack},
|
||||||
|
{text: "hred", code: FgHiRed},
|
||||||
|
{text: "hgreen", code: FgHiGreen},
|
||||||
|
{text: "hyellow", code: FgHiYellow},
|
||||||
|
{text: "hblue", code: FgHiBlue},
|
||||||
|
{text: "hmagent", code: FgHiMagenta},
|
||||||
|
{text: "hcyan", code: FgHiCyan},
|
||||||
|
{text: "hwhite", code: FgHiWhite},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range testColors {
|
||||||
|
New(c.code).Print(c.text)
|
||||||
|
|
||||||
|
line, _ := rb.ReadString('\n')
|
||||||
|
scannedLine := fmt.Sprintf("%q", line)
|
||||||
|
colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", c.code, c.text)
|
||||||
|
escapedForm := fmt.Sprintf("%q", colored)
|
||||||
|
|
||||||
|
fmt.Printf("%s\t: %s\n", c.text, line)
|
||||||
|
|
||||||
|
if scannedLine != escapedForm {
|
||||||
|
t.Errorf("Expecting %s, got '%s'\n", escapedForm, scannedLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range testColors {
|
||||||
|
line := New(c.code).Sprintf("%s", c.text)
|
||||||
|
scannedLine := fmt.Sprintf("%q", line)
|
||||||
|
colored := fmt.Sprintf("\x1b[%dm%s\x1b[0m", c.code, c.text)
|
||||||
|
escapedForm := fmt.Sprintf("%q", colored)
|
||||||
|
|
||||||
|
fmt.Printf("%s\t: %s\n", c.text, line)
|
||||||
|
|
||||||
|
if scannedLine != escapedForm {
|
||||||
|
t.Errorf("Expecting %s, got '%s'\n", escapedForm, scannedLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorEquals(t *testing.T) {
|
||||||
|
fgblack1 := New(FgBlack)
|
||||||
|
fgblack2 := New(FgBlack)
|
||||||
|
bgblack := New(BgBlack)
|
||||||
|
fgbgblack := New(FgBlack, BgBlack)
|
||||||
|
fgblackbgred := New(FgBlack, BgRed)
|
||||||
|
fgred := New(FgRed)
|
||||||
|
bgred := New(BgRed)
|
||||||
|
|
||||||
|
if !fgblack1.Equals(fgblack2) {
|
||||||
|
t.Error("Two black colors are not equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fgblack1.Equals(bgblack) {
|
||||||
|
t.Error("Fg and bg black colors are equal")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fgblack1.Equals(fgbgblack) {
|
||||||
|
t.Error("Fg black equals fg/bg black color")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fgblack1.Equals(fgred) {
|
||||||
|
t.Error("Fg black equals Fg red")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fgblack1.Equals(bgred) {
|
||||||
|
t.Error("Fg black equals Bg red")
|
||||||
|
}
|
||||||
|
|
||||||
|
if fgblack1.Equals(fgblackbgred) {
|
||||||
|
t.Error("Fg black equals fg black bg red")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoColor(t *testing.T) {
|
||||||
|
rb := new(bytes.Buffer)
|
||||||
|
Output = rb
|
||||||
|
|
||||||
|
testColors := []struct {
|
||||||
|
text string
|
||||||
|
code Attribute
|
||||||
|
}{
|
||||||
|
{text: "black", code: FgBlack},
|
||||||
|
{text: "red", code: FgRed},
|
||||||
|
{text: "green", code: FgGreen},
|
||||||
|
{text: "yellow", code: FgYellow},
|
||||||
|
{text: "blue", code: FgBlue},
|
||||||
|
{text: "magent", code: FgMagenta},
|
||||||
|
{text: "cyan", code: FgCyan},
|
||||||
|
{text: "white", code: FgWhite},
|
||||||
|
{text: "hblack", code: FgHiBlack},
|
||||||
|
{text: "hred", code: FgHiRed},
|
||||||
|
{text: "hgreen", code: FgHiGreen},
|
||||||
|
{text: "hyellow", code: FgHiYellow},
|
||||||
|
{text: "hblue", code: FgHiBlue},
|
||||||
|
{text: "hmagent", code: FgHiMagenta},
|
||||||
|
{text: "hcyan", code: FgHiCyan},
|
||||||
|
{text: "hwhite", code: FgHiWhite},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range testColors {
|
||||||
|
p := New(c.code)
|
||||||
|
p.DisableColor()
|
||||||
|
p.Print(c.text)
|
||||||
|
|
||||||
|
line, _ := rb.ReadString('\n')
|
||||||
|
if line != c.text {
|
||||||
|
t.Errorf("Expecting %s, got '%s'\n", c.text, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// global check
|
||||||
|
NoColor = true
|
||||||
|
defer func() {
|
||||||
|
NoColor = false
|
||||||
|
}()
|
||||||
|
for _, c := range testColors {
|
||||||
|
p := New(c.code)
|
||||||
|
p.Print(c.text)
|
||||||
|
|
||||||
|
line, _ := rb.ReadString('\n')
|
||||||
|
if line != c.text {
|
||||||
|
t.Errorf("Expecting %s, got '%s'\n", c.text, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorVisual(t *testing.T) {
|
||||||
|
// First Visual Test
|
||||||
|
Output = colorable.NewColorableStdout()
|
||||||
|
|
||||||
|
New(FgRed).Printf("red\t")
|
||||||
|
New(BgRed).Print(" ")
|
||||||
|
New(FgRed, Bold).Println(" red")
|
||||||
|
|
||||||
|
New(FgGreen).Printf("green\t")
|
||||||
|
New(BgGreen).Print(" ")
|
||||||
|
New(FgGreen, Bold).Println(" green")
|
||||||
|
|
||||||
|
New(FgYellow).Printf("yellow\t")
|
||||||
|
New(BgYellow).Print(" ")
|
||||||
|
New(FgYellow, Bold).Println(" yellow")
|
||||||
|
|
||||||
|
New(FgBlue).Printf("blue\t")
|
||||||
|
New(BgBlue).Print(" ")
|
||||||
|
New(FgBlue, Bold).Println(" blue")
|
||||||
|
|
||||||
|
New(FgMagenta).Printf("magenta\t")
|
||||||
|
New(BgMagenta).Print(" ")
|
||||||
|
New(FgMagenta, Bold).Println(" magenta")
|
||||||
|
|
||||||
|
New(FgCyan).Printf("cyan\t")
|
||||||
|
New(BgCyan).Print(" ")
|
||||||
|
New(FgCyan, Bold).Println(" cyan")
|
||||||
|
|
||||||
|
New(FgWhite).Printf("white\t")
|
||||||
|
New(BgWhite).Print(" ")
|
||||||
|
New(FgWhite, Bold).Println(" white")
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
// Second Visual test
|
||||||
|
Black("black")
|
||||||
|
Red("red")
|
||||||
|
Green("green")
|
||||||
|
Yellow("yellow")
|
||||||
|
Blue("blue")
|
||||||
|
Magenta("magenta")
|
||||||
|
Cyan("cyan")
|
||||||
|
White("white")
|
||||||
|
HiBlack("hblack")
|
||||||
|
HiRed("hred")
|
||||||
|
HiGreen("hgreen")
|
||||||
|
HiYellow("hyellow")
|
||||||
|
HiBlue("hblue")
|
||||||
|
HiMagenta("hmagenta")
|
||||||
|
HiCyan("hcyan")
|
||||||
|
HiWhite("hwhite")
|
||||||
|
|
||||||
|
// Third visual test
|
||||||
|
fmt.Println()
|
||||||
|
Set(FgBlue)
|
||||||
|
fmt.Println("is this blue?")
|
||||||
|
Unset()
|
||||||
|
|
||||||
|
Set(FgMagenta)
|
||||||
|
fmt.Println("and this magenta?")
|
||||||
|
Unset()
|
||||||
|
|
||||||
|
// Fourth Visual test
|
||||||
|
fmt.Println()
|
||||||
|
blue := New(FgBlue).PrintlnFunc()
|
||||||
|
blue("blue text with custom print func")
|
||||||
|
|
||||||
|
red := New(FgRed).PrintfFunc()
|
||||||
|
red("red text with a printf func: %d\n", 123)
|
||||||
|
|
||||||
|
put := New(FgYellow).SprintFunc()
|
||||||
|
warn := New(FgRed).SprintFunc()
|
||||||
|
|
||||||
|
fmt.Fprintf(Output, "this is a %s and this is %s.\n", put("warning"), warn("error"))
|
||||||
|
|
||||||
|
info := New(FgWhite, BgGreen).SprintFunc()
|
||||||
|
fmt.Fprintf(Output, "this %s rocks!\n", info("package"))
|
||||||
|
|
||||||
|
notice := New(FgBlue).FprintFunc()
|
||||||
|
notice(os.Stderr, "just a blue notice to stderr")
|
||||||
|
|
||||||
|
// Fifth Visual Test
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
fmt.Fprintln(Output, BlackString("black"))
|
||||||
|
fmt.Fprintln(Output, RedString("red"))
|
||||||
|
fmt.Fprintln(Output, GreenString("green"))
|
||||||
|
fmt.Fprintln(Output, YellowString("yellow"))
|
||||||
|
fmt.Fprintln(Output, BlueString("blue"))
|
||||||
|
fmt.Fprintln(Output, MagentaString("magenta"))
|
||||||
|
fmt.Fprintln(Output, CyanString("cyan"))
|
||||||
|
fmt.Fprintln(Output, WhiteString("white"))
|
||||||
|
fmt.Fprintln(Output, HiBlackString("hblack"))
|
||||||
|
fmt.Fprintln(Output, HiRedString("hred"))
|
||||||
|
fmt.Fprintln(Output, HiGreenString("hgreen"))
|
||||||
|
fmt.Fprintln(Output, HiYellowString("hyellow"))
|
||||||
|
fmt.Fprintln(Output, HiBlueString("hblue"))
|
||||||
|
fmt.Fprintln(Output, HiMagentaString("hmagenta"))
|
||||||
|
fmt.Fprintln(Output, HiCyanString("hcyan"))
|
||||||
|
fmt.Fprintln(Output, HiWhiteString("hwhite"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFormat(t *testing.T) {
|
||||||
|
fmt.Printf("%s %%s = ", BlackString("Black"))
|
||||||
|
Black("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", RedString("Red"))
|
||||||
|
Red("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", GreenString("Green"))
|
||||||
|
Green("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", YellowString("Yellow"))
|
||||||
|
Yellow("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", BlueString("Blue"))
|
||||||
|
Blue("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", MagentaString("Magenta"))
|
||||||
|
Magenta("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", CyanString("Cyan"))
|
||||||
|
Cyan("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", WhiteString("White"))
|
||||||
|
White("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiBlackString("HiBlack"))
|
||||||
|
HiBlack("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiRedString("HiRed"))
|
||||||
|
HiRed("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiGreenString("HiGreen"))
|
||||||
|
HiGreen("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiYellowString("HiYellow"))
|
||||||
|
HiYellow("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiBlueString("HiBlue"))
|
||||||
|
HiBlue("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiMagentaString("HiMagenta"))
|
||||||
|
HiMagenta("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiCyanString("HiCyan"))
|
||||||
|
HiCyan("%s")
|
||||||
|
|
||||||
|
fmt.Printf("%s %%s = ", HiWhiteString("HiWhite"))
|
||||||
|
HiWhite("%s")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoFormatString(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
f func(string, ...interface{}) string
|
||||||
|
format string
|
||||||
|
args []interface{}
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{BlackString, "%s", nil, "\x1b[30m%s\x1b[0m"},
|
||||||
|
{RedString, "%s", nil, "\x1b[31m%s\x1b[0m"},
|
||||||
|
{GreenString, "%s", nil, "\x1b[32m%s\x1b[0m"},
|
||||||
|
{YellowString, "%s", nil, "\x1b[33m%s\x1b[0m"},
|
||||||
|
{BlueString, "%s", nil, "\x1b[34m%s\x1b[0m"},
|
||||||
|
{MagentaString, "%s", nil, "\x1b[35m%s\x1b[0m"},
|
||||||
|
{CyanString, "%s", nil, "\x1b[36m%s\x1b[0m"},
|
||||||
|
{WhiteString, "%s", nil, "\x1b[37m%s\x1b[0m"},
|
||||||
|
{HiBlackString, "%s", nil, "\x1b[90m%s\x1b[0m"},
|
||||||
|
{HiRedString, "%s", nil, "\x1b[91m%s\x1b[0m"},
|
||||||
|
{HiGreenString, "%s", nil, "\x1b[92m%s\x1b[0m"},
|
||||||
|
{HiYellowString, "%s", nil, "\x1b[93m%s\x1b[0m"},
|
||||||
|
{HiBlueString, "%s", nil, "\x1b[94m%s\x1b[0m"},
|
||||||
|
{HiMagentaString, "%s", nil, "\x1b[95m%s\x1b[0m"},
|
||||||
|
{HiCyanString, "%s", nil, "\x1b[96m%s\x1b[0m"},
|
||||||
|
{HiWhiteString, "%s", nil, "\x1b[97m%s\x1b[0m"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, test := range tests {
|
||||||
|
s := fmt.Sprintf("%s", test.f(test.format, test.args...))
|
||||||
|
if s != test.want {
|
||||||
|
t.Errorf("[%d] want: %q, got: %q", i, test.want, s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
/*
|
||||||
|
Package color is an ANSI color package to output colorized or SGR defined
|
||||||
|
output to the standard output. The API can be used in several way, pick one
|
||||||
|
that suits you.
|
||||||
|
|
||||||
|
Use simple and default helper functions with predefined foreground colors:
|
||||||
|
|
||||||
|
color.Cyan("Prints text in cyan.")
|
||||||
|
|
||||||
|
// a newline will be appended automatically
|
||||||
|
color.Blue("Prints %s in blue.", "text")
|
||||||
|
|
||||||
|
// More default foreground colors..
|
||||||
|
color.Red("We have red")
|
||||||
|
color.Yellow("Yellow color too!")
|
||||||
|
color.Magenta("And many others ..")
|
||||||
|
|
||||||
|
// Hi-intensity colors
|
||||||
|
color.HiGreen("Bright green color.")
|
||||||
|
color.HiBlack("Bright black means gray..")
|
||||||
|
color.HiWhite("Shiny white color!")
|
||||||
|
|
||||||
|
However there are times where custom color mixes are required. Below are some
|
||||||
|
examples to create custom color objects and use the print functions of each
|
||||||
|
separate color object.
|
||||||
|
|
||||||
|
// Create a new color object
|
||||||
|
c := color.New(color.FgCyan).Add(color.Underline)
|
||||||
|
c.Println("Prints cyan text with an underline.")
|
||||||
|
|
||||||
|
// Or just add them to New()
|
||||||
|
d := color.New(color.FgCyan, color.Bold)
|
||||||
|
d.Printf("This prints bold cyan %s\n", "too!.")
|
||||||
|
|
||||||
|
|
||||||
|
// Mix up foreground and background colors, create new mixes!
|
||||||
|
red := color.New(color.FgRed)
|
||||||
|
|
||||||
|
boldRed := red.Add(color.Bold)
|
||||||
|
boldRed.Println("This will print text in bold red.")
|
||||||
|
|
||||||
|
whiteBackground := red.Add(color.BgWhite)
|
||||||
|
whiteBackground.Println("Red text with White background.")
|
||||||
|
|
||||||
|
// Use your own io.Writer output
|
||||||
|
color.New(color.FgBlue).Fprintln(myWriter, "blue color!")
|
||||||
|
|
||||||
|
blue := color.New(color.FgBlue)
|
||||||
|
blue.Fprint(myWriter, "This will print text in blue.")
|
||||||
|
|
||||||
|
You can create PrintXxx functions to simplify even more:
|
||||||
|
|
||||||
|
// Create a custom print function for convenient
|
||||||
|
red := color.New(color.FgRed).PrintfFunc()
|
||||||
|
red("warning")
|
||||||
|
red("error: %s", err)
|
||||||
|
|
||||||
|
// Mix up multiple attributes
|
||||||
|
notice := color.New(color.Bold, color.FgGreen).PrintlnFunc()
|
||||||
|
notice("don't forget this...")
|
||||||
|
|
||||||
|
You can also FprintXxx functions to pass your own io.Writer:
|
||||||
|
|
||||||
|
blue := color.New(FgBlue).FprintfFunc()
|
||||||
|
blue(myWriter, "important notice: %s", stars)
|
||||||
|
|
||||||
|
// Mix up with multiple attributes
|
||||||
|
success := color.New(color.Bold, color.FgGreen).FprintlnFunc()
|
||||||
|
success(myWriter, don't forget this...")
|
||||||
|
|
||||||
|
|
||||||
|
Or create SprintXxx functions to mix strings with other non-colorized strings:
|
||||||
|
|
||||||
|
yellow := New(FgYellow).SprintFunc()
|
||||||
|
red := New(FgRed).SprintFunc()
|
||||||
|
|
||||||
|
fmt.Printf("this is a %s and this is %s.\n", yellow("warning"), red("error"))
|
||||||
|
|
||||||
|
info := New(FgWhite, BgGreen).SprintFunc()
|
||||||
|
fmt.Printf("this %s rocks!\n", info("package"))
|
||||||
|
|
||||||
|
Windows support is enabled by default. All Print functions work as intended.
|
||||||
|
However only for color.SprintXXX functions, user should use fmt.FprintXXX and
|
||||||
|
set the output to color.Output:
|
||||||
|
|
||||||
|
fmt.Fprintf(color.Output, "Windows support: %s", color.GreenString("PASS"))
|
||||||
|
|
||||||
|
info := New(FgWhite, BgGreen).SprintFunc()
|
||||||
|
fmt.Fprintf(color.Output, "this %s rocks!\n", info("package"))
|
||||||
|
|
||||||
|
Using with existing code is possible. Just use the Set() method to set the
|
||||||
|
standard output to the given parameters. That way a rewrite of an existing
|
||||||
|
code is not required.
|
||||||
|
|
||||||
|
// Use handy standard colors.
|
||||||
|
color.Set(color.FgYellow)
|
||||||
|
|
||||||
|
fmt.Println("Existing text will be now in Yellow")
|
||||||
|
fmt.Printf("This one %s\n", "too")
|
||||||
|
|
||||||
|
color.Unset() // don't forget to unset
|
||||||
|
|
||||||
|
// You can mix up parameters
|
||||||
|
color.Set(color.FgMagenta, color.Bold)
|
||||||
|
defer color.Unset() // use it in your function
|
||||||
|
|
||||||
|
fmt.Println("All text will be now bold magenta.")
|
||||||
|
|
||||||
|
There might be a case where you want to disable color output (for example to
|
||||||
|
pipe the standard output of your app to somewhere else). `Color` has support to
|
||||||
|
disable colors both globally and for single color definition. For example
|
||||||
|
suppose you have a CLI app and a `--no-color` bool flag. You can easily disable
|
||||||
|
the color output with:
|
||||||
|
|
||||||
|
var flagNoColor = flag.Bool("no-color", false, "Disable color output")
|
||||||
|
|
||||||
|
if *flagNoColor {
|
||||||
|
color.NoColor = true // disables colorized output
|
||||||
|
}
|
||||||
|
|
||||||
|
It also has support for single color definitions (local). You can
|
||||||
|
disable/enable color output on the fly:
|
||||||
|
|
||||||
|
c := color.New(color.FgCyan)
|
||||||
|
c.Println("Prints cyan text")
|
||||||
|
|
||||||
|
c.DisableColor()
|
||||||
|
c.Println("This is printed without any color")
|
||||||
|
|
||||||
|
c.EnableColor()
|
||||||
|
c.Println("This prints again cyan...")
|
||||||
|
*/
|
||||||
|
package color
|
|
@ -0,0 +1,7 @@
|
||||||
|
language: go
|
||||||
|
sudo: false
|
||||||
|
install:
|
||||||
|
- go get ./...
|
||||||
|
go:
|
||||||
|
- 1.4
|
||||||
|
- tip
|
|
@ -0,0 +1,10 @@
|
||||||
|
MIT License
|
||||||
|
===========
|
||||||
|
|
||||||
|
Copyright (c) 2015, Greg Osuri
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,31 @@
|
||||||
|
# uilive [![GoDoc](https://godoc.org/github.com/gosuri/uilive?status.svg)](https://godoc.org/github.com/gosuri/uilive) [![Build Status](https://travis-ci.org/gosuri/uilive.svg?branch=master)](https://travis-ci.org/gosuri/uilive)
|
||||||
|
|
||||||
|
uilive is a go library for updating terminal output in realtime. It provides a buffered [io.Writer](https://golang.org/pkg/io/#Writer) that is flushed at a timed interval. uilive powers [uiprogress](https://github.com/gosuri/uiprogress).
|
||||||
|
|
||||||
|
## Usage Example
|
||||||
|
|
||||||
|
Calling `uilive.New()` will create a new writer. To start rendering, simply call `writer.Start()` and update the ui by writing to the `writer`. Full source for the below example is in [example/main.go](example/main.go).
|
||||||
|
|
||||||
|
```go
|
||||||
|
writer := uilive.New()
|
||||||
|
// start listening for updates and render
|
||||||
|
writer.Start()
|
||||||
|
|
||||||
|
for i := 0; i <= 100; i++ {
|
||||||
|
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
||||||
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||||
|
writer.Stop() // flush and stop rendering
|
||||||
|
```
|
||||||
|
|
||||||
|
The above will render
|
||||||
|
|
||||||
|
![example](doc/example.gif)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```sh
|
||||||
|
$ go get -v github.com/gosuri/uilive
|
||||||
|
```
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Package uilive provides a writer that live updates the terminal. It provides a buffered io.Writer that is flushed at a timed interval.
|
||||||
|
package uilive
|
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gosuri/uilive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
writer := uilive.New()
|
||||||
|
|
||||||
|
// start listening for updates and render
|
||||||
|
writer.Start()
|
||||||
|
|
||||||
|
for _, f := range []string{"Foo.zip", "Bar.iso"} {
|
||||||
|
for i := 0; i <= 50; i++ {
|
||||||
|
fmt.Fprintf(writer, "Downloading %s.. (%d/%d) GB\n", f, i, 50)
|
||||||
|
time.Sleep(time.Millisecond * 25)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(writer.Bypass(), "Downloaded %s\n", f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||||
|
writer.Stop() // flush and stop rendering
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package uilive_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gosuri/uilive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
writer := uilive.New()
|
||||||
|
|
||||||
|
// start listening to updates and render
|
||||||
|
writer.Start()
|
||||||
|
|
||||||
|
for i := 0; i <= 100; i++ {
|
||||||
|
fmt.Fprintf(writer, "Downloading.. (%d/%d) GB\n", i, 100)
|
||||||
|
time.Sleep(time.Millisecond * 5)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Fprintln(writer, "Finished: Downloaded 100GB")
|
||||||
|
writer.Stop() // flush and stop rendering
|
||||||
|
}
|
|
@ -0,0 +1,138 @@
|
||||||
|
package uilive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ESC is the ASCII code for escape character
|
||||||
|
const ESC = 27
|
||||||
|
|
||||||
|
// RefreshInterval is the default refresh interval to update the ui
|
||||||
|
var RefreshInterval = time.Millisecond
|
||||||
|
|
||||||
|
// Out is the default output writer for the Writer
|
||||||
|
var Out = os.Stdout
|
||||||
|
|
||||||
|
// ErrClosedPipe is the error returned when trying to writer is not listening
|
||||||
|
var ErrClosedPipe = errors.New("uilive: read/write on closed pipe")
|
||||||
|
|
||||||
|
// FdWriter is a writer with a file descriptor.
|
||||||
|
type FdWriter interface {
|
||||||
|
io.Writer
|
||||||
|
Fd() uintptr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer is a buffered the writer that updates the terminal. The contents of writer will be flushed on a timed interval or when Flush is called.
|
||||||
|
type Writer struct {
|
||||||
|
// Out is the writer to write to
|
||||||
|
Out io.Writer
|
||||||
|
|
||||||
|
// RefreshInterval is the time the UI sould refresh
|
||||||
|
RefreshInterval time.Duration
|
||||||
|
|
||||||
|
ticker *time.Ticker
|
||||||
|
tdone chan bool
|
||||||
|
|
||||||
|
buf bytes.Buffer
|
||||||
|
mtx *sync.Mutex
|
||||||
|
lineCount int
|
||||||
|
}
|
||||||
|
|
||||||
|
type bypass struct {
|
||||||
|
writer *Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// New returns a new Writer with defaults
|
||||||
|
func New() *Writer {
|
||||||
|
return &Writer{
|
||||||
|
Out: Out,
|
||||||
|
RefreshInterval: RefreshInterval,
|
||||||
|
|
||||||
|
mtx: &sync.Mutex{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush writes to the out and resets the buffer. It should be called after the last call to Write to ensure that any data buffered in the Writer is written to output.
|
||||||
|
// Any incomplete escape sequence at the end is considered complete for formatting purposes.
|
||||||
|
// An error is returned if the contents of the buffer cannot be written to the underlying output stream
|
||||||
|
func (w *Writer) Flush() error {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
|
||||||
|
// do nothing is buffer is empty
|
||||||
|
if len(w.buf.Bytes()) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
w.clearLines()
|
||||||
|
|
||||||
|
lines := 0
|
||||||
|
for _, b := range w.buf.Bytes() {
|
||||||
|
if b == '\n' {
|
||||||
|
lines++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
w.lineCount = lines
|
||||||
|
_, err := w.Out.Write(w.buf.Bytes())
|
||||||
|
w.buf.Reset()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start starts the listener in a non-blocking manner
|
||||||
|
func (w *Writer) Start() {
|
||||||
|
if w.ticker == nil {
|
||||||
|
w.ticker = time.NewTicker(w.RefreshInterval)
|
||||||
|
w.tdone = make(chan bool, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
go w.Listen()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop stops the listener that updates the terminal
|
||||||
|
func (w *Writer) Stop() {
|
||||||
|
w.Flush()
|
||||||
|
close(w.tdone)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen listens for updates to the writer's buffer and flushes to the out provided. It blocks the runtime.
|
||||||
|
func (w *Writer) Listen() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-w.ticker.C:
|
||||||
|
if w.ticker != nil {
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
case <-w.tdone:
|
||||||
|
w.mtx.Lock()
|
||||||
|
w.ticker.Stop()
|
||||||
|
w.ticker = nil
|
||||||
|
w.mtx.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write save the contents of b to its buffers. The only errors returned are ones encountered while writing to the underlying buffer.
|
||||||
|
func (w *Writer) Write(b []byte) (n int, err error) {
|
||||||
|
w.mtx.Lock()
|
||||||
|
defer w.mtx.Unlock()
|
||||||
|
return w.buf.Write(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bypass creates an io.Writer which allows non-buffered output to be written to the underlying output
|
||||||
|
func (w *Writer) Bypass() io.Writer {
|
||||||
|
return &bypass{writer: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *bypass) Write(p []byte) (n int, err error) {
|
||||||
|
b.writer.mtx.Lock()
|
||||||
|
defer b.writer.mtx.Unlock()
|
||||||
|
|
||||||
|
b.writer.clearLines()
|
||||||
|
b.writer.lineCount = 0
|
||||||
|
return b.writer.Out.Write(p)
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package uilive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (w *Writer) clearLines() {
|
||||||
|
for i := 0; i < w.lineCount; i++ {
|
||||||
|
fmt.Fprintf(w.Out, "%c[2K", ESC) // clear the line
|
||||||
|
fmt.Fprintf(w.Out, "%c[%dA", ESC, 1) // move the cursor up
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
package uilive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWriter(t *testing.T) {
|
||||||
|
w := New()
|
||||||
|
b := &bytes.Buffer{}
|
||||||
|
w.Out = b
|
||||||
|
w.Start()
|
||||||
|
for i := 0; i < 2; i++ {
|
||||||
|
fmt.Fprintln(w, "foo")
|
||||||
|
}
|
||||||
|
w.Stop()
|
||||||
|
fmt.Fprintln(b, "bar")
|
||||||
|
|
||||||
|
want := "foo\nfoo\nbar\n"
|
||||||
|
if b.String() != want {
|
||||||
|
t.Fatalf("want %q, got %q", want, b.String())
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package uilive
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
|
||||||
|
var (
|
||||||
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||||
|
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||||
|
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
|
||||||
|
)
|
||||||
|
|
||||||
|
type short int16
|
||||||
|
type dword uint32
|
||||||
|
type word uint16
|
||||||
|
|
||||||
|
type coord struct {
|
||||||
|
x short
|
||||||
|
y short
|
||||||
|
}
|
||||||
|
|
||||||
|
type smallRect struct {
|
||||||
|
left short
|
||||||
|
top short
|
||||||
|
right short
|
||||||
|
bottom short
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleScreenBufferInfo struct {
|
||||||
|
size coord
|
||||||
|
cursorPosition coord
|
||||||
|
attributes word
|
||||||
|
window smallRect
|
||||||
|
maximumWindowSize coord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Writer) clearLines() {
|
||||||
|
f, ok := w.Out.(FdWriter)
|
||||||
|
if ok && !isatty.IsTerminal(f.Fd()) {
|
||||||
|
ok = false
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
for i := 0; i < w.lineCount; i++ {
|
||||||
|
fmt.Fprintf(w.Out, "%c[%dA", ESC, 0) // move the cursor up
|
||||||
|
fmt.Fprintf(w.Out, "%c[2K\r", ESC) // clear the line
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fd := f.Fd()
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(fd, uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
for i := 0; i < w.lineCount; i++ {
|
||||||
|
// move the cursor up
|
||||||
|
csbi.cursorPosition.y--
|
||||||
|
procSetConsoleCursorPosition.Call(fd, uintptr(*(*int32)(unsafe.Pointer(&csbi.cursorPosition))))
|
||||||
|
// clear the line
|
||||||
|
cursor := coord{
|
||||||
|
x: csbi.window.left,
|
||||||
|
y: csbi.window.top + csbi.cursorPosition.y,
|
||||||
|
}
|
||||||
|
var count, w dword
|
||||||
|
count = dword(csbi.size.x)
|
||||||
|
procFillConsoleOutputCharacter.Call(fd, uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&w)))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
testdata/*.golden
|
||||||
|
coverage.out
|
|
@ -0,0 +1,19 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- 1.8
|
||||||
|
- 1.7
|
||||||
|
- tip
|
||||||
|
sudo: false
|
||||||
|
|
||||||
|
install: make setup
|
||||||
|
|
||||||
|
script: make check test
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
provider: releases
|
||||||
|
api_key:
|
||||||
|
secure: D2fwuS7n54ZkjfRmLMEAnwR2lQLa5xd+4s0l65pI6UN4ljVfQwQiFdZXaZWFr8PYSTKTm0UPj6Iqqw7VOl8I6iDzcaOqN3hoh5mDaJ7kyo7GZRPodwK9MKdOp5dPw459L2Atll8kxb4iYmfnjmcl0lDCUuWvMxXx+zgiFTB7BO0=
|
||||||
|
skip_cleanup: true
|
||||||
|
on:
|
||||||
|
tags: true
|
||||||
|
go: 1.8
|
|
@ -0,0 +1,22 @@
|
||||||
|
The MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2014, Jawher Moussa
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in
|
||||||
|
all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
THE SOFTWARE.
|
||||||
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
test:
|
||||||
|
go test -v ./...
|
||||||
|
|
||||||
|
check: lint vet fmtcheck ineffassign
|
||||||
|
|
||||||
|
lint:
|
||||||
|
golint -set_exit_status .
|
||||||
|
|
||||||
|
vet:
|
||||||
|
go vet
|
||||||
|
|
||||||
|
fmtcheck:
|
||||||
|
@ export output="$$(gofmt -s -d .)"; \
|
||||||
|
[ -n "$${output}" ] && echo "$${output}" && export status=1; \
|
||||||
|
exit $${status:-0}
|
||||||
|
|
||||||
|
ineffassign:
|
||||||
|
ineffassign .
|
||||||
|
|
||||||
|
setup:
|
||||||
|
go get github.com/gordonklaus/ineffassign
|
||||||
|
go get github.com/golang/lint/golint
|
||||||
|
go get -t -u ./...
|
||||||
|
|
||||||
|
.PHONY: test check lint vet fmtcheck ineffassign
|
|
@ -0,0 +1,771 @@
|
||||||
|
# mow.cli
|
||||||
|
|
||||||
|
[![Build Status](https://travis-ci.org/jawher/mow.cli.svg?branch=master)](https://travis-ci.org/jawher/mow.cli)
|
||||||
|
[![GoDoc](https://godoc.org/github.com/jawher/mow.cli?status.svg)](https://godoc.org/github.com/jawher/mow.cli)
|
||||||
|
|
||||||
|
A framework to build command line applications in Go with most of the burden of arguments parsing and validation placed on the framework instead of the developer.
|
||||||
|
|
||||||
|
## First app
|
||||||
|
|
||||||
|
### A simple app
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jawher/mow.cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.App("cp", "Copy files around")
|
||||||
|
|
||||||
|
app.Spec = "[-r] SRC... DST"
|
||||||
|
|
||||||
|
var (
|
||||||
|
recursive = app.BoolOpt("r recursive", false, "Copy files recursively")
|
||||||
|
src = app.StringsArg("SRC", nil, "Source files to copy")
|
||||||
|
dst = app.StringArg("DST", "", "Destination where to copy files to")
|
||||||
|
)
|
||||||
|
|
||||||
|
app.Action = func() {
|
||||||
|
fmt.Printf("Copying %v to %s [recursively: %v]\n", *src, *dst, *recursive)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### An app with multiple commands:
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/jawher/mow.cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
app := cli.App("uman", "User Manager")
|
||||||
|
|
||||||
|
app.Spec = "[-v]"
|
||||||
|
|
||||||
|
var (
|
||||||
|
verbose = app.BoolOpt("v verbose", false, "Verbose debug mode")
|
||||||
|
)
|
||||||
|
|
||||||
|
app.Before = func() {
|
||||||
|
if *verbose {
|
||||||
|
// Here you can enable debug output in your logger for example
|
||||||
|
fmt.Println("Verbose mode enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Declare a first command, invocable with "uman list"
|
||||||
|
app.Command("list", "list the users", func(cmd *cli.Cmd) {
|
||||||
|
// These are the command specific options and args, nicely scoped inside a func
|
||||||
|
var (
|
||||||
|
all = cmd.BoolOpt("all", false, "Display all users, including disabled ones")
|
||||||
|
)
|
||||||
|
|
||||||
|
// What to run when this command is called
|
||||||
|
cmd.Action = func() {
|
||||||
|
// Inside the action, and only inside, you can safely access the values of the options and arguments
|
||||||
|
fmt.Printf("user list (including disabled ones: %v)\n", *all)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// The second command, invocable with "uman get"
|
||||||
|
app.Command("get", "get a user details", func(cmd *cli.Cmd) {
|
||||||
|
var (
|
||||||
|
detailed = cmd.BoolOpt("detailed", false, "Disaply detailed information")
|
||||||
|
id = cmd.StringArg("ID", "", "The user id to display")
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.Action = func() {
|
||||||
|
fmt.Printf("user %q details (detailed mode: %v)\n", *id, *detailed)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Now that the app is configured, execute it passing in the os.Args array
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Motivation
|
||||||
|
|
||||||
|
| | mow.cli | codegangsta/cli | flag |
|
||||||
|
|----------------------------------------------------------------------|---------|-----------------|------|
|
||||||
|
| Contextual help | ✓ | ✓ | |
|
||||||
|
| Commands | ✓ | ✓ | |
|
||||||
|
| Option folding `-xyz` | ✓ | | |
|
||||||
|
| Option Value folding `-fValue` | ✓ | | |
|
||||||
|
| Option exclusion: `--start ❘ --stop` | ✓ | | |
|
||||||
|
| Option dependency : `[-a -b]` or `[-a [-b]]` | ✓ | | |
|
||||||
|
| Arguments validation : `SRC DST` | ✓ | | |
|
||||||
|
| Argument optionality : `SRC [DST]` | ✓ | | |
|
||||||
|
| Argument repetition : `SRC... DST` | ✓ | | |
|
||||||
|
| Option/Argument dependency : `SRC [-f DST]` | ✓ | | |
|
||||||
|
| Any combination of the above: `[-d ❘ --rm] IMAGE [COMMAND [ARG...]]` | ✓ | | |
|
||||||
|
|
||||||
|
In the goland, docopt is another library with rich flags and arguments validation.
|
||||||
|
However, it falls short for many use cases:
|
||||||
|
|
||||||
|
| | mow.cli | docopt |
|
||||||
|
|-----------------------------|---------|--------|
|
||||||
|
| Contextual help | ✓ | |
|
||||||
|
| Backtracking: `SRC... DST` | ✓ | |
|
||||||
|
| Backtracking: `[SRC] DST` | ✓ | |
|
||||||
|
| Branching: `(SRC ❘ -f DST)` | ✓ | |
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install this library, simply run:
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/jawher/mow.cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basics
|
||||||
|
|
||||||
|
You start by creating an application by passing a name and a description:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cp := cli.App("cp", "Copy files around")
|
||||||
|
```
|
||||||
|
|
||||||
|
To attach the code to execute when the app is launched, assign a function to the Action field:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cp.Action = func() {
|
||||||
|
fmt.Printf("Hello world\n")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want you can add support for printing the app version (invoked by ```-v, --version```) like so:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cp.Version("v version", "cp 1.2.3")
|
||||||
|
```
|
||||||
|
|
||||||
|
Finally, in your main func, call Run on the app:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cp.Run(os.Args)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Options
|
||||||
|
|
||||||
|
To add a (global) option, call one of the (String[s]|Int[s]|Bool)Opt methods on the app:
|
||||||
|
|
||||||
|
```go
|
||||||
|
recursive := cp.BoolOpt("R recursive", false, "recursively copy the src to dst")
|
||||||
|
```
|
||||||
|
|
||||||
|
* The first argument is a space separated list of names (short and long) for the option without the dashes, e.g `"f force"`. While you can specify multiple short or long names, e.g. `"f x force force-push"`, only the first short name and the first long name will be displayed in the help messages
|
||||||
|
* The second parameter is the default value for the option
|
||||||
|
* The third and last parameter is the option description, as will be shown in the help messages
|
||||||
|
|
||||||
|
There is also a second set of methods Bool, String, Int, Strings and Ints, which accepts a struct describing the option:
|
||||||
|
|
||||||
|
```go
|
||||||
|
recursive = cp.Bool(cli.BoolOpt{
|
||||||
|
Name: "R recursive",
|
||||||
|
Value: false,
|
||||||
|
Desc: "copy src files recursively",
|
||||||
|
EnvVar: "VAR_RECURSIVE",
|
||||||
|
SetByUser: &recursiveSetByUser,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
`EnvVar` accepts a space separated list of environment variables names to be used to initialize the option.
|
||||||
|
|
||||||
|
If `SetByUser` is specified (by passing a pointer to a bool variable), it will be set to `true` if the user explicitly set the option.
|
||||||
|
|
||||||
|
The result is a pointer to a value which will be populated after parsing the command line arguments.
|
||||||
|
You can access the values in the Action func.
|
||||||
|
|
||||||
|
In the command line, mow.cli accepts the following syntaxes
|
||||||
|
|
||||||
|
### For boolean options:
|
||||||
|
|
||||||
|
|
||||||
|
* `-f` : a single dash for the one letter names
|
||||||
|
* `-f=false` : a single dash for the one letter names, equal sign followed by true or false
|
||||||
|
* `--force` : double dash for longer option names
|
||||||
|
* `-it` : mow.cli supports option folding, this is equivalent to: -i -t
|
||||||
|
|
||||||
|
### For string, int options:
|
||||||
|
|
||||||
|
|
||||||
|
* `-e=value` : single dash for one letter names, equal sign followed by the value
|
||||||
|
* `-e value` : single dash for one letter names, space followed by the value
|
||||||
|
* `-Ivalue` : single dash for one letter names immediately followed by the value
|
||||||
|
* `--extra=value` : double dash for longer option names, equal sign followed by the value
|
||||||
|
* `--extra value` : double dash for longer option names, space followed by the value
|
||||||
|
|
||||||
|
### For slice options (StringsOpt, IntsOpt):
|
||||||
|
repeat the option to accumulate the values in the resulting slice:
|
||||||
|
|
||||||
|
* `-e PATH:/bin -e PATH:/usr/bin` : resulting slice contains `["/bin", "/usr/bin"]`
|
||||||
|
* `-ePATH:/bin -ePATH:/usr/bin` : resulting slice contains `["/bin", "/usr/bin"]`
|
||||||
|
* `-e=PATH:/bin -e=PATH:/usr/bin` : resulting slice contains `["/bin", "/usr/bin"]`
|
||||||
|
* `--env PATH:/bin --env PATH:/usr/bin` : resulting slice contains `["/bin", "/usr/bin"]`
|
||||||
|
* `--env=PATH:/bin --env=PATH:/usr/bin` : resulting slice contains `["/bin", "/usr/bin"]`
|
||||||
|
|
||||||
|
|
||||||
|
## Arguments
|
||||||
|
|
||||||
|
To accept arguments, you need to explicitly declare them by calling one of the (String[s]|Int[s]|Bool)Arg methods on the app:
|
||||||
|
|
||||||
|
```go
|
||||||
|
src := cp.StringArg("SRC", "", "the file to copy")
|
||||||
|
dst := cp.StringArg("DST", "", "the destination")
|
||||||
|
```
|
||||||
|
|
||||||
|
* The first argument is the argument name as will be shown in the help messages
|
||||||
|
* The second parameter is the default value for the argument
|
||||||
|
* The third parameter is the argument description, as will be shown in the help messages
|
||||||
|
|
||||||
|
|
||||||
|
There is also a second set of methods Bool, String, Int, Strings and Ints, which accepts structs describing the argument:
|
||||||
|
|
||||||
|
```go
|
||||||
|
src = cp.Strings(cli.StringsArg{
|
||||||
|
Name: "SRC",
|
||||||
|
Desc: "The source files to copy",
|
||||||
|
Value: "default value",
|
||||||
|
EnvVar: "VAR1 VAR2",
|
||||||
|
SetByUser: &srcSetByUser,
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The Value field is where you can set the initial value for the argument.
|
||||||
|
|
||||||
|
`EnvVar` accepts a space separated list of environment variables names to be used to initialize the argument.
|
||||||
|
|
||||||
|
If `SetByUser` is specified (by passing a pointer to a bool variable), it will be set to `true` only if the user explicitly sets the argument.
|
||||||
|
|
||||||
|
The result is a pointer to a value that will be populated after parsing the command line arguments.
|
||||||
|
You can access the values in the Action func.
|
||||||
|
|
||||||
|
|
||||||
|
## Operators
|
||||||
|
|
||||||
|
The `--` operator marks the end of options.
|
||||||
|
Everything that follow will be treated as an argument,
|
||||||
|
even if starts with a dash.
|
||||||
|
|
||||||
|
For example, given the `touch` command which takes a filename as an argument (and possibly other options):
|
||||||
|
|
||||||
|
```go
|
||||||
|
file := cp.StringArg("FILE", "", "the file to create")
|
||||||
|
```
|
||||||
|
|
||||||
|
If we try to create a file named `-f` this way:
|
||||||
|
|
||||||
|
```
|
||||||
|
touch -f
|
||||||
|
```
|
||||||
|
|
||||||
|
Would fail, because `-f` will be parsed as an option not as an argument.
|
||||||
|
The fix is to prefix the filename with the `--` operator:
|
||||||
|
|
||||||
|
```
|
||||||
|
touch -- -f
|
||||||
|
```
|
||||||
|
|
||||||
|
## Commands
|
||||||
|
|
||||||
|
mow.cli supports nesting commands and sub commands.
|
||||||
|
Declare a top level command by calling the Command func on the app struct, and a sub command by calling
|
||||||
|
the Command func on the command struct:
|
||||||
|
|
||||||
|
```go
|
||||||
|
docker := cli.App("docker", "A self-sufficient runtime for linux containers")
|
||||||
|
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
// initialize the run command here
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
* The first argument is the command name, as will be shown in the help messages and as will need to be input by the user in the command line to call the command
|
||||||
|
* The second argument is the command description as will be shown in the help messages
|
||||||
|
* The third argument is a CmdInitializer, a function that receives a pointer to a Cmd struct representing the command.
|
||||||
|
In this function, you can add options and arguments by calling the same methods as you would with an app struct (BoolOpt, StringArg, ...).
|
||||||
|
You would also assign a function to the Action field of the Cmd struct for it to be executed when the command is invoked.
|
||||||
|
|
||||||
|
```go
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
var (
|
||||||
|
detached = cmd.BoolOpt("d detach", false, "Detached mode: run the container in the background and print the new container ID")
|
||||||
|
memory = cmd.StringOpt("m memory", "", "Memory limit (format: <number><optional unit>, where unit = b, k, m or g)")
|
||||||
|
image = cmd.StringArg("IMAGE", "", "")
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.Action = func() {
|
||||||
|
if *detached {
|
||||||
|
//do something
|
||||||
|
}
|
||||||
|
runContainer(*image, *detached, *memory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
You can also add sub commands by calling Command on the Cmd struct:
|
||||||
|
|
||||||
|
```go
|
||||||
|
bzk.Command("job", "actions on jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Command("list", "list jobs", listJobs)
|
||||||
|
cmd.Command("start", "start a new job", startJob)
|
||||||
|
cmd.Command("log", "show a job log", nil)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
When you just want to set Action to cmd, you can use ActionCommand function for this:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Command("list", "list all configs", cli.ActionCommand(func() { list() }))
|
||||||
|
```
|
||||||
|
|
||||||
|
is the same as:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Command("list", "list all configs", func(cmd *cli.Cmd)) {
|
||||||
|
cmd.Action = func() {
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This could go on to any depth if need be.
|
||||||
|
|
||||||
|
mow.cli also supports command aliases. For example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Command("start run r", "start doing things", cli.ActionCommand(func() { start() }))
|
||||||
|
```
|
||||||
|
|
||||||
|
will alias `start`, `run`, and `r` to the same action. Aliases also work for
|
||||||
|
subcommands:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Command("job j", "actions on jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Command("list ls", "list jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Action = func() {
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
which then allows you to invoke the subcommand as `app job list`, `app job ls`,
|
||||||
|
`app j ls`, or `app j list`.
|
||||||
|
|
||||||
|
|
||||||
|
As a side-note: it may seem a bit weird the way mow.cli uses a function to initialize a command instead of just returning the command struct.
|
||||||
|
|
||||||
|
The motivation behind this choice is scoping: as with the standard flag package, adding an option or an argument returns a pointer to a value which will be populated when the app is run.
|
||||||
|
|
||||||
|
Since you'll want to store these pointers in variables, and to avoid having dozens of them in the same scope (the main func for example or as global variables),
|
||||||
|
mow.cli's API was specifically tailored to take a func parameter (called CmdInitializer) which accepts the command struct.
|
||||||
|
|
||||||
|
This way, the command specific variables scope is limited to this function.
|
||||||
|
|
||||||
|
## Custom types
|
||||||
|
|
||||||
|
Out of the box, mow.cli supports the following types for options and arguments:
|
||||||
|
|
||||||
|
* bool
|
||||||
|
* string
|
||||||
|
* int
|
||||||
|
* strings (slice of strings)
|
||||||
|
* ints (slice of ints)
|
||||||
|
|
||||||
|
You can however extend mow.cli to handle other types, e.g. `time.Duration`, `float64`, or even your own struct types for example.
|
||||||
|
|
||||||
|
To do so, you'll need to:
|
||||||
|
|
||||||
|
* implement the `flag.Value` interface for the custom type
|
||||||
|
* declare the option or the flag using `VarOpt`, `VarArg` for the short hands, and `Var` for the full form.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Declare your type
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (d *Duration) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = Duration(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) String() string {
|
||||||
|
duration := time.Duration(*d)
|
||||||
|
return duration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
duration := Duration(0)
|
||||||
|
|
||||||
|
app := App("var", "")
|
||||||
|
|
||||||
|
app.VarArg("DURATION", &duration, "")
|
||||||
|
|
||||||
|
app.Run([]string{"cp", "1h31m42s"})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Boolean custom types
|
||||||
|
|
||||||
|
To make your custom type behave as a boolean option, i.e. doesn't take a value, it has to implement a `IsBoolFlag` method that returns true:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type BoolLike int
|
||||||
|
|
||||||
|
|
||||||
|
func (d *BoolLike) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Multi-valued custom type
|
||||||
|
|
||||||
|
To make your custom type behave as a multi-valued option or argument, i.e. takes multiple values,
|
||||||
|
it has to implement a `Clear` method which will be called whenever the value list needs to be cleared,
|
||||||
|
e.g. when the value was initially populated from an environment variable, and then explicitly set from the CLI:
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Durations []time.Duration
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (d *Durations) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = append(*d, Duration(parsed))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Durations) String() string {
|
||||||
|
return fmt.Sprintf("%v", *d)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Make it multi-valued
|
||||||
|
func (d *Durations) Clear() {
|
||||||
|
*d = []Duration{}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hide default value of custom type
|
||||||
|
|
||||||
|
If your custom type implements a `IsDefault` method (returning a boolean), the help message generation will make use of it to decide whether or not to display the default value.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Action string
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
:
|
||||||
|
:
|
||||||
|
|
||||||
|
// Make it multi-valued
|
||||||
|
func (a *Action) IsDefault() bool {
|
||||||
|
return (*a) == "nop"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Interceptors
|
||||||
|
|
||||||
|
It is possible to define snippets of code to be executed before and after a command or any of its sub commands is executed.
|
||||||
|
|
||||||
|
For example, given an app with multiple commands but with a global flag which toggles a verbose mode:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app := cli.App("app", "bla bla")
|
||||||
|
|
||||||
|
verbose := app.Bool(cli.BoolOpt{
|
||||||
|
Name: "verbose",
|
||||||
|
Value: false,
|
||||||
|
Desc: "Enable debug logs",
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Command("command1", "...", func(cmd *cli.Cmd) {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Command("command2", "...", func(cmd *cli.Cmd) {
|
||||||
|
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
Instead of repeating yourself by checking if the verbose flag is set or not, and setting the debug level in every command (and its sub-commands),
|
||||||
|
a before interceptor can be set on the `app` instead:
|
||||||
|
|
||||||
|
```go
|
||||||
|
app.Before = func() {
|
||||||
|
if (*verbose) {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Whenever a valid command is called by the user, all the before interceptors defined on the app and the intermediate commands
|
||||||
|
will be called, in order from the root to the leaf.
|
||||||
|
|
||||||
|
Similarly, if you need to execute a code snippet after a command has been called, e.g. to cleanup resources allocated in before interceptors,
|
||||||
|
simply set the `After` field of the app struct or any other command.
|
||||||
|
`After` interceptors will be called, in order from the leaf up to the root (the opposite order of the `Before` interceptors).
|
||||||
|
|
||||||
|
Here's a diagram which shows in when and in which order multiple `Before` and `After` interceptors get executed:
|
||||||
|
|
||||||
|
![flow](http://i.imgur.com/oUEa8Sh.png)
|
||||||
|
|
||||||
|
## Spec
|
||||||
|
|
||||||
|
An app or command's call syntax can be customized using spec strings.
|
||||||
|
This can be useful to indicate that an argument is optional for example, or that 2 options are mutually exclusive.
|
||||||
|
|
||||||
|
You can set a spec string on:
|
||||||
|
|
||||||
|
* The app: to configure the syntax for global options and arguments
|
||||||
|
* A command: to configure the syntax for that command's options and arguments
|
||||||
|
|
||||||
|
In both cases, a spec string is assigned to the Spec field:
|
||||||
|
|
||||||
|
```go
|
||||||
|
cp := cli.App("cp", "Copy files around")
|
||||||
|
cp.Spec = "[-R [-H | -L | -P]]"
|
||||||
|
```
|
||||||
|
|
||||||
|
And:
|
||||||
|
```go
|
||||||
|
docker := cli.App("docker", "A self-sufficient runtime for linux containers")
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Spec = "[-d|--rm] IMAGE [COMMAND [ARG...]]"
|
||||||
|
:
|
||||||
|
:
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The spec syntax is mostly based on the conventions used in POSIX command line apps help messages and man pages:
|
||||||
|
|
||||||
|
### Options
|
||||||
|
|
||||||
|
You can use both short and long option names in spec strings:
|
||||||
|
```go
|
||||||
|
x.Spec="-f"
|
||||||
|
```
|
||||||
|
And:
|
||||||
|
```go
|
||||||
|
x.Spec="--force"
|
||||||
|
```
|
||||||
|
|
||||||
|
In both cases, we required that the f or force flag be set
|
||||||
|
|
||||||
|
Any option you reference in a spec string MUST be explicitly declared, otherwise mow.cli will panic:
|
||||||
|
```go
|
||||||
|
x.BoolOpt("f force", ...)
|
||||||
|
```
|
||||||
|
### Arguments
|
||||||
|
|
||||||
|
Arguments are all-uppercased words:
|
||||||
|
```go
|
||||||
|
x.Spec="SRC DST"
|
||||||
|
```
|
||||||
|
|
||||||
|
This spec string will force the user to pass exactly 2 arguments, SRC and DST
|
||||||
|
|
||||||
|
Any argument you reference in a spec string MUST be explicitly declared, otherwise mow.cli will panic:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.StringArg("SRC", ...)
|
||||||
|
x.StringArg("DST", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Ordering
|
||||||
|
|
||||||
|
Except for options, The order of the elements in a spec string is respected and enforced when parsing the command line arguments:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "-f -g SRC -h DST"
|
||||||
|
```
|
||||||
|
|
||||||
|
Consecutive options (`-f` and `-g` for example) get parsed regardless of the order they are specified in (both `-f=5 -g=6` and `-g=6 -f=5` are valid).
|
||||||
|
|
||||||
|
Order between options and arguments is significant (`-f` and `-g` must appear before the `SRC` argument).
|
||||||
|
|
||||||
|
Same goes for arguments, where `SRC` must appear before `DST`.
|
||||||
|
|
||||||
|
### Optionality
|
||||||
|
|
||||||
|
You can mark items as optional in a spec string by enclosing them in square brackets :`[...]`
|
||||||
|
```go
|
||||||
|
x.Spec = "[-x]"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Choice
|
||||||
|
|
||||||
|
You can use the `|` operator to indicate a choice between two or more items
|
||||||
|
```go
|
||||||
|
x.Spec = "--rm | --daemon"
|
||||||
|
x.Spec = "-H | -L | -P"
|
||||||
|
x.Spec = "-t | DST"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Repetition
|
||||||
|
|
||||||
|
You can use the `...` postfix operator to mark an element as repeatable:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec="SRC..."
|
||||||
|
x.Spec="-e..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Grouping
|
||||||
|
|
||||||
|
You can group items using parenthesis. This is useful in combination with the choice and repetition operators (`|` and `...`):
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "(-e COMMAND)... | (-x|-y)"
|
||||||
|
```
|
||||||
|
|
||||||
|
The parenthesis in the example above serve to mark that it is the sequence of a -e flag followed by an argument that is repeatable, and that
|
||||||
|
all that is mutually exclusive to a choice between -x and -y options.
|
||||||
|
|
||||||
|
### Option group
|
||||||
|
|
||||||
|
This is a shortcut to declare a choice between multiple options:
|
||||||
|
```go
|
||||||
|
x.Spec = "-abcd"
|
||||||
|
```
|
||||||
|
|
||||||
|
Is equivalent to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "(-a | -b | -c | -d)..."
|
||||||
|
```
|
||||||
|
|
||||||
|
I.e. any combination of the listed options in any order, with at least one option.
|
||||||
|
|
||||||
|
### All options
|
||||||
|
|
||||||
|
Another shortcut:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "[OPTIONS]"
|
||||||
|
```
|
||||||
|
|
||||||
|
This is a special syntax (the square brackets are not for marking an optional item, and the uppercased word is not for an argument).
|
||||||
|
This is equivalent to a repeatable choice between all the available options.
|
||||||
|
For example, if an app or a command declares 4 options a, b, c and d, `[OPTIONS]` is equivalent to:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "[-a | -b | -c | -d]..."
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inline option values
|
||||||
|
|
||||||
|
You can use the `=<some-text>` notation right after an option (long or short form) to give an inline description or value.
|
||||||
|
|
||||||
|
An example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "[ -a=<absolute-path> | --timeout=<in seconds> ] ARG"
|
||||||
|
```
|
||||||
|
|
||||||
|
The inline values are ignored by the spec parser and are just there for the final user as a contextual hint.
|
||||||
|
|
||||||
|
### Operators
|
||||||
|
|
||||||
|
The `--` operator can be used in a spec string to automatically treat everything following it as an options.
|
||||||
|
|
||||||
|
In other words, placing a `--` in the spec string automatically inserts a `--` in the same position in the program call arguments.
|
||||||
|
|
||||||
|
This lets you write programs like the `time` utility for example:
|
||||||
|
|
||||||
|
```go
|
||||||
|
x.Spec = "time -lp [-- CMD [ARG...]]"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Grammar
|
||||||
|
|
||||||
|
Here's the (simplified) EBNF grammar for the Specs language:
|
||||||
|
|
||||||
|
```
|
||||||
|
spec -> sequence
|
||||||
|
sequence -> choice*
|
||||||
|
req_sequence -> choice+
|
||||||
|
choice -> atom ('|' atom)*
|
||||||
|
atom -> (shortOpt | longOpt | optSeq | allOpts | group | optional) rep? | optEnd
|
||||||
|
shortOp -> '-' [A-Za-z]
|
||||||
|
longOpt -> '--' [A-Za-z][A-Za-z0-9]*
|
||||||
|
optSeq -> '-' [A-Za-z]+
|
||||||
|
allOpts -> '[OPTIONS]'
|
||||||
|
group -> '(' req_sequence ')'
|
||||||
|
optional -> '[' req_sequence ']'
|
||||||
|
rep -> '...'
|
||||||
|
optEnd -> '--'
|
||||||
|
```
|
||||||
|
And that's it for the spec language.
|
||||||
|
You can combine these few building blocks in any way you want (while respecting the grammar above) to construct sophisticated validation constraints
|
||||||
|
(don't go too wild though).
|
||||||
|
|
||||||
|
Behind the scenes, mow.cli parses the spec string and constructs a finite state machine to be used to parse the command line arguments.
|
||||||
|
mow.cli also handles backtracking, and so it can handle tricky cases, or what I like to call "the cp test"
|
||||||
|
```
|
||||||
|
cp SRC... DST
|
||||||
|
```
|
||||||
|
|
||||||
|
Without backtracking, this deceptively simple spec string cannot be parsed correctly.
|
||||||
|
For instance, docopt can't handle this case, whereas mow.cli does.
|
||||||
|
|
||||||
|
## Default spec
|
||||||
|
|
||||||
|
By default, and unless a spec string is set by the user, mow.cli auto-generates one for the app and every command using this logic:
|
||||||
|
|
||||||
|
* Start with an empty spec string
|
||||||
|
* If at least one option was declared, append `[OPTIONS]` to the spec string
|
||||||
|
* For every declared argument, append it, in the order of declaration, to the spec string
|
||||||
|
|
||||||
|
For example, given this command declaration:
|
||||||
|
|
||||||
|
```go
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
var (
|
||||||
|
detached = cmd.BoolOpt("d detach", false, "Detached mode: run the container in the background and print the new container ID", nil)
|
||||||
|
memory = cmd.StringOpt("m memory", "", "Memory limit (format: <number><optional unit>, where unit = b, k, m or g)", nil)
|
||||||
|
image = cmd.StringArg("IMAGE", "", "", nil)
|
||||||
|
args = cmd.StringsArg("ARG", "", "", nil)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
The auto-generated spec string would be:
|
||||||
|
```go
|
||||||
|
[OPTIONS] IMAGE ARG
|
||||||
|
```
|
||||||
|
|
||||||
|
Which should suffice for simple cases. If not, the spec string has to be set explicitly.
|
||||||
|
|
||||||
|
## Exiting
|
||||||
|
|
||||||
|
`mow.cli` provides the `Exit` function which accepts an exit code and exits the app with the provided code.
|
||||||
|
|
||||||
|
You are highly encouraged to call `cli.Exit` instead of `os.Exit` for the `After` interceptors to be executed.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
This work is published under the MIT license.
|
||||||
|
|
||||||
|
Please see the `LICENSE` file for details.
|
|
@ -0,0 +1,224 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoolArg describes a boolean argument
|
||||||
|
type BoolArg struct {
|
||||||
|
// The argument name as will be shown in help messages
|
||||||
|
Name string
|
||||||
|
// The argument description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this argument
|
||||||
|
EnvVar string
|
||||||
|
// The argument's inital value
|
||||||
|
Value bool
|
||||||
|
// A boolean to display or not the current value of the argument in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a BoolArg) value() bool {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringArg describes a string argument
|
||||||
|
type StringArg struct {
|
||||||
|
// The argument name as will be shown in help messages
|
||||||
|
Name string
|
||||||
|
// The argument description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this argument
|
||||||
|
EnvVar string
|
||||||
|
// The argument's initial value
|
||||||
|
Value string
|
||||||
|
// A boolean to display or not the current value of the argument in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a StringArg) value() string {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntArg describes an int argument
|
||||||
|
type IntArg struct {
|
||||||
|
// The argument name as will be shown in help messages
|
||||||
|
Name string
|
||||||
|
// The argument description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this argument
|
||||||
|
EnvVar string
|
||||||
|
// The argument's initial value
|
||||||
|
Value int
|
||||||
|
// A boolean to display or not the current value of the argument in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a IntArg) value() int {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringsArg describes a string slice argument
|
||||||
|
type StringsArg struct {
|
||||||
|
// The argument name as will be shown in help messages
|
||||||
|
Name string
|
||||||
|
// The argument description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this argument.
|
||||||
|
// The env variable should contain a comma separated list of values
|
||||||
|
EnvVar string
|
||||||
|
// The argument's initial value
|
||||||
|
Value []string
|
||||||
|
// A boolean to display or not the current value of the argument in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a StringsArg) value() []string {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntsArg describes an int slice argument
|
||||||
|
type IntsArg struct {
|
||||||
|
// The argument name as will be shown in help messages
|
||||||
|
Name string
|
||||||
|
// The argument description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this argument.
|
||||||
|
// The env variable should contain a comma separated list of values
|
||||||
|
EnvVar string
|
||||||
|
// The argument's initial value
|
||||||
|
Value []int
|
||||||
|
// A boolean to display or not the current value of the argument in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a IntsArg) value() []int {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarArg describes an argument where the type and format of the value is controlled by the developer
|
||||||
|
type VarArg struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option
|
||||||
|
EnvVar string
|
||||||
|
// A value implementing the flag.Value type (will hold the final value)
|
||||||
|
Value flag.Value
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this arg was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a VarArg) value() flag.Value {
|
||||||
|
return a.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BoolArg defines a boolean argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a bool) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) BoolArg(name string, value bool, desc string) *bool {
|
||||||
|
return c.Bool(BoolArg{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringArg defines a string argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) StringArg(name string, value string, desc string) *string {
|
||||||
|
return c.String(StringArg{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntArg defines an int argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) IntArg(name string, value int, desc string) *int {
|
||||||
|
return c.Int(IntArg{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringsArg defines a string slice argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) StringsArg(name string, value []string, desc string) *[]string {
|
||||||
|
return c.Strings(StringsArg{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntsArg defines an int slice argument on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) IntsArg(name string, value []int, desc string) *[]int {
|
||||||
|
return c.Ints(IntsArg{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
VarArg defines an argument where the type and format is controlled by the developer on the command c named `name` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The result will be stored in the value parameter (a value implementing the flag.Value interface) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) VarArg(name string, value flag.Value, desc string) {
|
||||||
|
c.mkArg(arg{name: name, desc: desc, value: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
type arg struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
envVar string
|
||||||
|
hideValue bool
|
||||||
|
valueSetFromEnv bool
|
||||||
|
valueSetByUser *bool
|
||||||
|
value flag.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *arg) String() string {
|
||||||
|
return fmt.Sprintf("ARG(%s)", a.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) mkArg(arg arg) {
|
||||||
|
arg.valueSetFromEnv = setFromEnv(arg.value, arg.envVar)
|
||||||
|
|
||||||
|
c.args = append(c.args, &arg)
|
||||||
|
c.argsIdx[arg.name] = &arg
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringArg(t *testing.T) {
|
||||||
|
cmd := &Cmd{argsIdx: map[string]*arg{}}
|
||||||
|
a := cmd.String(StringArg{Name: "a", Value: "test"})
|
||||||
|
require.Equal(t, "test", *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.String(StringArg{Name: "b", Value: "test", EnvVar: "B"})
|
||||||
|
require.Equal(t, "test", *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow")
|
||||||
|
b = cmd.String(StringArg{Name: "b", Value: "test", EnvVar: "B"})
|
||||||
|
require.Equal(t, "mow", *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "cli")
|
||||||
|
os.Setenv("D", "mow")
|
||||||
|
b = cmd.String(StringArg{Name: "b", Value: "test", EnvVar: "B C D"})
|
||||||
|
require.Equal(t, "cli", *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolArg(t *testing.T) {
|
||||||
|
cmd := &Cmd{argsIdx: map[string]*arg{}}
|
||||||
|
a := cmd.Bool(BoolArg{Name: "a", Value: true, Desc: ""})
|
||||||
|
require.True(t, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Bool(BoolArg{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.False(t, *b)
|
||||||
|
|
||||||
|
trueValues := []string{"1", "true", "TRUE"}
|
||||||
|
for _, tv := range trueValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b = cmd.Bool(BoolArg{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.True(t, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
falseValues := []string{"0", "false", "FALSE", "xyz"}
|
||||||
|
for _, tv := range falseValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b = cmd.Bool(BoolArg{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.False(t, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "false")
|
||||||
|
os.Setenv("D", "true")
|
||||||
|
b = cmd.Bool(BoolArg{Name: "b", Value: true, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.False(t, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntArg(t *testing.T) {
|
||||||
|
cmd := &Cmd{argsIdx: map[string]*arg{}}
|
||||||
|
a := cmd.Int(IntArg{Name: "a", Value: -1, Desc: ""})
|
||||||
|
require.Equal(t, -1, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Int(IntArg{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, -1, *b)
|
||||||
|
|
||||||
|
goodValues := []int{1, 0, 33}
|
||||||
|
for _, tv := range goodValues {
|
||||||
|
os.Setenv("B", strconv.Itoa(tv))
|
||||||
|
b := cmd.Int(IntArg{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, tv, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
badValues := []string{"", "b", "q1", "_"}
|
||||||
|
for _, tv := range badValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b := cmd.Int(IntArg{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, -1, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "42")
|
||||||
|
os.Setenv("D", "666")
|
||||||
|
b = cmd.Int(IntArg{Name: "b", Value: -1, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.Equal(t, 42, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsArg(t *testing.T) {
|
||||||
|
cmd := &Cmd{argsIdx: map[string]*arg{}}
|
||||||
|
v := []string{"test"}
|
||||||
|
a := cmd.Strings(StringsArg{Name: "a", Value: v, Desc: ""})
|
||||||
|
require.Equal(t, v, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Strings(StringsArg{Name: "b", Value: v, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, v, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow")
|
||||||
|
b = cmd.Strings(StringsArg{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []string{"mow"}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow, cli")
|
||||||
|
b = cmd.Strings(StringsArg{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []string{"mow", "cli"}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "test")
|
||||||
|
os.Setenv("D", "xxx")
|
||||||
|
b = cmd.Strings(StringsArg{Name: "b", Value: nil, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.Equal(t, v, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntsArg(t *testing.T) {
|
||||||
|
cmd := &Cmd{argsIdx: map[string]*arg{}}
|
||||||
|
|
||||||
|
vi := []int{42}
|
||||||
|
a := cmd.Ints(IntsArg{Name: "a", Value: vi, Desc: ""})
|
||||||
|
require.Equal(t, vi, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Ints(IntsArg{Name: "b", Value: vi, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, vi, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "666")
|
||||||
|
b = cmd.Ints(IntsArg{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []int{666}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "1, 2 , 3")
|
||||||
|
b = cmd.Ints(IntsArg{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []int{1, 2, 3}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "abc")
|
||||||
|
os.Setenv("D", "1, abc")
|
||||||
|
os.Setenv("E", "42")
|
||||||
|
os.Setenv("F", "666")
|
||||||
|
b = cmd.Ints(IntsArg{Name: "b", Value: nil, EnvVar: "B C D E F", Desc: ""})
|
||||||
|
require.Equal(t, vi, *b)
|
||||||
|
}
|
|
@ -0,0 +1,133 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cli represents the structure of a CLI app. It should be constructed using the App() function
|
||||||
|
*/
|
||||||
|
type Cli struct {
|
||||||
|
*Cmd
|
||||||
|
version *cliVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
type cliVersion struct {
|
||||||
|
version string
|
||||||
|
option *opt
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
App creates a new and empty CLI app configured with the passed name and description.
|
||||||
|
|
||||||
|
name and description will be used to construct the help message for the app:
|
||||||
|
|
||||||
|
Usage: $name [OPTIONS] COMMAND [arg...]
|
||||||
|
|
||||||
|
$desc
|
||||||
|
|
||||||
|
*/
|
||||||
|
func App(name, desc string) *Cli {
|
||||||
|
return &Cli{
|
||||||
|
Cmd: &Cmd{
|
||||||
|
name: name,
|
||||||
|
desc: desc,
|
||||||
|
optionsIdx: map[string]*opt{},
|
||||||
|
argsIdx: map[string]*arg{},
|
||||||
|
ErrorHandling: flag.ExitOnError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Version sets the version string of the CLI app together with the options that can be used to trigger
|
||||||
|
printing the version string via the CLI.
|
||||||
|
|
||||||
|
Usage: appName --$name
|
||||||
|
$version
|
||||||
|
|
||||||
|
*/
|
||||||
|
func (cli *Cli) Version(name, version string) {
|
||||||
|
cli.Bool(BoolOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: false,
|
||||||
|
Desc: "Show the version and exit",
|
||||||
|
HideValue: true,
|
||||||
|
})
|
||||||
|
names := mkOptStrs(name)
|
||||||
|
option := cli.optionsIdx[names[0]]
|
||||||
|
cli.version = &cliVersion{version, option}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *Cli) parse(args []string, entry, inFlow, outFlow *step) error {
|
||||||
|
// We overload Cmd.parse() and handle cases that only apply to the CLI command, like versioning
|
||||||
|
// After that, we just call Cmd.parse() for the default behavior
|
||||||
|
if cli.versionSetAndRequested(args) {
|
||||||
|
cli.PrintVersion()
|
||||||
|
cli.onError(errVersionRequested)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return cli.Cmd.parse(args, entry, inFlow, outFlow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cli *Cli) versionSetAndRequested(args []string) bool {
|
||||||
|
return cli.version != nil && cli.isFlagSet(args, cli.version.option.names)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PrintVersion prints the CLI app's version.
|
||||||
|
In most cases the library users won't need to call this method, unless
|
||||||
|
a more complex validation is needed.
|
||||||
|
*/
|
||||||
|
func (cli *Cli) PrintVersion() {
|
||||||
|
fmt.Fprintln(stdErr, cli.version.version)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Run uses the app configuration (specs, commands, ...) to parse the args slice
|
||||||
|
and to execute the matching command.
|
||||||
|
|
||||||
|
In case of an incorrect usage, and depending on the configured ErrorHandling policy,
|
||||||
|
it may return an error, panic or exit
|
||||||
|
*/
|
||||||
|
func (cli *Cli) Run(args []string) error {
|
||||||
|
if err := cli.doInit(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
inFlow := &step{desc: "RootIn"}
|
||||||
|
outFlow := &step{desc: "RootOut"}
|
||||||
|
return cli.parse(args[1:], inFlow, inFlow, outFlow)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
ActionCommand is a convenience function to configure a command with an action.
|
||||||
|
|
||||||
|
cmd.ActionCommand(_, _, myFun } is equivalent to cmd.Command(_, _, func(cmd *cli.Cmd) { cmd.Action = myFun })
|
||||||
|
*/
|
||||||
|
func ActionCommand(action func()) CmdInitializer {
|
||||||
|
return func(cmd *Cmd) {
|
||||||
|
cmd.Action = action
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Exit causes the app the exit with the specified exit code while giving the After interceptors a chance to run.
|
||||||
|
This should be used instead of os.Exit.
|
||||||
|
*/
|
||||||
|
func Exit(code int) {
|
||||||
|
panic(exit(code))
|
||||||
|
}
|
||||||
|
|
||||||
|
type exit int
|
||||||
|
|
||||||
|
var exiter = func(code int) {
|
||||||
|
os.Exit(code)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
stdOut io.Writer = os.Stdout
|
||||||
|
stdErr io.Writer = os.Stderr
|
||||||
|
)
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,545 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"text/tabwriter"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Cmd represents a command (or sub command) in a CLI application. It should be constructed
|
||||||
|
by calling Command() on an app to create a top level command or by calling Command() on another
|
||||||
|
command to create a sub command
|
||||||
|
*/
|
||||||
|
type Cmd struct {
|
||||||
|
// The code to execute when this command is matched
|
||||||
|
Action func()
|
||||||
|
// The code to execute before this command or any of its children is matched
|
||||||
|
Before func()
|
||||||
|
// The code to execute after this command or any of its children is matched
|
||||||
|
After func()
|
||||||
|
// The command options and arguments
|
||||||
|
Spec string
|
||||||
|
// The command long description to be shown when help is requested
|
||||||
|
LongDesc string
|
||||||
|
// The command error handling strategy
|
||||||
|
ErrorHandling flag.ErrorHandling
|
||||||
|
|
||||||
|
init CmdInitializer
|
||||||
|
name string
|
||||||
|
aliases []string
|
||||||
|
desc string
|
||||||
|
|
||||||
|
commands []*Cmd
|
||||||
|
options []*opt
|
||||||
|
optionsIdx map[string]*opt
|
||||||
|
args []*arg
|
||||||
|
argsIdx map[string]*arg
|
||||||
|
|
||||||
|
parents []string
|
||||||
|
|
||||||
|
fsm *state
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BoolParam represents a Bool option or argument
|
||||||
|
*/
|
||||||
|
type BoolParam interface {
|
||||||
|
value() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringParam represents a String option or argument
|
||||||
|
*/
|
||||||
|
type StringParam interface {
|
||||||
|
value() string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntParam represents an Int option or argument
|
||||||
|
*/
|
||||||
|
type IntParam interface {
|
||||||
|
value() int
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringsParam represents a string slice option or argument
|
||||||
|
*/
|
||||||
|
type StringsParam interface {
|
||||||
|
value() []string
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntsParam represents an int slice option or argument
|
||||||
|
*/
|
||||||
|
type IntsParam interface {
|
||||||
|
value() []int
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
VarParam represents an custom option or argument where the type and format are controlled by the developer
|
||||||
|
*/
|
||||||
|
type VarParam interface {
|
||||||
|
value() flag.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
CmdInitializer is a function that configures a command by adding options, arguments, a spec, sub commands and the code
|
||||||
|
to execute when the command is called
|
||||||
|
*/
|
||||||
|
type CmdInitializer func(*Cmd)
|
||||||
|
|
||||||
|
/*
|
||||||
|
Command adds a new (sub) command to c where name is the command name (what you type in the console),
|
||||||
|
description is what would be shown in the help messages, e.g.:
|
||||||
|
|
||||||
|
Usage: git [OPTIONS] COMMAND [arg...]
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
$name $desc
|
||||||
|
|
||||||
|
the last argument, init, is a function that will be called by mow.cli to further configure the created
|
||||||
|
(sub) command, e.g. to add options, arguments and the code to execute
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Command(name, desc string, init CmdInitializer) {
|
||||||
|
aliases := strings.Fields(name)
|
||||||
|
c.commands = append(c.commands, &Cmd{
|
||||||
|
ErrorHandling: c.ErrorHandling,
|
||||||
|
name: aliases[0],
|
||||||
|
aliases: aliases,
|
||||||
|
desc: desc,
|
||||||
|
init: init,
|
||||||
|
commands: []*Cmd{},
|
||||||
|
options: []*opt{},
|
||||||
|
optionsIdx: map[string]*opt{},
|
||||||
|
args: []*arg{},
|
||||||
|
argsIdx: map[string]*arg{},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Bool can be used to add a bool option or argument to a command.
|
||||||
|
It accepts either a BoolOpt or a BoolArg struct.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a bool) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Bool(p BoolParam) *bool {
|
||||||
|
into := new(bool)
|
||||||
|
value := newBoolValue(into, p.value())
|
||||||
|
|
||||||
|
switch x := p.(type) {
|
||||||
|
case BoolOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
case BoolArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
String can be used to add a string option or argument to a command.
|
||||||
|
It accepts either a StringOpt or a StringArg struct.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) String(p StringParam) *string {
|
||||||
|
into := new(string)
|
||||||
|
value := newStringValue(into, p.value())
|
||||||
|
|
||||||
|
switch x := p.(type) {
|
||||||
|
case StringOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
case StringArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Int can be used to add an int option or argument to a command.
|
||||||
|
It accepts either a IntOpt or a IntArg struct.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Int(p IntParam) *int {
|
||||||
|
into := new(int)
|
||||||
|
value := newIntValue(into, p.value())
|
||||||
|
|
||||||
|
switch x := p.(type) {
|
||||||
|
case IntOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
case IntArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Strings can be used to add a string slice option or argument to a command.
|
||||||
|
It accepts either a StringsOpt or a StringsArg struct.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Strings(p StringsParam) *[]string {
|
||||||
|
into := new([]string)
|
||||||
|
value := newStringsValue(into, p.value())
|
||||||
|
|
||||||
|
switch x := p.(type) {
|
||||||
|
case StringsOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
case StringsArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Ints can be used to add an int slice option or argument to a command.
|
||||||
|
It accepts either a IntsOpt or a IntsArg struct.
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Ints(p IntsParam) *[]int {
|
||||||
|
into := new([]int)
|
||||||
|
value := newIntsValue(into, p.value())
|
||||||
|
|
||||||
|
switch x := p.(type) {
|
||||||
|
case IntsOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
case IntsArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: value, valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return into
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Var can be used to add a custom option or argument to a command.
|
||||||
|
It accepts either a VarOpt or a VarArg struct.
|
||||||
|
|
||||||
|
As opposed to the other built-in types, this function does not return a pointer the the value.
|
||||||
|
Instead, the VarOpt or VarOptArg structs hold the said value.
|
||||||
|
*/
|
||||||
|
func (c *Cmd) Var(p VarParam) {
|
||||||
|
switch x := p.(type) {
|
||||||
|
case VarOpt:
|
||||||
|
c.mkOpt(opt{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: p.value(), valueSetByUser: x.SetByUser})
|
||||||
|
case VarArg:
|
||||||
|
c.mkArg(arg{name: x.Name, desc: x.Desc, envVar: x.EnvVar, hideValue: x.HideValue, value: p.value(), valueSetByUser: x.SetByUser})
|
||||||
|
default:
|
||||||
|
panic(fmt.Sprintf("Unhandled param %v", p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) doInit() error {
|
||||||
|
if c.init != nil {
|
||||||
|
c.init(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
parents := append(c.parents, c.name)
|
||||||
|
|
||||||
|
for _, sub := range c.commands {
|
||||||
|
sub.parents = parents
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Spec) == 0 {
|
||||||
|
if len(c.options) > 0 {
|
||||||
|
c.Spec = "[OPTIONS] "
|
||||||
|
}
|
||||||
|
for _, arg := range c.args {
|
||||||
|
c.Spec += arg.name + " "
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fsm, err := uParse(c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c.fsm = fsm
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) onError(err error) {
|
||||||
|
if err == errHelpRequested || err == errVersionRequested {
|
||||||
|
if c.ErrorHandling == flag.ExitOnError {
|
||||||
|
exiter(0)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch c.ErrorHandling {
|
||||||
|
case flag.ExitOnError:
|
||||||
|
exiter(2)
|
||||||
|
case flag.PanicOnError:
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PrintHelp prints the command's help message.
|
||||||
|
In most cases the library users won't need to call this method, unless
|
||||||
|
a more complex validation is needed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) PrintHelp() {
|
||||||
|
c.printHelp(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
PrintLongHelp prints the command's help message using the command long description if specified.
|
||||||
|
In most cases the library users won't need to call this method, unless
|
||||||
|
a more complex validation is needed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) PrintLongHelp() {
|
||||||
|
c.printHelp(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) printHelp(longDesc bool) {
|
||||||
|
full := append(c.parents, c.name)
|
||||||
|
path := strings.Join(full, " ")
|
||||||
|
fmt.Fprintf(stdErr, "\nUsage: %s", path)
|
||||||
|
|
||||||
|
spec := strings.TrimSpace(c.Spec)
|
||||||
|
if len(spec) > 0 {
|
||||||
|
fmt.Fprintf(stdErr, " %s", spec)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.commands) > 0 {
|
||||||
|
fmt.Fprint(stdErr, " COMMAND [arg...]")
|
||||||
|
}
|
||||||
|
fmt.Fprint(stdErr, "\n\n")
|
||||||
|
|
||||||
|
desc := c.desc
|
||||||
|
if longDesc && len(c.LongDesc) > 0 {
|
||||||
|
desc = c.LongDesc
|
||||||
|
}
|
||||||
|
if len(desc) > 0 {
|
||||||
|
fmt.Fprintf(stdErr, "%s\n", desc)
|
||||||
|
}
|
||||||
|
|
||||||
|
w := tabwriter.NewWriter(stdErr, 15, 1, 3, ' ', 0)
|
||||||
|
|
||||||
|
if len(c.args) > 0 {
|
||||||
|
fmt.Fprint(w, "\t\nArguments:\t\n")
|
||||||
|
|
||||||
|
for _, arg := range c.args {
|
||||||
|
var (
|
||||||
|
env = formatEnvVarsForHelp(arg.envVar)
|
||||||
|
value = formatValueForHelp(arg.hideValue, arg.value)
|
||||||
|
)
|
||||||
|
fmt.Fprintf(w, " %s\t%s\n", arg.name, joinStrings(arg.desc, env, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.options) > 0 {
|
||||||
|
fmt.Fprint(w, "\t\nOptions:\t\n")
|
||||||
|
|
||||||
|
for _, opt := range c.options {
|
||||||
|
var (
|
||||||
|
optNames = formatOptNamesForHelp(opt)
|
||||||
|
env = formatEnvVarsForHelp(opt.envVar)
|
||||||
|
value = formatValueForHelp(opt.hideValue, opt.value)
|
||||||
|
)
|
||||||
|
fmt.Fprintf(w, " %s\t%s\n", optNames, joinStrings(opt.desc, env, value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.commands) > 0 {
|
||||||
|
fmt.Fprint(w, "\t\nCommands:\t\n")
|
||||||
|
|
||||||
|
for _, c := range c.commands {
|
||||||
|
fmt.Fprintf(w, " %s\t%s\n", strings.Join(c.aliases, ", "), c.desc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.commands) > 0 {
|
||||||
|
fmt.Fprintf(w, "\t\nRun '%s COMMAND --help' for more information on a command.\n", path)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatOptNamesForHelp(o *opt) string {
|
||||||
|
short, long := "", ""
|
||||||
|
|
||||||
|
for _, n := range o.names {
|
||||||
|
if len(n) == 2 && short == "" {
|
||||||
|
short = n
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(n) > 2 && long == "" {
|
||||||
|
long = n
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case short != "" && long != "":
|
||||||
|
return fmt.Sprintf("%s, %s", short, long)
|
||||||
|
case short != "":
|
||||||
|
return fmt.Sprintf("%s", short)
|
||||||
|
case long != "":
|
||||||
|
// 2 spaces instead of the short option (-x), one space for the comma (,) and one space for the after comma blank
|
||||||
|
return fmt.Sprintf(" %s", long)
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatValueForHelp(hide bool, v flag.Value) string {
|
||||||
|
if hide {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if dv, ok := v.(defaultValued); ok {
|
||||||
|
if dv.IsDefault() {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("(default %s)", v.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatEnvVarsForHelp(envVars string) string {
|
||||||
|
if strings.TrimSpace(envVars) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
vars := strings.Fields(envVars)
|
||||||
|
res := "(env"
|
||||||
|
sep := " "
|
||||||
|
for i, v := range vars {
|
||||||
|
if i > 0 {
|
||||||
|
sep = ", "
|
||||||
|
}
|
||||||
|
res += fmt.Sprintf("%s$%s", sep, v)
|
||||||
|
}
|
||||||
|
res += ")"
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) parse(args []string, entry, inFlow, outFlow *step) error {
|
||||||
|
if c.helpRequested(args) {
|
||||||
|
c.PrintLongHelp()
|
||||||
|
c.onError(errHelpRequested)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
nargsLen := c.getOptsAndArgs(args)
|
||||||
|
|
||||||
|
if err := c.fsm.parse(args[:nargsLen]); err != nil {
|
||||||
|
fmt.Fprintf(stdErr, "Error: %s\n", err.Error())
|
||||||
|
c.PrintHelp()
|
||||||
|
c.onError(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newInFlow := &step{
|
||||||
|
do: c.Before,
|
||||||
|
error: outFlow,
|
||||||
|
desc: fmt.Sprintf("%s.Before", c.name),
|
||||||
|
}
|
||||||
|
inFlow.success = newInFlow
|
||||||
|
|
||||||
|
newOutFlow := &step{
|
||||||
|
do: c.After,
|
||||||
|
success: outFlow,
|
||||||
|
error: outFlow,
|
||||||
|
desc: fmt.Sprintf("%s.After", c.name),
|
||||||
|
}
|
||||||
|
|
||||||
|
args = args[nargsLen:]
|
||||||
|
if len(args) == 0 {
|
||||||
|
if c.Action != nil {
|
||||||
|
newInFlow.success = &step{
|
||||||
|
do: c.Action,
|
||||||
|
success: newOutFlow,
|
||||||
|
error: newOutFlow,
|
||||||
|
desc: fmt.Sprintf("%s.Action", c.name),
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.run(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c.PrintHelp()
|
||||||
|
c.onError(nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := args[0]
|
||||||
|
for _, sub := range c.commands {
|
||||||
|
if sub.isAlias(arg) {
|
||||||
|
if err := sub.doInit(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return sub.parse(args[1:], entry, newInFlow, newOutFlow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
switch {
|
||||||
|
case strings.HasPrefix(arg, "-"):
|
||||||
|
err = fmt.Errorf("Error: illegal option %s", arg)
|
||||||
|
fmt.Fprintln(stdErr, err.Error())
|
||||||
|
default:
|
||||||
|
err = fmt.Errorf("Error: illegal input %s", arg)
|
||||||
|
fmt.Fprintln(stdErr, err.Error())
|
||||||
|
}
|
||||||
|
c.PrintHelp()
|
||||||
|
c.onError(err)
|
||||||
|
return err
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) helpRequested(args []string) bool {
|
||||||
|
return c.isFlagSet(args, []string{"-h", "--help"})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) isFlagSet(args []string, searchArgs []string) bool {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
arg := args[0]
|
||||||
|
for _, searchArg := range searchArgs {
|
||||||
|
if arg == searchArg {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) getOptsAndArgs(args []string) int {
|
||||||
|
consumed := 0
|
||||||
|
|
||||||
|
for _, arg := range args {
|
||||||
|
for _, sub := range c.commands {
|
||||||
|
if sub.isAlias(arg) {
|
||||||
|
return consumed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
consumed++
|
||||||
|
}
|
||||||
|
return consumed
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) isAlias(arg string) bool {
|
||||||
|
for _, alias := range c.aliases {
|
||||||
|
if arg == alias {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,518 @@
|
||||||
|
/*
|
||||||
|
Package cli provides a framework to build command line applications in Go with most of the burden of arguments parsing and validation
|
||||||
|
placed on the framework instead of the user.
|
||||||
|
|
||||||
|
|
||||||
|
Basics
|
||||||
|
|
||||||
|
You start by creating an application by passing a name and a description:
|
||||||
|
|
||||||
|
cp = cli.App("cp", "Copy files around")
|
||||||
|
|
||||||
|
To attach the code to execute when the app is launched, assign a function to the Action field:
|
||||||
|
cp.Action = func() {
|
||||||
|
fmt.Printf("Hello world\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
Finally, in your main func, call Run on the app:
|
||||||
|
|
||||||
|
cp.Run(os.Args)
|
||||||
|
|
||||||
|
Options
|
||||||
|
|
||||||
|
To add a (global) option, call one of the (String[s]|Int[s]|Bool)Opt methods on the app:
|
||||||
|
|
||||||
|
recursive := cp.BoolOpt("R recursive", false, "recursively copy the src to dst")
|
||||||
|
|
||||||
|
* The first argument is a space separated list of names for the option without the dashes
|
||||||
|
|
||||||
|
* The second parameter is the default value for the option
|
||||||
|
|
||||||
|
* The third parameter is the option description, as will be shown in the help messages
|
||||||
|
|
||||||
|
There is also a second set of methods Bool, String, Int, Strings and Ints, which accepts a struct describing the option:
|
||||||
|
|
||||||
|
recursive = cp.Bool(BoolOpt{
|
||||||
|
Name: "R",
|
||||||
|
Value: false,
|
||||||
|
Desc: "copy src files recursively",
|
||||||
|
EnvVar: "",
|
||||||
|
})
|
||||||
|
|
||||||
|
There EnvVar field is a space separated list of environment variables names to be used to initialize the option.
|
||||||
|
|
||||||
|
The result is a pointer to a value which will be populated after parsing the command line arguments.
|
||||||
|
You can access the values in the Action func.
|
||||||
|
|
||||||
|
In the command line, mow.cli accepts the following syntaxes
|
||||||
|
|
||||||
|
* For boolean options:
|
||||||
|
|
||||||
|
-f : a single dash for the one letter names
|
||||||
|
-f=false : a single dash for the one letter names, equal sign followed by true or false
|
||||||
|
--force : double dash for longer option names
|
||||||
|
-it : mow.cli supports option folding, this is equivalent to: -i -t
|
||||||
|
|
||||||
|
* For string, int options:
|
||||||
|
|
||||||
|
-e=value : single dash for one letter names, equal sign followed by the value
|
||||||
|
-e value : single dash for one letter names, space followed by the value
|
||||||
|
-Ivalue : single dash for one letter names immediately followed by the value
|
||||||
|
--extra=value : double dash for longer option names, equal sign followed by the value
|
||||||
|
--extra value : double dash for longer option names, space followed by the value
|
||||||
|
|
||||||
|
* For slice options (StringsOpt, IntsOpt): repeat the option to accumulate the values in the resulting slice:
|
||||||
|
|
||||||
|
-e PATH:/bin -e PATH:/usr/bin : resulting slice contains ["/bin", "/usr/bin"]
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
|
||||||
|
To accept arguments, you need to explicitly declare them by calling one of the (String[s]|Int[s]|Bool)Arg methods on the app:
|
||||||
|
|
||||||
|
src := cp.StringArg("SRC", "", "the file to copy")
|
||||||
|
dst := cp.StringArg("DST", "", "the destination")
|
||||||
|
|
||||||
|
* The first argument is the argument name as will be shown in the help messages
|
||||||
|
|
||||||
|
* The second parameter is the default value for the argument
|
||||||
|
|
||||||
|
* The third parameter is the argument description, as will be shown in the help messages
|
||||||
|
|
||||||
|
There is also a second set of methods Bool, String, Int, Strings and Ints, which accepts structs describing the argument:
|
||||||
|
|
||||||
|
src = cp.Strings(StringsArg{
|
||||||
|
Name: "SRC",
|
||||||
|
Desc: "The source files to copy",
|
||||||
|
Value: "",
|
||||||
|
EnvVar: "",
|
||||||
|
SetByUser: &srcSetByUser,
|
||||||
|
})
|
||||||
|
|
||||||
|
The Value field is where you can set the initial value for the argument.
|
||||||
|
|
||||||
|
EnvVar accepts a space separated list of environment variables names to be used to initialize the argument.
|
||||||
|
|
||||||
|
If SetByUser is specified (by passing a pointer to a bool variable), it will be set to true only if the user explicitly sets the argument.
|
||||||
|
|
||||||
|
|
||||||
|
The result is a pointer to a value that will be populated after parsing the command line arguments.
|
||||||
|
You can access the values in the Action func.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Operators
|
||||||
|
|
||||||
|
The -- operator marks the end of options.
|
||||||
|
Everything that follow will be treated as an argument,
|
||||||
|
even if starts with a dash.
|
||||||
|
|
||||||
|
For example, given the touch command which takes a filename as an argument (and possibly other options):
|
||||||
|
|
||||||
|
|
||||||
|
file := cp.StringArg("FILE", "", "the file to create")
|
||||||
|
|
||||||
|
|
||||||
|
If we try to create a file named -f this way:
|
||||||
|
|
||||||
|
|
||||||
|
touch -f
|
||||||
|
|
||||||
|
Would fail, because -f will be parsed as an option not as an argument.
|
||||||
|
The fix is to prefix the filename with the -- operator:
|
||||||
|
|
||||||
|
|
||||||
|
touch -- -f
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
Commands
|
||||||
|
|
||||||
|
mow.cli supports nesting commands and sub commands.
|
||||||
|
Declare a top level command by calling the Command func on the app struct, and a sub command by calling
|
||||||
|
the Command func on the command struct:
|
||||||
|
|
||||||
|
docker := cli.App("docker", "A self-sufficient runtime for linux containers")
|
||||||
|
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
// initialize the run command here
|
||||||
|
})
|
||||||
|
|
||||||
|
* The first argument is the command name, as will be shown in the help messages and as will need to be input by the user in the command line to call the command
|
||||||
|
|
||||||
|
* The second argument is the command description as will be shown in the help messages
|
||||||
|
|
||||||
|
* The third argument is a CmdInitializer, a function that receives a pointer to a Cmd struct representing the command.
|
||||||
|
In this function, you can add options and arguments by calling the same methods as you would with an app struct (BoolOpt, StringArg, ...).
|
||||||
|
You would also assign a function to the Action field of the Cmd struct for it to be executed when the command is invoked.
|
||||||
|
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
detached := cmd.BoolOpt("d detach", false, "Detached mode: run the container in the background and print the new container ID")
|
||||||
|
memory := cmd.StringOpt("m memory", "", "Memory limit (format: <number><optional unit>, where unit = b, k, m or g)")
|
||||||
|
|
||||||
|
image := cmd.StringArg("IMAGE", "", "The image to run")
|
||||||
|
|
||||||
|
cmd.Action = func() {
|
||||||
|
if *detached {
|
||||||
|
//do something
|
||||||
|
}
|
||||||
|
runContainer(*image, *detached, *memory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
You can also add sub commands by calling Command on the Cmd struct:
|
||||||
|
|
||||||
|
bzk.Command("job", "actions on jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Command("list", "list jobs", listJobs)
|
||||||
|
cmd.Command("start", "start a new job", startJob)
|
||||||
|
cmd.Command("log", "show a job log", nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
This could go on to any depth if need be.
|
||||||
|
|
||||||
|
mow.cli also supports command aliases. For example:
|
||||||
|
|
||||||
|
app.Command("start run r", "start doing things", cli.ActionCommand(func() { start() }))
|
||||||
|
|
||||||
|
will alias `start`, `run`, and `r` to the same action. Aliases also work for
|
||||||
|
subcommands:
|
||||||
|
|
||||||
|
app.Command("job j", "actions on jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Command("list ls", "list jobs", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Action = func() {
|
||||||
|
list()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
which then allows you to invoke the subcommand as `app job list`, `app job ls`,
|
||||||
|
`app j ls`, or `app j list`.
|
||||||
|
|
||||||
|
|
||||||
|
As a side-note: it may seem a bit weird the way mow.cli uses a function to initialize a command
|
||||||
|
instead of just returning the command struct.
|
||||||
|
|
||||||
|
The motivation behind this choice is scoping: as with the standard flag package, adding an option or an argument
|
||||||
|
returns a pointer to a value which will be populated when the app is run.
|
||||||
|
|
||||||
|
Since you'll want to store these pointers in variables, and to avoid having dozens of them in the same scope (the main func for example or as global variables),
|
||||||
|
mow.cli's API was specifically tailored to take a func parameter (called CmdInitializer) which accepts the command struct.
|
||||||
|
|
||||||
|
This way, the command specific variables scope is limited to this function.
|
||||||
|
|
||||||
|
Custom types
|
||||||
|
|
||||||
|
Out of the box, mow.cli supports the following types for options and arguments: bool, string, int, strings (slice of strings) and ints (slice of ints)
|
||||||
|
|
||||||
|
You can however extend mow.cli to handle other types, e.g. `time.Duration`, `float64`, or even your own struct types for example.
|
||||||
|
|
||||||
|
To do so, you'll need to:
|
||||||
|
|
||||||
|
* implement the `flag.Value` interface for the custom type
|
||||||
|
|
||||||
|
* declare the option or the flag using `VarOpt`, `VarArg` for the short hands, and `Var` for the full form.
|
||||||
|
|
||||||
|
Here's an example:
|
||||||
|
|
||||||
|
|
||||||
|
// Declare your type
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (d *Duration) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = Duration(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) String() string {
|
||||||
|
duration := time.Duration(*d)
|
||||||
|
return duration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
duration := Duration(0)
|
||||||
|
|
||||||
|
app := App("var", "")
|
||||||
|
|
||||||
|
app.VarArg("DURATION", &duration, "")
|
||||||
|
|
||||||
|
app.Run([]string{"cp", "1h31m42s"})
|
||||||
|
}
|
||||||
|
|
||||||
|
Boolean custom types
|
||||||
|
|
||||||
|
To make your custom type behave as a boolean option, i.e. doesn't take a value, it has to implement a IsBoolFlag method that returns true:
|
||||||
|
|
||||||
|
|
||||||
|
type BoolLike int
|
||||||
|
|
||||||
|
|
||||||
|
func (d *BoolLike) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Multi-valued custom type
|
||||||
|
|
||||||
|
To make your custom type behave as a multi-valued option or argument, i.e. takes multiple values,
|
||||||
|
it has to implement a `Clear` method which will be called whenever the values list needs to be cleared,
|
||||||
|
e.g. when the value was initially populated from an environment variable, and then explicitly set from the CLI:
|
||||||
|
|
||||||
|
type Durations []time.Duration
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (d *Durations) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = append(*d, Duration(parsed))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Durations) String() string {
|
||||||
|
return fmt.Sprintf("%v", *d)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Make it multi-valued
|
||||||
|
func (d *Durations) Clear() {
|
||||||
|
*d = []Duration{}
|
||||||
|
}
|
||||||
|
|
||||||
|
Interceptors
|
||||||
|
|
||||||
|
It is possible to define snippets of code to be executed before and after a command or any of its sub commands is executed.
|
||||||
|
|
||||||
|
For example, given an app with multiple commands but with a global flag which toggles a verbose mode:
|
||||||
|
|
||||||
|
|
||||||
|
app := cli.App("app", "bla bla")
|
||||||
|
verbose := app.Bool(cli.BoolOpt{
|
||||||
|
Name: "verbose",
|
||||||
|
Value: false,
|
||||||
|
Desc: "Enable debug logs",
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Command("command1", "...", func(cmd *cli.Cmd) {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Command("command2", "...", func(cmd *cli.Cmd) {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Instead of repeating yourself by checking if the verbose flag is set or not, and setting the debug level in every command (and its sub-commands),
|
||||||
|
a before interceptor can be set on the `app` instead:
|
||||||
|
|
||||||
|
app.Before = func() {
|
||||||
|
if (*verbose) {
|
||||||
|
logrus.SetLevel(logrus.DebugLevel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Whenever a valid command is called by the user, all the before interceptors defined on the app and the intermediate commands
|
||||||
|
will be called, in order from the root to the leaf.
|
||||||
|
|
||||||
|
Similarly, if you need to execute a code snippet after a command has been called, e.g. to cleanup resources allocated in before interceptors,
|
||||||
|
simply set the After field of the app struct or any other command.
|
||||||
|
|
||||||
|
After interceptors will be called, in order from the leaf up to the root (the opposite order of the Before interceptors).
|
||||||
|
|
||||||
|
Here's a diagram which shows in when and in which order multiple Before and After interceptors get executed:
|
||||||
|
|
||||||
|
+------------+ success +------------+ success +----------------+ success
|
||||||
|
| app.Before +---------------> cmd.Before +-------------> sub_cmd.Before +---------+
|
||||||
|
+------------+ +-+----------+ +--+-------------+ |
|
||||||
|
| | +-v-------+
|
||||||
|
error | error | | sub_cmd |
|
||||||
|
+-----------------------+ +-----------------------+ | Action |
|
||||||
|
| | +-+-------+
|
||||||
|
+------v-----+ +-----v------+ +----------------+ |
|
||||||
|
| app.After <---------------+ cmd.After <-------------+ sub_cmd.After <---------+
|
||||||
|
+------------+ always +------------+ always +----------------+ always
|
||||||
|
|
||||||
|
Spec
|
||||||
|
|
||||||
|
An app or command's call syntax can be customized using spec strings.
|
||||||
|
This can be useful to indicate that an argument is optional for example, or that 2 options are mutually exclusive.
|
||||||
|
|
||||||
|
You can set a spec string on:
|
||||||
|
|
||||||
|
* The app: to configure the syntax for global options and arguments
|
||||||
|
|
||||||
|
* A command: to configure the syntax for that command's options and arguments
|
||||||
|
|
||||||
|
In both cases, a spec string is assigned to the Spec field:
|
||||||
|
|
||||||
|
cp := cli.App("cp", "Copy files around")
|
||||||
|
cp.Spec = "[-R [-H | -L | -P]]"
|
||||||
|
|
||||||
|
And:
|
||||||
|
|
||||||
|
docker := cli.App("docker", "A self-sufficient runtime for linux containers")
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
cmd.Spec = "[-d|--rm] IMAGE [COMMAND [ARG...]]"
|
||||||
|
:
|
||||||
|
:
|
||||||
|
}
|
||||||
|
|
||||||
|
The spec syntax is mostly based on the conventions used in POSIX command line apps help messages and man pages:
|
||||||
|
|
||||||
|
Options
|
||||||
|
|
||||||
|
You can use both short and long option names in spec strings:
|
||||||
|
x.Spec="-f"
|
||||||
|
And:
|
||||||
|
x.Spec="--force"
|
||||||
|
|
||||||
|
In both cases, we required that the f or force flag be set
|
||||||
|
|
||||||
|
Any option you reference in a spec string MUST be explicitly declared, otherwise mow.cli will panic:
|
||||||
|
|
||||||
|
x.BoolOpt("f force", ...)
|
||||||
|
|
||||||
|
Arguments
|
||||||
|
|
||||||
|
Arguments are all-uppercased words:
|
||||||
|
x.Spec="SRC DST"
|
||||||
|
This spec string will force the user to pass exactly 2 arguments, SRC and DST
|
||||||
|
|
||||||
|
Any argument you reference in a spec string MUST be explicitly declared, otherwise mow.cli will panic:
|
||||||
|
|
||||||
|
x.StringArg("SRC", ...)
|
||||||
|
x.StringArg("DST", ...)
|
||||||
|
|
||||||
|
Ordering
|
||||||
|
|
||||||
|
Except for options, The order of the elements in a spec string is respected and enforced when parsing the command line arguments:
|
||||||
|
|
||||||
|
x.Spec = "-f -g SRC -h DST"
|
||||||
|
|
||||||
|
Consecutive options (-f and -g for example) get parsed regardless of the order they are specified in (both "-f=5 -g=6" and "-g=6 -f=5" are valid).
|
||||||
|
|
||||||
|
Order between options and arguments is significant (-f and -g must appear before the SRC argument).
|
||||||
|
|
||||||
|
Same goes for arguments, where SRC must appear before DST.
|
||||||
|
|
||||||
|
Optionality
|
||||||
|
|
||||||
|
You can mark items as optional in a spec string by enclosing them in square brackets :[...]
|
||||||
|
x.Spec = "[-x]"
|
||||||
|
|
||||||
|
Choice
|
||||||
|
|
||||||
|
You can use the | operator to indicate a choice between two or more items
|
||||||
|
x.Spec = "--rm | --daemon"
|
||||||
|
x.Spec = "-H | -L | -P"
|
||||||
|
x.Spec = "-t | DST"
|
||||||
|
|
||||||
|
Repetition
|
||||||
|
|
||||||
|
You can use the ... postfix operator to mark an element as repeatable:
|
||||||
|
x.Spec="SRC..."
|
||||||
|
x.Spec="-e..."
|
||||||
|
|
||||||
|
Grouping
|
||||||
|
|
||||||
|
You can group items using parenthesis. This is useful in combination with the choice and repetition operators (| and ...):
|
||||||
|
x.Spec = "(-e COMMAND)... | (-x|-y)"
|
||||||
|
The parenthesis in the example above serve to mark that it is the sequence of a -e flag followed by an argument that is repeatable, and that
|
||||||
|
all that is mutually exclusive to a choice between -x and -y options.
|
||||||
|
|
||||||
|
Option group
|
||||||
|
|
||||||
|
This is a shortcut to declare a choice between multiple options:
|
||||||
|
x.Spec = "-abcd"
|
||||||
|
Is equivalent to:
|
||||||
|
x.Spec = "(-a | -b | -c | -d)..."
|
||||||
|
I.e. any combination of the listed options in any order, with at least one option.
|
||||||
|
|
||||||
|
All options
|
||||||
|
|
||||||
|
Another shortcut:
|
||||||
|
x.Spec = "[OPTIONS]"
|
||||||
|
This is a special syntax (the square brackets are not for marking an optional item, and the uppercased word is not for an argument).
|
||||||
|
This is equivalent to a repeatable choice between all the available options.
|
||||||
|
For example, if an app or a command declares 4 options a, b, c and d, [OPTIONS] is equivalent to
|
||||||
|
x.Spec = "[-a | -b | -c | -d]..."
|
||||||
|
|
||||||
|
Inline option values
|
||||||
|
|
||||||
|
You can use the =<some-text> notation right after an option (long or short form) to give an inline description or value.
|
||||||
|
An example:
|
||||||
|
x.Spec = "[ -a=<absolute-path> | --timeout=<in seconds> ] ARG"
|
||||||
|
The inline values are ignored by the spec parser and are just there for the final user as a contextual hint.
|
||||||
|
|
||||||
|
Operators
|
||||||
|
|
||||||
|
The `--` operator can be used in a spec string to automatically treat everything following it as an options.
|
||||||
|
|
||||||
|
In other words, placing a `--` in the spec string automatically inserts a `--` in the same position in the program call arguments.
|
||||||
|
|
||||||
|
This lets you write programs like the `time` utility for example:
|
||||||
|
|
||||||
|
x.Spec = "time -lp [-- CMD [ARG...]]"
|
||||||
|
|
||||||
|
|
||||||
|
Spec Grammar
|
||||||
|
|
||||||
|
Here's the EBNF grammar for the Specs language:
|
||||||
|
|
||||||
|
spec -> sequence
|
||||||
|
sequence -> choice*
|
||||||
|
req_sequence -> choice+
|
||||||
|
choice -> atom ('|' atom)*
|
||||||
|
atom -> (shortOpt | longOpt | optSeq | allOpts | group | optional) rep?
|
||||||
|
shortOp -> '-' [A-Za-z]
|
||||||
|
longOpt -> '--' [A-Za-z][A-Za-z0-9]*
|
||||||
|
optSeq -> '-' [A-Za-z]+
|
||||||
|
allOpts -> '[OPTIONS]'
|
||||||
|
group -> '(' req_sequence ')'
|
||||||
|
optional -> '[' req_sequence ']'
|
||||||
|
rep -> '...'
|
||||||
|
|
||||||
|
And that's it for the spec language.
|
||||||
|
You can combine these few building blocks in any way you want (while respecting the grammar above) to construct sophisticated validation constraints
|
||||||
|
(don't go too wild though).
|
||||||
|
|
||||||
|
Behind the scenes, mow.cli parses the spec string and constructs a finite state machine to be used to parse the command line arguments.
|
||||||
|
mow.cli also handles backtracking, and so it can handle tricky cases, or what I like to call "the cp test"
|
||||||
|
cp SRC... DST
|
||||||
|
Without backtracking, this deceptively simple spec string cannot be parsed correctly.
|
||||||
|
For instance, docopt can't handle this case, whereas mow.cli does.
|
||||||
|
|
||||||
|
Default spec
|
||||||
|
|
||||||
|
By default, and unless a spec string is set by the user, mow.cli auto-generates one for the app and every command using this logic:
|
||||||
|
|
||||||
|
* Start with an empty spec string
|
||||||
|
|
||||||
|
* If at least one option was declared, append "[OPTIONS]" to the spec string
|
||||||
|
|
||||||
|
* For every declared argument, append it, in the order of declaration, to the spec string
|
||||||
|
|
||||||
|
For example, given this command declaration:
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *cli.Cmd) {
|
||||||
|
detached := cmd.BoolOpt("d detach", false, "Detached mode: run the container in the background and print the new container ID")
|
||||||
|
memory := cmd.StringOpt("m memory", "", "Memory limit (format: <number><optional unit>, where unit = b, k, m or g)")
|
||||||
|
|
||||||
|
image := cmd.StringArg("IMAGE", "", "")
|
||||||
|
args := cmd.StringsArg("ARG", "", "")
|
||||||
|
})
|
||||||
|
The auto-generated spec string would be:
|
||||||
|
[OPTIONS] IMAGE ARG
|
||||||
|
|
||||||
|
Which should suffice for simple cases. If not, the spec string has to be set explicitly.
|
||||||
|
|
||||||
|
|
||||||
|
Exiting
|
||||||
|
|
||||||
|
mow.cli provides the Exit function which accepts an exit code and exits the app with the provided code.
|
||||||
|
|
||||||
|
You are highly encouraged to call cli.Exit instead of os.Exit for the After interceptors to be executed.
|
||||||
|
*/
|
||||||
|
package cli
|
|
@ -0,0 +1,10 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
errHelpRequested = errors.New("Help requested")
|
||||||
|
errVersionRequested = errors.New("Version requested")
|
||||||
|
)
|
|
@ -0,0 +1,149 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example_greet() {
|
||||||
|
app := App("greet", "Greet")
|
||||||
|
app.Spec = "[NAME]"
|
||||||
|
name := app.String(StringArg{Name: "NAME", Value: "stranger", Desc: "Your name", EnvVar: "USER"})
|
||||||
|
app.Action = func() {
|
||||||
|
fmt.Printf("Hello %s\n", *name)
|
||||||
|
}
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_cp() {
|
||||||
|
cp := App("cp", "Copy files around")
|
||||||
|
cp.Spec = "[-R [-H | -L | -P]] [-fi | -n] SRC... DST"
|
||||||
|
|
||||||
|
var (
|
||||||
|
recursive = cp.Bool(BoolOpt{
|
||||||
|
Name: "R",
|
||||||
|
Value: false,
|
||||||
|
Desc: "copy src files recursively",
|
||||||
|
})
|
||||||
|
|
||||||
|
followSymbolicCL = cp.Bool(BoolOpt{Name: "H", Value: false, Desc: "If the -R option is specified, symbolic links on the command line are followed. (Symbolic links encountered in the tree traversal are not followed.)"})
|
||||||
|
followSymbolicTree = cp.Bool(BoolOpt{Name: "L", Value: false, Desc: "If the -R option is specified, all symbolic links are followed."})
|
||||||
|
followSymbolicNo = cp.Bool(BoolOpt{Name: "P", Value: true, Desc: "If the -R option is specified, no symbolic links are followed. This is the default."})
|
||||||
|
|
||||||
|
force = cp.Bool(BoolOpt{Name: "f", Value: false, Desc: "If the destination file cannot be opened, remove it and create a new file, without prompting for confirmation regardless of its permissions. (The -f option overrides any previous -n option.)"})
|
||||||
|
interactive = cp.Bool(BoolOpt{Name: "i", Value: false, Desc: "Cause cp to write a prompt to the standard error output before copying a file that would overwrite an existing file. If the response from the standard input begins with the character `y' or `Y', the file copy is attempted. (The -i option overrides any previous -n option.)"})
|
||||||
|
noOverwrite = cp.Bool(BoolOpt{Name: "f", Value: false, Desc: "Do not overwrite an existing file. (The -n option overrides any previous -f or -i options.)"})
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
src = cp.Strings(StringsArg{
|
||||||
|
Name: "SRC",
|
||||||
|
Desc: "The source files to copy",
|
||||||
|
})
|
||||||
|
dst = cp.Strings(StringsArg{Name: "DST", Value: nil, Desc: "The destination directory"})
|
||||||
|
)
|
||||||
|
|
||||||
|
cp.Action = func() {
|
||||||
|
fmt.Printf(`copy:
|
||||||
|
SRC: %v
|
||||||
|
DST: %v
|
||||||
|
recursive: %v
|
||||||
|
follow links (CL, Tree, No): %v %v %v
|
||||||
|
force: %v
|
||||||
|
interactive: %v
|
||||||
|
no overwrite: %v`,
|
||||||
|
*src, *dst, *recursive,
|
||||||
|
*followSymbolicCL, *followSymbolicTree, *followSymbolicNo,
|
||||||
|
*force,
|
||||||
|
*interactive,
|
||||||
|
*noOverwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
cp.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_docker() {
|
||||||
|
docker := App("docker", "A self-sufficient runtime for linux containers")
|
||||||
|
|
||||||
|
docker.Command("run", "Run a command in a new container", func(cmd *Cmd) {
|
||||||
|
cmd.Spec = "[-d|--rm] IMAGE [COMMAND [ARG...]]"
|
||||||
|
|
||||||
|
var (
|
||||||
|
detached = cmd.Bool(BoolOpt{Name: "d detach", Value: false, Desc: "Detached mode: run the container in the background and print the new container ID"})
|
||||||
|
rm = cmd.Bool(BoolOpt{Name: "rm", Value: false, Desc: "Automatically remove the container when it exits (incompatible with -d)"})
|
||||||
|
memory = cmd.String(StringOpt{Name: "m memory", Value: "", Desc: "Memory limit (format: <number><optional unit>, where unit = b, k, m or g)"})
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
image = cmd.String(StringArg{Name: "IMAGE", Value: "", Desc: ""})
|
||||||
|
command = cmd.String(StringArg{Name: "COMMAND", Value: "", Desc: "The command to run"})
|
||||||
|
args = cmd.Strings(StringsArg{Name: "ARG", Value: nil, Desc: "The command arguments"})
|
||||||
|
)
|
||||||
|
|
||||||
|
cmd.Action = func() {
|
||||||
|
var how string
|
||||||
|
switch {
|
||||||
|
case *detached:
|
||||||
|
how = "detached"
|
||||||
|
case *rm:
|
||||||
|
how = "rm after"
|
||||||
|
default:
|
||||||
|
how = "--"
|
||||||
|
}
|
||||||
|
fmt.Printf("Run image %s, command %s, args %v, how? %v, mem %s", *image, *command, *args, how, *memory)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
docker.Command("pull", "Pull an image or a repository from the registry", func(cmd *Cmd) {
|
||||||
|
cmd.Spec = "[-a] NAME"
|
||||||
|
|
||||||
|
all := cmd.Bool(BoolOpt{Name: "a all-tags", Value: false, Desc: "Download all tagged images in the repository"})
|
||||||
|
|
||||||
|
name := cmd.String(StringArg{Name: "NAME", Value: "", Desc: "Image name (optionally NAME:TAG)"})
|
||||||
|
|
||||||
|
cmd.Action = func() {
|
||||||
|
if *all {
|
||||||
|
fmt.Printf("Download all tags for image %s", *name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Printf("Download image %s", *name)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
docker.Run(os.Args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_beforeAfter() {
|
||||||
|
app := App("app", "App")
|
||||||
|
bench := app.BoolOpt("b bench", false, "Measure execution time")
|
||||||
|
|
||||||
|
var t0 time.Time
|
||||||
|
|
||||||
|
app.Before = func() {
|
||||||
|
if *bench {
|
||||||
|
t0 = time.Now()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.After = func() {
|
||||||
|
if *bench {
|
||||||
|
d := time.Since(t0)
|
||||||
|
fmt.Printf("Command execution took: %vs", d.Seconds())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Command("cmd1", "first command", func(cmd *Cmd) {
|
||||||
|
cmd.Action = func() {
|
||||||
|
fmt.Print("Running command 1")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Command("cmd2", "second command", func(cmd *Cmd) {
|
||||||
|
cmd.Action = func() {
|
||||||
|
fmt.Print("Running command 2")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
app.Run(os.Args)
|
||||||
|
}
|
|
@ -0,0 +1,43 @@
|
||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
cli "github.com/jawher/mow.cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Declare your type
|
||||||
|
type Counter int
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (c *Counter) Set(v string) error {
|
||||||
|
*c++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Counter) String() string {
|
||||||
|
return fmt.Sprintf("%d", *c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make it a bool option
|
||||||
|
func (c *Counter) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleVarOpt() {
|
||||||
|
|
||||||
|
app := cli.App("var", "Var opt example")
|
||||||
|
|
||||||
|
// Declare a variable of your type
|
||||||
|
verbosity := Counter(0)
|
||||||
|
// Call one of the Var methods (arg, opt, ...) to declare your custom type
|
||||||
|
app.VarOpt("v", &verbosity, "verbosity level")
|
||||||
|
|
||||||
|
app.Action = func() {
|
||||||
|
// The variable will be populated after the app is ran
|
||||||
|
fmt.Print(verbosity)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"app", "-vvvvv"})
|
||||||
|
// Output: 5
|
||||||
|
}
|
|
@ -0,0 +1,44 @@
|
||||||
|
package cli_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
cli "github.com/jawher/mow.cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Declare your type
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
// Make it implement flag.Value
|
||||||
|
func (d *Duration) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = Duration(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) String() string {
|
||||||
|
duration := time.Duration(*d)
|
||||||
|
return duration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ExampleVarArg() {
|
||||||
|
|
||||||
|
app := cli.App("var", "Var arg example")
|
||||||
|
|
||||||
|
// Declare a variable of your type
|
||||||
|
duration := Duration(0)
|
||||||
|
// Call one of the Var methods (arg, opt, ...) to declare your custom type
|
||||||
|
app.VarArg("DURATION", &duration, "")
|
||||||
|
|
||||||
|
app.Action = func() {
|
||||||
|
// The variable will be populated after the app is ran
|
||||||
|
fmt.Print(time.Duration(duration))
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"cp", "1h31m42s"})
|
||||||
|
// Output: 1h31m42s
|
||||||
|
}
|
|
@ -0,0 +1,68 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type step struct {
|
||||||
|
do func()
|
||||||
|
success *step
|
||||||
|
error *step
|
||||||
|
desc string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *step) run(p interface{}) {
|
||||||
|
s.callDo(p)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case s.success != nil:
|
||||||
|
s.success.run(p)
|
||||||
|
case p == nil:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if code, ok := p.(exit); ok {
|
||||||
|
exiter(int(code))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *step) callDo(p interface{}) {
|
||||||
|
if s.do == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if e := recover(); e != nil {
|
||||||
|
if s.error == nil {
|
||||||
|
panic(p)
|
||||||
|
}
|
||||||
|
s.error.run(e)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s.do()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *step) dot() string {
|
||||||
|
trs := flowDot(s, map[*step]bool{})
|
||||||
|
return fmt.Sprintf("digraph G {\n\trankdir=LR\n%s\n}\n", strings.Join(trs, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func flowDot(s *step, visited map[*step]bool) []string {
|
||||||
|
res := []string{}
|
||||||
|
if visited[s] {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
visited[s] = true
|
||||||
|
|
||||||
|
if s.success != nil {
|
||||||
|
res = append(res, fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"ok\"]", s.desc, s.success.desc))
|
||||||
|
res = append(res, flowDot(s.success, visited)...)
|
||||||
|
}
|
||||||
|
if s.error != nil {
|
||||||
|
res = append(res, fmt.Sprintf("\t\"%s\" -> \"%s\" [label=\"ko\"]", s.desc, s.error.desc))
|
||||||
|
res = append(res, flowDot(s.error, visited)...)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,236 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStepCallsDo(t *testing.T) {
|
||||||
|
called := false
|
||||||
|
step := &step{
|
||||||
|
do: func() {
|
||||||
|
called = true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
step.run(nil)
|
||||||
|
|
||||||
|
require.True(t, called, "Step's do wasn't called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCallsSuccessAfterDo(t *testing.T) {
|
||||||
|
calls := 0
|
||||||
|
step := &step{
|
||||||
|
do: func() {
|
||||||
|
require.Equal(t, 0, calls, "Do should be called first")
|
||||||
|
calls++
|
||||||
|
},
|
||||||
|
success: &step{
|
||||||
|
do: func() {
|
||||||
|
require.Equal(t, 1, calls, "Success should be called second")
|
||||||
|
calls++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: &step{
|
||||||
|
do: func() {
|
||||||
|
t.Fatalf("Error should not have been called")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
step.run(nil)
|
||||||
|
|
||||||
|
require.Equal(t, 2, calls, "Both do and success should be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCallsErrorIfDoPanics(t *testing.T) {
|
||||||
|
defer func() { recover() }()
|
||||||
|
calls := 0
|
||||||
|
step := &step{
|
||||||
|
do: func() {
|
||||||
|
require.Equal(t, 0, calls, "Do should be called first")
|
||||||
|
calls++
|
||||||
|
panic(42)
|
||||||
|
},
|
||||||
|
success: &step{
|
||||||
|
do: func() {
|
||||||
|
t.Fatalf("Success should not have been called")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
error: &step{
|
||||||
|
do: func() {
|
||||||
|
require.Equal(t, 1, calls, "Error should be called second")
|
||||||
|
calls++
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
step.run(nil)
|
||||||
|
|
||||||
|
require.Equal(t, 2, calls, "Both do and error should be called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCallsOsExitIfAskedTo(t *testing.T) {
|
||||||
|
exitCalled := false
|
||||||
|
defer exitShouldBeCalledWith(t, 42, &exitCalled)()
|
||||||
|
|
||||||
|
step := &step{}
|
||||||
|
|
||||||
|
step.run(exit(42))
|
||||||
|
|
||||||
|
require.True(t, exitCalled, "should have called exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepRethrowsPanic(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
require.Equal(t, 42, recover(), "should panicked with the same value")
|
||||||
|
}()
|
||||||
|
|
||||||
|
step := &step{}
|
||||||
|
|
||||||
|
step.run(42)
|
||||||
|
|
||||||
|
t.Fatalf("Should have panicked")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepShouldNopIfNoSuccessNorPanic(t *testing.T) {
|
||||||
|
defer exitShouldNotCalled(t)()
|
||||||
|
|
||||||
|
step := &step{}
|
||||||
|
|
||||||
|
step.run(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBeforeAndAfterFlowOrder(t *testing.T) {
|
||||||
|
counter := 0
|
||||||
|
|
||||||
|
app := App("app", "")
|
||||||
|
|
||||||
|
app.Before = callChecker(t, 0, &counter)
|
||||||
|
app.Command("c", "", func(c *Cmd) {
|
||||||
|
c.Before = callChecker(t, 1, &counter)
|
||||||
|
c.Command("cc", "", func(cc *Cmd) {
|
||||||
|
cc.Before = callChecker(t, 2, &counter)
|
||||||
|
cc.Action = callChecker(t, 3, &counter)
|
||||||
|
cc.After = callChecker(t, 4, &counter)
|
||||||
|
})
|
||||||
|
c.After = callChecker(t, 5, &counter)
|
||||||
|
})
|
||||||
|
app.After = callChecker(t, 6, &counter)
|
||||||
|
|
||||||
|
app.Run([]string{"app", "c", "cc"})
|
||||||
|
require.Equal(t, 7, counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBeforeAndAfterFlowOrderWhenOneBeforePanics(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
recover()
|
||||||
|
}()
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
|
||||||
|
app := App("app", "")
|
||||||
|
|
||||||
|
app.Before = callChecker(t, 0, &counter)
|
||||||
|
app.Command("c", "", func(c *Cmd) {
|
||||||
|
c.Before = callChecker(t, 1, &counter)
|
||||||
|
c.Command("cc", "", func(cc *Cmd) {
|
||||||
|
cc.Before = callCheckerAndPanic(t, 42, 2, &counter)
|
||||||
|
cc.Action = func() {
|
||||||
|
t.Fatalf("should not have been called")
|
||||||
|
}
|
||||||
|
cc.After = func() {
|
||||||
|
t.Fatalf("should not have been called")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
c.After = callChecker(t, 3, &counter)
|
||||||
|
})
|
||||||
|
app.After = callChecker(t, 4, &counter)
|
||||||
|
|
||||||
|
app.Run([]string{"app", "c", "cc"})
|
||||||
|
require.Equal(t, 5, counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBeforeAndAfterFlowOrderWhenOneAfterPanics(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
e := recover()
|
||||||
|
require.Equal(t, 42, e)
|
||||||
|
}()
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
|
||||||
|
app := App("app", "")
|
||||||
|
|
||||||
|
app.Before = callChecker(t, 0, &counter)
|
||||||
|
app.Command("c", "", func(c *Cmd) {
|
||||||
|
c.Before = callChecker(t, 1, &counter)
|
||||||
|
c.Command("cc", "", func(cc *Cmd) {
|
||||||
|
cc.Before = callChecker(t, 2, &counter)
|
||||||
|
cc.Action = callChecker(t, 3, &counter)
|
||||||
|
cc.After = callCheckerAndPanic(t, 42, 4, &counter)
|
||||||
|
})
|
||||||
|
c.After = callChecker(t, 5, &counter)
|
||||||
|
})
|
||||||
|
app.After = callChecker(t, 6, &counter)
|
||||||
|
|
||||||
|
app.Run([]string{"app", "c", "cc"})
|
||||||
|
require.Equal(t, 7, counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBeforeAndAfterFlowOrderWhenMultipleAftersPanic(t *testing.T) {
|
||||||
|
defer func() {
|
||||||
|
e := recover()
|
||||||
|
require.Equal(t, 666, e)
|
||||||
|
}()
|
||||||
|
|
||||||
|
counter := 0
|
||||||
|
|
||||||
|
app := App("app", "")
|
||||||
|
|
||||||
|
app.Before = callChecker(t, 0, &counter)
|
||||||
|
app.Command("c", "", func(c *Cmd) {
|
||||||
|
c.Before = callChecker(t, 1, &counter)
|
||||||
|
c.Command("cc", "", func(cc *Cmd) {
|
||||||
|
cc.Before = callChecker(t, 2, &counter)
|
||||||
|
cc.Action = callChecker(t, 3, &counter)
|
||||||
|
cc.After = callCheckerAndPanic(t, 42, 4, &counter)
|
||||||
|
})
|
||||||
|
c.After = callChecker(t, 5, &counter)
|
||||||
|
})
|
||||||
|
app.After = callCheckerAndPanic(t, 666, 6, &counter)
|
||||||
|
|
||||||
|
app.Run([]string{"app", "c", "cc"})
|
||||||
|
require.Equal(t, 7, counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCommandAction(t *testing.T) {
|
||||||
|
|
||||||
|
called := false
|
||||||
|
|
||||||
|
app := App("app", "")
|
||||||
|
|
||||||
|
app.Command("a", "", ActionCommand(func() { called = true }))
|
||||||
|
|
||||||
|
app.Run([]string{"app", "a"})
|
||||||
|
|
||||||
|
require.True(t, called, "commandAction should be called")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func callChecker(t *testing.T, wanted int, counter *int) func() {
|
||||||
|
return func() {
|
||||||
|
t.Logf("checker: wanted: %d, got %d", wanted, *counter)
|
||||||
|
require.Equal(t, wanted, *counter)
|
||||||
|
*counter++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callCheckerAndPanic(t *testing.T, panicValue interface{}, wanted int, counter *int) func() {
|
||||||
|
return func() {
|
||||||
|
t.Logf("checker: wanted: %d, got %d", wanted, *counter)
|
||||||
|
require.Equal(t, wanted, *counter)
|
||||||
|
*counter++
|
||||||
|
panic(panicValue)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,254 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type state struct {
|
||||||
|
id int
|
||||||
|
terminal bool
|
||||||
|
transitions transitions
|
||||||
|
cmd *Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type transition struct {
|
||||||
|
matcher upMatcher
|
||||||
|
next *state
|
||||||
|
}
|
||||||
|
|
||||||
|
type transitions []*transition
|
||||||
|
|
||||||
|
func (t transitions) Len() int { return len(t) }
|
||||||
|
func (t transitions) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
|
||||||
|
func (t transitions) Less(i, j int) bool {
|
||||||
|
a, _ := t[i].matcher, t[j].matcher
|
||||||
|
switch a.(type) {
|
||||||
|
case upShortcut:
|
||||||
|
return false
|
||||||
|
case upOptsEnd:
|
||||||
|
return false
|
||||||
|
case *arg:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var _id = 0
|
||||||
|
|
||||||
|
func newState(cmd *Cmd) *state {
|
||||||
|
_id++
|
||||||
|
return &state{_id, false, []*transition{}, cmd}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) t(matcher upMatcher, next *state) *state {
|
||||||
|
s.transitions = append(s.transitions, &transition{matcher, next})
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) has(tr *transition) bool {
|
||||||
|
for _, t := range s.transitions {
|
||||||
|
if t.next == tr.next && t.matcher == tr.matcher {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func incoming(s, into *state, visited map[*state]bool) []*transition {
|
||||||
|
res := []*transition{}
|
||||||
|
if visited[s] {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
visited[s] = true
|
||||||
|
|
||||||
|
for _, tr := range s.transitions {
|
||||||
|
if tr.next == into {
|
||||||
|
res = append(res, tr)
|
||||||
|
}
|
||||||
|
res = append(res, incoming(tr.next, into, visited)...)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeTransitionAt(idx int, arr transitions) transitions {
|
||||||
|
res := make([]*transition, len(arr)-1)
|
||||||
|
copy(res, arr[:idx])
|
||||||
|
copy(res[idx:], arr[idx+1:])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) simplify() {
|
||||||
|
simplify(s, s, map[*state]bool{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func simplify(start, s *state, visited map[*state]bool) {
|
||||||
|
if visited[s] {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
visited[s] = true
|
||||||
|
for _, tr := range s.transitions {
|
||||||
|
simplify(start, tr.next, visited)
|
||||||
|
}
|
||||||
|
for s.simplifySelf(start) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) simplifySelf(start *state) bool {
|
||||||
|
for idx, tr := range s.transitions {
|
||||||
|
if _, ok := tr.matcher.(upShortcut); ok {
|
||||||
|
next := tr.next
|
||||||
|
s.transitions = removeTransitionAt(idx, s.transitions)
|
||||||
|
for _, tr := range next.transitions {
|
||||||
|
if !s.has(tr) {
|
||||||
|
s.transitions = append(s.transitions, tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if next.terminal {
|
||||||
|
s.terminal = true
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) dot() string {
|
||||||
|
trs := dot(s, map[*state]bool{})
|
||||||
|
return fmt.Sprintf("digraph G {\n\trankdir=LR\n%s\n}\n", strings.Join(trs, "\n"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func dot(s *state, visited map[*state]bool) []string {
|
||||||
|
res := []string{}
|
||||||
|
if visited[s] {
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
visited[s] = true
|
||||||
|
|
||||||
|
for _, tr := range s.transitions {
|
||||||
|
res = append(res, fmt.Sprintf("\tS%d -> S%d [label=\"%v\"]", s.id, tr.next.id, tr.matcher))
|
||||||
|
res = append(res, dot(tr.next, visited)...)
|
||||||
|
}
|
||||||
|
if s.terminal {
|
||||||
|
res = append(res, fmt.Sprintf("\tS%d [peripheries=2]", s.id))
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseContext struct {
|
||||||
|
args map[*arg][]string
|
||||||
|
opts map[*opt][]string
|
||||||
|
excludedOpts map[*opt]struct{}
|
||||||
|
rejectOptions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func newParseContext() parseContext {
|
||||||
|
return parseContext{
|
||||||
|
args: map[*arg][]string{},
|
||||||
|
opts: map[*opt][]string{},
|
||||||
|
excludedOpts: map[*opt]struct{}{},
|
||||||
|
rejectOptions: false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pc parseContext) merge(o parseContext) {
|
||||||
|
for k, vs := range o.args {
|
||||||
|
pc.args[k] = append(pc.args[k], vs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, vs := range o.opts {
|
||||||
|
pc.opts[k] = append(pc.opts[k], vs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) parse(args []string) error {
|
||||||
|
pc := newParseContext()
|
||||||
|
ok, err := s.apply(args, pc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("incorrect usage")
|
||||||
|
}
|
||||||
|
|
||||||
|
for opt, vs := range pc.opts {
|
||||||
|
if multiValued, ok := opt.value.(multiValued); ok {
|
||||||
|
multiValued.Clear()
|
||||||
|
opt.valueSetFromEnv = false
|
||||||
|
}
|
||||||
|
for _, v := range vs {
|
||||||
|
if err := opt.value.Set(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.valueSetByUser != nil {
|
||||||
|
*opt.valueSetByUser = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for arg, vs := range pc.args {
|
||||||
|
if multiValued, ok := arg.value.(multiValued); ok {
|
||||||
|
multiValued.Clear()
|
||||||
|
arg.valueSetFromEnv = false
|
||||||
|
}
|
||||||
|
for _, v := range vs {
|
||||||
|
if err := arg.value.Set(v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if arg.valueSetByUser != nil {
|
||||||
|
*arg.valueSetByUser = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *state) apply(args []string, pc parseContext) (bool, error) {
|
||||||
|
if s.terminal && len(args) == 0 {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
sort.Sort(s.transitions)
|
||||||
|
|
||||||
|
if len(args) > 0 {
|
||||||
|
arg := args[0]
|
||||||
|
|
||||||
|
if !pc.rejectOptions && arg == "--" {
|
||||||
|
pc.rejectOptions = true
|
||||||
|
args = args[1:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type match struct {
|
||||||
|
tr *transition
|
||||||
|
rem []string
|
||||||
|
pc parseContext
|
||||||
|
}
|
||||||
|
|
||||||
|
matches := []*match{}
|
||||||
|
for _, tr := range s.transitions {
|
||||||
|
fresh := newParseContext()
|
||||||
|
fresh.rejectOptions = pc.rejectOptions
|
||||||
|
if ok, rem := tr.matcher.match(args, &fresh); ok {
|
||||||
|
matches = append(matches, &match{tr, rem, fresh})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, m := range matches {
|
||||||
|
ok, err := m.tr.next.apply(m.rem, m.pc)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
if ok {
|
||||||
|
pc.merge(m.pc)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
|
@ -0,0 +1,73 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"bytes"
|
||||||
|
|
||||||
|
"io/ioutil"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func exitShouldBeCalledWith(t *testing.T, wantedExitCode int, called *bool) func() {
|
||||||
|
oldExiter := exiter
|
||||||
|
exiter = func(code int) {
|
||||||
|
require.Equal(t, wantedExitCode, code, "unwanted exit code")
|
||||||
|
*called = true
|
||||||
|
}
|
||||||
|
return func() { exiter = oldExiter }
|
||||||
|
}
|
||||||
|
|
||||||
|
func exitShouldNotCalled(t *testing.T) func() {
|
||||||
|
oldExiter := exiter
|
||||||
|
exiter = func(code int) {
|
||||||
|
t.Errorf("exit should not have been called")
|
||||||
|
}
|
||||||
|
return func() { exiter = oldExiter }
|
||||||
|
}
|
||||||
|
|
||||||
|
func suppressOutput() func() {
|
||||||
|
return captureAndRestoreOutput(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func captureAndRestoreOutput(out, err *string) func() {
|
||||||
|
oldStdOut := stdOut
|
||||||
|
oldStdErr := stdErr
|
||||||
|
|
||||||
|
if out == nil {
|
||||||
|
stdOut = ioutil.Discard
|
||||||
|
} else {
|
||||||
|
stdOut = trapWriter(out)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
stdErr = ioutil.Discard
|
||||||
|
} else {
|
||||||
|
stdErr = trapWriter(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return func() {
|
||||||
|
stdOut = oldStdOut
|
||||||
|
stdErr = oldStdErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trapWriter(writeTo *string) *writerTrap {
|
||||||
|
return &writerTrap{
|
||||||
|
buffer: bytes.NewBuffer(nil),
|
||||||
|
writeTo: writeTo,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type writerTrap struct {
|
||||||
|
buffer *bytes.Buffer
|
||||||
|
writeTo *string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *writerTrap) Write(p []byte) (n int, err error) {
|
||||||
|
n, err = w.buffer.Write(p)
|
||||||
|
if err == nil {
|
||||||
|
*(w.writeTo) = w.buffer.String()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
|
@ -0,0 +1,282 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type upMatcher interface {
|
||||||
|
match(args []string, c *parseContext) (bool, []string)
|
||||||
|
}
|
||||||
|
|
||||||
|
type upShortcut bool
|
||||||
|
|
||||||
|
func (u upShortcut) match(args []string, c *parseContext) (bool, []string) {
|
||||||
|
return true, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u upShortcut) String() string {
|
||||||
|
return "*"
|
||||||
|
}
|
||||||
|
|
||||||
|
type upOptsEnd bool
|
||||||
|
|
||||||
|
func (u upOptsEnd) match(args []string, c *parseContext) (bool, []string) {
|
||||||
|
c.rejectOptions = true
|
||||||
|
return true, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u upOptsEnd) String() string {
|
||||||
|
return "--"
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
shortcut = upShortcut(true)
|
||||||
|
optsEnd = upOptsEnd(true)
|
||||||
|
)
|
||||||
|
|
||||||
|
func (arg *arg) match(args []string, c *parseContext) (bool, []string) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return false, args
|
||||||
|
}
|
||||||
|
if !c.rejectOptions && strings.HasPrefix(args[0], "-") && args[0] != "-" {
|
||||||
|
return false, args
|
||||||
|
}
|
||||||
|
c.args[arg] = append(c.args[arg], args[0])
|
||||||
|
return true, args[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
type optMatcher struct {
|
||||||
|
theOne *opt
|
||||||
|
optionsIdx map[string]*opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *optMatcher) match(args []string, c *parseContext) (bool, []string) {
|
||||||
|
if len(args) == 0 || c.rejectOptions {
|
||||||
|
return o.theOne.valueSetFromEnv, args
|
||||||
|
}
|
||||||
|
|
||||||
|
idx := 0
|
||||||
|
for idx < len(args) {
|
||||||
|
arg := args[idx]
|
||||||
|
switch {
|
||||||
|
case arg == "-":
|
||||||
|
idx++
|
||||||
|
case arg == "--":
|
||||||
|
return o.theOne.valueSetFromEnv, nil
|
||||||
|
case strings.HasPrefix(arg, "--"):
|
||||||
|
matched, consumed, nargs := o.matchLongOpt(args, idx, c)
|
||||||
|
|
||||||
|
if matched {
|
||||||
|
return true, nargs
|
||||||
|
}
|
||||||
|
if consumed == 0 {
|
||||||
|
return o.theOne.valueSetFromEnv, args
|
||||||
|
}
|
||||||
|
idx += consumed
|
||||||
|
|
||||||
|
case strings.HasPrefix(arg, "-"):
|
||||||
|
matched, consumed, nargs := o.matchShortOpt(args, idx, c)
|
||||||
|
if matched {
|
||||||
|
return true, nargs
|
||||||
|
}
|
||||||
|
if consumed == 0 {
|
||||||
|
return o.theOne.valueSetFromEnv, args
|
||||||
|
}
|
||||||
|
idx += consumed
|
||||||
|
|
||||||
|
default:
|
||||||
|
return o.theOne.valueSetFromEnv, args
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return o.theOne.valueSetFromEnv, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *optMatcher) matchLongOpt(args []string, idx int, c *parseContext) (bool, int, []string) {
|
||||||
|
arg := args[idx]
|
||||||
|
kv := strings.Split(arg, "=")
|
||||||
|
name := kv[0]
|
||||||
|
opt, found := o.optionsIdx[name]
|
||||||
|
if !found {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(kv) == 2:
|
||||||
|
if opt != o.theOne {
|
||||||
|
return false, 1, args
|
||||||
|
}
|
||||||
|
value := kv[1]
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], value)
|
||||||
|
return true, 1, removeStringAt(idx, args)
|
||||||
|
case opt.isBool():
|
||||||
|
if opt != o.theOne {
|
||||||
|
return false, 1, args
|
||||||
|
}
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], "true")
|
||||||
|
return true, 1, removeStringAt(idx, args)
|
||||||
|
default:
|
||||||
|
if len(args[idx:]) < 2 {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
if opt != o.theOne {
|
||||||
|
return false, 2, args
|
||||||
|
}
|
||||||
|
value := args[idx+1]
|
||||||
|
if strings.HasPrefix(value, "-") {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], value)
|
||||||
|
return true, 2, removeStringsBetween(idx, idx+1, args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *optMatcher) matchShortOpt(args []string, idx int, c *parseContext) (bool, int, []string) {
|
||||||
|
arg := args[idx]
|
||||||
|
if len(arg) < 2 {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasPrefix(arg[2:], "=") {
|
||||||
|
name := arg[0:2]
|
||||||
|
opt, _ := o.optionsIdx[name]
|
||||||
|
if opt == o.theOne {
|
||||||
|
value := arg[3:]
|
||||||
|
if value == "" {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], value)
|
||||||
|
return true, 1, removeStringAt(idx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 1, args
|
||||||
|
}
|
||||||
|
|
||||||
|
rem := arg[1:]
|
||||||
|
|
||||||
|
remIdx := 0
|
||||||
|
for len(rem[remIdx:]) > 0 {
|
||||||
|
name := "-" + rem[remIdx:remIdx+1]
|
||||||
|
|
||||||
|
opt, found := o.optionsIdx[name]
|
||||||
|
if !found {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt.isBool() {
|
||||||
|
if opt != o.theOne {
|
||||||
|
remIdx++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], "true")
|
||||||
|
newRem := rem[:remIdx] + rem[remIdx+1:]
|
||||||
|
if newRem == "" {
|
||||||
|
return true, 1, removeStringAt(idx, args)
|
||||||
|
}
|
||||||
|
return true, 0, replaceStringAt(idx, "-"+newRem, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
value := rem[remIdx+1:]
|
||||||
|
if value == "" {
|
||||||
|
if len(args[idx+1:]) == 0 {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
if opt != o.theOne {
|
||||||
|
return false, 2, args
|
||||||
|
}
|
||||||
|
|
||||||
|
value = args[idx+1]
|
||||||
|
if strings.HasPrefix(value, "-") {
|
||||||
|
return false, 0, args
|
||||||
|
}
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], value)
|
||||||
|
|
||||||
|
newRem := rem[:remIdx]
|
||||||
|
if newRem == "" {
|
||||||
|
return true, 2, removeStringsBetween(idx, idx+1, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
nargs := replaceStringAt(idx, "-"+newRem, args)
|
||||||
|
|
||||||
|
return true, 1, removeStringAt(idx+1, nargs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opt != o.theOne {
|
||||||
|
return false, 1, args
|
||||||
|
}
|
||||||
|
c.opts[o.theOne] = append(c.opts[o.theOne], value)
|
||||||
|
newRem := rem[:remIdx]
|
||||||
|
if newRem == "" {
|
||||||
|
return true, 1, removeStringAt(idx, args)
|
||||||
|
}
|
||||||
|
return true, 0, replaceStringAt(idx, "-"+newRem, args)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, 1, args
|
||||||
|
}
|
||||||
|
|
||||||
|
type optsMatcher struct {
|
||||||
|
options []*opt
|
||||||
|
optionsIndex map[string]*opt
|
||||||
|
}
|
||||||
|
|
||||||
|
func (om optsMatcher) try(args []string, c *parseContext) (bool, []string) {
|
||||||
|
if len(args) == 0 || c.rejectOptions {
|
||||||
|
return false, args
|
||||||
|
}
|
||||||
|
for _, o := range om.options {
|
||||||
|
if _, exclude := c.excludedOpts[o]; exclude {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if ok, nargs := (&optMatcher{theOne: o, optionsIdx: om.optionsIndex}).match(args, c); ok {
|
||||||
|
if o.valueSetFromEnv {
|
||||||
|
c.excludedOpts[o] = struct{}{}
|
||||||
|
}
|
||||||
|
return true, nargs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false, args
|
||||||
|
}
|
||||||
|
|
||||||
|
func (om optsMatcher) match(args []string, c *parseContext) (bool, []string) {
|
||||||
|
ok, nargs := om.try(args, c)
|
||||||
|
if !ok {
|
||||||
|
return false, args
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
ok, nnargs := om.try(nargs, c)
|
||||||
|
if !ok {
|
||||||
|
return true, nargs
|
||||||
|
}
|
||||||
|
nargs = nnargs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (om optsMatcher) String() string {
|
||||||
|
return fmt.Sprintf("Opts(%v)", om.options)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStringAt(idx int, arr []string) []string {
|
||||||
|
res := make([]string, len(arr)-1)
|
||||||
|
copy(res, arr[:idx])
|
||||||
|
copy(res[idx:], arr[idx+1:])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeStringsBetween(from, to int, arr []string) []string {
|
||||||
|
res := make([]string, len(arr)-(to-from+1))
|
||||||
|
copy(res, arr[:from])
|
||||||
|
copy(res[from:], arr[to+1:])
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func replaceStringAt(idx int, with string, arr []string) []string {
|
||||||
|
res := make([]string, len(arr))
|
||||||
|
copy(res, arr[:idx])
|
||||||
|
res[idx] = with
|
||||||
|
copy(res[idx+1:], arr[idx+1:])
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,227 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestShortcut(t *testing.T) {
|
||||||
|
pc := &parseContext{}
|
||||||
|
args := []string{"a", "b"}
|
||||||
|
ok, nargs := shortcut.match(args, pc)
|
||||||
|
require.True(t, ok, "shortcut always matches")
|
||||||
|
require.Equal(t, args, nargs, "shortcut doesn't touch the passed args")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptsEnd(t *testing.T) {
|
||||||
|
pc := &parseContext{}
|
||||||
|
args := []string{"a", "b"}
|
||||||
|
ok, nargs := optsEnd.match(args, pc)
|
||||||
|
require.True(t, ok, "optsEnd always matches")
|
||||||
|
require.Equal(t, args, nargs, "optsEnd doesn't touch the passed args")
|
||||||
|
require.True(t, pc.rejectOptions, "optsEnd sets the rejectOptions flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArgMatcher(t *testing.T) {
|
||||||
|
arg := &arg{name: "X"}
|
||||||
|
|
||||||
|
{
|
||||||
|
pc := newParseContext()
|
||||||
|
args := []string{"a", "b"}
|
||||||
|
ok, nargs := arg.match(args, &pc)
|
||||||
|
require.True(t, ok, "arg should match")
|
||||||
|
require.Equal(t, []string{"b"}, nargs, "arg should consume the matched value")
|
||||||
|
require.Equal(t, []string{"a"}, pc.args[arg], "arg should stored the matched value")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
pc := newParseContext()
|
||||||
|
ok, _ := arg.match([]string{"-v"}, &pc)
|
||||||
|
require.False(t, ok, "arg should not match options")
|
||||||
|
}
|
||||||
|
{
|
||||||
|
pc := newParseContext()
|
||||||
|
pc.rejectOptions = true
|
||||||
|
ok, _ := arg.match([]string{"-v"}, &pc)
|
||||||
|
require.True(t, ok, "arg should match options when the reject flag is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolOptMatcher(t *testing.T) {
|
||||||
|
forceOpt := &opt{names: []string{"-f", "--force"}, value: newBoolValue(new(bool), false)}
|
||||||
|
optMatcher := &optMatcher{
|
||||||
|
theOne: forceOpt,
|
||||||
|
optionsIdx: map[string]*opt{
|
||||||
|
"-f": forceOpt,
|
||||||
|
"--force": forceOpt,
|
||||||
|
"-g": {names: []string{"-g"}, value: newBoolValue(new(bool), false)},
|
||||||
|
"-x": {names: []string{"-x"}, value: newBoolValue(new(bool), false)},
|
||||||
|
"-y": {names: []string{"-y"}, value: newBoolValue(new(bool), false)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cases := []struct {
|
||||||
|
args []string
|
||||||
|
nargs []string
|
||||||
|
val []string
|
||||||
|
}{
|
||||||
|
{[]string{"-f", "x"}, []string{"x"}, []string{"true"}},
|
||||||
|
{[]string{"-f=true", "x"}, []string{"x"}, []string{"true"}},
|
||||||
|
{[]string{"-f=false", "x"}, []string{"x"}, []string{"false"}},
|
||||||
|
{[]string{"--force", "x"}, []string{"x"}, []string{"true"}},
|
||||||
|
{[]string{"--force=true", "x"}, []string{"x"}, []string{"true"}},
|
||||||
|
{[]string{"--force=false", "x"}, []string{"x"}, []string{"false"}},
|
||||||
|
{[]string{"-fgxy", "x"}, []string{"-gxy", "x"}, []string{"true"}},
|
||||||
|
{[]string{"-gfxy", "x"}, []string{"-gxy", "x"}, []string{"true"}},
|
||||||
|
{[]string{"-gxfy", "x"}, []string{"-gxy", "x"}, []string{"true"}},
|
||||||
|
{[]string{"-gxyf", "x"}, []string{"-gxy", "x"}, []string{"true"}},
|
||||||
|
}
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("Testing case: %#v", cas)
|
||||||
|
pc := newParseContext()
|
||||||
|
ok, nargs := optMatcher.match(cas.args, &pc)
|
||||||
|
require.True(t, ok, "opt should match")
|
||||||
|
require.Equal(t, cas.nargs, nargs, "opt should consume the option name")
|
||||||
|
require.Equal(t, cas.val, pc.opts[forceOpt], "true should stored as the option's value")
|
||||||
|
|
||||||
|
pc = newParseContext()
|
||||||
|
pc.rejectOptions = true
|
||||||
|
nok, _ := optMatcher.match(cas.args, &pc)
|
||||||
|
require.False(t, nok, "opt shouldn't match when rejectOptions flag is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptMatcher(t *testing.T) {
|
||||||
|
names := []string{"-f", "--force"}
|
||||||
|
opts := []*opt{
|
||||||
|
{names: names, value: newStringValue(new(string), "")},
|
||||||
|
{names: names, value: newIntValue(new(int), 0)},
|
||||||
|
{names: names, value: newStringsValue(new([]string), nil)},
|
||||||
|
{names: names, value: newIntsValue(new([]int), nil)},
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
args []string
|
||||||
|
nargs []string
|
||||||
|
val []string
|
||||||
|
}{
|
||||||
|
{[]string{"-f", "x"}, []string{}, []string{"x"}},
|
||||||
|
{[]string{"-f=x", "y"}, []string{"y"}, []string{"x"}},
|
||||||
|
{[]string{"-fx", "y"}, []string{"y"}, []string{"x"}},
|
||||||
|
{[]string{"-afx", "y"}, []string{"-a", "y"}, []string{"x"}},
|
||||||
|
{[]string{"-af", "x", "y"}, []string{"-a", "y"}, []string{"x"}},
|
||||||
|
{[]string{"--force", "x"}, []string{}, []string{"x"}},
|
||||||
|
{[]string{"--force=x", "y"}, []string{"y"}, []string{"x"}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
for _, forceOpt := range opts {
|
||||||
|
t.Logf("Testing case: %#v with opt: %#v", cas, forceOpt)
|
||||||
|
optMatcher := &optMatcher{
|
||||||
|
theOne: forceOpt,
|
||||||
|
optionsIdx: map[string]*opt{
|
||||||
|
"-f": forceOpt,
|
||||||
|
"--force": forceOpt,
|
||||||
|
"-a": {names: []string{"-a"}, value: newBoolValue(new(bool), false)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pc := newParseContext()
|
||||||
|
ok, nargs := optMatcher.match(cas.args, &pc)
|
||||||
|
require.True(t, ok, "opt %#v should match args %v, %v", forceOpt, cas.args, forceOpt.isBool())
|
||||||
|
require.Equal(t, cas.nargs, nargs, "opt should consume the option name")
|
||||||
|
require.Equal(t, cas.val, pc.opts[forceOpt], "true should stored as the option's value")
|
||||||
|
|
||||||
|
pc = newParseContext()
|
||||||
|
pc.rejectOptions = true
|
||||||
|
nok, _ := optMatcher.match(cas.args, &pc)
|
||||||
|
require.False(t, nok, "opt shouldn't match when rejectOptions flag is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestOptsMatcher(t *testing.T) {
|
||||||
|
opts := optsMatcher{
|
||||||
|
options: []*opt{
|
||||||
|
{names: []string{"-f", "--force"}, value: newBoolValue(new(bool), false)},
|
||||||
|
{names: []string{"-g", "--green"}, value: newStringValue(new(string), "")},
|
||||||
|
},
|
||||||
|
optionsIndex: map[string]*opt{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts.options {
|
||||||
|
for _, n := range o.names {
|
||||||
|
opts.optionsIndex[n] = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
args []string
|
||||||
|
nargs []string
|
||||||
|
val [][]string
|
||||||
|
}{
|
||||||
|
{[]string{"-f", "x"}, []string{"x"}, [][]string{{"true"}, nil}},
|
||||||
|
{[]string{"-f=false", "y"}, []string{"y"}, [][]string{{"false"}, nil}},
|
||||||
|
{[]string{"--force", "x"}, []string{"x"}, [][]string{{"true"}, nil}},
|
||||||
|
{[]string{"--force=false", "y"}, []string{"y"}, [][]string{{"false"}, nil}},
|
||||||
|
|
||||||
|
{[]string{"-g", "x"}, []string{}, [][]string{nil, {"x"}}},
|
||||||
|
{[]string{"-g=x", "y"}, []string{"y"}, [][]string{nil, {"x"}}},
|
||||||
|
{[]string{"-gx", "y"}, []string{"y"}, [][]string{nil, {"x"}}},
|
||||||
|
{[]string{"--green", "x"}, []string{}, [][]string{nil, {"x"}}},
|
||||||
|
{[]string{"--green=x", "y"}, []string{"y"}, [][]string{nil, {"x"}}},
|
||||||
|
|
||||||
|
{[]string{"-f", "-g", "x", "y"}, []string{"y"}, [][]string{{"true"}, {"x"}}},
|
||||||
|
{[]string{"-g", "x", "-f", "y"}, []string{"y"}, [][]string{{"true"}, {"x"}}},
|
||||||
|
{[]string{"-fg", "x", "y"}, []string{"y"}, [][]string{{"true"}, {"x"}}},
|
||||||
|
{[]string{"-fgxxx", "y"}, []string{"y"}, [][]string{{"true"}, {"xxx"}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("testing with args %v", cas.args)
|
||||||
|
pc := newParseContext()
|
||||||
|
ok, nargs := opts.match(cas.args, &pc)
|
||||||
|
require.True(t, ok, "opts should match")
|
||||||
|
require.Equal(t, cas.nargs, nargs, "opts should consume the option name")
|
||||||
|
for i, opt := range opts.options {
|
||||||
|
require.Equal(t, cas.val[i], pc.opts[opt], "the option value for %v should be stored", opt)
|
||||||
|
}
|
||||||
|
|
||||||
|
pc = newParseContext()
|
||||||
|
pc.rejectOptions = true
|
||||||
|
nok, _ := opts.match(cas.args, &pc)
|
||||||
|
require.False(t, nok, "opts shouldn't match when rejectOptions flag is set")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Issue 55
|
||||||
|
func TestOptsMatcherInfiniteLoop(t *testing.T) {
|
||||||
|
opts := optsMatcher{
|
||||||
|
options: []*opt{
|
||||||
|
{names: []string{"-g"}, value: newStringValue(new(string), ""), valueSetFromEnv: true},
|
||||||
|
},
|
||||||
|
optionsIndex: map[string]*opt{},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, o := range opts.options {
|
||||||
|
for _, n := range o.names {
|
||||||
|
opts.optionsIndex[n] = o
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
done := make(chan struct{}, 1)
|
||||||
|
pc := newParseContext()
|
||||||
|
go func() {
|
||||||
|
opts.match([]string{"-x"}, &pc)
|
||||||
|
done <- struct{}{}
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
// nop, everything is good
|
||||||
|
case <-time.After(5 * time.Second):
|
||||||
|
t.Fatalf("Timed out after 5 seconds. Infinite loop in optsMatcher.")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,279 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BoolOpt describes a boolean option
|
||||||
|
type BoolOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option
|
||||||
|
EnvVar string
|
||||||
|
// The option's initial value
|
||||||
|
Value bool
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o BoolOpt) value() bool {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringOpt describes a string option
|
||||||
|
type StringOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option
|
||||||
|
EnvVar string
|
||||||
|
// The option's initial value
|
||||||
|
Value string
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o StringOpt) value() string {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntOpt describes an int option
|
||||||
|
type IntOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option
|
||||||
|
EnvVar string
|
||||||
|
// The option's initial value
|
||||||
|
Value int
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o IntOpt) value() int {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// StringsOpt describes a string slice option
|
||||||
|
type StringsOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option.
|
||||||
|
// The env variable should contain a comma separated list of values
|
||||||
|
EnvVar string
|
||||||
|
// The option's initial value
|
||||||
|
Value []string
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o StringsOpt) value() []string {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// IntsOpt describes an int slice option
|
||||||
|
type IntsOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option.
|
||||||
|
// The env variable should contain a comma separated list of values
|
||||||
|
EnvVar string
|
||||||
|
// The option's initial value
|
||||||
|
Value []int
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o IntsOpt) value() []int {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
// VarOpt describes an option where the type and format of the value is controlled by the developer
|
||||||
|
type VarOpt struct {
|
||||||
|
// A space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
// The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
Name string
|
||||||
|
// The option description as will be shown in help messages
|
||||||
|
Desc string
|
||||||
|
// A space separated list of environment variables names to be used to initialize this option
|
||||||
|
EnvVar string
|
||||||
|
// A value implementing the flag.Value type (will hold the final value)
|
||||||
|
Value flag.Value
|
||||||
|
// A boolean to display or not the current value of the option in the help message
|
||||||
|
HideValue bool
|
||||||
|
// Set to true if this option was set by the user (as opposed to being set from env or not set at all)
|
||||||
|
SetByUser *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o VarOpt) value() flag.Value {
|
||||||
|
return o.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
BoolOpt defines a boolean option on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a bool) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) BoolOpt(name string, value bool, desc string) *bool {
|
||||||
|
return c.Bool(BoolOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringOpt defines a string option on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) StringOpt(name string, value string, desc string) *string {
|
||||||
|
return c.String(StringOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntOpt defines an int option on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) IntOpt(name string, value int, desc string) *int {
|
||||||
|
return c.Int(IntOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
StringsOpt defines a string slice option on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to a string slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) StringsOpt(name string, value []string, desc string) *[]string {
|
||||||
|
return c.Strings(StringsOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
IntsOpt defines an int slice option on the command c named `name`, with an initial value of `value` and a description of `desc` which will be used in help messages.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result should be stored in a variable (a pointer to an int slice) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) IntsOpt(name string, value []int, desc string) *[]int {
|
||||||
|
return c.Ints(IntsOpt{
|
||||||
|
Name: name,
|
||||||
|
Value: value,
|
||||||
|
Desc: desc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
VarOpt defines an option where the type and format is controlled by the developer.
|
||||||
|
|
||||||
|
The name is a space separated list of the option names *WITHOUT* the dashes, e.g. `f force` and *NOT* `-f --force`.
|
||||||
|
The one letter names will then be called with a single dash (short option), the others with two (long options).
|
||||||
|
|
||||||
|
|
||||||
|
The result will be stored in the value parameter (a value implementing the flag.Value interface) which will be populated when the app is run and the call arguments get parsed
|
||||||
|
*/
|
||||||
|
func (c *Cmd) VarOpt(name string, value flag.Value, desc string) {
|
||||||
|
c.mkOpt(opt{name: name, desc: desc, value: value})
|
||||||
|
}
|
||||||
|
|
||||||
|
type opt struct {
|
||||||
|
name string
|
||||||
|
desc string
|
||||||
|
envVar string
|
||||||
|
names []string
|
||||||
|
hideValue bool
|
||||||
|
valueSetFromEnv bool
|
||||||
|
valueSetByUser *bool
|
||||||
|
value flag.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opt) isBool() bool {
|
||||||
|
if bf, ok := o.value.(boolValued); ok {
|
||||||
|
return bf.IsBoolFlag()
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *opt) String() string {
|
||||||
|
return fmt.Sprintf("Opt(%v)", o.names)
|
||||||
|
}
|
||||||
|
|
||||||
|
func mkOptStrs(optName string) []string {
|
||||||
|
res := strings.Fields(optName)
|
||||||
|
for i, name := range res {
|
||||||
|
prefix := "-"
|
||||||
|
if len(name) > 1 {
|
||||||
|
prefix = "--"
|
||||||
|
}
|
||||||
|
res[i] = prefix + name
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Cmd) mkOpt(opt opt) {
|
||||||
|
opt.valueSetFromEnv = setFromEnv(opt.value, opt.envVar)
|
||||||
|
|
||||||
|
opt.names = mkOptStrs(opt.name)
|
||||||
|
|
||||||
|
c.options = append(c.options, &opt)
|
||||||
|
for _, name := range opt.names {
|
||||||
|
c.optionsIdx[name] = &opt
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,141 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStringOpt(t *testing.T) {
|
||||||
|
cmd := &Cmd{optionsIdx: map[string]*opt{}}
|
||||||
|
a := cmd.String(StringOpt{Name: "a", Value: "test", Desc: ""})
|
||||||
|
require.Equal(t, "test", *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.String(StringOpt{Name: "b", Value: "test", EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, "test", *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow")
|
||||||
|
b = cmd.String(StringOpt{Name: "b", Value: "test", EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, "mow", *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "cli")
|
||||||
|
os.Setenv("D", "mow")
|
||||||
|
b = cmd.String(StringOpt{Name: "b", Value: "test", EnvVar: "B C D", Desc: ""})
|
||||||
|
require.Equal(t, "cli", *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBoolOpt(t *testing.T) {
|
||||||
|
cmd := &Cmd{optionsIdx: map[string]*opt{}}
|
||||||
|
a := cmd.Bool(BoolOpt{Name: "a", Value: true, Desc: ""})
|
||||||
|
require.True(t, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Bool(BoolOpt{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.False(t, *b)
|
||||||
|
|
||||||
|
trueValues := []string{"1", "true", "TRUE"}
|
||||||
|
for _, tv := range trueValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b = cmd.Bool(BoolOpt{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.True(t, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
falseValues := []string{"0", "false", "FALSE", "xyz"}
|
||||||
|
for _, tv := range falseValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b = cmd.Bool(BoolOpt{Name: "b", Value: false, EnvVar: "B", Desc: ""})
|
||||||
|
require.False(t, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "false")
|
||||||
|
os.Setenv("D", "true")
|
||||||
|
b = cmd.Bool(BoolOpt{Name: "b", Value: true, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.False(t, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntOpt(t *testing.T) {
|
||||||
|
cmd := &Cmd{optionsIdx: map[string]*opt{}}
|
||||||
|
a := cmd.Int(IntOpt{Name: "a", Value: -1, Desc: ""})
|
||||||
|
require.Equal(t, -1, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Int(IntOpt{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, -1, *b)
|
||||||
|
|
||||||
|
goodValues := []int{1, 0, 33}
|
||||||
|
for _, tv := range goodValues {
|
||||||
|
os.Setenv("B", strconv.Itoa(tv))
|
||||||
|
b := cmd.Int(IntOpt{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, tv, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
badValues := []string{"", "b", "q1", "_"}
|
||||||
|
for _, tv := range badValues {
|
||||||
|
os.Setenv("B", tv)
|
||||||
|
b := cmd.Int(IntOpt{Name: "b", Value: -1, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, -1, *b, "env=%s", tv)
|
||||||
|
}
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "42")
|
||||||
|
os.Setenv("D", "666")
|
||||||
|
b = cmd.Int(IntOpt{Name: "b", Value: -1, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.Equal(t, 42, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsOpt(t *testing.T) {
|
||||||
|
cmd := &Cmd{optionsIdx: map[string]*opt{}}
|
||||||
|
v := []string{"test"}
|
||||||
|
a := cmd.Strings(StringsOpt{Name: "a", Value: v, Desc: ""})
|
||||||
|
require.Equal(t, v, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Strings(StringsOpt{Name: "b", Value: v, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, v, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow")
|
||||||
|
b = cmd.Strings(StringsOpt{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []string{"mow"}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "mow, cli")
|
||||||
|
b = cmd.Strings(StringsOpt{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []string{"mow", "cli"}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "test")
|
||||||
|
os.Setenv("D", "xxx")
|
||||||
|
b = cmd.Strings(StringsOpt{Name: "b", Value: nil, EnvVar: "B C D", Desc: ""})
|
||||||
|
require.Equal(t, v, *b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntsOpt(t *testing.T) {
|
||||||
|
cmd := &Cmd{optionsIdx: map[string]*opt{}}
|
||||||
|
vi := []int{42}
|
||||||
|
a := cmd.Ints(IntsOpt{Name: "a", Value: vi, Desc: ""})
|
||||||
|
require.Equal(t, vi, *a)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
b := cmd.Ints(IntsOpt{Name: "b", Value: vi, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, vi, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "666")
|
||||||
|
b = cmd.Ints(IntsOpt{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []int{666}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "1, 2 , 3")
|
||||||
|
b = cmd.Ints(IntsOpt{Name: "b", Value: nil, EnvVar: "B", Desc: ""})
|
||||||
|
require.Equal(t, []int{1, 2, 3}, *b)
|
||||||
|
|
||||||
|
os.Setenv("B", "")
|
||||||
|
os.Setenv("C", "abc")
|
||||||
|
os.Setenv("D", "1, abc")
|
||||||
|
os.Setenv("E", "42")
|
||||||
|
os.Setenv("F", "666")
|
||||||
|
b = cmd.Ints(IntsOpt{Name: "b", Value: nil, EnvVar: "B C D E F", Desc: ""})
|
||||||
|
require.Equal(t, vi, *b)
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,243 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
func uParse(c *Cmd) (*state, error) {
|
||||||
|
tokens, err := uTokenize(c.Spec)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
p := &uParser{cmd: c, tokens: tokens}
|
||||||
|
return p.parse()
|
||||||
|
}
|
||||||
|
|
||||||
|
type uParser struct {
|
||||||
|
cmd *Cmd
|
||||||
|
tokens []*uToken
|
||||||
|
|
||||||
|
tkpos int
|
||||||
|
|
||||||
|
matchedToken *uToken
|
||||||
|
|
||||||
|
rejectOptions bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) parse() (s *state, err error) {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
pos := len(p.cmd.Spec)
|
||||||
|
if !p.eof() {
|
||||||
|
pos = p.token().pos
|
||||||
|
}
|
||||||
|
s = nil
|
||||||
|
switch t, ok := v.(string); ok {
|
||||||
|
case true:
|
||||||
|
err = &parseError{p.cmd.Spec, t, pos}
|
||||||
|
default:
|
||||||
|
panic(v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
err = nil
|
||||||
|
var e *state
|
||||||
|
s, e = p.seq(false)
|
||||||
|
if !p.eof() {
|
||||||
|
s = nil
|
||||||
|
err = &parseError{p.cmd.Spec, "Unexpected input", p.token().pos}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
e.terminal = true
|
||||||
|
s.simplify()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) seq(required bool) (*state, *state) {
|
||||||
|
start := newState(p.cmd)
|
||||||
|
end := start
|
||||||
|
|
||||||
|
appendComp := func(s, e *state) {
|
||||||
|
for _, tr := range s.transitions {
|
||||||
|
end.t(tr.matcher, tr.next)
|
||||||
|
}
|
||||||
|
end = e
|
||||||
|
}
|
||||||
|
|
||||||
|
if required {
|
||||||
|
s, e := p.choice()
|
||||||
|
appendComp(s, e)
|
||||||
|
}
|
||||||
|
for p.canAtom() {
|
||||||
|
s, e := p.choice()
|
||||||
|
appendComp(s, e)
|
||||||
|
}
|
||||||
|
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) choice() (*state, *state) {
|
||||||
|
start, end := newState(p.cmd), newState(p.cmd)
|
||||||
|
|
||||||
|
add := func(s, e *state) {
|
||||||
|
start.t(shortcut, s)
|
||||||
|
e.t(shortcut, end)
|
||||||
|
}
|
||||||
|
|
||||||
|
add(p.atom())
|
||||||
|
for p.found(utChoice) {
|
||||||
|
add(p.atom())
|
||||||
|
}
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) atom() (*state, *state) {
|
||||||
|
start := newState(p.cmd)
|
||||||
|
var end *state
|
||||||
|
switch {
|
||||||
|
case p.eof():
|
||||||
|
panic("Unexpected end of input")
|
||||||
|
case p.found(utPos):
|
||||||
|
name := p.matchedToken.val
|
||||||
|
arg, declared := p.cmd.argsIdx[name]
|
||||||
|
if !declared {
|
||||||
|
p.back()
|
||||||
|
panic(fmt.Sprintf("Undeclared arg %s", name))
|
||||||
|
}
|
||||||
|
end = start.t(arg, newState(p.cmd))
|
||||||
|
case p.found(utOptions):
|
||||||
|
if p.rejectOptions {
|
||||||
|
p.back()
|
||||||
|
panic("No options after --")
|
||||||
|
}
|
||||||
|
end = newState(p.cmd)
|
||||||
|
start.t(optsMatcher{options: p.cmd.options, optionsIndex: p.cmd.optionsIdx}, end)
|
||||||
|
case p.found(utShortOpt):
|
||||||
|
if p.rejectOptions {
|
||||||
|
p.back()
|
||||||
|
panic("No options after --")
|
||||||
|
}
|
||||||
|
name := p.matchedToken.val
|
||||||
|
opt, declared := p.cmd.optionsIdx[name]
|
||||||
|
if !declared {
|
||||||
|
p.back()
|
||||||
|
panic(fmt.Sprintf("Undeclared option %s", name))
|
||||||
|
}
|
||||||
|
end = start.t(&optMatcher{
|
||||||
|
theOne: opt,
|
||||||
|
optionsIdx: p.cmd.optionsIdx,
|
||||||
|
}, newState(p.cmd))
|
||||||
|
p.found(utOptValue)
|
||||||
|
case p.found(utLongOpt):
|
||||||
|
if p.rejectOptions {
|
||||||
|
p.back()
|
||||||
|
panic("No options after --")
|
||||||
|
}
|
||||||
|
name := p.matchedToken.val
|
||||||
|
opt, declared := p.cmd.optionsIdx[name]
|
||||||
|
if !declared {
|
||||||
|
p.back()
|
||||||
|
panic(fmt.Sprintf("Undeclared option %s", name))
|
||||||
|
}
|
||||||
|
end = start.t(&optMatcher{
|
||||||
|
theOne: opt,
|
||||||
|
optionsIdx: p.cmd.optionsIdx,
|
||||||
|
}, newState(p.cmd))
|
||||||
|
p.found(utOptValue)
|
||||||
|
case p.found(utOptSeq):
|
||||||
|
if p.rejectOptions {
|
||||||
|
p.back()
|
||||||
|
panic("No options after --")
|
||||||
|
}
|
||||||
|
end = newState(p.cmd)
|
||||||
|
sq := p.matchedToken.val
|
||||||
|
opts := []*opt{}
|
||||||
|
for i := range sq {
|
||||||
|
sn := sq[i : i+1]
|
||||||
|
opt, declared := p.cmd.optionsIdx["-"+sn]
|
||||||
|
if !declared {
|
||||||
|
p.back()
|
||||||
|
panic(fmt.Sprintf("Undeclared option %s", sn))
|
||||||
|
}
|
||||||
|
opts = append(opts, opt)
|
||||||
|
}
|
||||||
|
start.t(optsMatcher{options: opts, optionsIndex: p.cmd.optionsIdx}, end)
|
||||||
|
case p.found(utOpenPar):
|
||||||
|
start, end = p.seq(true)
|
||||||
|
p.expect(utClosePar)
|
||||||
|
case p.found(utOpenSq):
|
||||||
|
start, end = p.seq(true)
|
||||||
|
start.t(shortcut, end)
|
||||||
|
p.expect(utCloseSq)
|
||||||
|
case p.found(utDoubleDash):
|
||||||
|
p.rejectOptions = true
|
||||||
|
end = start.t(optsEnd, newState(p.cmd))
|
||||||
|
return start, end
|
||||||
|
default:
|
||||||
|
panic("Unexpected input: was expecting a command or a positional argument or an option")
|
||||||
|
}
|
||||||
|
if p.found(utRep) {
|
||||||
|
end.t(shortcut, start)
|
||||||
|
}
|
||||||
|
return start, end
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) canAtom() bool {
|
||||||
|
switch {
|
||||||
|
case p.is(utPos):
|
||||||
|
return true
|
||||||
|
case p.is(utOptions):
|
||||||
|
return true
|
||||||
|
case p.is(utShortOpt):
|
||||||
|
return true
|
||||||
|
case p.is(utLongOpt):
|
||||||
|
return true
|
||||||
|
case p.is(utOptSeq):
|
||||||
|
return true
|
||||||
|
case p.is(utOpenPar):
|
||||||
|
return true
|
||||||
|
case p.is(utOpenSq):
|
||||||
|
return true
|
||||||
|
case p.is(utDoubleDash):
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) found(t uTokenType) bool {
|
||||||
|
if p.is(t) {
|
||||||
|
p.matchedToken = p.token()
|
||||||
|
p.tkpos++
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) is(t uTokenType) bool {
|
||||||
|
if p.eof() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return p.token().typ == t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) expect(t uTokenType) {
|
||||||
|
if !p.found(t) {
|
||||||
|
panic(fmt.Sprintf("Was expecting %v", t))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) back() {
|
||||||
|
p.tkpos--
|
||||||
|
}
|
||||||
|
func (p *uParser) eof() bool {
|
||||||
|
return p.tkpos >= len(p.tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *uParser) token() *uToken {
|
||||||
|
if p.eof() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.tokens[p.tkpos]
|
||||||
|
}
|
|
@ -0,0 +1,223 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uTokenType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
utPos uTokenType = "Pos"
|
||||||
|
utOpenPar uTokenType = "OpenPar"
|
||||||
|
utClosePar uTokenType = "ClosePar"
|
||||||
|
utOpenSq uTokenType = "OpenSq"
|
||||||
|
utCloseSq uTokenType = "CloseSq"
|
||||||
|
utChoice uTokenType = "Choice"
|
||||||
|
utOptions uTokenType = "Options"
|
||||||
|
utRep uTokenType = "Rep"
|
||||||
|
utShortOpt uTokenType = "ShortOpt"
|
||||||
|
utLongOpt uTokenType = "LongOpt"
|
||||||
|
utOptSeq uTokenType = "OptSeq"
|
||||||
|
utOptValue uTokenType = "OptValue"
|
||||||
|
utDoubleDash uTokenType = "DblDash"
|
||||||
|
)
|
||||||
|
|
||||||
|
type uToken struct {
|
||||||
|
typ uTokenType
|
||||||
|
val string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *uToken) String() string {
|
||||||
|
return fmt.Sprintf("%s('%s')@%d", t.typ, t.val, t.pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseError struct {
|
||||||
|
input string
|
||||||
|
msg string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *parseError) ident() string {
|
||||||
|
return strings.Map(func(c rune) rune {
|
||||||
|
switch c {
|
||||||
|
case '\t':
|
||||||
|
return c
|
||||||
|
default:
|
||||||
|
return ' '
|
||||||
|
}
|
||||||
|
}, t.input[:t.pos])
|
||||||
|
}
|
||||||
|
func (t *parseError) Error() string {
|
||||||
|
return fmt.Sprintf("Parse error at position %d:\n%s\n%s^ %s",
|
||||||
|
t.pos, t.input, t.ident(), t.msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uTokenize(usage string) ([]*uToken, *parseError) {
|
||||||
|
pos := 0
|
||||||
|
res := []*uToken{}
|
||||||
|
var (
|
||||||
|
tk = func(t uTokenType, v string) {
|
||||||
|
res = append(res, &uToken{t, v, pos})
|
||||||
|
}
|
||||||
|
|
||||||
|
tkp = func(t uTokenType, v string, p int) {
|
||||||
|
res = append(res, &uToken{t, v, p})
|
||||||
|
}
|
||||||
|
|
||||||
|
err = func(msg string) *parseError {
|
||||||
|
return &parseError{usage, msg, pos}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
eof := len(usage)
|
||||||
|
for pos < eof {
|
||||||
|
switch c := usage[pos]; c {
|
||||||
|
case ' ':
|
||||||
|
pos++
|
||||||
|
case '\t':
|
||||||
|
pos++
|
||||||
|
case '[':
|
||||||
|
tk(utOpenSq, "[")
|
||||||
|
pos++
|
||||||
|
case ']':
|
||||||
|
tk(utCloseSq, "]")
|
||||||
|
pos++
|
||||||
|
case '(':
|
||||||
|
tk(utOpenPar, "(")
|
||||||
|
pos++
|
||||||
|
case ')':
|
||||||
|
tk(utClosePar, ")")
|
||||||
|
pos++
|
||||||
|
case '|':
|
||||||
|
tk(utChoice, "|")
|
||||||
|
pos++
|
||||||
|
case '.':
|
||||||
|
start := pos
|
||||||
|
pos++
|
||||||
|
if pos >= eof || usage[pos] != '.' {
|
||||||
|
return nil, err("Unexpected end of usage, was expecting '..'")
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
if pos >= eof || usage[pos] != '.' {
|
||||||
|
return nil, err("Unexpected end of usage, was expecting '.'")
|
||||||
|
}
|
||||||
|
tkp(utRep, "...", start)
|
||||||
|
pos++
|
||||||
|
case '-':
|
||||||
|
start := pos
|
||||||
|
pos++
|
||||||
|
if pos >= eof {
|
||||||
|
return nil, err("Unexpected end of usage, was expecting an option name")
|
||||||
|
}
|
||||||
|
|
||||||
|
switch o := usage[pos]; {
|
||||||
|
case isLetter(o):
|
||||||
|
pos++
|
||||||
|
for ; pos < eof; pos++ {
|
||||||
|
ok := isLetter(usage[pos])
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
typ := utShortOpt
|
||||||
|
if pos-start > 2 {
|
||||||
|
typ = utOptSeq
|
||||||
|
start++
|
||||||
|
}
|
||||||
|
opt := usage[start:pos]
|
||||||
|
tkp(typ, opt, start)
|
||||||
|
if pos < eof && usage[pos] == '-' {
|
||||||
|
return nil, err("Invalid syntax")
|
||||||
|
}
|
||||||
|
case o == '-':
|
||||||
|
pos++
|
||||||
|
if pos == eof || usage[pos] == ' ' {
|
||||||
|
tkp(utDoubleDash, "--", start)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for pos0 := pos; pos < eof; pos++ {
|
||||||
|
ok := isOkLongOpt(usage[pos], pos == pos0)
|
||||||
|
if !ok {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
opt := usage[start:pos]
|
||||||
|
if len(opt) == 2 {
|
||||||
|
return nil, err("Was expecting a long option name")
|
||||||
|
}
|
||||||
|
tkp(utLongOpt, opt, start)
|
||||||
|
}
|
||||||
|
|
||||||
|
case '=':
|
||||||
|
start := pos
|
||||||
|
pos++
|
||||||
|
if pos >= eof || usage[pos] != '<' {
|
||||||
|
return nil, err("Unexpected end of usage, was expecting '=<'")
|
||||||
|
}
|
||||||
|
closed := false
|
||||||
|
for ; pos < eof; pos++ {
|
||||||
|
closed = usage[pos] == '>'
|
||||||
|
if closed {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !closed {
|
||||||
|
return nil, err("Unclosed option value")
|
||||||
|
}
|
||||||
|
if pos-start == 2 {
|
||||||
|
return nil, err("Was expecting an option value")
|
||||||
|
}
|
||||||
|
pos++
|
||||||
|
value := usage[start:pos]
|
||||||
|
|
||||||
|
tkp(utOptValue, value, start)
|
||||||
|
|
||||||
|
default:
|
||||||
|
switch {
|
||||||
|
case isUppercase(c):
|
||||||
|
start := pos
|
||||||
|
for pos = pos + 1; pos < eof; pos++ {
|
||||||
|
if !isOkPos(usage[pos]) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s := usage[start:pos]
|
||||||
|
typ := utPos
|
||||||
|
if s == "OPTIONS" {
|
||||||
|
typ = utOptions
|
||||||
|
}
|
||||||
|
tkp(typ, s, start)
|
||||||
|
default:
|
||||||
|
return nil, err("Unexpected input")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLowercase(c uint8) bool {
|
||||||
|
return c >= 'a' && c <= 'z'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isUppercase(c uint8) bool {
|
||||||
|
return c >= 'A' && c <= 'Z'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isOkPos(c uint8) bool {
|
||||||
|
return isUppercase(c) || isDigit(c) || c == '_'
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLetter(c uint8) bool {
|
||||||
|
return isLowercase(c) || isUppercase(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isDigit(c uint8) bool {
|
||||||
|
return c >= '0' && c <= '9'
|
||||||
|
}
|
||||||
|
func isOkLongOpt(c uint8, first bool) bool {
|
||||||
|
return isLetter(c) || isDigit(c) || c == '_' || (!first && c == '-')
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestUTokenize(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
usage string
|
||||||
|
expected []*uToken
|
||||||
|
}{
|
||||||
|
{"OPTIONS", []*uToken{{utOptions, "OPTIONS", 0}}},
|
||||||
|
|
||||||
|
{"XOPTIONS", []*uToken{{utPos, "XOPTIONS", 0}}},
|
||||||
|
{"OPTIONSX", []*uToken{{utPos, "OPTIONSX", 0}}},
|
||||||
|
{"ARG", []*uToken{{utPos, "ARG", 0}}},
|
||||||
|
{"ARG42", []*uToken{{utPos, "ARG42", 0}}},
|
||||||
|
{"ARG_EXTRA", []*uToken{{utPos, "ARG_EXTRA", 0}}},
|
||||||
|
|
||||||
|
{"ARG1 ARG2", []*uToken{{utPos, "ARG1", 0}, {utPos, "ARG2", 5}}},
|
||||||
|
{"ARG1 ARG2", []*uToken{{utPos, "ARG1", 0}, {utPos, "ARG2", 6}}},
|
||||||
|
|
||||||
|
{"[ARG]", []*uToken{{utOpenSq, "[", 0}, {utPos, "ARG", 1}, {utCloseSq, "]", 4}}},
|
||||||
|
{"[ ARG ]", []*uToken{{utOpenSq, "[", 0}, {utPos, "ARG", 2}, {utCloseSq, "]", 6}}},
|
||||||
|
{"ARG [ARG2 ]", []*uToken{{utPos, "ARG", 0}, {utOpenSq, "[", 4}, {utPos, "ARG2", 5}, {utCloseSq, "]", 10}}},
|
||||||
|
{"ARG [ ARG2]", []*uToken{{utPos, "ARG", 0}, {utOpenSq, "[", 4}, {utPos, "ARG2", 6}, {utCloseSq, "]", 10}}},
|
||||||
|
|
||||||
|
{"...", []*uToken{{utRep, "...", 0}}},
|
||||||
|
{"ARG...", []*uToken{{utPos, "ARG", 0}, {utRep, "...", 3}}},
|
||||||
|
{"ARG ...", []*uToken{{utPos, "ARG", 0}, {utRep, "...", 4}}},
|
||||||
|
{"[ARG...]", []*uToken{{utOpenSq, "[", 0}, {utPos, "ARG", 1}, {utRep, "...", 4}, {utCloseSq, "]", 7}}},
|
||||||
|
|
||||||
|
{"|", []*uToken{{utChoice, "|", 0}}},
|
||||||
|
{"ARG|ARG2", []*uToken{{utPos, "ARG", 0}, {utChoice, "|", 3}, {utPos, "ARG2", 4}}},
|
||||||
|
{"ARG |ARG2", []*uToken{{utPos, "ARG", 0}, {utChoice, "|", 4}, {utPos, "ARG2", 5}}},
|
||||||
|
{"ARG| ARG2", []*uToken{{utPos, "ARG", 0}, {utChoice, "|", 3}, {utPos, "ARG2", 5}}},
|
||||||
|
|
||||||
|
{"[OPTIONS]", []*uToken{{utOpenSq, "[", 0}, {utOptions, "OPTIONS", 1}, {utCloseSq, "]", 8}}},
|
||||||
|
|
||||||
|
{"-p", []*uToken{{utShortOpt, "-p", 0}}},
|
||||||
|
{"-X", []*uToken{{utShortOpt, "-X", 0}}},
|
||||||
|
|
||||||
|
{"--force", []*uToken{{utLongOpt, "--force", 0}}},
|
||||||
|
{"--sig-proxy", []*uToken{{utLongOpt, "--sig-proxy", 0}}},
|
||||||
|
|
||||||
|
{"-aBc", []*uToken{{utOptSeq, "aBc", 1}}},
|
||||||
|
{"--", []*uToken{{utDoubleDash, "--", 0}}},
|
||||||
|
{"=<bla>", []*uToken{{utOptValue, "=<bla>", 0}}},
|
||||||
|
{"=<bla-bla>", []*uToken{{utOptValue, "=<bla-bla>", 0}}},
|
||||||
|
{"=<bla--bla>", []*uToken{{utOptValue, "=<bla--bla>", 0}}},
|
||||||
|
{"-p=<file-path>", []*uToken{{utShortOpt, "-p", 0}, {utOptValue, "=<file-path>", 2}}},
|
||||||
|
{"--path=<absolute-path>", []*uToken{{utLongOpt, "--path", 0}, {utOptValue, "=<absolute-path>", 6}}},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Logf("test %s", c.usage)
|
||||||
|
tks, err := uTokenize(c.usage)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("[Tokenize '%s']: Unexpected error: %v", c.usage, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("actual: %v\n", tks)
|
||||||
|
if len(tks) != len(c.expected) {
|
||||||
|
t.Errorf("[Tokenize '%s']: token count mismatch:\n\tExpected: %v\n\tActual : %v", c.usage, c.expected, tks)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, actual := range tks {
|
||||||
|
expected := c.expected[i]
|
||||||
|
switch {
|
||||||
|
case actual.typ != expected.typ:
|
||||||
|
t.Errorf("[Tokenize '%s']: token type mismatch:\n\tExpected: %v\n\tActual : %v", c.usage, expected, actual)
|
||||||
|
case actual.val != expected.val:
|
||||||
|
t.Errorf("[Tokenize '%s']: token text mismatch:\n\tExpected: %v\n\tActual : %v", c.usage, expected, actual)
|
||||||
|
case actual.pos != expected.pos:
|
||||||
|
t.Errorf("[Tokenize '%s']: token pos mismatch:\n\tExpected: %v\n\tActual : %v", c.usage, expected, actual)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUTokenizeErrors(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
usage string
|
||||||
|
pos int
|
||||||
|
}{
|
||||||
|
{"-", 1},
|
||||||
|
{"---x", 2},
|
||||||
|
{"-x-", 2},
|
||||||
|
|
||||||
|
{"=", 1},
|
||||||
|
{"=<", 2},
|
||||||
|
{"=<dsdf", 6},
|
||||||
|
{"=<>", 2},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
t.Logf("test %s", c.usage)
|
||||||
|
tks, err := uTokenize(c.usage)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Tokenize('%s') should have failed, instead got %v", c.usage, tks)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
t.Logf("Got expected error %v", err)
|
||||||
|
if err.pos != c.pos {
|
||||||
|
t.Errorf("[Tokenize '%s']: error pos mismatch:\n\tExpected: %v\n\tActual : %v", c.usage, c.pos, err.pos)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,45 @@
|
||||||
|
|
||||||
|
Usage: app [-bdsuikqs] BOOL1 [STR1] INT3... COMMAND [arg...]
|
||||||
|
|
||||||
|
App Desc
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
BOOL1 Bool Argument 1 (env $BOOL1)
|
||||||
|
BOOL2 Bool Argument 2 (default true)
|
||||||
|
BOOL3 Bool Argument 3 (env $BOOL3)
|
||||||
|
STR1 String Argument 1 (env $STR1)
|
||||||
|
STR2 String Argument 2 (env $STR2) (default "a value")
|
||||||
|
STR3 String Argument 3 (env $STR3)
|
||||||
|
INT1 Int Argument 1 (env $INT1) (default 0)
|
||||||
|
INT2 Int Argument 2 (env $INT2) (default 1)
|
||||||
|
INT3 Int Argument 3 (env $INT3)
|
||||||
|
STRS1 Strings Argument 1 (env $STRS1)
|
||||||
|
STRS2 (env $STRS2) (default ["value1", "value2"])
|
||||||
|
STRS3 Strings Argument 3 (env $STRS3)
|
||||||
|
INTS1 Ints Argument 1 (env $INTS1)
|
||||||
|
INTS2 Ints Argument 2 (env $INTS2) (default [1, 2, 3])
|
||||||
|
INTS3 Ints Argument 3 (env $INTS3)
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-b, --bool1 Bool Option 1 (env $BOOL1)
|
||||||
|
--bool2 Bool Option 2 (default true)
|
||||||
|
-d Bool Option 3 (env $BOOL3)
|
||||||
|
-s, --str1 String Option 1 (env $STR1)
|
||||||
|
--str2 String Option 2 (default "a value")
|
||||||
|
-u String Option 3 (env $STR3)
|
||||||
|
-i, --int1 (env $INT1, $ALIAS_INT1) (default 0)
|
||||||
|
--int2 Int Option 2 (env $INT2) (default 1)
|
||||||
|
-k Int Option 3 (env $INT3)
|
||||||
|
-x, --strs1 Strings Option 1 (env $STRS1)
|
||||||
|
--strs2 Strings Option 2 (env $STRS2) (default ["value1", "value2"])
|
||||||
|
-z Strings Option 3 (env $STRS3)
|
||||||
|
-q, --ints1 Ints Option 1 (env $INTS1)
|
||||||
|
--ints2 Ints Option 2 (env $INTS2) (default [1, 2, 3])
|
||||||
|
-s Ints Option 3 (env $INTS3)
|
||||||
|
|
||||||
|
Commands:
|
||||||
|
command1 command1 description
|
||||||
|
command2 command2 description
|
||||||
|
command3 command3 description
|
||||||
|
|
||||||
|
Run 'app COMMAND --help' for more information on a command.
|
|
@ -0,0 +1,10 @@
|
||||||
|
|
||||||
|
Usage: app [-o] ARG
|
||||||
|
|
||||||
|
Longer App Desc
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
ARG Argument
|
||||||
|
|
||||||
|
Options:
|
||||||
|
-o, --opt Option
|
|
@ -0,0 +1,66 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func setFromEnv(into flag.Value, envVars string) bool {
|
||||||
|
multiValued, isMulti := into.(multiValued)
|
||||||
|
|
||||||
|
if len(envVars) > 0 {
|
||||||
|
for _, rev := range strings.Split(envVars, " ") {
|
||||||
|
ev := strings.TrimSpace(rev)
|
||||||
|
if len(ev) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
v := os.Getenv(ev)
|
||||||
|
if len(v) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !isMulti {
|
||||||
|
if err := into.Set(v); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
vs := strings.Split(v, ",")
|
||||||
|
if err := setMultivalued(multiValued, vs); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func setMultivalued(into multiValued, values []string) error {
|
||||||
|
into.Clear()
|
||||||
|
|
||||||
|
for _, v := range values {
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if err := into.Set(v); err != nil {
|
||||||
|
into.Clear()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinStrings(parts ...string) string {
|
||||||
|
res := ""
|
||||||
|
for _, part := range parts {
|
||||||
|
s := strings.TrimSpace(part)
|
||||||
|
if s == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if res != "" {
|
||||||
|
res += " "
|
||||||
|
}
|
||||||
|
res += part
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestJoinStrings(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
input []string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{nil, ""},
|
||||||
|
{[]string{""}, ""},
|
||||||
|
{[]string{" "}, ""},
|
||||||
|
{[]string{"\t"}, ""},
|
||||||
|
{[]string{"", " ", "\t"}, ""},
|
||||||
|
{[]string{"a"}, "a"},
|
||||||
|
{[]string{"a", "b c"}, "a b c"},
|
||||||
|
{[]string{"", "a", " ", "b", "\t"}, "a b"},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("Testing %#v", cas.input)
|
||||||
|
actual := joinStrings(cas.input...)
|
||||||
|
|
||||||
|
require.Equal(t, cas.expected, actual)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,203 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type boolValued interface {
|
||||||
|
flag.Value
|
||||||
|
IsBoolFlag() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type multiValued interface {
|
||||||
|
flag.Value
|
||||||
|
Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultValued interface {
|
||||||
|
IsDefault() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
/* BOOL */
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
type boolValue bool
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ flag.Value = newBoolValue(new(bool), false)
|
||||||
|
_ boolValued = newBoolValue(new(bool), false)
|
||||||
|
_ defaultValued = newBoolValue(new(bool), false)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newBoolValue(into *bool, v bool) *boolValue {
|
||||||
|
*into = v
|
||||||
|
return (*boolValue)(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *boolValue) Set(s string) error {
|
||||||
|
b, err := strconv.ParseBool(s)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*bo = boolValue(b)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *boolValue) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *boolValue) String() string {
|
||||||
|
return fmt.Sprintf("%v", *bo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (bo *boolValue) IsDefault() bool {
|
||||||
|
return !bool(*bo)
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
/* STRING */
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
type stringValue string
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ flag.Value = newStringValue(new(string), "")
|
||||||
|
_ defaultValued = newStringValue(new(string), "")
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStringValue(into *string, v string) *stringValue {
|
||||||
|
*into = v
|
||||||
|
return (*stringValue)(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringValue) Set(s string) error {
|
||||||
|
*sa = stringValue(s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringValue) String() string {
|
||||||
|
return fmt.Sprintf("%#v", *sa)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringValue) IsDefault() bool {
|
||||||
|
return string(*sa) == ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
/* INT */
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
type intValue int
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ flag.Value = newIntValue(new(int), 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newIntValue(into *int, v int) *intValue {
|
||||||
|
*into = v
|
||||||
|
return (*intValue)(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intValue) Set(s string) error {
|
||||||
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*ia = intValue(int(i))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intValue) String() string {
|
||||||
|
return fmt.Sprintf("%v", *ia)
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
/* STRINGS */
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
// Strings describes a string slice argument
|
||||||
|
type stringsValue []string
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ flag.Value = newStringsValue(new([]string), nil)
|
||||||
|
_ multiValued = newStringsValue(new([]string), nil)
|
||||||
|
_ defaultValued = newStringsValue(new([]string), nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newStringsValue(into *[]string, v []string) *stringsValue {
|
||||||
|
*into = v
|
||||||
|
return (*stringsValue)(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringsValue) Set(s string) error {
|
||||||
|
*sa = append(*sa, s)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringsValue) String() string {
|
||||||
|
res := "["
|
||||||
|
for idx, s := range *sa {
|
||||||
|
if idx > 0 {
|
||||||
|
res += ", "
|
||||||
|
}
|
||||||
|
res += fmt.Sprintf("%#v", s)
|
||||||
|
}
|
||||||
|
return res + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringsValue) Clear() {
|
||||||
|
*sa = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sa *stringsValue) IsDefault() bool {
|
||||||
|
return len(*sa) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
/******************************************************************************/
|
||||||
|
/* INTS */
|
||||||
|
/******************************************************************************/
|
||||||
|
|
||||||
|
type intsValue []int
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ flag.Value = newIntsValue(new([]int), nil)
|
||||||
|
_ multiValued = newIntsValue(new([]int), nil)
|
||||||
|
_ defaultValued = newIntsValue(new([]int), nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
func newIntsValue(into *[]int, v []int) *intsValue {
|
||||||
|
*into = v
|
||||||
|
return (*intsValue)(into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intsValue) Set(s string) error {
|
||||||
|
i, err := strconv.ParseInt(s, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*ia = append(*ia, int(i))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intsValue) String() string {
|
||||||
|
res := "["
|
||||||
|
for idx, s := range *ia {
|
||||||
|
if idx > 0 {
|
||||||
|
res += ", "
|
||||||
|
}
|
||||||
|
res += fmt.Sprintf("%v", s)
|
||||||
|
}
|
||||||
|
return res + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intsValue) Clear() {
|
||||||
|
*ia = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ia *intsValue) IsDefault() bool {
|
||||||
|
return len(*ia) == 0
|
||||||
|
}
|
|
@ -0,0 +1,137 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBoolParam(t *testing.T) {
|
||||||
|
var into bool
|
||||||
|
|
||||||
|
param := newBoolValue(&into, false)
|
||||||
|
|
||||||
|
require.True(t, param.IsBoolFlag())
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
err bool
|
||||||
|
result bool
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{"true", false, true, "true"},
|
||||||
|
{"false", false, false, "false"},
|
||||||
|
{"123", true, false, ""},
|
||||||
|
{"", true, false, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("testing with %q", cas.input)
|
||||||
|
|
||||||
|
err := param.Set(cas.input)
|
||||||
|
|
||||||
|
if cas.err {
|
||||||
|
require.Errorf(t, err, "value %q should have returned an error", cas.input)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, cas.result, into)
|
||||||
|
require.Equal(t, cas.string, param.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringParam(t *testing.T) {
|
||||||
|
var into string
|
||||||
|
|
||||||
|
param := newStringValue(&into, "")
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{"a", `"a"`},
|
||||||
|
{"", `""`},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("testing with %q", cas.input)
|
||||||
|
|
||||||
|
err := param.Set(cas.input)
|
||||||
|
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, cas.input, into)
|
||||||
|
require.Equal(t, cas.string, param.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntParam(t *testing.T) {
|
||||||
|
var into int
|
||||||
|
|
||||||
|
param := newIntValue(&into, 0)
|
||||||
|
|
||||||
|
cases := []struct {
|
||||||
|
input string
|
||||||
|
err bool
|
||||||
|
result int
|
||||||
|
string string
|
||||||
|
}{
|
||||||
|
{"12", false, 12, "12"},
|
||||||
|
{"0", false, 0, "0"},
|
||||||
|
{"01", false, 1, "1"},
|
||||||
|
{"", true, 0, ""},
|
||||||
|
{"abc", true, 0, ""},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cas := range cases {
|
||||||
|
t.Logf("testing with %q", cas.input)
|
||||||
|
|
||||||
|
err := param.Set(cas.input)
|
||||||
|
|
||||||
|
if cas.err {
|
||||||
|
require.Errorf(t, err, "value %q should have returned an error", cas.input)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t, cas.result, into)
|
||||||
|
require.Equal(t, cas.string, param.String())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStringsParam(t *testing.T) {
|
||||||
|
into := []string{}
|
||||||
|
param := newStringsValue(&into, nil)
|
||||||
|
|
||||||
|
param.Set("a")
|
||||||
|
param.Set("b")
|
||||||
|
|
||||||
|
require.Equal(t, []string{"a", "b"}, into)
|
||||||
|
require.Equal(t, `["a", "b"]`, param.String())
|
||||||
|
|
||||||
|
param.Clear()
|
||||||
|
|
||||||
|
require.Empty(t, into)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIntsParam(t *testing.T) {
|
||||||
|
into := []int{}
|
||||||
|
param := newIntsValue(&into, nil)
|
||||||
|
|
||||||
|
err := param.Set("1")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
err = param.Set("2")
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, []int{1, 2}, into)
|
||||||
|
|
||||||
|
require.Equal(t, `[1, 2]`, param.String())
|
||||||
|
|
||||||
|
err = param.Set("c")
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Equal(t, []int{1, 2}, into)
|
||||||
|
|
||||||
|
param.Clear()
|
||||||
|
|
||||||
|
require.Empty(t, into)
|
||||||
|
}
|
|
@ -0,0 +1,162 @@
|
||||||
|
package cli
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Counter
|
||||||
|
type Counter int
|
||||||
|
|
||||||
|
func (d *Counter) Set(v string) error {
|
||||||
|
*d++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Counter) String() string {
|
||||||
|
return fmt.Sprintf("%d", *d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Counter) IsBoolFlag() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Duration
|
||||||
|
type Duration time.Duration
|
||||||
|
|
||||||
|
func (d *Duration) Set(v string) error {
|
||||||
|
parsed, err := time.ParseDuration(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*d = Duration(parsed)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Duration) String() string {
|
||||||
|
duration := time.Duration(*d)
|
||||||
|
return duration.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
type Percent []float64
|
||||||
|
|
||||||
|
func parsePercent(v string) (float64, error) {
|
||||||
|
var d int
|
||||||
|
_, err := fmt.Sscanf(v, "%d%%", &d)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return float64(d) / 100, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Percent) Set(v string) error {
|
||||||
|
f, err := parsePercent(v)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
*p = append(*p, f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Percent) Clear() {
|
||||||
|
*p = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Percent) String() string {
|
||||||
|
res := "["
|
||||||
|
for idx, p := range *p {
|
||||||
|
if idx > 0 {
|
||||||
|
res += ", "
|
||||||
|
}
|
||||||
|
res += fmt.Sprintf("%.0f%%", p*100)
|
||||||
|
}
|
||||||
|
return res + "]"
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVar(t *testing.T) {
|
||||||
|
value := Counter(0)
|
||||||
|
duration := Duration(0)
|
||||||
|
percents := Percent{}
|
||||||
|
|
||||||
|
app := App("var", "")
|
||||||
|
app.Spec = "-v... DURATION PERCENT..."
|
||||||
|
|
||||||
|
app.VarOpt("v", &value, "")
|
||||||
|
app.VarArg("DURATION", &duration, "")
|
||||||
|
app.VarArg("PERCENT", &percents, "")
|
||||||
|
|
||||||
|
ex := false
|
||||||
|
app.Action = func() {
|
||||||
|
ex = true
|
||||||
|
}
|
||||||
|
app.Run([]string{"cp", "-vvv", "1h", "10%", "5%"})
|
||||||
|
|
||||||
|
require.Equal(t, Counter(3), value)
|
||||||
|
require.Equal(t, Duration(1*time.Hour), duration)
|
||||||
|
require.Equal(t, Percent([]float64{0.1, 0.05}), percents)
|
||||||
|
|
||||||
|
require.True(t, ex, "Exec wasn't called")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVarFromEnv(t *testing.T) {
|
||||||
|
os.Setenv("MOWCLI_DURATION", "1h2m3s")
|
||||||
|
os.Setenv("MOWCLI_ARG_PERCENTS", "25%, 1%")
|
||||||
|
os.Setenv("MOWCLI_OPT_PERCENTS", "90%, 42%")
|
||||||
|
|
||||||
|
duration := Duration(0)
|
||||||
|
argPercents := Percent{}
|
||||||
|
optPercents := Percent{}
|
||||||
|
|
||||||
|
app := App("var", "")
|
||||||
|
app.Spec = "-p... DURATION PERCENT..."
|
||||||
|
|
||||||
|
app.Var(VarArg{
|
||||||
|
Name: "DURATION",
|
||||||
|
Value: &duration,
|
||||||
|
EnvVar: "MOWCLI_DURATION",
|
||||||
|
})
|
||||||
|
app.Var(VarArg{
|
||||||
|
Name: "PERCENT",
|
||||||
|
Value: &argPercents,
|
||||||
|
EnvVar: "MOWCLI_ARG_PERCENTS",
|
||||||
|
})
|
||||||
|
app.Var(VarOpt{
|
||||||
|
Name: "p",
|
||||||
|
Value: &optPercents,
|
||||||
|
EnvVar: "MOWCLI_OPT_PERCENTS",
|
||||||
|
})
|
||||||
|
|
||||||
|
require.Equal(t, Duration(1*time.Hour+2*time.Minute+3*time.Second), duration)
|
||||||
|
|
||||||
|
require.Equal(t, Percent([]float64{0.25, 0.01}), argPercents)
|
||||||
|
require.Equal(t, Percent([]float64{0.9, 0.42}), optPercents)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestVarOverrideEnv(t *testing.T) {
|
||||||
|
os.Setenv("MOWCLI_PERCENTS", "25%, 1%")
|
||||||
|
|
||||||
|
percents := Percent{}
|
||||||
|
|
||||||
|
app := App("var", "")
|
||||||
|
app.Spec = "PERCENT..."
|
||||||
|
app.Var(VarArg{
|
||||||
|
Name: "PERCENT",
|
||||||
|
Value: &percents,
|
||||||
|
EnvVar: "MOWCLI_PERCENTS",
|
||||||
|
})
|
||||||
|
|
||||||
|
ex := false
|
||||||
|
app.Action = func() {
|
||||||
|
ex = true
|
||||||
|
require.Equal(t, Percent([]float64{0, 0.99}), percents)
|
||||||
|
}
|
||||||
|
|
||||||
|
app.Run([]string{"var", "0%", "99%"})
|
||||||
|
|
||||||
|
require.True(t, ex, "Action should have been called")
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -repotoken xnXqRGwgW3SXIguzxf90ZSK1GPYZPaGrw
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2016 Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,48 @@
|
||||||
|
# go-colorable
|
||||||
|
|
||||||
|
[![Godoc Reference](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable)
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/mattn/go-colorable/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-colorable?branch=master)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable)
|
||||||
|
|
||||||
|
Colorable writer for windows.
|
||||||
|
|
||||||
|
For example, most of logger packages doesn't show colors on windows. (I know we can do it with ansicon. But I don't want.)
|
||||||
|
This package is possible to handle escape sequence for ansi color on windows.
|
||||||
|
|
||||||
|
## Too Bad!
|
||||||
|
|
||||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/bad.png)
|
||||||
|
|
||||||
|
|
||||||
|
## So Good!
|
||||||
|
|
||||||
|
![](https://raw.githubusercontent.com/mattn/go-colorable/gh-pages/good.png)
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||||
|
logrus.SetOutput(colorable.NewColorableStdout())
|
||||||
|
|
||||||
|
logrus.Info("succeeded")
|
||||||
|
logrus.Warn("not correct")
|
||||||
|
logrus.Error("something error")
|
||||||
|
logrus.Fatal("panic")
|
||||||
|
```
|
||||||
|
|
||||||
|
You can compile above code on non-windows OSs.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/mattn/go-colorable
|
||||||
|
```
|
||||||
|
|
||||||
|
# License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
# Author
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto (a.k.a mattn)
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
stdOut := bufio.NewWriter(colorable.NewColorableStdout())
|
||||||
|
|
||||||
|
fmt.Fprint(stdOut, "\x1B[3GMove to 3rd Column\n")
|
||||||
|
fmt.Fprint(stdOut, "\x1B[1;2HMove to 2nd Column on 1st Line\n")
|
||||||
|
stdOut.Flush()
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/mattn/go-colorable"
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
logrus.SetFormatter(&logrus.TextFormatter{ForceColors: true})
|
||||||
|
logrus.SetOutput(colorable.NewColorableStdout())
|
||||||
|
|
||||||
|
logrus.Info("succeeded")
|
||||||
|
logrus.Warn("not correct")
|
||||||
|
logrus.Error("something error")
|
||||||
|
logrus.Fatal("panic")
|
||||||
|
}
|
|
@ -0,0 +1,14 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
. "github.com/mattn/go-colorable"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
out := NewColorableStdout()
|
||||||
|
fmt.Fprint(out, "\x1B]0;TITLE Changed\007(See title and hit any key)")
|
||||||
|
var c [1]byte
|
||||||
|
os.Stdin.Read(c[:])
|
||||||
|
}
|
|
@ -0,0 +1,29 @@
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewColorable return new instance of Writer which handle escape sequence.
|
||||||
|
func NewColorable(file *os.File) io.Writer {
|
||||||
|
if file == nil {
|
||||||
|
panic("nil passed instead of *os.File to NewColorable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||||
|
func NewColorableStdout() io.Writer {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||||
|
func NewColorableStderr() io.Writer {
|
||||||
|
return os.Stderr
|
||||||
|
}
|
|
@ -0,0 +1,30 @@
|
||||||
|
// +build !windows
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
_ "github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewColorable return new instance of Writer which handle escape sequence.
|
||||||
|
func NewColorable(file *os.File) io.Writer {
|
||||||
|
if file == nil {
|
||||||
|
panic("nil passed instead of *os.File to NewColorable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||||
|
func NewColorableStdout() io.Writer {
|
||||||
|
return os.Stdout
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||||
|
func NewColorableStderr() io.Writer {
|
||||||
|
return os.Stderr
|
||||||
|
}
|
|
@ -0,0 +1,83 @@
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// checkEncoding checks that colorable is output encoding agnostic as long as
|
||||||
|
// the encoding is a superset of ASCII. This implies that one byte not part of
|
||||||
|
// an ANSI sequence must give exactly one byte in output
|
||||||
|
func checkEncoding(t *testing.T, data []byte) {
|
||||||
|
// Send non-UTF8 data to colorable
|
||||||
|
b := bytes.NewBuffer(make([]byte, 0, 10))
|
||||||
|
if b.Len() != 0 {
|
||||||
|
t.FailNow()
|
||||||
|
}
|
||||||
|
// TODO move colorable wrapping outside the test
|
||||||
|
c := NewNonColorable(b)
|
||||||
|
c.Write(data)
|
||||||
|
if b.Len() != len(data) {
|
||||||
|
t.Fatalf("%d bytes expected, got %d", len(data), b.Len())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEncoding(t *testing.T) {
|
||||||
|
checkEncoding(t, []byte{}) // Empty
|
||||||
|
checkEncoding(t, []byte(`abc`)) // "abc"
|
||||||
|
checkEncoding(t, []byte(`é`)) // "é" in UTF-8
|
||||||
|
checkEncoding(t, []byte{233}) // 'é' in Latin-1
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonColorable(t *testing.T) {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
want := "hello"
|
||||||
|
NewNonColorable(&buf).Write([]byte("\x1b[0m" + want + "\x1b[2J"))
|
||||||
|
got := buf.String()
|
||||||
|
if got != "hello" {
|
||||||
|
t.Fatalf("want %q but %q", want, got)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.Reset()
|
||||||
|
NewNonColorable(&buf).Write([]byte("\x1b["))
|
||||||
|
got = buf.String()
|
||||||
|
if got != "" {
|
||||||
|
t.Fatalf("want %q but %q", "", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNonColorableNil(t *testing.T) {
|
||||||
|
paniced := false
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
recover()
|
||||||
|
paniced = true
|
||||||
|
}()
|
||||||
|
NewNonColorable(nil)
|
||||||
|
NewColorable(nil)
|
||||||
|
}()
|
||||||
|
|
||||||
|
if !paniced {
|
||||||
|
t.Fatalf("should panic")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestColorable(t *testing.T) {
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
t.Skipf("skip this test on windows")
|
||||||
|
}
|
||||||
|
_, ok := NewColorableStdout().(*os.File)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should os.Stdout on UNIX")
|
||||||
|
}
|
||||||
|
_, ok = NewColorableStderr().(*os.File)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should os.Stdout on UNIX")
|
||||||
|
}
|
||||||
|
_, ok = NewColorable(os.Stdout).(*os.File)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should os.Stdout on UNIX")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,884 @@
|
||||||
|
// +build windows
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
"math"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
foregroundBlue = 0x1
|
||||||
|
foregroundGreen = 0x2
|
||||||
|
foregroundRed = 0x4
|
||||||
|
foregroundIntensity = 0x8
|
||||||
|
foregroundMask = (foregroundRed | foregroundBlue | foregroundGreen | foregroundIntensity)
|
||||||
|
backgroundBlue = 0x10
|
||||||
|
backgroundGreen = 0x20
|
||||||
|
backgroundRed = 0x40
|
||||||
|
backgroundIntensity = 0x80
|
||||||
|
backgroundMask = (backgroundRed | backgroundBlue | backgroundGreen | backgroundIntensity)
|
||||||
|
)
|
||||||
|
|
||||||
|
type wchar uint16
|
||||||
|
type short int16
|
||||||
|
type dword uint32
|
||||||
|
type word uint16
|
||||||
|
|
||||||
|
type coord struct {
|
||||||
|
x short
|
||||||
|
y short
|
||||||
|
}
|
||||||
|
|
||||||
|
type smallRect struct {
|
||||||
|
left short
|
||||||
|
top short
|
||||||
|
right short
|
||||||
|
bottom short
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleScreenBufferInfo struct {
|
||||||
|
size coord
|
||||||
|
cursorPosition coord
|
||||||
|
attributes word
|
||||||
|
window smallRect
|
||||||
|
maximumWindowSize coord
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleCursorInfo struct {
|
||||||
|
size dword
|
||||||
|
visible int32
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||||
|
procSetConsoleTextAttribute = kernel32.NewProc("SetConsoleTextAttribute")
|
||||||
|
procSetConsoleCursorPosition = kernel32.NewProc("SetConsoleCursorPosition")
|
||||||
|
procFillConsoleOutputCharacter = kernel32.NewProc("FillConsoleOutputCharacterW")
|
||||||
|
procFillConsoleOutputAttribute = kernel32.NewProc("FillConsoleOutputAttribute")
|
||||||
|
procGetConsoleCursorInfo = kernel32.NewProc("GetConsoleCursorInfo")
|
||||||
|
procSetConsoleCursorInfo = kernel32.NewProc("SetConsoleCursorInfo")
|
||||||
|
procSetConsoleTitle = kernel32.NewProc("SetConsoleTitleW")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Writer provide colorable Writer to the console
|
||||||
|
type Writer struct {
|
||||||
|
out io.Writer
|
||||||
|
handle syscall.Handle
|
||||||
|
oldattr word
|
||||||
|
oldpos coord
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorable return new instance of Writer which handle escape sequence from File.
|
||||||
|
func NewColorable(file *os.File) io.Writer {
|
||||||
|
if file == nil {
|
||||||
|
panic("nil passed instead of *os.File to NewColorable()")
|
||||||
|
}
|
||||||
|
|
||||||
|
if isatty.IsTerminal(file.Fd()) {
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
handle := syscall.Handle(file.Fd())
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
return &Writer{out: file, handle: handle, oldattr: csbi.attributes, oldpos: coord{0, 0}}
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStdout return new instance of Writer which handle escape sequence for stdout.
|
||||||
|
func NewColorableStdout() io.Writer {
|
||||||
|
return NewColorable(os.Stdout)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewColorableStderr return new instance of Writer which handle escape sequence for stderr.
|
||||||
|
func NewColorableStderr() io.Writer {
|
||||||
|
return NewColorable(os.Stderr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var color256 = map[int]int{
|
||||||
|
0: 0x000000,
|
||||||
|
1: 0x800000,
|
||||||
|
2: 0x008000,
|
||||||
|
3: 0x808000,
|
||||||
|
4: 0x000080,
|
||||||
|
5: 0x800080,
|
||||||
|
6: 0x008080,
|
||||||
|
7: 0xc0c0c0,
|
||||||
|
8: 0x808080,
|
||||||
|
9: 0xff0000,
|
||||||
|
10: 0x00ff00,
|
||||||
|
11: 0xffff00,
|
||||||
|
12: 0x0000ff,
|
||||||
|
13: 0xff00ff,
|
||||||
|
14: 0x00ffff,
|
||||||
|
15: 0xffffff,
|
||||||
|
16: 0x000000,
|
||||||
|
17: 0x00005f,
|
||||||
|
18: 0x000087,
|
||||||
|
19: 0x0000af,
|
||||||
|
20: 0x0000d7,
|
||||||
|
21: 0x0000ff,
|
||||||
|
22: 0x005f00,
|
||||||
|
23: 0x005f5f,
|
||||||
|
24: 0x005f87,
|
||||||
|
25: 0x005faf,
|
||||||
|
26: 0x005fd7,
|
||||||
|
27: 0x005fff,
|
||||||
|
28: 0x008700,
|
||||||
|
29: 0x00875f,
|
||||||
|
30: 0x008787,
|
||||||
|
31: 0x0087af,
|
||||||
|
32: 0x0087d7,
|
||||||
|
33: 0x0087ff,
|
||||||
|
34: 0x00af00,
|
||||||
|
35: 0x00af5f,
|
||||||
|
36: 0x00af87,
|
||||||
|
37: 0x00afaf,
|
||||||
|
38: 0x00afd7,
|
||||||
|
39: 0x00afff,
|
||||||
|
40: 0x00d700,
|
||||||
|
41: 0x00d75f,
|
||||||
|
42: 0x00d787,
|
||||||
|
43: 0x00d7af,
|
||||||
|
44: 0x00d7d7,
|
||||||
|
45: 0x00d7ff,
|
||||||
|
46: 0x00ff00,
|
||||||
|
47: 0x00ff5f,
|
||||||
|
48: 0x00ff87,
|
||||||
|
49: 0x00ffaf,
|
||||||
|
50: 0x00ffd7,
|
||||||
|
51: 0x00ffff,
|
||||||
|
52: 0x5f0000,
|
||||||
|
53: 0x5f005f,
|
||||||
|
54: 0x5f0087,
|
||||||
|
55: 0x5f00af,
|
||||||
|
56: 0x5f00d7,
|
||||||
|
57: 0x5f00ff,
|
||||||
|
58: 0x5f5f00,
|
||||||
|
59: 0x5f5f5f,
|
||||||
|
60: 0x5f5f87,
|
||||||
|
61: 0x5f5faf,
|
||||||
|
62: 0x5f5fd7,
|
||||||
|
63: 0x5f5fff,
|
||||||
|
64: 0x5f8700,
|
||||||
|
65: 0x5f875f,
|
||||||
|
66: 0x5f8787,
|
||||||
|
67: 0x5f87af,
|
||||||
|
68: 0x5f87d7,
|
||||||
|
69: 0x5f87ff,
|
||||||
|
70: 0x5faf00,
|
||||||
|
71: 0x5faf5f,
|
||||||
|
72: 0x5faf87,
|
||||||
|
73: 0x5fafaf,
|
||||||
|
74: 0x5fafd7,
|
||||||
|
75: 0x5fafff,
|
||||||
|
76: 0x5fd700,
|
||||||
|
77: 0x5fd75f,
|
||||||
|
78: 0x5fd787,
|
||||||
|
79: 0x5fd7af,
|
||||||
|
80: 0x5fd7d7,
|
||||||
|
81: 0x5fd7ff,
|
||||||
|
82: 0x5fff00,
|
||||||
|
83: 0x5fff5f,
|
||||||
|
84: 0x5fff87,
|
||||||
|
85: 0x5fffaf,
|
||||||
|
86: 0x5fffd7,
|
||||||
|
87: 0x5fffff,
|
||||||
|
88: 0x870000,
|
||||||
|
89: 0x87005f,
|
||||||
|
90: 0x870087,
|
||||||
|
91: 0x8700af,
|
||||||
|
92: 0x8700d7,
|
||||||
|
93: 0x8700ff,
|
||||||
|
94: 0x875f00,
|
||||||
|
95: 0x875f5f,
|
||||||
|
96: 0x875f87,
|
||||||
|
97: 0x875faf,
|
||||||
|
98: 0x875fd7,
|
||||||
|
99: 0x875fff,
|
||||||
|
100: 0x878700,
|
||||||
|
101: 0x87875f,
|
||||||
|
102: 0x878787,
|
||||||
|
103: 0x8787af,
|
||||||
|
104: 0x8787d7,
|
||||||
|
105: 0x8787ff,
|
||||||
|
106: 0x87af00,
|
||||||
|
107: 0x87af5f,
|
||||||
|
108: 0x87af87,
|
||||||
|
109: 0x87afaf,
|
||||||
|
110: 0x87afd7,
|
||||||
|
111: 0x87afff,
|
||||||
|
112: 0x87d700,
|
||||||
|
113: 0x87d75f,
|
||||||
|
114: 0x87d787,
|
||||||
|
115: 0x87d7af,
|
||||||
|
116: 0x87d7d7,
|
||||||
|
117: 0x87d7ff,
|
||||||
|
118: 0x87ff00,
|
||||||
|
119: 0x87ff5f,
|
||||||
|
120: 0x87ff87,
|
||||||
|
121: 0x87ffaf,
|
||||||
|
122: 0x87ffd7,
|
||||||
|
123: 0x87ffff,
|
||||||
|
124: 0xaf0000,
|
||||||
|
125: 0xaf005f,
|
||||||
|
126: 0xaf0087,
|
||||||
|
127: 0xaf00af,
|
||||||
|
128: 0xaf00d7,
|
||||||
|
129: 0xaf00ff,
|
||||||
|
130: 0xaf5f00,
|
||||||
|
131: 0xaf5f5f,
|
||||||
|
132: 0xaf5f87,
|
||||||
|
133: 0xaf5faf,
|
||||||
|
134: 0xaf5fd7,
|
||||||
|
135: 0xaf5fff,
|
||||||
|
136: 0xaf8700,
|
||||||
|
137: 0xaf875f,
|
||||||
|
138: 0xaf8787,
|
||||||
|
139: 0xaf87af,
|
||||||
|
140: 0xaf87d7,
|
||||||
|
141: 0xaf87ff,
|
||||||
|
142: 0xafaf00,
|
||||||
|
143: 0xafaf5f,
|
||||||
|
144: 0xafaf87,
|
||||||
|
145: 0xafafaf,
|
||||||
|
146: 0xafafd7,
|
||||||
|
147: 0xafafff,
|
||||||
|
148: 0xafd700,
|
||||||
|
149: 0xafd75f,
|
||||||
|
150: 0xafd787,
|
||||||
|
151: 0xafd7af,
|
||||||
|
152: 0xafd7d7,
|
||||||
|
153: 0xafd7ff,
|
||||||
|
154: 0xafff00,
|
||||||
|
155: 0xafff5f,
|
||||||
|
156: 0xafff87,
|
||||||
|
157: 0xafffaf,
|
||||||
|
158: 0xafffd7,
|
||||||
|
159: 0xafffff,
|
||||||
|
160: 0xd70000,
|
||||||
|
161: 0xd7005f,
|
||||||
|
162: 0xd70087,
|
||||||
|
163: 0xd700af,
|
||||||
|
164: 0xd700d7,
|
||||||
|
165: 0xd700ff,
|
||||||
|
166: 0xd75f00,
|
||||||
|
167: 0xd75f5f,
|
||||||
|
168: 0xd75f87,
|
||||||
|
169: 0xd75faf,
|
||||||
|
170: 0xd75fd7,
|
||||||
|
171: 0xd75fff,
|
||||||
|
172: 0xd78700,
|
||||||
|
173: 0xd7875f,
|
||||||
|
174: 0xd78787,
|
||||||
|
175: 0xd787af,
|
||||||
|
176: 0xd787d7,
|
||||||
|
177: 0xd787ff,
|
||||||
|
178: 0xd7af00,
|
||||||
|
179: 0xd7af5f,
|
||||||
|
180: 0xd7af87,
|
||||||
|
181: 0xd7afaf,
|
||||||
|
182: 0xd7afd7,
|
||||||
|
183: 0xd7afff,
|
||||||
|
184: 0xd7d700,
|
||||||
|
185: 0xd7d75f,
|
||||||
|
186: 0xd7d787,
|
||||||
|
187: 0xd7d7af,
|
||||||
|
188: 0xd7d7d7,
|
||||||
|
189: 0xd7d7ff,
|
||||||
|
190: 0xd7ff00,
|
||||||
|
191: 0xd7ff5f,
|
||||||
|
192: 0xd7ff87,
|
||||||
|
193: 0xd7ffaf,
|
||||||
|
194: 0xd7ffd7,
|
||||||
|
195: 0xd7ffff,
|
||||||
|
196: 0xff0000,
|
||||||
|
197: 0xff005f,
|
||||||
|
198: 0xff0087,
|
||||||
|
199: 0xff00af,
|
||||||
|
200: 0xff00d7,
|
||||||
|
201: 0xff00ff,
|
||||||
|
202: 0xff5f00,
|
||||||
|
203: 0xff5f5f,
|
||||||
|
204: 0xff5f87,
|
||||||
|
205: 0xff5faf,
|
||||||
|
206: 0xff5fd7,
|
||||||
|
207: 0xff5fff,
|
||||||
|
208: 0xff8700,
|
||||||
|
209: 0xff875f,
|
||||||
|
210: 0xff8787,
|
||||||
|
211: 0xff87af,
|
||||||
|
212: 0xff87d7,
|
||||||
|
213: 0xff87ff,
|
||||||
|
214: 0xffaf00,
|
||||||
|
215: 0xffaf5f,
|
||||||
|
216: 0xffaf87,
|
||||||
|
217: 0xffafaf,
|
||||||
|
218: 0xffafd7,
|
||||||
|
219: 0xffafff,
|
||||||
|
220: 0xffd700,
|
||||||
|
221: 0xffd75f,
|
||||||
|
222: 0xffd787,
|
||||||
|
223: 0xffd7af,
|
||||||
|
224: 0xffd7d7,
|
||||||
|
225: 0xffd7ff,
|
||||||
|
226: 0xffff00,
|
||||||
|
227: 0xffff5f,
|
||||||
|
228: 0xffff87,
|
||||||
|
229: 0xffffaf,
|
||||||
|
230: 0xffffd7,
|
||||||
|
231: 0xffffff,
|
||||||
|
232: 0x080808,
|
||||||
|
233: 0x121212,
|
||||||
|
234: 0x1c1c1c,
|
||||||
|
235: 0x262626,
|
||||||
|
236: 0x303030,
|
||||||
|
237: 0x3a3a3a,
|
||||||
|
238: 0x444444,
|
||||||
|
239: 0x4e4e4e,
|
||||||
|
240: 0x585858,
|
||||||
|
241: 0x626262,
|
||||||
|
242: 0x6c6c6c,
|
||||||
|
243: 0x767676,
|
||||||
|
244: 0x808080,
|
||||||
|
245: 0x8a8a8a,
|
||||||
|
246: 0x949494,
|
||||||
|
247: 0x9e9e9e,
|
||||||
|
248: 0xa8a8a8,
|
||||||
|
249: 0xb2b2b2,
|
||||||
|
250: 0xbcbcbc,
|
||||||
|
251: 0xc6c6c6,
|
||||||
|
252: 0xd0d0d0,
|
||||||
|
253: 0xdadada,
|
||||||
|
254: 0xe4e4e4,
|
||||||
|
255: 0xeeeeee,
|
||||||
|
}
|
||||||
|
|
||||||
|
// `\033]0;TITLESTR\007`
|
||||||
|
func doTitleSequence(er *bytes.Reader) error {
|
||||||
|
var c byte
|
||||||
|
var err error
|
||||||
|
|
||||||
|
c, err = er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c != '0' && c != '2' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
c, err = er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c != ';' {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
title := make([]byte, 0, 80)
|
||||||
|
for {
|
||||||
|
c, err = er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if c == 0x07 || c == '\n' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
title = append(title, c)
|
||||||
|
}
|
||||||
|
if len(title) > 0 {
|
||||||
|
title8, err := syscall.UTF16PtrFromString(string(title))
|
||||||
|
if err == nil {
|
||||||
|
procSetConsoleTitle.Call(uintptr(unsafe.Pointer(title8)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write write data on console
|
||||||
|
func (w *Writer) Write(data []byte) (n int, err error) {
|
||||||
|
var csbi consoleScreenBufferInfo
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
|
||||||
|
er := bytes.NewReader(data)
|
||||||
|
var bw [1]byte
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
c1, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if c1 != 0x1b {
|
||||||
|
bw[0] = c1
|
||||||
|
w.out.Write(bw[:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c2, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
|
||||||
|
if c2 == ']' {
|
||||||
|
if err := doTitleSequence(er); err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if c2 != 0x5b {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var m byte
|
||||||
|
for {
|
||||||
|
c, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||||
|
m = c
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.Write([]byte(string(c)))
|
||||||
|
}
|
||||||
|
|
||||||
|
switch m {
|
||||||
|
case 'A':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.y -= short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'B':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.y += short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'C':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.x += short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'D':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.x -= short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'E':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.x = 0
|
||||||
|
csbi.cursorPosition.y += short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'F':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.x = 0
|
||||||
|
csbi.cursorPosition.y -= short(n)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'G':
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
csbi.cursorPosition.x = short(n - 1)
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'H', 'f':
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
token := strings.Split(buf.String(), ";")
|
||||||
|
switch len(token) {
|
||||||
|
case 1:
|
||||||
|
n1, err := strconv.Atoi(token[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
csbi.cursorPosition.y = short(n1 - 1)
|
||||||
|
case 2:
|
||||||
|
n1, err := strconv.Atoi(token[0])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n2, err := strconv.Atoi(token[1])
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
csbi.cursorPosition.x = short(n2 - 1)
|
||||||
|
csbi.cursorPosition.y = short(n1 - 1)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
csbi.cursorPosition.y = 0
|
||||||
|
}
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&csbi.cursorPosition)))
|
||||||
|
case 'J':
|
||||||
|
n := 0
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var count, written dword
|
||||||
|
var cursor coord
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
cursor = coord{x: csbi.cursorPosition.x, y: csbi.cursorPosition.y}
|
||||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
|
||||||
|
case 1:
|
||||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top}
|
||||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.window.top-csbi.cursorPosition.y)*csbi.size.x)
|
||||||
|
case 2:
|
||||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top}
|
||||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x + (csbi.size.y-csbi.cursorPosition.y)*csbi.size.x)
|
||||||
|
}
|
||||||
|
procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||||
|
procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||||
|
case 'K':
|
||||||
|
n := 0
|
||||||
|
if buf.Len() > 0 {
|
||||||
|
n, err = strconv.Atoi(buf.String())
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
var cursor coord
|
||||||
|
var count, written dword
|
||||||
|
switch n {
|
||||||
|
case 0:
|
||||||
|
cursor = coord{x: csbi.cursorPosition.x + 1, y: csbi.cursorPosition.y}
|
||||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x - 1)
|
||||||
|
case 1:
|
||||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
|
||||||
|
count = dword(csbi.size.x - csbi.cursorPosition.x)
|
||||||
|
case 2:
|
||||||
|
cursor = coord{x: csbi.window.left, y: csbi.window.top + csbi.cursorPosition.y}
|
||||||
|
count = dword(csbi.size.x)
|
||||||
|
}
|
||||||
|
procFillConsoleOutputCharacter.Call(uintptr(w.handle), uintptr(' '), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||||
|
procFillConsoleOutputAttribute.Call(uintptr(w.handle), uintptr(csbi.attributes), uintptr(count), *(*uintptr)(unsafe.Pointer(&cursor)), uintptr(unsafe.Pointer(&written)))
|
||||||
|
case 'm':
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
attr := csbi.attributes
|
||||||
|
cs := buf.String()
|
||||||
|
if cs == "" {
|
||||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(w.oldattr))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
token := strings.Split(cs, ";")
|
||||||
|
for i := 0; i < len(token); i++ {
|
||||||
|
ns := token[i]
|
||||||
|
if n, err = strconv.Atoi(ns); err == nil {
|
||||||
|
switch {
|
||||||
|
case n == 0 || n == 100:
|
||||||
|
attr = w.oldattr
|
||||||
|
case 1 <= n && n <= 5:
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
case n == 7:
|
||||||
|
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
|
||||||
|
case n == 22 || n == 25:
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
case n == 27:
|
||||||
|
attr = ((attr & foregroundMask) << 4) | ((attr & backgroundMask) >> 4)
|
||||||
|
case 30 <= n && n <= 37:
|
||||||
|
attr &= backgroundMask
|
||||||
|
if (n-30)&1 != 0 {
|
||||||
|
attr |= foregroundRed
|
||||||
|
}
|
||||||
|
if (n-30)&2 != 0 {
|
||||||
|
attr |= foregroundGreen
|
||||||
|
}
|
||||||
|
if (n-30)&4 != 0 {
|
||||||
|
attr |= foregroundBlue
|
||||||
|
}
|
||||||
|
case n == 38: // set foreground color.
|
||||||
|
if i < len(token)-2 && (token[i+1] == "5" || token[i+1] == "05") {
|
||||||
|
if n256, err := strconv.Atoi(token[i+2]); err == nil {
|
||||||
|
if n256foreAttr == nil {
|
||||||
|
n256setup()
|
||||||
|
}
|
||||||
|
attr &= backgroundMask
|
||||||
|
attr |= n256foreAttr[n256]
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attr = attr & (w.oldattr & backgroundMask)
|
||||||
|
}
|
||||||
|
case n == 39: // reset foreground color.
|
||||||
|
attr &= backgroundMask
|
||||||
|
attr |= w.oldattr & foregroundMask
|
||||||
|
case 40 <= n && n <= 47:
|
||||||
|
attr &= foregroundMask
|
||||||
|
if (n-40)&1 != 0 {
|
||||||
|
attr |= backgroundRed
|
||||||
|
}
|
||||||
|
if (n-40)&2 != 0 {
|
||||||
|
attr |= backgroundGreen
|
||||||
|
}
|
||||||
|
if (n-40)&4 != 0 {
|
||||||
|
attr |= backgroundBlue
|
||||||
|
}
|
||||||
|
case n == 48: // set background color.
|
||||||
|
if i < len(token)-2 && token[i+1] == "5" {
|
||||||
|
if n256, err := strconv.Atoi(token[i+2]); err == nil {
|
||||||
|
if n256backAttr == nil {
|
||||||
|
n256setup()
|
||||||
|
}
|
||||||
|
attr &= foregroundMask
|
||||||
|
attr |= n256backAttr[n256]
|
||||||
|
i += 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
attr = attr & (w.oldattr & foregroundMask)
|
||||||
|
}
|
||||||
|
case n == 49: // reset foreground color.
|
||||||
|
attr &= foregroundMask
|
||||||
|
attr |= w.oldattr & backgroundMask
|
||||||
|
case 90 <= n && n <= 97:
|
||||||
|
attr = (attr & backgroundMask)
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
if (n-90)&1 != 0 {
|
||||||
|
attr |= foregroundRed
|
||||||
|
}
|
||||||
|
if (n-90)&2 != 0 {
|
||||||
|
attr |= foregroundGreen
|
||||||
|
}
|
||||||
|
if (n-90)&4 != 0 {
|
||||||
|
attr |= foregroundBlue
|
||||||
|
}
|
||||||
|
case 100 <= n && n <= 107:
|
||||||
|
attr = (attr & foregroundMask)
|
||||||
|
attr |= backgroundIntensity
|
||||||
|
if (n-100)&1 != 0 {
|
||||||
|
attr |= backgroundRed
|
||||||
|
}
|
||||||
|
if (n-100)&2 != 0 {
|
||||||
|
attr |= backgroundGreen
|
||||||
|
}
|
||||||
|
if (n-100)&4 != 0 {
|
||||||
|
attr |= backgroundBlue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
procSetConsoleTextAttribute.Call(uintptr(w.handle), uintptr(attr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'h':
|
||||||
|
var ci consoleCursorInfo
|
||||||
|
cs := buf.String()
|
||||||
|
if cs == "5>" {
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
ci.visible = 0
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
} else if cs == "?25" {
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
ci.visible = 1
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
}
|
||||||
|
case 'l':
|
||||||
|
var ci consoleCursorInfo
|
||||||
|
cs := buf.String()
|
||||||
|
if cs == "5>" {
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
ci.visible = 1
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
} else if cs == "?25" {
|
||||||
|
procGetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
ci.visible = 0
|
||||||
|
procSetConsoleCursorInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&ci)))
|
||||||
|
}
|
||||||
|
case 's':
|
||||||
|
procGetConsoleScreenBufferInfo.Call(uintptr(w.handle), uintptr(unsafe.Pointer(&csbi)))
|
||||||
|
w.oldpos = csbi.cursorPosition
|
||||||
|
case 'u':
|
||||||
|
procSetConsoleCursorPosition.Call(uintptr(w.handle), *(*uintptr)(unsafe.Pointer(&w.oldpos)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(data), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type consoleColor struct {
|
||||||
|
rgb int
|
||||||
|
red bool
|
||||||
|
green bool
|
||||||
|
blue bool
|
||||||
|
intensity bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c consoleColor) foregroundAttr() (attr word) {
|
||||||
|
if c.red {
|
||||||
|
attr |= foregroundRed
|
||||||
|
}
|
||||||
|
if c.green {
|
||||||
|
attr |= foregroundGreen
|
||||||
|
}
|
||||||
|
if c.blue {
|
||||||
|
attr |= foregroundBlue
|
||||||
|
}
|
||||||
|
if c.intensity {
|
||||||
|
attr |= foregroundIntensity
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c consoleColor) backgroundAttr() (attr word) {
|
||||||
|
if c.red {
|
||||||
|
attr |= backgroundRed
|
||||||
|
}
|
||||||
|
if c.green {
|
||||||
|
attr |= backgroundGreen
|
||||||
|
}
|
||||||
|
if c.blue {
|
||||||
|
attr |= backgroundBlue
|
||||||
|
}
|
||||||
|
if c.intensity {
|
||||||
|
attr |= backgroundIntensity
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var color16 = []consoleColor{
|
||||||
|
{0x000000, false, false, false, false},
|
||||||
|
{0x000080, false, false, true, false},
|
||||||
|
{0x008000, false, true, false, false},
|
||||||
|
{0x008080, false, true, true, false},
|
||||||
|
{0x800000, true, false, false, false},
|
||||||
|
{0x800080, true, false, true, false},
|
||||||
|
{0x808000, true, true, false, false},
|
||||||
|
{0xc0c0c0, true, true, true, false},
|
||||||
|
{0x808080, false, false, false, true},
|
||||||
|
{0x0000ff, false, false, true, true},
|
||||||
|
{0x00ff00, false, true, false, true},
|
||||||
|
{0x00ffff, false, true, true, true},
|
||||||
|
{0xff0000, true, false, false, true},
|
||||||
|
{0xff00ff, true, false, true, true},
|
||||||
|
{0xffff00, true, true, false, true},
|
||||||
|
{0xffffff, true, true, true, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
type hsv struct {
|
||||||
|
h, s, v float32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a hsv) dist(b hsv) float32 {
|
||||||
|
dh := a.h - b.h
|
||||||
|
switch {
|
||||||
|
case dh > 0.5:
|
||||||
|
dh = 1 - dh
|
||||||
|
case dh < -0.5:
|
||||||
|
dh = -1 - dh
|
||||||
|
}
|
||||||
|
ds := a.s - b.s
|
||||||
|
dv := a.v - b.v
|
||||||
|
return float32(math.Sqrt(float64(dh*dh + ds*ds + dv*dv)))
|
||||||
|
}
|
||||||
|
|
||||||
|
func toHSV(rgb int) hsv {
|
||||||
|
r, g, b := float32((rgb&0xFF0000)>>16)/256.0,
|
||||||
|
float32((rgb&0x00FF00)>>8)/256.0,
|
||||||
|
float32(rgb&0x0000FF)/256.0
|
||||||
|
min, max := minmax3f(r, g, b)
|
||||||
|
h := max - min
|
||||||
|
if h > 0 {
|
||||||
|
if max == r {
|
||||||
|
h = (g - b) / h
|
||||||
|
if h < 0 {
|
||||||
|
h += 6
|
||||||
|
}
|
||||||
|
} else if max == g {
|
||||||
|
h = 2 + (b-r)/h
|
||||||
|
} else {
|
||||||
|
h = 4 + (r-g)/h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
h /= 6.0
|
||||||
|
s := max - min
|
||||||
|
if max != 0 {
|
||||||
|
s /= max
|
||||||
|
}
|
||||||
|
v := max
|
||||||
|
return hsv{h: h, s: s, v: v}
|
||||||
|
}
|
||||||
|
|
||||||
|
type hsvTable []hsv
|
||||||
|
|
||||||
|
func toHSVTable(rgbTable []consoleColor) hsvTable {
|
||||||
|
t := make(hsvTable, len(rgbTable))
|
||||||
|
for i, c := range rgbTable {
|
||||||
|
t[i] = toHSV(c.rgb)
|
||||||
|
}
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t hsvTable) find(rgb int) consoleColor {
|
||||||
|
hsv := toHSV(rgb)
|
||||||
|
n := 7
|
||||||
|
l := float32(5.0)
|
||||||
|
for i, p := range t {
|
||||||
|
d := hsv.dist(p)
|
||||||
|
if d < l {
|
||||||
|
l, n = d, i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return color16[n]
|
||||||
|
}
|
||||||
|
|
||||||
|
func minmax3f(a, b, c float32) (min, max float32) {
|
||||||
|
if a < b {
|
||||||
|
if b < c {
|
||||||
|
return a, c
|
||||||
|
} else if a < c {
|
||||||
|
return a, b
|
||||||
|
} else {
|
||||||
|
return c, b
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if a < c {
|
||||||
|
return b, c
|
||||||
|
} else if b < c {
|
||||||
|
return b, a
|
||||||
|
} else {
|
||||||
|
return c, a
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var n256foreAttr []word
|
||||||
|
var n256backAttr []word
|
||||||
|
|
||||||
|
func n256setup() {
|
||||||
|
n256foreAttr = make([]word, 256)
|
||||||
|
n256backAttr = make([]word, 256)
|
||||||
|
t := toHSVTable(color16)
|
||||||
|
for i, rgb := range color256 {
|
||||||
|
c := t.find(rgb)
|
||||||
|
n256foreAttr[i] = c.foregroundAttr()
|
||||||
|
n256backAttr[i] = c.backgroundAttr()
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,55 @@
|
||||||
|
package colorable
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"io"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NonColorable hold writer but remove escape sequence.
|
||||||
|
type NonColorable struct {
|
||||||
|
out io.Writer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewNonColorable return new instance of Writer which remove escape sequence from Writer.
|
||||||
|
func NewNonColorable(w io.Writer) io.Writer {
|
||||||
|
return &NonColorable{out: w}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write write data on console
|
||||||
|
func (w *NonColorable) Write(data []byte) (n int, err error) {
|
||||||
|
er := bytes.NewReader(data)
|
||||||
|
var bw [1]byte
|
||||||
|
loop:
|
||||||
|
for {
|
||||||
|
c1, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if c1 != 0x1b {
|
||||||
|
bw[0] = c1
|
||||||
|
w.out.Write(bw[:])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
c2, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if c2 != 0x5b {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
for {
|
||||||
|
c, err := er.ReadByte()
|
||||||
|
if err != nil {
|
||||||
|
break loop
|
||||||
|
}
|
||||||
|
if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
buf.Write([]byte(string(c)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(data), nil
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
language: go
|
||||||
|
go:
|
||||||
|
- tip
|
||||||
|
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -repotoken 3gHdORO5k5ziZcWMBxnd9LrMZaJs8m9x5
|
|
@ -0,0 +1,9 @@
|
||||||
|
Copyright (c) Yasuhiro MATSUMOTO <mattn.jp@gmail.com>
|
||||||
|
|
||||||
|
MIT License (Expat)
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
@ -0,0 +1,50 @@
|
||||||
|
# go-isatty
|
||||||
|
|
||||||
|
[![Godoc Reference](https://godoc.org/github.com/mattn/go-isatty?status.svg)](http://godoc.org/github.com/mattn/go-isatty)
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-isatty.svg?branch=master)](https://travis-ci.org/mattn/go-isatty)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/github/mattn/go-isatty/badge.svg?branch=master)](https://coveralls.io/github/mattn/go-isatty?branch=master)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/mattn/go-isatty)](https://goreportcard.com/report/mattn/go-isatty)
|
||||||
|
|
||||||
|
isatty for golang
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```go
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
fmt.Println("Is Terminal")
|
||||||
|
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||||
|
fmt.Println("Is Cygwin/MSYS2 Terminal")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Is Not Terminal")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
$ go get github.com/mattn/go-isatty
|
||||||
|
```
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
|
|
||||||
|
## Author
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto (a.k.a mattn)
|
||||||
|
|
||||||
|
## Thanks
|
||||||
|
|
||||||
|
* k-takata: base idea for IsCygwinTerminal
|
||||||
|
|
||||||
|
https://github.com/k-takata/go-iscygpty
|
|
@ -0,0 +1,2 @@
|
||||||
|
// Package isatty implements interface to isatty
|
||||||
|
package isatty
|
|
@ -0,0 +1,18 @@
|
||||||
|
package isatty_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-isatty"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Example() {
|
||||||
|
if isatty.IsTerminal(os.Stdout.Fd()) {
|
||||||
|
fmt.Println("Is Terminal")
|
||||||
|
} else if isatty.IsCygwinTerminal(os.Stdout.Fd()) {
|
||||||
|
fmt.Println("Is Cygwin/MSYS2 Terminal")
|
||||||
|
} else {
|
||||||
|
fmt.Println("Is Not Terminal")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// +build appengine
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
// IsTerminal returns true if the file descriptor is terminal which
|
||||||
|
// is always false on on appengine classic which is a sandboxed PaaS.
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||||
|
// terminal. This is also always false on this environment.
|
||||||
|
func IsCygwinTerminal(fd uintptr) bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// +build darwin freebsd openbsd netbsd dragonfly
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TIOCGETA
|
||||||
|
|
||||||
|
// IsTerminal return true if the file descriptor is terminal.
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
var termios syscall.Termios
|
||||||
|
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||||
|
return err == 0
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
// +build linux
|
||||||
|
// +build !appengine,!ppc64,!ppc64le
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TCGETS
|
||||||
|
|
||||||
|
// IsTerminal return true if the file descriptor is terminal.
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
var termios syscall.Termios
|
||||||
|
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||||
|
return err == 0
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// +build linux
|
||||||
|
// +build ppc64 ppc64le
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
|
syscall "golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ioctlReadTermios = syscall.TCGETS
|
||||||
|
|
||||||
|
// IsTerminal return true if the file descriptor is terminal.
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
var termios syscall.Termios
|
||||||
|
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, fd, ioctlReadTermios, uintptr(unsafe.Pointer(&termios)), 0, 0, 0)
|
||||||
|
return err == 0
|
||||||
|
}
|
|
@ -0,0 +1,10 @@
|
||||||
|
// +build !windows
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||||
|
// terminal. This is also always false on this environment.
|
||||||
|
func IsCygwinTerminal(fd uintptr) bool {
|
||||||
|
return false
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTerminal(t *testing.T) {
|
||||||
|
// test for non-panic
|
||||||
|
IsTerminal(os.Stdout.Fd())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCygwinPipeName(t *testing.T) {
|
||||||
|
if IsCygwinTerminal(os.Stdout.Fd()) {
|
||||||
|
t.Fatal("should be false always")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,16 @@
|
||||||
|
// +build solaris
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"golang.org/x/sys/unix"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||||
|
// see: http://src.illumos.org/source/xref/illumos-gate/usr/src/lib/libbc/libc/gen/common/isatty.c
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
var termio unix.Termio
|
||||||
|
err := unix.IoctlSetTermio(int(fd), unix.TCGETA, &termio)
|
||||||
|
return err == nil
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
// +build windows
|
||||||
|
// +build !appengine
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"unicode/utf16"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
fileNameInfo uintptr = 2
|
||||||
|
fileTypePipe = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||||
|
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||||
|
procGetFileInformationByHandleEx = kernel32.NewProc("GetFileInformationByHandleEx")
|
||||||
|
procGetFileType = kernel32.NewProc("GetFileType")
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Check if GetFileInformationByHandleEx is available.
|
||||||
|
if procGetFileInformationByHandleEx.Find() != nil {
|
||||||
|
procGetFileInformationByHandleEx = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsTerminal return true if the file descriptor is terminal.
|
||||||
|
func IsTerminal(fd uintptr) bool {
|
||||||
|
var st uint32
|
||||||
|
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, fd, uintptr(unsafe.Pointer(&st)), 0)
|
||||||
|
return r != 0 && e == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check pipe name is used for cygwin/msys2 pty.
|
||||||
|
// Cygwin/MSYS2 PTY has a name like:
|
||||||
|
// \{cygwin,msys}-XXXXXXXXXXXXXXXX-ptyN-{from,to}-master
|
||||||
|
func isCygwinPipeName(name string) bool {
|
||||||
|
token := strings.Split(name, "-")
|
||||||
|
if len(token) < 5 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token[0] != `\msys` && token[0] != `\cygwin` {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token[1] == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasPrefix(token[2], "pty") {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token[3] != `from` && token[3] != `to` {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if token[4] != "master" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCygwinTerminal() return true if the file descriptor is a cygwin or msys2
|
||||||
|
// terminal.
|
||||||
|
func IsCygwinTerminal(fd uintptr) bool {
|
||||||
|
if procGetFileInformationByHandleEx == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cygwin/msys's pty is a pipe.
|
||||||
|
ft, _, e := syscall.Syscall(procGetFileType.Addr(), 1, fd, 0, 0)
|
||||||
|
if ft != fileTypePipe || e != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf [2 + syscall.MAX_PATH]uint16
|
||||||
|
r, _, e := syscall.Syscall6(procGetFileInformationByHandleEx.Addr(),
|
||||||
|
4, fd, fileNameInfo, uintptr(unsafe.Pointer(&buf)),
|
||||||
|
uintptr(len(buf)*2), 0, 0)
|
||||||
|
if r == 0 || e != 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
l := *(*uint32)(unsafe.Pointer(&buf))
|
||||||
|
return isCygwinPipeName(string(utf16.Decode(buf[2 : 2+l/2])))
|
||||||
|
}
|
|
@ -0,0 +1,35 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package isatty
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCygwinPipeName(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
result bool
|
||||||
|
}{
|
||||||
|
{``, false},
|
||||||
|
{`\msys-`, false},
|
||||||
|
{`\cygwin-----`, false},
|
||||||
|
{`\msys-x-PTY5-pty1-from-master`, false},
|
||||||
|
{`\cygwin-x-PTY5-from-master`, false},
|
||||||
|
{`\cygwin-x-pty2-from-toaster`, false},
|
||||||
|
{`\cygwin--pty2-from-master`, false},
|
||||||
|
{`\\cygwin-x-pty2-from-master`, false},
|
||||||
|
{`\cygwin-x-pty2-from-master-`, true}, // for the feature
|
||||||
|
{`\cygwin-e022582115c10879-pty4-from-master`, true},
|
||||||
|
{`\msys-e022582115c10879-pty4-to-master`, true},
|
||||||
|
{`\cygwin-e022582115c10879-pty4-to-master`, true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
want := test.result
|
||||||
|
got := isCygwinPipeName(test.name)
|
||||||
|
if want != got {
|
||||||
|
t.Fatalf("isatty(%q): got %v, want %v:", test.name, got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
*.db
|
||||||
|
*.exe
|
||||||
|
*.dll
|
||||||
|
*.o
|
|
@ -0,0 +1,19 @@
|
||||||
|
language: go
|
||||||
|
sudo: required
|
||||||
|
dist: trusty
|
||||||
|
env:
|
||||||
|
- GOTAGS=
|
||||||
|
- GOTAGS=libsqlite3
|
||||||
|
- GOTAGS=trace
|
||||||
|
- GOTAGS=vtable
|
||||||
|
go:
|
||||||
|
- 1.7.x
|
||||||
|
- 1.8.x
|
||||||
|
- 1.9.x
|
||||||
|
- master
|
||||||
|
before_install:
|
||||||
|
- go get github.com/mattn/goveralls
|
||||||
|
- go get golang.org/x/tools/cmd/cover
|
||||||
|
script:
|
||||||
|
- $HOME/gopath/bin/goveralls -repotoken 3qJVUE0iQwqnCbmNcDsjYu1nh4J4KIFXx
|
||||||
|
- go test -race -v . -tags "$GOTAGS"
|
|
@ -0,0 +1,21 @@
|
||||||
|
The MIT License (MIT)
|
||||||
|
|
||||||
|
Copyright (c) 2014 Yasuhiro Matsumoto
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
|
@ -0,0 +1,97 @@
|
||||||
|
go-sqlite3
|
||||||
|
==========
|
||||||
|
|
||||||
|
[![GoDoc Reference](https://godoc.org/github.com/mattn/go-sqlite3?status.svg)](http://godoc.org/github.com/mattn/go-sqlite3)
|
||||||
|
[![Build Status](https://travis-ci.org/mattn/go-sqlite3.svg?branch=master)](https://travis-ci.org/mattn/go-sqlite3)
|
||||||
|
[![Coverage Status](https://coveralls.io/repos/mattn/go-sqlite3/badge.svg?branch=master)](https://coveralls.io/r/mattn/go-sqlite3?branch=master)
|
||||||
|
[![Go Report Card](https://goreportcard.com/badge/github.com/mattn/go-sqlite3)](https://goreportcard.com/report/github.com/mattn/go-sqlite3)
|
||||||
|
|
||||||
|
Description
|
||||||
|
-----------
|
||||||
|
|
||||||
|
sqlite3 driver conforming to the built-in database/sql interface
|
||||||
|
|
||||||
|
Installation
|
||||||
|
------------
|
||||||
|
|
||||||
|
This package can be installed with the go get command:
|
||||||
|
|
||||||
|
go get github.com/mattn/go-sqlite3
|
||||||
|
|
||||||
|
_go-sqlite3_ is *cgo* package.
|
||||||
|
If you want to build your app using go-sqlite3, you need gcc.
|
||||||
|
However, if you install _go-sqlite3_ with `go install github.com/mattn/go-sqlite3`, you don't need gcc to build your app anymore.
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
API documentation can be found here: http://godoc.org/github.com/mattn/go-sqlite3
|
||||||
|
|
||||||
|
Examples can be found under the `./_example` directory
|
||||||
|
|
||||||
|
FAQ
|
||||||
|
---
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with libsqlite3 on my linux.
|
||||||
|
|
||||||
|
Use `go build --tags "libsqlite3 linux"`
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with libsqlite3 on OS X.
|
||||||
|
|
||||||
|
Install sqlite3 from homebrew: `brew install sqlite3`
|
||||||
|
|
||||||
|
Use `go build --tags "libsqlite3 darwin"`
|
||||||
|
|
||||||
|
* Want to build go-sqlite3 with icu extension.
|
||||||
|
|
||||||
|
Use `go build --tags "icu"`
|
||||||
|
|
||||||
|
Available extensions: `json1`, `fts5`, `icu`
|
||||||
|
|
||||||
|
* Can't build go-sqlite3 on windows 64bit.
|
||||||
|
|
||||||
|
> Probably, you are using go 1.0, go1.0 has a problem when it comes to compiling/linking on windows 64bit.
|
||||||
|
> See: [#27](https://github.com/mattn/go-sqlite3/issues/27)
|
||||||
|
|
||||||
|
* Getting insert error while query is opened.
|
||||||
|
|
||||||
|
> You can pass some arguments into the connection string, for example, a URI.
|
||||||
|
> See: [#39](https://github.com/mattn/go-sqlite3/issues/39)
|
||||||
|
|
||||||
|
* Do you want to cross compile? mingw on Linux or Mac?
|
||||||
|
|
||||||
|
> See: [#106](https://github.com/mattn/go-sqlite3/issues/106)
|
||||||
|
> See also: http://www.limitlessfx.com/cross-compile-golang-app-for-windows-from-linux.html
|
||||||
|
|
||||||
|
* Want to get time.Time with current locale
|
||||||
|
|
||||||
|
Use `_loc=auto` in SQLite3 filename schema like `file:foo.db?_loc=auto`.
|
||||||
|
|
||||||
|
* Can I use this in multiple routines concurrently?
|
||||||
|
|
||||||
|
Yes for readonly. But, No for writable. See [#50](https://github.com/mattn/go-sqlite3/issues/50), [#51](https://github.com/mattn/go-sqlite3/issues/51), [#209](https://github.com/mattn/go-sqlite3/issues/209).
|
||||||
|
|
||||||
|
* Why is it racy if I use a `sql.Open("sqlite3", ":memory:")` database?
|
||||||
|
|
||||||
|
Each connection to :memory: opens a brand new in-memory sql database, so if
|
||||||
|
the stdlib's sql engine happens to open another connection and you've only
|
||||||
|
specified ":memory:", that connection will see a brand new database. A
|
||||||
|
workaround is to use "file::memory:?mode=memory&cache=shared". Every
|
||||||
|
connection to this string will point to the same in-memory database. See
|
||||||
|
[#204](https://github.com/mattn/go-sqlite3/issues/204) for more info.
|
||||||
|
|
||||||
|
License
|
||||||
|
-------
|
||||||
|
|
||||||
|
MIT: http://mattn.mit-license.org/2012
|
||||||
|
|
||||||
|
sqlite3-binding.c, sqlite3-binding.h, sqlite3ext.h
|
||||||
|
|
||||||
|
The -binding suffix was added to avoid build failures under gccgo.
|
||||||
|
|
||||||
|
In this repository, those files are an amalgamation of code that was copied from SQLite3. The license of that code is the same as the license of SQLite3.
|
||||||
|
|
||||||
|
Author
|
||||||
|
------
|
||||||
|
|
||||||
|
Yasuhiro Matsumoto (a.k.a mattn)
|
|
@ -0,0 +1,133 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
|
||||||
|
sqlite "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Computes x^y
|
||||||
|
func pow(x, y int64) int64 {
|
||||||
|
return int64(math.Pow(float64(x), float64(y)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the bitwise exclusive-or of all its arguments
|
||||||
|
func xor(xs ...int64) int64 {
|
||||||
|
var ret int64
|
||||||
|
for _, x := range xs {
|
||||||
|
ret ^= x
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a random number. It's actually deterministic here because
|
||||||
|
// we don't seed the RNG, but it's an example of a non-pure function
|
||||||
|
// from SQLite's POV.
|
||||||
|
func getrand() int64 {
|
||||||
|
return rand.Int63()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Computes the standard deviation of a GROUPed BY set of values
|
||||||
|
type stddev struct {
|
||||||
|
xs []int64
|
||||||
|
// Running average calculation
|
||||||
|
sum int64
|
||||||
|
n int64
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStddev() *stddev { return &stddev{} }
|
||||||
|
|
||||||
|
func (s *stddev) Step(x int64) {
|
||||||
|
s.xs = append(s.xs, x)
|
||||||
|
s.sum += x
|
||||||
|
s.n++
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stddev) Done() float64 {
|
||||||
|
mean := float64(s.sum) / float64(s.n)
|
||||||
|
var sqDiff []float64
|
||||||
|
for _, x := range s.xs {
|
||||||
|
sqDiff = append(sqDiff, math.Pow(float64(x)-mean, 2))
|
||||||
|
}
|
||||||
|
var dev float64
|
||||||
|
for _, x := range sqDiff {
|
||||||
|
dev += x
|
||||||
|
}
|
||||||
|
dev /= float64(len(sqDiff))
|
||||||
|
return math.Sqrt(dev)
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sql.Register("sqlite3_custom", &sqlite.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite.SQLiteConn) error {
|
||||||
|
if err := conn.RegisterFunc("pow", pow, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := conn.RegisterFunc("xor", xor, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := conn.RegisterFunc("rand", getrand, false); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := conn.RegisterAggregator("stddev", newStddev, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3_custom", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to open database:", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var i int64
|
||||||
|
err = db.QueryRow("SELECT pow(2,3)").Scan(&i)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("POW query error:", err)
|
||||||
|
}
|
||||||
|
fmt.Println("pow(2,3) =", i) // 8
|
||||||
|
|
||||||
|
err = db.QueryRow("SELECT xor(1,2,3,4,5,6)").Scan(&i)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("XOR query error:", err)
|
||||||
|
}
|
||||||
|
fmt.Println("xor(1,2,3,4,5) =", i) // 7
|
||||||
|
|
||||||
|
err = db.QueryRow("SELECT rand()").Scan(&i)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("RAND query error:", err)
|
||||||
|
}
|
||||||
|
fmt.Println("rand() =", i) // pseudorandom
|
||||||
|
|
||||||
|
_, err = db.Exec("create table foo (department integer, profits integer)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to create table:", err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec("insert into foo values (1, 10), (1, 20), (1, 45), (2, 42), (2, 115)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Failed to insert records:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query("select department, stddev(profits) from foo group by department")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("STDDEV query error:", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var dept int64
|
||||||
|
var dev float64
|
||||||
|
if err := rows.Scan(&dept, &dev); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("dept=%d stddev=%f\n", dept, dev)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,78 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sqlite3conn := []*sqlite3.SQLiteConn{}
|
||||||
|
sql.Register("sqlite3_with_hook_example",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
sqlite3conn = append(sqlite3conn, conn)
|
||||||
|
conn.RegisterUpdateHook(func(op int, db string, table string, rowid int64) {
|
||||||
|
switch op {
|
||||||
|
case sqlite3.SQLITE_INSERT:
|
||||||
|
log.Println("Notified of insert on db", db, "table", table, "rowid", rowid)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
os.Remove("./foo.db")
|
||||||
|
os.Remove("./bar.db")
|
||||||
|
|
||||||
|
srcDb, err := sql.Open("sqlite3_with_hook_example", "./foo.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer srcDb.Close()
|
||||||
|
srcDb.Ping()
|
||||||
|
|
||||||
|
_, err = srcDb.Exec("create table foo(id int, value text)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = srcDb.Exec("insert into foo values(1, 'foo')")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = srcDb.Exec("insert into foo values(2, 'bar')")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = srcDb.Query("select * from foo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
destDb, err := sql.Open("sqlite3_with_hook_example", "./bar.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer destDb.Close()
|
||||||
|
destDb.Ping()
|
||||||
|
|
||||||
|
bk, err := sqlite3conn[1].Backup("main", sqlite3conn[0], "main")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = bk.Step(-1)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = destDb.Query("select * from foo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err = destDb.Exec("insert into foo values(3, 'bar')")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
bk.Finish()
|
||||||
|
}
|
|
@ -0,0 +1,113 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func createBulkInsertQuery(n int, start int) (query string, args []interface{}) {
|
||||||
|
values := make([]string, n)
|
||||||
|
args = make([]interface{}, n*2)
|
||||||
|
pos := 0
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
values[i] = "(?, ?)"
|
||||||
|
args[pos] = start + i
|
||||||
|
args[pos+1] = fmt.Sprintf("こんにちわ世界%03d", i)
|
||||||
|
pos += 2
|
||||||
|
}
|
||||||
|
query = fmt.Sprintf(
|
||||||
|
"insert into foo(id, name) values %s",
|
||||||
|
strings.Join(values, ", "),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func bukInsert(db *sql.DB, query string, args []interface{}) (err error) {
|
||||||
|
stmt, err := db.Prepare(query)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = stmt.Exec(args...)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var sqlite3conn *sqlite3.SQLiteConn
|
||||||
|
sql.Register("sqlite3_with_limit", &sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
sqlite3conn = conn
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
os.Remove("./foo.db")
|
||||||
|
db, err := sql.Open("sqlite3_with_limit", "./foo.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
sqlStmt := `
|
||||||
|
create table foo (id integer not null primary key, name text);
|
||||||
|
delete from foo;
|
||||||
|
`
|
||||||
|
_, err = db.Exec(sqlStmt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%q: %s\n", err, sqlStmt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if sqlite3conn == nil {
|
||||||
|
log.Fatal("not set sqlite3 connection")
|
||||||
|
}
|
||||||
|
|
||||||
|
limitVariableNumber := sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
||||||
|
log.Printf("default SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
||||||
|
|
||||||
|
num := 400
|
||||||
|
query, args := createBulkInsertQuery(num, 0)
|
||||||
|
err = bukInsert(db, query, args)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
smallLimitVariableNumber := 100
|
||||||
|
sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, smallLimitVariableNumber)
|
||||||
|
|
||||||
|
limitVariableNumber = sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
||||||
|
log.Printf("updated SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
||||||
|
|
||||||
|
query, args = createBulkInsertQuery(num, num)
|
||||||
|
err = bukInsert(db, query, args)
|
||||||
|
if err != nil {
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("expect failed since SQLITE_LIMIT_VARIABLE_NUMBER is too small: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bigLimitVariableNumber := 999999
|
||||||
|
sqlite3conn.SetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER, bigLimitVariableNumber)
|
||||||
|
limitVariableNumber = sqlite3conn.GetLimit(sqlite3.SQLITE_LIMIT_VARIABLE_NUMBER)
|
||||||
|
log.Printf("set SQLITE_LIMIT_VARIABLE_NUMBER: %d", bigLimitVariableNumber)
|
||||||
|
log.Printf("updated SQLITE_LIMIT_VARIABLE_NUMBER: %d", limitVariableNumber)
|
||||||
|
|
||||||
|
query, args = createBulkInsertQuery(500, num+num)
|
||||||
|
err = bukInsert(db, query, args)
|
||||||
|
if err != nil {
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("no error if SQLITE_LIMIT_VARIABLE_NUMBER > 999")
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXE=extension.exe
|
||||||
|
EXT=sqlite3_mod_regexp.dll
|
||||||
|
RM=cmd /c del
|
||||||
|
LDFLAG=
|
||||||
|
else
|
||||||
|
EXE=extension
|
||||||
|
EXT=sqlite3_mod_regexp.so
|
||||||
|
RM=rm
|
||||||
|
LDFLAG=-fPIC
|
||||||
|
endif
|
||||||
|
|
||||||
|
all : $(EXE) $(EXT)
|
||||||
|
|
||||||
|
$(EXE) : extension.go
|
||||||
|
go build $<
|
||||||
|
|
||||||
|
$(EXT) : sqlite3_mod_regexp.c
|
||||||
|
gcc $(LDFLAG) -shared -o $@ $< -lsqlite3 -lpcre
|
||||||
|
|
||||||
|
clean :
|
||||||
|
@-$(RM) $(EXE) $(EXT)
|
43
vendor/github.com/mattn/go-sqlite3/_example/mod_regexp/extension.go
generated
vendored
Normal file
43
vendor/github.com/mattn/go-sqlite3/_example/mod_regexp/extension.go
generated
vendored
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sql.Register("sqlite3_with_extensions",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
Extensions: []string{
|
||||||
|
"sqlite3_mod_regexp",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
// Force db to make a new connection in pool
|
||||||
|
// by putting the original in a transaction
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer tx.Commit()
|
||||||
|
|
||||||
|
// New connection works (hopefully!)
|
||||||
|
rows, err := db.Query("select 'hello world' where 'hello world' regexp '^hello.*d$'")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var helloworld string
|
||||||
|
rows.Scan(&helloworld)
|
||||||
|
fmt.Println(helloworld)
|
||||||
|
}
|
||||||
|
}
|
31
vendor/github.com/mattn/go-sqlite3/_example/mod_regexp/sqlite3_mod_regexp.c
generated
vendored
Normal file
31
vendor/github.com/mattn/go-sqlite3/_example/mod_regexp/sqlite3_mod_regexp.c
generated
vendored
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
#include <pcre.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sqlite3ext.h>
|
||||||
|
|
||||||
|
SQLITE_EXTENSION_INIT1
|
||||||
|
static void regexp_func(sqlite3_context *context, int argc, sqlite3_value **argv) {
|
||||||
|
if (argc >= 2) {
|
||||||
|
const char *target = (const char *)sqlite3_value_text(argv[1]);
|
||||||
|
const char *pattern = (const char *)sqlite3_value_text(argv[0]);
|
||||||
|
const char* errstr = NULL;
|
||||||
|
int erroff = 0;
|
||||||
|
int vec[500];
|
||||||
|
int n, rc;
|
||||||
|
pcre* re = pcre_compile(pattern, 0, &errstr, &erroff, NULL);
|
||||||
|
rc = pcre_exec(re, NULL, target, strlen(target), 0, 0, vec, 500);
|
||||||
|
if (rc <= 0) {
|
||||||
|
sqlite3_result_error(context, errstr, 0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
sqlite3_result_int(context, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
__declspec(dllexport)
|
||||||
|
#endif
|
||||||
|
int sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
|
||||||
|
SQLITE_EXTENSION_INIT2(api);
|
||||||
|
return sqlite3_create_function(db, "regexp", 2, SQLITE_UTF8, (void*)db, regexp_func, NULL, NULL);
|
||||||
|
}
|
|
@ -0,0 +1,24 @@
|
||||||
|
ifeq ($(OS),Windows_NT)
|
||||||
|
EXE=extension.exe
|
||||||
|
EXT=sqlite3_mod_vtable.dll
|
||||||
|
RM=cmd /c del
|
||||||
|
LIBCURL=-lcurldll
|
||||||
|
LDFLAG=
|
||||||
|
else
|
||||||
|
EXE=extension
|
||||||
|
EXT=sqlite3_mod_vtable.so
|
||||||
|
RM=rm
|
||||||
|
LDFLAG=-fPIC
|
||||||
|
LIBCURL=-lcurl
|
||||||
|
endif
|
||||||
|
|
||||||
|
all : $(EXE) $(EXT)
|
||||||
|
|
||||||
|
$(EXE) : extension.go
|
||||||
|
go build $<
|
||||||
|
|
||||||
|
$(EXT) : sqlite3_mod_vtable.cc
|
||||||
|
g++ $(LDFLAG) -shared -o $@ $< -lsqlite3 $(LIBCURL)
|
||||||
|
|
||||||
|
clean :
|
||||||
|
@-$(RM) $(EXE) $(EXT)
|
37
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/extension.go
generated
vendored
Normal file
37
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/extension.go
generated
vendored
Normal file
|
@ -0,0 +1,37 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sql.Register("sqlite3_with_extensions",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
Extensions: []string{
|
||||||
|
"sqlite3_mod_vtable",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
db.Exec("create virtual table repo using github(id, full_name, description, html_url)")
|
||||||
|
|
||||||
|
rows, err := db.Query("select id, full_name, description, html_url from repo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id, fullName, description, htmlURL string
|
||||||
|
rows.Scan(&id, &fullName, &description, &htmlURL)
|
||||||
|
fmt.Printf("%s: %s\n\t%s\n\t%s\n\n", id, fullName, description, htmlURL)
|
||||||
|
}
|
||||||
|
}
|
1040
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/picojson.h
generated
vendored
Normal file
1040
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/picojson.h
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
238
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/sqlite3_mod_vtable.cc
generated
vendored
Normal file
238
vendor/github.com/mattn/go-sqlite3/_example/mod_vtable/sqlite3_mod_vtable.cc
generated
vendored
Normal file
|
@ -0,0 +1,238 @@
|
||||||
|
#include <string>
|
||||||
|
#include <sstream>
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#include <sqlite3ext.h>
|
||||||
|
#include <curl/curl.h>
|
||||||
|
#include "picojson.h"
|
||||||
|
|
||||||
|
#ifdef _WIN32
|
||||||
|
# define EXPORT __declspec(dllexport)
|
||||||
|
#else
|
||||||
|
# define EXPORT
|
||||||
|
#endif
|
||||||
|
|
||||||
|
SQLITE_EXTENSION_INIT1;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char* data; // response data from server
|
||||||
|
size_t size; // response size of data
|
||||||
|
} MEMFILE;
|
||||||
|
|
||||||
|
MEMFILE*
|
||||||
|
memfopen() {
|
||||||
|
MEMFILE* mf = (MEMFILE*) malloc(sizeof(MEMFILE));
|
||||||
|
if (mf) {
|
||||||
|
mf->data = NULL;
|
||||||
|
mf->size = 0;
|
||||||
|
}
|
||||||
|
return mf;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
memfclose(MEMFILE* mf) {
|
||||||
|
if (mf->data) free(mf->data);
|
||||||
|
free(mf);
|
||||||
|
}
|
||||||
|
|
||||||
|
size_t
|
||||||
|
memfwrite(char* ptr, size_t size, size_t nmemb, void* stream) {
|
||||||
|
MEMFILE* mf = (MEMFILE*) stream;
|
||||||
|
int block = size * nmemb;
|
||||||
|
if (!mf) return block; // through
|
||||||
|
if (!mf->data)
|
||||||
|
mf->data = (char*) malloc(block);
|
||||||
|
else
|
||||||
|
mf->data = (char*) realloc(mf->data, mf->size + block);
|
||||||
|
if (mf->data) {
|
||||||
|
memcpy(mf->data + mf->size, ptr, block);
|
||||||
|
mf->size += block;
|
||||||
|
}
|
||||||
|
return block;
|
||||||
|
}
|
||||||
|
|
||||||
|
char*
|
||||||
|
memfstrdup(MEMFILE* mf) {
|
||||||
|
char* buf;
|
||||||
|
if (mf->size == 0) return NULL;
|
||||||
|
buf = (char*) malloc(mf->size + 1);
|
||||||
|
memcpy(buf, mf->data, mf->size);
|
||||||
|
buf[mf->size] = 0;
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_connect(sqlite3 *db, void *pAux, int argc, const char * const *argv, sqlite3_vtab **ppVTab, char **c) {
|
||||||
|
std::stringstream ss;
|
||||||
|
ss << "CREATE TABLE " << argv[0]
|
||||||
|
<< "(id int, full_name text, description text, html_url text)";
|
||||||
|
int rc = sqlite3_declare_vtab(db, ss.str().c_str());
|
||||||
|
*ppVTab = (sqlite3_vtab *) sqlite3_malloc(sizeof(sqlite3_vtab));
|
||||||
|
memset(*ppVTab, 0, sizeof(sqlite3_vtab));
|
||||||
|
return rc;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_create(sqlite3 *db, void *pAux, int argc, const char * const * argv, sqlite3_vtab **ppVTab, char **c) {
|
||||||
|
return my_connect(db, pAux, argc, argv, ppVTab, c);
|
||||||
|
}
|
||||||
|
|
||||||
|
static int my_disconnect(sqlite3_vtab *pVTab) {
|
||||||
|
sqlite3_free(pVTab);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_destroy(sqlite3_vtab *pVTab) {
|
||||||
|
sqlite3_free(pVTab);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
sqlite3_vtab_cursor base;
|
||||||
|
int index;
|
||||||
|
picojson::value* rows;
|
||||||
|
} cursor;
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_open(sqlite3_vtab *pVTab, sqlite3_vtab_cursor **ppCursor) {
|
||||||
|
MEMFILE* mf;
|
||||||
|
CURL* curl;
|
||||||
|
char* json;
|
||||||
|
CURLcode res = CURLE_OK;
|
||||||
|
char error[CURL_ERROR_SIZE] = {0};
|
||||||
|
char* cert_file = getenv("SSL_CERT_FILE");
|
||||||
|
|
||||||
|
mf = memfopen();
|
||||||
|
curl = curl_easy_init();
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 1);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 2);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_USERAGENT, "curl/7.29.0");
|
||||||
|
curl_easy_setopt(curl, CURLOPT_URL, "https://api.github.com/repositories");
|
||||||
|
if (cert_file)
|
||||||
|
curl_easy_setopt(curl, CURLOPT_CAINFO, cert_file);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_ERRORBUFFER, error);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEDATA, mf);
|
||||||
|
curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, memfwrite);
|
||||||
|
res = curl_easy_perform(curl);
|
||||||
|
curl_easy_cleanup(curl);
|
||||||
|
if (res != CURLE_OK) {
|
||||||
|
std::cerr << error << std::endl;
|
||||||
|
return SQLITE_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
picojson::value* v = new picojson::value;
|
||||||
|
std::string err;
|
||||||
|
picojson::parse(*v, mf->data, mf->data + mf->size, &err);
|
||||||
|
memfclose(mf);
|
||||||
|
|
||||||
|
if (!err.empty()) {
|
||||||
|
delete v;
|
||||||
|
std::cerr << err << std::endl;
|
||||||
|
return SQLITE_FAIL;
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor *c = (cursor *)sqlite3_malloc(sizeof(cursor));
|
||||||
|
c->rows = v;
|
||||||
|
c->index = 0;
|
||||||
|
*ppCursor = &c->base;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_close(cursor *c) {
|
||||||
|
delete c->rows;
|
||||||
|
sqlite3_free(c);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_filter(cursor *c, int idxNum, const char *idxStr, int argc, sqlite3_value **argv) {
|
||||||
|
c->index = 0;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_next(cursor *c) {
|
||||||
|
c->index++;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_eof(cursor *c) {
|
||||||
|
return c->index >= c->rows->get<picojson::array>().size() ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_column(cursor *c, sqlite3_context *ctxt, int i) {
|
||||||
|
picojson::value v = c->rows->get<picojson::array>()[c->index];
|
||||||
|
picojson::object row = v.get<picojson::object>();
|
||||||
|
const char* p = NULL;
|
||||||
|
switch (i) {
|
||||||
|
case 0:
|
||||||
|
p = row["id"].to_str().c_str();
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
p = row["full_name"].to_str().c_str();
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
p = row["description"].to_str().c_str();
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
p = row["html_url"].to_str().c_str();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
sqlite3_result_text(ctxt, strdup(p), strlen(p), free);
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_rowid(cursor *c, sqlite3_int64 *pRowid) {
|
||||||
|
*pRowid = c->index;
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
my_bestindex(sqlite3_vtab *tab, sqlite3_index_info *pIdxInfo) {
|
||||||
|
return SQLITE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
static const sqlite3_module module = {
|
||||||
|
0,
|
||||||
|
my_create,
|
||||||
|
my_connect,
|
||||||
|
my_bestindex,
|
||||||
|
my_disconnect,
|
||||||
|
my_destroy,
|
||||||
|
my_open,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *)) my_close,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *, int, char const *, int, sqlite3_value **)) my_filter,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *)) my_next,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *)) my_eof,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *, sqlite3_context *, int)) my_column,
|
||||||
|
(int (*)(sqlite3_vtab_cursor *, sqlite3_int64 *)) my_rowid,
|
||||||
|
NULL, // my_update
|
||||||
|
NULL, // my_begin
|
||||||
|
NULL, // my_sync
|
||||||
|
NULL, // my_commit
|
||||||
|
NULL, // my_rollback
|
||||||
|
NULL, // my_findfunction
|
||||||
|
NULL, // my_rename
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
destructor(void *arg) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
EXPORT int
|
||||||
|
sqlite3_extension_init(sqlite3 *db, char **errmsg, const sqlite3_api_routines *api) {
|
||||||
|
SQLITE_EXTENSION_INIT2(api);
|
||||||
|
sqlite3_create_module_v2(db, "github", &module, NULL, destructor);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,106 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
_ "github.com/mattn/go-sqlite3"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
os.Remove("./foo.db")
|
||||||
|
|
||||||
|
db, err := sql.Open("sqlite3", "./foo.db")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
sqlStmt := `
|
||||||
|
create table foo (id integer not null primary key, name text);
|
||||||
|
delete from foo;
|
||||||
|
`
|
||||||
|
_, err = db.Exec(sqlStmt)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%q: %s\n", err, sqlStmt)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := db.Begin()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
stmt, err := tx.Prepare("insert into foo(id, name) values(?, ?)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
_, err = stmt.Exec(i, fmt.Sprintf("こんにちわ世界%03d", i))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tx.Commit()
|
||||||
|
|
||||||
|
rows, err := db.Query("select id, name from foo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var name string
|
||||||
|
err = rows.Scan(&id, &name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(id, name)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stmt, err = db.Prepare("select name from foo where id = ?")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
var name string
|
||||||
|
err = stmt.QueryRow("3").Scan(&name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(name)
|
||||||
|
|
||||||
|
_, err = db.Exec("delete from foo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Exec("insert into foo(id, name) values(1, 'foo'), (2, 'bar'), (3, 'baz')")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err = db.Query("select id, name from foo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id int
|
||||||
|
var name string
|
||||||
|
err = rows.Scan(&id, &name)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println(id, name)
|
||||||
|
}
|
||||||
|
err = rows.Err()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,264 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
sqlite3 "github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func traceCallback(info sqlite3.TraceInfo) int {
|
||||||
|
// Not very readable but may be useful; uncomment next line in case of doubt:
|
||||||
|
//fmt.Printf("Trace: %#v\n", info)
|
||||||
|
|
||||||
|
var dbErrText string
|
||||||
|
if info.DBError.Code != 0 || info.DBError.ExtendedCode != 0 {
|
||||||
|
dbErrText = fmt.Sprintf("; DB error: %#v", info.DBError)
|
||||||
|
} else {
|
||||||
|
dbErrText = "."
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show the Statement-or-Trigger text in curly braces ('{', '}')
|
||||||
|
// since from the *paired* ASCII characters they are
|
||||||
|
// the least used in SQL syntax, therefore better visual delimiters.
|
||||||
|
// Maybe show 'ExpandedSQL' the same way as 'StmtOrTrigger'.
|
||||||
|
//
|
||||||
|
// A known use of curly braces (outside strings) is
|
||||||
|
// for ODBC escape sequences. Not likely to appear here.
|
||||||
|
//
|
||||||
|
// Template languages, etc. don't matter, we should see their *result*
|
||||||
|
// at *this* level.
|
||||||
|
// Strange curly braces in SQL code that reached the database driver
|
||||||
|
// suggest that there is a bug in the application.
|
||||||
|
// The braces are likely to be either template syntax or
|
||||||
|
// a programming language's string interpolation syntax.
|
||||||
|
|
||||||
|
var expandedText string
|
||||||
|
if info.ExpandedSQL != "" {
|
||||||
|
if info.ExpandedSQL == info.StmtOrTrigger {
|
||||||
|
expandedText = " = exp"
|
||||||
|
} else {
|
||||||
|
expandedText = fmt.Sprintf(" expanded {%q}", info.ExpandedSQL)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
expandedText = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLite docs as of September 6, 2016: Tracing and Profiling Functions
|
||||||
|
// https://www.sqlite.org/c3ref/profile.html
|
||||||
|
//
|
||||||
|
// The profile callback time is in units of nanoseconds, however
|
||||||
|
// the current implementation is only capable of millisecond resolution
|
||||||
|
// so the six least significant digits in the time are meaningless.
|
||||||
|
// Future versions of SQLite might provide greater resolution on the profiler callback.
|
||||||
|
|
||||||
|
var runTimeText string
|
||||||
|
if info.RunTimeNanosec == 0 {
|
||||||
|
if info.EventCode == sqlite3.TraceProfile {
|
||||||
|
//runTimeText = "; no time" // seems confusing
|
||||||
|
runTimeText = "; time 0" // no measurement unit
|
||||||
|
} else {
|
||||||
|
//runTimeText = "; no time" // seems useless and confusing
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const nanosPerMillisec = 1000000
|
||||||
|
if info.RunTimeNanosec%nanosPerMillisec == 0 {
|
||||||
|
runTimeText = fmt.Sprintf("; time %d ms", info.RunTimeNanosec/nanosPerMillisec)
|
||||||
|
} else {
|
||||||
|
// unexpected: better than millisecond resolution
|
||||||
|
runTimeText = fmt.Sprintf("; time %d ns!!!", info.RunTimeNanosec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var modeText string
|
||||||
|
if info.AutoCommit {
|
||||||
|
modeText = "-AC-"
|
||||||
|
} else {
|
||||||
|
modeText = "+Tx+"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Trace: ev %d %s conn 0x%x, stmt 0x%x {%q}%s%s%s\n",
|
||||||
|
info.EventCode, modeText, info.ConnHandle, info.StmtHandle,
|
||||||
|
info.StmtOrTrigger, expandedText,
|
||||||
|
runTimeText,
|
||||||
|
dbErrText)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
eventMask := sqlite3.TraceStmt | sqlite3.TraceProfile | sqlite3.TraceRow | sqlite3.TraceClose
|
||||||
|
|
||||||
|
sql.Register("sqlite3_tracing",
|
||||||
|
&sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
err := conn.SetTrace(&sqlite3.TraceConfig{
|
||||||
|
Callback: traceCallback,
|
||||||
|
EventMask: uint(eventMask),
|
||||||
|
WantExpandedSQL: true,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
os.Exit(dbMain())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Harder to do DB work in main().
|
||||||
|
// It's better with a separate function because
|
||||||
|
// 'defer' and 'os.Exit' don't go well together.
|
||||||
|
//
|
||||||
|
// DO NOT use 'log.Fatal...' below: remember that it's equivalent to
|
||||||
|
// Print() followed by a call to os.Exit(1) --- and
|
||||||
|
// we want to avoid Exit() so 'defer' can do cleanup.
|
||||||
|
// Use 'log.Panic...' instead.
|
||||||
|
|
||||||
|
func dbMain() int {
|
||||||
|
db, err := sql.Open("sqlite3_tracing", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("Failed to open database: %#+v\n", err)
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
err = db.Ping()
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
dbSetup(db)
|
||||||
|
|
||||||
|
dbDoInsert(db)
|
||||||
|
dbDoInsertPrepared(db)
|
||||||
|
dbDoSelect(db)
|
||||||
|
dbDoSelectPrepared(db)
|
||||||
|
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 'DDL' stands for "Data Definition Language":
|
||||||
|
|
||||||
|
// Note: "INTEGER PRIMARY KEY NOT NULL AUTOINCREMENT" causes the error
|
||||||
|
// 'near "AUTOINCREMENT": syntax error'; without "NOT NULL" it works.
|
||||||
|
const tableDDL = `CREATE TABLE t1 (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
|
note VARCHAR NOT NULL
|
||||||
|
)`
|
||||||
|
|
||||||
|
// 'DML' stands for "Data Manipulation Language":
|
||||||
|
|
||||||
|
const insertDML = "INSERT INTO t1 (note) VALUES (?)"
|
||||||
|
const selectDML = "SELECT id, note FROM t1 WHERE note LIKE ?"
|
||||||
|
|
||||||
|
const textPrefix = "bla-1234567890-"
|
||||||
|
const noteTextPattern = "%Prep%"
|
||||||
|
|
||||||
|
const nGenRows = 4 // Number of Rows to Generate (for *each* approach tested)
|
||||||
|
|
||||||
|
func dbSetup(db *sql.DB) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
_, err = db.Exec("DROP TABLE IF EXISTS t1")
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
_, err = db.Exec(tableDDL)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbDoInsert(db *sql.DB) {
|
||||||
|
const Descr = "DB-Exec"
|
||||||
|
for i := 0; i < nGenRows; i++ {
|
||||||
|
result, err := db.Exec(insertDML, textPrefix+Descr)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDoCheck(result, Descr, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbDoInsertPrepared(db *sql.DB) {
|
||||||
|
const Descr = "DB-Prepare"
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(insertDML)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
for i := 0; i < nGenRows; i++ {
|
||||||
|
result, err := stmt.Exec(textPrefix + Descr)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resultDoCheck(result, Descr, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resultDoCheck(result sql.Result, callerDescr string, callIndex int) {
|
||||||
|
lastID, err := result.LastInsertId()
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
nAffected, err := result.RowsAffected()
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Exec result for %s (%d): ID = %d, affected = %d\n", callerDescr, callIndex, lastID, nAffected)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbDoSelect(db *sql.DB) {
|
||||||
|
const Descr = "DB-Query"
|
||||||
|
|
||||||
|
rows, err := db.Query(selectDML, noteTextPattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rowsDoFetch(rows, Descr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func dbDoSelectPrepared(db *sql.DB) {
|
||||||
|
const Descr = "DB-Prepare"
|
||||||
|
|
||||||
|
stmt, err := db.Prepare(selectDML)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer stmt.Close()
|
||||||
|
|
||||||
|
rows, err := stmt.Query(noteTextPattern)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
rowsDoFetch(rows, Descr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func rowsDoFetch(rows *sql.Rows, callerDescr string) {
|
||||||
|
var nRows int
|
||||||
|
var id int64
|
||||||
|
var note string
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
err := rows.Scan(&id, ¬e)
|
||||||
|
if err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
log.Printf("Row for %s (%d): id=%d, note=%q\n",
|
||||||
|
callerDescr, nRows, id, note)
|
||||||
|
nRows++
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
log.Panic(err)
|
||||||
|
}
|
||||||
|
log.Printf("Total %d rows for %s.\n", nRows, callerDescr)
|
||||||
|
}
|
|
@ -0,0 +1,38 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
sql.Register("sqlite3_with_extensions", &sqlite3.SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *sqlite3.SQLiteConn) error {
|
||||||
|
return conn.CreateModule("github", &githubModule{})
|
||||||
|
},
|
||||||
|
})
|
||||||
|
db, err := sql.Open("sqlite3_with_extensions", ":memory:")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
_, err = db.Exec("create virtual table repo using github(id, full_name, description, html_url)")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rows, err := db.Query("select id, full_name, description, html_url from repo")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
for rows.Next() {
|
||||||
|
var id, fullName, description, htmlURL string
|
||||||
|
rows.Scan(&id, &fullName, &description, &htmlURL)
|
||||||
|
fmt.Printf("%s: %s\n\t%s\n\t%s\n\n", id, fullName, description, htmlURL)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,111 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/mattn/go-sqlite3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type githubRepo struct {
|
||||||
|
ID int `json:"id"`
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
HTMLURL string `json:"html_url"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type githubModule struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *githubModule) Create(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
|
||||||
|
err := c.DeclareVTab(fmt.Sprintf(`
|
||||||
|
CREATE TABLE %s (
|
||||||
|
id INT,
|
||||||
|
full_name TEXT,
|
||||||
|
description TEXT,
|
||||||
|
html_url TEXT
|
||||||
|
)`, args[0]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ghRepoTable{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *githubModule) Connect(c *sqlite3.SQLiteConn, args []string) (sqlite3.VTab, error) {
|
||||||
|
return m.Create(c, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *githubModule) DestroyModule() {}
|
||||||
|
|
||||||
|
type ghRepoTable struct {
|
||||||
|
repos []githubRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ghRepoTable) Open() (sqlite3.VTabCursor, error) {
|
||||||
|
resp, err := http.Get("https://api.github.com/repositories")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
body, err := ioutil.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var repos []githubRepo
|
||||||
|
if err := json.Unmarshal(body, &repos); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &ghRepoCursor{0, repos}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ghRepoTable) BestIndex(cst []sqlite3.InfoConstraint, ob []sqlite3.InfoOrderBy) (*sqlite3.IndexResult, error) {
|
||||||
|
return &sqlite3.IndexResult{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *ghRepoTable) Disconnect() error { return nil }
|
||||||
|
func (v *ghRepoTable) Destroy() error { return nil }
|
||||||
|
|
||||||
|
type ghRepoCursor struct {
|
||||||
|
index int
|
||||||
|
repos []githubRepo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) Column(c *sqlite3.SQLiteContext, col int) error {
|
||||||
|
switch col {
|
||||||
|
case 0:
|
||||||
|
c.ResultInt(vc.repos[vc.index].ID)
|
||||||
|
case 1:
|
||||||
|
c.ResultText(vc.repos[vc.index].FullName)
|
||||||
|
case 2:
|
||||||
|
c.ResultText(vc.repos[vc.index].Description)
|
||||||
|
case 3:
|
||||||
|
c.ResultText(vc.repos[vc.index].HTMLURL)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) Filter(idxNum int, idxStr string, vals []interface{}) error {
|
||||||
|
vc.index = 0
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) Next() error {
|
||||||
|
vc.index++
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) EOF() bool {
|
||||||
|
return vc.index >= len(vc.repos)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) Rowid() (int64, error) {
|
||||||
|
return int64(vc.index), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (vc *ghRepoCursor) Close() error {
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,85 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
/*
|
||||||
|
#ifndef USE_LIBSQLITE3
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#else
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#endif
|
||||||
|
#include <stdlib.h>
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
import (
|
||||||
|
"runtime"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
// SQLiteBackup implement interface of Backup.
|
||||||
|
type SQLiteBackup struct {
|
||||||
|
b *C.sqlite3_backup
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup make backup from src to dest.
|
||||||
|
func (c *SQLiteConn) Backup(dest string, conn *SQLiteConn, src string) (*SQLiteBackup, error) {
|
||||||
|
destptr := C.CString(dest)
|
||||||
|
defer C.free(unsafe.Pointer(destptr))
|
||||||
|
srcptr := C.CString(src)
|
||||||
|
defer C.free(unsafe.Pointer(srcptr))
|
||||||
|
|
||||||
|
if b := C.sqlite3_backup_init(c.db, destptr, conn.db, srcptr); b != nil {
|
||||||
|
bb := &SQLiteBackup{b: b}
|
||||||
|
runtime.SetFinalizer(bb, (*SQLiteBackup).Finish)
|
||||||
|
return bb, nil
|
||||||
|
}
|
||||||
|
return nil, c.lastError()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step to backs up for one step. Calls the underlying `sqlite3_backup_step`
|
||||||
|
// function. This function returns a boolean indicating if the backup is done
|
||||||
|
// and an error signalling any other error. Done is returned if the underlying
|
||||||
|
// C function returns SQLITE_DONE (Code 101)
|
||||||
|
func (b *SQLiteBackup) Step(p int) (bool, error) {
|
||||||
|
ret := C.sqlite3_backup_step(b.b, C.int(p))
|
||||||
|
if ret == C.SQLITE_DONE {
|
||||||
|
return true, nil
|
||||||
|
} else if ret != 0 && ret != C.SQLITE_LOCKED && ret != C.SQLITE_BUSY {
|
||||||
|
return false, Error{Code: ErrNo(ret)}
|
||||||
|
}
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remaining return whether have the rest for backup.
|
||||||
|
func (b *SQLiteBackup) Remaining() int {
|
||||||
|
return int(C.sqlite3_backup_remaining(b.b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// PageCount return count of pages.
|
||||||
|
func (b *SQLiteBackup) PageCount() int {
|
||||||
|
return int(C.sqlite3_backup_pagecount(b.b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish close backup.
|
||||||
|
func (b *SQLiteBackup) Finish() error {
|
||||||
|
return b.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close close backup.
|
||||||
|
func (b *SQLiteBackup) Close() error {
|
||||||
|
ret := C.sqlite3_backup_finish(b.b)
|
||||||
|
|
||||||
|
// sqlite3_backup_finish() never fails, it just returns the
|
||||||
|
// error code from previous operations, so clean up before
|
||||||
|
// checking and returning an error
|
||||||
|
b.b = nil
|
||||||
|
runtime.SetFinalizer(b, nil)
|
||||||
|
|
||||||
|
if ret != 0 {
|
||||||
|
return Error{Code: ErrNo(ret)}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
|
@ -0,0 +1,290 @@
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The number of rows of test data to create in the source database.
|
||||||
|
// Can be used to control how many pages are available to be backed up.
|
||||||
|
const testRowCount = 100
|
||||||
|
|
||||||
|
// The maximum number of seconds after which the page-by-page backup is considered to have taken too long.
|
||||||
|
const usePagePerStepsTimeoutSeconds = 30
|
||||||
|
|
||||||
|
// Test the backup functionality.
|
||||||
|
func testBackup(t *testing.T, testRowCount int, usePerPageSteps bool) {
|
||||||
|
// This function will be called multiple times.
|
||||||
|
// It uses sql.Register(), which requires the name parameter value to be unique.
|
||||||
|
// There does not currently appear to be a way to unregister a registered driver, however.
|
||||||
|
// So generate a database driver name that will likely be unique.
|
||||||
|
var driverName = fmt.Sprintf("sqlite3_testBackup_%v_%v_%v", testRowCount, usePerPageSteps, time.Now().UnixNano())
|
||||||
|
|
||||||
|
// The driver's connection will be needed in order to perform the backup.
|
||||||
|
driverConns := []*SQLiteConn{}
|
||||||
|
sql.Register(driverName, &SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *SQLiteConn) error {
|
||||||
|
driverConns = append(driverConns, conn)
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to the source database.
|
||||||
|
srcTempFilename := TempFilename(t)
|
||||||
|
defer os.Remove(srcTempFilename)
|
||||||
|
srcDb, err := sql.Open(driverName, srcTempFilename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to open the source database:", err)
|
||||||
|
}
|
||||||
|
defer srcDb.Close()
|
||||||
|
err = srcDb.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to connect to the source database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Connect to the destination database.
|
||||||
|
destTempFilename := TempFilename(t)
|
||||||
|
defer os.Remove(destTempFilename)
|
||||||
|
destDb, err := sql.Open(driverName, destTempFilename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to open the destination database:", err)
|
||||||
|
}
|
||||||
|
defer destDb.Close()
|
||||||
|
err = destDb.Ping()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to connect to the destination database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check the driver connections.
|
||||||
|
if len(driverConns) != 2 {
|
||||||
|
t.Fatalf("Expected 2 driver connections, but found %v.", len(driverConns))
|
||||||
|
}
|
||||||
|
srcDbDriverConn := driverConns[0]
|
||||||
|
if srcDbDriverConn == nil {
|
||||||
|
t.Fatal("The source database driver connection is nil.")
|
||||||
|
}
|
||||||
|
destDbDriverConn := driverConns[1]
|
||||||
|
if destDbDriverConn == nil {
|
||||||
|
t.Fatal("The destination database driver connection is nil.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate some test data for the given ID.
|
||||||
|
var generateTestData = func(id int) string {
|
||||||
|
return fmt.Sprintf("test-%v", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the source database with a test table containing some test data.
|
||||||
|
tx, err := srcDb.Begin()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to begin a transaction when populating the source database:", err)
|
||||||
|
}
|
||||||
|
_, err = srcDb.Exec("CREATE TABLE test (id INTEGER PRIMARY KEY, value TEXT)")
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
t.Fatal("Failed to create the source database \"test\" table:", err)
|
||||||
|
}
|
||||||
|
for id := 0; id < testRowCount; id++ {
|
||||||
|
_, err = srcDb.Exec("INSERT INTO test (id, value) VALUES (?, ?)", id, generateTestData(id))
|
||||||
|
if err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
t.Fatal("Failed to insert a row into the source database \"test\" table:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Commit()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to populate the source database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm that the destination database is initially empty.
|
||||||
|
var destTableCount int
|
||||||
|
err = destDb.QueryRow("SELECT COUNT(*) FROM sqlite_master WHERE type = 'table'").Scan(&destTableCount)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to check the destination table count:", err)
|
||||||
|
}
|
||||||
|
if destTableCount != 0 {
|
||||||
|
t.Fatalf("The destination database is not empty; %v table(s) found.", destTableCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to perform the backup.
|
||||||
|
backup, err := destDbDriverConn.Backup("main", srcDbDriverConn, "main")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to initialize the backup:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Allow the initial page count and remaining values to be retrieved.
|
||||||
|
// According to <https://www.sqlite.org/c3ref/backup_finish.html>, the page count and remaining values are "... only updated by sqlite3_backup_step()."
|
||||||
|
isDone, err := backup.Step(0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Unable to perform an initial 0-page backup step:", err)
|
||||||
|
}
|
||||||
|
if isDone {
|
||||||
|
t.Fatal("Backup is unexpectedly done.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the page count and remaining values are reasonable.
|
||||||
|
initialPageCount := backup.PageCount()
|
||||||
|
if initialPageCount <= 0 {
|
||||||
|
t.Fatalf("Unexpected initial page count value: %v", initialPageCount)
|
||||||
|
}
|
||||||
|
initialRemaining := backup.Remaining()
|
||||||
|
if initialRemaining <= 0 {
|
||||||
|
t.Fatalf("Unexpected initial remaining value: %v", initialRemaining)
|
||||||
|
}
|
||||||
|
if initialRemaining != initialPageCount {
|
||||||
|
t.Fatalf("Initial remaining value differs from the initial page count value; remaining: %v; page count: %v", initialRemaining, initialPageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform the backup.
|
||||||
|
if usePerPageSteps {
|
||||||
|
var startTime = time.Now().Unix()
|
||||||
|
|
||||||
|
// Test backing-up using a page-by-page approach.
|
||||||
|
var latestRemaining = initialRemaining
|
||||||
|
for {
|
||||||
|
// Perform the backup step.
|
||||||
|
isDone, err = backup.Step(1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to perform a backup step:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The page count should remain unchanged from its initial value.
|
||||||
|
currentPageCount := backup.PageCount()
|
||||||
|
if currentPageCount != initialPageCount {
|
||||||
|
t.Fatalf("Current page count differs from the initial page count; initial page count: %v; current page count: %v", initialPageCount, currentPageCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There should now be one less page remaining.
|
||||||
|
currentRemaining := backup.Remaining()
|
||||||
|
expectedRemaining := latestRemaining - 1
|
||||||
|
if currentRemaining != expectedRemaining {
|
||||||
|
t.Fatalf("Unexpected remaining value; expected remaining value: %v; actual remaining value: %v", expectedRemaining, currentRemaining)
|
||||||
|
}
|
||||||
|
latestRemaining = currentRemaining
|
||||||
|
|
||||||
|
if isDone {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the runtime of the backup attempt.
|
||||||
|
if (time.Now().Unix() - startTime) > usePagePerStepsTimeoutSeconds {
|
||||||
|
t.Fatal("Backup is taking longer than expected.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Test the copying of all remaining pages.
|
||||||
|
isDone, err = backup.Step(-1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to perform a backup step:", err)
|
||||||
|
}
|
||||||
|
if !isDone {
|
||||||
|
t.Fatal("Backup is unexpectedly not done.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the page count and remaining values are reasonable.
|
||||||
|
finalPageCount := backup.PageCount()
|
||||||
|
if finalPageCount != initialPageCount {
|
||||||
|
t.Fatalf("Final page count differs from the initial page count; initial page count: %v; final page count: %v", initialPageCount, finalPageCount)
|
||||||
|
}
|
||||||
|
finalRemaining := backup.Remaining()
|
||||||
|
if finalRemaining != 0 {
|
||||||
|
t.Fatalf("Unexpected remaining value: %v", finalRemaining)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finish the backup.
|
||||||
|
err = backup.Finish()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to finish backup:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm that the "test" table now exists in the destination database.
|
||||||
|
var doesTestTableExist bool
|
||||||
|
err = destDb.QueryRow("SELECT EXISTS (SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'test' LIMIT 1) AS test_table_exists").Scan(&doesTestTableExist)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to check if the \"test\" table exists in the destination database:", err)
|
||||||
|
}
|
||||||
|
if !doesTestTableExist {
|
||||||
|
t.Fatal("The \"test\" table could not be found in the destination database.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Confirm that the number of rows in the destination database's "test" table matches that of the source table.
|
||||||
|
var actualTestTableRowCount int
|
||||||
|
err = destDb.QueryRow("SELECT COUNT(*) FROM test").Scan(&actualTestTableRowCount)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to determine the rowcount of the \"test\" table in the destination database:", err)
|
||||||
|
}
|
||||||
|
if testRowCount != actualTestTableRowCount {
|
||||||
|
t.Fatalf("Unexpected destination \"test\" table row count; expected: %v; found: %v", testRowCount, actualTestTableRowCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each of the rows in the destination database.
|
||||||
|
for id := 0; id < testRowCount; id++ {
|
||||||
|
var checkedValue string
|
||||||
|
err = destDb.QueryRow("SELECT value FROM test WHERE id = ?", id).Scan(&checkedValue)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to query the \"test\" table in the destination database:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedValue = generateTestData(id)
|
||||||
|
if checkedValue != expectedValue {
|
||||||
|
t.Fatalf("Unexpected value in the \"test\" table in the destination database; expected value: %v; actual value: %v", expectedValue, checkedValue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupStepByStep(t *testing.T) {
|
||||||
|
testBackup(t, testRowCount, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBackupAllRemainingPages(t *testing.T) {
|
||||||
|
testBackup(t, testRowCount, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test the error reporting when preparing to perform a backup.
|
||||||
|
func TestBackupError(t *testing.T) {
|
||||||
|
const driverName = "sqlite3_TestBackupError"
|
||||||
|
|
||||||
|
// The driver's connection will be needed in order to perform the backup.
|
||||||
|
var dbDriverConn *SQLiteConn
|
||||||
|
sql.Register(driverName, &SQLiteDriver{
|
||||||
|
ConnectHook: func(conn *SQLiteConn) error {
|
||||||
|
dbDriverConn = conn
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Connect to the database.
|
||||||
|
dbTempFilename := TempFilename(t)
|
||||||
|
defer os.Remove(dbTempFilename)
|
||||||
|
db, err := sql.Open(driverName, dbTempFilename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Failed to open the database:", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
db.Ping()
|
||||||
|
|
||||||
|
// Need the driver connection in order to perform the backup.
|
||||||
|
if dbDriverConn == nil {
|
||||||
|
t.Fatal("Failed to get the driver connection.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare to perform the backup.
|
||||||
|
// Intentionally using the same connection for both the source and destination databases, to trigger an error result.
|
||||||
|
backup, err := dbDriverConn.Backup("main", dbDriverConn, "main")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("Failed to get the expected error result.")
|
||||||
|
}
|
||||||
|
const expectedError = "source and destination must be distinct"
|
||||||
|
if err.Error() != expectedError {
|
||||||
|
t.Fatalf("Unexpected error message; expected value: \"%v\"; actual value: \"%v\"", expectedError, err.Error())
|
||||||
|
}
|
||||||
|
if backup != nil {
|
||||||
|
t.Fatal("Failed to get the expected nil backup result.")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,364 @@
|
||||||
|
// Copyright (C) 2014 Yasuhiro Matsumoto <mattn.jp@gmail.com>.
|
||||||
|
//
|
||||||
|
// Use of this source code is governed by an MIT-style
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
// You can't export a Go function to C and have definitions in the C
|
||||||
|
// preamble in the same file, so we have to have callbackTrampoline in
|
||||||
|
// its own file. Because we need a separate file anyway, the support
|
||||||
|
// code for SQLite custom functions is in here.
|
||||||
|
|
||||||
|
/*
|
||||||
|
#ifndef USE_LIBSQLITE3
|
||||||
|
#include <sqlite3-binding.h>
|
||||||
|
#else
|
||||||
|
#include <sqlite3.h>
|
||||||
|
#endif
|
||||||
|
#include <stdlib.h>
|
||||||
|
|
||||||
|
void _sqlite3_result_text(sqlite3_context* ctx, const char* s);
|
||||||
|
void _sqlite3_result_blob(sqlite3_context* ctx, const void* b, int l);
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
//export callbackTrampoline
|
||||||
|
func callbackTrampoline(ctx *C.sqlite3_context, argc int, argv **C.sqlite3_value) {
|
||||||
|
args := (*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.sqlite3_value)(nil))]*C.sqlite3_value)(unsafe.Pointer(argv))[:argc:argc]
|
||||||
|
fi := lookupHandle(uintptr(C.sqlite3_user_data(ctx))).(*functionInfo)
|
||||||
|
fi.Call(ctx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export stepTrampoline
|
||||||
|
func stepTrampoline(ctx *C.sqlite3_context, argc C.int, argv **C.sqlite3_value) {
|
||||||
|
args := (*[(math.MaxInt32 - 1) / unsafe.Sizeof((*C.sqlite3_value)(nil))]*C.sqlite3_value)(unsafe.Pointer(argv))[:int(argc):int(argc)]
|
||||||
|
ai := lookupHandle(uintptr(C.sqlite3_user_data(ctx))).(*aggInfo)
|
||||||
|
ai.Step(ctx, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export doneTrampoline
|
||||||
|
func doneTrampoline(ctx *C.sqlite3_context) {
|
||||||
|
handle := uintptr(C.sqlite3_user_data(ctx))
|
||||||
|
ai := lookupHandle(handle).(*aggInfo)
|
||||||
|
ai.Done(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
//export compareTrampoline
|
||||||
|
func compareTrampoline(handlePtr uintptr, la C.int, a *C.char, lb C.int, b *C.char) C.int {
|
||||||
|
cmp := lookupHandle(handlePtr).(func(string, string) int)
|
||||||
|
return C.int(cmp(C.GoStringN(a, la), C.GoStringN(b, lb)))
|
||||||
|
}
|
||||||
|
|
||||||
|
//export commitHookTrampoline
|
||||||
|
func commitHookTrampoline(handle uintptr) int {
|
||||||
|
callback := lookupHandle(handle).(func() int)
|
||||||
|
return callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export rollbackHookTrampoline
|
||||||
|
func rollbackHookTrampoline(handle uintptr) {
|
||||||
|
callback := lookupHandle(handle).(func())
|
||||||
|
callback()
|
||||||
|
}
|
||||||
|
|
||||||
|
//export updateHookTrampoline
|
||||||
|
func updateHookTrampoline(handle uintptr, op int, db *C.char, table *C.char, rowid int64) {
|
||||||
|
callback := lookupHandle(handle).(func(int, string, string, int64))
|
||||||
|
callback(op, C.GoString(db), C.GoString(table), rowid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use handles to avoid passing Go pointers to C.
|
||||||
|
|
||||||
|
type handleVal struct {
|
||||||
|
db *SQLiteConn
|
||||||
|
val interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var handleLock sync.Mutex
|
||||||
|
var handleVals = make(map[uintptr]handleVal)
|
||||||
|
var handleIndex uintptr = 100
|
||||||
|
|
||||||
|
func newHandle(db *SQLiteConn, v interface{}) uintptr {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
i := handleIndex
|
||||||
|
handleIndex++
|
||||||
|
handleVals[i] = handleVal{db, v}
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
|
||||||
|
func lookupHandle(handle uintptr) interface{} {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
r, ok := handleVals[handle]
|
||||||
|
if !ok {
|
||||||
|
if handle >= 100 && handle < handleIndex {
|
||||||
|
panic("deleted handle")
|
||||||
|
} else {
|
||||||
|
panic("invalid handle")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r.val
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteHandles(db *SQLiteConn) {
|
||||||
|
handleLock.Lock()
|
||||||
|
defer handleLock.Unlock()
|
||||||
|
for handle, val := range handleVals {
|
||||||
|
if val.db == db {
|
||||||
|
delete(handleVals, handle)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This is only here so that tests can refer to it.
|
||||||
|
type callbackArgRaw C.sqlite3_value
|
||||||
|
|
||||||
|
type callbackArgConverter func(*C.sqlite3_value) (reflect.Value, error)
|
||||||
|
|
||||||
|
type callbackArgCast struct {
|
||||||
|
f callbackArgConverter
|
||||||
|
typ reflect.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c callbackArgCast) Run(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
val, err := c.f(v)
|
||||||
|
if err != nil {
|
||||||
|
return reflect.Value{}, err
|
||||||
|
}
|
||||||
|
if !val.Type().ConvertibleTo(c.typ) {
|
||||||
|
return reflect.Value{}, fmt.Errorf("cannot convert %s to %s", val.Type(), c.typ)
|
||||||
|
}
|
||||||
|
return val.Convert(c.typ), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgInt64(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_INTEGER {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be an INTEGER")
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(int64(C.sqlite3_value_int64(v))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgBool(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_INTEGER {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be an INTEGER")
|
||||||
|
}
|
||||||
|
i := int64(C.sqlite3_value_int64(v))
|
||||||
|
val := false
|
||||||
|
if i != 0 {
|
||||||
|
val = true
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(val), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgFloat64(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
if C.sqlite3_value_type(v) != C.SQLITE_FLOAT {
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be a FLOAT")
|
||||||
|
}
|
||||||
|
return reflect.ValueOf(float64(C.sqlite3_value_double(v))), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgBytes(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
p := C.sqlite3_value_blob(v)
|
||||||
|
return reflect.ValueOf(C.GoBytes(p, l)), nil
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
c := unsafe.Pointer(C.sqlite3_value_text(v))
|
||||||
|
return reflect.ValueOf(C.GoBytes(c, l)), nil
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be BLOB or TEXT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgString(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
l := C.sqlite3_value_bytes(v)
|
||||||
|
p := (*C.char)(C.sqlite3_value_blob(v))
|
||||||
|
return reflect.ValueOf(C.GoStringN(p, l)), nil
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
c := (*C.char)(unsafe.Pointer(C.sqlite3_value_text(v)))
|
||||||
|
return reflect.ValueOf(C.GoString(c)), nil
|
||||||
|
default:
|
||||||
|
return reflect.Value{}, fmt.Errorf("argument must be BLOB or TEXT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArgGeneric(v *C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
switch C.sqlite3_value_type(v) {
|
||||||
|
case C.SQLITE_INTEGER:
|
||||||
|
return callbackArgInt64(v)
|
||||||
|
case C.SQLITE_FLOAT:
|
||||||
|
return callbackArgFloat64(v)
|
||||||
|
case C.SQLITE_TEXT:
|
||||||
|
return callbackArgString(v)
|
||||||
|
case C.SQLITE_BLOB:
|
||||||
|
return callbackArgBytes(v)
|
||||||
|
case C.SQLITE_NULL:
|
||||||
|
// Interpret NULL as a nil byte slice.
|
||||||
|
var ret []byte
|
||||||
|
return reflect.ValueOf(ret), nil
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackArg(typ reflect.Type) (callbackArgConverter, error) {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Interface:
|
||||||
|
if typ.NumMethod() != 0 {
|
||||||
|
return nil, errors.New("the only supported interface type is interface{}")
|
||||||
|
}
|
||||||
|
return callbackArgGeneric, nil
|
||||||
|
case reflect.Slice:
|
||||||
|
if typ.Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, errors.New("the only supported slice type is []byte")
|
||||||
|
}
|
||||||
|
return callbackArgBytes, nil
|
||||||
|
case reflect.String:
|
||||||
|
return callbackArgString, nil
|
||||||
|
case reflect.Bool:
|
||||||
|
return callbackArgBool, nil
|
||||||
|
case reflect.Int64:
|
||||||
|
return callbackArgInt64, nil
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
c := callbackArgCast{callbackArgInt64, typ}
|
||||||
|
return c.Run, nil
|
||||||
|
case reflect.Float64:
|
||||||
|
return callbackArgFloat64, nil
|
||||||
|
case reflect.Float32:
|
||||||
|
c := callbackArgCast{callbackArgFloat64, typ}
|
||||||
|
return c.Run, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("don't know how to convert to %s", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackConvertArgs(argv []*C.sqlite3_value, converters []callbackArgConverter, variadic callbackArgConverter) ([]reflect.Value, error) {
|
||||||
|
var args []reflect.Value
|
||||||
|
|
||||||
|
if len(argv) < len(converters) {
|
||||||
|
return nil, fmt.Errorf("function requires at least %d arguments", len(converters))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, arg := range argv[:len(converters)] {
|
||||||
|
v, err := converters[i](arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
if variadic != nil {
|
||||||
|
for _, arg := range argv[len(converters):] {
|
||||||
|
v, err := variadic(arg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
args = append(args, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return args, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type callbackRetConverter func(*C.sqlite3_context, reflect.Value) error
|
||||||
|
|
||||||
|
func callbackRetInteger(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Int64:
|
||||||
|
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
v = v.Convert(reflect.TypeOf(int64(0)))
|
||||||
|
case reflect.Bool:
|
||||||
|
b := v.Interface().(bool)
|
||||||
|
if b {
|
||||||
|
v = reflect.ValueOf(int64(1))
|
||||||
|
} else {
|
||||||
|
v = reflect.ValueOf(int64(0))
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot convert %s to INTEGER", v.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
C.sqlite3_result_int64(ctx, C.sqlite3_int64(v.Interface().(int64)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetFloat(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
switch v.Type().Kind() {
|
||||||
|
case reflect.Float64:
|
||||||
|
case reflect.Float32:
|
||||||
|
v = v.Convert(reflect.TypeOf(float64(0)))
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("cannot convert %s to FLOAT", v.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
C.sqlite3_result_double(ctx, C.double(v.Interface().(float64)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetBlob(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
if v.Type().Kind() != reflect.Slice || v.Type().Elem().Kind() != reflect.Uint8 {
|
||||||
|
return fmt.Errorf("cannot convert %s to BLOB", v.Type())
|
||||||
|
}
|
||||||
|
i := v.Interface()
|
||||||
|
if i == nil || len(i.([]byte)) == 0 {
|
||||||
|
C.sqlite3_result_null(ctx)
|
||||||
|
} else {
|
||||||
|
bs := i.([]byte)
|
||||||
|
C._sqlite3_result_blob(ctx, unsafe.Pointer(&bs[0]), C.int(len(bs)))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRetText(ctx *C.sqlite3_context, v reflect.Value) error {
|
||||||
|
if v.Type().Kind() != reflect.String {
|
||||||
|
return fmt.Errorf("cannot convert %s to TEXT", v.Type())
|
||||||
|
}
|
||||||
|
C._sqlite3_result_text(ctx, C.CString(v.Interface().(string)))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackRet(typ reflect.Type) (callbackRetConverter, error) {
|
||||||
|
switch typ.Kind() {
|
||||||
|
case reflect.Slice:
|
||||||
|
if typ.Elem().Kind() != reflect.Uint8 {
|
||||||
|
return nil, errors.New("the only supported slice type is []byte")
|
||||||
|
}
|
||||||
|
return callbackRetBlob, nil
|
||||||
|
case reflect.String:
|
||||||
|
return callbackRetText, nil
|
||||||
|
case reflect.Bool, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Int, reflect.Uint:
|
||||||
|
return callbackRetInteger, nil
|
||||||
|
case reflect.Float32, reflect.Float64:
|
||||||
|
return callbackRetFloat, nil
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("don't know how to convert to %s", typ)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func callbackError(ctx *C.sqlite3_context, err error) {
|
||||||
|
cstr := C.CString(err.Error())
|
||||||
|
defer C.free(unsafe.Pointer(cstr))
|
||||||
|
C.sqlite3_result_error(ctx, cstr, -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test support code. Tests are not allowed to import "C", so we can't
|
||||||
|
// declare any functions that use C.sqlite3_value.
|
||||||
|
func callbackSyntheticForTests(v reflect.Value, err error) callbackArgConverter {
|
||||||
|
return func(*C.sqlite3_value) (reflect.Value, error) {
|
||||||
|
return v, err
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
package sqlite3
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCallbackArgCast(t *testing.T) {
|
||||||
|
intConv := callbackSyntheticForTests(reflect.ValueOf(int64(math.MaxInt64)), nil)
|
||||||
|
floatConv := callbackSyntheticForTests(reflect.ValueOf(float64(math.MaxFloat64)), nil)
|
||||||
|
errConv := callbackSyntheticForTests(reflect.Value{}, errors.New("test"))
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
f callbackArgConverter
|
||||||
|
o reflect.Value
|
||||||
|
}{
|
||||||
|
{intConv, reflect.ValueOf(int8(-1))},
|
||||||
|
{intConv, reflect.ValueOf(int16(-1))},
|
||||||
|
{intConv, reflect.ValueOf(int32(-1))},
|
||||||
|
{intConv, reflect.ValueOf(uint8(math.MaxUint8))},
|
||||||
|
{intConv, reflect.ValueOf(uint16(math.MaxUint16))},
|
||||||
|
{intConv, reflect.ValueOf(uint32(math.MaxUint32))},
|
||||||
|
// Special case, int64->uint64 is only 1<<63 - 1, not 1<<64 - 1
|
||||||
|
{intConv, reflect.ValueOf(uint64(math.MaxInt64))},
|
||||||
|
{floatConv, reflect.ValueOf(float32(math.Inf(1)))},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
conv := callbackArgCast{test.f, test.o.Type()}
|
||||||
|
val, err := conv.Run(nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Couldn't convert to %s: %s", test.o.Type(), err)
|
||||||
|
} else if !reflect.DeepEqual(val.Interface(), test.o.Interface()) {
|
||||||
|
t.Errorf("Unexpected result from converting to %s: got %v, want %v", test.o.Type(), val.Interface(), test.o.Interface())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
conv := callbackArgCast{errConv, reflect.TypeOf(int8(0))}
|
||||||
|
_, err := conv.Run(nil)
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("Expected error during callbackArgCast, but got none")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCallbackConverters(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
v interface{}
|
||||||
|
err bool
|
||||||
|
}{
|
||||||
|
// Unfortunately, we can't tell which converter was returned,
|
||||||
|
// but we can at least check which types can be converted.
|
||||||
|
{[]byte{0}, false},
|
||||||
|
{"text", false},
|
||||||
|
{true, false},
|
||||||
|
{int8(0), false},
|
||||||
|
{int16(0), false},
|
||||||
|
{int32(0), false},
|
||||||
|
{int64(0), false},
|
||||||
|
{uint8(0), false},
|
||||||
|
{uint16(0), false},
|
||||||
|
{uint32(0), false},
|
||||||
|
{uint64(0), false},
|
||||||
|
{int(0), false},
|
||||||
|
{uint(0), false},
|
||||||
|
{float64(0), false},
|
||||||
|
{float32(0), false},
|
||||||
|
|
||||||
|
{func() {}, true},
|
||||||
|
{complex64(complex(0, 0)), true},
|
||||||
|
{complex128(complex(0, 0)), true},
|
||||||
|
{struct{}{}, true},
|
||||||
|
{map[string]string{}, true},
|
||||||
|
{[]string{}, true},
|
||||||
|
{(*int8)(nil), true},
|
||||||
|
{make(chan int), true},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
_, err := callbackArg(reflect.TypeOf(test.v))
|
||||||
|
if test.err && err == nil {
|
||||||
|
t.Errorf("Expected an error when converting %s, got no error", reflect.TypeOf(test.v))
|
||||||
|
} else if !test.err && err != nil {
|
||||||
|
t.Errorf("Expected converter when converting %s, got error: %s", reflect.TypeOf(test.v), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
_, err := callbackRet(reflect.TypeOf(test.v))
|
||||||
|
if test.err && err == nil {
|
||||||
|
t.Errorf("Expected an error when converting %s, got no error", reflect.TypeOf(test.v))
|
||||||
|
} else if !test.err && err != nil {
|
||||||
|
t.Errorf("Expected converter when converting %s, got error: %s", reflect.TypeOf(test.v), err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue