hasseg.org

Trash files from the OS X command line

Filed under Featured, Mac, Programming

trash picture 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:

# relevant code snippet:
...
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:

# relevant code snippets:
...
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):

// relevant code snippets:
...
// 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.

12 Comments

has March 9, 2010 at 9:36 PM

You can do this in AppleScript; just save the following as plain text and make it executable:

#!/usr/bin/osascript

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.

Ali Rantakari March 10, 2010 at 4:40 AM

Hi has, and thanks for your helpful suggestions!

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:

tell application "Finder" to delete every item of {(POSIX file "/path/one"), (POSIX file "/path/two")}

The Ruby and Python examples ASTranslate gave me seemed fine, but the Objective-C translation didn’t:

#import "FNGlue/FNGlue.h"
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.

has March 10, 2010 at 10:15 PM

Bug filed on ASTranslate. It does that sometimes. Try [[finder delete: nsArrayOfNSURLs] send].

executeAppleEvent:error: to send Apple events to other apps

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.

Thomas Rose January 18, 2012 at 10:55 PM

trash -l file per the usage seems like it should delete file and list, but it only “lists”

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.

Ali Rantakari January 22, 2012 at 7:58 PM

Hi Thomas,

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.

Tom December 30, 2012 at 3:09 PM

Are there any known issues with using trash over ssh? When I remotely connect to my Mac mini and try to trash any file I always get

trash: error -600

Doing the same locally works just fine. I installed version 0.8.2 via homebrew on 10.8.2.

Ali Rantakari December 30, 2012 at 5:08 PM

Hi Tom,

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:

make USE_SYSTEM_API=1

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).

Tom December 30, 2012 at 6:23 PM

Cool! Re-compiling with USE_SYSTEM_API=1 did the trick. I won’t be needing the “put back” feature and file permissions shouldn’t be an issue in my situation so I can live with the API limitations.

Thanks for this wonderful little tool!

Thai November 14, 2014 at 2:43 AM

I’m running without the Finder open at all (using Path Finder, which quits the Finder). I tried the make “USE_SYSTEM_API=1” option, but it’s still not working. Is there a way around this?

Jason December 27, 2014 at 12:16 AM

Awesome - thanks for posting this trash on the internet. ;-)

After I upgraded OS X and found that the osx-trash gem had stopped working, I went looking for alternatives and found yours. It’s been working just dandy so far.

I didn’t want to install Homebrew - I use MacPorts, and like managing /usr/local myself - so to install I just did the following:

  1. Installed the Xcode command-line tools:

    xcode-select --install

  2. Downloaded a zip from your GitHub repo, extracted, opened that folder in Terminal, and built:

    make
    make trash.1

  3. Copied the files to their installed locations:

    sudo cp trash /usr/local/bin
    sudo cp trash.1 /usr/local/share/man/man1

Scott January 13, 2015 at 4:38 PM

In regards to putting the files in the correct trash if you have multiple volumes in place, can’t you use the $HOME environment variable, and it will figure it out.

We know our username, in an example, say it is user-x, to put something in the trash you it would go into /Users/user-x/.Trash

But, since $echo $USER returns user-x, you have a built in scenario where the OS knows which trash to use based on the user account you are logged into.

ENV of $HOME should always know the right thing to do unless you have messed with your environment in some very strange ways. Changing your $HOME and $USER values will significantly mess things up.

The Trash is always invisible via a dot file, and always after your username, so /Users/username/.Trash

For remote Volumes at /Volumes, excluding the boot partition, the format for the Trash is /Volumes/The\ Volume\ Name/.Trashes

Would that not work to rely on?

Ryan Schmidt May 23, 2015 at 1:01 AM

trash is now available in MacPorts via the usual “sudo port install trash”

Categories