Files
Ingo Karkat 483cff66a5 ingo#fs#path#Canonicalize(): Drop trailing path separator
Functions like getcwd() don't return it, but somehow fnamemodify(..., ':p') of a dirspec does. That's inconsistent; fix it.
2022-06-20 20:22:51 +02:00

278 lines
10 KiB
VimL

" ingo/fs/path.vim: Functions for manipulating a file system path.
"
" DEPENDENCIES:
" - ingo/compat.vim autoload script
" - ingo/escape/file.vim autoload script
" - ingo/os.vim autoload script
" - ingo/fs/path/split.vim autoload script
"
" Copyright: (C) 2012-2022 Ingo Karkat
" The VIM LICENSE applies to this script; see ':help copyright'.
"
" Maintainer: Ingo Karkat <ingo@karkat.de>
function! ingo#fs#path#Separator()
return (exists('+shellslash') && ! &shellslash ? '\' : '/')
endfunction
function! ingo#fs#path#Normalize( filespec, ... )
"******************************************************************************
"* PURPOSE:
" Change all path separators in a:filespec to the passed or the typical format
" for the current platform.
" On Windows and Cygwin, also converts between the different D:\ and
" /cygdrive/d/ notations.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Filespec, potentially with mixed / and \ path separators.
" a:pathSeparator Optional path separator to be used. With the special value
" of ":/", normalizes to "/", but keeps a "C:/" drive letter
" prefix instead of translating to "/cygdrive/c/".
"* RETURN VALUES:
" a:filespec with uniform path separators, according to the platform.
"******************************************************************************
let l:pathSeparator = (a:0 ? (a:1 ==# ':/' ? '/' : a:1) : ingo#fs#path#Separator())
let l:badSeparator = (l:pathSeparator ==# '/' ? '\' : '/')
let l:result = tr(a:filespec, l:badSeparator, l:pathSeparator)
if ingo#os#IsWinOrDos()
let l:result = substitute(l:result, '^[/\\]cygdrive[/\\]\(\a\)\ze[/\\]', '\u\1:', '')
elseif ingo#os#IsCygwin() && l:pathSeparator ==# '/' && ! (a:0 && a:1 ==# ':/')
let l:result = substitute(l:result, '^\(\a\):', '/cygdrive/\l\1', '')
endif
return l:result
endfunction
function! s:Canonicalize( filespec, isResolveLinks ) abort
let l:absoluteFilespec = fnamemodify(a:filespec, ':p') " Expand to absolute filespec before resolving; as this handles ~/, too.
let l:simplifiedFilespec = (a:isResolveLinks ? resolve(l:absoluteFilespec) : simplify(l:absoluteFilespec))
let l:normalizedFilespec = ingo#fs#path#Normalize(l:simplifiedFilespec)
let l:pathSeparator = ingo#fs#path#Separator()
if ingo#str#EndsWith(l:normalizedFilespec, l:pathSeparator) && l:normalizedFilespec !=# l:pathSeparator
let l:normalizedFilespec = strpart(l:normalizedFilespec, 0, len(l:normalizedFilespec) - len(l:pathSeparator))
endif
return l:normalizedFilespec
endfunction
function! ingo#fs#path#Canonicalize( filespec, ... )
"******************************************************************************
"* PURPOSE:
" Convert a:filespec into a unique, canonical form that other instances can be
" compared against for equality. Expands to an absolute filespec and may
" change case. Removes ../ etc. Only resolves shortcuts / symbolic links on
" demand, as it depends on the use case whether these should be identical or
" not.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Filespec, potentially relative or with mixed / and \ path
" separators.
" a:isResolveLinks Flag whether to resolve shortcuts / symbolic links, too;
" off by default.
"* RETURN VALUES:
" Absolute a:filespec with uniform path separators and case, according to the
" platform.
"******************************************************************************
let l:result = s:Canonicalize(a:filespec, (a:0 ? a:1 : 0))
if ingo#fs#path#IsCaseInsensitive(l:result)
let l:result = tolower(l:result)
endif
return l:result
endfunction
function! ingo#fs#path#Combine( first, ... )
"******************************************************************************
"* PURPOSE:
" Concatenate the passed filespec fragments into a filespec, ensuring that all
" fragments are combined with proper path separators.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" Either pass a dirspec and one or many filenames:
" a:dirspec, a:filename [, a:filename2, ...]
" Or a single list containing all filespec fragments.
" [a:dirspec, a:filename, ...]
"* RETURN VALUES:
" Combined filespec.
"******************************************************************************
if type(a:first) == type([])
let l:dirspec = a:first[0]
let l:filenames = a:first[1:]
else
let l:dirspec = a:first
let l:filenames = a:000
endif
" Use path separator as exemplified by the passed dirspec.
if l:dirspec =~# '\' && l:dirspec !~# '/'
let l:pathSeparator = '\'
elseif l:dirspec =~# '/'
let l:pathSeparator = '/'
else
" The dirspec doesn't contain a path separator, fall back to the
" system's default.
let l:pathSeparator = ingo#fs#path#Separator()
endif
let l:filespec = l:dirspec
for l:filename in l:filenames
let l:filename = substitute(l:filename, '^[/\\]', '', '')
let l:filespec .= (l:filespec =~# '^$\|[/\\]$' ? '' : l:pathSeparator) . l:filename
endfor
return l:filespec
endfunction
function! ingo#fs#path#IsUncPathRoot( filespec )
let l:ps = escape(ingo#fs#path#Separator(), '\')
let l:uncPathPattern = printf('^%s%s[^%s]\+%s[^%s]\+$', l:ps, l:ps, l:ps, l:ps, l:ps)
return (a:filespec =~# l:uncPathPattern)
endfunction
function! ingo#fs#path#GetRootDir( filespec )
"******************************************************************************
"* PURPOSE:
" Determine the root directory of a:filespec.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Full path (use |::p| modifier if necessary).
"* RETURN VALUES:
" Root drive / UNC path / "/".
"******************************************************************************
if ! ingo#os#IsWinOrDos()
return '/'
endif
let l:dir = a:filespec
while fnamemodify(l:dir, ':h') !=# l:dir && ! ingo#fs#path#IsUncPathRoot(l:dir)
let l:dir = fnamemodify(l:dir, ':h')
endwhile
if empty(l:dir)
throw 'GetRootDir: Could not determine root dir!'
endif
return l:dir
endfunction
function! ingo#fs#path#IsAbsolute( filespec )
"******************************************************************************
"* PURPOSE:
" Test whether a:filespec is an absolute filespec; i.e. starts with a root
" drive / UNC path / "/".
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Relative / absolute filespec. Does not need to exist.
"* RETURN VALUES:
" 1 if it is absolute, else 0.
"******************************************************************************
let l:rootDir = ingo#fs#path#GetRootDir(fnamemodify(a:filespec, ':p'))
return (type(ingo#fs#path#split#AtBasePath(a:filespec, l:rootDir)) != type([]))
endfunction
function! ingo#fs#path#IsUpwards( filespec )
"******************************************************************************
"* PURPOSE:
" Test whether a:filespec navigates to a parent directory through ".." path
" elements.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Relative / absolute filespec. Does not need to exist.
"* RETURN VALUES:
" 1 if it navigates to a parent. 0 if it is absolute, or relative within the
" current context.
"******************************************************************************
return (ingo#fs#path#Normalize(simplify(a:filespec), '/') =~# '^\.\./')
endfunction
function! ingo#fs#path#IsPath( filespec ) abort
"******************************************************************************
"* PURPOSE:
" Test whether a:filespec represents an actual path (i.e. anything with a path
" separator in it, either relative or absolute) or just a (also possibly
" non-existing) file name.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Relative / absolute filespec. Does not need to exist.
"* RETURN VALUES:
" 1 if it represents a path, 0 if it is just a file name.
"******************************************************************************
let [l:dirspec, l:filename] = ingo#fs#path#split#PathAndName(simplify(a:filespec))
return ! empty(a:filespec) && (ingo#fs#path#Normalize(l:dirspec, '/') !=# './' || empty(l:filename))
endfunction
function! ingo#fs#path#IsCaseInsensitive( ... )
return ingo#os#IsWinOrDos() " Note: Check based on path not yet implemented.
endfunction
function! ingo#fs#path#Equals( p1, p2, ... )
"******************************************************************************
"* PURPOSE:
" Test whether a:p1 and a:p2 are identical.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:p1 Filespec.
" a:p2 Filespec.
" a:isResolveLinks Flag whether to resolve shortcuts / symbolic links, too;
" off by default.
"* RETURN VALUES:
" 1 if identical, 0 if not.
"******************************************************************************
let l:isResolveLinks = (a:0 ? a:1 : 0)
if ingo#fs#path#IsCaseInsensitive(a:p1) || ingo#fs#path#IsCaseInsensitive(a:p2)
return a:p1 ==? a:p2 || s:Canonicalize(a:p1, l:isResolveLinks) ==? s:Canonicalize(a:p2, l:isResolveLinks)
else
return a:p1 ==# a:p2 || s:Canonicalize(a:p1, l:isResolveLinks) ==# s:Canonicalize(a:p2, l:isResolveLinks)
endif
endfunction
function! ingo#fs#path#Exists( filespec )
"******************************************************************************
"* PURPOSE:
" Test whether the passed a:filespec exists (as a file or directory). This is
" like the combination of filereadable() and isdirectory(), but without the
" requirement that the file must be readable.
"* ASSUMPTIONS / PRECONDITIONS:
" None.
"* EFFECTS / POSTCONDITIONS:
" None.
"* INPUTS:
" a:filespec Filespec or dirspec.
"* RETURN VALUES:
" 0 if there's no such file or directory, 1 if it exists.
"******************************************************************************
" I suppose these are faster than the glob(), and this avoids any escaping
" issues, too, so it is more robust.
if filereadable(a:filespec) || isdirectory(a:filespec)
return 1
endif
let l:filespec = ingo#escape#file#wildcardescape(a:filespec)
return ! empty(ingo#compat#glob(l:filespec, 1))
endfunction
" vim: set ts=8 sts=4 sw=4 noexpandtab ff=unix fdm=syntax :