Trash files from the OS X command line
Posted on March 9, 2010
Filed under Featured, Mac, Programming
I spend a lot of time in the Terminal on my computer — a lot of things are just better done with a command-line interface than in the GUI. When removing files via the command-line people usually just, well, remove them (with the rm command), but this means that they’ll be eschewing the Trash, one of the user-friendliest things (even relatively) modern operating systems have had to offer for a long time.
It’s obvious we need a trash command that we can use instead of rm when we’re not 101% sure we need to actually remove these files, like, right now. Here are a couple of existing solutions I’ve found, my impressions of them, and then my version at the bottom:
osxutils’ trash by Sveinbjörn Þórðarson
This is what I’ve used up until now. It’s a small Perl script that simply manually moves the specified files into the trash folder under the current user’s home directory:
...
while (-e "$ENV{HOME}/.Trash/$path_segs[$#path_segs]")
{
$path_segs[$#path_segs] .= " copy $cnt";
}
system("mv '$ARGV[$argnum]' '$ENV{HOME}/.Trash/$path_segs[$#path_segs]'");
...
The biggest problem with this is that it moves all trashed files onto the same volume instead of using each volume’s own trash folders (this is not a good idea). It also deals with filename collisions manually, which is a bit volatile. The good thing about this is that it doesn’t follow “leaf” symbolic links, which is in my opinion the expected behaviour (what if I want to trash the link instead of its target?).
osx-trash by Dave Dribin
This one is a lot better. It’s a Ruby script that uses the Scripting Bridge to ask Finder to perform all the actual “heavy lifting” (e.g. moving the files to the trash) which means that each volume’s own trash folders are utilized properly and filename collisions are handled in a standard way:
...
finder = SBApplication.applicationWithBundleIdentifier("com.apple.Finder")
...
path = Pathname.new(file)
url = NSURL.fileURLWithPath(path.realpath.to_s)
item = finder.items.objectAtLocation(url)
item.delete
...
It also allows you to list all the files that are currently in the trash and empty it (normally or securely). The few minor negative things are the fact that it follows leaf symbolic links and the fact that when you try to trash multiple files you don’t have access rights for, Finder will pop up an authentication dialog separately for each file.
my trash
Due to the few things that I wanted the trash command to do differently, I wrote my own. In order to make sure that filename collisions are handled in the standard manner and that each volume’s trash folders are properly used (just like with Dave Dribin’s script) this one first tries to use the standard system API for trashing files, and if that fails due to the user not having the correct access rights, then asks Finder to trash those files (Finder can authenticate the current user as an administrator and move the files into that user’s trash — if you simply run this program as root, the files will be moved to root’s trash, which is not what we want):
...
// first try the standard API function (this should be
// the fastest way, and we get a nice status value
// as well):
//
FSRef fsRef;
FSPathMakeRefWithOptions(
(const UInt8 *)[filePath fileSystemRepresentation],
kFSPathMakeRefDoNotFollowLeafSymlink,
&fsRef,
NULL // Boolean *isDirectory
);
OSStatus ret = FSMoveObjectToTrashSync(&fsRef, NULL, kFSFileOperationDefaultOptions);
...
// if no access rights, construct Apple event describing
// a "delete all items in this list" action and send it to Finder:
//
NSAppleEventDescriptor *descr = [NSAppleEventDescriptor
descriptorWithDescriptorType:'furl'
data:[[url absoluteString] dataUsingEncoding:NSUTF8StringEncoding]];
[urlListDescr insertDescriptor:descr atIndex:i++];
...
ProcessSerialNumber finderPSN = getFinderPSN();
NSAppleEventDescriptor *targetDesc = [NSAppleEventDescriptor
descriptorWithDescriptorType:'psn '
bytes:&finderPSN
length:sizeof(finderPSN)];
NSAppleEventDescriptor *descriptor = [NSAppleEventDescriptor
appleEventWithEventClass:'core'
eventID:'delo'
targetDescriptor:targetDesc
returnID:kAutoGenerateReturnID
transactionID:kAnyTransactionID];
[descriptor setDescriptor:urlListDescr forKeyword:'----'];
...
AESendMessage([descriptor aeDesc], &reply, kAEWaitReply, kAEDefaultTimeout);
...
The difference is that this one doesn’t follow leaf symlinks, and when deleting multiple files that you don’t have access rights for, Finder will only show one authentication dialog (I had to implement this kind of “manually” because I couldn’t find any way to accomplish it with the Scripting Bridge). I also copied the idea and implementation of emptying the trash and listing its contents via the Scripting Bridge from Dave’s script. This trash is written in (Objective-)C, for all those extra milliseconds that are so goddamn important when waiting for files to be trashed.
You can go through my trash here.
Comments
9 Responses to “Trash files from the OS X command line”
on run file_paths
repeat with path_ref in file_paths
set f to POSIX file path_ref
tell application "Finder" to delete f
end repeat
return
end run
Re. the code generation approach, remember to sanitize your inputs if you don’t want nasty surprises. Or, pack your args into an NSAppleEventDescriptor and pass to -executeAppleEvent:error: (though in this case it’d be as easy just to build a core/delo event and send it directly to Finder). Or, if you’re using appscript (which is more capable than SB), run the AppleScript command through ASTranslate to get its Python/Ruby/ObjC equivalent.
This can definitely be accomplished just with AppleScript as well, although you’d need a bit more than the script you provided (it doesn’t resolve paths relative to the current working directory and it authenticates the user once for each file the user lacks access rights for).
Good that you mentioned the sanitization because I did indeed forget to escape double quotes in file paths :). I applied a quick fix for that but I also started to look into constructing the NSAppleEventDescriptor manually and sending it as an Apple Event to Finder, which I thought would be the best way to go around the problems that the dynamic code generation brings. Apparently you can’t use executeAppleEvent:error: to send Apple events to other apps but I found some examples of how to get it done with AESendMessage(). Version 0.7.0 now has this fix.
I looked at appscript as well (I’ve had it in my bookmarks for a long time but I’ve forgotten about it), and tried to translate this with ASTranslate:
The Ruby and Python examples ASTranslate gave me seemed fine, but the Objective-C translation didn’t:
FNApplication *finder = [FNApplication applicationWithName: @"Finder"];
id result = [[finder delete] send];
Apparently, though, you can just create an NSArray of NSURLs and pass it as an argument to FNApplication’s -delete:. For this app, though, I decided to leave appscript out because it seemed a bit overkill to add a whole library’s worth of dependencies just to strip ~20 lines of code down to ~2 (some might disagree, but this is what I decided). Anyway, your comments were very helpful.
Correct, -executeAppleEvent:error: is for invoking handlers in a loaded AppleScript, which is only worth the effort if it’s a user-supplied script (i.e. you’re making your application attachable). Using ASSendMessage to dispatch a core/delo event directly to Finder is the easiest way to go here. One of these days I’ll need to look into building appscript as a static library; frameworks are pretty hopeless when it comes to developing CLI tools.
[...] ???????????????????????, ????????? ?????????????????????????????????????????????????????????????trash???????? ???http://hasseg.org/blog/post/406/trash-files-from-the-os-x-command-line/ [...]
trash -u file per the usage seems like it should delete file and check for update, but it only updates.
You’d maybe think that “trash -l” would give you a size of the entire data in trash, but it does not. That is, it does not account in the size calculation that data sitting with folders in the trash.
Great little command. Keep up the good work.
Thanks for the feedback; I’ll try to make the usage output more clear in the next version, and the lack of recursive file size calculation is definitely an issue I need to address.
Doing the same locally works just fine. I installed version 0.8.2 via homebrew on 10.8.2.
The problem is probably that `trash` is trying to send apple events to Finder in order to trash the files. This is probably failing because your session is “headless”. You can try compiling trash with the command:
This will compile a build that uses the C API for trashing files instead of calling Finder. Note that this only works for files that you have the access rights to, and that the “put back” feature will not work because the metadata for the “original location” will not be added to the file when it is moved into the trash folder (Finder does this, so that’s why the default build always asks Finder to perform the trashing).
Thanks for this wonderful little tool!