os: add norm_path and abs_path function (#14435)
							parent
							
								
									efc5cab8c3
								
							
						
					
					
						commit
						971c55cf30
					
				|  | @ -10,7 +10,7 @@ on: | |||
|       - "**.md" | ||||
| 
 | ||||
| concurrency: | ||||
|   group: build-other-${{ github.event.pull_request.number || github.sha }} | ||||
|   group: build-v-apps-and-modules-${{ github.event.pull_request.number || github.sha }} | ||||
|   cancel-in-progress: true | ||||
| 
 | ||||
| jobs: | ||||
|  |  | |||
|  | @ -1,13 +1,20 @@ | |||
| module os | ||||
| 
 | ||||
| import strings | ||||
| import strings.textscanner | ||||
| 
 | ||||
| // Collection of useful functions for manipulation, validation and analysis of system paths.
 | ||||
| // The following functions handle paths depending on the operating system,
 | ||||
| // therefore results may be different for certain operating systems.
 | ||||
| 
 | ||||
| const ( | ||||
| 	fslash = `/` | ||||
| 	bslash = `\\` | ||||
| 	dot    = `.` | ||||
| 	fslash  = `/` | ||||
| 	bslash  = `\\` | ||||
| 	dot     = `.` | ||||
| 	qmark   = `?` | ||||
| 	dot_dot = '..' | ||||
| 	empty   = '' | ||||
| 	dot_str = '.' | ||||
| ) | ||||
| 
 | ||||
| // is_abs_path returns `true` if the given `path` is absolute.
 | ||||
|  | @ -16,11 +23,152 @@ pub fn is_abs_path(path string) bool { | |||
| 		return false | ||||
| 	} | ||||
| 	$if windows { | ||||
| 		return is_device_path(path) || is_drive_rooted(path) || is_normal_path(path) | ||||
| 		return is_unc_path(path) || is_drive_rooted(path) || is_normal_path(path) | ||||
| 	} | ||||
| 	return path[0] == os.fslash | ||||
| } | ||||
| 
 | ||||
| // abs_path joins the current working directory
 | ||||
| // with the given `path` (if the `path` is relative)
 | ||||
| // and returns the absolute path representation.
 | ||||
| pub fn abs_path(path string) string { | ||||
| 	wd := getwd() | ||||
| 	if path.len == 0 { | ||||
| 		return wd | ||||
| 	} | ||||
| 	npath := norm_path(path) | ||||
| 	if npath == os.dot_str { | ||||
| 		return wd | ||||
| 	} | ||||
| 	if !is_abs_path(npath) { | ||||
| 		mut sb := strings.new_builder(npath.len) | ||||
| 		sb.write_string(wd) | ||||
| 		sb.write_string(path_separator) | ||||
| 		sb.write_string(npath) | ||||
| 		return norm_path(sb.str()) | ||||
| 	} | ||||
| 	return npath | ||||
| } | ||||
| 
 | ||||
| // norm_path returns the normalized version of the given `path`
 | ||||
| // by resolving backlinks (..), turning forward slashes into
 | ||||
| // back slashes on a Windows system and eliminating:
 | ||||
| // - references to current directories (.)
 | ||||
| // - redundant path separators
 | ||||
| // - the last path separator
 | ||||
| [direct_array_access] | ||||
| pub fn norm_path(path string) string { | ||||
| 	if path.len == 0 { | ||||
| 		return '.' | ||||
| 	} | ||||
| 	rooted := is_abs_path(path) | ||||
| 	volume := get_volume(path) | ||||
| 	volume_len := volume.len | ||||
| 	cpath := clean_path(path[volume_len..]) | ||||
| 	if cpath.len == 0 && volume_len == 0 { | ||||
| 		return '.' | ||||
| 	} | ||||
| 	spath := cpath.split(path_separator) | ||||
| 	if os.dot_dot !in spath { | ||||
| 		return if volume_len != 0 { volume + cpath } else { cpath } | ||||
| 	} | ||||
| 	// resolve backlinks (..)
 | ||||
| 	spath_len := spath.len | ||||
| 	mut sb := strings.new_builder(cpath.len) | ||||
| 	if rooted { | ||||
| 		sb.write_string(path_separator) | ||||
| 	} | ||||
| 	mut new_path := []string{cap: spath_len} | ||||
| 	mut backlink_count := 0 | ||||
| 	for i := spath_len - 1; i >= 0; i-- { | ||||
| 		part := spath[i] | ||||
| 		if part == os.empty { | ||||
| 			continue | ||||
| 		} | ||||
| 		if part == os.dot_dot { | ||||
| 			backlink_count++ | ||||
| 			continue | ||||
| 		} | ||||
| 		if backlink_count != 0 { | ||||
| 			backlink_count-- | ||||
| 			continue | ||||
| 		} | ||||
| 		new_path.prepend(part) | ||||
| 	} | ||||
| 	// append backlink(s) to the path if backtracking
 | ||||
| 	// is not possible and the given path is not rooted
 | ||||
| 	if backlink_count != 0 && !rooted { | ||||
| 		for i in 0 .. backlink_count { | ||||
| 			sb.write_string(os.dot_dot) | ||||
| 			if new_path.len == 0 && i == backlink_count - 1 { | ||||
| 				break | ||||
| 			} | ||||
| 			sb.write_string(path_separator) | ||||
| 		} | ||||
| 	} | ||||
| 	sb.write_string(new_path.join(path_separator)) | ||||
| 	res := sb.str() | ||||
| 	if res.len == 0 { | ||||
| 		if volume_len != 0 { | ||||
| 			return volume | ||||
| 		} | ||||
| 		if !rooted { | ||||
| 			return '.' | ||||
| 		} | ||||
| 		return path_separator | ||||
| 	} | ||||
| 	if volume_len != 0 { | ||||
| 		return volume + res | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
| 
 | ||||
| // clean_path returns the "cleaned" version of the given `path`
 | ||||
| // by turning forward slashes into back slashes
 | ||||
| // on a Windows system and eliminating:
 | ||||
| // - references to current directories (.)
 | ||||
| // - redundant separators
 | ||||
| // - the last path separator
 | ||||
| fn clean_path(path string) string { | ||||
| 	if path.len == 0 { | ||||
| 		return '' | ||||
| 	} | ||||
| 	mut sb := strings.new_builder(path.len) | ||||
| 	mut sc := textscanner.new(path) | ||||
| 	for sc.next() != -1 { | ||||
| 		curr := u8(sc.current()) | ||||
| 		back := sc.peek_back() | ||||
| 		peek := sc.peek() | ||||
| 		// skip current path separator if last byte was a path separator
 | ||||
| 		if back != -1 && is_slash(u8(back)) && is_slash(curr) { | ||||
| 			continue | ||||
| 		} | ||||
| 		// skip reference to current dir (.)
 | ||||
| 		if (back == -1 || is_slash(u8(back))) && curr == os.dot | ||||
| 			&& (peek == -1 || is_slash(u8(peek))) { | ||||
| 			// skip if the next byte is a path separator
 | ||||
| 			if peek != -1 && is_slash(u8(peek)) { | ||||
| 				sc.skip_n(1) | ||||
| 			} | ||||
| 			continue | ||||
| 		} | ||||
| 		// turn foward slash into a back slash on a Windows system
 | ||||
| 		$if windows { | ||||
| 			if curr == os.fslash { | ||||
| 				sb.write_u8(os.bslash) | ||||
| 				continue | ||||
| 			} | ||||
| 		} | ||||
| 		sb.write_u8(u8(sc.current())) | ||||
| 	} | ||||
| 	res := sb.str() | ||||
| 	// eliminate the last path separator
 | ||||
| 	if res.len > 1 && is_slash(res[res.len - 1]) { | ||||
| 		return res[..res.len - 1] | ||||
| 	} | ||||
| 	return res | ||||
| } | ||||
| 
 | ||||
| // win_volume_len returns the length of the
 | ||||
| // Windows volume/drive from the given `path`.
 | ||||
| fn win_volume_len(path string) int { | ||||
|  | @ -32,7 +180,7 @@ fn win_volume_len(path string) int { | |||
| 		return 2 | ||||
| 	} | ||||
| 	// its UNC path / DOS device path?
 | ||||
| 	if path.len >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) { | ||||
| 	if plen >= 5 && starts_w_slash_slash(path) && !is_slash(path[2]) { | ||||
| 		for i := 3; i < plen; i++ { | ||||
| 			if is_slash(path[i]) { | ||||
| 				if i + 1 >= plen || is_slash(path[i + 1]) { | ||||
|  | @ -51,6 +199,20 @@ fn win_volume_len(path string) int { | |||
| 	return 0 | ||||
| } | ||||
| 
 | ||||
| fn get_volume(path string) string { | ||||
| 	$if !windows { | ||||
| 		return '' | ||||
| 	} | ||||
| 	volume := path[..win_volume_len(path)] | ||||
| 	if volume.len == 0 { | ||||
| 		return '' | ||||
| 	} | ||||
| 	if volume[0] == os.fslash { | ||||
| 		return volume.replace('/', '\\') | ||||
| 	} | ||||
| 	return volume | ||||
| } | ||||
| 
 | ||||
| fn is_slash(b u8) bool { | ||||
| 	$if windows { | ||||
| 		return b == os.bslash || b == os.fslash | ||||
|  | @ -58,7 +220,7 @@ fn is_slash(b u8) bool { | |||
| 	return b == os.fslash | ||||
| } | ||||
| 
 | ||||
| fn is_device_path(path string) bool { | ||||
| fn is_unc_path(path string) bool { | ||||
| 	return win_volume_len(path) >= 5 && starts_w_slash_slash(path) | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -27,3 +27,103 @@ fn test_is_abs_path() { | |||
| 	assert !is_abs_path('./') | ||||
| 	assert !is_abs_path('.') | ||||
| } | ||||
| 
 | ||||
| fn test_clean_path() { | ||||
| 	$if windows { | ||||
| 		assert clean_path(r'\\path\to\files/file.v') == r'\path\to\files\file.v' | ||||
| 		assert clean_path(r'\/\//\/') == '\\' | ||||
| 		assert clean_path(r'./path\\dir/\\./\/\\/file.v\.\\\.') == r'path\dir\file.v' | ||||
| 		assert clean_path(r'\./path/dir\\file.exe') == r'\path\dir\file.exe' | ||||
| 		assert clean_path(r'.') == '' | ||||
| 		assert clean_path(r'./') == '' | ||||
| 		assert clean_path(r'\./') == '\\' | ||||
| 		assert clean_path(r'//\/\/////') == '\\' | ||||
| 		return | ||||
| 	} | ||||
| 	assert clean_path('./../.././././//') == '../..' | ||||
| 	assert clean_path('.') == '' | ||||
| 	assert clean_path('./path/to/file.v//./') == 'path/to/file.v' | ||||
| 	assert clean_path('./') == '' | ||||
| 	assert clean_path('/.') == '/' | ||||
| 	assert clean_path('//path/./to/.///files/file.v///') == '/path/to/files/file.v' | ||||
| 	assert clean_path('path/./to/.///files/.././file.v///') == 'path/to/files/../file.v' | ||||
| 	assert clean_path('\\') == '\\' | ||||
| 	assert clean_path('//////////') == '/' | ||||
| } | ||||
| 
 | ||||
| fn test_norm_path() { | ||||
| 	$if windows { | ||||
| 		assert norm_path(r'C:/path/to//file.v\\') == r'C:\path\to\file.v' | ||||
| 		assert norm_path(r'C:path\.\..\\\.\to//file.v') == r'C:to\file.v' | ||||
| 		assert norm_path(r'D:path\.\..\..\\\\.\to//dir/..\') == r'D:..\to' | ||||
| 		assert norm_path(r'D:/path\.\..\/..\file.v') == r'D:\file.v' | ||||
| 		assert norm_path(r'') == '.' | ||||
| 		assert norm_path(r'/') == '\\' | ||||
| 		assert norm_path(r'\/') == '\\' | ||||
| 		assert norm_path(r'path\../dir\..') == '.' | ||||
| 		assert norm_path(r'.\.\') == '.' | ||||
| 		assert norm_path(r'G:.\.\dir\././\.\.\\\\///to/././\file.v/./\\') == r'G:dir\to\file.v' | ||||
| 		assert norm_path(r'G:\..\..\.\.\file.v\\\.') == r'G:\file.v' | ||||
| 		assert norm_path(r'\\Server\share\\\dir/..\file.v\./.') == r'\\Server\share\file.v' | ||||
| 		assert norm_path(r'\\.\device\\\dir/to/./file.v\.') == r'\\.\device\dir\to\file.v' | ||||
| 		assert norm_path(r'C:dir/../dir2/../../../file.v') == r'C:..\..\file.v' | ||||
| 		assert norm_path(r'\\.\C:\\\Users/\Documents//..') == r'\\.\C:\Users' | ||||
| 		assert norm_path(r'\\.\C:\Users') == r'\\.\C:\Users' | ||||
| 		assert norm_path(r'\\') == '\\' | ||||
| 		assert norm_path(r'//') == '\\' | ||||
| 		assert norm_path(r'\\\') == '\\' | ||||
| 		assert norm_path(r'.') == '.' | ||||
| 		assert norm_path(r'\\Server') == '\\Server' | ||||
| 		assert norm_path(r'\\Server\') == '\\Server' | ||||
| 		return | ||||
| 	} | ||||
| 	assert norm_path('/path/././../to/file//file.v/.') == '/to/file/file.v' | ||||
| 	assert norm_path('path/././to/files/../../file.v/.') == 'path/file.v' | ||||
| 	assert norm_path('path/././/../../to/file.v/.') == '../to/file.v' | ||||
| 	assert norm_path('/path/././/../..///.././file.v/././') == '/file.v' | ||||
| 	assert norm_path('path/././//../../../to/dir//.././file.v/././') == '../../to/file.v' | ||||
| 	assert norm_path('path/../dir/..') == '.' | ||||
| 	assert norm_path('../dir/..') == '..' | ||||
| 	assert norm_path('/../dir/..') == '/' | ||||
| 	assert norm_path('//././dir/../files/././/file.v') == '/files/file.v' | ||||
| 	assert norm_path('/\\../dir/////////.') == '/\\../dir' | ||||
| 	assert norm_path('/home/') == '/home' | ||||
| 	assert norm_path('/home/////./.') == '/home' | ||||
| 	assert norm_path('...') == '...' | ||||
| } | ||||
| 
 | ||||
| fn test_abs_path() { | ||||
| 	wd := getwd() | ||||
| 	wd_w_sep := wd + path_separator | ||||
| 	$if windows { | ||||
| 		assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v' | ||||
| 		assert abs_path('path/to/file.v') == '${wd_w_sep}path\\to\\file.v' | ||||
| 		assert abs_path('/') == r'\' | ||||
| 		assert abs_path(r'C:\path\to\files\file.v') == r'C:\path\to\files\file.v' | ||||
| 		assert abs_path(r'C:\/\path\.\to\../files\file.v\.\\\.\') == r'C:\path\files\file.v' | ||||
| 		assert abs_path(r'\\Host\share\files\..\..\.') == r'\\Host\share\' | ||||
| 		assert abs_path(r'\\.\HardDiskvolume2\files\..\..\.') == r'\\.\HardDiskvolume2\' | ||||
| 		assert abs_path(r'\\?\share') == r'\\?\share' | ||||
| 		assert abs_path(r'\\.\') == r'\' | ||||
| 		assert abs_path(r'G:/\..\\..\.\.\file.v\\.\.\\\\') == r'G:\file.v' | ||||
| 		assert abs_path('files') == '${wd_w_sep}files' | ||||
| 		assert abs_path('') == wd | ||||
| 		assert abs_path('.') == wd | ||||
| 		assert abs_path('files/../file.v') == '${wd_w_sep}file.v' | ||||
| 		assert abs_path('///') == r'\' | ||||
| 		assert abs_path('/path/to/file.v') == r'\path\to\file.v' | ||||
| 		assert abs_path('D:/') == r'D:\' | ||||
| 		assert abs_path(r'\\.\HardiskVolume6') == r'\\.\HardiskVolume6' | ||||
| 		return | ||||
| 	} | ||||
| 	assert abs_path('/') == '/' | ||||
| 	assert abs_path('.') == wd | ||||
| 	assert abs_path('files') == '${wd_w_sep}files' | ||||
| 	assert abs_path('') == wd | ||||
| 	assert abs_path('files/../file.v') == '${wd_w_sep}file.v' | ||||
| 	assert abs_path('///') == '/' | ||||
| 	assert abs_path('/path/to/file.v') == '/path/to/file.v' | ||||
| 	assert abs_path('/path/to/file.v/../..') == '/path' | ||||
| 	assert abs_path('path/../file.v/..') == wd | ||||
| 	assert abs_path('///') == '/' | ||||
| } | ||||
|  |  | |||
|  | @ -270,7 +270,7 @@ pub fn (b &Builder) import_graph() &depgraph.DepGraph { | |||
| 			deps << 'builtin' | ||||
| 			if b.pref.backend == .c { | ||||
| 				// TODO JavaScript backend doesn't handle os for now
 | ||||
| 				if b.pref.is_vsh && p.mod.name !in ['os', 'dl'] { | ||||
| 				if b.pref.is_vsh && p.mod.name !in ['os', 'dl', 'strings.textscanner'] { | ||||
| 					deps << 'os' | ||||
| 				} | ||||
| 			} | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue