Chapters

Hide chapters

macOS by Tutorials

First Edition · macOS 12 · Swift 5.5 · Xcode 13

Section I: Your First App: On This Day

Section 1: 6 chapters
Show chapters Hide chapters

12. Diving Deeper Into Your Mac
Written by Sarah Reichelt

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

So far, you’ve created three different Mac apps: a windowed app, a menu bar app and a document-based app. In this section, you’re going to create another windowed app, but with a different purpose.

You’ll dive deep into your Mac’s system and learn about the command line utilities that are part of macOS. You’ll learn more about using Terminal and how to invoke Terminal commands using Swift.

In the app, you’ll use a Terminal command called sips, which stands for scriptable image processing system. This is a powerful tool, but it’s difficult to remember the syntax. Creating a graphical user interface will make it much easier to use.

Terminal Commands

macOS, and its predecessor OS X, are built on top of Unix. Unix has an extensive set of commands you can run from the command line. These commands are mostly small, single-purpose utilities you can chain together, if needed. You’ve already used a sequence of commands like this in Chapter 1, “Designing the Data Model”, where you formatted the downloaded JSON to make it easier to read.

These commands are executable files stored in secret folders, hidden deep in your Mac’s file system, but now you’re going to find them.

Open Terminal: In Finder, double-click Applications/Utilities/Terminal.app, or press Command-Space to activate Spotlight, and start typing Terminal until the app becomes selectable.

In the Terminal window, type these two commands, pressing Return after each one:

cd /usr/bin
ls

The cd command changes directory to one of the hidden folders. And ls lists the contents. This gives you a huge list of commands that you have access to. Scroll back to see where you typed ls, then right-click it and select Open man Page:

Opening a manual page
Opening a manual page

Nearly every command has its own man page, or manual page, that you can read to find what the command does and what arguments it takes. Some manual pages, like this one for ls, even have examples, which can be extremely useful.

Terminal displays these manual pages using the man command. Scroll through all the commands you listed, find man and right-click to show its manual page.

You can also type man man to see the manual page in your active Terminal window. Press Space to step through the pages or use your trackpad or mouse wheel to scroll. Press q to exit, which makes the manual page disappear.

There are more commands in other folders, mostly in /usr/sbin and /bin, but other apps may have stored commands in other places.

Note: You can make your system unusable with Terminal commands. Terminal assumes you know what you’re doing and will allow you to erase every file off your drive or perform other catastrophic actions. Read the manual pages, have a good backup system and don’t use commands you don’t understand.

While iOS has the same Unix basis as macOS, the locked-down nature of iOS doesn’t give you access to Terminal commands the way macOS does.

Testing Some Commands

Now that you know where macOS keeps these command files, it’s time for you to test some. Here are some interesting — and safe — ones you can run by typing them one at a time into Terminal:

whoami
uptime
cal
man splain
Testing some commands.
Juvsamz lowo rufxavtg.

networkQuality -sv
networkQuality manual page
cadfeqrRaenefd foluef secu

ping -c 5 apple.com

Terminal Shortcuts

Here is a collection of tips to make working in Terminal easier, and more efficient.

Running Commands in a Playground

Now that you know something about Terminal commands, it’s time to get back into Xcode and see how to run them from there.

New macOS playground
Wav cutUL qmowqyeush

which whoami
Finding a command file.
Kuyzijc o tazvigb tode.

// 1
import Cocoa

// 2
let process = Process()

// 3
process.executableURL = URL(fileURLWithPath: "/usr/bin/whoami")

// arguments go here

// standard output goes here

// 4
try? process.run()
Running your first process.
Suprowr wuad saszw jmiwuzs.

Adding Some Pipes

When you run any Terminal command, it opens three channels:

// 1
let outPipe = Pipe()
// 2
let outFile = outPipe.fileHandleForReading
// 3
process.standardOutput = outPipe
// 1
do {
  // 2
  try process.run()
  process.waitUntilExit()

  // 3
  if
    let data = try outFile.readToEnd(),
    let returnValue = String(data: data, encoding: .utf8) {
    print("Result: \(returnValue)")
  }
} catch {
  // 4
  print(error)
}
Getting returned data.
Lemhuwv rekilvav hoqo.

Supplying Arguments

There’s only one more complication, and that’s when a command needs more input. Remember how you pinged Apple’s servers earlier from Terminal? Now, you’ll do the same from your playground.

process.executableURL = URL(fileURLWithPath: "/sbin/ping")
process.arguments = ["-c", "5", "apple.com"]
Running a command with arguments.
Qivrulh i nawkuvg wavz icmiwecbk.

Reading Data Sequentially

In the previous example, you waited until process finished and then used readToEnd() to get the complete output from the command in a single chunk. Now, you’re going to use availableData to read the output as it arrives.

// 1
func getAvailableData(from fileHandle: FileHandle) -> String {
  // 2
  let newData = fileHandle.availableData
  // 3
  if let string = String(data: newData, encoding: .utf8) {
    return string
  }
  // 4
  return ""
}
// 1
try process.run()

// 2
while process.isRunning {
  // 3
  let newString = getAvailableData(from: outFile)
  print(newString.trimmingCharacters(in: .whitespacesAndNewlines))
}
// 4
let newString = getAvailableData(from: outFile)
print(newString.trimmingCharacters(in: .whitespacesAndNewlines))

Finding Commands

You’ve probably spotted a flaw in this system. Using Terminal to find the path to each command is not a great solution. You could find all the paths you need and then hard-code them into your code, but that sounds tedious and error-prone. How about running the which command programmatically and using that?

Shell window
Jmubw safvay

which zsh
zsh -c "which whoami"
process.executableURL = URL(fileURLWithPath: "/bin/zsh")
process.arguments = ["-c", "which whoami"]

Wrapping it in Functions

You now have everything you need to run Terminal commands from your playground, but before you use this in an app, it makes sense to wrap it into reusable functions.

func runCommand(
  _ command: String,
  with arguments: [String] = []
) async -> String {
  // move all the process code below to here
  return ""
}
process.executableURL = URL(fileURLWithPath: command)
process.arguments = arguments
try process.run()

var returnValue = ""
while process.isRunning {
  let newString = getAvailableData(from: outFile)
  returnValue += newString
}
let newString = getAvailableData(from: outFile)
returnValue += newString
return returnValue
  .trimmingCharacters(in: .whitespacesAndNewlines)
func pathTo(command: String) async -> String {
  await runCommand("/bin/zsh", with: ["-c", "which \(command)"])
}
// 1
Task {
  // 2
  let commandPath = await pathTo(command: "cal")
  // 3
  let cal = await runCommand(commandPath, with: ["-h"])
  print(cal)
}
Using the functions.
Imixn plu vokltooqt.

Manipulating Images

You’re about to build an app called ImageSipper, and it’ll use the sips or scriptable image processing system command.

let imagePath = ""
Copy image path
Wepd udubi hikv

Task {
  let sipsPath = await runCommand("/bin/zsh", with: ["-c", "which sips"])

  // sips commands here
}
// 1
let args = ["--getProperty", "all", imagePath]
// 2
let imageData = await runCommand(sipsPath, with: args)
print(imageData)
Image information
Ahubo ugqeyyajaeq

Shrinking the Image

This is a large image, as you can see from the data you just read, so you’re going to use sips to make a smaller copy. So that you don’t overwrite the original, you’ll provide a new file path.

let imagePath = "/path/to/folder/rosella.png"
let imagePathSmall = "/path/to/folder/rosella_small.png"
let resizeArgs = [
  // 1
  "--resampleWidth", "800",
  // 2
  imagePath,
  // 3
  "--out", imagePathSmall
]

// 4
let output = await runCommand(sipsPath, with: resizeArgs)
print("Output: \(output)")
Resized image
Zejitub ajaca

Formatting Arguments

Go back to the manual page for sips. The first entry in the FUNCTIONS section is -g or --getProperty. You’ll see this pattern in many Terminal commands where there is a short form of an argument and a long form. Conventionally, one form has two leading dashes and the other form has only one.

Challenges

Challenge 1: Use Another Terminal Command

Pick another Terminal command and run it in the playground. Use pathTo(command:) to find the location of the command and then use runCommand(_:with:) to get its result.

Challenge 2: Rotate or Flip the Sample Image

You can use sips to flip or rotate an image. The syntax you’d use in Terminal is:

sips --rotate 90 rosella.png --out rosella_rotated.png
sips --flip vertical rosella.png --out rosella_flipped.png

Key Points

  • macOS is built on top of Unix and contains a lot of utility commands you can access through Terminal. These commands often have obscure syntax, which is difficult to remember.
  • You use Process to run these commands in Swift.
  • To run a command in a Process, you have to find the file path for the command. These commands are executable files buried deep inside hidden folders in your system.
  • In order to read the result of Process commands, you need a custom standardOutput with a Pipe and a FileHandle.

Where to Go From Here?

You now have a good understanding of Terminal commands, how to run them in Terminal and how to read their manual pages. You’ve learned how to run these commands using Swift in a playground, and you’ve started to see how you can use the sips command to edit image files.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.
© 2024 Kodeco Inc.

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a Kodeco Personal Plan.

Unlock now