How to read Command Line Input on macOS

I was building a simple Command Line Tool app for macOS. One thing the app needed was user input, i.e. it should wait for the user to type something that I’d like to make use of in the app.

Turns out it’s a rather complicated affair, and I haven’t found a comprehensive starter guide on how to actually accomplish this.

I wanted to create a Command Line Tool app that was capable of accepting text input from the Terminal window, use it, and then write output back for the user to read.

But that wasn’t enough: I also had to tell Xcode to setup the app appropriately, otherwise the Terminal window wasn’t launched – which is of course necessary for a Common Line Tool.

In this article we’ll do just that: prepare Xcode to launch Terminal, wait for input, and print it out again. Here we go.

Preparing Xcode to use Terminal

Create a new Xcode Project and choose Command Line Tool. Give it a funky name, choose Objective-C, and click OK. I’ll call mine CLI (as in Command Line Interface).

This will get us started with a very minimal project, only containing a main.m file. All the app currently does is to print an NSLog message, “Hello, World!” to the main log file. We want to divert this output so that it doesn’t go to the log file, but instead to the Command Line. We need to do two things to make this happen.

First, let’s ask Xcode to open the Terminal App on our machine when the app launches.

To do this, head over to Product – Scheme – Edit Scheme. We’re editing the Run scheme. Under the Info Tab, click the Executable drop-down menu and choose other, then navigate to Applications – Utilities and select the Terminal.app. Uncheck the debug tick box while you’re here.

Diverting NSLog Output to the Command Line

This will launch Terminal, but it won’t divert our log output just yet.Let’s do this next.

Stay in the scheme window and head over to the Arguments tab, then add the following under Arguments Passed On Launch:

${BUILT_PRODUCTS_DIR}/${FULL_PRODUCT_NAME}

Technically, with these parameters, we’re making sure that our app is launched right after the Terminal App, and as such, our output can appear there. That’s just what we want.

You may now close this window. Feel free to run the app and hopefully see the “Hello, World!” message printed to a new Terminal window that opens.

Reading User Input from the Command Line

With this setup done, we can now use the NSFileHandle class to read input from the Command Line. It’s not an easy or straightforward process of course. This elaborate method will do it for us though:

- (NSString *)getUserInput {

    // grab input from the command line and return it

    NSFileHandle *handle = [NSFileHandle fileHandleWithStandardInput];
    NSData *data = handle.availableData;
    NSString *input = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
    

    NSCharacterSet *set = [NSCharacterSet newlineCharacterSet];
    NSString *userInput = [input stringByTrimmingCharactersInSet:set];

    return userInput;
}

There’s a lot going on here: first we’ll create a new NSFileHandle and read any data it has to offer. Then we’ll convert the resulting data object into an NSString object.

When the user presses return, our string is returned just fine – but it will contain the “new line character” (i.e. return). I’m removing it here by using an NSCharacterSet before returning the user input.

If you’re interested, there is a (rather painfully looking) one-liner that does the same thing. Here it is, just in case you like your code to look cryptic:

NSString *userInput = [[[NSString alloc] initWithData:[[NSFileHandle fileHandleWithStandardInput] availableData] encoding:NSUTF8StringEncoding] stringByTrimmingCharactersInSet:[NSCharacterSet newlineCharacterSet]];

Either way, we’ll get a nicely formatted NSString object with whatever the user has typed. That’s a great start!

Writing messages to the Command Line

When we print an NSLog message to the Command Line though, it’ll be prefixed by the timestamp and the name of our app. This makes sense when it’s actual log output, because we need to know which app has been writing things to our log. But for our purposes, we’d like to communicate things to the user, and such output would just get in the way.

Therefore, NSLog isn’t the best way to communicate with our users.

Instead, let’s make use of the same NSFileHandle method as before. This time we’ll do the reverse of what we did in the method above:

- (void)printMessage:(NSString *)message {    

    // print a message to the console
    NSData *data = [message dataUsingEncoding:NSUTF8StringEncoding];
    NSFileHandle *handle = [NSFileHandle fileHandleWithStandardOutput];
    [handle writeData:data];
}

Here we create an NSData object from our string, create an NSFileHandle and write our data object to it. This results in a clean output without any timestamps. Note that we must insert our own line breaks where desired, otherwise everything will be on the same line (line breaks can be inserted using \n).

Demo Project

I’ve written a demo project using these two classes – check it out and start writing those Command Line apps you always wanted. It will greet the user, ask for input, and print it out again:

Kudos and Further Reading





Leave a Reply