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