@@ -515,30 +515,70 @@ function Path:rmdir()
515515 uv .fs_rmdir (self :absolute ())
516516end
517517
518+ --- Rename this file or directory to the given path (`opts.new_name`), and
519+ --- return a new Path instance pointing to it. The rename is aborted if the new
520+ --- path already exists as a separate file.
521+ --- @generic T : Path
522+ --- @param opts { new_name : Path | string }
523+ --- @return T
518524function Path :rename (opts )
525+ -- TODO: For reference, Python's `Path.rename()` actually says/does this:
526+ --
527+ -- > On Unix, if target exists and is a file, it will be replaced silently
528+ -- > if the user has permission.
529+ -- >
530+ -- > On Windows, if target exists, FileExistsError will be raised. target
531+ -- > can be either a string or another path object.
532+ --
533+ -- The behavior here may differ, as an error will be thrown regardless.
534+
535+ local self_lstat , new_lstat , status , errmsg
519536 opts = opts or {}
520- if not opts .new_name or opts .new_name == " " then
521- error " Please provide the new name!"
522- end
537+ assert (opts .new_name and opts .new_name ~= " " , " Please provide the new name!" )
538+ self_lstat , errmsg = uv .fs_lstat (self .filename )
539+
540+ -- Cannot rename a non-existing path (lstat is needed here, `Path:exists()`
541+ -- uses stat)
542+ assert (self_lstat , (" %s: %s" ):format (errmsg , self .filename ))
523543
544+ -- BUG
524545 -- handles `.`, `..`, `./`, and `../`
525546 if opts .new_name :match " ^%.%.?/?\\ ?.+" then
526547 opts .new_name = {
527548 uv .fs_realpath (opts .new_name :sub (1 , 3 )),
528- opts .new_name :sub (4 , # opts . new_name ),
549+ opts .new_name :sub (4 ),
529550 }
530551 end
531552
532553 local new_path = Path :new (opts .new_name )
533-
534- if new_path :exists () then
535- error " File or directory already exists!"
536- end
537-
538- local status = uv .fs_rename (self :absolute (), new_path :absolute ())
539- self .filename = new_path .filename
540-
541- return status
554+ new_lstat , errmsg = uv .fs_lstat (new_path .filename )
555+
556+ -- This allows changing only case (e.g. fname -> Fname) on case-insensitive
557+ -- file systems, otherwise throwing if `new_name` exists as a different file.
558+ --
559+ -- NOTE: to elaborate, `uv.fs_rename()` wont/shouldn't do anything if old
560+ -- and new both exist and are both hard links to the same file (inode),
561+ -- however, it appears to still allow you to change the case of a filename
562+ -- on case-insensitive file systems (e.g. if `new_name` doesn't _actually_
563+ -- exist as a separate file but would otherwise appear to via an lstat call;
564+ -- if it does actually exist (in which case the fs must be case-sensitive)
565+ -- idk 100% what happens b/c it needs to be tested on a case-sensitive fs,
566+ -- but it should simply result in a successful no-op according to rename(2)
567+ -- docs, at least on Linux anyway)
568+ assert (not new_lstat or (self_lstat .ino == new_lstat .ino ), " File or directory already exists!" )
569+
570+ status , errmsg = uv .fs_rename (self :absolute (), new_path :absolute ())
571+ assert (status , (" %s: Rename failed!" ):format (errmsg ))
572+
573+ -- NOTE: `uv.fs_rename()` _can_ return success even if no rename actually
574+ -- occurred (see rename(2)), and this is not an error...we're not changing
575+ -- `self.filename` if it didn't.
576+ if not uv .fs_lstat (self .filename ) then
577+ self .filename = new_path .filename
578+ end
579+
580+ -- TODO: Python returns a brand new instance here, should we do the same?
581+ return self
542582end
543583
544584--- Copy files or folders with defaults akin to GNU's `cp`.
0 commit comments