SoFunction
Updated on 2025-04-11

How to use Swift to implement a command line tool

This article briefly introduces how to develop command line tools in Swift and interact with shell commands. A hydrology article, please do not spray if you don’t like it.

The main purpose is to use this tool to parse the performance monitoring component of WeChat Matrix's OOM Log.

Basic modules

Here, only the common basic modules are briefly introduced.

Process

The Process class can be used to open another child process and monitor its operation.

  1. launchPath: Specifies the execution path. If set to /usr/bin/env , this command can be used to print all environment variables on the machine; it can also be used to execute shell commands if you follow parameters. The demo in this article uses it to execute the entered commands.
  2. arguments: Parameters, just pass them in an array.
  3. launch: Call the launch function to start the process and use it to execute commands.
  4. waitUntilExit: Generally, when executing shell commands, you need to wait for the command to return.
  5. terminationStatus: The current end state of the process, which is normally 0.
  6. standardOutput: standardOutput corresponds to the standard output of the terminal. standardError is the error output.

Pipe

The Pipe class is the operating system pipeline, which is used here to accept the output of child processes. Here, it can be used to pass the output of the process to the specified place in the pipeline, such as an output variable, or a file.

  • fileHandleForReading: Where does pipe read content?
  • fileHandleForWriting: Where does pipe write content?

CommandLine

It is just used to get script parameters.

print() // 2
print() // ["./", "hello"]

Encapsulate Shell commands

Execute only shell commands

There are two encapsulation functions for calling Shell commands. I personally prefer the second one, and directly encapsulate the shell commands and parameters into a string and pass them in.

@discardableResult
func runShell(_ command: String) -> Int32 {
 let task = Process()
  = "/bin/bash"
  = ["-c", command]
 ()
 ()
 return 
}

@discardableResult
func runShellWithArgs(_ args: String...) -> Int32 {
 let task = Process()
  = "/usr/bin/env"
  = args
 ()
 ()
 return 
}

Use as follows:

runShell("pwd")
runShell("ls -l")

runShellWithArgs("pwd")
runShellWithArgs("ls", "-l")

Requires the output of the shell command

Pipe is needed here.

@discardableResult
func runShellAndOutput(_ command: String) -> (Int32, String?) {
 let task = Process()
  = "/bin/bash"
  = ["-c", command]
 
 let pipe = Pipe()
  = pipe
  = pipe
 
 ()
 
 let data = ()
 let output = String(data: data, encoding: .utf8)
 
 ()
 
 return (, output)
}

@discardableResult
func runShellWithArgsAndOutput(_ args: String...) -> (Int32, String?) {
 let task = Process()

  = "/usr/bin/env"
  = args
 
 let pipe = Pipe()
  = pipe
  = pipe
 
 ()
 
 let data = ()
 let output = String(data: data, encoding: .utf8)
 
 ()
 
 return (, output)
}

Use as follows:

let (ret1, output1) = runShellAndOutput("ls -l")
if let output11 = output1 {
 print(output11)
}

let (ret2, output2) = runShellWithArgsAndOutput("ls", "-l")
if let output22 = output2 {
 print(output2)
}

How to parse Matrix's OOM Log

Matrix's OOM Log format is as follows, which is actually a big JSON:

{
 "head": {
  "protocol_ver": 1,
  "phone": "iPhone10,1",
  "os_ver": "13.4",
  "launch_time": 1589361495000,
  "report_time": 1589362109100,
  "app_uuid": ""
 },
 "items": [
  {
   "tag": "iOS_MemStat",
   "info": "",
   "scene": "",
   "name": "Malloc 12.54 MiB",
   "size": 146313216,
   "count": 1,
   "stacks": [
    {
     "caller": "f07199ac8a903127b17f0a906ffb0237@84128",
     "size": 146313216,
     "count": 1,
     "frames": [
      {
       "uuid": "a0a7d67af0f3399a8f006f92716d8e6f",
       "offset": 67308
      },
      {
       "uuid": "a0a7d67af0f3399a8f006f92716d8e6f",
       "offset": 69836
      },
      {
       "uuid": "f07199ac8a903127b17f0a906ffb0237",
       "offset": 84128
      },
      {
       "uuid": "b80198f7beb93e79b25c7a27d68bb489",
       "offset": 14934312
      },
      {
       "uuid": "1a46239df2fc34b695bc9f38869f0c85",
       "offset": 1126304
      },
      {
       "uuid": "1a46239df2fc34b695bc9f38869f0c85",
       "offset": 123584
      },
      {
       "uuid": "1a46239df2fc34b695bc9f38869f0c85",
       "offset": 1135100
      }]
    }
   ]
  }
 ]
}

The analysis idea is actually very simple. Just convert JSON to Model, and then extract the corresponding information according to your needs.

uuid is the unique identifier of mach-o, and offset is the offset of the symbol relative to the base address of mach-o. Get the dSYM file and use the atos command to symbolize it.

guard let rawLogModel = () else { exit(-1) }
print("______ Start to process Matrix OOM Log ...")

let group = DispatchGroup()

var metaLog = ""

for item in  {
 guard let stacks =  else { continue }
 
 ()
 
 ().async {
  var log = "______ item ______ name: \(), size: \(), count: \() \n"
  metaLog += log
  
  for stack in stacks {
   let outputs = ({ (frame: MatrixOOMLogModelFrame) -> String in
    // let uuid = 
    let offset = 
    let instructionAddress = loadAddress + offset
    let (_, output) = runShellAndOutput("xcrun atos -o \(dwarf) -arch arm64 -l 0x1 \()")
    return output ?? ""
   })
   
   log += ()
   
   print(log)
  }
  
  ()
 }
}

()

print("\n\(metaLog)\n")

print("______ Finished processing Matrix OOM Log ...")

() is to convert JSON to Model, and the Codable in Swift is used here.

There is a point to note here. Mac CLI does not have the concept of Bundle, only one bin file. Therefore, for the original JSON file, it can only be added through external bundles. Create a bundle separately through New->Target. You need to add the bundle name in Xcode -> Build Phases -> Copy Files, and then you can load the bundle and get the log file in it through Bundle(url: mockDataBundleURL).

Because the execution time of atos is long, a large number of symbolic operations will be very time-consuming. Generally speaking, this code is executed for about six or seven minutes, and it can completely symbolize a Matrix OOM Log. How to analyze the symbolized records is another topic.

References

How do I run an terminal command in a swift script? (. xcodebuild)

This is the end of this article about how to use Swift to implement a command line tool. For more related Swift command line content, please search for my previous articles or continue browsing the related articles below. I hope everyone will support me in the future!