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. 12package path 13 14import "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. 20type lazybuf struct { 21 s string 22 buf []byte 23 w int 24} 25 26func (b *lazybuf) index(i int) byte { 27 if b.buf != nil { 28 return b.buf[i] 29 } 30 return b.s[i] 31} 32 33func (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 46func (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 72func 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. 145func 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. 155func 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. 179func 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 "/". 192func 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. 212func 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. 223func Dir(path string) string { 224 dir, _ := Split(path) 225 return Clean(dir) 226} 227