SEBA-672 model update commands

Change-Id: I7fef3a4c1ee5ccb8c33a01f37a772142a82249c1
diff --git a/commands/backup.go b/commands/backup.go
index db6e51a..9d11db9 100644
--- a/commands/backup.go
+++ b/commands/backup.go
@@ -20,7 +20,6 @@
 	"errors"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
-	"github.com/opencord/cordctl/format"
 	"time"
 )
 
@@ -139,27 +138,13 @@
 
 	// STEP 6: Show results
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_BACKUP_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	data := make([]BackupOutput, 1)
 	data[0].Chunks = h.chunks
 	data[0].Bytes = h.bytes
 	data[0].Status = h.status
 	data[0].Checksum = h.GetChecksum()
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
@@ -235,14 +220,6 @@
 
 	// STEP 5: Show results
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_BACKUP_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	data := make([]BackupOutput, 1)
 	data[0].Checksum = upload_result.GetFieldByName("checksum").(string)
 	data[0].Chunks = int(upload_result.GetFieldByName("chunks_received").(int32))
@@ -254,13 +231,7 @@
 		data[0].Status = "FAILURE"
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_BACKUP_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
diff --git a/commands/command.go b/commands/command.go
index 6a8d663..53f1d8a 100644
--- a/commands/command.go
+++ b/commands/command.go
@@ -80,6 +80,7 @@
 	Cert     string `long:"tlscert" value-name:"CERT_FILE" description:"Path to TLS vertificate file"`
 	Key      string `long:"tlskey" value-name:"KEY_FILE" description:"Path to TLS key file"`
 	Verify   bool   `long:"tlsverify" description:"Use TLS and verify the remote"`
+	Yes      bool   `short:"y" long:"yes" description:"answer yes to any confirmation prompts"`
 }
 
 type OutputOptions struct {
@@ -143,6 +144,7 @@
 		GlobalConfig.Password = GlobalOptions.Password
 	}
 
+	// Generate error messages for required settings
 	if GlobalConfig.Server == "" {
 		log.Fatal("Server is not set. Please update config file or use the -s option")
 	}
@@ -179,3 +181,22 @@
 		}
 	}
 }
+
+// Applies common output options to format and generate output
+func FormatAndGenerateOutput(options *OutputOptions, default_format string, quiet_format string, data interface{}) {
+	outputFormat := CharReplacer.Replace(options.Format)
+	if outputFormat == "" {
+		outputFormat = default_format
+	}
+	if (options.Quiet) && (quiet_format != "") {
+		outputFormat = quiet_format
+	}
+
+	result := CommandResult{
+		Format:   format.Format(outputFormat),
+		OutputAs: toOutputType(options.OutputAs),
+		Data:     data,
+	}
+
+	GenerateOutput(&result)
+}
diff --git a/commands/common.go b/commands/common.go
index 9461f99..93c1b5d 100644
--- a/commands/common.go
+++ b/commands/common.go
@@ -16,6 +16,7 @@
 package commands
 
 import (
+	"bufio"
 	b64 "encoding/base64"
 	"fmt"
 	"github.com/fullstorydev/grpcurl"
@@ -23,6 +24,9 @@
 	"golang.org/x/net/context"
 	"google.golang.org/grpc"
 	reflectpb "google.golang.org/grpc/reflection/grpc_reflection_v1alpha"
+	"log"
+	"os"
+	"strings"
 )
 
 func GenerateHeaders() []string {
@@ -62,3 +66,30 @@
 		fmt.Printf(format, args...)
 	}
 }
+
+// Print a confirmation prompt and get a response from the user
+func Confirmf(format string, args ...interface{}) bool {
+	if GlobalOptions.Yes {
+		return true
+	}
+
+	reader := bufio.NewReader(os.Stdin)
+
+	for {
+		msg := fmt.Sprintf(format, args...)
+		fmt.Print(msg)
+
+		response, err := reader.ReadString('\n')
+		if err != nil {
+			log.Fatal(err)
+		}
+
+		response = strings.ToLower(strings.TrimSpace(response))
+
+		if response == "y" || response == "yes" {
+			return true
+		} else if response == "n" || response == "no" {
+			return false
+		}
+	}
+}
diff --git a/commands/handler.go b/commands/handler.go
index 8aadb44..678883d 100644
--- a/commands/handler.go
+++ b/commands/handler.go
@@ -69,7 +69,15 @@
 	}
 
 	for k, v := range fields {
-		dmsg.TrySetFieldByName(k, v)
+		// _json is a special field name that indicates we should unmarshal json data
+		if k == "_json" {
+			err = dmsg.UnmarshalMergeJSON(v.([]byte))
+			if err != nil {
+				return err
+			}
+		} else {
+			dmsg.SetFieldByName(k, v)
+		}
 	}
 	delete(h.Fields, dmsg.XXX_MessageName())
 
diff --git a/commands/models.go b/commands/models.go
index 16e4dc4..26e9d9f 100644
--- a/commands/models.go
+++ b/commands/models.go
@@ -20,15 +20,9 @@
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
-	"github.com/opencord/cordctl/format"
-	"sort"
 	"strings"
 )
 
-const (
-	DEFAULT_MODEL_AVAILABLE_FORMAT = "{{ . }}"
-)
-
 type ModelNameString string
 
 type ModelList struct {
@@ -42,13 +36,22 @@
 	} `positional-args:"yes" required:"yes"`
 }
 
-type ModelAvailable struct {
+type ModelUpdate struct {
 	OutputOptions
+	Filter    string `long:"filter" description:"Comma-separated list of filters"`
+	SetFields string `long:"set-field" description:"Comma-separated list of field=value to set"`
+	SetJSON   string `long:"set-json" description:"JSON dictionary to use for settings fields"`
+	Args      struct {
+		ModelName ModelNameString
+	} `positional-args:"yes" required:"yes"`
+	IDArgs struct {
+		ID []int32
+	} `positional-args:"yes" required:"no"`
 }
 
 type ModelOpts struct {
-	List      ModelList      `command:"list"`
-	Available ModelAvailable `command:"available"`
+	List   ModelList   `command:"list"`
+	Update ModelUpdate `command:"update"`
 }
 
 var modelOpts = ModelOpts{}
@@ -57,42 +60,6 @@
 	parser.AddCommand("model", "model commands", "Commands to query and manipulate XOS models", &modelOpts)
 }
 
-func (options *ModelAvailable) Execute(args []string) error {
-	conn, descriptor, err := InitReflectionClient()
-	if err != nil {
-		return err
-	}
-
-	defer conn.Close()
-
-	models, err := GetModelNames(descriptor)
-	if err != nil {
-		return err
-	}
-
-	model_names := []string{}
-	for k := range models {
-		model_names = append(model_names, k)
-	}
-
-	sort.Strings(model_names)
-
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_MODEL_AVAILABLE_FORMAT
-	}
-
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     model_names,
-	}
-
-	GenerateOutput(&result)
-
-	return nil
-}
-
 func (options *ModelList) Execute(args []string) error {
 	conn, descriptor, err := InitReflectionClient()
 	if err != nil {
@@ -106,18 +73,12 @@
 		return err
 	}
 
-	var models []*dynamic.Message
-
-	queries, err := CommaSeparatedQueryToMap(options.Filter)
+	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
 	if err != nil {
 		return err
 	}
 
-	if len(queries) == 0 {
-		models, err = ListModels(conn, descriptor, string(options.Args.ModelName))
-	} else {
-		models, err = FilterModels(conn, descriptor, string(options.Args.ModelName), queries)
-	}
+	models, err := ListOrFilterModels(conn, descriptor, string(options.Args.ModelName), queries)
 	if err != nil {
 		return err
 	}
@@ -167,21 +128,89 @@
 		}
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = default_format.String()
-	}
-	if options.Quiet {
-		outputFormat = "{{.Id}}"
+	FormatAndGenerateOutput(&options.OutputOptions, default_format.String(), "{{.id}}", data)
+
+	return nil
+}
+
+func (options *ModelUpdate) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
+	defer conn.Close()
+
+	err = CheckModelName(descriptor, string(options.Args.ModelName))
+	if err != nil {
+		return err
 	}
 
-	GenerateOutput(&result)
+	if (len(options.IDArgs.ID) == 0 && len(options.Filter) == 0) ||
+		(len(options.IDArgs.ID) != 0 && len(options.Filter) != 0) {
+		return fmt.Errorf("Use either an ID or a --filter to specify which models to update")
+	}
+
+	queries, err := CommaSeparatedQueryToMap(options.Filter, true)
+	if err != nil {
+		return err
+	}
+
+	updates, err := CommaSeparatedQueryToMap(options.SetFields, true)
+	if err != nil {
+		return err
+	}
+
+	modelName := string(options.Args.ModelName)
+
+	var models []*dynamic.Message
+
+	if len(options.IDArgs.ID) > 0 {
+		models = make([]*dynamic.Message, len(options.IDArgs.ID))
+		for i, id := range options.IDArgs.ID {
+			models[i], err = GetModel(conn, descriptor, modelName, id)
+			if err != nil {
+				return err
+			}
+		}
+	} else {
+		models, err = ListOrFilterModels(conn, descriptor, modelName, queries)
+		if err != nil {
+			return err
+		}
+	}
+
+	if len(models) == 0 {
+		return fmt.Errorf("Filter matches no objects")
+	} else if len(models) > 1 {
+		if !Confirmf("Filter matches %d objects. Continue [y/n] ? ", len(models)) {
+			return fmt.Errorf("Aborted by user")
+		}
+	}
+
+	fields := make(map[string]interface{})
+
+	if len(options.SetJSON) > 0 {
+		fields["_json"] = []byte(options.SetJSON)
+	}
+
+	for fieldName, value := range updates {
+		value = value[1:]
+		proto_value, err := TypeConvert(descriptor, modelName, fieldName, value)
+		if err != nil {
+			return err
+		}
+		fields[fieldName] = proto_value
+	}
+
+	for _, model := range models {
+		fields["id"] = model.GetFieldByName("id").(int32)
+		UpdateModel(conn, descriptor, modelName, fields)
+	}
+
+	count := len(models)
+	FormatAndGenerateOutput(&options.OutputOptions, "{{.}} models updated.", "{{.}}", count)
+
 	return nil
 }
 
diff --git a/commands/modeltypes.go b/commands/modeltypes.go
new file mode 100644
index 0000000..be2294c
--- /dev/null
+++ b/commands/modeltypes.go
@@ -0,0 +1,65 @@
+/*
+ * Portions copyright 2019-present Open Networking Foundation
+ * Original copyright 2019-present Ciena Corporation
+ *
+ * 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 commands
+
+import (
+	flags "github.com/jessevdk/go-flags"
+	"sort"
+)
+
+const (
+	DEFAULT_MODELTYPE_LIST_FORMAT = "{{ . }}"
+)
+
+type ModelTypeList struct {
+	OutputOptions
+}
+
+type ModelTypeOpts struct {
+	List ModelTypeList `command:"list"`
+}
+
+var modelTypeOpts = ModelTypeOpts{}
+
+func RegisterModelTypeCommands(parser *flags.Parser) {
+	parser.AddCommand("modeltype", "model type commands", "Commands to query the types of models", &modelTypeOpts)
+}
+
+func (options *ModelTypeList) Execute(args []string) error {
+	conn, descriptor, err := InitReflectionClient()
+	if err != nil {
+		return err
+	}
+
+	defer conn.Close()
+
+	models, err := GetModelNames(descriptor)
+	if err != nil {
+		return err
+	}
+
+	model_names := []string{}
+	for k := range models {
+		model_names = append(model_names, k)
+	}
+
+	sort.Strings(model_names)
+
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_MODELTYPE_LIST_FORMAT, "", model_names)
+
+	return nil
+}
diff --git a/commands/orm.go b/commands/orm.go
index 8a4108e..65304a2 100644
--- a/commands/orm.go
+++ b/commands/orm.go
@@ -51,21 +51,21 @@
 //    "==foo"  --> "EQUAL", "foo"
 func DecodeOperator(query string) (string, string, bool, error) {
 	if strings.HasPrefix(query, "!=") {
-		return query[2:], "EQUAL", true, nil
+		return strings.TrimSpace(query[2:]), "EQUAL", true, nil
 	} else if strings.HasPrefix(query, "==") {
 		return "", "", false, errors.New("Operator == is now allowed. Suggest using = instead.")
 	} else if strings.HasPrefix(query, "=") {
-		return query[1:], "EQUAL", false, nil
+		return strings.TrimSpace(query[1:]), "EQUAL", false, nil
 	} else if strings.HasPrefix(query, ">=") {
-		return query[2:], "GREATER_THAN_OR_EQUAL", false, nil
+		return strings.TrimSpace(query[2:]), "GREATER_THAN_OR_EQUAL", false, nil
 	} else if strings.HasPrefix(query, ">") {
-		return query[1:], "GREATER_THAN", false, nil
+		return strings.TrimSpace(query[1:]), "GREATER_THAN", false, nil
 	} else if strings.HasPrefix(query, "<=") {
-		return query[2:], "LESS_THAN_OR_EQUAL", false, nil
+		return strings.TrimSpace(query[2:]), "LESS_THAN_OR_EQUAL", false, nil
 	} else if strings.HasPrefix(query, "<") {
-		return query[1:], "LESS_THAN", false, nil
+		return strings.TrimSpace(query[1:]), "LESS_THAN", false, nil
 	} else {
-		return query, "EQUAL", false, nil
+		return strings.TrimSpace(query), "EQUAL", false, nil
 	}
 }
 
@@ -95,15 +95,32 @@
 
 		nm := dynamic.NewMessage(elements_mt)
 
-		field_type := h.Model.FindFieldByName(field_name).GetType()
-		if field_type == descriptor.FieldDescriptorProto_TYPE_INT32 {
-			i, _ := strconv.ParseInt(value, 10, 32)
+		field_descriptor := h.Model.FindFieldByName(field_name)
+		if field_descriptor == nil {
+			return fmt.Errorf("Field %s does not exist", field_name)
+		}
+
+		field_type := field_descriptor.GetType()
+		switch field_type {
+		case descriptor.FieldDescriptorProto_TYPE_INT32:
+			var i int64
+			i, err = strconv.ParseInt(value, 10, 32)
 			nm.SetFieldByName("iValue", int32(i))
-		} else if field_type == descriptor.FieldDescriptorProto_TYPE_UINT32 {
-			i, _ := strconv.ParseInt(value, 10, 32)
+		case descriptor.FieldDescriptorProto_TYPE_UINT32:
+			var i int64
+			i, err = strconv.ParseInt(value, 10, 32)
 			nm.SetFieldByName("iValue", uint32(i))
-		} else {
+		case descriptor.FieldDescriptorProto_TYPE_FLOAT:
+			err = errors.New("Floating point filters are unsupported")
+		case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
+			err = errors.New("Floating point filters are unsupported")
+		default:
 			nm.SetFieldByName("sValue", value)
+			err = nil
+		}
+
+		if err != nil {
+			return err
 		}
 
 		nm.SetFieldByName("name", field_name)
@@ -120,33 +137,83 @@
 }
 
 // Take a string list of queries and turns it into a map of queries
-func QueryStringsToMap(query_args []string) (map[string]string, error) {
+func QueryStringsToMap(query_args []string, allow_inequality bool) (map[string]string, error) {
 	queries := make(map[string]string)
 	for _, query_str := range query_args {
-		query_str := strings.Trim(query_str, " ")
+		query_str := strings.TrimSpace(query_str)
 		operator_pos := -1
 		for i, ch := range query_str {
-			if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
-				operator_pos = i
-				break
+			if allow_inequality {
+				if (ch == '!') || (ch == '=') || (ch == '>') || (ch == '<') {
+					operator_pos = i
+					break
+				}
+			} else {
+				if ch == '=' {
+					operator_pos = i
+					break
+				}
 			}
 		}
 		if operator_pos == -1 {
-			return nil, fmt.Errorf("Illegal query string %s", query_str)
+			return nil, fmt.Errorf("Illegal operator/value string %s", query_str)
 		}
-		queries[query_str[:operator_pos]] = query_str[operator_pos:]
+		queries[strings.TrimSpace(query_str[:operator_pos])] = query_str[operator_pos:]
 	}
 	return queries, nil
 }
 
 // Take a string of comma-separated queries and turn it into a map of queries
-func CommaSeparatedQueryToMap(query_str string) (map[string]string, error) {
+func CommaSeparatedQueryToMap(query_str string, allow_inequality bool) (map[string]string, error) {
 	if query_str == "" {
 		return nil, nil
 	}
 
 	query_strings := strings.Split(query_str, ",")
-	return QueryStringsToMap(query_strings)
+	return QueryStringsToMap(query_strings, allow_inequality)
+}
+
+// Convert a string into the appropriate gRPC type for a given field
+func TypeConvert(source grpcurl.DescriptorSource, modelName string, field_name string, v string) (interface{}, error) {
+	model_descriptor, err := source.FindSymbol("xos." + modelName)
+	if err != nil {
+		return nil, err
+	}
+	model_md, ok := model_descriptor.(*desc.MessageDescriptor)
+	if !ok {
+		return nil, fmt.Errorf("Failed to convert model %s to a messagedescriptor", modelName)
+	}
+	field_descriptor := model_md.FindFieldByName(field_name)
+	if field_descriptor == nil {
+		return nil, fmt.Errorf("Field %s does not exist in model %s", field_name, modelName)
+	}
+	field_type := field_descriptor.GetType()
+
+	var result interface{}
+
+	switch field_type {
+	case descriptor.FieldDescriptorProto_TYPE_INT32:
+		var i int64
+		i, err = strconv.ParseInt(v, 10, 32)
+		result = int32(i)
+	case descriptor.FieldDescriptorProto_TYPE_UINT32:
+		var i int64
+		i, err = strconv.ParseInt(v, 10, 32)
+		result = uint32(i)
+	case descriptor.FieldDescriptorProto_TYPE_FLOAT:
+		var f float64
+		f, err = strconv.ParseFloat(v, 32)
+		result = float32(f)
+	case descriptor.FieldDescriptorProto_TYPE_DOUBLE:
+		var f float64
+		f, err = strconv.ParseFloat(v, 64)
+		result = f
+	default:
+		result = v
+		err = nil
+	}
+
+	return result, err
 }
 
 // Return a list of all available model names
@@ -211,6 +278,34 @@
 	return nil
 }
 
+// Update a model in XOS given a map of fields
+func UpdateModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, fields map[string]interface{}) error {
+	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
+	defer cancel()
+
+	headers := GenerateHeaders()
+
+	h := &RpcEventHandler{
+		Fields: map[string]map[string]interface{}{"xos." + modelName: fields},
+	}
+	err := grpcurl.InvokeRPC(ctx, descriptor, conn, "xos.xos.Update"+modelName, headers, h, h.GetParams)
+	if err != nil {
+		return err
+	} else if h.Status != nil && h.Status.Err() != nil {
+		return h.Status.Err()
+	}
+
+	resp, err := dynamic.AsDynamicMessage(h.Response)
+	if err != nil {
+		return err
+	}
+
+	// TODO: Do we need to do anything with the response?
+	_ = resp
+
+	return nil
+}
+
 // Get a model from XOS given its ID
 func GetModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, id int32) (*dynamic.Message, error) {
 	ctx, cancel := context.WithTimeout(context.Background(), GlobalConfig.Grpc.Timeout)
@@ -385,6 +480,15 @@
 	return ItemsToDynamicMessageList(items), nil
 }
 
+// Call ListModels or FilterModels as appropriate
+func ListOrFilterModels(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) ([]*dynamic.Message, error) {
+	if len(queries) == 0 {
+		return ListModels(conn, descriptor, modelName)
+	} else {
+		return FilterModels(conn, descriptor, modelName, queries)
+	}
+}
+
 // Get a model from XOS given a fieldName/fieldValue
 func FindModel(conn *grpc.ClientConn, descriptor grpcurl.DescriptorSource, modelName string, queries map[string]string) (*dynamic.Message, error) {
 	models, err := FilterModels(conn, descriptor, modelName, queries)
diff --git a/commands/services.go b/commands/services.go
index a38c469..99d39c0 100644
--- a/commands/services.go
+++ b/commands/services.go
@@ -21,7 +21,7 @@
 	"github.com/fullstorydev/grpcurl"
 	flags "github.com/jessevdk/go-flags"
 	"github.com/jhump/protoreflect/dynamic"
-	"github.com/opencord/cordctl/format"
+	//"github.com/opencord/cordctl/format"
 )
 
 const (
@@ -86,14 +86,6 @@
 		return err
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_SERVICE_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Id}}"
-	}
-
 	data := make([]ServiceListOutput, len(items.([]interface{})))
 
 	for i, item := range items.([]interface{}) {
@@ -103,12 +95,7 @@
 		data[i].State = val.GetFieldByName("state").(string)
 	}
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_SERVICE_FORMAT, "{{.Name}}", data)
 
-	GenerateOutput(&result)
 	return nil
 }
diff --git a/commands/transfer.go b/commands/transfer.go
index 819e82f..699a4f7 100644
--- a/commands/transfer.go
+++ b/commands/transfer.go
@@ -20,7 +20,6 @@
 	"errors"
 	"fmt"
 	flags "github.com/jessevdk/go-flags"
-	"github.com/opencord/cordctl/format"
 	"strings"
 )
 
@@ -92,27 +91,13 @@
 			upload_result.GetFieldByName("checksum").(string))
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_TRANSFER_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	data := make([]TransferOutput, 1)
 	data[0].Checksum = upload_result.GetFieldByName("checksum").(string)
 	data[0].Chunks = int(upload_result.GetFieldByName("chunks_received").(int32))
 	data[0].Bytes = int(upload_result.GetFieldByName("bytes_received").(int32))
 	data[0].Status = GetEnumValue(upload_result, "status")
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
 
 	return nil
 }
@@ -144,26 +129,13 @@
 		return err
 	}
 
-	outputFormat := CharReplacer.Replace(options.Format)
-	if outputFormat == "" {
-		outputFormat = DEFAULT_TRANSFER_FORMAT
-	}
-	if options.Quiet {
-		outputFormat = "{{.Status}}"
-	}
-
 	data := make([]TransferOutput, 1)
 	data[0].Chunks = h.chunks
 	data[0].Bytes = h.bytes
 	data[0].Status = h.status
+	data[0].Checksum = h.GetChecksum()
 
-	result := CommandResult{
-		Format:   format.Format(outputFormat),
-		OutputAs: toOutputType(options.OutputAs),
-		Data:     data,
-	}
-
-	GenerateOutput(&result)
+	FormatAndGenerateOutput(&options.OutputOptions, DEFAULT_TRANSFER_FORMAT, "{{.Status}}", data)
 
 	return nil
 }