« Wrapping UNIX Commands

Posted by Andy Monitzer on April 29, 2001 [Feedback (6) & TrackBack (0)]

// First, a Warning

This tutorial is in the "Regular Topics" section and not in "Bare Basics." This is for a reason: if you have problems with this tutorial, it's very likely that you're missing some basic knowledge and should stop your current project and actually learn some Cocoa!

 

// Wrap it in a GUI

MacFixIt.com has mentioned more than a few times lately that having a GUI shell for the many UNIX commands people find themselves typing (such as "chmod" and "update_prebinding" and so on) would be a great opportunity for shareware developers. We agree, and this little tutorial should get you started in that area: creating wrappers for UNIX commands.

This will be a very common task on Mac OS X. Some UNIX crack will program some nifty tool that does something very well, but must be run from within the command line. Some people will whine that they don't like it and that they want to click some throbbing buttons, and then somebody will be so brave to create a nice wrapper for it (that's you!).

This tutorial will be one of those 'learning by doing' ones.

 

// About That Command We'll Wrap

Lets wrap "wget", a very usuable little utility. Think of it as a quasi-download manager. Well, kind of...

"wget --help" reveals all sorts of parameters:

GNU Wget 1.5.3, a non-interactive network retriever.
Usage: wget [OPTION]... [URL]...

Mandatory arguments to long options are mandatory for short options too.

Startup:
  -V,  --version           display the version of Wget and exit.
  -h,  --help              print this help.
  -b,  --background        go to background after startup.
  -e,  --execute=COMMAND   execute a `.wgetrc' command.

Logging and input file:
  -o,  --output-file=FILE     log messages to FILE.
  -a,  --append-output=FILE   append messages to FILE.
  -d,  --debug                print debug output.
  -q,  --quiet                quiet (no output).
  -v,  --verbose              be verbose (this is the default).
  -nv, --non-verbose          turn off verboseness, without being quiet.
  -i,  --input-file=FILE      read URL-s from file.
  -F,  --force-html           treat input file as HTML.

Download:
  -t,  --tries=NUMBER           set number of retries to NUMBER (0 unlimits).
  -O   --output-document=FILE   write documents to FILE.
  -nc, --no-clobber             don't clobber existing files.
  -c,  --continue               restart getting an existing file.
       --dot-style=STYLE        set retrieval display style.
  -N,  --timestamping           don't retrieve files if older than local.
  -S,  --server-response        print server response.
       --spider                 don't download anything.
  -T,  --timeout=SECONDS        set the read timeout to SECONDS.
  -w,  --wait=SECONDS           wait SECONDS between retrievals.
  -Y,  --proxy=on/off           turn proxy on or off.
  -Q,  --quota=NUMBER           set retrieval quota to NUMBER.

Directories:
  -nd  --no-directories            don't create directories.
  -x,  --force-directories         force creation of directories.
  -nH, --no-host-directories       don't create host directories.
  -P,  --directory-prefix=PREFIX   save files to PREFIX/...
       --cut-dirs=NUMBER           ignore NUMBER remote directory components.

HTTP options:
       --http-user=USER      set http user to USER.
       --http-passwd=PASS    set http password to PASS.
  -C,  --cache=on/off        (dis)allow server-cached data (normally allowed).
       --ignore-length       ignore `Content-Length' header field.
       --header=STRING       insert STRING among the headers.
       --proxy-user=USER     set USER as proxy username.
       --proxy-passwd=PASS   set PASS as proxy password.
  -s,  --save-headers        save the HTTP headers to file.
  -U,  --user-agent=AGENT    identify as AGENT instead of Wget/VERSION.

FTP options:
       --retr-symlinks   retrieve FTP symbolic links.
  -g,  --glob=on/off     turn file name globbing on or off.
       --passive-ftp     use the "passive" transfer mode.

Recursive retrieval:
  -r,  --recursive             recursive web-suck -- use with care!.
  -l,  --level=NUMBER          maximum recursion depth (0 to unlimit).
       --delete-after          delete downloaded files.
  -k,  --convert-links         convert non-relative links to relative.
  -m,  --mirror                turn on options suitable for mirroring.
  -nr, --dont-remove-listing   don't remove `.listing' files.

Recursive accept/reject:
  -A,  --accept=LIST                list of accepted extensions.
  -R,  --reject=LIST                list of rejected extensions.
  -D,  --domains=LIST               list of accepted domains.
       --exclude-domains=LIST       comma-separated list of rejected domains.
  -L,  --relative                   follow relative links only.
       --follow-ftp                 follow FTP links from HTML documents.
  -H,  --span-hosts                 go to foreign hosts when recursive.
  -I,  --include-directories=LIST   list of allowed directories.
  -X,  --exclude-directories=LIST   list of excluded directories.
  -nh, --no-host-lookup             don't DNS-lookup hosts.
  -np, --no-parent                  don't ascend to the parent directory.

Mail bug reports and suggestions to <bug-wget@gnu.org>.

We could implement all of those switches, but that'd require a resolution of 1600x1400 and our application would take over the whole screen. In other words: it's unadvisable and impractical.

From experience, the only switch that's really useful here is '-r' for 'recursive', which we can turn off and on using a simple checkbox. Additionally, we might need a NSTextField so that the user can specify the URL for the file they'd like to download.

 

// Let's Wrap!

Create a new Cocoa Application Project in PB (I'll call it wgetWrapper). Then create the interface in IB:

Software Update
Our main window (those two NSTextFields are in a matrix). Don't forget to name that window!

Now create a class called "wgetWrapper" with the outlets "window", "url", "downloadFolder", "recursive", "downloadButton" and the action "download". Instantiate this class and connect the outlets and the action accordingly. You might want to make the button respond to "return" (Equiv=\r). Make the class the application's delegate. Create the files (and insert them into your project). Y'know, the normal stuff...

This is a single-window application, so we'll let the system know that in our implementation that closing the window should quit the application:

- (BOOL)applicationShouldTerminateAfterLastWindowClosed:
	(NSApplication *)theApplication {
    return YES;
}

Our window should be in the front when the application launches:

- (void)awakeFromNib {
    [window makeKeyAndOrderFront:nil];
}

Next thing is to actually do something when our button is pressed:

- (IBAction)download:(id)sender {

The first thing is to tell the user that we're downloading the file (and preventing him from pressing the button again).

    [downloadButton setTitle:@"Running..."];
    [downloadButton setEnabled:NO];

Now we'll launch wget. To do this, we need a new object. Add NSTask *wget; to the interface declaration.

Initialize the object:

    wget=[[NSTask alloc] init];

Next thing is to tell our app what UNIX "thing" we want to execute.

    [wget setLaunchPath:@"/usr/bin/wget"];

Wget downloads into the current directory, so let's change that to the download folder.

    [wget setCurrentDirectoryPath:
	[[downloadFolder stringValue] stringByExpandingTildeInPath]];

(Don't forget that you can always look at Apple's reference if don't know what a particular call does!) Now we'll set the parameters.

    if([recursive state])
        [wget setArguments:[NSArray arrayWithObjects:@"-r",[url stringValue],nil]];
    else
        [wget setArguments:[NSArray arrayWithObject:[url stringValue]]];

And finally, we'll launch it.

    [wget launch];
}

Well, that's it, you'll think. Really? No, we forgot something: how can we tell when the download has finished? Why, that's something for the notification center. For this to work, we need a constructor and a new method:

- (id)init {
    self = [super init];
    [[NSNotificationCenter defaultCenter] addObserver:self 
            selector:@selector(finishedDownload:) 
            name:NSTaskDidTerminateNotification 
            object:nil];
	
	wget = nil; // This is a good time to initialize the pointer
	return self;
}

- (void)finishedDownload:(NSNotification *)aNotification {
	[downloadButton setTitle:@"Download"];
	[downloadButton setEnabled:YES];
	[wget release]; // Don't forget to clean up memory
	wget=nil; // Just in case...
}

That's it!

Notes for improvement:

  • You might want to implement some additional features, like passive FTP or limiting recursion.
  • You definitely need an icon :-).
  • You should read the download directory from the Internet configuration (using Carbon's InternetConfig).
  • Maybe you should implement a log window so that people can watch something while it's downloading (see NSTask's setStandardOutput:).

 

// Passing A Pipe To Another Command

Stefan Lange-Hegermann told me I should include an explanation on how to pipe one command to another (like "ps ax | grep Application"). In short, a pipe is a data stream. All data that the first one sends to stdout is passed to the second one as stdin. Here's what he wrote me:

One often needs to pass the pipe from one command line tool to another (like "locate something | head -n 2") which is a pretty common case. You might only want to get the first 2 lines of some output in many cases and doing this by hand blows your code up a lot.

Cool, it's called an NSPipe, pretty obvious hint. So something like

[command setStandardOutput:newPipe];
[target setStandardInput:newPipe];
[target setStandardOutput:colPipe];

[command launch];
[target launch];

will do the trick and you only have to parse the output of "target" to get the final result. I didn't try to parse the output of "command" too, but I guess that's also possible. You could get only two lines of output this way and if you don't find what you were looking for you could look into the whole result without the hassle of running "command" again.


Comments

Very helpful!

Thanks

Posted by: Jeff Brewster on January 28, 2003 01:53 PM

How do I get the return of the command such as in ftp I can see the tree?

Posted by: Gabriel on February 24, 2003 11:58 PM

Extremely helpful. A definite for UNIX wrapping. Such a simple example, that does so much teaching.

Posted by: Chris Giddings on March 15, 2003 11:33 PM

Very good examples, exactly what I was looking for. But another question, I didn't know about this technique when I wanted to run UNIX from a Cocoa program so I used strings and the system() POSIX command. I realize it is nowhere near as elegant but is this equivalent or are there other issues in using a system() command inside a Cocoa program ?

Posted by: jim Schimpf on September 2, 2003 04:53 AM

system() is a huge security risk, when not used properly.
First problem: relying on $PATH. Somebody could insert another directory in front of it, and the command "ls" (or any other) would do something completely different.
Second problem: shell commands, like the pipe (|) could be inserted and execute any command (desired or not). You have to escape them.
Third problem: the iTunes-installer-problem. 'rm -rf /Volumes/Disk 1/Applications/iTunes.app' doesn't do what you'd think it does: it recursively deletes /Volumes/Disk AND 1/Applications/iTunes.app. You have to escape spaces, too.

Posted by: andy on September 2, 2003 05:02 AM

Far and away the best tutorial I've ever read. Really helped me out.

Posted by: CyberLOL on December 13, 2003 02:36 PM
Post a comment