Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
170 changes: 170 additions & 0 deletions cmd/wsh/cmd/setmeta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
// Copyright 2025, Command Line Inc.
// SPDX-License-Identifier: Apache-2.0

package cmd

import (
"reflect"
"testing"
)

func TestParseMetaSets(t *testing.T) {
tests := []struct {
name string
input []string
want map[string]any
wantErr bool
}{
{
name: "basic types",
input: []string{"str=hello", "num=42", "float=3.14", "bool=true", "null=null"},
want: map[string]any{
"str": "hello",
"num": int64(42),
"float": float64(3.14),
"bool": true,
"null": nil,
},
},
{
name: "json values",
input: []string{
`arr=[1,2,3]`,
`obj={"foo":"bar"}`,
`str="quoted"`,
},
want: map[string]any{
"arr": []any{float64(1), float64(2), float64(3)},
"obj": map[string]any{"foo": "bar"},
"str": "quoted",
},
},
{
name: "nested paths",
input: []string{
"a/b=55",
"a/c=2",
},
want: map[string]any{
"a": map[string]any{
"b": int64(55),
"c": int64(2),
},
},
},
{
name: "deep nesting",
input: []string{
"a/b/c/d=hello",
},
want: map[string]any{
"a": map[string]any{
"b": map[string]any{
"c": map[string]any{
"d": "hello",
},
},
},
},
},
{
name: "override nested value",
input: []string{
"a/b/c=1",
"a/b=2",
},
want: map[string]any{
"a": map[string]any{
"b": int64(2),
},
},
},
{
name: "override with null",
input: []string{
"a/b=1",
"a/c=2",
"a=null",
},
want: map[string]any{
"a": nil,
},
},
{
name: "mixed types in path",
input: []string{
"a/b=1",
"a/c=[1,2,3]",
"a/d/e=true",
},
want: map[string]any{
"a": map[string]any{
"b": int64(1),
"c": []any{float64(1), float64(2), float64(3)},
"d": map[string]any{
"e": true,
},
},
},
},
{
name: "invalid format",
input: []string{"invalid"},
wantErr: true,
},
{
name: "invalid json",
input: []string{`a={"invalid`},
wantErr: true,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseMetaSets(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseMetaSets() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseMetaSets() = %v, want %v", got, tt.want)
}
})
}
}

func TestParseMetaValue(t *testing.T) {
tests := []struct {
name string
input string
want any
wantErr bool
}{
{"empty string", "", nil, false},
{"null", "null", nil, false},
{"true", "true", true, false},
{"false", "false", false, false},
{"integer", "42", int64(42), false},
{"negative integer", "-42", int64(-42), false},
{"hex integer", "0xff", int64(255), false},
{"float", "3.14", float64(3.14), false},
{"string", "hello", "hello", false},
{"json array", "[1,2,3]", []any{float64(1), float64(2), float64(3)}, false},
{"json object", `{"foo":"bar"}`, map[string]any{"foo": "bar"}, false},
{"quoted string", `"quoted"`, "quoted", false},
{"invalid json", `{"invalid`, nil, true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseMetaValue(tt.input)
if (err != nil) != tt.wantErr {
t.Errorf("parseMetaValue() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseMetaValue() = %v, want %v", got, tt.want)
}
})
}
}
104 changes: 76 additions & 28 deletions cmd/wsh/cmd/wshcmd-setmeta.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,40 +58,88 @@ func loadJSONFile(filepath string) (map[string]interface{}, error) {
return result, nil
}

func parseMetaSets(metaSets []string) (map[string]interface{}, error) {
meta := make(map[string]interface{})
func parseMetaValue(setVal string) (any, error) {
if setVal == "" || setVal == "null" {
return nil, nil
}
if setVal == "true" {
return true, nil
}
if setVal == "false" {
return false, nil
}
if setVal[0] == '[' || setVal[0] == '{' || setVal[0] == '"' {
var val any
err := json.Unmarshal([]byte(setVal), &val)
if err != nil {
return nil, fmt.Errorf("invalid json value: %v", err)
}
return val, nil
}

// Try parsing as integer
ival, err := strconv.ParseInt(setVal, 0, 64)
if err == nil {
return ival, nil
}

// Try parsing as float
fval, err := strconv.ParseFloat(setVal, 64)
if err == nil {
return fval, nil
}

// Fallback to string
return setVal, nil
}

func setNestedValue(meta map[string]any, path []string, value any) {
// For single key, just set directly
if len(path) == 1 {
meta[path[0]] = value
return
}

// For nested path, traverse or create maps as needed
current := meta
for i := 0; i < len(path)-1; i++ {
key := path[i]
// If next level doesn't exist or isn't a map, create new map
next, exists := current[key]
if !exists {
nextMap := make(map[string]any)
current[key] = nextMap
current = nextMap
} else if nextMap, ok := next.(map[string]any); ok {
current = nextMap
} else {
// If existing value isn't a map, replace with new map
nextMap = make(map[string]any)
current[key] = nextMap
current = nextMap
}
}

// Set the final value
current[path[len(path)-1]] = value
}

func parseMetaSets(metaSets []string) (map[string]any, error) {
meta := make(map[string]any)
for _, metaSet := range metaSets {
fields := strings.SplitN(metaSet, "=", 2)
if len(fields) != 2 {
return nil, fmt.Errorf("invalid meta set: %q", metaSet)
}
setVal := fields[1]
if setVal == "" || setVal == "null" {
meta[fields[0]] = nil
} else if setVal == "true" {
meta[fields[0]] = true
} else if setVal == "false" {
meta[fields[0]] = false
} else if setVal[0] == '[' || setVal[0] == '{' || setVal[0] == '"' {
var val interface{}
err := json.Unmarshal([]byte(setVal), &val)
if err != nil {
return nil, fmt.Errorf("invalid json value: %v", err)
}
meta[fields[0]] = val
} else {
ival, err := strconv.ParseInt(setVal, 0, 64)
if err == nil {
meta[fields[0]] = ival
} else {
fval, err := strconv.ParseFloat(setVal, 64)
if err == nil {
meta[fields[0]] = fval
} else {
meta[fields[0]] = setVal
}
}

val, err := parseMetaValue(fields[1])
if err != nil {
return nil, err
}

// Split the key path and set nested value
path := strings.Split(fields[0], "/")
setNestedValue(meta, path, val)
}
return meta, nil
}
Expand Down
7 changes: 7 additions & 0 deletions frontend/types/gotypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,13 @@ declare global {
"term:fontsize"?: number;
"term:fontfamily"?: string;
"term:theme"?: string;
"cmd:env"?: {[key: string]: string};
"cmd:initscript"?: string;
"cmd:initscript.sh"?: string;
"cmd:initscript.bash"?: string;
"cmd:initscript.zsh"?: string;
"cmd:initscript.pwsh"?: string;
"cmd:initscript.fish"?: string;
"ssh:user"?: string;
"ssh:hostname"?: string;
"ssh:port"?: string;
Expand Down
Loading
Loading