There are some buggy cases and a flaw in RENAME-FILE. If nobody
objects, I'd like to replace it with the code at the end of the message.
This is an incompatible change, so I'm asking first; but given the
proposed RENAME-FILE, it's fairly easy to define a function with our
current RENAME-FILE's behavior.
The bugs result from the fact that we try to rename the truename of the
first argument to a name that's the result of merging the second
argument with the truename of the first. Consequently,
1. When the first argument is a symlink, we rename the symlink's target,
and leave a broken symlink.
2. When both arguments are logical pathnames, by merging the second
argument with the truename of the first, we are liable to construct a
logical pathname that has no translation. For example,
(setf (logical-pathname-translations "NX")
no translation for #P"NX:DIR;ANOTHER-NAME.LSP.NEWEST"
[Condition of type SB-INT:SIMPLE-FILE-ERROR]
Something similar can happen when the first argument is a physical
pathname and the second is a logical pathname, but ISTM we can at
least get the case where they're both logical pathnames right.
The flaw is our handling of the case where NEW-NAME has a relative
directory. For instance, in this code
(let ((*default-pathname-defaults* (pathname "/tmp/")))
(rename-file "foo/quux.txt "bar/"))
we try to rename /tmp/foo/quux.txt to /tmp/foo/bar/quux.txt, because
that's what we get by merging the second argument with the first and
then with *DEFAULT-PATHNAME-DEFAULTS*. I think everybody would find
RENAME-FILE more useful if it tried to rename to /tmp/bar/quux.txt in
the above, and I think we have license to do so: the specification for
| The primary value, DEFAULTED-NEW-NAME, is the resulting name which is
| composed of NEW-NAME with any missing components filled in by
| performing a =E2=80=98merge-pathnames=E2=80=99 operation using FILESPEC =
The closest thing I can find for a definition of "missing" for pathname
components is in the dictionary entry for TRANSLATE-PATHNAME:
| A "missing field" is a pathname component with a value of =E2=80=98nil=
So I think we can conformingly interpret RENAME-FILE to say that it
fills in only the NIL components of a pathname, which gives us
recognizable behavior for RENAME-FILE when the second argument a
(setf *default-pathname-defaults* #P"/tmp/")
(open (ensure-directories-exist "dir1/file")
:direction :probe :if-does-not-exist :create)
(sb-posix:symlink "/tmp/dir1/file" "/tmp/dir1/symlink")
;; The existing routine renames "/tmp/dir1/file" to
;; "/tmp/dir1/./file", i.e., it does nothing, and does so by working
;; on the wrong file. The new results follow:
(rename-file "dir1/symlink" "./")
;; Suppose the setup above. The existing routine tries to rename
;; "/tmp/dir1/file" to "/tmp/dir1/dir2/file", which, given only the=20
;; setup above, will fail.
(rename-file "dir1/symlink" "dir2/")
The proposed new routine is below. Programs that depend on our old
RENAME-FILE can be modified to acheive equivalent effects: given the new
definition, our old RENAME-FILE is effectively the same as
(rename-file (truename old-name)
(merge-pathnames new-name (truename old-name)))
If nobody sees a problem with the change, I'll add it and some tests.
Happy New Year,
(defun rename-file (file new-name)
"Rename FILE to have the specified NEW-NAME. If FILE is a stream open to a
file, then the associated file is renamed."
(declare (type (or string pathname) new-name))
;; When the first argument is an open stream, RENAME-FILE is
;; specified to work on the file actually open, which our TRUENAME
;; does not currently return (it returns the truename of the
;; pathname used to open the file), but this will do the right thing
;; once our streams are correct on this detail. (We leave it to the
;; file system to decide whether we can rename a file while it's
;; open, however.)
(let* ((old-pathname (if (and (streamp file) (open-stream-p file))
(old-truename (truename file))
(new-pathname (pathname new-name))
(merged-pathname (merge-pathnames new-pathname old-pathname))
(or (pathname-host new-pathname)
(or (pathname-device new-pathname)
(or (pathname-directory new-pathname)
(or (pathname-name new-pathname)
(or (pathname-type new-pathname)
(or (pathname-version new-pathname)
(multiple-value-bind (res errno)
(sb!unix:unix-rename old-filename new-filename)
;; There are up to 6 different pathnames involved in this
;; routine, and it's entirely arbitrary which one we say is
;; "the" offending pathname, and which ones we include in the
;; error message.
(format nil "couldn't rename ~2I~_~A ~I~_to ~2I~_~A ~
by renaming ~2I~_~A to ~2I~_~A"
file new-name old-filename new-filename)
;; FIXME: if FILE is an open stream, change its truename to
;; reflect this renaming.
(values return-name old-truename (truename return-name)))))