Command Line Programs on macOS Tutorial

Discover how easy it is to make your own terminal-based apps with this command line programs on macOS tutorial. Updated for Xcode 9 and Swift 4! By Eric Soto.

Leave a rating/review
Save for later
Share
You are currently viewing page 2 of 4 of this article. Click here to view the first page.

Command-Line Arguments

When you start a command-line program, everything you type after the name is passed as an argument to the program. Arguments can be separated with whitespace characters. Usually, you’ll run into two kind of arguments: options and strings.

Options start with a dash followed by a character, or two dashes followed by a word. For example, many programs have the option -h or --help, the first being simply a shortcut for the second. To keep things simple, Panagram will only support the short version of options.

Open Panagram.swift and add the following enum at the top of the file, outside the scope of the Panagram class:

enum OptionType: String {
  case palindrome = "p"
  case anagram = "a"
  case help = "h"
  case unknown
  
  init(value: String) {
    switch value {
    case "a": self = .anagram
    case "p": self = .palindrome
    case "h": self = .help
    default: self = .unknown
    }
  }
}

This defines an enum with String as its base type so you can pass the option argument directly to init(_:). Panagram has three options: -p to detect palindromes, -a for anagrams and -h to show the usage information. Everything else will be handled as an error.

Next, add the following method to the Panagram class:

func getOption(_ option: String) -> (option:OptionType, value: String) {
  return (OptionType(value: option), option)
}

The above method accepts an option argument as a String and returns a tuple of OptionType and String.

Note: If you’re not yet familiar with Tuples in Swift, check out our video series on Beginning Swift 3, specifically PART 5: Tuples.

In the Panagram, class replace the contents of staticMode() with the following:

//1
let argCount = CommandLine.argc
//2
let argument = CommandLine.arguments[1]
//3
let (option, value) = getOption(argument.substring(from: argument.index(argument.startIndex, offsetBy: 1)))
//4
consoleIO.writeMessage("Argument count: \(argCount) Option: \(option) value: \(value)")

Here’s what’s going on in the code above:

  1. You first get the number of arguments passed to the program. Since the executable path is always passed in (as CommandLine.arguments[0]), the count value will always be greater than or equal to 1.
  2. Next, take the first “real” argument (the option argument) from the arguments array.
  3. Then you parse the argument and convert it to an OptionType. The index(_:offsetBy:) method is simply skipping the first character in the argument’s string, which in this case is the hyphen (`-`) character before the option.
  4. Finally, you log the parsing results to the Console.

In main.swift, replace the line panagram.staticMode() with the following:

if CommandLine.argc < 2 {
  //TODO: Handle interactive mode
} else {
  panagram.staticMode()
}

If your program is invoked with fewer than 2 arguments, then you're going to start interactive mode - you'll do this part later. Otherwise, you use the non-interactive static mode.

You now need to figure out how to pass arguments to your command-line tool from within Xcode. To do this, click on the Scheme named Panagram in the Toolbar:

Scheme

Select Edit Scheme... from the menu that appears:

Edit_Scheme

Ensure Run is selected in the left pane, click the Arguments tab, then click the + sign under Arguments Passed On Launch. Add -p as argument and click Close:

Scheme_Settings

Now build and run, and you'll see the following output in the Console:

Argument count: 2 Option: Palindrome value: p
Program ended with exit code: 0

So far, you've added a basic option system to your tool, learned how to handle command-line arguments and how to pass arguments from within Xcode.

Next up, you'll build the main functionality of Panagram.

Anagrams and Palindromes

Before you can write any code to detect palindromes or anagrams, you should be clear on what they are!

Palindromes are words or sentences that read the same backwards and forwards. Here are some examples:

  • level
  • noon
  • A man, a plan, a canal - Panama!

As you can see, capitalization and punctuation are often ignored. To keep things simple, Panagram will ignore capitalization and white spaces, but will not handle punctuation.

Anagrams are words or sentences that are built using the characters of other words or sentences. Some examples are:

  • silent listen
  • Bolivia Lobivia (it's a cactus from Bolivia)

You'll encapsulate the detection logic inside a small extension to String.

Create a new file named StringExtension.swift and add the following code to it:

extension String {
}

Time for a bit of design work. First, how to detect an anagram:

  1. Ignore capitalization and whitespace for both strings.
  2. Check that both strings contain the same characters, and that all characters appear the same number of times.

Add the following method to StringExtension.swift:

func isAnagramOf(_ s: String) -> Bool {
  //1
  let lowerSelf = self.lowercased().replacingOccurrences(of: " ", with: "")
  let lowerOther = s.lowercased().replacingOccurrences(of: " ", with: "")
  //2
  return lowerSelf.sorted() == lowerOther.sorted()
}

Taking a closer look at the algorithm above:

  1. First, you remove capitalization and whitespace from both Strings.
  2. Then you sort and compare the characters.

Detecting palindromes is simple as well:

  1. Ignore all capitalization and whitespace.
  2. Reverse the string and compare; if it's the same, then you have a palindrome.

Add the following method to detect palindromes:

func isPalindrome() -> Bool {
  //1
  let f = self.lowercased().replacingOccurrences(of: " ", with: "")
  //2
  let s = String(f.reversed())
  //3
  return  f == s
}

The logic here is quite straightforward:

  1. Remove capitalization and whitespace.
  2. Create a second string with the reversed characters.
  3. If they are equal, it is a palindrome.

Time to pull this all together and help Panagram do its job.

Open Panagram.swift and replace the call to writeMessage(_:to:) in staticMode() with the following:

//1
switch option {
case .anagram:
    //2
    if argCount != 4 {
        if argCount > 4 {
            consoleIO.writeMessage("Too many arguments for option \(option.rawValue)", to: .error)
        } else {
            consoleIO.writeMessage("Too few arguments for option \(option.rawValue)", to: .error)
        }        
        consoleIO.printUsage()
    } else {
        //3
        let first = CommandLine.arguments[2]
        let second = CommandLine.arguments[3]
        
        if first.isAnagramOf(second) {
            consoleIO.writeMessage("\(second) is an anagram of \(first)")
        } else {
            consoleIO.writeMessage("\(second) is not an anagram of \(first)")
        }
    }
case .palindrome:
    //4
    if argCount != 3 {
        if argCount > 3 {
            consoleIO.writeMessage("Too many arguments for option \(option.rawValue)", to: .error)
        } else {
            consoleIO.writeMessage("Too few arguments for option \(option.rawValue)", to: .error)
        }
        consoleIO.printUsage()
    } else {
        //5
        let s = CommandLine.arguments[2]
        let isPalindrome = s.isPalindrome()
        consoleIO.writeMessage("\(s) is \(isPalindrome ? "" : "not ")a palindrome")
    }
//6
case .help:
    consoleIO.printUsage()
case .unknown:
    //7
    consoleIO.writeMessage("Unknown option \(value)")
    consoleIO.printUsage()
}

Going through the above code step-by-step:

  1. First, switch based on what argument you were passed, to determine what operation will be performed.
  2. In the case of an anagram, there must be four command-line arguments passed in. The first is the executable path, the second the -a option and finally the two strings to check. If you don't have four arguments, then print an error message.
  3. If the argument count is good, store the two strings in local variables, check them to see if they are anagrams of each other, and print the result.
  4. In the case of a palindrome, you must have three arguments. The first is the executable path, the second is the -p option and finally the string to check. If you don't have three arguments, then print an error message.
  5. Check the string to see if it is a palindrome and print the result.
  6. If the -h option was passed in, then print the usage information.
  7. If an unknown option is passed, print the usage information.

Now, modify the arguments inside the scheme. For example, to use the -p option you must pass two arguments (in addition to the first argument, the executable's path, which is always passed implicitly).

Select Edit Scheme... from the Set Active Scheme toolbar item, and add a second argument with the value "level" as shown below:

p-option-settings

Build and run, and you'll see the following output in the console:

level is a palindrome
Program ended with exit code: 0

Contributors

Gabriel Miro

Tech Editor

Fahim Farook

Final Pass Editor

Michael Briscoe

Team Lead

Over 300 content creators. Join our team.