Swiftpack.co - Package - ABridoux/scout

Swift package:
Swift Package Manager
Install:
Mac + Linux
Donwloads:

Scout

This library aims to make specific formats data values reading and writing simple when the data format is not known at build time. It was inspired by SwiftyJson and all the projects that followed, while trying to cover more ground, like Xml or Plist. It unifies writing and reading for those different formats. Getting a value in a Json format would be the same as getting a value in a Xml format.

Why?

With the Foundation libraries to encode/decode Json and Plist, one could ask: why would someone need Scout? Simple answer: there are still cases where you do not know the data format. Sometimes, you will just want to read a single value from a Plist file, and you do not want to create the the struct to decode this file. Or you simply cannot know the data format at build time.

Context

I have been working with many Mac admins recently, and many had to deal with Json, Plist and Xml data. While some where using a format-specific library like jq to parse Json, others where using awk. Each approach is valid, though it comes with some compromises.

Using a format-specific library

You can use a library for each format. But I am not aware today of a library that unifies all of them. So, what you learnt with jq cannot be reused to parse Plist data. You would have to learn to use PlistBuddy or the defaults command. With Scout, you can parse the same way Json, Plist and Xml data.

Using a generic text-processing tool

Don't get me wrong, awk is a wonderful tool. It can do so many things. But it is not that easy to learn. And you have to find a way to parse each different format. Scout is really easy to use, as we will see.

How to use it

Command Line

Homebrew

Use the following command.

brew install ABridoux/formulae/scout

It will download the notarized executable from the latest release. I believe that most Homebrew users do not really care about building the program themselves. If I am wrong, please let me know (by opening an issue for example). Note that you can still build the program by cloning this git as explained below.

Download

You can download the latest version of the executable from the releases. Note that the executable is notarized. Also, a notarized scout package is provided.

After having unzipped the file, you can install it if you want to:

install scout /usr/local/bin/ 

Here is a command which downloads the latest version of the program and install it in /usr/local/bin. Run it to download and install the latest version of the program. It erases the current version you may have.

curl -LO https://github.com/ABridoux/scout/releases/latest/download/scout.zip && \
unzip scout.zip && \
rm scout.zip && \
install scout /usr/local/bin && \
rm scout
Note
  • To find all scout versions, please browse the releases page.
  • When deploying a package (with a MDM for example), it might be useful to add the version to the name. To get scout latest version: simply run scout version to get your installed scout version, or curl --silent "https://api.github.com/repos/ABridoux/scout/releases/latest" | scout tag_name to get the latest version available on the Github repository.

Git

Use the following lines to clone the repository and to install scout (requires Swift 5.2 toolchain to be installed). You can check the Makefile to see the commands used to build and install the executable.

$ git clone https://github.com/ABridoux/scout
$ cd scout
$ make

The program should be install in /usr/local/bin. You can then remove the repository if you do not want to keep it:

$ cd ..
$ rm -r Scout

Swift package

Start by importing the package in your file Packages.swift.

let package = Package (
    ...
    dependencies: [
        .package(url: "https://github.com/ABridoux/scout", from: "0.1.0")
    ],
    ...
)

You can then import Scout in a file.

Usage examples

Some remarks

Invalid paths

When getting/setting/deleting a value, if a key does not exist in the path, an error will be returned/thrown.

add command specificities

  • When adding a value, all the keys which do not exist in the path will be created. Thus, to add a dictionary or an array, you have to specify one child key. Otherwise scout will consider that it is a single value which should be added.
  • That said: when accessing an array child key using the index -1 with the add command, the program will add a new key rather than accessing the last element of the array.
  • Adding a value to an existing key is the same as using the set command.

Swift package

The type of a value is automatically inferred when setting or adding a key value. You can try to force the type with the as type parameter. An error will be thrown if the value is not convertible to the given type.

Command-line

Playground

You can find and try examples with one file People using the different available formats in the Playground folder. The folder contains an Example commands file so that you can see how to use the same commands to parse the different formats.

Examples

Given the following Json (as input stream or file with the input option)

{
  "people": {
    "Tom": {
      "height": 175,
      "age": 68,
      "hobbies": [
        "cooking",
        "guitar"
      ]
    },
    "Arnaud": {
      "height": 180,
      "age": 23,
      "hobbies": [
        "video games",
        "party",
        "tennis"
      ]
    }
  }
}
Reading

scout "people.Tom.hobbies[0]" will output "cooking"

scout "people.Arnaud.height" will output "180"

scout "people.Arnaud" will output Arnaud dictionary:

"height": 180,
"age": 23,
"hobbies": [
    "video games",
    "party",
    "tennis"
]
Setting

scout set "people.Tom.hobbies[0]"=basket will change Tom first hobby from "cooking" to "basket"

scout set "people.Arnaud.height=160" will change Arnaud's height from 180 to 160

scout set "people.Tom.hobbies[0]=basket" "people.Arnaud.height"=160 will change Tom first hobby from "cooking" to "basket" and change Arnaud's height from 180 to 160

scout set "people.Tom.age=#years#" will change Tom age key name from #age# to #years#

scout set "people.Tom.height=/175/" will change Tom height from 180 to a String value "175"

scout set "people.Tom.height=~175~" will change Tom height from 180 to a Real value 175

Deleting

scout delete "people.Tom.height" will delete Tom height scout delete "people.Tom.hobbies[0]" will delete Tom first hobby

Adding

scout add "people.Franklin.height"=165 will create a new dictionary Franklin and add a height key into it with the value 165

scout add "people.Tom.hobbies[-1]="Playing music" will add the hobby "Playing music" to Tom hobbies at the end of the array

scout add "people.Arnaud.hobbies[1]"=reading will insert the hobby "reading" to Arnaud hobbies between the hobby "video games" and "party"

scout add "people.Franklin.hobbies[0]"=football will create a new dictionary Franklin, add a hobbies array into it, and insert the value "football" in the array

scout add "people.Franklin.height"=/165/ will create a new dictionary Franklin and add a height key into it with the String value "165"

scout set "people.Tom.isChild"=true or scout set "people.Tom.isChild=?y?" will add a key #isChild# to Tom dictionary with the value true

Options

Each command will have several options, like the possibility to output the modified data to string or into a file.

cat People.json | scout "people.Tom.height"
is the same as
scout "people.Tom.height -i People.json

The command

scout set \
"people.Tom.height"=190 \
"people.Arnaud.hobbies[1]"=football \
-m People.json

will copy the content in the file People.json, modify it and write it back to People.json.

The command

scout set \
"people.Tom.height"=190 \
"people.Arnaud.hobbies[1]"=football \
-i People.json -v

will output the modified data in the console.

Key names containing dots

If a key name contains dots, e.g. com.company.product, you can enclose it between brackets:

scout "bundle.(com.company.product).version"

Forcing a type

When setting or adding a value, scout will automatically infer the value type. For example, true will be interpreted as a boolean, and 25.3 as a real. That said, you can ask scout to try to force a type when setting or adding a value. This is useful to force a number to be interpreted as a string for example, if the key has to be a string. This type enforcing is not useful for all types and all formats. Xml for example only has string values. Finally, the program will return an error if the value cannot be converted to the given type. For example Hello cannot be converted as an Integer, nor a Real. Here is the syntax for each type:

String

/value/
Example: scout set "path=/valueToConvertToString/"
Useful for Plist and Json

Boolean

?value?
Example: scout add "path=?valueToConvertToBoolean?"
Useful for Plist and Json
Available recognised boolean strings: "y", "yes", "Y", "Yes", "YES", "t", "true", "T", "True", "TRUE", "n", "no", "N", "No", "NO", "f", "false", "F", "False", "FALSE"

Real

~value~
Example: scout add "path=~valueToConvertToReal~"
Useful for Plist

Integer

<value>
Example: scout set "path=<valueToConvertToInteger>"
Useful for Plist

Swift

Unlike SwiftyJson, Scout does not offer the subscript methods. As those methods do not allow today to throw an error, using them implies to find sometimes strange ways to return value when the key is missing.

To explore a format, start by creating the corresponding explorer:

let json = try PathExplorerFactory.make(Json.self, from: data)

Given the following Json

{
  "people": {
    "Tom": {
      "height": 175,
      "age": 68,
      "hobbies": [
        "cooking",
        "guitar"
      ]
    },
    "Arnaud": {
      "height": 180,
      "age": 23,
      "hobbies": [
        "video games",
        "party",
        "tennis"
      ]
    }
  }
}

Here are some examples

// Reading
// --------

try json.get("people", "Tom", "height").int // output 175
try json.get("people", "Arnaud", "hobbies", 2).string // output "party"

// Updating
// -------

// will change Tom's height from 175 to 160
try json.set("people", "Tom", "height", to: 160)

// will change Tom's height from 175 to the String value "160" 
try json.set("people", "Tom", "height", to: 160, as: .string)

// will change Tom's height from 175 to the Double value 160.0
try json.set("people", "Tom", "height", to: 160, as: .real)

// will throw an error as "height" is not convertible to an integer
try json.set("people", "Tom", "height", to: "height", as: .int)

// will change Arnaud second hobby from "party" to "basketball"
try json.set("people", "Arnaud", "hobbies",  1, to: "basketball")

// will change Tom's age key name from #age# to #years#
try json.set("people", "Tom", "age", keyNameTo: "years")

// Deleting
// --------

try json.delete("people", "Tom", "height") // will delete Tom height key

// Adding
// -------

// will add a new dictionary key named "Franklin" into "people" and insert a key named "height" into it with the value 190
try json.add(190, at: "people", "Franklin", "height")

// will add a new dictionary key named "Franklin" into "people" and insert a key named "height" into it with the String value "190"
try json.add(190, at: "people", "Franklin", "height", as: .string)

// will add a new dictionary key named "Franklin" into "people", adding a hobbies array with one element: "basket"
try json.add("basket", at: "people", "Franklin", "hobbies", 0)

// will add a new hobby to Tom's hobbies at the end of the hobbies array
try json.add("football", at: "people", "Tom", "hobbies", -1)

// will add a new key named "color" into "Arnaud" dictionary, with the value "blue"
try json.add("football", at: "people", "Arnaud", "color", "blue")

Note that when parsing the same file but with the Plist format, you would just have to change one line.

So use this

let plist = try PathExplorerFactory.make(Plist.self, from: data)

to parse this file

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>people</key>
	<dict>
		<key>Tom</key>
		<dict>
			<key>height</key>
			<integer>175</integer>
			<key>age</key>
			<integer>68</integer>
			<key>hobbies</key>
			<array>
				<string>cooking</string>
				<string>guitar</string>
			</array>
		</dict>
		<key>Arnaud</key>
		<dict>
			<key>height</key>
			<integer>181</integer>
			<key>age</key>
			<integer>23</integer>
			<key>hobbies</key>
			<array>
				<string>video games</string>
				<string>party</string>
				<string>tennis</string>
			</array>
		</dict>
	</dict>
</dict>
</plist>

Export

If you have modified the path explorer, you can export it to a Data or a String

let xml = try PathExplorerFactory.make(Xml.self, from: data)

// do some modifications...

let data = try xml.exportData()
let string = try xml.exportString()

Special thanks

To parse Xml data, as the standard library does not offer simple way to do it, Scout uses the wonderful library of Marko Tadić: AEXML. He has done an amazing work. And if several Xml parsing and writing libraries exist today, I would definitely recommend his. Marko, you might never read those lines, but thank you again!

Thanks also to the team at Apple behind the ArgumentParser library. They have done an incredible work to make command line tools in Swift easy to implement.

Contributing

Scout is open-source and under a MIT license. If you want to make a change or to add a new feature, please open a Pull Request. Also, feel free to report a bug, an error or even a typo.

Github

link
Stars: 14

Dependencies

Used By

Total: 0

Releases

- 2020-03-29 14:56:40

Added

  • License
  • CLT: output a dictionary or an array rather than return an error
  • Json: backslashes removed when outputing the string

Root nested arrays - 2020-03-29 09:30:52

Fixed

  • Root element with nested arrays: [0][2][1].firstKey

Better subscript error description. CLT type enforcing. - 2020-03-28 16:03:25

Added

  • Reading path for subscript errors. A subscript error now shows precisely where the error occurs.
  • CLT force type. Possibility to try to force a type when setting/adding a value. ~25~ for reals, <25> for integers and ?Yes? for booleans.
  • Possibility to initialise a boolean with string values like 'y", "NO", "t", "True"...
  • PathExplorer generic get functions to try to convert to a KeyAllowedType type.
  • The newly added PKG and the Zip files are notarized

Fixed

  • It was not possible to initialise a Path starting with an array subscript like '[1].key1.key2'

Added nested array support and CLT modify option - 2020-03-23 23:56:52

Added

  • Github test action
  • Nested array support: array[0][2]...
  • CLT [-m | --modify] option to read and write the data from/to the same file

Fixed

  • Xml value adding when key already existed was not working

Several setting and adding actions bug fixes - 2020-03-22 17:06:03

Added

  • PathExplorer format value to indicate the data format
  • Playground files to try the CLT
  • Get a last element in an array at the end with the negative index
  • Setting a value in an array at the end with the negative index

Fixed

  • Custom separator to initialise a path now working
  • Initialise and convert to aKeyAllowedTypeKey now uses CustomStringConvertible to try the String option
  • Negative index to initialise a path now working
  • Array value setting was not working if the value was not a string
  • Inserting a value in an empty array was possible
  • Inserting a value in a Xml only worked when the element was the root element

- 2020-03-19 23:15:38

Added

  • SwiftLint file to execute SwiftLint analysis
  • CLT brackets for key names containing the separator

Changed

  • CLT path to read the values: the separator was changed from -> to .
  • CLT path to set the values: the separator was changed from : to =
  • CLT array subscript. Removed the separator e.g. array.[index] to array[index]

- 2020-03-19 12:19:23

Added

  • Possiblity to try to force the type of a value when setting or adding
  • Command-line tool options to force the string value
  • Command-line tool version command.
  • More in-line documentation
  • Readme instructions to use Homebrew

Changed

  • Refractored the PathExplorerSerialization and PahExplorerxml

Fixed

  • Command-line tool ""----input"" long option to specify a file input fixed to "--input"

Hotfix: updated Makefile and Package.swift - 2020-03-17 09:55:04

Instructions to download and use the executable - 2020-03-16 18:58:11

- 2020-03-16 18:23:48

Fixed oversights.

- 2020-03-16 18:12:47

CRUD operations with following formats: Json, Plist and Xml.