{"id":18081712,"url":"https://github.com/twostraws/brisk","last_synced_at":"2025-04-05T20:08:07.019Z","repository":{"id":47387578,"uuid":"242777655","full_name":"twostraws/Brisk","owner":"twostraws","description":"A proof of concept scripting library for Swift","archived":false,"fork":false,"pushed_at":"2021-01-04T18:32:29.000Z","size":262,"stargazers_count":502,"open_issues_count":3,"forks_count":18,"subscribers_count":20,"default_branch":"main","last_synced_at":"2025-03-29T19:06:23.995Z","etag":null,"topics":["script","scripting-language","swift","swift-package-manager","xcode"],"latest_commit_sha":null,"homepage":null,"language":"Swift","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/twostraws.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-02-24T15:54:26.000Z","updated_at":"2025-03-23T06:32:14.000Z","dependencies_parsed_at":"2022-08-23T04:21:11.981Z","dependency_job_id":null,"html_url":"https://github.com/twostraws/Brisk","commit_stats":null,"previous_names":[],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twostraws%2FBrisk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twostraws%2FBrisk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twostraws%2FBrisk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/twostraws%2FBrisk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/twostraws","download_url":"https://codeload.github.com/twostraws/Brisk/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393570,"owners_count":20931812,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["script","scripting-language","swift","swift-package-manager","xcode"],"created_at":"2024-10-31T13:15:55.412Z","updated_at":"2025-04-05T20:08:06.995Z","avatar_url":"https://github.com/twostraws.png","language":"Swift","readme":"\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://www.hackingwithswift.com/img/brisk/logo.png\" alt=\"Brisk logo\" width=\"413\" maxHeight=\"83\" /\u003e\n\u003c/p\u003e\n\n\u003cp align=\"center\"\u003e\n    \u003cimg src=\"https://img.shields.io/badge/Swift-5.1-brightgreen.svg\" /\u003e\n    \u003cimg src=\"https://img.shields.io/badge/macOS-10.15-blue.svg\" /\u003e\n    \u003ca href=\"https://twitter.com/twostraws\"\u003e\n        \u003cimg src=\"https://img.shields.io/badge/Contact-@twostraws-lightgrey.svg?style=flat\" alt=\"Twitter: @twostraws\" /\u003e\n    \u003c/a\u003e\n\u003c/p\u003e\n\nBrisk is a *proof of concept* scripting library for Swift developers. It keeps all the features we like about Swift, but provides wrappers around common functionality to make them more convenient for local scripting.\n\nDo you see that 💥 right there next to the logo? That’s there for a reason: Brisk bypasses some of Swift’s built-in safety features to make it behave more like Python or Ruby, which means it’s awesome for quick scripts but a really, really bad idea to use in shipping apps.\n\nThis means you get:\n\n1. All of Swift’s type safety\n2. All of Swift’s functionality (protocols, extensions, etc)\n3. All of Swift’s performance\n\nBut:\n\n1. Many calls that use `try` are assumed to work – if they don’t, your code will print a message and either continue or halt depending on your setting.\n2. You get many helper functions that make common scripting functionality easier: reading and writing files, parsing JSON and XML, string manipulation, regular expressions, and more.\n3. Network fetches are synchronous.\n4. Strings can be indexed using integers, and you can add, subtract, multiply, and divide `Int`, `Double`, and `CGFloat` freely. So `someStr[3]` and `someInt + someDouble` works as in scripting languages. (Again, please don’t use this in production code.)\n5. We assume many sensible defaults: you want to write strings with UTF-8, you want to create directories with intermediates, `trim()` should remove whitespace unless asked otherwise, and so on.\n\nWe don’t replace any of Swift’s default functionality, which means if you want to mix Brisk’s scripting wrappers with the full Foundation APIs (or Apple’s other frameworks), you can.\n\nSo, it’s called Brisk: it’s fast like Swift, but with that little element of risk 🙂\n\n\n## Installation\n\nRun these two commands: \n\n```\ngit clone https://github.com/twostraws/Brisk\ncd Brisk\nmake install\n```\n\nBrisk installs a template full of its helper functions in `~/.brisk`, plus a simple helper script in `/usr/local/bin`.\n\nUsage:\n\n```\nbrisk myscriptname\n```\n\nThat will create a new directory called `myscriptname`, copy in all the helper functions, then open it in Xcode ready for you to edit. Using Xcode means you get full code completion, and can run your script by pressing Cmd+R like usual.\n\nUsing `swift run` from that directory, the script will run from the command line; if you use `swift build` you'll get a finished binary you can put anywhere.\n\n**Warning:** The `brisk` command is easily the most experimental part of this whole package, so please let me know how you get on with it. Ideally it should create open Xcode straight to an editing window saying `print(\"Hello, Brisk!\")`, but let me know if you get something else.\n\n\n## Examples\n\nThis creates a directory, changes into it, copies in an example JSON file, parses it into a string array, then saves the number of items in a new file called output.txt:\n\n```swift\nmkdir(\"example\")\nchdir(\"example\")\nfileCopy(\"~/example.json\", to: \".\")\n\nlet names = decode(file: \"example.json\", as: [String].self)\nlet output = \"Result \\(names.count)\"\noutput.write(to: \"output.txt\")\n```\n\nIf you were writing this using the regular Foundation APIs, your code might look something like this:\n\n```swift\nlet fm = FileManager.default\ntry fm.createDirectory(atPath: \"example\", withIntermediateDirectories: true)\nfm.changeCurrentDirectoryPath(\"example\")\ntry fm.copyItem(atPath: NSHomeDirectory() + \"/example.json\", toPath: \"example\")\n\nlet input = try String(contentsOfFile: \"example.json\")\nlet data = Data(input.utf8)\nlet names = try JSONDecoder().decode([String].self, from: data)\nlet output = \"Result \\(names.count)\"\ntry output.write(toFile: \"output.txt\", atomically: true, encoding: .utf8)\n```\n\nThe Foundation code has lots of throwing functions, which is why we need to repeat the use of `try`. This is really important when shipping production software because it forces us to handle errors gracefully, but in simple scripts where you know the structure of your code, it gets in the way.\n\nThis example finds all .txt files in a directory and its subdirectories, counting how many lines there are in total:\n\n```swift\nvar totalLines = 0\n\nfor file in scandir(\"~/Input\", recursively: true) {\n    guard file.hasSuffix(\".txt\") else { continue }\n    let contents = String(file: \"~/Input\"/file) ?? \"\"\n    totalLines += contents.lines.count\n}\n\nprint(\"Counted \\(totalLines) lines\")\n```\n\nOr using `recurse()`:\n\n```swift\nvar totalLines = 0\n\nrecurse(\"~/Input\", extensions: \".txt\") { file in\n    let contents = String(file: \"~/Input\"/file) ?? \"\"\n    totalLines += contents.lines.count\n}\n\nprint(\"Counted \\(totalLines) lines\")\n```\n\nAnd here’s the same thing using the Foundation APIs:\n\n```swift\nlet enumerator = FileManager.default.enumerator(atPath: NSHomeDirectory() + \"/Input\")\nlet files = enumerator?.allObjects as! [String]\nvar totalLines = 0\n\nfor file in files {\n    guard file.hasSuffix(\".txt\") else { continue }\n    let contents = try! String(contentsOfFile: NSHomeDirectory() + \"/Input/\\(file)\")\n    totalLines += contents.components(separatedBy: .newlines).count\n}\n\nprint(\"Counted \\(totalLines) lines\")\n```\n\nHere are some more examples – I’m not going to keep on showing you the Foundation equivalent, because you can imagine it for yourself.\n\nThis fetches the contents of Swift.org and checks whether it was changed since the script was last run:\n\n```swift\nlet html = String(url: \"https://www.swift.org\")\nlet newHash = html.sha256()\nlet oldHash = String(file: \"oldHash\")\nnewHash.write(to: \"oldHash\")\n\nif newHash != oldHash {\n    print(\"Site changed!\")\n}\n```\n\nThis creates an array of names, removes any duplicates, then writes the result out to a file as JSON:\n\n```swift\nlet names = [\"Ron\", \"Harry\", \"Ron\", \"Hermione\", \"Ron\"]\nlet json = names.unique().jsonData()\njson.write(to: \"names.txt\")\n```\n\nThis checks whether a string matches a regular expression:\n\n```swift\nlet example = \"Hacking with Swift is a great site.\"\n\nif example.matches(regex: \"(great|awesome) site\") {\n    print(\"Trufax\")\n}\n```\n\nLoop through all files in a directory recursively, printing the name of each file and its string contents:\n\n```swift\nrecurse(\"~/Input\") { file in\n    let text = String(file: \"~/Input\"/file) ?? \"\"\n    print(\"\\(file): \\(text)\")\n}\n```\n\nPrint whether a directory contains any zip files:\n\n```swift\nlet contents = scandir(\"~/Input\")\nlet hasZips = contents.any { $0.hasSuffix(\".zip\") }\nprint(hasZips)\n```\n\nThis loads Apple’s latest newsroom RSS and prints out the titles of all the stories:\n\n```swift\nlet data = Data(url: \"https://apple.com/newsroom/rss-feed.rss\")\nif let node = parseXML(data) {\n    let titles = node.getElementsByTagName(\"title\")\n\n    for title in titles {\n        print(title.data)\n    }\n}\n```\n\n\n## Wait, but… why?\n\nI was working on a general purpose scripting library for Swift, following fairly standard Swift conventions – you created a struct to represent the file you wanted to work with, for example.\n\nAnd it worked – you could write scripts in Swift that were a little less cumbersome than Foundation. But it still wasn’t *nice*: you could achieve results, but it still felt like Python, Ruby, or any number of alternatives were better choices, and I was choosing Swift just because it was Swift.\n\nSo, Brisk is a pragmatic selection of wrappers around Foundation APIs, letting us get quick results for common operations, but still draw on the full power of the language and Apple’s frameworks. The result is a set of function calls, initializers, and extensions that make common things trivial, while allowing you to benefit from Swift’s power features and “gracefully upgrade” to the full fat Foundation APIs whenever you need.\n\n\n## Naming conventions\n\nThis code has gone through so many iterations over time, because it’s fundamentally built on functions I’ve been using locally. However, as I worked towards an actual proof of concept I had to try to bring things together a cohesive way, which meant figuring out How to Name Things.\n\n* When using long-time standard things from POSIX or C, those function names were preserved. So, `mkdir()`, `chdir()`, `getcwd()`, all exist.\n* Where equivalent functions existed in other popular languages, they were imported: `isdir()`, `scandir()`, `recurse()`, `getpid()`, `basename()`, etc.\n* Where functionality made for natural extensions of common Swift types – `String`, `Comparable`, `Date`, etc – extensions were always preferred.\n\nThe only really problematic names were things for common file operations, such as checking whether a file exists or reading the contents of a file. Originally I used short names such as `exists(\"someFile.txt\")` and `copy(\"someFile\", to: \"dir\")`, which made for concise and expressive code. However, as soon as you made a variable called `copy` – which is easily done! – you lose visibility to the function\n\nI then moved to using `File.copy()`, `File.exists()` and more, giving the functions a clear namespace. That works great for avoiding name collisions, and also helps with discoverability, but became more cumbersome to read and write. So, after trying them both for a while I found that the current versions worked best: `fileDelete()`, and so on.\n\nI’d be more than happy to continue exploring alternatives!\n\n\n\n## Reference\n\nThis needs way more documentation, but hopefully this is enough to get you started.\n\n\n### Extensions on Array\n\nRemoves all instances of an element from an array:\n\n```swift\nfunc remove(_: Element)\n```\n\n### Extensions on Comparable\n\nClamps any comparable value between a low and a high value, inclusive:\n\n```swift\nfunc Comparable.clamp(low: Self, high: Self) -\u003e Self\n```\n\n### Extensions on Data\n\nCalculates the hash value of this `Data` instance:\n\n```swift\nfunc Data.md5() -\u003e String\nfunc Data.sha1() -\u003e String\nfunc Data.sha256() -\u003e String\n```\n\nConverts the `Data` instance to base 64 representation:\n\n```swift\nfunc Data.base64() -\u003e String\n```\n\nWrites the `Data` instance to a file path; returns true on success or false otherwise:\n\n```swift\nfunc write(to file: String) -\u003e Bool\n```\n\nCreates a `Data` instance by downloading from a URL or by reading a local file:\n\n```swift\nData(url: String)\nData?(file: String)\n```\n\n\n### Extensions on Date\n\nReads a `Date` instance as an Unix epoch time integer:\n\n```swift\nfunc unixTime() -\u003e Int\n```\n\nFormats a `Date` as a string:\n\n```swift\nfunc string(using format: String) -\u003e String\n```\n\n### Decoding\n\nDecodes a string to a specific `Decodable` type, optionally providing strategies for decoding keys and dates:\n\n```swift\nfunc decode\u003cT: Decodable\u003e(string input: String, as type: T.Type, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate) -\u003e T\n```\n\nThe same as above, except now loading from a local file:\n\n```swift\nfunc decode\u003cT: Decodable\u003e(file: String, as type: T.Type, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate) -\u003e T\n```\n\nCreates a `Decodable` instance by fetching data a URL:\n\n```swift\nDecodable.init(url: String, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate)\n```\n\n\n### Directories\n\nThe user’s home directory:\n\n```swift\nDirectory.homeDir: String\n```\n\nMakes a directory:\n\n```swift\n@discardableResult func mkdir(_ directory: String, withIntermediates: Bool = true) -\u003e Bool\n```\n\nRemoves a directory:\n\n```swift\n@discardableResult func rmdir(_ file: String) -\u003e Bool\nfunc getcwd() -\u003e String\n```\n\nReturns true if a file path represents a directory, or false otherwise:\n\n```swift\nfunc isdir(_ name: String) -\u003e Bool\n```\n\nChanges the current working directory:\n\n```swift\nfunc chdir(_ newDirectory: String) -\u003e Bool\n```\n\nRetrieves all files in a directory, either including all subdirectories or not:\n\n```swift\nfunc scandir(_ directory: String, recursively: Bool = false) -\u003e [String]\n```\n\nRuns through all files in a directory, including subdirectories, and runs a closure for each file that matches an extension list:\n\n```swift\nfunc recurse(_ directory: String, extensions: String..., action: (String) throws -\u003e Void) rethrows\n```\n\nSame as above, except now you can pass in a custom predicate:\n\n```swift\nfunc recurse(_ directory: String, predicate: (String) -\u003e Bool, action: (String) throws -\u003e Void) rethrows\n```\n\n\n### Extensions on Encodable\n\nConverts any `Encodable` type to some JSON `Data`, optionally providing strategies for encoding keys and dates:\n\n```swift\nfunc Encodable.jsonData(keys: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, dates: JSONEncoder.DateEncodingStrategy = .deferredToDate) -\u003e Data\n```\n\nSame as above, except converts it a JSON `String`:\n\n```swift\nfunc Decodable.jsonString(keys: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, dates: JSONEncoder.DateEncodingStrategy = .deferredToDate) -\u003e String\n```\n\n\n### Files\n\nCreates a file, optionally providing initial contents. Returns true on success or false otherwise:\n\n```swift\n@discardableResult func fileCreate(_ file: String, contents: Data? = nil) -\u003e Bool\n```\n\nRemoves a file at a path; returns true on success or false otherwise:\n\n```swift\n@discardableResult func fileDelete(_ file: String) -\u003e Bool\n```\n\nReturns true if a file exists:\n\n```swift\nfunc fileExists(_ name: String) -\u003e Bool\n```\n\nReturns all properties for a file:\n\n```swift\nfunc fileProperties(_ file: String) -\u003e [FileAttributeKey: Any]\n```\n\nReturns the size of a file:\n\n```swift\nfunc fileSize(_ file: String) -\u003e UInt64\n```\n\nReturns the date a file was created or modified:\n\n```swift\nfunc fileCreation(_ file: String) -\u003e Date\nfunc fileModified(_ file: String) -\u003e Date\n```\n\nReturns a temporary filename:\n\n```swift\nfunc tempFile() -\u003e String\n```\n\nReturns the base name of a file – the filename itself, excluding any directories:\n\n```swift\nfunc basename(of file: String) -\u003e String\n```\n\nCopies a file from one place to another:\n\n```swift\n@discardableResult func fileCopy(_ from: String, to: String) -\u003e Bool\n```\n\n\n### Numeric operators\n\nA series of operator overloads that let you add, subtract, multiply, and divide across integers, floats, and doubles:\n\n```swift\nfunc +\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: I, rhs: F) -\u003e F\nfunc +\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: F, rhs: I) -\u003e F\nfunc -\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: I, rhs: F) -\u003e F\nfunc -\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: F, rhs: I) -\u003e F\nfunc *\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: I, rhs: F) -\u003e F\nfunc *\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: F, rhs: I) -\u003e F\nfunc /\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: I, rhs: F) -\u003e F\nfunc /\u003cI: BinaryInteger, F: BinaryFloatingPoint\u003e(lhs: F, rhs: I) -\u003e F\n```\n\n\n### Processes\n\nReturns the current process ID:\n\n```swift\nfunc getpid() -\u003e Int\n```\n\nReturns the host name:\n\n```swift\nfunc getHostName() -\u003e String\n```\n\nReturns the username of the logged in user:\n\n```swift\nfunc getUserName() -\u003e String\n```\n\nGets or sets environment variables:\n\n```swift\nfunc getenv(_ key: String) -\u003e String\nfunc setenv(_ key: String, _ value: String)\n```\n\n\n### Extensions on Sequence\n\nReturns any sequence, with duplicates removed. Element must conform to `Hashable`:\n\n```swift\nfunc Sequence.unique() -\u003e [Element]\n```\n\nReturns all the indexes where an element exists in a sequence. Element must conform to `Equatable`:\n\n```swift\nfunc Sequence.indexes(of searchItem: Element) -\u003e [Int]\n```\n\nReturns true if any or none of the items in a sequence match a predicate:\n\n```swift\nfunc any(match predicate: (Element) throws -\u003e Bool) rethrows -\u003e Bool\nfunc none(match predicate: (Element) throws -\u003e Bool) rethrows -\u003e Bool\n```\n\nReturns several random numbers from a sequence, up to the number requested:\n\n```swift\nSequence.random(_ num: Int) -\u003e [Element]\n```\n\n\n### Extensions on String\n\nThe string as an array of lines:\n\n```swift\nvar lines: [String]\n```\n\nAn operator that lets us join strings together into a path:\n\n```swift\nstatic func / (lhs: String, rhs: String) -\u003e String\n```\n\nCalculates the hash value of this `String` instance:\n\n```swift\nfunc md5() -\u003e String\nfunc sha1() -\u003e String\nfunc sha256() -\u003e String\n```\n\nConverts the `String` instance to base 64 representation:\n\n```swift\nfunc base64() -\u003e String\n```\n\nWrites a string to a file:\n\n```swift\n@discardableResult func write(to file: String) -\u003e Bool\n```\n\nReplaces all instances of one string with another in the source `String`:\n\n```swift\nfunc replacing(_ search: String, with replacement: String) -\u003e String\nmutating func String.replace(_ search: String, with replacement: String)\n```\n\nReplaces `count` instances of one string with another in the source `String`:\n\n```swift\nfunc replacing(_ search: String, with replacement: String, count maxReplacements: Int) -\u003e String\nmutating func String.replace(_ search: String, with replacement: String, count maxReplacements: Int)\n```\n\nTrims characters from a string, whitespace by default:\n\n```swift\nmutating func trim(_ characters: String = \" \\t\\n\\r\\0\")\nfunc String.trimmed(_ characters: String = \" \\t\\n\\r\\0\") -\u003e String\n```\n\nReturns true if a string matches a regular expression, with optional extra options:\n\n```swift\nfunc matches(regex: String, options: NSRegularExpression.Options = []) -\u003e Bool\n```\n\nReplaces matches for a regular expression with a replacement string:\n\n```swift\nreplacing(regex: String, with replacement: String, options: NSString.CompareOptions) -\u003e String\nmutating func String.replace(regex: String, with replacement: String, options: NSString.CompareOptions)\n```\n\nSubscripts to let us read strings using integers and ranges:\n\n```swift\nsubscript(idx: Int) -\u003e String\nsubscript(range: Range\u003cInt\u003e) -\u003e String\nsubscript(range: ClosedRange\u003cInt\u003e) -\u003e String\nsubscript(range: CountablePartialRangeFrom\u003cInt\u003e) -\u003e String\nsubscript(range: PartialRangeThrough\u003cInt\u003e) -\u003e String\nsubscript(range: PartialRangeUpTo\u003cInt\u003e) -\u003e String\n```\n\nExpands path components such as `.` and `~`:\n\n```swift\nexpandingPath() -\u003e String\n```\n\nCreates a `String` instance by downloading from a URL or by reading a local file:\n\n```swift\nString.init(url: String)\nString.init?(file: String)\n```\n\nRemoves a prefix or suffix from a string, if it exists:\n\n```swift\ndeletingPrefix(_ prefix: String) -\u003e String\ndeletingSuffix(_ suffix: String) -\u003e String\n```\n\nAdds a prefix or suffix to a string, if it doesn’t already have it:\n\n```swift\nfunc String.withPrefix(_ prefix: String) -\u003e String\nfunc String.withSuffix(_ suffix: String) -\u003e String\n```\n\n\n### System functionality\n\nMany functions will print a message and return a default value if their functionality failed. Set this to true if you want your script to terminate on these problems:\n\n```swift\nstatic var Brisk.haltOnError: Bool\n```\n\nPrints a message, or terminates the script if `Brisk.haltOnError` is true:\n\n```swift\nfunc printOrDie(_ message: String)\n```\n\nTerminates the program, printing a message and returning an error code to the system:\n\n```swift\nfunc exit(_ message: String = \"\", code: Int = 0) -\u003e Never\n```\n\nIf Cocoa is available, this opens a file or folder using the correct app. This is helpful for showing the results of a script, because you can use `open(getcwd())`:\n\n```swift\nfunc open(_ thing: String)\n```\n\n### Extensions on URL\n\nAdd a string to a URL:\n\n```swift\nstatic func +(lhs: URL, rhs: String) -\u003e URL\nstatic func +=(lhs: inout URL, rhs: String)\n```\n\n### XML parsing\n\nParses an instance of `Data` or `String` into an XML, or loads a file and does the same:\n\n```swift\nfunc parseXML(_ data: Data) -\u003e XML.XMLNode?\nfunc parseXML(_ string: String) -\u003e XML.XMLNode?\nfunc parseXML(from file: String) -\u003e XML.XMLNode?\n```\n\nThe resulting `XMLNode` has the following properties:\n\n- `tag`: The tag name used, e.g. `\u003ch1\u003e`.\n- `data`: The text inside the tag, e.g. `\u003ch1\u003eThis bit is the data\u003c/h1\u003e`\n- `attributes`: A dictionary containing the keys and values for all attributes.\n- `childNodes`: an array of `XMLNode` that belong to this node.\n\nIt also has a tiny subset of minidom functionality to make querying possible.\n\nThis finds all elements by a tag name, looking through all children, grandchildren, and so on:\n\n```swift\nfunc getElementsByTagName(_ name: String) -\u003e [XMLNode]\n```\n\nThis returns true if the current node has a specific attribute, or false otherwise:\n\n```swift\nfunc hasAttribute(_ name: String) -\u003e Bool\n```\n\nThis reads a single attribute, or sends back an empty string otherwise:\n\n```swift\nfunc getAttribute(_ name: String) -\u003e String\n```\n\n\n## Contribution guide\n\nAny help you can offer with this project is most welcome – there are opportunities big and small so that someone with only a small amount of Swift experience can help.\n\nSome suggestions you might want to explore, ordered by usefulness:\n\n- Write some tests.\n- Contribute example scripts.\n- Add more helper functions.\n \n \n## What now?\n\nThis is a proof of concept scripting library for Swift developers. I don’t think it’s perfect, but I do at least hope it gives you some things to think about.\n\nSome tips:\n\n1. If you already write scripts in Bash, Ruby, Python, PHP, JavaScript, etc, your muscle memory will always feel like it’s drawing you back there. That’s OK – learning anything new takes time.\n2. Stay away from macOS protected directories, such as your Desktop, Documents, and Photos.\n3. If you intend to keep scripts around for a long period of time, you can easily “upgrade” your code from Brisk’s helpers up to Foundation calls; nothing is overridden.\n4. The code is open source. Even if you end up not using Brisk at all, you’re welcome to read the code, learn from it, take it for your own projects, and so on.\n \n\n## Credits\n\nBrisk was designed and built by Paul Hudson, and is copyright © Paul Hudson 2020. Brisk is licensed under the MIT license; for the full license please see the LICENSE file.\n\nSwift, the Swift logo, and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries.\n\nIf you find Brisk useful, you might find my website full of Swift tutorials equally useful: [Hacking with Swift](https://www.hackingwithswift.com).\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwostraws%2Fbrisk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftwostraws%2Fbrisk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftwostraws%2Fbrisk/lists"}