...

Source file src/path/filepath/path.go

Documentation: path/filepath

     1  // Copyright 2009 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package filepath implements utility routines for manipulating filename paths
     6  // in a way compatible with the target operating system-defined file paths.
     7  //
     8  // The filepath package uses either forward slashes or backslashes,
     9  // depending on the operating system. To process paths such as URLs
    10  // that always use forward slashes regardless of the operating
    11  // system, see the [path] package.
    12  package filepath
    13  
    14  import (
    15  	"errors"
    16  	"io/fs"
    17  	"os"
    18  	"slices"
    19  	"sort"
    20  	"strings"
    21  )
    22  
    23  // A lazybuf is a lazily constructed path buffer.
    24  // It supports append, reading previously appended bytes,
    25  // and retrieving the final string. It does not allocate a buffer
    26  // to hold the output until that output diverges from s.
    27  type lazybuf struct {
    28  	path       string
    29  	buf        []byte
    30  	w          int
    31  	volAndPath string
    32  	volLen     int
    33  }
    34  
    35  func (b *lazybuf) index(i int) byte {
    36  	if b.buf != nil {
    37  		return b.buf[i]
    38  	}
    39  	return b.path[i]
    40  }
    41  
    42  func (b *lazybuf) append(c byte) {
    43  	if b.buf == nil {
    44  		if b.w < len(b.path) && b.path[b.w] == c {
    45  			b.w++
    46  			return
    47  		}
    48  		b.buf = make([]byte, len(b.path))
    49  		copy(b.buf, b.path[:b.w])
    50  	}
    51  	b.buf[b.w] = c
    52  	b.w++
    53  }
    54  
    55  func (b *lazybuf) prepend(prefix ...byte) {
    56  	b.buf = slices.Insert(b.buf, 0, prefix...)
    57  	b.w += len(prefix)
    58  }
    59  
    60  func (b *lazybuf) string() string {
    61  	if b.buf == nil {
    62  		return b.volAndPath[:b.volLen+b.w]
    63  	}
    64  	return b.volAndPath[:b.volLen] + string(b.buf[:b.w])
    65  }
    66  
    67  const (
    68  	Separator     = os.PathSeparator
    69  	ListSeparator = os.PathListSeparator
    70  )
    71  
    72  // Clean returns the shortest path name equivalent to path
    73  // by purely lexical processing. It applies the following rules
    74  // iteratively until no further processing can be done:
    75  //
    76  //  1. Replace multiple [Separator] elements with a single one.
    77  //  2. Eliminate each . path name element (the current directory).
    78  //  3. Eliminate each inner .. path name element (the parent directory)
    79  //     along with the non-.. element that precedes it.
    80  //  4. Eliminate .. elements that begin a rooted path:
    81  //     that is, replace "/.." by "/" at the beginning of a path,
    82  //     assuming Separator is '/'.
    83  //
    84  // The returned path ends in a slash only if it represents a root directory,
    85  // such as "/" on Unix or `C:\` on Windows.
    86  //
    87  // Finally, any occurrences of slash are replaced by Separator.
    88  //
    89  // If the result of this process is an empty string, Clean
    90  // returns the string ".".
    91  //
    92  // On Windows, Clean does not modify the volume name other than to replace
    93  // occurrences of "/" with `\`.
    94  // For example, Clean("//host/share/../x") returns `\\host\share\x`.
    95  //
    96  // See also Rob Pike, “Lexical File Names in Plan 9 or
    97  // Getting Dot-Dot Right,”
    98  // https://9p.io/sys/doc/lexnames.html
    99  func Clean(path string) string {
   100  	originalPath := path
   101  	volLen := volumeNameLen(path)
   102  	path = path[volLen:]
   103  	if path == "" {
   104  		if volLen > 1 && os.IsPathSeparator(originalPath[0]) && os.IsPathSeparator(originalPath[1]) {
   105  			// should be UNC
   106  			return FromSlash(originalPath)
   107  		}
   108  		return originalPath + "."
   109  	}
   110  	rooted := os.IsPathSeparator(path[0])
   111  
   112  	// Invariants:
   113  	//	reading from path; r is index of next byte to process.
   114  	//	writing to buf; w is index of next byte to write.
   115  	//	dotdot is index in buf where .. must stop, either because
   116  	//		it is the leading slash or it is a leading ../../.. prefix.
   117  	n := len(path)
   118  	out := lazybuf{path: path, volAndPath: originalPath, volLen: volLen}
   119  	r, dotdot := 0, 0
   120  	if rooted {
   121  		out.append(Separator)
   122  		r, dotdot = 1, 1
   123  	}
   124  
   125  	for r < n {
   126  		switch {
   127  		case os.IsPathSeparator(path[r]):
   128  			// empty path element
   129  			r++
   130  		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
   131  			// . element
   132  			r++
   133  		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
   134  			// .. element: remove to last separator
   135  			r += 2
   136  			switch {
   137  			case out.w > dotdot:
   138  				// can backtrack
   139  				out.w--
   140  				for out.w > dotdot && !os.IsPathSeparator(out.index(out.w)) {
   141  					out.w--
   142  				}
   143  			case !rooted:
   144  				// cannot backtrack, but not rooted, so append .. element.
   145  				if out.w > 0 {
   146  					out.append(Separator)
   147  				}
   148  				out.append('.')
   149  				out.append('.')
   150  				dotdot = out.w
   151  			}
   152  		default:
   153  			// real path element.
   154  			// add slash if needed
   155  			if rooted && out.w != 1 || !rooted && out.w != 0 {
   156  				out.append(Separator)
   157  			}
   158  			// copy element
   159  			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
   160  				out.append(path[r])
   161  			}
   162  		}
   163  	}
   164  
   165  	// Turn empty string into "."
   166  	if out.w == 0 {
   167  		out.append('.')
   168  	}
   169  
   170  	postClean(&out) // avoid creating absolute paths on Windows
   171  	return FromSlash(out.string())
   172  }
   173  
   174  // IsLocal reports whether path, using lexical analysis only, has all of these properties:
   175  //
   176  //   - is within the subtree rooted at the directory in which path is evaluated
   177  //   - is not an absolute path
   178  //   - is not empty
   179  //   - on Windows, is not a reserved name such as "NUL"
   180  //
   181  // If IsLocal(path) returns true, then
   182  // Join(base, path) will always produce a path contained within base and
   183  // Clean(path) will always produce an unrooted path with no ".." path elements.
   184  //
   185  // IsLocal is a purely lexical operation.
   186  // In particular, it does not account for the effect of any symbolic links
   187  // that may exist in the filesystem.
   188  func IsLocal(path string) bool {
   189  	return isLocal(path)
   190  }
   191  
   192  func unixIsLocal(path string) bool {
   193  	if IsAbs(path) || path == "" {
   194  		return false
   195  	}
   196  	hasDots := false
   197  	for p := path; p != ""; {
   198  		var part string
   199  		part, p, _ = strings.Cut(p, "/")
   200  		if part == "." || part == ".." {
   201  			hasDots = true
   202  			break
   203  		}
   204  	}
   205  	if hasDots {
   206  		path = Clean(path)
   207  	}
   208  	if path == ".." || strings.HasPrefix(path, "../") {
   209  		return false
   210  	}
   211  	return true
   212  }
   213  
   214  // ToSlash returns the result of replacing each separator character
   215  // in path with a slash ('/') character. Multiple separators are
   216  // replaced by multiple slashes.
   217  func ToSlash(path string) string {
   218  	if Separator == '/' {
   219  		return path
   220  	}
   221  	return strings.ReplaceAll(path, string(Separator), "/")
   222  }
   223  
   224  // FromSlash returns the result of replacing each slash ('/') character
   225  // in path with a separator character. Multiple slashes are replaced
   226  // by multiple separators.
   227  func FromSlash(path string) string {
   228  	if Separator == '/' {
   229  		return path
   230  	}
   231  	return strings.ReplaceAll(path, "/", string(Separator))
   232  }
   233  
   234  // SplitList splits a list of paths joined by the OS-specific [ListSeparator],
   235  // usually found in PATH or GOPATH environment variables.
   236  // Unlike strings.Split, SplitList returns an empty slice when passed an empty
   237  // string.
   238  func SplitList(path string) []string {
   239  	return splitList(path)
   240  }
   241  
   242  // Split splits path immediately following the final [Separator],
   243  // separating it into a directory and file name component.
   244  // If there is no Separator in path, Split returns an empty dir
   245  // and file set to path.
   246  // The returned values have the property that path = dir+file.
   247  func Split(path string) (dir, file string) {
   248  	vol := VolumeName(path)
   249  	i := len(path) - 1
   250  	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
   251  		i--
   252  	}
   253  	return path[:i+1], path[i+1:]
   254  }
   255  
   256  // Join joins any number of path elements into a single path,
   257  // separating them with an OS specific [Separator]. Empty elements
   258  // are ignored. The result is Cleaned. However, if the argument
   259  // list is empty or all its elements are empty, Join returns
   260  // an empty string.
   261  // On Windows, the result will only be a UNC path if the first
   262  // non-empty element is a UNC path.
   263  func Join(elem ...string) string {
   264  	return join(elem)
   265  }
   266  
   267  // Ext returns the file name extension used by path.
   268  // The extension is the suffix beginning at the final dot
   269  // in the final element of path; it is empty if there is
   270  // no dot.
   271  func Ext(path string) string {
   272  	for i := len(path) - 1; i >= 0 && !os.IsPathSeparator(path[i]); i-- {
   273  		if path[i] == '.' {
   274  			return path[i:]
   275  		}
   276  	}
   277  	return ""
   278  }
   279  
   280  // EvalSymlinks returns the path name after the evaluation of any symbolic
   281  // links.
   282  // If path is relative the result will be relative to the current directory,
   283  // unless one of the components is an absolute symbolic link.
   284  // EvalSymlinks calls [Clean] on the result.
   285  func EvalSymlinks(path string) (string, error) {
   286  	return evalSymlinks(path)
   287  }
   288  
   289  // Abs returns an absolute representation of path.
   290  // If the path is not absolute it will be joined with the current
   291  // working directory to turn it into an absolute path. The absolute
   292  // path name for a given file is not guaranteed to be unique.
   293  // Abs calls [Clean] on the result.
   294  func Abs(path string) (string, error) {
   295  	return abs(path)
   296  }
   297  
   298  func unixAbs(path string) (string, error) {
   299  	if IsAbs(path) {
   300  		return Clean(path), nil
   301  	}
   302  	wd, err := os.Getwd()
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	return Join(wd, path), nil
   307  }
   308  
   309  // Rel returns a relative path that is lexically equivalent to targpath when
   310  // joined to basepath with an intervening separator. That is,
   311  // [Join](basepath, Rel(basepath, targpath)) is equivalent to targpath itself.
   312  // On success, the returned path will always be relative to basepath,
   313  // even if basepath and targpath share no elements.
   314  // An error is returned if targpath can't be made relative to basepath or if
   315  // knowing the current working directory would be necessary to compute it.
   316  // Rel calls [Clean] on the result.
   317  func Rel(basepath, targpath string) (string, error) {
   318  	baseVol := VolumeName(basepath)
   319  	targVol := VolumeName(targpath)
   320  	base := Clean(basepath)
   321  	targ := Clean(targpath)
   322  	if sameWord(targ, base) {
   323  		return ".", nil
   324  	}
   325  	base = base[len(baseVol):]
   326  	targ = targ[len(targVol):]
   327  	if base == "." {
   328  		base = ""
   329  	} else if base == "" && volumeNameLen(baseVol) > 2 /* isUNC */ {
   330  		// Treat any targetpath matching `\\host\share` basepath as absolute path.
   331  		base = string(Separator)
   332  	}
   333  
   334  	// Can't use IsAbs - `\a` and `a` are both relative in Windows.
   335  	baseSlashed := len(base) > 0 && base[0] == Separator
   336  	targSlashed := len(targ) > 0 && targ[0] == Separator
   337  	if baseSlashed != targSlashed || !sameWord(baseVol, targVol) {
   338  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   339  	}
   340  	// Position base[b0:bi] and targ[t0:ti] at the first differing elements.
   341  	bl := len(base)
   342  	tl := len(targ)
   343  	var b0, bi, t0, ti int
   344  	for {
   345  		for bi < bl && base[bi] != Separator {
   346  			bi++
   347  		}
   348  		for ti < tl && targ[ti] != Separator {
   349  			ti++
   350  		}
   351  		if !sameWord(targ[t0:ti], base[b0:bi]) {
   352  			break
   353  		}
   354  		if bi < bl {
   355  			bi++
   356  		}
   357  		if ti < tl {
   358  			ti++
   359  		}
   360  		b0 = bi
   361  		t0 = ti
   362  	}
   363  	if base[b0:bi] == ".." {
   364  		return "", errors.New("Rel: can't make " + targpath + " relative to " + basepath)
   365  	}
   366  	if b0 != bl {
   367  		// Base elements left. Must go up before going down.
   368  		seps := strings.Count(base[b0:bl], string(Separator))
   369  		size := 2 + seps*3
   370  		if tl != t0 {
   371  			size += 1 + tl - t0
   372  		}
   373  		buf := make([]byte, size)
   374  		n := copy(buf, "..")
   375  		for i := 0; i < seps; i++ {
   376  			buf[n] = Separator
   377  			copy(buf[n+1:], "..")
   378  			n += 3
   379  		}
   380  		if t0 != tl {
   381  			buf[n] = Separator
   382  			copy(buf[n+1:], targ[t0:])
   383  		}
   384  		return string(buf), nil
   385  	}
   386  	return targ[t0:], nil
   387  }
   388  
   389  // SkipDir is used as a return value from [WalkFunc] to indicate that
   390  // the directory named in the call is to be skipped. It is not returned
   391  // as an error by any function.
   392  var SkipDir error = fs.SkipDir
   393  
   394  // SkipAll is used as a return value from [WalkFunc] to indicate that
   395  // all remaining files and directories are to be skipped. It is not returned
   396  // as an error by any function.
   397  var SkipAll error = fs.SkipAll
   398  
   399  // WalkFunc is the type of the function called by [Walk] to visit each
   400  // file or directory.
   401  //
   402  // The path argument contains the argument to Walk as a prefix.
   403  // That is, if Walk is called with root argument "dir" and finds a file
   404  // named "a" in that directory, the walk function will be called with
   405  // argument "dir/a".
   406  //
   407  // The directory and file are joined with Join, which may clean the
   408  // directory name: if Walk is called with the root argument "x/../dir"
   409  // and finds a file named "a" in that directory, the walk function will
   410  // be called with argument "dir/a", not "x/../dir/a".
   411  //
   412  // The info argument is the fs.FileInfo for the named path.
   413  //
   414  // The error result returned by the function controls how Walk continues.
   415  // If the function returns the special value [SkipDir], Walk skips the
   416  // current directory (path if info.IsDir() is true, otherwise path's
   417  // parent directory). If the function returns the special value [SkipAll],
   418  // Walk skips all remaining files and directories. Otherwise, if the function
   419  // returns a non-nil error, Walk stops entirely and returns that error.
   420  //
   421  // The err argument reports an error related to path, signaling that Walk
   422  // will not walk into that directory. The function can decide how to
   423  // handle that error; as described earlier, returning the error will
   424  // cause Walk to stop walking the entire tree.
   425  //
   426  // Walk calls the function with a non-nil err argument in two cases.
   427  //
   428  // First, if an [os.Lstat] on the root directory or any directory or file
   429  // in the tree fails, Walk calls the function with path set to that
   430  // directory or file's path, info set to nil, and err set to the error
   431  // from os.Lstat.
   432  //
   433  // Second, if a directory's Readdirnames method fails, Walk calls the
   434  // function with path set to the directory's path, info, set to an
   435  // [fs.FileInfo] describing the directory, and err set to the error from
   436  // Readdirnames.
   437  type WalkFunc func(path string, info fs.FileInfo, err error) error
   438  
   439  var lstat = os.Lstat // for testing
   440  
   441  // walkDir recursively descends path, calling walkDirFn.
   442  func walkDir(path string, d fs.DirEntry, walkDirFn fs.WalkDirFunc) error {
   443  	if err := walkDirFn(path, d, nil); err != nil || !d.IsDir() {
   444  		if err == SkipDir && d.IsDir() {
   445  			// Successfully skipped directory.
   446  			err = nil
   447  		}
   448  		return err
   449  	}
   450  
   451  	dirs, err := os.ReadDir(path)
   452  	if err != nil {
   453  		// Second call, to report ReadDir error.
   454  		err = walkDirFn(path, d, err)
   455  		if err != nil {
   456  			if err == SkipDir && d.IsDir() {
   457  				err = nil
   458  			}
   459  			return err
   460  		}
   461  	}
   462  
   463  	for _, d1 := range dirs {
   464  		path1 := Join(path, d1.Name())
   465  		if err := walkDir(path1, d1, walkDirFn); err != nil {
   466  			if err == SkipDir {
   467  				break
   468  			}
   469  			return err
   470  		}
   471  	}
   472  	return nil
   473  }
   474  
   475  // walk recursively descends path, calling walkFn.
   476  func walk(path string, info fs.FileInfo, walkFn WalkFunc) error {
   477  	if !info.IsDir() {
   478  		return walkFn(path, info, nil)
   479  	}
   480  
   481  	names, err := readDirNames(path)
   482  	err1 := walkFn(path, info, err)
   483  	// If err != nil, walk can't walk into this directory.
   484  	// err1 != nil means walkFn want walk to skip this directory or stop walking.
   485  	// Therefore, if one of err and err1 isn't nil, walk will return.
   486  	if err != nil || err1 != nil {
   487  		// The caller's behavior is controlled by the return value, which is decided
   488  		// by walkFn. walkFn may ignore err and return nil.
   489  		// If walkFn returns SkipDir or SkipAll, it will be handled by the caller.
   490  		// So walk should return whatever walkFn returns.
   491  		return err1
   492  	}
   493  
   494  	for _, name := range names {
   495  		filename := Join(path, name)
   496  		fileInfo, err := lstat(filename)
   497  		if err != nil {
   498  			if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
   499  				return err
   500  			}
   501  		} else {
   502  			err = walk(filename, fileInfo, walkFn)
   503  			if err != nil {
   504  				if !fileInfo.IsDir() || err != SkipDir {
   505  					return err
   506  				}
   507  			}
   508  		}
   509  	}
   510  	return nil
   511  }
   512  
   513  // WalkDir walks the file tree rooted at root, calling fn for each file or
   514  // directory in the tree, including root.
   515  //
   516  // All errors that arise visiting files and directories are filtered by fn:
   517  // see the [fs.WalkDirFunc] documentation for details.
   518  //
   519  // The files are walked in lexical order, which makes the output deterministic
   520  // but requires WalkDir to read an entire directory into memory before proceeding
   521  // to walk that directory.
   522  //
   523  // WalkDir does not follow symbolic links.
   524  //
   525  // WalkDir calls fn with paths that use the separator character appropriate
   526  // for the operating system. This is unlike [io/fs.WalkDir], which always
   527  // uses slash separated paths.
   528  func WalkDir(root string, fn fs.WalkDirFunc) error {
   529  	info, err := os.Lstat(root)
   530  	if err != nil {
   531  		err = fn(root, nil, err)
   532  	} else {
   533  		err = walkDir(root, fs.FileInfoToDirEntry(info), fn)
   534  	}
   535  	if err == SkipDir || err == SkipAll {
   536  		return nil
   537  	}
   538  	return err
   539  }
   540  
   541  // Walk walks the file tree rooted at root, calling fn for each file or
   542  // directory in the tree, including root.
   543  //
   544  // All errors that arise visiting files and directories are filtered by fn:
   545  // see the [WalkFunc] documentation for details.
   546  //
   547  // The files are walked in lexical order, which makes the output deterministic
   548  // but requires Walk to read an entire directory into memory before proceeding
   549  // to walk that directory.
   550  //
   551  // Walk does not follow symbolic links.
   552  //
   553  // Walk is less efficient than [WalkDir], introduced in Go 1.16,
   554  // which avoids calling os.Lstat on every visited file or directory.
   555  func Walk(root string, fn WalkFunc) error {
   556  	info, err := os.Lstat(root)
   557  	if err != nil {
   558  		err = fn(root, nil, err)
   559  	} else {
   560  		err = walk(root, info, fn)
   561  	}
   562  	if err == SkipDir || err == SkipAll {
   563  		return nil
   564  	}
   565  	return err
   566  }
   567  
   568  // readDirNames reads the directory named by dirname and returns
   569  // a sorted list of directory entry names.
   570  func readDirNames(dirname string) ([]string, error) {
   571  	f, err := os.Open(dirname)
   572  	if err != nil {
   573  		return nil, err
   574  	}
   575  	names, err := f.Readdirnames(-1)
   576  	f.Close()
   577  	if err != nil {
   578  		return nil, err
   579  	}
   580  	sort.Strings(names)
   581  	return names, nil
   582  }
   583  
   584  // Base returns the last element of path.
   585  // Trailing path separators are removed before extracting the last element.
   586  // If the path is empty, Base returns ".".
   587  // If the path consists entirely of separators, Base returns a single separator.
   588  func Base(path string) string {
   589  	if path == "" {
   590  		return "."
   591  	}
   592  	// Strip trailing slashes.
   593  	for len(path) > 0 && os.IsPathSeparator(path[len(path)-1]) {
   594  		path = path[0 : len(path)-1]
   595  	}
   596  	// Throw away volume name
   597  	path = path[len(VolumeName(path)):]
   598  	// Find the last element
   599  	i := len(path) - 1
   600  	for i >= 0 && !os.IsPathSeparator(path[i]) {
   601  		i--
   602  	}
   603  	if i >= 0 {
   604  		path = path[i+1:]
   605  	}
   606  	// If empty now, it had only slashes.
   607  	if path == "" {
   608  		return string(Separator)
   609  	}
   610  	return path
   611  }
   612  
   613  // Dir returns all but the last element of path, typically the path's directory.
   614  // After dropping the final element, Dir calls [Clean] on the path and trailing
   615  // slashes are removed.
   616  // If the path is empty, Dir returns ".".
   617  // If the path consists entirely of separators, Dir returns a single separator.
   618  // The returned path does not end in a separator unless it is the root directory.
   619  func Dir(path string) string {
   620  	vol := VolumeName(path)
   621  	i := len(path) - 1
   622  	for i >= len(vol) && !os.IsPathSeparator(path[i]) {
   623  		i--
   624  	}
   625  	dir := Clean(path[len(vol) : i+1])
   626  	if dir == "." && len(vol) > 2 {
   627  		// must be UNC
   628  		return vol
   629  	}
   630  	return vol + dir
   631  }
   632  
   633  // VolumeName returns leading volume name.
   634  // Given "C:\foo\bar" it returns "C:" on Windows.
   635  // Given "\\host\share\foo" it returns "\\host\share".
   636  // On other platforms it returns "".
   637  func VolumeName(path string) string {
   638  	return FromSlash(path[:volumeNameLen(path)])
   639  }
   640  

View as plain text