{"id":391704,"date":"2024-06-29T09:58:58","date_gmt":"2024-06-29T09:58:58","guid":{"rendered":"http:\/\/savepearlharbor.com\/?p=391704"},"modified":"-0001-11-30T00:00:00","modified_gmt":"-0001-11-29T21:00:00","slug":"","status":"publish","type":"post","link":"https:\/\/savepearlharbor.com\/?p=391704","title":{"rendered":"<span>Building your own CLI with Swift Programming Language<\/span>"},"content":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Command-line interfaces (CLI) are a common way to use applications. In iOS, we usually use scripting languages like Bash or Ruby to build those CLIs and automate mundane tasks. The most popular CLI for app signing and build automation is, without a doubt, <a href=\"https:\/\/fastlane.tools\/\" rel=\"noopener noreferrer nofollow\">Fastlane<\/a>, which was initially written in Ruby. Fastlane is a great tool, convenient and fairly easy to use, and a lot of effort came into building it.<\/p>\n<p>However, there&#8217;s a great chance you considered moving away from Fastlane to avoid learning Ruby and to lower the entry threshold for your developers. Setting up a Ruby environment could be quite tedious and require additional devs&#8217; expertise to write and support those scripts. <\/p>\n<p>Also, Fastlane comes with a lot of dependencies itself. There are 200+ lines in <code>Gemfile.lock<\/code> describing dependencies.<\/p>\n<figure class=\"full-width\"><img loading=\"lazy\" decoding=\"async\" src=\"https:\/\/habrastorage.org\/r\/w1560\/getpro\/habr\/upload_files\/9d9\/6cb\/fba\/9d96cbfbab3de6427a290b4d2480115e.png\" width=\"1382\" height=\"2044\" data-src=\"https:\/\/habrastorage.org\/getpro\/habr\/upload_files\/9d9\/6cb\/fba\/9d96cbfbab3de6427a290b4d2480115e.png\"\/><figcaption><\/figcaption><\/figure>\n<p>And that&#8217;s just a part of that list.<\/p>\n<hr\/>\n<p>To counter that, we can use our currently most popular language in iOS \u2013 Swift. As a multipurpose language, Swift allows us to write not only apps, but also backend, scripts, and, specifically, command line tools. <\/p>\n<blockquote>\n<p>An interesting note on Fastlane \u2013 they are <a href=\"https:\/\/docs.fastlane.tools\/getting-started\/ios\/fastlane-swift\/\" rel=\"noopener noreferrer nofollow\">adding<\/a> the Swift version of their tools. It&#8217;s currently in beta, and most times it&#8217;s just a bridge to Ruby code. <\/p>\n<\/blockquote>\n<h2>1. Setting up the project<\/h2>\n<h3>The hard way<\/h3>\n<p>The first alternative would be to create a project in Xcode and use the <code>Command line tool<\/code> template. Without any additional dependencies, we could create a <code>Script.swift<\/code> file and add code like this:<\/p>\n<pre><code class=\"swift\">import Foundation  let arguments = CommandLine.arguments  guard      arguments.count == 3,     let num1 = Int(arguments[1]), let num2 = Int(arguments[2])  else {         print(\"Usage: add &lt;num1> &lt;num2>\")         exit(1) }  let result = num1 + num2 print(result)<\/code><\/pre>\n<p>This simple example wouldn&#8217;t need compiling and could be executed as a script. We only need to add a swift environment to our script like so:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env swift<\/code><\/pre>\n<p>It implies you have Xcode command line tools or a separate <code>swift<\/code> runtime installed. We should also make our file executable and give it all the needed permissions to run:<\/p>\n<ul>\n<li>\n<p>remove <code>.swift<\/code> extension<\/p>\n<\/li>\n<li>\n<p>run <code>chmod u+x Script<\/code><\/p>\n<\/li>\n<\/ul>\n<p>Where <code>u<\/code> is the owner and <code>x<\/code> is the execution permission. We can consider adding <code>a<\/code> instead of <code>u<\/code> for our developer scripts.<\/p>\n<p>The final script file will look like this:<\/p>\n<pre><code class=\"swift\">#!\/usr\/bin\/env swift  import Foundation  let arguments = CommandLine.arguments  guard      arguments.count == 3,     let num1 = Int(arguments[1]), let num2 = Int(arguments[2])  else {         print(\"Usage: add &lt;num1> &lt;num2>\")         exit(1) }  let result = num1 + num2 print(result) <\/code><\/pre>\n<p>Then we could execute it with <code>.\/Script<\/code> command in our terminal.<\/p>\n<h3>The fun way<\/h3>\n<p>But if you plan to build a thorough CLI with subcommands and arguments there is a nicer way. <\/p>\n<p>The first step would be the same. Create an Xcode project with a command line tool template. <\/p>\n<p>Then add a <a href=\"https:\/\/github.com\/apple\/swift-argument-parser\" rel=\"noopener noreferrer nofollow\">Swift Argument Parser<\/a> framework using your dependency manager of choice. <a href=\"https:\/\/www.swift.org\/package-manager\/\" rel=\"noopener noreferrer nofollow\">Swift package manager<\/a> would be a great option nowadays. <\/p>\n<p>Swift Argument Parser is supported by Apple itself and uses all the latest Swift features like <a href=\"https:\/\/docs.swift.org\/swift-book\/documentation\/the-swift-programming-language\/properties\/#Property-Wrappers\" rel=\"noopener noreferrer nofollow\">property wrappers<\/a> and <a href=\"https:\/\/docs.swift.org\/swift-book\/documentation\/the-swift-programming-language\/concurrency\/\" rel=\"noopener noreferrer nofollow\">structured concurrency<\/a>. At the same time, we are maintaining our CLI code readable and structured.<\/p>\n<h2>2. Writing the tools<\/h2>\n<p>With that dependency in place, we could write our reusable commands and subcommands in either object-oriented or protocol-oriented way. Here&#8217;s my example of using CLI to test the iOS app on pull requests.<\/p>\n<p><strong>Adding app`s entry point<\/strong><\/p>\n<pre><code class=\"swift\">import Foundation import ArgumentParser  struct Habramator: ParsableCommand {          static let configuration = CommandConfiguration(         commandName: \"habramator\",         abstract: \"Command line tools for your iOS project\",         subcommands: [             CI.self,             Dev.self         ]     ) }  <\/code><\/pre>\n<p>First, we define our app&#8217;s main command. If you&#8217;re building your CLI as a package, you&#8217;ll need a <code>@main<\/code>  attribute added to the Habramator struct. In this case, the <code>habramator<\/code> is our CLI entry point. <\/p>\n<p>If your CLI doesn&#8217;t need subcommands or you need a default action, override the <code>func run() throws<\/code>  for that at the <code>Habramator<\/code> struct level.<\/p>\n<p><strong>Adding subcommands<\/strong><\/p>\n<p>Then we define our subcommands if needed. Habramator contains 2 subcommands which might look like this:<\/p>\n<pre><code class=\"swift\">import Foundation import ArgumentParser  struct CI: ParsableCommand {          static var configuration = CommandConfiguration(         commandName: \"ci\",         abstract: \"Runs on CI only\",         shouldDisplay: false,         subcommands: [             UnitTests.self         ]     ) }  struct Dev: ParsableCommand {          static let configuration = CommandConfiguration(         commandName: \"dev\",         abstract: \"Runs on dev machine\",         subcommands: [             \/\/ List of subcommands         ]     ) }  <\/code><\/pre>\n<p>We intend to use <code>CI<\/code> subcommand in our continuous integration system. Subcommands could contain sensitive info usage, like API keys or passwords for production certificates. We can obtain sensitive info from the execution environment like this:<\/p>\n<pre><code class=\"swift\">import Foundation  let environmentVariable = ProcessInfo.processInfo.environment[\"KEY\"] <\/code><\/pre>\n<p>The <code>Dev<\/code> subcommand is intended for our team. It could include such tasks as getting provisional profiles, updating our project with resources, etc.<\/p>\n<p><strong>Adding more subcommands<\/strong><\/p>\n<pre><code class=\"swift\">struct UnitTests: ParsableCommand {        static let configuration = CommandConfiguration(         commandName: \"unit-tests\",         abstract: \"Run Unit tests\",         shouldDisplay: false     )      func run() throws {         CommandRunner.execute(command: Test.unitTests)     } }  <\/code><\/pre>\n<p>Note how we can reuse these sub subcommands both in <code>dev<\/code> and <code>ci<\/code> if needed. We used a <code>CommandRunner<\/code> entity, which is essentially our strongly typed wrapper around the <code>shell<\/code> executor:<\/p>\n<pre><code class=\"swift\">struct CommandRunner {          private static let shell = Shell()     private init() {}          static func execute(command: any Command) {         shell.run(             command: \"\"\"             xcodebuild test \\             -workspace \\(Workspace.app) \\             -scheme \\(Scheme.mainAppScheme) \\             -destination \\\"\\(TestDestination.iPhone12iOS15)\\\" \\             -testPlan \\(TestPlan.appUnitTests)             \"\"\"         )     } } <\/code><\/pre>\n<p>That&#8217;s a simple example of how we could shorten our CI calls from a long <code>xcodebuild<\/code> command to only <code>habramator ci unit-tests<\/code>. This way, we keep our CI pipeline <code>yamls<\/code> the same while changing the implementation of the <code>execute<\/code> method. We also use constants to define our project location, a scheme to test, a test plan, and so on. <\/p>\n<p>It could all be passed into the execution environment or as command arguments if needed. To add an argument to our command we should use an <code>@Argument<\/code> property wrapper<\/p>\n<pre><code class=\"swift\">@Argument(help: \"An app scheme to test\", completion: .default) var scheme: String <\/code><\/pre>\n<p>The <code>Shell<\/code> itself might look like this:<\/p>\n<pre><code class=\"swift\">import Foundation  private struct Shell {        private let zsh = \"\/bin\/zsh\"     private let env = ProcessInfo.processInfo.environment          @discardableResult     func run(command: String) -> String? {         print(\"Executing: \\\"\\(command)\\\"...\") \/\/ print will use the stdOut                  let process = Process()         let stdOut = Pipe()         let stdErr = Pipe()                  process.environment = env         process.standardOutput = stdOut         process.standardError = stdOut         process.arguments = [\"-c\" + command]         process.launch()                  output(to: stdOut)         process.waitUntilExit()                exit(process.terminationStatus)     } }  \/\/ MARK: - Private  private extension Shell {          func output(to pipe: Pipe) {         let data = pipe.fileHandleForReading.readDataToEndOfFile()         let outputString = String(decoding: data, as: UTF8.self)         print(outputString)     } }  <\/code><\/pre>\n<p>Here we made some assumptions about the dev&#8217;s and CI&#8217;s environments, but in macOS, a <code>zsh<\/code> shell is set as default from macOS Catalina. We can also move it to the execution environment.<\/p>\n<p>Now we need to build our executable for macOS and give it the same permissions as in the first scenario with <code>chmod<\/code>. With that in place, we could start using our brand-new CLI!<\/p>\n<h2>3. What we achieved<\/h2>\n<p>By writing command line tools in Swift, we decrease the entry threshold for new developers and simplify our overall project setup. <\/p>\n<p>We can reuse our scripts for both developers and CI. At the same time, we maintain readability and open the road for other devs to contribute to our CI pipelines without any prior knowledge of Ruby or Bash.<\/p>\n<p>This, of course, is just a starting point for our CLI. Writing a signing framework like<a href=\"https:\/\/docs.fastlane.tools\/actions\/match\/\" rel=\"noopener noreferrer nofollow\"> Fastlane match<\/a> will require a whole other effort and will be covered in another article.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/717778\/\"> https:\/\/habr.com\/ru\/articles\/717778\/<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<div><!--[--><!--]--><\/div>\n<div id=\"post-content-body\">\n<div>\n<div class=\"article-formatted-body article-formatted-body article-formatted-body_version-2\">\n<div xmlns=\"http:\/\/www.w3.org\/1999\/xhtml\">\n<p>Command-line interfaces (CLI) are a common way to use applications. In iOS, we usually use scripting languages like Bash or Ruby to build those CLIs and automate mundane tasks. The most popular CLI for app signing and build automation is, without a doubt, <a href=\"https:\/\/fastlane.tools\/\" rel=\"noopener noreferrer nofollow\">Fastlane<\/a>, which was initially written in Ruby. Fastlane is a great tool, convenient and fairly easy to use, and a lot of effort came into building it.<\/p>\n<p>However, there&#8217;s a great chance you considered moving away from Fastlane to avoid learning Ruby and to lower the entry threshold for your developers. Setting up a Ruby environment could be quite tedious and require additional devs&#8217; expertise to write and support those scripts. <\/p>\n<p>Also, Fastlane comes with a lot of dependencies itself. There are 200+ lines in <code>Gemfile.lock<\/code> describing dependencies.<\/p>\n<figure class=\"full-width\"><figcaption><\/figcaption><\/figure>\n<p>And that&#8217;s just a part of that list.<\/p>\n<hr\/>\n<p>To counter that, we can use our currently most popular language in iOS \u2013 Swift. As a multipurpose language, Swift allows us to write not only apps, but also backend, scripts, and, specifically, command line tools. <\/p>\n<blockquote>\n<p>An interesting note on Fastlane \u2013 they are <a href=\"https:\/\/docs.fastlane.tools\/getting-started\/ios\/fastlane-swift\/\" rel=\"noopener noreferrer nofollow\">adding<\/a> the Swift version of their tools. It&#8217;s currently in beta, and most times it&#8217;s just a bridge to Ruby code. <\/p>\n<\/blockquote>\n<h2>1. Setting up the project<\/h2>\n<h3>The hard way<\/h3>\n<p>The first alternative would be to create a project in Xcode and use the <code>Command line tool<\/code> template. Without any additional dependencies, we could create a <code>Script.swift<\/code> file and add code like this:<\/p>\n<pre><code class=\"swift\">import Foundation  let arguments = CommandLine.arguments  guard      arguments.count == 3,     let num1 = Int(arguments[1]), let num2 = Int(arguments[2])  else {         print(\"Usage: add &lt;num1> &lt;num2>\")         exit(1) }  let result = num1 + num2 print(result)<\/code><\/pre>\n<p>This simple example wouldn&#8217;t need compiling and could be executed as a script. We only need to add a swift environment to our script like so:<\/p>\n<pre><code class=\"bash\">#!\/usr\/bin\/env swift<\/code><\/pre>\n<p>It implies you have Xcode command line tools or a separate <code>swift<\/code> runtime installed. We should also make our file executable and give it all the needed permissions to run:<\/p>\n<ul>\n<li>\n<p>remove <code>.swift<\/code> extension<\/p>\n<\/li>\n<li>\n<p>run <code>chmod u+x Script<\/code><\/p>\n<\/li>\n<\/ul>\n<p>Where <code>u<\/code> is the owner and <code>x<\/code> is the execution permission. We can consider adding <code>a<\/code> instead of <code>u<\/code> for our developer scripts.<\/p>\n<p>The final script file will look like this:<\/p>\n<pre><code class=\"swift\">#!\/usr\/bin\/env swift  import Foundation  let arguments = CommandLine.arguments  guard      arguments.count == 3,     let num1 = Int(arguments[1]), let num2 = Int(arguments[2])  else {         print(\"Usage: add &lt;num1> &lt;num2>\")         exit(1) }  let result = num1 + num2 print(result) <\/code><\/pre>\n<p>Then we could execute it with <code>.\/Script<\/code> command in our terminal.<\/p>\n<h3>The fun way<\/h3>\n<p>But if you plan to build a thorough CLI with subcommands and arguments there is a nicer way. <\/p>\n<p>The first step would be the same. Create an Xcode project with a command line tool template. <\/p>\n<p>Then add a <a href=\"https:\/\/github.com\/apple\/swift-argument-parser\" rel=\"noopener noreferrer nofollow\">Swift Argument Parser<\/a> framework using your dependency manager of choice. <a href=\"https:\/\/www.swift.org\/package-manager\/\" rel=\"noopener noreferrer nofollow\">Swift package manager<\/a> would be a great option nowadays. <\/p>\n<p>Swift Argument Parser is supported by Apple itself and uses all the latest Swift features like <a href=\"https:\/\/docs.swift.org\/swift-book\/documentation\/the-swift-programming-language\/properties\/#Property-Wrappers\" rel=\"noopener noreferrer nofollow\">property wrappers<\/a> and <a href=\"https:\/\/docs.swift.org\/swift-book\/documentation\/the-swift-programming-language\/concurrency\/\" rel=\"noopener noreferrer nofollow\">structured concurrency<\/a>. At the same time, we are maintaining our CLI code readable and structured.<\/p>\n<h2>2. Writing the tools<\/h2>\n<p>With that dependency in place, we could write our reusable commands and subcommands in either object-oriented or protocol-oriented way. Here&#8217;s my example of using CLI to test the iOS app on pull requests.<\/p>\n<p><strong>Adding app`s entry point<\/strong><\/p>\n<pre><code class=\"swift\">import Foundation import ArgumentParser  struct Habramator: ParsableCommand {          static let configuration = CommandConfiguration(         commandName: \"habramator\",         abstract: \"Command line tools for your iOS project\",         subcommands: [             CI.self,             Dev.self         ]     ) }  <\/code><\/pre>\n<p>First, we define our app&#8217;s main command. If you&#8217;re building your CLI as a package, you&#8217;ll need a <code>@main<\/code>  attribute added to the Habramator struct. In this case, the <code>habramator<\/code> is our CLI entry point. <\/p>\n<p>If your CLI doesn&#8217;t need subcommands or you need a default action, override the <code>func run() throws<\/code>  for that at the <code>Habramator<\/code> struct level.<\/p>\n<p><strong>Adding subcommands<\/strong><\/p>\n<p>Then we define our subcommands if needed. Habramator contains 2 subcommands which might look like this:<\/p>\n<pre><code class=\"swift\">import Foundation import ArgumentParser  struct CI: ParsableCommand {          static var configuration = CommandConfiguration(         commandName: \"ci\",         abstract: \"Runs on CI only\",         shouldDisplay: false,         subcommands: [             UnitTests.self         ]     ) }  struct Dev: ParsableCommand {          static let configuration = CommandConfiguration(         commandName: \"dev\",         abstract: \"Runs on dev machine\",         subcommands: [             \/\/ List of subcommands         ]     ) }  <\/code><\/pre>\n<p>We intend to use <code>CI<\/code> subcommand in our continuous integration system. Subcommands could contain sensitive info usage, like API keys or passwords for production certificates. We can obtain sensitive info from the execution environment like this:<\/p>\n<pre><code class=\"swift\">import Foundation  let environmentVariable = ProcessInfo.processInfo.environment[\"KEY\"] <\/code><\/pre>\n<p>The <code>Dev<\/code> subcommand is intended for our team. It could include such tasks as getting provisional profiles, updating our project with resources, etc.<\/p>\n<p><strong>Adding more subcommands<\/strong><\/p>\n<pre><code class=\"swift\">struct UnitTests: ParsableCommand {        static let configuration = CommandConfiguration(         commandName: \"unit-tests\",         abstract: \"Run Unit tests\",         shouldDisplay: false     )      func run() throws {         CommandRunner.execute(command: Test.unitTests)     } }  <\/code><\/pre>\n<p>Note how we can reuse these sub subcommands both in <code>dev<\/code> and <code>ci<\/code> if needed. We used a <code>CommandRunner<\/code> entity, which is essentially our strongly typed wrapper around the <code>shell<\/code> executor:<\/p>\n<pre><code class=\"swift\">struct CommandRunner {          private static let shell = Shell()     private init() {}          static func execute(command: any Command) {         shell.run(             command: \"\"\"             xcodebuild test \\             -workspace \\(Workspace.app) \\             -scheme \\(Scheme.mainAppScheme) \\             -destination \\\"\\(TestDestination.iPhone12iOS15)\\\" \\             -testPlan \\(TestPlan.appUnitTests)             \"\"\"         )     } } <\/code><\/pre>\n<p>That&#8217;s a simple example of how we could shorten our CI calls from a long <code>xcodebuild<\/code> command to only <code>habramator ci unit-tests<\/code>. This way, we keep our CI pipeline <code>yamls<\/code> the same while changing the implementation of the <code>execute<\/code> method. We also use constants to define our project location, a scheme to test, a test plan, and so on. <\/p>\n<p>It could all be passed into the execution environment or as command arguments if needed. To add an argument to our command we should use an <code>@Argument<\/code> property wrapper<\/p>\n<pre><code class=\"swift\">@Argument(help: \"An app scheme to test\", completion: .default) var scheme: String <\/code><\/pre>\n<p>The <code>Shell<\/code> itself might look like this:<\/p>\n<pre><code class=\"swift\">import Foundation  private struct Shell {        private let zsh = \"\/bin\/zsh\"     private let env = ProcessInfo.processInfo.environment          @discardableResult     func run(command: String) -> String? {         print(\"Executing: \\\"\\(command)\\\"...\") \/\/ print will use the stdOut                  let process = Process()         let stdOut = Pipe()         let stdErr = Pipe()                  process.environment = env         process.standardOutput = stdOut         process.standardError = stdOut         process.arguments = [\"-c\" + command]         process.launch()                  output(to: stdOut)         process.waitUntilExit()                exit(process.terminationStatus)     } }  \/\/ MARK: - Private  private extension Shell {          func output(to pipe: Pipe) {         let data = pipe.fileHandleForReading.readDataToEndOfFile()         let outputString = String(decoding: data, as: UTF8.self)         print(outputString)     } }  <\/code><\/pre>\n<p>Here we made some assumptions about the dev&#8217;s and CI&#8217;s environments, but in macOS, a <code>zsh<\/code> shell is set as default from macOS Catalina. We can also move it to the execution environment.<\/p>\n<p>Now we need to build our executable for macOS and give it the same permissions as in the first scenario with <code>chmod<\/code>. With that in place, we could start using our brand-new CLI!<\/p>\n<h2>3. What we achieved<\/h2>\n<p>By writing command line tools in Swift, we decrease the entry threshold for new developers and simplify our overall project setup. <\/p>\n<p>We can reuse our scripts for both developers and CI. At the same time, we maintain readability and open the road for other devs to contribute to our CI pipelines without any prior knowledge of Ruby or Bash.<\/p>\n<p>This, of course, is just a starting point for our CLI. Writing a signing framework like<a href=\"https:\/\/docs.fastlane.tools\/actions\/match\/\" rel=\"noopener noreferrer nofollow\"> Fastlane match<\/a> will require a whole other effort and will be covered in another article.<\/p>\n<\/p>\n<\/div>\n<\/div>\n<\/div>\n<p><!----><!----><\/div>\n<p><!----><!----><br \/> \u0441\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043e\u0440\u0438\u0433\u0438\u043d\u0430\u043b \u0441\u0442\u0430\u0442\u044c\u0438 <a href=\"https:\/\/habr.com\/ru\/articles\/717778\/\"> https:\/\/habr.com\/ru\/articles\/717778\/<\/a><br \/><\/br><\/br><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[],"tags":[],"class_list":["post-391704","post","type-post","status-publish","format-standard","hentry"],"_links":{"self":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/391704","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=391704"}],"version-history":[{"count":0,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=\/wp\/v2\/posts\/391704\/revisions"}],"wp:attachment":[{"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=391704"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=391704"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/savepearlharbor.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=391704"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}