Quantcast
Channel: NSHipster
Viewing all articles
Browse latest Browse all 382

swift-sh

$
0
0

Swift is a fast, safe, modern programming language with an open governance model and a vibrant community. There’s no reason that it should be limited to just making apps. And indeed, many smart people are working hard to bring Swift to new platforms and evolve its capabilities for web development and machine learning.

Scripting is another point of interest for the Swift community, but the amount of setup required to incorporate 3rd-party dependencies has long been seen as a non-starter.

…that is, until now.

On Friday, Max Howellannounced a new project called swift-sh. The project provides a shim around the Swift compiler that creates a package for your script and uses it to automatically add and manage external dependencies imported with a special trailing comment.

Although in its initial stages of development, it’s already seemingly managed to solve the biggest obstacle to Swift from becoming a productive scripting language.

This week, let’s take a very early look at this promising new library and learn how to start using it today to write Swift scripts.

Installation and Setup

Assuming you have Swift and the Xcode Developer Tools on your system, you can install swift-sh with Homebrew using the following command:

$ brew install mxcl/made/swift-sh
        

Example Usage

The original Swift Package Manager examples provided a shuffleableDeck of PlayingCard values. So it feels appropriate to revisit them here. For this example, let’s build a Swift script to deal and print out a formatted representation of a hand of Bridge. The final result should look something like this:

♠ 10 9 8 7
♥ 6 5 4 3
♦ —
♣ 7 6 5 3 2
♠ 6 5 4 3 2

M

Meyer               Drax

Bond

♠ A K Q J
♥ 10 9 8 7 2♥ A K Q J
♦ J 10 2♦ A K
♣ —♣ K J 9
♠ —
♥ —
♦ Q 8 7 6 5 4 3 2
♣ A Q 10 8 4

Importing Dependencies

Start by creating a new .swift file with a shebang at the beginning (more on that later).

$echo"#!/usr/bin/swift sh"> bridge.swift
        

Next, add import declarations for three modules: DeckOfPlayingCards, PlayingCard, and Cycle.

importDeckOfPlayingCards// @NSHipster ~> 4.0.0importPlayingCardimportCycle// @NSHipster == bb11e28

The comment after the import declaration for DeckOfPlayingCards tells swift-sh to look for the package on GitHub in a repository by the same name under the NSHipster username. The tilde-greater-than operator in ~> 4.0.0 is shorthand for specifying a version “equal to or greater than in the least significant digit” according to Semantic Versioning conventions. In this case, Swift Package Manager will use the latest release whose major is equal to 4 and minor release is equal 0 (that is, it will use 4.0.1 or4.0.0, but not 4.1.0 or 5.0.0).

For the PlayingCard module, we don’t have to add an import specification for because it’s already included as a dependency of DeckOfPlayingCards.

Finally, the Cycle module is an external package and includes an external import specification that tells swift-sh how to add it as a dependency. The notable difference here is that the == operator is used to specify a revision rather than a tagged release.

Enlisting Players

With all of our dependencies accounted for, we have everything we need to write our script.

First, create a Player class, consisting of name and hand properties.

classPlayer{varname:Stringvarhand:[PlayingCard]=[]init(name:String){self.name=name}}

In an extension, conform Player to CustomStringConvertible and implement the description property, taking advantage of the convenient Dictionary(grouping:by:) initializer added to Swift 4. By convention, a player’s hand is grouped by suit and ordered by rank.

extensionPlayer:CustomStringConvertible{vardescription:String{vardescription="\(name):"letcardsBySuit=Dictionary(grouping:hand){$0.suit}for(suit,cards)incardsBySuit.sorted(by:{$0.0>$1.0}){description+="\t\(suit)"description+=cards.sorted(by:>).map{"\($0.rank)"}.joined(separator:"")description+="\n"}returndescription}}

Shuffling and Dealing the Deck

Create and shuffle a standard deck of 52 playing cards, and initialize players at each of the four cardinal directions.

vardeck=Deck.standard52CardDeck()deck.shuffle()varnorth=Player(name:"North")varwest=Player(name:"West")vareast=Player(name:"East")varsouth=Player(name:"South")

In Bridge, cards are dealt one-by-one to each player until no cards remain. We use the cycled() method to rotate between each of our players to ensure an even and fair deal.

letplayers=[north,east,west,south]varround=players.cycled()whileletcard=deck.deal(),letplayer=round.next(){player.hand.append(card)}

After the cards are dealt, each player has 13 cards.

forplayerinplayers{print(player)}

Running the Card Game

We can run our completed Swift script from the command line by passing the file as an argument to the swift sh subcommand.

$ swift sh ./bridge.swift
        North:	♠︎ K 10 9 8 5 4
        ♡ K 3
        ♢ A 10
        ♣︎ A 4 2
        West:	♠︎ J 3 2
        ♡ 9 6 5
        ♢ Q 9 8 3 2
        ♣︎ 6 5
        East:	♠︎ Q 7
        ♡ A J 10 7 4
        ♢ K 5 4
        ♣︎ 10 7 3
        South:	♠︎ A 6
        ♡ Q 8 2
        ♢ J 7 6
        ♣︎ K Q J 9 8
        

Smashing!

Making an Executable

On Unix systems, a shebang (#!) indicates how a script should be interpreted. In our case, the shebang line at the top of bridge.swift tells the system to run the file using the sh subcommand of the swift command (/usr/bin/swift):

#!/usr/bin/swift sh

Doing so allows you to take the extra step to make a Swift script look and act just like a binary executable. Use mv to strip the .swift extension and chmod to add executable (+x) permissions.

$mv bridge.swift bridge
        $chmod +x bridge
        $ ./bridge
        

Current Limitations

As a very early release, it’s expected for there to be a few rough edges and missing features. Here are some important details to keep in mind as you’re getting started with swift-sh:

Dependency on GitHub

Imported dependencies can correspond to only GitHub repositories. There’s currently no way to specify other remote or local locations. We expect this to be added in a future release.

// 🔮 Possible Future SyntaximportRemote// git://example.com/Remote.gitimportLocal// ~/path/to/Local.git

Module Names Must Match Repository Names

swift-sh requires module names to match the name of their repository. In many cases, this isn’t a problem because projects typically have descriptive names. However, in our example, the DeckOfPlayingCards module was provided by the repository apple/example-package-deckofplayingcards.

You might expect the following syntax to be supported, but this doesn’t work yet:

// 🔮 Possible Future SyntaximportDeckOfPlayingCards// @apple/example-package-deckofplayingcards

Until such an affordance is provided, the easiest workaround is to fork the existing repository and rename your fork (as we did with @NSHipster/DeckOfPlayingCards).

Lack of Support for Import Declaration Syntax

As described in last week’s article, Swift provides special syntax for importing individual declarations from external modules.

In our example, we import the Cycle package to access its cycle() function, which is used to iterate over the players during the initial deal repeatedly.

In a conventional Swift package setup, we could import that function only. However, that syntax isn’t yet supported by swift-sh.

// 🔮 Possible Future SyntaximportfuncCycle.cycle()// @NSHipster/Cycle

Until full support for import declaration syntax is added, you’ll only be able to import external modules in their entirety.


Given the importance of this functionality, we think swift-sh is destined to become part of the language. As momentum and excitement build around this project, keep an eye out in the Swift forums for proposals to incorporate this as a language feature in a future release.


Viewing all articles
Browse latest Browse all 382

Trending Articles