Repository: gohugoio/hugo (language: go)

Issue:
add path.BaseName function
<!-- Describe this new feature. Think about if it really belongs in the Hugo core module; you may want to discuss it on https://discourse.gohugo.io/ first.  -->

I'm trying to write an image render hook that looks for the presence of smaller image files to fill out the srcset field for responsive images.  I've settled on the following naming convention:

image1.jpg
image1_400.jpg
image1_800.jpg
...etc...

where the numbers are the various widths of the image.  Not all sizes will be available for all images, so the hook needs to test if it exists and only emit it if it exists.

I've settled on having a site-wide parameter that has a list of the sizes to look for.  That way I can have a tool check that parameter and generate the appropriate images.

To generate the path to look for, I am currently doing this:

      {{- $path := replaceRE ".[jJ][pP][gG]$" (printf "_%sw%s" $size (path.Ext $src.Name)) $src.Name -}}

which only works for jpg files.  I'd like to do something like this:

      {{- $path := (printf "%s/%s_%sw%s" (path.Dir $src.Name) (path.BaseName $src.Name) $size (path.Ext $src.Name)) -}}

Which would work for all file extensions.

The File object has BaseFileName which provides this operation, and I don't really care what the operation is named, but I would like to have the operation.

Some test cases (I expect they are similar to the .File.BaseFileName test cases):

```
path.BaseName "image1.jpg"  => image1
path.BaseName "/a/long/path/foo.txt" => foo
path.BaseName "filename.en.jpg" => filename.en
```



Discussion / hints:

Fix patch:
diff --git a/docs/content/en/functions/path.Base.md b/docs/content/en/functions/path.Base.md
index c44518a727b..a6bfc2bd5da 100644
--- a/docs/content/en/functions/path.Base.md
+++ b/docs/content/en/functions/path.Base.md
@@ -12,7 +12,7 @@ keywords: [path, base]
 signature: ["path.Base PATH"]
 workson: []
 hugoversion: "0.40"
-relatedfuncs: [path.Dir, path.Ext, path.Split]
+relatedfuncs: [path.BaseName, path.Clean, path.Dir, path.Ext, path.Join, path.Split]
 deprecated: false
 ---
 
diff --git a/docs/content/en/functions/path.BaseName.md b/docs/content/en/functions/path.BaseName.md
new file mode 100644
index 00000000000..6a5b9f0a0eb
--- /dev/null
+++ b/docs/content/en/functions/path.BaseName.md
@@ -0,0 +1,24 @@
+---
+title: path.BaseName
+description: BaseName returns the last element of a path, removing the extension if present.
+date: 2022-06-04
+categories: [functions]
+menu:
+  docs:
+    parent: "functions"
+keywords: [path, base]
+signature: ["path.BaseName PATH"]
+relatedfuncs: [path.Base, path.Clean, path.Dir, path.Ext, path.Join, path.Split]
+deprecated: false
+---
+
+If `PATH` is empty, `.` is returned.
+
+**Note:** On Windows, `PATH` is converted to slash (`/`) separators.
+
+```go-html-template
+{{ path.BaseName "a/news.html" }} → "news"
+{{ path.BaseName "news.html" }} → "news"
+{{ path.BaseName "a/b/c" }} → "c"
+{{ path.BaseName "/x/y/z/" }} → "z"
+```
diff --git a/docs/content/en/functions/path.Clean.md b/docs/content/en/functions/path.Clean.md
index 03ffbd83648..852de65fd5d 100644
--- a/docs/content/en/functions/path.Clean.md
+++ b/docs/content/en/functions/path.Clean.md
@@ -8,8 +8,9 @@ categories: [functions]
 menu:
   docs:
     parent: "functions"
-keywords: [path]
+keywords: [path, clean]
 signature: ["path.Clean PATH"]
+relatedfuncs: [path.Base, path.BaseName, path.Dir, path.Ext, path.Join, path.Split]
 ---
 
 `path.Clean` replaces path separators with slashes (`/`) and removes extraneous separators, including trailing separators.
diff --git a/docs/content/en/functions/path.Dir.md b/docs/content/en/functions/path.Dir.md
index 851a3dc8375..161a0daa825 100644
--- a/docs/content/en/functions/path.Dir.md
+++ b/docs/content/en/functions/path.Dir.md
@@ -12,7 +12,7 @@ keywords: [path, dir]
 signature: ["path.Dir PATH"]
 workson: []
 hugoversion: "0.40"
-relatedfuncs: [path.Base, path.Ext, path.Split]
+relatedfuncs: [path.Base, path.BaseName, path.Clean, path.Ext, path.Join, path.Split]
 deprecated: false
 ---
 
diff --git a/docs/content/en/functions/path.Ext.md b/docs/content/en/functions/path.Ext.md
index 73db7ab3886..8c6fe907c32 100644
--- a/docs/content/en/functions/path.Ext.md
+++ b/docs/content/en/functions/path.Ext.md
@@ -12,7 +12,7 @@ keywords: [path, ext, extension]
 signature: ["path.Ext PATH"]
 workson: []
 hugoversion: "0.40"
-relatedfuncs: [path.Base, path.Dir, path.Split]
+relatedfuncs: [path.Base, path.BaseName, path.Clean, path.Dir, path.Join, path.Split]
 deprecated: false
 ---
 
diff --git a/docs/content/en/functions/path.Join.md b/docs/content/en/functions/path.Join.md
index 96ce8632264..579e8667e04 100644
--- a/docs/content/en/functions/path.Join.md
+++ b/docs/content/en/functions/path.Join.md
@@ -12,7 +12,7 @@ keywords: [path, join]
 signature: ["path.Join ELEMENT..."]
 workson: []
 hugoversion: "0.39"
-relatedfuncs: [path.Split]
+relatedfuncs: [path.Base, path.BaseName, path.Clean, path.Dir, path.Ext, path.Split]
 deprecated: false
 ---
 
diff --git a/docs/content/en/functions/path.Split.md b/docs/content/en/functions/path.Split.md
index 9a09fbe4ab1..7737b77d397 100644
--- a/docs/content/en/functions/path.Split.md
+++ b/docs/content/en/functions/path.Split.md
@@ -12,7 +12,7 @@ keywords: [path, split]
 signature: ["path.Split PATH"]
 workson: []
 hugoversion: "0.39"
-relatedfuncs: [path.Split]
+relatedfuncs: [path.Base, path.BaseName, path.Clean, path.Dir, path.Ext, path.Join]
 deprecated: false
 ---
 
diff --git a/tpl/path/path.go b/tpl/path/path.go
index d334dd90660..378b97e0335 100644
--- a/tpl/path/path.go
+++ b/tpl/path/path.go
@@ -18,6 +18,7 @@ import (
 	"fmt"
 	_path "path"
 	"path/filepath"
+	"strings"
 
 	"github.com/gohugoio/hugo/deps"
 	"github.com/spf13/cast"
@@ -94,6 +95,21 @@ func (ns *Namespace) Base(path any) (string, error) {
 	return _path.Base(spath), nil
 }
 
+// BaseName returns the last element of path, removing the extension if present.
+// Trailing slashes are removed before extracting the last element.
+// If the path is empty, Base returns ".".
+// If the path consists entirely of slashes, Base returns "/".
+// The input path is passed into filepath.ToSlash converting any Windows slashes
+// to forward slashes.
+func (ns *Namespace) BaseName(path any) (string, error) {
+	spath, err := cast.ToStringE(path)
+	if err != nil {
+		return "", err
+	}
+	spath = filepath.ToSlash(spath)
+	return strings.TrimSuffix(_path.Base(spath), _path.Ext(spath)), nil
+}
+
 // Split splits path immediately following the final slash,
 // separating it into a directory and file name component.
 // If there is no slash in path, Split returns an empty dir and


Test patch:
diff --git a/tpl/path/path_test.go b/tpl/path/path_test.go
index c9f8469e74e..599d8367a97 100644
--- a/tpl/path/path_test.go
+++ b/tpl/path/path_test.go
@@ -56,6 +56,36 @@ func TestBase(t *testing.T) {
 	}
 }
 
+func TestBaseName(t *testing.T) {
+	t.Parallel()
+	c := qt.New(t)
+
+	for _, test := range []struct {
+		path   any
+		expect any
+	}{
+		{filepath.FromSlash(`foo/bar.txt`), `bar`},
+		{filepath.FromSlash(`foo/bar/txt `), `txt `},
+		{filepath.FromSlash(`foo/bar.t`), `bar`},
+		{`foo.bar.txt`, `foo.bar`},
+		{`.x`, ``},
+		{``, `.`},
+		// errors
+		{tstNoStringer{}, false},
+	} {
+
+		result, err := ns.BaseName(test.path)
+
+		if b, ok := test.expect.(bool); ok && !b {
+			c.Assert(err, qt.Not(qt.IsNil))
+			continue
+		}
+
+		c.Assert(err, qt.IsNil)
+		c.Assert(result, qt.Equals, test.expect)
+	}
+}
+
 func TestDir(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)