« Wrapping UNIX Commands
// 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:
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.
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 PMExtremely 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 PMVery 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 AMsystem() 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.
Far and away the best tutorial I've ever read. Really helped me out.
Posted by: CyberLOL on December 13, 2003 02:36 PM