/* 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" "fmt" "io" "reflect" "strings" "text/tabwriter" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" metav1beta1 "k8s.io/apimachinery/pkg/apis/meta/v1beta1" "k8s.io/apimachinery/pkg/labels" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" utilruntime "k8s.io/apimachinery/pkg/util/runtime" ) type TablePrinter interface { PrintTable(obj runtime.Object, options PrintOptions) (*metav1beta1.Table, error) } type PrintHandler interface { Handler(columns, columnsWithWide []string, printFunc interface{}) error TableHandler(columns []metav1beta1.TableColumnDefinition, printFunc interface{}) error DefaultTableHandler(columns []metav1beta1.TableColumnDefinition, printFunc interface{}) error } var withNamespacePrefixColumns = []string{"NAMESPACE"} // TODO(erictune): print cluster name too. type handlerEntry struct { columnDefinitions []metav1beta1.TableColumnDefinition printRows bool printFunc reflect.Value args []reflect.Value } // HumanReadablePrinter is an implementation of ResourcePrinter which attempts to provide // more elegant output. It is not threadsafe, but you may call PrintObj repeatedly; headers // will only be printed if the object type changes. This makes it useful for printing items // received from watches. type HumanReadablePrinter struct { handlerMap map[reflect.Type]*handlerEntry defaultHandler *handlerEntry options PrintOptions lastType interface{} skipTabWriter bool encoder runtime.Encoder decoder runtime.Decoder } var _ PrintHandler = &HumanReadablePrinter{} // NewHumanReadablePrinter creates a HumanReadablePrinter. // If encoder and decoder are provided, an attempt to convert unstructured types to internal types is made. func NewHumanReadablePrinter(decoder runtime.Decoder, options PrintOptions) *HumanReadablePrinter { printer := &HumanReadablePrinter{ handlerMap: make(map[reflect.Type]*handlerEntry), options: options, decoder: decoder, } return printer } // NewTablePrinter creates a HumanReadablePrinter suitable for calling PrintTable(). func NewTablePrinter() *HumanReadablePrinter { return &HumanReadablePrinter{ handlerMap: make(map[reflect.Type]*handlerEntry), } } // AddTabWriter sets whether the PrintObj function will format with tabwriter (true // by default). func (a *HumanReadablePrinter) AddTabWriter(t bool) *HumanReadablePrinter { a.skipTabWriter = !t return a } func (a *HumanReadablePrinter) With(fns ...func(PrintHandler)) *HumanReadablePrinter { for _, fn := range fns { fn(a) } return a } // EnsurePrintHeaders sets the HumanReadablePrinter option "NoHeaders" to false // and removes the .lastType that was printed, which forces headers to be // printed in cases where multiple lists of the same resource are printed // consecutively, but are separated by non-printer related information. func (h *HumanReadablePrinter) EnsurePrintHeaders() { h.options.NoHeaders = false h.lastType = nil } // Handler adds a print handler with a given set of columns to HumanReadablePrinter instance. // See ValidatePrintHandlerFunc for required method signature. func (h *HumanReadablePrinter) Handler(columns, columnsWithWide []string, printFunc interface{}) error { var columnDefinitions []metav1beta1.TableColumnDefinition for i, column := range columns { format := "" if i == 0 && strings.EqualFold(column, "name") { format = "name" } columnDefinitions = append(columnDefinitions, metav1beta1.TableColumnDefinition{ Name: column, Description: column, Type: "string", Format: format, }) } for _, column := range columnsWithWide { columnDefinitions = append(columnDefinitions, metav1beta1.TableColumnDefinition{ Name: column, Description: column, Type: "string", Priority: 1, }) } printFuncValue := reflect.ValueOf(printFunc) if err := ValidatePrintHandlerFunc(printFuncValue); err != nil { utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err)) return err } entry := &handlerEntry{ columnDefinitions: columnDefinitions, printFunc: printFuncValue, } objType := printFuncValue.Type().In(0) if _, ok := h.handlerMap[objType]; ok { err := fmt.Errorf("registered duplicate printer for %v", objType) utilruntime.HandleError(err) return err } h.handlerMap[objType] = entry return nil } // TableHandler adds a print handler with a given set of columns to HumanReadablePrinter instance. // See ValidateRowPrintHandlerFunc for required method signature. func (h *HumanReadablePrinter) TableHandler(columnDefinitions []metav1beta1.TableColumnDefinition, printFunc interface{}) error { printFuncValue := reflect.ValueOf(printFunc) if err := ValidateRowPrintHandlerFunc(printFuncValue); err != nil { utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err)) return err } entry := &handlerEntry{ columnDefinitions: columnDefinitions, printRows: true, printFunc: printFuncValue, } objType := printFuncValue.Type().In(0) if _, ok := h.handlerMap[objType]; ok { err := fmt.Errorf("registered duplicate printer for %v", objType) utilruntime.HandleError(err) return err } h.handlerMap[objType] = entry return nil } // DefaultTableHandler registers a set of columns and a print func that is given a chance to process // any object without an explicit handler. Only the most recently set print handler is used. // See ValidateRowPrintHandlerFunc for required method signature. func (h *HumanReadablePrinter) DefaultTableHandler(columnDefinitions []metav1beta1.TableColumnDefinition, printFunc interface{}) error { printFuncValue := reflect.ValueOf(printFunc) if err := ValidateRowPrintHandlerFunc(printFuncValue); err != nil { utilruntime.HandleError(fmt.Errorf("unable to register print function: %v", err)) return err } entry := &handlerEntry{ columnDefinitions: columnDefinitions, printRows: true, printFunc: printFuncValue, } h.defaultHandler = entry return nil } // ValidateRowPrintHandlerFunc validates print handler signature. // printFunc is the function that will be called to print an object. // It must be of the following type: // func printFunc(object ObjectType, options PrintOptions) ([]metav1beta1.TableRow, error) // where ObjectType is the type of the object that will be printed, and the first // return value is an array of rows, with each row containing a number of cells that // match the number of columns defined for that printer function. func ValidateRowPrintHandlerFunc(printFunc reflect.Value) error { if printFunc.Kind() != reflect.Func { return fmt.Errorf("invalid print handler. %#v is not a function", printFunc) } funcType := printFunc.Type() if funcType.NumIn() != 2 || funcType.NumOut() != 2 { return fmt.Errorf("invalid print handler." + "Must accept 2 parameters and return 2 value.") } if funcType.In(1) != reflect.TypeOf((*PrintOptions)(nil)).Elem() || funcType.Out(0) != reflect.TypeOf((*[]metav1beta1.TableRow)(nil)).Elem() || funcType.Out(1) != reflect.TypeOf((*error)(nil)).Elem() { return fmt.Errorf("invalid print handler. The expected signature is: "+ "func handler(obj %v, options PrintOptions) ([]metav1beta1.TableRow, error)", funcType.In(0)) } return nil } // ValidatePrintHandlerFunc validates print handler signature. // printFunc is the function that will be called to print an object. // It must be of the following type: // func printFunc(object ObjectType, w io.Writer, options PrintOptions) error // where ObjectType is the type of the object that will be printed. // DEPRECATED: will be replaced with ValidateRowPrintHandlerFunc func ValidatePrintHandlerFunc(printFunc reflect.Value) error { if printFunc.Kind() != reflect.Func { return fmt.Errorf("invalid print handler. %#v is not a function", printFunc) } funcType := printFunc.Type() if funcType.NumIn() != 3 || funcType.NumOut() != 1 { return fmt.Errorf("invalid print handler." + "Must accept 3 parameters and return 1 value.") } if funcType.In(1) != reflect.TypeOf((*io.Writer)(nil)).Elem() || funcType.In(2) != reflect.TypeOf((*PrintOptions)(nil)).Elem() || funcType.Out(0) != reflect.TypeOf((*error)(nil)).Elem() { return fmt.Errorf("invalid print handler. The expected signature is: "+ "func handler(obj %v, w io.Writer, options PrintOptions) error", funcType.In(0)) } return nil } func (h *HumanReadablePrinter) HandledResources() []string { keys := make([]string, 0) for k := range h.handlerMap { // k.String looks like "*api.PodList" and we want just "pod" api := strings.Split(k.String(), ".") resource := api[len(api)-1] if strings.HasSuffix(resource, "List") { continue } resource = strings.ToLower(resource) keys = append(keys, resource) } return keys } func (h *HumanReadablePrinter) unknown(data []byte, w io.Writer) error { _, err := fmt.Fprintf(w, "Unknown object: %s", string(data)) return err } func printHeader(columnNames []string, w io.Writer) error { if _, err := fmt.Fprintf(w, "%s\n", strings.Join(columnNames, "\t")); err != nil { return err } return nil } // PrintObj prints the obj in a human-friendly format according to the type of the obj. func (h *HumanReadablePrinter) PrintObj(obj runtime.Object, output io.Writer) error { w, found := output.(*tabwriter.Writer) if !found && !h.skipTabWriter { w = GetNewTabWriter(output) output = w defer w.Flush() } // display tables following the rules of options if table, ok := obj.(*metav1beta1.Table); ok { if err := DecorateTable(table, h.options); err != nil { return err } return PrintTable(table, output, h.options) } // check if the object is unstructured. If so, let's attempt to convert it to a type we can understand before // trying to print, since the printers are keyed by type. This is extremely expensive. if h.decoder != nil { obj, _ = decodeUnknownObject(obj, h.decoder) } // print with a registered handler t := reflect.TypeOf(obj) if handler := h.handlerMap[t]; handler != nil { includeHeaders := h.lastType != t && !h.options.NoHeaders if h.lastType != nil && h.lastType != t && !h.options.NoHeaders { fmt.Fprintln(output) } if err := printRowsForHandlerEntry(output, handler, obj, h.options, includeHeaders); err != nil { return err } h.lastType = t return nil } // print with the default handler if set, and use the columns from the last time if h.defaultHandler != nil { includeHeaders := h.lastType != h.defaultHandler && !h.options.NoHeaders if h.lastType != nil && h.lastType != h.defaultHandler && !h.options.NoHeaders { fmt.Fprintln(output) } if err := printRowsForHandlerEntry(output, h.defaultHandler, obj, h.options, includeHeaders); err != nil { return err } h.lastType = h.defaultHandler return nil } // we failed all reasonable printing efforts, report failure return fmt.Errorf("error: unknown type %#v", obj) } func hasCondition(conditions []metav1beta1.TableRowCondition, t metav1beta1.RowConditionType) bool { for _, condition := range conditions { if condition.Type == t { return condition.Status == metav1beta1.ConditionTrue } } return false } // PrintTable prints a table to the provided output respecting the filtering rules for options // for wide columns and filtered rows. It filters out rows that are Completed. You should call // DecorateTable if you receive a table from a remote server before calling PrintTable. func PrintTable(table *metav1beta1.Table, output io.Writer, options PrintOptions) error { if !options.NoHeaders { // avoid printing headers if we have no rows to display if len(table.Rows) == 0 { return nil } first := true for _, column := range table.ColumnDefinitions { if !options.Wide && column.Priority != 0 { continue } if first { first = false } else { fmt.Fprint(output, "\t") } fmt.Fprint(output, strings.ToUpper(column.Name)) } fmt.Fprintln(output) } for _, row := range table.Rows { first := true for i, cell := range row.Cells { if i >= len(table.ColumnDefinitions) { // https://issue.k8s.io/66379 // don't panic in case of bad output from the server, with more cells than column definitions break } column := table.ColumnDefinitions[i] if !options.Wide && column.Priority != 0 { continue } if first { first = false } else { fmt.Fprint(output, "\t") } if cell != nil { fmt.Fprint(output, cell) } } fmt.Fprintln(output) } return nil } // DecorateTable takes a table and attempts to add label columns and the // namespace column. It will fill empty columns with nil (if the object // does not expose metadata). It returns an error if the table cannot // be decorated. func DecorateTable(table *metav1beta1.Table, options PrintOptions) error { width := len(table.ColumnDefinitions) + len(options.ColumnLabels) if options.WithNamespace { width++ } if options.ShowLabels { width++ } columns := table.ColumnDefinitions nameColumn := -1 if options.WithKind && !options.Kind.Empty() { for i := range columns { if columns[i].Format == "name" && columns[i].Type == "string" { nameColumn = i break } } } if width != len(table.ColumnDefinitions) { columns = make([]metav1beta1.TableColumnDefinition, 0, width) if options.WithNamespace { columns = append(columns, metav1beta1.TableColumnDefinition{ Name: "Namespace", Type: "string", }) } columns = append(columns, table.ColumnDefinitions...) for _, label := range formatLabelHeaders(options.ColumnLabels) { columns = append(columns, metav1beta1.TableColumnDefinition{ Name: label, Type: "string", }) } if options.ShowLabels { columns = append(columns, metav1beta1.TableColumnDefinition{ Name: "Labels", Type: "string", }) } } rows := table.Rows includeLabels := len(options.ColumnLabels) > 0 || options.ShowLabels if includeLabels || options.WithNamespace || nameColumn != -1 { for i := range rows { row := rows[i] if nameColumn != -1 { row.Cells[nameColumn] = fmt.Sprintf("%s/%s", strings.ToLower(options.Kind.String()), row.Cells[nameColumn]) } var m metav1.Object if obj := row.Object.Object; obj != nil { if acc, err := meta.Accessor(obj); err == nil { m = acc } } // if we can't get an accessor, fill out the appropriate columns with empty spaces if m == nil { if options.WithNamespace { r := make([]interface{}, 1, width) row.Cells = append(r, row.Cells...) } for j := 0; j < width-len(row.Cells); j++ { row.Cells = append(row.Cells, nil) } rows[i] = row continue } if options.WithNamespace { r := make([]interface{}, 1, width) r[0] = m.GetNamespace() row.Cells = append(r, row.Cells...) } if includeLabels { row.Cells = appendLabelCells(row.Cells, m.GetLabels(), options) } rows[i] = row } } table.ColumnDefinitions = columns table.Rows = rows return nil } // PrintTable returns a table for the provided object, using the printer registered for that type. It returns // a table that includes all of the information requested by options, but will not remove rows or columns. The // caller is responsible for applying rules related to filtering rows or columns. func (h *HumanReadablePrinter) PrintTable(obj runtime.Object, options PrintOptions) (*metav1beta1.Table, error) { t := reflect.TypeOf(obj) handler, ok := h.handlerMap[t] if !ok { return nil, fmt.Errorf("no table handler registered for this type %v", t) } if !handler.printRows { return h.legacyPrinterToTable(obj, handler) } args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)} results := handler.printFunc.Call(args) if !results[1].IsNil() { return nil, results[1].Interface().(error) } columns := handler.columnDefinitions if !options.Wide { columns = make([]metav1beta1.TableColumnDefinition, 0, len(handler.columnDefinitions)) for i := range handler.columnDefinitions { if handler.columnDefinitions[i].Priority != 0 { continue } columns = append(columns, handler.columnDefinitions[i]) } } table := &metav1beta1.Table{ ListMeta: metav1.ListMeta{ ResourceVersion: "", }, ColumnDefinitions: columns, Rows: results[0].Interface().([]metav1beta1.TableRow), } if m, err := meta.ListAccessor(obj); err == nil { table.ResourceVersion = m.GetResourceVersion() table.SelfLink = m.GetSelfLink() table.Continue = m.GetContinue() } else { if m, err := meta.CommonAccessor(obj); err == nil { table.ResourceVersion = m.GetResourceVersion() table.SelfLink = m.GetSelfLink() } } if err := DecorateTable(table, options); err != nil { return nil, err } return table, nil } // printRowsForHandlerEntry prints the incremental table output (headers if the current type is // different from lastType) including all the rows in the object. It returns the current type // or an error, if any. func printRowsForHandlerEntry(output io.Writer, handler *handlerEntry, obj runtime.Object, options PrintOptions, includeHeaders bool) error { var results []reflect.Value if handler.printRows { args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(options)} results = handler.printFunc.Call(args) if !results[1].IsNil() { return results[1].Interface().(error) } } if includeHeaders { var headers []string for _, column := range handler.columnDefinitions { if column.Priority != 0 && !options.Wide { continue } headers = append(headers, strings.ToUpper(column.Name)) } headers = append(headers, formatLabelHeaders(options.ColumnLabels)...) // LABELS is always the last column. headers = append(headers, formatShowLabelsHeader(options.ShowLabels)...) if options.WithNamespace { headers = append(withNamespacePrefixColumns, headers...) } printHeader(headers, output) } if !handler.printRows { // TODO: this code path is deprecated and will be removed when all handlers are row printers args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(output), reflect.ValueOf(options)} resultValue := handler.printFunc.Call(args)[0] if resultValue.IsNil() { return nil } return resultValue.Interface().(error) } if results[1].IsNil() { rows := results[0].Interface().([]metav1beta1.TableRow) printRows(output, rows, options) return nil } return results[1].Interface().(error) } // printRows writes the provided rows to output. func printRows(output io.Writer, rows []metav1beta1.TableRow, options PrintOptions) { for _, row := range rows { if options.WithNamespace { if obj := row.Object.Object; obj != nil { if m, err := meta.Accessor(obj); err == nil { fmt.Fprint(output, m.GetNamespace()) } } fmt.Fprint(output, "\t") } for i, cell := range row.Cells { if i != 0 { fmt.Fprint(output, "\t") } else { // TODO: remove this once we drop the legacy printers if options.WithKind && !options.Kind.Empty() { fmt.Fprintf(output, "%s/%s", strings.ToLower(options.Kind.String()), cell) continue } } fmt.Fprint(output, cell) } hasLabels := len(options.ColumnLabels) > 0 if obj := row.Object.Object; obj != nil && (hasLabels || options.ShowLabels) { if m, err := meta.Accessor(obj); err == nil { for _, value := range labelValues(m.GetLabels(), options) { output.Write([]byte("\t")) output.Write([]byte(value)) } } } output.Write([]byte("\n")) } } // legacyPrinterToTable uses the old printFunc with tabbed writer to generate a table. // TODO: remove when all legacy printers are removed. func (h *HumanReadablePrinter) legacyPrinterToTable(obj runtime.Object, handler *handlerEntry) (*metav1beta1.Table, error) { printFunc := handler.printFunc table := &metav1beta1.Table{ ColumnDefinitions: handler.columnDefinitions, } options := PrintOptions{ NoHeaders: true, Wide: true, } buf := &bytes.Buffer{} args := []reflect.Value{reflect.ValueOf(obj), reflect.ValueOf(buf), reflect.ValueOf(options)} if meta.IsListType(obj) { listInterface, ok := obj.(metav1.ListInterface) if ok { table.ListMeta.SelfLink = listInterface.GetSelfLink() table.ListMeta.ResourceVersion = listInterface.GetResourceVersion() table.ListMeta.Continue = listInterface.GetContinue() } // TODO: this uses more memory than it has to, as we refactor printers we should remove the need // for this. args[0] = reflect.ValueOf(obj) resultValue := printFunc.Call(args)[0] if !resultValue.IsNil() { return nil, resultValue.Interface().(error) } data := buf.Bytes() i := 0 items, err := meta.ExtractList(obj) if err != nil { return nil, err } for len(data) > 0 { cells, remainder := tabbedLineToCells(data, len(table.ColumnDefinitions)) table.Rows = append(table.Rows, metav1beta1.TableRow{ Cells: cells, Object: runtime.RawExtension{Object: items[i]}, }) data = remainder i++ } } else { args[0] = reflect.ValueOf(obj) resultValue := printFunc.Call(args)[0] if !resultValue.IsNil() { return nil, resultValue.Interface().(error) } data := buf.Bytes() cells, _ := tabbedLineToCells(data, len(table.ColumnDefinitions)) table.Rows = append(table.Rows, metav1beta1.TableRow{ Cells: cells, Object: runtime.RawExtension{Object: obj}, }) } return table, nil } // 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, additionalFields []string, options PrintOptions) error { metadata, err := meta.Accessor(unstructured) if err != nil { return err } if options.WithNamespace { if _, err := fmt.Fprintf(w, "%s\t", metadata.GetNamespace()); err != nil { return err } } content := unstructured.UnstructuredContent() kind := "<missing>" if objKind, ok := content["kind"]; ok { if str, ok := objKind.(string); ok { kind = str } } if objAPIVersion, ok := content["apiVersion"]; ok { if str, ok := objAPIVersion.(string); ok { version, err := schema.ParseGroupVersion(str) if err != nil { return err } 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 } if _, err := fmt.Fprint(w, AppendAllLabels(options.ShowLabels, metadata.GetLabels())); err != nil { return err } return nil } func formatLabelHeaders(columnLabels []string) []string { formHead := make([]string, len(columnLabels)) for i, l := range columnLabels { p := strings.Split(l, "/") formHead[i] = strings.ToUpper((p[len(p)-1])) } return formHead } // headers for --show-labels=true func formatShowLabelsHeader(showLabels bool) []string { if showLabels { return []string{"LABELS"} } return nil } // labelValues returns a slice of value columns matching the requested print options. func labelValues(itemLabels map[string]string, opts PrintOptions) []string { var values []string for _, key := range opts.ColumnLabels { values = append(values, itemLabels[key]) } if opts.ShowLabels { values = append(values, labels.FormatLabels(itemLabels)) } return values } // appendLabelCells returns a slice of value columns matching the requested print options. // Intended for use with tables. func appendLabelCells(values []interface{}, itemLabels map[string]string, opts PrintOptions) []interface{} { for _, key := range opts.ColumnLabels { values = append(values, itemLabels[key]) } if opts.ShowLabels { values = append(values, labels.FormatLabels(itemLabels)) } return values } // FormatResourceName receives a resource kind, name, and boolean specifying // whether or not to update the current name to "kind/name" func FormatResourceName(kind schema.GroupKind, name string, withKind bool) string { if !withKind || kind.Empty() { return name } return strings.ToLower(kind.String()) + "/" + name } func AppendLabels(itemLabels map[string]string, columnLabels []string) string { var buffer bytes.Buffer for _, cl := range columnLabels { buffer.WriteString(fmt.Sprint("\t")) if il, ok := itemLabels[cl]; ok { buffer.WriteString(fmt.Sprint(il)) } else { buffer.WriteString("<none>") } } return buffer.String() } // Append all labels to a single column. We need this even when show-labels flag* is // false, since this adds newline delimiter to the end of each row. func AppendAllLabels(showLabels bool, itemLabels map[string]string) string { var buffer bytes.Buffer if showLabels { buffer.WriteString(fmt.Sprint("\t")) buffer.WriteString(labels.FormatLabels(itemLabels)) } buffer.WriteString("\n") return buffer.String() } // check if the object is unstructured. If so, attempt to convert it to a type we can understand. func decodeUnknownObject(obj runtime.Object, decoder runtime.Decoder) (runtime.Object, error) { var err error switch t := obj.(type) { case runtime.Unstructured: if objBytes, err := runtime.Encode(unstructured.UnstructuredJSONScheme, obj); err == nil { if decodedObj, err := runtime.Decode(decoder, objBytes); err == nil { obj = decodedObj } } case *runtime.Unknown: if decodedObj, err := runtime.Decode(decoder, t.Raw); err == nil { obj = decodedObj } } return obj, err } func tabbedLineToCells(data []byte, expected int) ([]interface{}, []byte) { var remainder []byte max := bytes.Index(data, []byte("\n")) if max != -1 { remainder = data[max+1:] data = data[:max] } cells := make([]interface{}, expected) for i := 0; i < expected; i++ { next := bytes.Index(data, []byte("\t")) if next == -1 { cells[i] = string(data) // fill the remainder with empty strings, this indicates a printer bug for j := i + 1; j < expected; j++ { cells[j] = "" } break } cells[i] = string(data[:next]) data = data[next+1:] } return cells, remainder }