Skip to content

strings: add CutPrefix and CutSuffix #42537

@cespare

Description

@cespare

Inspired by #40135, I have another proposal for an addition to the strings package (and by extension, I suppose, the bytes package).

The strings functions I most frequently wish for are variants of TrimPrefix and TrimSuffix which report whether they did any trimming.

// TrimmedPrefix returns s without the provided leading prefix string
// and reports whether it found the prefix.
// If s doesn't start with prefix, TrimmedPrefix returns s, false.
// If prefix is the empty string, TrimmedPrefix returns s, true.
func TrimmedPrefix(s, prefix string) (trimmed string, ok bool)

Lacking these functions, Go authors resort to fragile code, often writing a prefix/suffix literal twice or hard-coding its length:

if strings.HasPrefix(s, "myprefix") {
	s = strings.TrimPrefix(s, "myprefix")
	...
}
if strings.HasPrefix(s, "myprefix") {
	s = s[len("myprefix"):]
	...
}
if strings.HasPrefix(s, "myprefix") {
	s = s[8:]
	...
}
if t := strings.TrimPrefix(s, "myprefix"); s != t {
	// had prefix
	s = t
	...
}

Of course, a function like TrimmedPrefix is easy to write and can exist outside of the standard library. At my company we have these functions in an internal string helper package and they see regular use. But certainly I wouldn't pull in a dependency for such a tiny helper function and so when I'm working on my own projects I generally just use the above workarounds.


Here are some examples from the Go source tree along with how they could be altered to use TrimmedPrefix/TrimmedSuffix:

  • src/cmd/go/internal/modload/load.go:

    if strings.HasPrefix(suffix, "/vendor/") {
    	return strings.TrimPrefix(suffix, "/vendor/")
    }
    

    becomes

    if v, ok := strings.TrimmedPrefix(suffix, "/vendor/"); ok {
    	return v
    }
    
  • src/cmd/go/proxy_test.go:

    if !strings.HasSuffix(name, ".txt") {
    	continue
    }
    name = strings.TrimSuffix(name, ".txt")
    

    becomes

    name, ok := strings.TrimmedSuffix(name, ".txt")
    if !ok {
    	continue
    }
    
  • src/testing/benchmark.go:

    if strings.HasSuffix(s, "x") {
    	n, err := strconv.ParseInt(s[:len(s)-1], 10, 0)
    	...
    }
    

    becomes

    if s, ok := strings.TrimmedSuffix(s, "x"); ok {
    	n, err := strconv.ParseInt(s, 10, 0)
    	...
    }
    
  • src/testing/fstest/mapfs.go:

    if strings.HasPrefix(fname, prefix) {
    	felem := fname[len(prefix):]
    	...
    }
    

    becomes

    if felem, ok := strings.TrimmedPrefix(fname, prefix); ok {
    	...
    }
    
  • src/mime/mediatype.go:

    if !strings.HasPrefix(rest, ";") {
    	return "", "", v
    }
    
    rest = rest[1:] // consume semicolon
    

    becomes

    rest, ok := strings.TrimmedPrefix(rest, ";")
    if !ok {
    	return "", "", v
    }
    
  • test/run.go:

    if strings.HasPrefix(line, "//") {
    	line = line[2:]
    } else {
    	continue
    }
    

    becomes

    line, ok := strings.TrimmedPrefix(line, "//")
    if !ok {
    	continue
    }
    
  • test/run.go:

    if strings.HasPrefix(m, "LINE+") {
    	delta, _ := strconv.Atoi(m[5:])
    	n += delta
    	...
    }
    

    becomes

    if d, ok := strings.TrimmedPrefix(m, "LINE+"); ok {
    	delta, _ := strconv.Atoi(d)
    	n += delta
    	...
    }
    
  • src/runtime/testdata/testprog/traceback_ancestors.go:

    if strings.HasPrefix(tb[pos:], "goroutine ") {
    	id := tb[pos+len("goroutine "):]
    	...
    }
    

    becomes

    if id, ok := strings.TrimmedPrefix(tb[pos:], "goroutine "); ok {
    	...
    }
    

Update 2022-03-26: Changed proposed names to TrimmedPrefix/TrimmedSuffix (per @ianlancetaylor's suggestion).

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions