• Stars
    star
    503
  • Rank 87,705 (Top 2 %)
  • Language
    Swift
  • License
    MIT License
  • Created over 4 years ago
  • Updated almost 4 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A proof of concept scripting library for Swift

Brisk logo

Twitter: @twostraws

Brisk 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.

Do 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.

This means you get:

  1. All of Swift’s type safety
  2. All of Swift’s functionality (protocols, extensions, etc)
  3. All of Swift’s performance

But:

  1. 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.
  2. 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.
  3. Network fetches are synchronous.
  4. 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.)
  5. 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.

We 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.

So, it’s called Brisk: it’s fast like Swift, but with that little element of risk 🙂

Installation

Run these two commands:

git clone https://github.com/twostraws/Brisk
cd Brisk
make install

Brisk installs a template full of its helper functions in ~/.brisk, plus a simple helper script in /usr/local/bin.

Usage:

brisk myscriptname

That 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.

Using 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.

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.

Examples

This 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:

mkdir("example")
chdir("example")
fileCopy("~/example.json", to: ".")

let names = decode(file: "example.json", as: [String].self)
let output = "Result \(names.count)"
output.write(to: "output.txt")

If you were writing this using the regular Foundation APIs, your code might look something like this:

let fm = FileManager.default
try fm.createDirectory(atPath: "example", withIntermediateDirectories: true)
fm.changeCurrentDirectoryPath("example")
try fm.copyItem(atPath: NSHomeDirectory() + "/example.json", toPath: "example")

let input = try String(contentsOfFile: "example.json")
let data = Data(input.utf8)
let names = try JSONDecoder().decode([String].self, from: data)
let output = "Result \(names.count)"
try output.write(toFile: "output.txt", atomically: true, encoding: .utf8)

The 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.

This example finds all .txt files in a directory and its subdirectories, counting how many lines there are in total:

var totalLines = 0

for file in scandir("~/Input", recursively: true) {
    guard file.hasSuffix(".txt") else { continue }
    let contents = String(file: "~/Input"/file) ?? ""
    totalLines += contents.lines.count
}

print("Counted \(totalLines) lines")

Or using recurse():

var totalLines = 0

recurse("~/Input", extensions: ".txt") { file in
    let contents = String(file: "~/Input"/file) ?? ""
    totalLines += contents.lines.count
}

print("Counted \(totalLines) lines")

And here’s the same thing using the Foundation APIs:

let enumerator = FileManager.default.enumerator(atPath: NSHomeDirectory() + "/Input")
let files = enumerator?.allObjects as! [String]
var totalLines = 0

for file in files {
    guard file.hasSuffix(".txt") else { continue }
    let contents = try! String(contentsOfFile: NSHomeDirectory() + "/Input/\(file)")
    totalLines += contents.components(separatedBy: .newlines).count
}

print("Counted \(totalLines) lines")

Here are some more examples – I’m not going to keep on showing you the Foundation equivalent, because you can imagine it for yourself.

This fetches the contents of Swift.org and checks whether it was changed since the script was last run:

let html = String(url: "https://www.swift.org")
let newHash = html.sha256()
let oldHash = String(file: "oldHash")
newHash.write(to: "oldHash")

if newHash != oldHash {
    print("Site changed!")
}

This creates an array of names, removes any duplicates, then writes the result out to a file as JSON:

let names = ["Ron", "Harry", "Ron", "Hermione", "Ron"]
let json = names.unique().jsonData()
json.write(to: "names.txt")

This checks whether a string matches a regular expression:

let example = "Hacking with Swift is a great site."

if example.matches(regex: "(great|awesome) site") {
    print("Trufax")
}

Loop through all files in a directory recursively, printing the name of each file and its string contents:

recurse("~/Input") { file in
    let text = String(file: "~/Input"/file) ?? ""
    print("\(file): \(text)")
}

Print whether a directory contains any zip files:

let contents = scandir("~/Input")
let hasZips = contents.any { $0.hasSuffix(".zip") }
print(hasZips)

This loads Apple’s latest newsroom RSS and prints out the titles of all the stories:

let data = Data(url: "https://apple.com/newsroom/rss-feed.rss")
if let node = parseXML(data) {
    let titles = node.getElementsByTagName("title")

    for title in titles {
        print(title.data)
    }
}

Wait, but… why?

I 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.

And 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.

So, 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.

Naming conventions

This 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.

  • When using long-time standard things from POSIX or C, those function names were preserved. So, mkdir(), chdir(), getcwd(), all exist.
  • Where equivalent functions existed in other popular languages, they were imported: isdir(), scandir(), recurse(), getpid(), basename(), etc.
  • Where functionality made for natural extensions of common Swift types – String, Comparable, Date, etc – extensions were always preferred.

The 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

I 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.

I’d be more than happy to continue exploring alternatives!

Reference

This needs way more documentation, but hopefully this is enough to get you started.

Extensions on Array

Removes all instances of an element from an array:

func remove(_: Element)

Extensions on Comparable

Clamps any comparable value between a low and a high value, inclusive:

func Comparable.clamp(low: Self, high: Self) -> Self

Extensions on Data

Calculates the hash value of this Data instance:

func Data.md5() -> String
func Data.sha1() -> String
func Data.sha256() -> String

Converts the Data instance to base 64 representation:

func Data.base64() -> String

Writes the Data instance to a file path; returns true on success or false otherwise:

func write(to file: String) -> Bool

Creates a Data instance by downloading from a URL or by reading a local file:

Data(url: String)
Data?(file: String)

Extensions on Date

Reads a Date instance as an Unix epoch time integer:

func unixTime() -> Int

Formats a Date as a string:

func string(using format: String) -> String

Decoding

Decodes a string to a specific Decodable type, optionally providing strategies for decoding keys and dates:

func decode<T: Decodable>(string input: String, as type: T.Type, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate) -> T

The same as above, except now loading from a local file:

func decode<T: Decodable>(file: String, as type: T.Type, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate) -> T

Creates a Decodable instance by fetching data a URL:

Decodable.init(url: String, keys: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys, dates: JSONDecoder.DateDecodingStrategy = .deferredToDate)

Directories

The user’s home directory:

Directory.homeDir: String

Makes a directory:

@discardableResult func mkdir(_ directory: String, withIntermediates: Bool = true) -> Bool

Removes a directory:

@discardableResult func rmdir(_ file: String) -> Bool
func getcwd() -> String

Returns true if a file path represents a directory, or false otherwise:

func isdir(_ name: String) -> Bool

Changes the current working directory:

func chdir(_ newDirectory: String) -> Bool

Retrieves all files in a directory, either including all subdirectories or not:

func scandir(_ directory: String, recursively: Bool = false) -> [String]

Runs through all files in a directory, including subdirectories, and runs a closure for each file that matches an extension list:

func recurse(_ directory: String, extensions: String..., action: (String) throws -> Void) rethrows

Same as above, except now you can pass in a custom predicate:

func recurse(_ directory: String, predicate: (String) -> Bool, action: (String) throws -> Void) rethrows

Extensions on Encodable

Converts any Encodable type to some JSON Data, optionally providing strategies for encoding keys and dates:

func Encodable.jsonData(keys: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, dates: JSONEncoder.DateEncodingStrategy = .deferredToDate) -> Data

Same as above, except converts it a JSON String:

func Decodable.jsonString(keys: JSONEncoder.KeyEncodingStrategy = .useDefaultKeys, dates: JSONEncoder.DateEncodingStrategy = .deferredToDate) -> String

Files

Creates a file, optionally providing initial contents. Returns true on success or false otherwise:

@discardableResult func fileCreate(_ file: String, contents: Data? = nil) -> Bool

Removes a file at a path; returns true on success or false otherwise:

@discardableResult func fileDelete(_ file: String) -> Bool

Returns true if a file exists:

func fileExists(_ name: String) -> Bool

Returns all properties for a file:

func fileProperties(_ file: String) -> [FileAttributeKey: Any]

Returns the size of a file:

func fileSize(_ file: String) -> UInt64

Returns the date a file was created or modified:

func fileCreation(_ file: String) -> Date
func fileModified(_ file: String) -> Date

Returns a temporary filename:

func tempFile() -> String

Returns the base name of a file – the filename itself, excluding any directories:

func basename(of file: String) -> String

Copies a file from one place to another:

@discardableResult func fileCopy(_ from: String, to: String) -> Bool

Numeric operators

A series of operator overloads that let you add, subtract, multiply, and divide across integers, floats, and doubles:

func +<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: I, rhs: F) -> F
func +<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: F, rhs: I) -> F
func -<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: I, rhs: F) -> F
func -<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: F, rhs: I) -> F
func *<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: I, rhs: F) -> F
func *<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: F, rhs: I) -> F
func /<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: I, rhs: F) -> F
func /<I: BinaryInteger, F: BinaryFloatingPoint>(lhs: F, rhs: I) -> F

Processes

Returns the current process ID:

func getpid() -> Int

Returns the host name:

func getHostName() -> String

Returns the username of the logged in user:

func getUserName() -> String

Gets or sets environment variables:

func getenv(_ key: String) -> String
func setenv(_ key: String, _ value: String)

Extensions on Sequence

Returns any sequence, with duplicates removed. Element must conform to Hashable:

func Sequence.unique() -> [Element]

Returns all the indexes where an element exists in a sequence. Element must conform to Equatable:

func Sequence.indexes(of searchItem: Element) -> [Int]

Returns true if any or none of the items in a sequence match a predicate:

func any(match predicate: (Element) throws -> Bool) rethrows -> Bool
func none(match predicate: (Element) throws -> Bool) rethrows -> Bool

Returns several random numbers from a sequence, up to the number requested:

Sequence.random(_ num: Int) -> [Element]

Extensions on String

The string as an array of lines:

var lines: [String]

An operator that lets us join strings together into a path:

static func / (lhs: String, rhs: String) -> String

Calculates the hash value of this String instance:

func md5() -> String
func sha1() -> String
func sha256() -> String

Converts the String instance to base 64 representation:

func base64() -> String

Writes a string to a file:

@discardableResult func write(to file: String) -> Bool

Replaces all instances of one string with another in the source String:

func replacing(_ search: String, with replacement: String) -> String
mutating func String.replace(_ search: String, with replacement: String)

Replaces count instances of one string with another in the source String:

func replacing(_ search: String, with replacement: String, count maxReplacements: Int) -> String
mutating func String.replace(_ search: String, with replacement: String, count maxReplacements: Int)

Trims characters from a string, whitespace by default:

mutating func trim(_ characters: String = " \t\n\r\0")
func String.trimmed(_ characters: String = " \t\n\r\0") -> String

Returns true if a string matches a regular expression, with optional extra options:

func matches(regex: String, options: NSRegularExpression.Options = []) -> Bool

Replaces matches for a regular expression with a replacement string:

replacing(regex: String, with replacement: String, options: NSString.CompareOptions) -> String
mutating func String.replace(regex: String, with replacement: String, options: NSString.CompareOptions)

Subscripts to let us read strings using integers and ranges:

subscript(idx: Int) -> String
subscript(range: Range<Int>) -> String
subscript(range: ClosedRange<Int>) -> String
subscript(range: CountablePartialRangeFrom<Int>) -> String
subscript(range: PartialRangeThrough<Int>) -> String
subscript(range: PartialRangeUpTo<Int>) -> String

Expands path components such as . and ~:

expandingPath() -> String

Creates a String instance by downloading from a URL or by reading a local file:

String.init(url: String)
String.init?(file: String)

Removes a prefix or suffix from a string, if it exists:

deletingPrefix(_ prefix: String) -> String
deletingSuffix(_ suffix: String) -> String

Adds a prefix or suffix to a string, if it doesn’t already have it:

func String.withPrefix(_ prefix: String) -> String
func String.withSuffix(_ suffix: String) -> String

System functionality

Many 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:

static var Brisk.haltOnError: Bool

Prints a message, or terminates the script if Brisk.haltOnError is true:

func printOrDie(_ message: String)

Terminates the program, printing a message and returning an error code to the system:

func exit(_ message: String = "", code: Int = 0) -> Never

If 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()):

func open(_ thing: String)

Extensions on URL

Add a string to a URL:

static func +(lhs: URL, rhs: String) -> URL
static func +=(lhs: inout URL, rhs: String)

XML parsing

Parses an instance of Data or String into an XML, or loads a file and does the same:

func parseXML(_ data: Data) -> XML.XMLNode?
func parseXML(_ string: String) -> XML.XMLNode?
func parseXML(from file: String) -> XML.XMLNode?

The resulting XMLNode has the following properties:

  • tag: The tag name used, e.g. <h1>.
  • data: The text inside the tag, e.g. <h1>This bit is the data</h1>
  • attributes: A dictionary containing the keys and values for all attributes.
  • childNodes: an array of XMLNode that belong to this node.

It also has a tiny subset of minidom functionality to make querying possible.

This finds all elements by a tag name, looking through all children, grandchildren, and so on:

func getElementsByTagName(_ name: String) -> [XMLNode]

This returns true if the current node has a specific attribute, or false otherwise:

func hasAttribute(_ name: String) -> Bool

This reads a single attribute, or sends back an empty string otherwise:

func getAttribute(_ name: String) -> String

Contribution guide

Any 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.

Some suggestions you might want to explore, ordered by usefulness:

  • Write some tests.
  • Contribute example scripts.
  • Add more helper functions.

What now?

This 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.

Some tips:

  1. 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.
  2. Stay away from macOS protected directories, such as your Desktop, Documents, and Photos.
  3. 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.
  4. 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.

Credits

Brisk 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.

Swift, the Swift logo, and Xcode are trademarks of Apple Inc., registered in the U.S. and other countries.

If you find Brisk useful, you might find my website full of Swift tutorials equally useful: Hacking with Swift.

More Repositories

1

ControlRoom

A macOS app to control the Xcode Simulator.
Swift
5,610
star
2

HackingWithSwift

The project source code for hackingwithswift.com
Swift
5,607
star
3

Unwrap

Learn Swift interactively on your iPhone.
Swift
2,265
star
4

Inferno

Metal shaders for SwiftUI.
Metal
2,238
star
5

wwdc

WWDC Community: Learning and sharing together
1,972
star
6

Sitrep

A source code analyzer for Swift projects.
Swift
1,294
star
7

CodeScanner

A SwiftUI view that is able to scan barcodes, QR codes, and more, and send back what was found.
Swift
930
star
8

Vortex

High-performance particle effects for SwiftUI.
Swift
910
star
9

whats-new-in-swift-5-0

An Xcode playground that demonstrates the new features introduced in Swift 5.0.
Swift
731
star
10

Ignite

A static site generator for Swift developers.
Swift
707
star
11

Sourceful

A syntax highlighting source editor for iOS and macOS using UITextView and NSTextView.
Swift
683
star
12

ShaderKit

A library of fragment shaders you can use in any SpriteKit project.
GLSL
655
star
13

SwiftOnSundays

Completed projects for the Swift on Sundays livestream series
Swift
635
star
14

simple-swiftui

A collection of small SwiftUI sample projects.
Swift
634
star
15

SwiftGD

A simple Swift wrapper for libgd
Swift
448
star
16

VisualEffects

A semi-official SwiftUI wrapper for UIVisualEffectView
Swift
354
star
17

whats-new-in-swift-5-5

Swift
312
star
18

Subsonic

A small library that makes it easier to play audio with SwiftUI.
Swift
308
star
19

swiftui-changelog

A repository to track changes in the SwiftUI generated interface.
Swift
258
star
20

macOS

The project source code for Hacking with macOS.
Swift
239
star
21

whats-new-in-swift-4-1

An Xcode playground that demonstrates the new features introduced in Swift 4.1.
Swift
221
star
22

TapStore

Code for a YouTube video on UICollectionView.
Swift
162
star
23

NeumorphismSwiftUI

Code to accompany my article on this topic.
Swift
139
star
24

whats-new-in-swift-5-3

An Xcode playground that demonstrates the new features introduced in Swift 5.3.
Swift
127
star
25

iDine

Source code for my SwiftUI introduction tutorial.
Swift
126
star
26

whats-new-in-swift-5-1

An Xcode playground that demonstrates the new features introduced in Swift 5.1.
Swift
121
star
27

whats-new-in-swift-5-8

Swift
120
star
28

whats-new-in-swift-4-2

An Xcode playground that demonstrates the new features introduced in Swift 4.2.
Swift
116
star
29

Sharpshooter

A tiny Xcode extension for people who debug with print().
Swift
115
star
30

whats-new-in-swift-5-7

Swift
115
star
31

100

A list of solutions for the 100 Days of Swift challenge
113
star
32

whats-new-in-swift-5-4

Swift
100
star
33

HWSTranslation

A community project to translate free Swift tutorials
99
star
34

watchOS

The project source code for Hacking with watchOS.
Swift
98
star
35

vapor-clean

A Vapor 3 template with no additional cruft.
Swift
97
star
36

whats-new-in-swift-5-2

An Xcode playground that demonstrates the new features introduced in Swift 5.2.
Swift
95
star
37

AppleTime

A tiny program to use 9:41 in your iOS simulators.
Swift
92
star
38

whats-new-in-swift-5-6

Swift
91
star
39

HackingWithReact

The project source code for hackingwithreact.com
JavaScript
72
star
40

iTour

Source code for my SwiftData introduction tutorial.
Swift
67
star
41

Placeholder

Place temporary images in your iOS app showing the size of the available space.
Swift
56
star
42

Trekr

Companion code for a YouTube livestream.
Swift
54
star
43

FaceFacts

Source code for my SwiftUI + SwiftData tutorial.
Swift
51
star
44

Markdown

A small and fast Markdown parser library for Swift.
Swift
44
star
45

SwiftOverCoffee

Links to solutions for Swift over Coffee challenges
39
star
46

SwiftSlug

A simple package to convert strings to URL slugs.
Swift
38
star
47

Playmaker

Create Xcode playgrounds from Markdown.
Swift
35
star
48

IgniteStarter

A starter template for the Ignite static site generator.
Swift
33
star
49

tvOS

The project source code for Hacking with tvOS.
Swift
30
star
50

IgniteSamples

Sample code for the Ignite static site generator.
Swift
23
star
51

kitura-vs-vapor

A side-by-side comparison of two popular server-side Swift frameworks.
Swift
22
star
52

Cgd

A small Swift package exposing libgd to Swift.
Swift
20
star
53

super-powered-string-interpolation

Swift
18
star
54

SamplePackage

A test package for Swift Package Manager.
Swift
18
star
55

betterbeeb

Better Beeb
Swift
16
star
56

HowToInstrument

A deliberately broken app to help demonstrate Instruments.
Swift
14
star
57

BioBlitz

Code created during my birthday livestream 2022.
Swift
14
star
58

Paraphrase

A trivial app for storing and viewing famous quotes
Swift
12
star
59

DadJokes

The code from my try! Swift NYC 2019 talk.
Swift
10
star
60

homebrew-brew

Homebrew formulae.
Ruby
8
star
61

whats-new-in-swift-6-0

Swift
6
star
62

Paraphrase-Improved

Swift
4
star
63

switcharoo

Switcharoo
Python
4
star
64

easyoc

EasyOC
Objective-C
4
star
65

whats-new-in-swift-5-10

Swift
1
star