Merge pull request #44222 from fabianofranz/better_generic_getters_and_describers

Automatic merge from submit-queue (batch tested with PRs 44222, 44614, 44292, 44638)

Smarter generic getters and describers

Makes printers and describers smarter for generic resources.

This traverses unstructured objects and prints their attributes for generic resources (TPR, federated API, etc) in `kubectl get` and `kubectl describe`. Makes use of the object's field names to come up with a best guess for describer labels and get headers, and field value types to understand how to better print it, indent, etc.

A nice intermediate solution while we don't have [get and describe extensions](https://github.com/kubernetes/community/pull/308).

Examples:

```
$ kubectl get serviceclasses
NAME                    KIND                                          BINDABLE   BROKER NAME   OSB GUID
user-provided-service   ServiceClass.v1alpha1.servicecatalog.k8s.io   false      ups-broker    4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468
```

```
$ kubectl describe serviceclasses/user-provided-service
Name:		user-provided-service
Namespace:	
Labels:		<none>
Annotations:	FOO=BAR
		openshift.io/deployment.phase=test
OSB Metadata:	<nil>
Kind:		ServiceClass
Metadata:
  Self Link:		/apis/servicecatalog.k8s.io/v1alpha1/serviceclassesuser-provided-service
  UID:			1509bd96-1b05-11e7-98bd-0242ac110006
  Resource Version:	256
  Creation Timestamp:	2017-04-06T20:10:29Z
Broker Name:		ups-broker
Bindable:		false
Plan Updatable:		false
OSB GUID:		4f6e6cf6-ffdd-425f-a2c7-3c9258ad2468
API Version:		servicecatalog.k8s.io/v1alpha1
Plans:
  Name:		default
  OSB GUID:	86064792-7ea2-467b-af93-ac9694d96d52
  OSB Free:	true
  OSB Metadata:	<nil>
Events:		<none>
```

**Release note**:
```release-note
Improved output on 'kubectl get' and 'kubectl describe' for generic objects.
```
PTAL @pmorie @pwittrock @kubernetes/sig-cli-pr-reviews
pull/6/head
Kubernetes Submit Queue 2017-04-18 17:41:59 -07:00 committed by GitHub
commit 409b0a6f5d
15 changed files with 560 additions and 7 deletions

4
Godeps/Godeps.json generated
View File

@ -986,6 +986,10 @@
"ImportPath": "github.com/exponent-io/jsonpath",
"Rev": "d6023ce2651d8eafb5c75bb0c7167536102ec9f5"
},
{
"ImportPath": "github.com/fatih/camelcase",
"Rev": "f6a740d52f961c60348ebb109adde9f4635d7540"
},
{
"ImportPath": "github.com/fsnotify/fsnotify",
"Comment": "v1.3.1-1-gf12c623",

28
Godeps/LICENSES generated
View File

@ -34163,6 +34163,34 @@ SOFTWARE.
================================================================================
================================================================================
= vendor/github.com/fatih/camelcase licensed under: =
The MIT License (MIT)
Copyright (c) 2015 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.
= vendor/github.com/fatih/camelcase/LICENSE.md 4c59b216ce25dc182cdb837e07ba07c0 -
================================================================================
================================================================================
= vendor/github.com/fsnotify/fsnotify licensed under: =

View File

@ -25,6 +25,8 @@ go_library(
],
tags = ["automanaged"],
deps = [
"//pkg/util/slice:go_default_library",
"//vendor/github.com/fatih/camelcase:go_default_library",
"//vendor/github.com/ghodss/yaml:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",
@ -64,3 +66,11 @@ filegroup(
],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["humanreadable_test.go"],
library = ":go_default_library",
tags = ["automanaged"],
deps = ["//vendor/k8s.io/apimachinery/pkg/apis/meta/v1/unstructured:go_default_library"],
)

View File

@ -21,15 +21,18 @@ import (
"fmt"
"io"
"reflect"
"sort"
"strings"
"text/tabwriter"
"github.com/fatih/camelcase"
"github.com/golang/glog"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/kubernetes/pkg/util/slice"
)
var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too.
@ -201,8 +204,51 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
}
if _, err := meta.Accessor(obj); err == nil {
// we don't recognize this type, but we can still attempt to print some reasonable information about.
unstructured, ok := obj.(runtime.Unstructured)
if !ok {
return fmt.Errorf("error: unknown type %#v", obj)
}
content := unstructured.UnstructuredContent()
// we'll elect a few more fields to print depending on how much columns are already taken
maxDiscoveredFieldsToPrint := 3
maxDiscoveredFieldsToPrint = maxDiscoveredFieldsToPrint - len(h.options.ColumnLabels)
if h.options.WithNamespace { // where's my ternary
maxDiscoveredFieldsToPrint--
}
if h.options.ShowLabels {
maxDiscoveredFieldsToPrint--
}
if maxDiscoveredFieldsToPrint < 0 {
maxDiscoveredFieldsToPrint = 0
}
var discoveredFieldNames []string // we want it predictable so this will be used to sort
ignoreIfDiscovered := []string{"kind", "apiVersion"} // these are already covered
for field, value := range content {
if slice.ContainsString(ignoreIfDiscovered, field, nil) {
continue
}
switch value.(type) {
case map[string]interface{}:
// just simpler types
continue
}
discoveredFieldNames = append(discoveredFieldNames, field)
}
sort.Strings(discoveredFieldNames)
if len(discoveredFieldNames) > maxDiscoveredFieldsToPrint {
discoveredFieldNames = discoveredFieldNames[:maxDiscoveredFieldsToPrint]
}
if !h.options.NoHeaders && t != h.lastType {
headers := []string{"NAME", "KIND"}
for _, discoveredField := range discoveredFieldNames {
fieldAsHeader := strings.ToUpper(strings.Join(camelcase.Split(discoveredField), " "))
headers = append(headers, fieldAsHeader)
}
headers = append(headers, formatLabelHeaders(h.options.ColumnLabels)...)
// LABELS is always the last column.
headers = append(headers, formatShowLabelsHeader(h.options.ShowLabels, t)...)
@ -213,13 +259,8 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
h.lastType = t
}
// we don't recognize this type, but we can still attempt to print some reasonable information about.
unstructured, ok := obj.(runtime.Unstructured)
if !ok {
return fmt.Errorf("error: unknown type %#v", obj)
}
// if the error isn't nil, report the "I don't recognize this" error
if err := printUnstructured(unstructured, w, h.options); err != nil {
if err := printUnstructured(unstructured, w, discoveredFieldNames, h.options); err != nil {
return err
}
return nil
@ -230,7 +271,7 @@ func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) er
}
// TODO: this method assumes the meta/v1 server API, so should be refactored out of this package
func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options PrintOptions) error {
func printUnstructured(unstructured runtime.Unstructured, w io.Writer, additionalFields []string, options PrintOptions) error {
metadata, err := meta.Accessor(unstructured)
if err != nil {
return err
@ -258,11 +299,26 @@ func printUnstructured(unstructured runtime.Unstructured, w io.Writer, options P
kind = kind + "." + version.Version + "." + version.Group
}
}
name := formatResourceName(options.Kind, metadata.GetName(), options.WithKind)
if _, err := fmt.Fprintf(w, "%s\t%s", name, kind); err != nil {
return err
}
for _, field := range additionalFields {
if value, ok := content[field]; ok {
var formattedValue string
switch typedValue := value.(type) {
case []interface{}:
formattedValue = fmt.Sprintf("%d item(s)", len(typedValue))
default:
formattedValue = fmt.Sprintf("%v", value)
}
if _, err := fmt.Fprintf(w, "\t%s", formattedValue); err != nil {
return err
}
}
}
if _, err := fmt.Fprint(w, appendLabels(metadata.GetLabels(), options.ColumnLabels)); err != nil {
return err
}

View File

@ -0,0 +1,90 @@
/*
Copyright 2017 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package printers
import (
"bytes"
"regexp"
"testing"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
)
func TestPrintUnstructuredObject(t *testing.T) {
tests := []struct {
expected string
options PrintOptions
}{
{
expected: "NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\\s+ITEMS\nMyName\\s+Test\\.v1\\.\\s+present\\s+present\\s+1 item\\(s\\)",
},
{
options: PrintOptions{
WithNamespace: true,
},
expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+DUMMY 2\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+present",
},
{
options: PrintOptions{
ShowLabels: true,
WithNamespace: true,
},
expected: "NAMESPACE\\s+NAME\\s+KIND\\s+DUMMY 1\\s+LABELS\nMyNamespace\\s+MyName\\s+Test\\.v1\\.\\s+present\\s+<none>",
},
}
out := bytes.NewBuffer([]byte{})
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Test",
"dummy1": "present",
"dummy2": "present",
"metadata": map[string]interface{}{
"name": "MyName",
"namespace": "MyNamespace",
"creationTimestamp": "2017-04-01T00:00:00Z",
"resourceVersion": 123,
"uid": "00000000-0000-0000-0000-000000000001",
"dummy3": "present",
},
"items": []interface{}{
map[string]interface{}{
"itemBool": true,
"itemInt": 42,
},
},
"url": "http://localhost",
"status": "ok",
},
}
for _, test := range tests {
printer := &HumanReadablePrinter{
options: test.options,
}
printer.PrintObj(obj, out)
matches, err := regexp.MatchString(test.expected, out.String())
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !matches {
t.Errorf("wanted %s, got %s", test.expected, out)
}
}
}

View File

@ -87,6 +87,8 @@ go_library(
"//pkg/kubelet/qos:go_default_library",
"//pkg/printers:go_default_library",
"//pkg/util/node:go_default_library",
"//pkg/util/slice:go_default_library",
"//vendor/github.com/fatih/camelcase:go_default_library",
"//vendor/github.com/golang/glog:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/errors:go_default_library",
"//vendor/k8s.io/apimachinery/pkg/api/meta:go_default_library",

View File

@ -31,6 +31,7 @@ import (
"github.com/golang/glog"
"github.com/fatih/camelcase"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/api/resource"
@ -70,6 +71,7 @@ import (
"k8s.io/kubernetes/pkg/fieldpath"
"k8s.io/kubernetes/pkg/kubelet/qos"
"k8s.io/kubernetes/pkg/printers"
"k8s.io/kubernetes/pkg/util/slice"
)
// Each level has 2 spaces for PrefixWriter
@ -198,6 +200,8 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr
w.Write(LEVEL_0, "Name:\t%s\n", obj.GetName())
w.Write(LEVEL_0, "Namespace:\t%s\n", obj.GetNamespace())
printLabelsMultiline(w, "Labels", obj.GetLabels())
printAnnotationsMultiline(w, "Annotations", obj.GetAnnotations())
printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".metadata.name", ".metadata.namespace", ".metadata.labels", ".metadata.annotations")
if events != nil {
DescribeEvents(events, w)
}
@ -205,6 +209,67 @@ func (g *genericDescriber) Describe(namespace, name string, describerSettings pr
})
}
func printUnstructuredContent(w PrefixWriter, level int, content map[string]interface{}, skipPrefix string, skip ...string) {
fields := []string{}
for field := range content {
fields = append(fields, field)
}
sort.Strings(fields)
for _, field := range fields {
value := content[field]
switch typedValue := value.(type) {
case map[string]interface{}:
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
if slice.ContainsString(skip, skipExpr, nil) {
continue
}
w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field)))
printUnstructuredContent(w, level+1, typedValue, skipExpr, skip...)
case []interface{}:
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
if slice.ContainsString(skip, skipExpr, nil) {
continue
}
w.Write(level, fmt.Sprintf("%s:\n", smartLabelFor(field)))
for _, child := range typedValue {
switch typedChild := child.(type) {
case map[string]interface{}:
printUnstructuredContent(w, level+1, typedChild, skipExpr, skip...)
default:
w.Write(level+1, fmt.Sprintf("%v\n", typedChild))
}
}
default:
skipExpr := fmt.Sprintf("%s.%s", skipPrefix, field)
if slice.ContainsString(skip, skipExpr, nil) {
continue
}
w.Write(level, fmt.Sprintf("%s:\t%v\n", smartLabelFor(field), typedValue))
}
}
}
func smartLabelFor(field string) string {
commonAcronyms := []string{"API", "URL", "UID", "OSB", "GUID"}
splitted := camelcase.Split(field)
for i := 0; i < len(splitted); i++ {
part := splitted[i]
if slice.ContainsString(commonAcronyms, strings.ToUpper(part), nil) {
part = strings.ToUpper(part)
} else {
part = strings.Title(part)
}
splitted[i] = part
}
return strings.Join(splitted, " ")
}
// DefaultObjectDescriber can describe the default Kubernetes objects.
var DefaultObjectDescriber printers.ObjectDescriber

View File

@ -27,6 +27,7 @@ import (
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/kubernetes/federation/apis/federation"
fedfake "k8s.io/kubernetes/federation/client/clientset_generated/federation_internalclientset/fake"
@ -1313,3 +1314,87 @@ func TestPrintLabelsMultiline(t *testing.T) {
}
}
}
func TestDescribeUnstructuredContent(t *testing.T) {
testCases := []struct {
expected string
unexpected string
}{
{
expected: `API Version: v1
Dummy 2: present
Items:
Item Bool: true
Item Int: 42
Kind: Test
Metadata:
Creation Timestamp: 2017-04-01T00:00:00Z
Name: MyName
Namespace: MyNamespace
Resource Version: 123
UID: 00000000-0000-0000-0000-000000000001
Status: ok
URL: http://localhost
`,
},
{
unexpected: "\nDummy 1:\tpresent\n",
},
{
unexpected: "Dummy 1",
},
{
unexpected: "Dummy 3",
},
{
unexpected: "Dummy3",
},
{
unexpected: "dummy3",
},
{
unexpected: "dummy 3",
},
}
out := new(bytes.Buffer)
w := NewPrefixWriter(out)
obj := &unstructured.Unstructured{
Object: map[string]interface{}{
"apiVersion": "v1",
"kind": "Test",
"dummy1": "present",
"dummy2": "present",
"metadata": map[string]interface{}{
"name": "MyName",
"namespace": "MyNamespace",
"creationTimestamp": "2017-04-01T00:00:00Z",
"resourceVersion": 123,
"uid": "00000000-0000-0000-0000-000000000001",
"dummy3": "present",
},
"items": []interface{}{
map[string]interface{}{
"itemBool": true,
"itemInt": 42,
},
},
"url": "http://localhost",
"status": "ok",
},
}
printUnstructuredContent(w, LEVEL_0, obj.UnstructuredContent(), "", ".dummy1", ".metadata.dummy3")
output := out.String()
for _, test := range testCases {
if len(test.expected) > 0 {
if !strings.Contains(output, test.expected) {
t.Errorf("Expected to find %q in: %q", test.expected, output)
}
}
if len(test.unexpected) > 0 {
if strings.Contains(output, test.unexpected) {
t.Errorf("Didn't expect to find %q in: %q", test.unexpected, output)
}
}
}
}

View File

@ -49,6 +49,20 @@ func ShuffleStrings(s []string) []string {
return shuffled
}
// ContainsString checks if a given slice of strings contains the provided string.
// If a modifier func is provided, it is called with the slice item before the comparation.
func ContainsString(slice []string, s string, modifier func(s string) string) bool {
for _, item := range slice {
if item == s {
return true
}
if modifier != nil && modifier(item) == s {
return true
}
}
return false
}
// Int64Slice attaches the methods of Interface to []int64,
// sorting in increasing order.
type Int64Slice []int64

1
vendor/BUILD vendored
View File

@ -144,6 +144,7 @@ filegroup(
"//vendor/github.com/emicklei/go-restful:all-srcs",
"//vendor/github.com/evanphx/json-patch:all-srcs",
"//vendor/github.com/exponent-io/jsonpath:all-srcs",
"//vendor/github.com/fatih/camelcase:all-srcs",
"//vendor/github.com/fsnotify/fsnotify:all-srcs",
"//vendor/github.com/garyburd/redigo/internal:all-srcs",
"//vendor/github.com/garyburd/redigo/redis:all-srcs",

3
vendor/github.com/fatih/camelcase/.travis.yml generated vendored Normal file
View File

@ -0,0 +1,3 @@
language: go
go: 1.4

27
vendor/github.com/fatih/camelcase/BUILD generated vendored Normal file
View File

@ -0,0 +1,27 @@
package(default_visibility = ["//visibility:public"])
licenses(["notice"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
go_library(
name = "go_default_library",
srcs = ["camelcase.go"],
tags = ["automanaged"],
)
filegroup(
name = "package-srcs",
srcs = glob(["**"]),
tags = ["automanaged"],
visibility = ["//visibility:private"],
)
filegroup(
name = "all-srcs",
srcs = [":package-srcs"],
tags = ["automanaged"],
)

20
vendor/github.com/fatih/camelcase/LICENSE.md generated vendored Normal file
View File

@ -0,0 +1,20 @@
The MIT License (MIT)
Copyright (c) 2015 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.

58
vendor/github.com/fatih/camelcase/README.md generated vendored Normal file
View File

@ -0,0 +1,58 @@
# CamelCase [![GoDoc](http://img.shields.io/badge/go-documentation-blue.svg?style=flat-square)](http://godoc.org/github.com/fatih/camelcase) [![Build Status](http://img.shields.io/travis/fatih/camelcase.svg?style=flat-square)](https://travis-ci.org/fatih/camelcase)
CamelCase is a Golang (Go) package to split the words of a camelcase type
string into a slice of words. It can be used to convert a camelcase word (lower
or upper case) into any type of word.
## Splitting rules:
1. If string is not valid UTF-8, return it without splitting as
single item array.
2. Assign all unicode characters into one of 4 sets: lower case
letters, upper case letters, numbers, and all other characters.
3. Iterate through characters of string, introducing splits
between adjacent characters that belong to different sets.
4. Iterate through array of split strings, and if a given string
is upper case:
* if subsequent string is lower case:
* move last character of upper case string to beginning of
lower case string
## Install
```bash
go get github.com/fatih/camelcase
```
## Usage and examples
```go
splitted := camelcase.Split("GolangPackage")
fmt.Println(splitted[0], splitted[1]) // prints: "Golang", "Package"
```
Both lower camel case and upper camel case are supported. For more info please
check: [http://en.wikipedia.org/wiki/CamelCase](http://en.wikipedia.org/wiki/CamelCase)
Below are some example cases:
```
"" => []
"lowercase" => ["lowercase"]
"Class" => ["Class"]
"MyClass" => ["My", "Class"]
"MyC" => ["My", "C"]
"HTML" => ["HTML"]
"PDFLoader" => ["PDF", "Loader"]
"AString" => ["A", "String"]
"SimpleXMLParser" => ["Simple", "XML", "Parser"]
"vimRPCPlugin" => ["vim", "RPC", "Plugin"]
"GL11Version" => ["GL", "11", "Version"]
"99Bottles" => ["99", "Bottles"]
"May5" => ["May", "5"]
"BFG9000" => ["BFG", "9000"]
"BöseÜberraschung" => ["Böse", "Überraschung"]
"Two spaces" => ["Two", " ", "spaces"]
"BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
```

90
vendor/github.com/fatih/camelcase/camelcase.go generated vendored Normal file
View File

@ -0,0 +1,90 @@
// Package camelcase is a micro package to split the words of a camelcase type
// string into a slice of words.
package camelcase
import (
"unicode"
"unicode/utf8"
)
// Split splits the camelcase word and returns a list of words. It also
// supports digits. Both lower camel case and upper camel case are supported.
// For more info please check: http://en.wikipedia.org/wiki/CamelCase
//
// Examples
//
// "" => [""]
// "lowercase" => ["lowercase"]
// "Class" => ["Class"]
// "MyClass" => ["My", "Class"]
// "MyC" => ["My", "C"]
// "HTML" => ["HTML"]
// "PDFLoader" => ["PDF", "Loader"]
// "AString" => ["A", "String"]
// "SimpleXMLParser" => ["Simple", "XML", "Parser"]
// "vimRPCPlugin" => ["vim", "RPC", "Plugin"]
// "GL11Version" => ["GL", "11", "Version"]
// "99Bottles" => ["99", "Bottles"]
// "May5" => ["May", "5"]
// "BFG9000" => ["BFG", "9000"]
// "BöseÜberraschung" => ["Böse", "Überraschung"]
// "Two spaces" => ["Two", " ", "spaces"]
// "BadUTF8\xe2\xe2\xa1" => ["BadUTF8\xe2\xe2\xa1"]
//
// Splitting rules
//
// 1) If string is not valid UTF-8, return it without splitting as
// single item array.
// 2) Assign all unicode characters into one of 4 sets: lower case
// letters, upper case letters, numbers, and all other characters.
// 3) Iterate through characters of string, introducing splits
// between adjacent characters that belong to different sets.
// 4) Iterate through array of split strings, and if a given string
// is upper case:
// if subsequent string is lower case:
// move last character of upper case string to beginning of
// lower case string
func Split(src string) (entries []string) {
// don't split invalid utf8
if !utf8.ValidString(src) {
return []string{src}
}
entries = []string{}
var runes [][]rune
lastClass := 0
class := 0
// split into fields based on class of unicode character
for _, r := range src {
switch true {
case unicode.IsLower(r):
class = 1
case unicode.IsUpper(r):
class = 2
case unicode.IsDigit(r):
class = 3
default:
class = 4
}
if class == lastClass {
runes[len(runes)-1] = append(runes[len(runes)-1], r)
} else {
runes = append(runes, []rune{r})
}
lastClass = class
}
// handle upper case -> lower case sequences, e.g.
// "PDFL", "oader" -> "PDF", "Loader"
for i := 0; i < len(runes)-1; i++ {
if unicode.IsUpper(runes[i][0]) && unicode.IsLower(runes[i+1][0]) {
runes[i+1] = append([]rune{runes[i][len(runes[i])-1]}, runes[i+1]...)
runes[i] = runes[i][:len(runes[i])-1]
}
}
// construct []string from results
for _, s := range runes {
if len(s) > 0 {
entries = append(entries, string(s))
}
}
return
}