Add "Keep both" option when writing a file that will overwrite an existing file (as in Finder)?

Many of my scripts create an output file, and the scripts include a test whether the output file exists, so that the user can choose whether or not to overwrite the existing version.

Is there a way to add a “Keep both” option to the dialog that asks whether to overwrite the existing file, an option that would work the same way the Finder works when you copy a file over an existing file? The Finder knows how to add a number to the name of the additional copy of the file, but I haven’t found AppleScript that can do the same thing.

The only thing I can think of is a shell script that would run something like ls filename*.ext and then use grep to test for a space followed by one or more numbers, followed by .ext in the output (to find something like filename 2.ext, and then increment the highest number and add that number to the new filename. This seems over-elaborate, and I wonder if the Finder or System Events offers some better way.

If a method exists, I’ll be grateful for information about it.

Here is a sample script that will add a number to the file name, or will increment if it end with a number

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

set myFile to choose file name "Choose a file name to save as…" default location (alias "Mac SSD:Users:robert:Desktop:") default name "test.txt" --"My.testing11.txt"
set myFile to myFile as text
try
	myFile as alias
	set doesExist to true
on error errMsg -- does not exist
	set doesExist to false
	display alert "Does not exist"
end try
if doesExist then
	set dResult to display dialog "Do you wish to keep the old file?" buttons {"No", "Keep"} default button "Keep"
	if button returned of dResult is "Keep" then
		set text item delimiters to ":"
		set fname to last text item of myFile
		set text item delimiters to "."
		set fname to text items of fname
		if (count fname) > 1 then
			set fext to last text item of fname
			set fname to items 1 thru -2 of fname
		else
			set fext to ""
		end if
		set fname to fname as text
		set i to length of fname
		repeat while i > 0
			if text i of fname is not in "1234567890" then exit repeat
			set i to i - 1
		end repeat
		if i < length of fname then -- already has number
			set fname to (text 1 thru i of fname) & (((text (i + 1) thru -1 of fname) as integer) + 1)
		else
			set fname to fname & "1"
		end if
		if fext ≠ "" then set fname to {fname, fext} as text
		set text item delimiters to ":"
		set myFile to ((text items 1 thru -2 of myFile) & fname) as text
	end if
end if

** EDIT ** - I added a second dialog

This seemed like a useful function to me, so I took a “fileExtension” AppleScript handler (function) I already had, and tweaked it to do the behavior you describe. Hope this helps. Note that it works with either a simple file name, or an entire path. Note also that, if trying to increment an existing number-at-end-of-filename, it really does look for a simple number like 3 or 46. If any other characters are included, that isn’t a thing to increment, and it just puts " 2" after that, followed by original extension.

-- sample usage:
fileNameKeepBoth("folder:subfolder:filename 23ad45.ext")

on fileNameKeepBoth(someFileNameOrPath)
	-- version 1.0, Daniel A. Shockley
	
	set newNum to 0 -- INITIALIZE
	
	-- get the file extension, if any:
	set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {""}}
	(reverse of characters of someFileNameOrPath) as string
	
	text 1 thru ((offset of "." in result) - 1) of result
	
	set fileExt to (reverse of characters of result) as string
	set AppleScript's text item delimiters to od
	set fileExtLength to length of fileExt
	if fileExtLength is less than length of someFileNameOrPath then
		set posPreExt to -1 * (2 + fileExtLength)
		set nonExtPath to text 1 thru posPreExt of someFileNameOrPath
	else
		-- no extension, so the whole path is "before" it:
		set fileExtLength to 0
		set nonExtPath to someFileNameOrPath
	end if
	
	-- Get the last text before extension after a space, if any:
	-- Note: do NOT try to use the "word" function, since that uses more than a space as word-sep, 
	--   which would lead, e.g. to the the day part of an ISO8601 date being the final word, 
	--   so incrementing that would change something unintentionally. 
	set lastWordBySpace to last word of nonExtPath
	
	-- now, check whether the last word is a simple number:
	set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {""}}
	(reverse of characters of nonExtPath) as string
	text 1 thru ((offset of " " in result) - 1) of result
	set lastWordBySpace to (reverse of characters of result) as string
	set AppleScript's text item delimiters to od
	set lastWordLength to length of lastWordBySpace
	if lastWordLength is greater than 0 then
		try
			set lastWordNum to lastWordBySpace as number
			if lastWordNum as text is equal to lastWordBySpace then
				set newNum to lastWordNum + 1
			end if
		end try
	end if
	
	if newNum is greater than 0 then
		-- now, if we found a simple nubmer as the last word, use it:
		set posPreWord to -1 * (2 + lastWordLength)
		set nonLastWordPath to text 1 thru posPreWord of nonExtPath
		set nonExtPath to nonLastWordPath
	else
		-- no previous last-word-is-simple-number-to-increment, so just use 2:
		set newNum to 2
	end if
	
	set newPath to nonExtPath & " " & (newNum as text)
	if fileExtLength is greater than 0 then
		set newPath to newPath & "." & fileExt
	end if
	
	return newPath
		
end fileNameKeepBoth

@robertfern and @Krioni - These are both superb solutions, far beyond anything I could have devised. Thank you both!

Some thoughts:

@robertfern - Two small details: when the Finder lets you “keep both”, it adds a space and then the number 2 for the first new copy (not “1” for the first new copy). This was easy to change in your script.

I’ve only tried the two scripts very quickly, and, unless I’m mistaken, I think there’s one feature that they don’t have but which would be very useful - I mean the ability to output a file with a higher number in its name when existing files already have numbers. To clarify:

My script creates a PDF file from a PostScript or EPS file, using the built-in pstopdf executable. For example, if the user drops a file named /Ventura/Users/somename/sample.ps then the script will output /Ventura/Users/somename/sample.pdf. But before I run pstopdf, I test whether sample.pdf already exists and ask the user whether to overwrite it.

With a “keep both” option, the user ought to be able to choose “keep both” every time they run the script, so that the output folder will have sample.pdf, sample 2.pdf, sample 3.pdf, and maybe sample 6.pdf if the user has created and then deleted a few of these files.

Is there a way for these scripts to do what the Finder does, and automatically create an output file named sample 7.pdf if sample 6.pdf exists?

I know I’m asking a lot, but I hope this function would be genuinely useful to many scripters.

Doesn’t mine already do that?

Oh crap, you’re right. It doesn’t find duplicates
I’ll try to fix

Try this

use AppleScript version "2.4" -- Yosemite (10.10) or later
use scripting additions

set myFile to choose file name "Choose a file name to save as…" default location (alias "Mac SSD:Users:robert:Desktop:") default name "My.testing.txt" --"My.testing11.txt"
set myFile to myFile as text
try
	myFile as alias
	set doesExist to true
on error errMsg -- does not exist
	set doesExist to false
end try
if doesExist then
	set dResult to display dialog "Do you wish to keep the old file?" buttons {"No", "Keep"} default button "Keep"
	if button returned of dResult is "Keep" then
		set text item delimiters to ":"
		set fname to last text item of myFile
		set myPath to text items 1 thru -2 of myFile
		set text item delimiters to "."
		set fname to text items of fname
		if (count fname) > 1 then
			set fext to last text item of fname
			set fname to items 1 thru -2 of fname
		else
			set fext to ""
		end if
		set fname to fname as text
		set i to length of fname
		repeat while i > 0
			if text i of fname is not in "1234567890" then exit repeat
			set i to i - 1
		end repeat
		set text item delimiters to ":"
		if i < length of fname then -- already has number
			set fname to (text 1 thru i of fname) --& (((text (i + 1) thru -1 of fname) as integer) + 1)
			set c to ((text (i + 1) thru -1 of fname) as integer) + 1
		else
			set c to 0
		end if
		repeat
			set c to c + 1
			set newName to fname & " " & c
			if fext ≠ "" then
				set newName to newName & "." & fext
			else
				set newName to newName
			end if
			try
				(myPath & newName as text) as alias
			on error
				exit repeat
			end try
		end repeat
		set myFile to (myPath & newName) as text
	end if
end if
-- file path is in myfile

I’ve included my suggestion below. As regards its operation:

  • If the destination file does not exist, the handler returns the destination file path with no change.
  • If the destination file does exist and the user selects “Overwrite”, the handler returns the destination file path with no change.
  • If the destination file does exist and the user selects “Keep Both”, the handler returns the destination file path with a counter appended.
  • The returned files are file objects, although this can be changed to whatever is desired.
use framework "Foundation"
use scripting additions

set destinationFile to "Macintosh HD:Users:Robert:Desktop:Test.txt"
set destinationFile to getDestinationFile(destinationFile)
-- insert code here to save or copy or move source file to destination

on getDestinationFile(theFile)
	set thePosixFile to current application's NSString's stringWithString:(POSIX path of theFile)
	set fileManager to current application's NSFileManager's defaultManager()
	set fileExists to (fileManager's fileExistsAtPath:thePosixFile isDirectory:false)
	if fileExists is false then return theFile as «class furl»
	set buttonReturned to button returned of (display dialog "The source file exists at the destination" buttons {"Cancel", "Overwrite", "Keep Both"} cancel button 1 default button 3)
	if buttonReturned is "Overwrite" then return theFile as «class furl»
	set fileNoExtension to (thePosixFile's stringByDeletingPathExtension)
	set fileExtension to thePosixFile's pathExtension()
	repeat with i from 1 to 100
		set newFile to ((fileNoExtension's stringByAppendingString:(" " & i))'s stringByAppendingPathExtension:fileExtension)
		set fileExists to (fileManager's fileExistsAtPath:newFile isDirectory:false)
		if fileExists is false then return (POSIX file (newFile as text))
	end repeat
end getDestinationFile
1 Like

@peavine and @robertfern - Both of these work beautifully! I know that I’m not the only person who will be extremely grateful for them.

@peavine - This is a really elegant solution that teaches me an enormous amount about AppleScript that I should have known already, but didn’t.

Has the Finder changed the way it handles copying a file over an existing file? If I have a file named foobar.pdf on my desktop, and I Option-drag a file with the same name on to the desktop, the Finder asks me if I want to keep both - and if I click keep both, the new copy is named foobar 2.pdf (not foobar 1.pdf and not a name with copy in it.

If I duplicate a file (Cmd-D), then the copy does get the name “copy” in it, but that’s because it’s obviously a copy of the other file. When I option-drag a file from another folder where a file with the same name doesn’t exist, the Finder doesn’t know whether the file that I’m dragging is an exact copy of the file in the destination, so it adds 2 to the name of the dragged file, and then higher numbers.

Does this correspond to other people’s experience?

Meanwhile, a thousand thanks for those superb scripts.

emendelson. I just checked and it does work exactly as you describe. I wonder what the logic is in the differing approaches.

@peavine - My guess is that the Finder adds “copy” to the filename when it knows the newly-created file is an exact copy of the original. Apparently it doesn’t perform any tests when copying from one folder to another, because two files with different names could have entirely different contents. But that’s just a guess.

1 Like

I like the approach @robertfern used, although I thought I might as well update mine. One difference that might be useful in some cases is that the handler I wrote just assumes you want the “look for the next available number suffix” behavior, rather than asking. That could be useful when you don’t want interaction. It is also recursive, and has no limit on how big that suffix-number could get. It takes an AppleScript file path, and returns either that path, if nothing is there yet, or the “next available” path with a numeric-word suffix before the extension.

-- fileNameKeepBoth

(*
	Handler to return the specified file path, if nothing exists there yet, otherwise the "next" file with a numeric word appended/incremented. 

HISTORY: 
	2023-04-02 ( danshockley ): First created. 
*)

-- sample usage:
filePathSave_OrNextByNumSuffix("Macintosh HD:Users:YOUR_USER_NAME:Desktop:testfile.txt")


on filePathSave_OrNextByNumSuffix(someFilePath)
	-- version 1.0, Daniel A. Shockley
	
	-- if it does NOT exist already, just use this path:
	if not testPathExists(someFilePath) then return someFilePath
	
	-- otherwise, continue by appending/incrementing a numeric word at end of file 
	-- (before extension, if any), until we have an available file path:
	
	set newNum to 0 -- INITIALIZE
	
	-- get the file extension, if any:
	set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {""}}
	(reverse of characters of someFilePath) as string
	
	text 1 thru ((offset of "." in result) - 1) of result
	
	set fileExt to (reverse of characters of result) as string
	set AppleScript's text item delimiters to od
	set fileExtLength to length of fileExt
	if fileExtLength is less than length of someFilePath then
		set posPreExt to -1 * (2 + fileExtLength)
		set nonExtPath to text 1 thru posPreExt of someFilePath
	else
		-- no extension, so the whole path is "before" it:
		set fileExtLength to 0
		set nonExtPath to someFilePath
	end if
	
	-- Get the last text before extension after a space, if any:
	-- Note: do NOT try to use the "word" function, since that uses more than a space as word-sep, 
	--   which would lead, e.g. to the the day part of an ISO8601 date being the final word, 
	--   so incrementing that would change something unintentionally. 
	set lastWordBySpace to last word of nonExtPath
	
	-- now, check whether the last word is a simple number:
	set {od, AppleScript's text item delimiters} to {AppleScript's text item delimiters, {""}}
	(reverse of characters of nonExtPath) as string
	text 1 thru ((offset of " " in result) - 1) of result
	set lastWordBySpace to (reverse of characters of result) as string
	set AppleScript's text item delimiters to od
	set lastWordLength to length of lastWordBySpace
	if lastWordLength is greater than 0 then
		try
			set lastWordNum to lastWordBySpace as number
			if lastWordNum as text is equal to lastWordBySpace then
				set newNum to lastWordNum + 1
			end if
		end try
	end if
	
	
	if newNum is greater than 0 then
		-- now, if we found a simple nubmer as the last word, use it:
		set posPreWord to -1 * (2 + lastWordLength)
		set nonLastWordPath to text 1 thru posPreWord of nonExtPath
		set nonExtPath to nonLastWordPath
	else
		-- no previous last-word-is-simple-number-to-increment, so just use 2:
		set newNum to 2
	end if
	
	set newPath to nonExtPath & " " & (newNum as text)
	if fileExtLength is greater than 0 then
		set newPath to newPath & "." & fileExt
	end if
	
	-- now that we have picked the next likely candidate, see if we can use it:
	if testPathExists(newPath) then
		-- it also exists, so try next step:
		return filePathSave_OrNextByNumSuffix(newPath)
	else
		-- it is available, so return that:
		return newPath
	end if
	
end filePathSave_OrNextByNumSuffix




on testPathExists(inputPath)
	-- version 1.5
	-- from Richard Morton, on applescript-users@lists.apple.com
	-- public domain, of course. :-)
	-- gets somewhat slower as nested-depth level goes over 10 nested folders
	if inputPath is not equal to "" then try
		get alias (inputPath as string) -- just in case inputPath was not string
		return true
	end try
	return false
end testPathExists

1 Like

My previous script includes the file-exists dialog in the handler, which might not always be a good idea, and I’ve changed that below. Also, my new script returns an HFS path rather than a file object and starts the counter numbering at 2, which mimics the Finder behavior.

With five existing files in the destination folder (i.e. the counter of the new destination file is 6), the script takes one millisecond to run. This assumes that the Foundation framework is in memory.

use framework "Foundation"
use scripting additions

set destinationFile to (path to desktop as text) & "Test.txt"
set newDestinationFile to getNewDestinationFile(destinationFile)
-- if destinationFile is not newDestinationFile then prompt or use newDestinationFile as destination

on getNewDestinationFile(theFile)
	set thePosixFile to current application's NSString's stringWithString:(POSIX path of theFile)
	set fileManager to current application's NSFileManager's defaultManager()
	set fileExists to (fileManager's fileExistsAtPath:thePosixFile isDirectory:false)
	if fileExists is false then return theFile
	set fileNoExtension to (thePosixFile's stringByDeletingPathExtension)
	set fileExtension to thePosixFile's pathExtension()
	repeat with i from 2 to 100 -- increase 100 if desired
		set newFile to ((fileNoExtension's stringByAppendingString:(" " & i))'s stringByAppendingPathExtension:fileExtension)
		set fileExists to (fileManager's fileExistsAtPath:newFile isDirectory:false)
		if fileExists is false then return (current application's |NSURL|'s fileURLWithPath:newFile) as text
	end repeat
end getNewDestinationFile

This is even better - thank you again. By keeping the file-exists dialog out of the handler, it makes it easier for the main part of the script to sort out which file to write.

A thousand thanks again.