Drone

Can't use comma in plugin settings list because there is no escaping / no way to parse the result

  - name: print-vars
    image: ubuntu
    pull: if-not-exists
    commands:
       - 'env | grep "^DRONE.*" | sort'
       - 'env | grep "^PLUGIN.*" | sort'
    settings:
      foo:
        - a,b
        - c

results from drone exec

[print-vars:0] + env | grep "^DRONE.*" | sort
[print-vars:1] DRONE_BUILD_FINISHED=1593081581
[print-vars:2] DRONE_BUILD_STARTED=1593081581
[print-vars:3] DRONE_BUILD_STATUS=success
[print-vars:4] DRONE_JOB_FINISHED=1593081581
[print-vars:5] DRONE_JOB_STARTED=1593081581
[print-vars:6] DRONE_JOB_STATUS=success
[print-vars:7] DRONE_WORKSPACE=/drone/src
[print-vars:8] DRONE_WORKSPACE_BASE=/drone/src
[print-vars:9] DRONE_WORKSPACE_PATH=
[print-vars:10] + env | grep "^PLUGIN.*" | sort
[print-vars:11] PLUGIN_FOO=a,b,c

Since thee doenst seem to be any escaping mechanism for , (?) of the commas are escaped there is no way to parse the lines inside a plugin in a way that preserves commas.

This is tedious to work around because every setting of every plugin that needs this has to be converted away from urfave/cli (because it does not support user defined flag types that integrate properly with it due to it requiering package private access) and then create a custom flag type that either has support for escaping , or replace with a custom separator that is replaced by the custom flag type.

I am up to maintaining 4 different plugin forks now for clients and is currently converting the fifth one because drone plugins dictates that the inflexible cli package is to be used and , are actually required and there is AFAIK no way around it.

my work around is a basic custom a stdlib flag package flag can that supports custom escaping so that \\, in the drone yaml doens’t split.

import (
	"fmt"
	"strings"
)

// DroneStringSliceFlag is a flag type which support comma separated values and escaping to not split at unwanted lines
type DroneStringSliceFlag []string

func (s *DroneStringSliceFlag) String() string {
	return strings.Join(*s, " AND ")
}

func (s *DroneStringSliceFlag) Set(value string) error {
	ss, err := splitEscapedCommaString(value)
	if err != nil {
		return err
	}
	*s = ss
	return nil
}

func splitEscapedCommaString(str string) ([]string, error) {
	if len(str) == 0 {
		return []string{}, nil
	}
	var res []string
	isEscape := false
	var sb strings.Builder
loop:
	for _, s := range str {
		if isEscape {
			switch s {
			case '\\':
				fallthrough
			case ',':
				sb.WriteRune(s)
			default:
				return nil, fmt.Errorf("unrecognized escapeable character: '%s' in '%s'", string(s), str)
			}
			isEscape = false
			continue loop
		}
		switch s {
		case '\\':
			isEscape = true
			continue loop
		case ',':
			res = append(res, sb.String())
			sb.Reset()
		default:
			sb.WriteRune(s)
		}
	}
	res = append(res, sb.String())
	return res, nil
}

tests


import (
	"flag"
	"testing"

	"github.com/google/go-cmp/cmp"
)

func TestSplitEscapedCommaString(t *testing.T) {

	testEscapedString(t, "a,b\\,d\\\\2,e", []string{"a", "b,d\\2", "e"}, true)
	testEscapedString(t, "a,e", []string{"a", "e"}, true)
	testEscapedString(t, "", []string{}, true)
	testEscapedString(t, ",,,", []string{"", "", "", ""}, true)
	testEscapedString(t, ",foo\\,", []string{"", "foo,"}, true)

	testEscapedString(t, "a,\\s", nil, false)
}

func testEscapedString(t *testing.T, s string, expected []string, expectSuccess bool) {
	t.Helper()
	t.Run(s, func(t *testing.T) {
		ss, err := splitEscapedCommaString(s)
		if !expectSuccess {
			if err == nil {
				t.Fatalf("expected string %s to fail parsing", s)
			}
			return
		}
		if !cmp.Equal(ss, expected) {
			t.Fatalf("not correct output: %v", cmp.Diff(ss, expected))
		}
	})
}

func TestDroneStringSlice(t *testing.T) {
	fs := flag.NewFlagSet("test", flag.ContinueOnError)

	var (
		ss []string
	)

	fs.Var((*DroneStringSliceFlag)(&ss), "ss", "string slice flag")

	args := []string{
		"-ss", "a,b,c\\,c1\\,c2\\\\,d",
	}

	err := fs.Parse(args)
	if err != nil {
		t.Fatal(err)
	}
	if !cmp.Equal(ss, []string{"a", "b", "c,c1,c2\\", "d"}) {
		t.Fatal("not correct output")
	}

}