Add Path.directory

This method can be used to obtain a Path pointing to the directory of
another Path, without touching the file system. A simple example:

    import std::fs::path::Path

    Path.new('/foo/bar/baz').directory.to_string # => '/foo/bar'

The internals of Path.directory depend on the newly introduced
std::fs::path::bits module. This module will be used for defining
internal methods used as the building blocks of the Path type. This
allows us to offload work to separate methods (so we can return early
where needed), without exposing all this in the public API of
std::fs::path.
parent a65c52d0
Pipeline #104634857 passed with stages
in 23 minutes and 21 seconds
......@@ -45,15 +45,10 @@ import std::operators::Equal
import std::os
import std::string_buffer::StringBuffer
import std::time::SystemTime
import std::fs::path::bits::(self, SEPARATOR as _SEPARATOR)
# The primary separator of path components.
let SEPARATOR = os.windows?.if(true: { '\\' }, false: { '/' })
# The byte range for lowercase Windows drive letters (a..z)
let WINDOWS_LOWER_DRIVE_LETTERS = 97..122
# The byte range for uppercase Windows drive letters (A..Z)
let WINDOWS_UPPER_DRIVE_LETTERS = 65..90
let SEPARATOR = _SEPARATOR
# A path to a file or directory.
#
......@@ -153,26 +148,9 @@ object Path {
# Path.new('foo').absolute? # => False
# Path.new('/foo').absolute? # => True
def absolute? -> Boolean {
@path.starts_with?(SEPARATOR).if_true {
return True
}
os.windows?.if_false {
return False
}
let first_byte = @path.byte(index: 0)
WINDOWS_LOWER_DRIVE_LETTERS.cover?(first_byte)
.or { WINDOWS_UPPER_DRIVE_LETTERS.cover?(first_byte) }
.and {
# 58 is the byte for ":"
@path.byte(index: 1) == 58
}
.and {
# 92 is the byte for "\"
@path.byte(index: 2) == 92
}
bits
.path_separator?(@path.byte(index: 0))
.or { os.windows?.and { bits.starts_with_windows_drive_name?(@path) } }
}
# Returns `True` if this `Path` is a relative path.
......@@ -223,6 +201,34 @@ object Path {
Path.new(buffer.to_string)
}
# Returns a `Path` to the directory of the current `Path`.
#
# This method does not touch the filesystem, and thus does not resolve paths
# like `..` and symbolic links to their real paths.
#
# # Examples
#
# Obtaining the directory of a path:
#
# import std::fs::path::Path
#
# Path.new('/foo/bar').directory # => Path.new('/foo')
#
# Obtaining the directory of the root directory:
#
# import std::fs::path::Path
#
# Path.new('/').directory # Path.new('/')
def directory -> Path {
let length = bits.bytes_before_last_separator(@path)
length.negative?.if_true {
return Path.new('.')
}
Path.new(@path.slice_bytes(start: 0, length: length).drain_to_string)
}
}
impl Equal for Path {
......
# Functionality used by `std::fs::path` to implement bits of the `Path` type.
#
# This module is not part of the public API and should not be used directly.
import std::os
# The separator to use for Unix path components.
let UNIX_SEPARATOR = '/'
# The byte separator to use for Unix path components.
let UNIX_SEPARATOR_BYTE = UNIX_SEPARATOR.byte(0)
# The primary separator of path components.
let SEPARATOR = os.windows?.if(true: { '\\' }, false: { UNIX_SEPARATOR })
# The primary byte separator to use for path components.
let SEPARATOR_BYTE = SEPARATOR.byte(0)
# The alternative separator byte of path components, if there is any.
let ALT_SEPARATOR_BYTE =
os.windows?.if(true: { UNIX_SEPARATOR_BYTE }, false: { -1 })
# The byte range for lowercase Windows drive letters (a..z)
let WINDOWS_LOWER_DRIVE_LETTERS = 97..122
# The byte range for uppercase Windows drive letters (A..Z)
let WINDOWS_UPPER_DRIVE_LETTERS = 65..90
# The byte for a single colon (":").
let COLON_BYTE = 58
# Returns `True` if the given `String` starts with a Windows drive name, such as
# C:/.
def starts_with_windows_drive_name?(path: String) -> Boolean {
let first_byte = path.byte(index: 0)
WINDOWS_LOWER_DRIVE_LETTERS.cover?(first_byte)
.or { WINDOWS_UPPER_DRIVE_LETTERS.cover?(first_byte) }
.and { path.byte(index: 1) == COLON_BYTE }
.and { path_separator?(path.byte(index: 2)) }
}
# Returns `True` if the byte is a valid path separator byte.
def path_separator?(byte: Integer) -> Boolean {
(byte == SEPARATOR_BYTE).or { byte == ALT_SEPARATOR_BYTE }
}
# Returns the number of bytes leading up to the last path separator.
#
# If no separator could be found, `-1` is returned.
def bytes_before_last_separator(path: String) -> Integer {
path.empty?.if_true {
return -1
}
let windows_drive_path =
os.windows?.and { starts_with_windows_drive_name?(path) }
# If the path starts with a Windows drive name (e.g. "C:\foo") we don't want
# to trim off the \ in C:\, as it's part of the drive name.
let trim_until = windows_drive_path.if(true: { 2 }, false: { 0 })
let mut index = path.bytesize - 1
# Trailing separators should be ignored, so we'll skip over them until the
# first non-separator byte.
{
path_separator?(path.byte(index)).and { index > trim_until }
}.while_true {
index -= 1
}
let mut in_separator = False
{ index > -1 }.while_true {
let byte = path.byte(index)
path_separator?(byte).if(
true: { in_separator = True },
false: {
in_separator.if_true {
windows_drive_path.and { index == 1 }.if_true {
# We have reached the ":" in a drive name such as "C:\". In this
# case we want to include the "\" since it's part of the drive
# name.
return 3
}
return index + 1
}
}
)
index -= 1
}
in_separator.if(true: { 1 }, false: { -1 })
}
......@@ -14,6 +14,13 @@ let WINDOWS_DRIVES = Array.new(
'A:\\', 'B:\\', 'C:\\', 'D:\\', 'E:\\', 'F:\\', 'G:\\', 'H:\\', 'I:\\',
'J:\\', 'K:\\', 'L:\\', 'M:\\', 'N:\\', 'O:\\', 'P:\\', 'Q:\\', 'R:\\',
'S:\\', 'T:\\', 'U:\\', 'V:\\', 'W:\\', 'X:\\', 'Y:\\', 'Z:\\',
'a:/', 'b:/', 'c:/', 'd:/', 'e:/', 'f:/', 'g:/', 'h:/', 'i:/',
'j:/', 'k:/', 'l:/', 'm:/', 'n:/', 'o:/', 'p:/', 'q:/', 'r:/',
's:/', 't:/', 'u:/', 'v:/', 'w:/', 'x:/', 'y:/', 'z:/',
'A:/', 'B:/', 'C:/', 'D:/', 'E:/', 'F:/', 'G:/', 'H:/', 'I:/',
'J:/', 'K:/', 'L:/', 'M:/', 'N:/', 'O:/', 'P:/', 'Q:/', 'R:/',
'S:/', 'T:/', 'U:/', 'V:/', 'W:/', 'X:/', 'Y:/', 'Z:/',
)
test.group('std::fs::Path.file?') do (g) {
......@@ -98,6 +105,9 @@ test.group('std::fs::path::Path.absolute?') do (g) {
assert.false(Path.new('foo').absolute?)
assert.false(Path.new('..').absolute?)
assert.true(Path.new(SEPARATOR + 'foo').absolute?)
# On Windows /foo (or any path starting with /) is also absolute.
assert.true(Path.new('/foo').absolute?)
}
os.windows?.if_true {
......@@ -186,3 +196,59 @@ test.group('std::fs::path::Path.join') do (g) {
assert.equal(path.join('bar'), expected)
}
}
test.group('std::path::Path.directory') do (g) {
g.test('Obtaining the directory of an absolute Path') {
os.windows?.if_true {
assert.equal(Path.new('C:/foo/bar').directory, Path.new('C:/foo'))
assert.equal(Path.new('C:/foo').directory, Path.new('C:/'))
assert.equal(Path.new('C:/').directory, Path.new('C:/'))
assert.equal(Path.new('C:\\foo\\bar').directory, Path.new('C:\\foo'))
assert.equal(Path.new('C:\\foo').directory, Path.new('C:\\'))
assert.equal(Path.new('C:\\').directory, Path.new('C:\\'))
}
assert.equal(Path.new('/foo/bar').directory, Path.new('/foo'))
assert.equal(Path.new('/foo/bar/..').directory, Path.new('/foo/bar'))
assert.equal(Path.new('/foo').directory, Path.new('/'))
assert.equal(Path.new('/').directory, Path.new('/'))
}
g.test('Obtaining the directory of a Path with trailing separators') {
os.windows?.if_true {
assert.equal(Path.new('C:\\foo\\bar\\').directory, Path.new('C:\\foo'))
assert.equal(Path.new('C:\\foo\\').directory, Path.new('C:\\'))
assert.equal(Path.new('C:/foo/bar/').directory, Path.new('C:/foo'))
assert.equal(Path.new('C:/foo/').directory, Path.new('C:/'))
}
assert.equal(Path.new('/foo/bar/').directory, Path.new('/foo'))
assert.equal(Path.new('/foo/').directory, Path.new('/'))
}
g.test('Obtaining the directory of a Path containing multiple separators') {
os.windows?.if_true {
assert.equal(
Path.new('C:\\foo\\\\bar\\\\baz').directory,
Path.new('C:\\foo\\\\bar')
)
}
assert.equal(Path.new('/foo//bar//').directory, Path.new('/foo'))
assert.equal(Path.new('/foo//bar//baz').directory, Path.new('/foo//bar'))
}
g.test('Obtaining the directory of a relative Path') {
os.windows?.if_true {
assert.equal(Path.new('foo\\bar').directory, Path.new('foo'))
}
assert.equal(Path.new('foo/bar').directory, Path.new('foo'))
assert.equal(Path.new('foo').directory, Path.new('.'))
assert.equal(Path.new('.').directory, Path.new('.'))
assert.equal(Path.new('./foo').directory, Path.new('.'))
assert.equal(Path.new('').directory, Path.new('.'))
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment