Writing Swift scripts is easy:
$ cat <<EOF > script
#!/usr/bin/swift
print("Hi!")
EOF
$ chmod u+x script
$ ./script
Hi!
Sadly, to use third-party dependencies we have to migrate our script to a swift
package and use swift build
, a relatively heavy solution when all we wanted
was to whip up a quick script. swift-sh
gives us the best of both worlds:
$ cat <<EOF > script
#!/usr/bin/swift sh
import PromiseKit // @mxcl ~> 6.5
print(Promise.value("Hi!"))
EOF
$ chmod u+x script
$ ./script
Promise("Hi!")
In case it’s not clear, swift-sh
reads the comment after the import
and
uses this information to fetch your dependencies.
Let’s work through an example: if you had a single file called foo.swift
and you wanted to import mxcl/PromiseKit:
#!/usr/bin/swift sh
import Foundation
import PromiseKit // @mxcl ~> 6.5
firstly {
after(.seconds(2))
}.then {
after(.milliseconds(500))
}.done {
print("notice: two and a half seconds elapsed")
exit(0)
}
RunLoop.main.run()
You could run it with:
$ swift sh foo.swift
Or to make it more “scripty”, first make it executable:
$ chmod u+x foo.swift
$ mv foo.swift foo # optional step!
And then run it directly:
$ ./foo
Hey there, I’m Max Howell. I’m a prolific producer of open source software and
probably you already use some of it (for example, I created brew
). I work
full-time on open source and it’s hard; currently I earn less than minimum
wage. Please help me continue my work, I appreciate it 🙏🏻
brew install mxcl/made/swift-sh
Or with Mint:
mint install mxcl/swift-sh
Or you can build manually using swift build
.
Installation results in a single executable called swift-sh
, the swift
executable will call this (provided it is in your PATH
) when you type:
swift sh
.
We actively support both Linux and Mac and will support Windows as soon as it is possible to do so.
Add the shebang as the first line in your script: #!/usr/bin/swift sh
.
Your dependencies are determined via your import
lines:
#!/usr/bin/swift sh
import AppUpdater // @mxcl
// ^^ https://github.com/mxcl/AppUpdater, latest version
import PromiseKit // @mxcl ~> 6.5
// ^^ mxcl/PromiseKit, version 6.5.0 or higher up to but not including 7.0.0 or higher
import Chalk // @mxcl == 0.3.1
// ^^ mxcl/Chalk, only version 0.3.1
import LegibleError // @mxcl == b4de8c12
// ^^ mxcl/LegibleError, the precise commit `b4de8c12`
import Path // mxcl/Path.swift ~> 0.16
// ^^ for when the module-name and repo-name are not identical
import BumbleButt // https://example.com/bb.git ~> 9
// ^^ non-GitHub URLs are fine
import CommonTaDa // [email protected]:mxcl/tada.git ~> 1
// ^^ ssh URLs are fine
import TaDa // ssh://[email protected]:mxcl/tada.git ~> 1
// ^^ this style of ssh URL is also fine
import Foo // ./my/project
import Bar // ../my/other/project
import Baz // ~/my/other/other/project
import Fuz // /I/have/many/projects
// ^^ local dependencies must expose library products in their `Package.swift`
// careful: `foo/bar` will be treated as a GitHub dependency; prefix with `./`
// local dependencies do *not* need to be versioned
import Floibles // @mxcl ~> 1.0.0-alpha.1
import Bloibles // @mxcl == 1.0.0-alpha.1
// ^^ alphas/betas will only be fetched if you specify them explicitly like so
// this is per Semantic Versioning guidelines
swift-sh
reads the comments after your imports and fetches the requested
SwiftPM dependencies.
It is not necessary to add a comment specification for transitive dependencies.
The following will generate an Xcode project (not in the working directory, we keep it out the way in our cache directory) and open it, edits are saved to your script file.
$ swift sh edit ./myScript
Simple scripts can quickly become bigger projects that would benefit from being
packages that you build with SwiftPM. To help you migrate your project we
provide swift sh eject
, for example:
$ swift sh eject foo.swift
creates a Swift package in ./Foo
, from now on use swift build
in the
Foo
directory. Your script is now ./Foo/Sources/main.swift
.
If you want to make scripts available to people using CI; use stdin
:
brew install mxcl/made/swift-sh
swift sh <(curl http://example.com/yourscript) arg1 arg2
swift sh
creates a Swift Package.swift
configured to fetch your dependencies
and build a single executable for your script in ~/Library/Developer/swift-sh.cache
†,
the script is then executed via swift run
.
† We use the FreeDesktop specified cache location on Linux.
swift-sh
uses the active tools version, (ie: xcode-select
) or whichever
Swift is first in the PATH
on Linux. It writes a manifest for the package
it will swift build
with that tools-version. Thus Xcode 10.1 builds a script
with Swift 4.2, Xcode 10.2 builds with Swift 5. Dependencies build with the
Swift versions they declare support for, provided the active toolchain can do
that (eg. Xcode 10.1 supports Swift 3.4, 4.0 and 4.2, Xcode 10.2 supports
Swift 4.0, 4.2 and 5.0).
To declare a support for specific Swift versions in your script itself, use
#if swift
or #if compiler
directives.
error: unable to invoke subcommand: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-sh
If you got here via Google, you have a script that uses this tool, if you now
install swift-sh
, you will be able to run your script:
brew install mxcl/made/swift-sh
Or see the above installation instructions.