diff --git a/doc/lispref/files.texi b/doc/lispref/files.texi index 2033177fbb..8096c9e861 100644 --- a/doc/lispref/files.texi +++ b/doc/lispref/files.texi @@ -2129,6 +2129,24 @@ the period that delimits the extension, and if @var{filename} has no extension, the value is @code{""}. @end defun +@defun file-name-with-extension filename extension +This function returns @var{filename} with its extension set to +@var{extension}. A single leading dot in the @var{extension} will be +stripped if there is one. For exmaple, + +@example +(file-name-with-extension "file" "el") + @result{} "file.el" +(file-name-with-extension "file" ".el") + @result{} "file.el" +(file-name-with-extension "file.c" "el") + @result{} "file.el" +@end example + +Note that this function will error if the @var{filename} or +@var{extension} are empty, or if the @var{filename} is shaped like a +directory (i.e. if @code{directory-name-p} returns @code{t}). + @defun file-name-sans-extension filename This function returns @var{filename} minus its extension, if any. The version/backup part, if present, is only removed if the file has an diff --git a/etc/NEWS b/etc/NEWS index 60226f0a3e..9838693a65 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -2964,6 +2964,11 @@ been added, and takes a callback to handle the return status. --- ** 'ascii' is now a coding system alias for 'us-ascii'. ++++ +** New function 'file-name-with-extension'. +This function allows a canonical way to set/replace the extension of a +filename string. + +++ ** New function 'file-backup-file-names'. This function returns the list of file names of all the backup files diff --git a/lisp/files.el b/lisp/files.el index 2450daf5bf..a5ac1821b2 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -4892,6 +4892,22 @@ extension, the value is \"\"." (if period ""))))) +(defun file-name-with-extension (filename extension) + "Set the EXTENSION of a FILENAME. + +Trims a leading dot from the EXTENSION so that either `foo' or +`.foo' can be given. + +Errors if the filename or extension are empty, or if the given +filename has the format of a directory. + +See also `file-name-sans-extension'." + (let ((extn (string-trim-left extension "[.]"))) + (cond ((string-empty-p filename) (error "Empty filename: %s" filename)) + ((string-empty-p extn) (error "Malformed extension: %s" extension)) + ((directory-name-p filename) (error "Filename is a directory: %s" filename)) + (t (concat (file-name-sans-extension filename) "." extn))))) + (defun file-name-base (&optional filename) "Return the base name of the FILENAME: no directory, no extension." (declare (advertised-calling-convention (filename) "27.1")) diff --git a/test/lisp/files-tests.el b/test/lisp/files-tests.el index dc96dff639..257cbc2d32 100644 --- a/test/lisp/files-tests.el +++ b/test/lisp/files-tests.el @@ -1478,5 +1478,23 @@ The door of all subtleties! (buffer-substring (point-min) (point-max)) nil nil))))) +(ert-deftest files-tests-file-name-with-extension-good () + "Test that `file-name-with-extension' succeeds with reasonable input." + (should (string= (file-name-with-extension "Jack" "css") "Jack.css")) + (should (string= (file-name-with-extension "Jack" ".css") "Jack.css")) + (should (string= (file-name-with-extension "Jack.scss" "css") "Jack.css")) + (should (string= (file-name-with-extension "/path/to/Jack.md" "org") "/path/to/Jack.org"))) + +(ert-deftest files-tests-file-name-with-extension-bad () + "Test that `file-name-with-extension' fails on malformed input." + (should-error (file-name-with-extension nil nil)) + (should-error (file-name-with-extension "Jack" nil)) + (should-error (file-name-with-extension nil "css")) + (should-error (file-name-with-extension "" "")) + (should-error (file-name-with-extension "" "css")) + (should-error (file-name-with-extension "Jack" "")) + (should-error (file-name-with-extension "Jack" ".")) + (should-error (file-name-with-extension "/is/a/directory/" "css"))) + (provide 'files-tests) ;;; files-tests.el ends here