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