Home | History | Annotate | Download | only in path
      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 path implements utility routines for manipulating slash-separated
      6 // paths.
      7 //
      8 // The path package should only be used for paths separated by forward
      9 // slashes, such as the paths in URLs. This package does not deal with
     10 // Windows paths with drive letters or backslashes; to manipulate
     11 // operating system paths, use the path/filepath package.
     12 package path
     13 
     14 import (
     15 	"strings"
     16 )
     17 
     18 // A lazybuf is a lazily constructed path buffer.
     19 // It supports append, reading previously appended bytes,
     20 // and retrieving the final string. It does not allocate a buffer
     21 // to hold the output until that output diverges from s.
     22 type lazybuf struct {
     23 	s   string
     24 	buf []byte
     25 	w   int
     26 }
     27 
     28 func (b *lazybuf) index(i int) byte {
     29 	if b.buf != nil {
     30 		return b.buf[i]
     31 	}
     32 	return b.s[i]
     33 }
     34 
     35 func (b *lazybuf) append(c byte) {
     36 	if b.buf == nil {
     37 		if b.w < len(b.s) && b.s[b.w] == c {
     38 			b.w++
     39 			return
     40 		}
     41 		b.buf = make([]byte, len(b.s))
     42 		copy(b.buf, b.s[:b.w])
     43 	}
     44 	b.buf[b.w] = c
     45 	b.w++
     46 }
     47 
     48 func (b *lazybuf) string() string {
     49 	if b.buf == nil {
     50 		return b.s[:b.w]
     51 	}
     52 	return string(b.buf[:b.w])
     53 }
     54 
     55 // Clean returns the shortest path name equivalent to path
     56 // by purely lexical processing. It applies the following rules
     57 // iteratively until no further processing can be done:
     58 //
     59 //	1. Replace multiple slashes with a single slash.
     60 //	2. Eliminate each . path name element (the current directory).
     61 //	3. Eliminate each inner .. path name element (the parent directory)
     62 //	   along with the non-.. element that precedes it.
     63 //	4. Eliminate .. elements that begin a rooted path:
     64 //	   that is, replace "/.." by "/" at the beginning of a path.
     65 //
     66 // The returned path ends in a slash only if it is the root "/".
     67 //
     68 // If the result of this process is an empty string, Clean
     69 // returns the string ".".
     70 //
     71 // See also Rob Pike, ``Lexical File Names in Plan 9 or
     72 // Getting Dot-Dot Right,''
     73 // https://9p.io/sys/doc/lexnames.html
     74 func Clean(path string) string {
     75 	if path == "" {
     76 		return "."
     77 	}
     78 
     79 	rooted := path[0] == '/'
     80 	n := len(path)
     81 
     82 	// Invariants:
     83 	//	reading from path; r is index of next byte to process.
     84 	//	writing to buf; w is index of next byte to write.
     85 	//	dotdot is index in buf where .. must stop, either because
     86 	//		it is the leading slash or it is a leading ../../.. prefix.
     87 	out := lazybuf{s: path}
     88 	r, dotdot := 0, 0
     89 	if rooted {
     90 		out.append('/')
     91 		r, dotdot = 1, 1
     92 	}
     93 
     94 	for r < n {
     95 		switch {
     96 		case path[r] == '/':
     97 			// empty path element
     98 			r++
     99 		case path[r] == '.' && (r+1 == n || path[r+1] == '/'):
    100 			// . element
    101 			r++
    102 		case path[r] == '.' && path[r+1] == '.' && (r+2 == n || path[r+2] == '/'):
    103 			// .. element: remove to last /
    104 			r += 2
    105 			switch {
    106 			case out.w > dotdot:
    107 				// can backtrack
    108 				out.w--
    109 				for out.w > dotdot && out.index(out.w) != '/' {
    110 					out.w--
    111 				}
    112 			case !rooted:
    113 				// cannot backtrack, but not rooted, so append .. element.
    114 				if out.w > 0 {
    115 					out.append('/')
    116 				}
    117 				out.append('.')
    118 				out.append('.')
    119 				dotdot = out.w
    120 			}
    121 		default:
    122 			// real path element.
    123 			// add slash if needed
    124 			if rooted && out.w != 1 || !rooted && out.w != 0 {
    125 				out.append('/')
    126 			}
    127 			// copy element
    128 			for ; r < n && path[r] != '/'; r++ {
    129 				out.append(path[r])
    130 			}
    131 		}
    132 	}
    133 
    134 	// Turn empty string into "."
    135 	if out.w == 0 {
    136 		return "."
    137 	}
    138 
    139 	return out.string()
    140 }
    141 
    142 // Split splits path immediately following the final slash,
    143 // separating it into a directory and file name component.
    144 // If there is no slash in path, Split returns an empty dir and
    145 // file set to path.
    146 // The returned values have the property that path = dir+file.
    147 func Split(path string) (dir, file string) {
    148 	i := strings.LastIndex(path, "/")
    149 	return path[:i+1], path[i+1:]
    150 }
    151 
    152 // Join joins any number of path elements into a single path, adding a
    153 // separating slash if necessary. The result is Cleaned; in particular,
    154 // all empty strings are ignored.
    155 func Join(elem ...string) string {
    156 	for i, e := range elem {
    157 		if e != "" {
    158 			return Clean(strings.Join(elem[i:], "/"))
    159 		}
    160 	}
    161 	return ""
    162 }
    163 
    164 // Ext returns the file name extension used by path.
    165 // The extension is the suffix beginning at the final dot
    166 // in the final slash-separated element of path;
    167 // it is empty if there is no dot.
    168 func Ext(path string) string {
    169 	for i := len(path) - 1; i >= 0 && path[i] != '/'; i-- {
    170 		if path[i] == '.' {
    171 			return path[i:]
    172 		}
    173 	}
    174 	return ""
    175 }
    176 
    177 // Base returns the last element of path.
    178 // Trailing slashes are removed before extracting the last element.
    179 // If the path is empty, Base returns ".".
    180 // If the path consists entirely of slashes, Base returns "/".
    181 func Base(path string) string {
    182 	if path == "" {
    183 		return "."
    184 	}
    185 	// Strip trailing slashes.
    186 	for len(path) > 0 && path[len(path)-1] == '/' {
    187 		path = path[0 : len(path)-1]
    188 	}
    189 	// Find the last element
    190 	if i := strings.LastIndex(path, "/"); i >= 0 {
    191 		path = path[i+1:]
    192 	}
    193 	// If empty now, it had only slashes.
    194 	if path == "" {
    195 		return "/"
    196 	}
    197 	return path
    198 }
    199 
    200 // IsAbs reports whether the path is absolute.
    201 func IsAbs(path string) bool {
    202 	return len(path) > 0 && path[0] == '/'
    203 }
    204 
    205 // Dir returns all but the last element of path, typically the path's directory.
    206 // After dropping the final element using Split, the path is Cleaned and trailing
    207 // slashes are removed.
    208 // If the path is empty, Dir returns ".".
    209 // If the path consists entirely of slashes followed by non-slash bytes, Dir
    210 // returns a single slash. In any other case, the returned path does not end in a
    211 // slash.
    212 func Dir(path string) string {
    213 	dir, _ := Split(path)
    214 	return Clean(dir)
    215 }
    216