Command-Line Tools in Objective-C
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production; see the section bibliography for the reference material consulted while preparing these pages. |
Objective-C is the oldest way to write a native command-line tool on macOS that uses the Cocoa frameworks. A tool is an
ordinary C program whose main function creates an autorelease pool, calls Foundation classes instead of (or next to)
the C library, and returns an exit status. Foundation supplies the pieces a command-line program needs: the arguments
and environment (NSProcessInfo), a -key value parser (NSUserDefaults), the standard streams (NSFileHandle),
subprocesses (NSTask) and an event loop (NSRunLoop, with Grand Central Dispatch underneath). This page builds the
running Bookshelf scenario with them: a shelf command-line tool and a shelfd background daemon.
The page was written against Apple clang 21 and the Foundation framework of macOS 27, with ARC (automatic reference
counting). Every Objective-C snippet on it was compiled with clang -fobjc-arc -Wall -Wextra and run on macOS, and
the outputs shown are the real ones. The GNUstep makefile near the end was not built while the page was written, because
no GNUstep installation was available. New projects that do not need Objective-C should look at
Command-Line Tools in Swift, which covers the same ground with swift-argument-parser.
The language itself is documented in the Objective-C Reference; the
POSIX mechanisms that Foundation wraps (fork, exec, sigaction, descriptors) are on
POSIX Processes, Signals & I/O, and the design rules for the output of
a tool (streams, exit codes, machine-readable output) are on
Streams & Exit Codes.
Platforms and what to expect
Foundation is a macOS (and iOS, tvOS, watchOS, visionOS) framework. Apple documents it only for its own platforms, so the table separates what is Apple-supported from what is a community port.
| Platform | How Objective-C command-line tools are built | Status |
|---|---|---|
macOS |
Xcode (Command Line Tool template) or |
Supported by Apple. The tool is a Mach-O executable. |
Linux |
GNUstep ( |
Community project. It implements the OpenStep and Cocoa APIs, so Apple’s documentation describes the intent but not every method exists, behaves identically or is available in the version you installed. |
Windows |
GNUstep through MSYS2 / MinGW, or clang with the GNUstep runtime |
Community project, with the same caveat about API coverage. The Win32 console details on Windows Console & Processes apply to the resulting program. |
Apple does not publish an article about the Xcode Command Line Tool template, so the template is described below from what Xcode generates rather than from a dedicated documentation page.
The Xcode template and the clang command line
In Xcode, File > New > Project, then the macOS tab and the Command Line Tool template. The Language popup lets
you pick Objective-C instead of Swift, C or C++. Xcode generates a single main.m that links Foundation, with
a main function shaped like the one below, and a scheme that runs the tool inside Xcode’s console. The Xcode side of
projects, schemes and building from the command line with xcodebuild is on
Xcode Projects & Previews and
CI/CD, Xcode Cloud & the Command Line.
A tool does not need Xcode. The Command Line Tools package (xcode-select --install) provides clang and the macOS
SDK, which is enough for everything on this page.
$ clang -fobjc-arc -Wall -Wextra -framework Foundation -o shelf shelf.m
| Flag | Meaning |
|---|---|
|
Compile with automatic reference counting. Without it the file uses manual retain / release, which is covered on Manual Retain-Release. ARC is the default in Xcode’s template and should be used for new code. |
|
Link the Foundation framework. A tool that also uses Core Foundation, Security or AppKit names those frameworks too. |
|
Turn on the common warnings. Deprecated Foundation methods (for example |
|
Build a universal binary in one command; see Universal binaries, signing and distribution. |
|
Set the lowest macOS version the binary may run on (use the version your users have). Newer API calls then produce availability warnings instead of runtime crashes. |
The snippets below use NSFileHandle -writeData:error: and NSTask -launchAndReturnError:, which are the error-reporting
forms of the older exception-throwing methods. They require macOS 10.15 or later.
main, @autoreleasepool and NSProcessInfo
Foundation-based programs keep the C signature int main(int argc, const char *argv[]), so argc and argv are
available exactly as in C. The body is wrapped in @autoreleasepool: objects returned autoreleased by Foundation
methods are released when the block ends, and a long-running loop that creates many temporary objects should wrap each
iteration in its own pool so memory stays flat. The arguments and environment are also available as objects through
NSProcessInfo.processInfo.
#import <Foundation/Foundation.h>
int main(int argc, const char *argv[]) {
@autoreleasepool {
NSProcessInfo *info = NSProcessInfo.processInfo;
NSArray<NSString *> *args = info.arguments; // args[0] is the program name
NSDictionary<NSString *, NSString *> *env = info.environment;
NSString *home = env[@"SHELF_HOME"] ?: @"(unset)";
printf("process name : %s\n", info.processName.UTF8String);
printf("argc / count : %d / %lu\n", argc, (unsigned long)args.count);
printf("SHELF_HOME : %s\n", home.UTF8String);
for (NSUInteger i = 1; i < args.count; i++) {
printf("arg[%lu] : %s\n", (unsigned long)i, args[i].UTF8String);
}
if (args.count < 2) {
fprintf(stderr, "usage: %s <command> [options]\n", argv[0]);
return 64; // EX_USAGE from <sysexits.h>
}
}
return 0;
}
$ SHELF_HOME=/srv/shelf ./args list --all "Dune Messiah"
process name : args
argc / count : 4 / 4
SHELF_HOME : /srv/shelf
arg[1] : list
arg[2] : --all
arg[3] : Dune Messiah
$ ./args
usage: ./args <command> [options]
$ echo $?
64
Things to know about these APIs:
-
NSProcessInfo.argumentscontains the same strings asargv, asNSStringobjects.environmentreturns a new dictionary on each call; in a test, a variable added withsetenvwas visible in a later call. Pass changed variables to children explicitly throughNSTask.environment(see Running subprocesses with NSTask). -
processNameis the file name of the executable, and is the nameNSLogprints. -
Returning from
mainends the process with that status, like in C.exit(andabort) skips the end of the autorelease pool, which is harmless at process exit. -
Foundation also offers
NSProcessInfo.processIdentifier,hostName,operatingSystemVersion,physicalMemoryandactiveProcessorCount, which are useful for diagnostics output such asshelf --version -v.
Source: Apple — ProcessInfo and
Apple — NSAutoreleasePool. How a
C main receives its arguments and what an exit status means in the context of the whole language is on
C program structure.
The NSUserDefaults argument domain
NSUserDefaults is Cocoa’s preferences database, and it has a feature unique to the platform: arguments of the form
-key value on the command line are searched first when a value is looked up. Apple calls this the argument domain,
which sits ahead of the application’s saved preferences, the global domain and the registered defaults in the search
list. The tool does not parse anything: it asks NSUserDefaults for limit, and the command line wins over the
preferences file, which in turn wins over the value registered in code. This is how many Cocoa developer tools take
options such as -NSDocumentRevisionsDebugMode YES.
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSUserDefaults *defaults = NSUserDefaults.standardUserDefaults;
// Defaults used when neither the command line nor a preferences file sets a key.
[defaults registerDefaults:@{ @"limit": @20, @"title": @"", @"verbose": @NO }];
NSInteger limit = [defaults integerForKey:@"limit"];
NSString *title = [defaults stringForKey:@"title"];
BOOL verbose = [defaults boolForKey:@"verbose"];
NSArray *tags = [defaults arrayForKey:@"tags"];
printf("limit=%ld title='%s' verbose=%s tags=%s\n", (long)limit, title.UTF8String,
verbose ? "YES" : "NO", tags ? tags.description.UTF8String : "(none)");
}
return 0;
}
$ ./defaults
limit=20 title='' verbose=NO tags=(none)
$ ./defaults -limit 5 -title "Dune Messiah" -verbose YES
limit=5 title='Dune Messiah' verbose=YES tags=(none)
$ ./defaults list -limit 7 extra
limit=7 title='' verbose=NO tags=(none)
$ ./defaults -tags '(sci-fi,classic)'
limit=20 title='' verbose=NO tags=(
"sci-fi",
classic
)
Behaviours observed while running this program on macOS, none of which comes from a dedicated guarantee in Apple’s documentation:
-
The pairs may appear anywhere on the line, after positional words such as
list; the argument domain looks at every-key valuepair it can find. -
The value is read as a property list fragment, so
(sci-fi,classic)becomes an array. Typed accessors convert:boolForKey:acceptsYES, andintegerForKey:parses digits. -
There is no
--limit, no-n 5short form, no--help, no validation and no error for an unknown key. A key that is missing or does not convert silently yields the registered default. -
A value that begins with a hyphen is not taken as the value:
-limit -3printed the default of 20. A key with no value (-verboseas the last word) also leaves the default. -
Keys that Cocoa itself reads are also set from the command line.
-AppleLanguages '(fr)'changedNSLocale.preferredLanguagesfor that run from the system language tofr, which is useful for testing localized output and a surprise if a user happens to pass the same key.
The argument domain is a good fit for developer tools, test harnesses and Xcode scheme arguments, where users already
know the -key value convention from Apple’s own tools. It is not a fit for a tool that follows the POSIX utility
syntax guidelines, or that needs help text, subcommands or error messages; use getopt_long (below) or
swift-argument-parser for that.
Source: Apple — UserDefaults and the archived Preferences and Settings Programming Guide — About Preference Domains.
Mixing in getopt_long
Because Objective-C is C, the POSIX and GNU option parsers are available without any wrapper: getopt and
getopt_long come from <getopt.h>, and the BSD <sysexits.h> header names the conventional exit codes (EX_USAGE is
64, EX_DATAERR is 65, EX_IOERR is 74). The listing is a complete shelf with options and help, using Foundation
only where it helps (NSString for the title).
#import <Foundation/Foundation.h>
#include <getopt.h>
#include <sysexits.h>
static void usage(FILE *to) {
fputs("usage: shelf list [-n|--limit N] [-t|--title TEXT] [-v|--verbose]\n"
" shelf --help | --version\n", to);
}
int main(int argc, char *argv[]) {
@autoreleasepool {
static const struct option longopts[] = {
{ "limit", required_argument, NULL, 'n' },
{ "title", required_argument, NULL, 't' },
{ "verbose", no_argument, NULL, 'v' },
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'V' },
{ NULL, 0, NULL, 0 }
};
long limit = 20;
NSString *title = nil;
BOOL verbose = NO;
int c;
while ((c = getopt_long(argc, argv, "n:t:vhV", longopts, NULL)) != -1) {
switch (c) {
case 'n': {
char *end = NULL;
limit = strtol(optarg, &end, 10);
if (*optarg == '\0' || *end != '\0' || limit < 1) {
fprintf(stderr, "shelf: invalid --limit '%s'\n", optarg);
return EX_USAGE;
}
break;
}
case 't': title = [NSString stringWithUTF8String:optarg]; break;
case 'v': verbose = YES; break;
case 'h': usage(stdout); return EX_OK;
case 'V': puts("shelf 1.0.0"); return EX_OK;
default: usage(stderr); return EX_USAGE; // getopt_long already printed the reason
}
}
if (optind >= argc || strcmp(argv[optind], "list") != 0) {
usage(stderr);
return EX_USAGE;
}
printf("list: limit=%ld title=%s verbose=%s\n", limit, title ? title.UTF8String : "(any)",
verbose ? "yes" : "no");
}
return EX_OK;
}
$ ./shelf list --limit 5 --title Dune -v
list: limit=5 title=Dune verbose=yes
$ ./shelf --limit 0 list; echo $?
shelf: invalid --limit '0'
64
$ ./shelf --bogus; echo $?
shelf: unrecognized option `--bogus'
usage: shelf list [-n|--limit N] [-t|--title TEXT] [-v|--verbose]
shelf --help | --version
64
Pitfalls when choosing between the two parsers:
-
Use one scheme per tool.
getopt_longreads-limit 5as the short options-l -i -m -i -t, andNSUserDefaultsreads-n 5as a key namedn. A tool that usesgetopt_longand also touchesNSUserDefaults.standardUserDefaults(for example through a framework) will have unrelated-key valuepairs on its command line interpreted by Cocoa without anybody asking. -
getopt_longaccepts unambiguous abbreviations of long options:shelf list --lim 3was accepted as--limit 3. That is convenient interactively and a compatibility trap for scripts, because adding a later option such as--limitsmakes the old abbreviation ambiguous. -
The options are processed on the C
argv. Convert toNSStringonly after validation, and note thatoptargis a C string in the user’s bytes:stringWithUTF8String:returnsnilfor invalid UTF-8. -
macOS
getoptkeeps its state in global variables such asoptind. Reset it withoptreset = 1; optind = 1;if a program parses a second argument vector; the GNU C library uses a different convention (optind = 0), which is one of the differences between the platforms that GNUstep builds on Linux have to account for.
Source: POSIX — getopt. getopt_long is a GNU
extension that macOS and the BSDs also provide; the C-specific behaviour, argp and error reporting are on
Command-Line Programs in C.
Standard streams with NSFileHandle
NSFileHandle wraps a file descriptor and has factory methods for the three standard streams. The error-returning
methods (readDataToEndOfFileAndReturnError:, writeData:error:) report failures as an NSError rather than raising
an exception. The program below numbers the lines that arrive on standard input, writes the result to standard
output and diagnostics to standard error, and uses sysexits.h-style exit statuses for bad data and I/O failures.
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSFileHandle *in = NSFileHandle.fileHandleWithStandardInput;
NSFileHandle *out = NSFileHandle.fileHandleWithStandardOutput;
NSFileHandle *err = NSFileHandle.fileHandleWithStandardError;
// Read all of stdin (until EOF), one title per line.
NSError *error = nil;
NSData *data = [in readDataToEndOfFileAndReturnError:&error];
if (data == nil) {
NSString *msg = [NSString stringWithFormat:@"shelf: cannot read stdin: %@\n", error.localizedDescription];
[err writeData:[msg dataUsingEncoding:NSUTF8StringEncoding] error:NULL];
return 74; // EX_IOERR
}
NSString *text = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (text == nil) {
[err writeData:[@"shelf: input is not valid UTF-8\n" dataUsingEncoding:NSUTF8StringEncoding] error:NULL];
return 65; // EX_DATAERR
}
NSUInteger n = 0;
for (NSString *line in [text componentsSeparatedByString:@"\n"]) {
if (line.length == 0) continue;
n++;
NSString *row = [NSString stringWithFormat:@"%2lu. %@\n", (unsigned long)n, line];
if (![out writeData:[row dataUsingEncoding:NSUTF8StringEncoding] error:&error]) {
return 74; // e.g. EPIPE when the reader has gone away
}
}
}
return 0;
}
$ printf 'Dune\nHyperion\n' | ./fh
1. Dune
2. Hyperion
$ printf '\xff\n' | ./fh; echo $?
shelf: input is not valid UTF-8
65
Pitfalls specific to this API:
-
fileHandleWithStandardInputand its siblings return shared handles for descriptors 0, 1 and 2. Do not close them unless the program really intends to close the stream. -
readDataToEndOfFileAndReturnError:loads everything into memory. For large or endless input (tail -f, a pipe from a long-running process) read in pieces withreadDataUpToLength:error:or use the asynchronousreadabilityHandlerproperty together with a run loop. -
SIGPIPEstill applies. When the reader of the pipe exits early, the default action ofSIGPIPEends the process beforewriteData:error:can return an error. In this testyes Dune | head -c 100000 | ./fh | head -1madefhend with status 141 (128 + 13). A tool that needs to clean up or to reportEPIPEignores the signal at startup withsignal(SIGPIPE, SIG_IGN), then handles the error return; see POSIX Processes, Signals & I/O. -
printfandNSFileHandleuse different buffers.printfto a pipe is block-buffered by the C library, whilewriteData:writes immediately, so a program that mixes both can emit lines out of order. Pick one for output, or callfflush(stdout)before a handle write. -
NSLogis not an output function. It writes a time-stamped, process-identified line to standard error (for example2026-10-25 14:42:13.208 nslog[88439:6337083] hello 1) and, on macOS, to the unified logging system. Do not use it for a tool’s normal output; useprintforNSFileHandle, andfprintf(stderr, ...)for messages meant for the user. -
There is no portable way to ask
NSFileHandlewhether a stream is a terminal. Useisatty(STDOUT_FILENO)from<unistd.h>to decide on colour or progress output, as described in TTY, Colour & Interactivity.
Source: Apple — FileHandle. The streams, isatty and
buffering from the C side are on C input, output and files.
Running subprocesses with NSTask
NSTask (called Process in Swift and in Apple’s current documentation) starts another program without a shell. The
executable is given as a file URL, the arguments as an array of strings (no quoting or globbing happens), and the
standard streams as NSPipe or NSFileHandle objects. The example runs /usr/bin/sort -f as a filter: it writes
three titles to the child’s standard input, closes that end so the child sees end-of-file, reads the sorted result and
returns the child’s exit status.
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSTask *task = [[NSTask alloc] init];
task.executableURL = [NSURL fileURLWithPath:@"/usr/bin/sort"];
task.arguments = @[@"-f"];
NSPipe *toChild = [NSPipe pipe];
NSPipe *fromChild = [NSPipe pipe];
task.standardInput = toChild;
task.standardOutput = fromChild;
task.standardError = NSFileHandle.fileHandleWithStandardError;
NSError *error = nil;
if (![task launchAndReturnError:&error]) {
fprintf(stderr, "shelf: cannot launch sort: %s\n", error.localizedDescription.UTF8String);
return 127;
}
[toChild.fileHandleForWriting writeData:[@"hyperion\nDune\nemma\n" dataUsingEncoding:NSUTF8StringEncoding] error:NULL];
[toChild.fileHandleForWriting closeAndReturnError:NULL]; // EOF for the child
// Read before waiting: a full pipe would otherwise block the child forever.
NSData *sorted = [fromChild.fileHandleForReading readDataToEndOfFileAndReturnError:NULL];
[task waitUntilExit];
fwrite(sorted.bytes, 1, sorted.length, stdout);
if (task.terminationReason == NSTaskTerminationReasonUncaughtSignal) {
fprintf(stderr, "sort killed by signal %d\n", task.terminationStatus);
return 128 + task.terminationStatus;
}
return task.terminationStatus;
}
}
$ ./task
Dune
emma
hyperion
| Property or method | Behaviour |
|---|---|
|
There is no |
|
If left |
|
The child’s working directory; the default is the parent’s. |
|
An |
|
Starts the child and returns |
|
Blocks, running the current run loop, until the child ends. It returns after |
|
The exit code, or the signal number when |
|
A block called on a background queue when the child ends; the way to avoid blocking. Hop back to the main queue with
|
|
Send |
Pitfalls:
-
Deadlock on full pipes. A pipe buffer is finite. If the parent only calls
waitUntilExitwhile the child is blocked writing a large output, neither side makes progress. Read the output (or give the child a file) before waiting, as in the listing, or read incrementally throughreadabilityHandler. -
Always close the write end of the child’s standard input when the data is complete, or a filter such as
sortwaits for more input forever. -
Never build a command string for
/bin/sh -cfrom user input. Keep program and arguments separate inarguments: nothing in that array is interpreted by a shell, which removes the usual injection risk. -
Child processes inherit all inheritable descriptors. Tools that open sensitive files should set the close-on-exec flag (
O_CLOEXEC) on them; see POSIX Processes, Signals & I/O. -
NSTaskis not available on iOS, tvOS or watchOS. It is a macOS (and GNUstep) API.
Source: Apple — Process (NSTask) and Apple — Pipe.
Asynchronous work: GCD signal sources and the run loop
A one-shot tool runs top to bottom and exits. A tool that has to wait (for a child process, a timer, a network event or a
signal) needs an event loop. On macOS there are two cooperating ones: the main dispatch queue of Grand Central
Dispatch (GCD) and the main run loop (NSRunLoop / CFRunLoop). The main dispatch queue is serviced by the main
run loop, so either can be the loop that keeps main alive.
Signals are the classic trouble spot for a daemon because a signal handler may only call async-signal-safe functions
(printf is not one of them). GCD solves this with signal sources: the handler is an ordinary block that GCD runs on a
queue of your choice, outside signal context, so it can use Objective-C and the C library freely. The shelfd daemon
below checks for overdue loans every second, reloads its configuration on SIGHUP and shuts down on SIGTERM.
#import <Foundation/Foundation.h>
#include <signal.h>
static dispatch_source_t makeSignalSource(int signo, dispatch_block_t handler) {
signal(signo, SIG_IGN); // the default action must be off, or the process dies first
dispatch_source_t src =
dispatch_source_create(DISPATCH_SOURCE_TYPE_SIGNAL, (uintptr_t)signo, 0, dispatch_get_main_queue());
dispatch_source_set_event_handler(src, handler);
dispatch_resume(src);
return src;
}
int main(void) {
@autoreleasepool {
__block int ticks = 0;
dispatch_source_t timer =
dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, dispatch_get_main_queue());
dispatch_source_set_timer(timer, DISPATCH_TIME_NOW, 1 * NSEC_PER_SEC, 100 * NSEC_PER_MSEC);
dispatch_source_set_event_handler(timer, ^{
printf("shelfd: overdue-loan check %d\n", ++ticks);
fflush(stdout);
});
dispatch_resume(timer);
dispatch_source_t term = makeSignalSource(SIGTERM, ^{
printf("shelfd: SIGTERM, shutting down after %d checks\n", ticks);
fflush(stdout);
exit(0);
});
dispatch_source_t hup = makeSignalSource(SIGHUP, ^{
printf("shelfd: SIGHUP, reloading configuration\n");
fflush(stdout);
});
(void)term; (void)hup; // keep the sources alive for the life of the process
dispatch_main(); // never returns; or [[NSRunLoop mainRunLoop] run]
}
}
$ ./shelfd &
shelfd: overdue-loan check 1
shelfd: overdue-loan check 2
shelfd: overdue-loan check 3
$ kill -HUP %1
shelfd: SIGHUP, reloading configuration
shelfd: overdue-loan check 4
$ kill -TERM %1
shelfd: SIGTERM, shutting down after 4 checks
The points that make this work:
-
signal(signo, SIG_IGN)is required. The dispatch source receives a signal that is ignored or blocked; if the default action (termination forSIGTERM) is still installed, the process dies before the source can run. -
The sources are held in local variables. Under ARC, a dispatch source that is released stops delivering events, so the variables must stay alive for as long as the loop runs. Here
mainnever returns, which does that implicitly. -
dispatch_main()parks the main thread and services the main queue forever. An alternative is to run[[NSRunLoop mainRunLoop] run], which also lets Foundation timers,NSTasktermination handlers andreadabilityHandlercallbacks run. -
Run the handlers on the main queue to keep daemon state single-threaded; use a serial queue of your own if the main thread has other work.
-
Exit from the handler for
SIGTERMafter finishing cleanup.exitruns functions registered withatexitand flushes the C library’s streams.
When the loop is meant to end after some asynchronous work (rather than run forever), let the work flag completion on the main
queue and run the loop in a condition loop, so main can return an exit status. The program waits for a one-second child
without blocking on waitUntilExit.
#import <Foundation/Foundation.h>
int main(void) {
@autoreleasepool {
NSTask *task = [[NSTask alloc] init];
task.executableURL = [NSURL fileURLWithPath:@"/bin/sleep"];
task.arguments = @[@"1"];
__block BOOL finished = NO;
__block int status = -1;
task.terminationHandler = ^(NSTask *t) {
// Runs on a background queue; hop to the main queue to touch main-thread state.
int s = t.terminationStatus;
dispatch_async(dispatch_get_main_queue(), ^{ status = s; finished = YES; });
};
NSError *error = nil;
if (![task launchAndReturnError:&error]) return 127;
NSRunLoop *loop = NSRunLoop.currentRunLoop;
while (!finished && [loop runMode:NSDefaultRunLoopMode beforeDate:NSDate.distantFuture]) {}
printf("child exited with %d\n", status);
return status;
}
}
$ ./async
child exited with 0
A version that called CFRunLoopStop from the termination handler’s background queue instead of dispatching to the main
queue did not return when it was tried on macOS 27, so the pattern above is the one to copy.
Pitfalls:
-
A run loop with no input sources returns immediately. Timers, ports, dispatch sources and main-queue work are what keep it running; a tool whose only job is to wait for a signal needs a signal source, as above.
-
Blocks capture variables by value unless they are marked
__block. A counter incremented inside a handler needs the qualifier. -
Do not mix a hand-written
sigactionhandler and a dispatch source for the same signal. -
This pattern is for daemons that run under a supervisor. The platform specifics (launchd property lists,
KeepAlive, logging withos_log) are on Long-Running Processes & Services.
Source: Apple — DispatchSource,
Apple — Dispatch and
Apple — RunLoop. The archived
Threading Programming Guide — Run Loops
explains how input sources and modes decide when a run loop returns. Apple’s pages for these APIs are written for Swift;
the Objective-C names used here (dispatch_source_create, DISPATCH_SOURCE_TYPE_SIGNAL) are the C API they wrap.
The concurrency model of the language is on Concurrency in Objective-C.
Universal binaries, signing and distribution
A binary built by default on an Apple silicon Mac contains only an arm64 slice. Passing both architectures to clang
builds a universal (fat) binary that runs natively on Apple silicon and Intel Macs.
$ clang -fobjc-arc -framework Foundation -arch arm64 -arch x86_64 -o shelf shelf.m
$ lipo -info shelf
Architectures in the fat file: shelf are: x86_64 arm64
$ otool -L shelf
shelf:
/System/Library/Frameworks/Foundation.framework/Versions/C/Foundation (compatibility version 300.0.0, current version 5027.0.69)
/usr/lib/libobjc.A.dylib (compatibility version 1.0.0, current version 228.0.0)
/usr/lib/libSystem.B.dylib (compatibility version 1.0.0, current version 1359.0.0)
The tool links dynamically against system libraries that ship with macOS, so the binary is small (about 51 KB for
the arm64-only build of the shelf above) and nothing has to be installed next to it. Foundation is a system
framework that Apple provides only as a dynamic library, so the tool depends on the macOS it runs on; this is one reason
the portable choice for tools that must also run on Linux and Windows is usually not Objective-C.
Distributing the tool to other people is covered once for all languages: a bare binary can be signed and notarized but not stapled, so ship it in a signed installer package, disk image or archive; see Signing, Notarization & OS Trust and macOS Distribution Outside the App Store. Building fat binaries for several architectures and OSes with different toolchains is on Native Binaries & Cross-Compilation.
GNUstep on Linux and Windows
GNUstep is a free implementation of the OpenStep specification and much of Cocoa. Its gnustep-base library provides
NSProcessInfo, NSUserDefaults, NSFileHandle, NSTask, NSRunLoop and the other Foundation classes used on this
page, and gnustep-make provides a makefile framework. The library is built against the GNUstep Objective-C runtime
rather than Apple’s, and clang compiles it. A command-line tool uses tool.make:
include $(GNUSTEP_MAKEFILES)/common.make
TOOL_NAME = shelf
shelf_OBJC_FILES = shelf.m
include $(GNUSTEP_MAKEFILES)/tool.make
$ . /usr/share/GNUstep/Makefiles/GNUstep.sh
$ make
The location of GNUstep.sh depends on the distribution and on how GNUstep was installed. Build without the makefile
framework by asking gnustep-config for the flags; the exact flag set depends on the installed version and the
Objective-C runtime it uses.
$ clang $(gnustep-config --objc-flags) shelf.m $(gnustep-config --base-libs) -o shelf
These GNUstep commands were not run while the page was written. What to expect from the porting layer:
-
The
getopt_long,sysexits.h,signalandisattyparts of this page are plain C and run unchanged on Linux with glibc.<sysexits.h>is not part of the Windows C runtime; define the exit codes yourself there. -
NSTask,NSRunLoop,NSFileHandleandNSUserDefaultsexist in GNUstep, but GNUstep’s own documentation is the authority on which methods and which behaviour (for example the argument domain, or the error-returninglaunchAndReturnError:variants Apple added in recent macOS versions) are available in your version. Apple’s current reference should not be assumed to apply. -
Grand Central Dispatch is a separate library (libdispatch) on other platforms, and whether
dispatch_mainand signal sources are available depends on how GNUstep and libdispatch were built. Windows has no POSIX signals, so a Windows tool needs the console control handler described on Windows Console & Processes. -
Standard terminal behaviour, ANSI escape sequences and the UTF-8 code page questions on Windows are independent of the language; the same page applies.
If the goal is one program for macOS, Linux and Windows, the languages with first-class cross-platform toolchains (Swift, Rust, Go, .NET, C++) are easier choices than GNUstep; the comparison is on The CLI Landscape & Choosing a Stack and Cross-Platform Portability.
Source: GNUstep — Developer site and GNUstep.
Pitfalls and security notes
-
Exit statuses are 0 to 255. Use
sysexits.hnames or your own table; do not return error codes outside that range and do not returnNSIntegervalues frommainwithout checking them. -
Output belongs on stdout, diagnostics on stderr. Use
printf/NSFileHandlefor the former andfprintf(stderr, ...)for the latter.NSLogis only for debugging. -
Treat argument values as untrusted. Validate numbers (
strtolwith an end-pointer check, as above, rather thanNSString.intValue, which returns 0 for garbage), validate paths before opening files, and never pass user text to a shell.NSStringformatting with a user-supplied format string ([NSString stringWithFormat:userText]) is a format-string vulnerability; always pass a constant format string and give the text as an argument. -
Do not log secrets that arrive through arguments. Every user can read the argument vector of every process on macOS and Linux (through
ps), so a password or token should be read from a file descriptor, a prompt without echo, or the environment, not from-password secret. -
Autorelease pools in loops. A loop that processes millions of records should drain a pool per iteration. Wrap the body in an
@autoreleasepool { ... }block inside the loop and memory stays flat. -
Exceptions are not errors. Foundation raises
NSExceptionfor programming mistakes (such as an out-of-bounds array access) and the tool terminates with an uncaught-exception message. The Cocoa convention for expected failures is anNSError **out-parameter, as used throughout this page. See Errors and Exceptions in Objective-C.