17

I am trying to write a script with Swift (not an Xcode project). To be clear, the first line of my file is

 #!/usr/bin/swift

And I am just calling it from the command-line.

However, I can't figure out how to have that script use code that is in another .swift file. It doesn't pick it up from the same directory and there is no way to import that I can see.

Is this supported?

Lou Franco
  • 87,846
  • 14
  • 132
  • 192
  • 1
    Related question here http://stackoverflow.com/questions/27872589/swift-repl-how-to-import-load-evaluate-or-require-a-swift-file. – I *assume* that it is just not possible yet. – Martin R Jan 21 '15 at 14:14
  • I guess it must be possible since the XCode can do it (by passing the right parameters to the compiler). But obviously nobody figured it out so far. In Playground for example you can include multiple files too (there's an answer somewhere on SO). But again this is within XCode. – qwerty_so Jan 21 '15 at 14:47
  • If I wanted to compile, I could do it with `swiftc -o ./myscript main.swift b.swift` -- which might be the only solution (I can write a bash around it -- ugh) – Lou Franco Jan 21 '15 at 20:43
  • 1
    This is slightly better. For me, I have a few swift files that won't change much and one that is more of a script. So, I can `swiftc -emit-library -olibutils.dylib a.swift b.swift c.swift ` and then put `#!/usr/bin/swift -I . -lutils` at the top of my script. Neither of these options are an answer, but perhaps this will help someone – Lou Franco Jan 21 '15 at 20:57

4 Answers4

23

I use a variant of Marián's solution:

cat A.swift B.swift main.swift | swift -
Mario Zannone
  • 2,843
  • 12
  • 18
4

My current solution is a simple shell script that concatenates all the files into one and executes the concatenated file:

TMPFILE=`mktemp /tmp/Project.swift.XXXXXX` || exit 1
trap "rm -f $TMPFILE" EXIT 
cat *.swift > $TMPFILE
swift $TMPFILE
Marián Černý
  • 15,096
  • 4
  • 70
  • 83
4

There's a better way!

#!/usr/bin/swift -frontend -interpret -enable-source-import -I.

import other_file  // this imports other_file.swift in the same folder

funcFromOtherFile()

if you want to import files from ExampleFolder it would be like:

#!/usr/bin/swift -frontend -interpret -enable-source-import -I./ExampleFolder

import other_file  // this imports ./ExampleFolder/other_file.swift

funcFromOtherFile()
vol
  • 1,594
  • 11
  • 16
1

Inspired by this answer, I've written a simple Python script to replicate a gcc-esque compilation process. You can find the gist here.

The core idea is to (1) concat all Swift files into one, (2) add some boilerplate code if applicable, and then (3) write the joined text to a temporary file and have Swift execute that file.

#!/usr/bin/env python3

"""
Simple bootstrap script for quickly prototyping
Swift-based macOS applications.

usage: swift-run.py [-h] [-c SWIFT_COMPILER] [-a ARGS] [-m MAIN] [-s SAVE]
                    file [file ...]

positional arguments:
  file                  list of files to run

optional arguments:
  -h, --help            show this help message and exit
  -c SWIFT_COMPILER, --swift-compiler SWIFT_COMPILER
                        swift compiler path (default: swift)
  -a ARGS, --args ARGS  swift compiler args (default: )
  -m MAIN, --main MAIN  main SwiftUI view (default:
                        ContentView().frame(maxWidth: .infinity, maxHeight:
                        .infinity))
  -s SAVE, --save SAVE  saves the joined swift files + boilerplate to this
                        file (default: )

"""

import argparse
import os
import subprocess
import tempfile
from typing import List

DEFAULTS = {
    "main_view": "ContentView().frame(maxWidth: .infinity, maxHeight: .infinity)",
    "swift_args": "",
    "swift_exec": "swift",
}


def join_files(files: List[str], boilerplate: str = ""):
    all_text = ""

    for file in files:
        with open(file, "r") as f:
            all_text += f.read() + "\n\n"

    all_text += boilerplate
    return all_text


def exec_text(text: str, swift_exec: str = "", swift_args: str = "", script_out: str = None):
    with tempfile.TemporaryDirectory() as tmp_dir:
        if script_out is None or script_out.strip() == "":
            script_out = os.path.join(tmp_dir, "main.swift")
        
        with open(script_out, "w") as f:
            f.write(text)

        cmd = f"{swift_exec} {swift_args} {script_out}"
        print(cmd)

        subprocess.run(
            cmd.split(),
            check=True,
            capture_output=True,
        )


def get_boilerplate(main_view: str = "ContentView()"):
    return (
        """
// Run any SwiftUI view as a Mac app.

import Cocoa
import SwiftUI

NSApplication.shared.run {
    """
        + main_view
        + """
}

extension NSApplication {
    public func run<V: View>(@ViewBuilder view: () -> V) {
        let appDelegate = AppDelegate(view())
        NSApp.setActivationPolicy(.regular)
        mainMenu = customMenu
        delegate = appDelegate
        run()
    }
}

// Inspired by https://www.cocoawithlove.com/2010/09/minimalist-cocoa-programming.html

extension NSApplication {
    var customMenu: NSMenu {
        let appMenu = NSMenuItem()
        appMenu.submenu = NSMenu()
        let appName = ProcessInfo.processInfo.processName
        appMenu.submenu?.addItem(NSMenuItem(title: "About \(appName)", action: #selector(NSApplication.orderFrontStandardAboutPanel(_:)), keyEquivalent: ""))
        appMenu.submenu?.addItem(NSMenuItem.separator())
        let services = NSMenuItem(title: "Services", action: nil, keyEquivalent: "")
        self.servicesMenu = NSMenu()
        services.submenu = self.servicesMenu
        appMenu.submenu?.addItem(services)
        appMenu.submenu?.addItem(NSMenuItem.separator())
        appMenu.submenu?.addItem(NSMenuItem(title: "Hide \(appName)", action: #selector(NSApplication.hide(_:)), keyEquivalent: "h"))
        let hideOthers = NSMenuItem(title: "Hide Others", action: #selector(NSApplication.hideOtherApplications(_:)), keyEquivalent: "h")
        hideOthers.keyEquivalentModifierMask = [.command, .option]
        appMenu.submenu?.addItem(hideOthers)
        appMenu.submenu?.addItem(NSMenuItem(title: "Show All", action: #selector(NSApplication.unhideAllApplications(_:)), keyEquivalent: ""))
        appMenu.submenu?.addItem(NSMenuItem.separator())
        appMenu.submenu?.addItem(NSMenuItem(title: "Quit \(appName)", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q"))
        
        let windowMenu = NSMenuItem()
        windowMenu.submenu = NSMenu(title: "Window")
        windowMenu.submenu?.addItem(NSMenuItem(title: "Minmize", action: #selector(NSWindow.miniaturize(_:)), keyEquivalent: "m"))
        windowMenu.submenu?.addItem(NSMenuItem(title: "Zoom", action: #selector(NSWindow.performZoom(_:)), keyEquivalent: ""))
        windowMenu.submenu?.addItem(NSMenuItem.separator())
        windowMenu.submenu?.addItem(NSMenuItem(title: "Show All", action: #selector(NSApplication.arrangeInFront(_:)), keyEquivalent: "m"))
        
        let mainMenu = NSMenu(title: "Main Menu")
        mainMenu.addItem(appMenu)
        mainMenu.addItem(windowMenu)
        return mainMenu
    }
}

class AppDelegate<V: View>: NSObject, NSApplicationDelegate, NSWindowDelegate {
    init(_ contentView: V) {
        self.contentView = contentView
        
    }
    var window: NSWindow!
    var hostingView: NSView?
    var contentView: V
    
    func applicationDidFinishLaunching(_ notification: Notification) {
        window = NSWindow(
            contentRect: NSRect(x: 0, y: 0, width: 480, height: 300),
            styleMask: [.titled, .closable, .miniaturizable, .resizable, .fullSizeContentView],
            backing: .buffered, defer: false)
        window.center()
        window.setFrameAutosaveName("Main Window")
        hostingView = NSHostingView(rootView: contentView)
        window.contentView = hostingView
        window.makeKeyAndOrderFront(nil)
        window.delegate = self
        NSApp.activate(ignoringOtherApps: true)
    }
}
"""
    )


if __name__ == "__main__":
    parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter)
    parser.add_argument(
        "files", metavar="file", type=str, nargs="+", help="list of files to run"
    )
    parser.add_argument(
        "-c",
        "--swift-compiler",
        help="swift compiler path",
        type=str,
        default=DEFAULTS["swift_exec"],
    )
    parser.add_argument(
        "-a",
        "--args",
        help="swift compiler args",
        type=str,
        default=DEFAULTS["swift_args"],
    )
    parser.add_argument(
        "-m",
        "--main",
        help="main SwiftUI view",
        type=str,
        default=DEFAULTS["main_view"],
    )
    parser.add_argument(
        "-s",
        "--save",
        help="saves the joined swift files + boilerplate to this file",
        type=str,
        default="",
    )

    args = parser.parse_args()
    print(args)

    exec_text(
        join_files(args.files, boilerplate=get_boilerplate(args.main)),
        swift_exec=args.swift_compiler,
        swift_args=args.args,
        script_out=args.save
    )
Daniel R. Livingston
  • 1,227
  • 14
  • 36