Git Product home page Git Product logo

swift-sh's Introduction

swift sh badge-platforms badge-languages Build Status

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

Sponsorship

If your company depends on swift-sh please consider sponsoring the project. Otherwise it is hard for me to justify maintaining it.

Installation

brew install 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.

Usage

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.

Editing in Xcode

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

Examples

Converting your script to a package

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.

Use in CI

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

Internal Details

swift sh creates a Swift Package.swift package manager project with dependencies in a directory below the swift-sh cache directory †, builds the executable, and then executes it via swift run.
The script is (only) rebuilt when the script file is newer than the executable.

† Specify the cache parent directory using the (FreeDesktop) environment variable XDG_CACHE_HOME. If unspecified, on macOS swif-sh uses $HOME/Library/Developer/swift-sh.cache, and otherwise it uses $HOME/.cache/swift-sh.

Swift Versions

swfit-sh v2 requires Swift 5.1. We had to drop support for Swift v4.2 because maintenance was just too tricky.

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 11.0 builds with Swift 5.1. Dependencies build with the Swift versions they declare support for, provided the active toolchain can do that (eg. Xcode 11.0 supports Swift 4.2 and above)

To declare a support for specific Swift versions in your script itself, use #if swift or #if compiler directives.

Alternatives


Troubleshooting

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.

swift-sh's People

Contributors

adam-fowler avatar algal avatar atdrendel avatar djtech42 avatar frizlab avatar goranmoomin avatar haikusw avatar helje5 avatar jasonzurita avatar jboulter11 avatar josh avatar jsorge avatar kabouzeid avatar mariusciocanel avatar mxcl avatar purpleblues avatar repo-ranger[bot] avatar segabor avatar waynezhang avatar wti avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

swift-sh's Issues

readLine() does not return

I have this simple script which should get user input using readLine():

print("Enter name:")
if let name = readLine() {
    print("Entered \(name)")
}

When I run it using plain Swift (swift script.swift) it works as expected, but when I run it using swift-sh it hangs on readLine() despite I type a string and hit Enter.

Executing swift build;swift run in ~/Library/Developer/swift-sh.cache/scriptName (which, as I understand, is what swift-sh does behind the scene) it works as expected (read and prints the string).

Am I missing something?

CI/Deployment/Actions

  • Update Linux tests on push
  • Update Homebrew formula on release
  • Only run Travis for releases, PRs and merge commits

Feature Request: Editor/IDE/LSP support

When working on a swift-sh script in VS Code with sourcekit-lsp running, I get a few "No module found" errors for the imports intended to be resolved by swift-sh. I'm assuming this is because the packages don't get fetched/cached until the script is first run and/or because sourcekit-lsp doesn't know where the cache is located, in order to pull in module interfaces.

Would it be possible to automate fetching, caching, and pointing sourcekit-lsp to cached packages as part of swift-sh? If not, would it be possible to do it with an external VS Code extension?

Cannot install swift-sh on Arch Linux

Brew install fails with error missingLinuxMain. Here's the console log

[segabor@csihuhu domain11]$ brew install mxcl/made/swift-sh
==> Tapping mxcl/made
Cloning into '/home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/mxcl/homebrew-made'...
remote: Enumerating objects: 3, done.
remote: Counting objects: 100% (3/3), done.
remote: Compressing objects: 100% (2/2), done.
remote: Total 3 (delta 0), reused 2 (delta 0), pack-reused 0
Unpacking objects: 100% (3/3), done.
Tapped 1 formula (27 files, 21.9KB).
==> Installing swift-sh from mxcl/made
==> Downloading https://github.com/mxcl/swift-sh/archive/1.1.3.zip
==> Downloading from https://codeload.github.com/mxcl/swift-sh/zip/1.1.3
#=#=-#  #                                                                     
==> swift build --configuration release -Xswiftc -static-stdlib --disable-sandbox
Last 15 lines from /home/segabor/.cache/Homebrew/Logs/swift-sh/01.swift:
2019-01-14 14:04:48 +0100

swift
build
--configuration
release
-Xswiftc
-static-stdlib
--disable-sandbox

error: missingLinuxMain

If reporting this issue please do so at (not Homebrew/brew or Homebrew/core):
https://github.com/mxcl/homebrew-made/issues

Precisely-specified version dependencies don't work

I may be holding it wrong, but I tried updating the example in the README to use a precisely specified version number, by defining a script like so:

#!/usr/bin/swift sh

import Foundation
import PromiseKit  // @mxcl == 6.8.3

firstly {
    after(.seconds(2))
}.then {
    after(.milliseconds(500))
}.done {
    print("notice: two and a half seconds elapsed")
    exit(0)
}

RunLoop.main.run()

I get the following error:

⋊> ~/w/m/S/r/GetUserInfo swift sh foo.swift
/Users/alexis/Library/Developer/swift-sh.cache/foo: error: manifest parse error(s):
/Users/alexis/Library/Developer/swift-sh.cache/foo/Package.swift:10:6: error: ambiguous reference to member 'package'
    .package(url: "https://github.com/mxcl/PromiseKit.git", .exactItem(Version(6,8,3)))
     ^      ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/Users/alexis/Library/Developer/swift-sh.cache/foo/Package.swift:10:6: note: overloads for 'package' exist with these partially matching parameter lists: (url: String, Package.Dependency.Requirement), (url: String, Range<Version>), (url: String, ClosedRange<Version>)
    .package(url: "https://github.com/mxcl/PromiseKit.git", .exactItem(Version(6,8,3)))
     ^
error: /usr/bin/swift ["build", "-Xswiftc", "-suppress-warnings"]: 1: <nil> <nil>

Test case breaks due to unsupported methods on Linux

swift test command halts with error message

Test Case 'IntegrationTests.testConventional' started at 2019-01-14 16:46:47.176
Fatal error: init(for:) is not yet implemented: file Foundation/Bundle.swift, line 57

Can break with change of deps with same name

If you run this script:

import DeckOfPlayingCards  // apple/example-package-deckofplayingcards ~> 3.0.0
import PlayingCard
import Cycle  // @NSHipster == master

Then this:

import DeckOfPlayingCards // @NSHipster ~> 4.0.0
import PlayingCard
import Cycle // @NSHipster == bb11e28

It will fail because the Package.resolved contains the old package url, and doesn't update. Could be a bug in SwiftPM, but either way we should figure out what to do about a workaround.

Compiling scripts transparently

I'm considering replacing lot of small bash scripts with swift scripts, but they're too slow to start:

$ time ./script
Promise(Hi!)

real	0m0.548s
user	0m0.358s
sys	0m0.173s

Running compiled ones is fast though:

$ time ./Script
Promise(Hi!)

real	0m0.016s
user	0m0.009s
sys	0m0.006s

I could hack a solution with 'swift sh eject'-ing scripts, building them then copying binaries to /usr/local/bin, but possibly this can be supported transparently by swift-sh? I.e. it can maintain a cache of compiled scripts and check if compiled version already exists and is not older that script file, and if so, run it instead. What do you think, is it possible / viable?

Cannot install swift-sh on Arch Linux, pt2

Hi,

Again me and Arch Linux. I wanted to install swift-sh from Linuxbrew and got the following error:

Linking ./.build/x86_64-unknown-linux/release/swift-sh
LLVM ERROR: /usr/lib/swift_static/linux/static-stdlib-args.lnk not found
error: terminated(1): /usr/bin/swift-build-tool -f /tmp/swift-sh-20190222-22402-1af9tp6/swift-sh-1.7.2/.build/release.yaml main output:

I'm using (almost) the latest Swift 5 snapshot.

Full log:

[segabor@csihuhu ~]$ brew install mxcl/made/swift-sh
==> Installing swift-sh from mxcl/made
==> Downloading https://github.com/mxcl/swift-sh/archive/1.7.2.tar.gz
Already downloaded: /home/segabor/.cache/Homebrew/downloads/6e6ddc26b019910682d073be3bfdfab18b17529e1a0ce345e34fe170e160880b--swift-sh-1.7.2.tar.gz
==> swift build --configuration release -Xswiftc -static-stdlib --disable-sandbox
Last 15 lines from /home/segabor/.cache/Homebrew/Logs/swift-sh/01.swift:
Compile Swift Module 'Version' (5 sources)
Compile Swift Module 'LegibleError' (1 sources)
Compile Swift Module 'Path' (9 sources)
/tmp/swift-sh-20190222-26390-1urcu0l/swift-sh-1.7.2/.build/checkouts/LegibleError-1953298975037841886/Sources/LegibleError.swift:54:30: warning: 'is' test is always true
        let isNSError = self is NSError
                             ^
Compile Swift Module 'Utility' (5 sources)
Compile Swift Module 'Script' (3 sources)
Compile Swift Module 'Command' (4 sources)
Compile Swift Module 'swift_sh' (1 sources)
Linking ./.build/x86_64-unknown-linux/release/swift-sh
LLVM ERROR: /usr/lib/swift_static/linux/static-stdlib-args.lnk not found
error: terminated(1): /usr/bin/swift-build-tool -f /tmp/swift-sh-20190222-26390-1urcu0l/swift-sh-1.7.2/.build/release.yaml main output:
    


If reporting this issue please do so at (not Homebrew/brew or Homebrew/core):
https://github.com/mxcl/homebrew-made/issues

Clean cache folders of specific scripts

Currently, running swift sh -C deletes the entire ~/Library/Developer/swift-sh.cache folder. Often, I find myself wanting to clear the cache for a single script without blowing away the caches of all of the other scripts on my system.

It would be great if swift sh -C could be extended to take the path to a script which, if provided, would be the only cache deleted.

scripts can clash

Most notably with stdin, if two are run that generate to the same cache location from eg. different terminals at the same time, it's a race condition.

swift-sh VS Beak VS Marathon

Hi:

Just discovered swift-sh, and I am going to be using it very soon to switch my build scripts from bash to swift. Thanks!
I was reading the readme, and also found Marathon and Beak. After reading their readme, seems to me that everything that swift-sh can do, also Beak or Marathon can. Plus Beak offers the command line interface for free.

So, in what is swift-sh different than Marathon or Beak?
Thanks!

Swift sh conflict with previous cache

I really like the swift sh edit myFile.swift, and use it quite a lot now.

But I got an issue:

$>swift sh edit MyFile.swift 
error: The file “main.swift” couldn’t be saved in the folder “MyFile” because a file with the same name already exists.

First, I'd like to know where is that cache.

Second, is there a way to indicate in the error, as a suggestion, where is located the cache so I can remove it manually (maybe also suggest a rm command with the correct path?

Proxy support?

Hi.

It doesn't seem to work behind the proxy. E.g. doesn't seem to respect HTTP(S)_PROXY/i vars configured in my shell session.

It looks like it's using swift build under the hood and I don't remember if that needs some flags to work with proxy.

Import local packages relative to the location of the script

Problem:

Scripts inside a git repository which share local packages can only be run from one directory.
If I have a directory structure like this:

git repo root
| - Scripts/
    | - script.swift
    | - MyPackage/

And I import MyPackage in script.swift like this:
import MyPackage // ./MyPackage

I can only run the script from the Scripts directory. I can't specify an absolute path because it's a git repository that might be checked out to any location.

Proposed Solution:

Paths should be able to be specified as relative to the script's file location rather than only allowing relative paths with respect to the current working directory.

  1. Allow local paths to be specified with some different syntax, perhaps like:
    import MyPackage // #/path/to/package
  2. Detect location of script being run via command line inputs
  3. Combine location of script and location of package when resolving local paths with relative-to-script syntax to produce the path relative to the current working directory for the same execution as the existing relative paths.
    ./Scripts/script.swift + #/path/to/package == ./Scripts/path/to/package

Questions:

  • Are you open to pull requests for this functionality?
  • What should the syntax be exactly? I'm not in love with the # prefix, can we think of something better? ... could be an option?
  • Do we indeed get the location of the script relative to the working directory in the command line arguments, or does some other method need to be devised?

stdin is read and evaluated from script if stdin isn’t TTY even when invoking swift-sh -h

I’m not sure if this is intended or not, but swift-sh tries to read stdin even with the flags -h when stdin isn’t a TTY. I’m not sure if this is intended, but AFAIU -- -h does the same thing, right? I believe that just printing the help message would be more consistent and less surprising.

For context, I’m trying to add swift-sh on the Homebrew core tap, and it looks like the test (which is just checking the help message) is failing because the stdin isn’t a TTY in the testing environment so it tries to read from it, and (due to reasons I don’t know) errors due to the sandbox and returns 2.

I would like the formula included in the main tap, so I would like if you can consider to remove the conditional behavior. I also can open a (trivial) PR removing the conditional if it’s preferable.

Thanks.

Translate `.` in user/org names to `-`

When you create an organisation in GitHub, you can use a . in the name, say: Macro.swift. GitHub translates that to a -.
Would be cool if swift-sh would allow that as well. Currently it gives a:

error: The operation couldn’t be completed. (E.invalidGitHubUsername("Macro.swift"))

crontab: error unable to invoke subcommand

  1. create run.sh with content:

#!/bin/bash
./test.swift asdf.json

and create test.swift with content

#!/usr/bin/swift sh
print("test", CommandLine.arguments)

  1. exec crontab -e

  2. insert */1 * * * * cd ~/Projects/my-project-name && ./run.sh

  3. exec less /var/mail/my_user_name

See error:
error: unable to invoke subcommand: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-sh (No such file or directory)

Import from private repos (via ssh)

I want to start off saying thank you for building swift-sh and all the awesome work you're doing.
Recently I started looking into the Swift Package Manager and then discovered swift-sh.
Eager to try swift-sh out I made a private library installable via SPM and tried importing it in a script using the following syntax:

import Bar  // ssh://[email protected]:mxcl/swift-sh.git ~> 1.0

This format isn't currently supported. Is this something you'd be interested in being included in swift-sh?

I started looking at the regex pattern that parses the import line but I'm not familiar with Regex.

I'll continue to look into it but wanted to raise this issue in case you have any other ideas on how to proceed

Cannot save files in `swift sh edit` generated xcodeproj

Xcode doesn’t work with symlinked files, if you edit and try to save it complains that the file “doesn’t exist”…!

  • Symlinking a directory does work, but we cannot generate an executable unless the file is main.swift which it won’t be if we symlink the folder.
  • We could run swift as an interpreter. Not sure how well this will work inside Xcode though.
  • Hard links don't work due to Xcode saving atomically
  • Aliases don't work (long shot)
  • Could add a build phase which syncs changes back to the original script (not idiomatic since saves require a build execution).

More complicated solution would be to watch a copy of the file and sync when it changes, but this is a failure-prone choice.

Caches directory?

Great initiative, I love it!

Just for your consideration: on macOS it feels more logical to me to use .cachesDirectory instead of .itemReplacementDirectory for the location of the downloaded dependencies.

Thanks!!

Example file does not compile

test.swift:

#!/usr/bin/swift sh

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

After running chmod +x test.swift, running it with ./test.swift results in these errors:

Compile Swift Module 'test' (1 sources)
/Users/dan/Library/Developer/swift-sh.cache/test/main.swift:10:5: error: use of unresolved identifier 'exit'
    exit(0)
    ^~~~
/Users/dan/Library/Developer/swift-sh.cache/test/main.swift:13:1: error: use of unresolved identifier 'RunLoop'
RunLoop.main.run()
^~~~~~~
error: terminated(1): /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/swift-build-tool -f /Users/dan/Library/Developer/swift-sh.cache/test/.build/debug.yaml test.exe output:
    Compile Swift Module 'test' (1 sources)
    /Users/dan/Library/Developer/swift-sh.cache/test/main.swift:10:5: error: use of unresolved identifier 'exit'
        exit(0)
        ^~~~
    /Users/dan/Library/Developer/swift-sh.cache/test/main.swift:13:1: error: use of unresolved identifier 'RunLoop'
    RunLoop.main.run()
    ^~~~~~~


Fatal error: Error raised at top level: (extension in Shwifty):__C.NSTask.ExecutionError(stdout: (extension in Shwifty):__C.NSTask.Output, stderr: (extension in Shwifty):__C.NSTask.Output, status: 1, arg0: "/usr/bin/swift", args: ["run"]): file /BuildRoot/Library/Caches/com.apple.xbs/Sources/swiftlang_Fall2018/swiftlang_Fall2018-1000.11.42/src/swift/stdlib/public/core/ErrorType.swift, line 191
fish: './test.swift' terminated by signal SIGILL (Illegal instruction)

Flag to spool the runloop

Would be another thing that /usr/bin/env swift-sh would not be able to use, so we should perhaps also support this via a symlink of swift-sh to swift-sh-runloop or something better.

incompatible dependencies

ArgumentsParser appears to be the culprit, but I have a repo that uses SPMUtility as a dependency for the Command Line Module.

I am importing the library portion, but since the CLI module depends on SPMUtility, shift sh still throws up from a dependency conflict. I tried upgrading to Swift-5.0-branch, but that did not resolve the problem. One solution is to break my library into a Kit and CLI repo. I'd rather not do that.

➜  Shelltr git:(feature/test-post-0.2-release) ✗ swift sh bin/workflow.swift                                   
Fetching https://gitlab.com/thecb4/Shelltr
Fetching https://gitlab.com/thecb4/changelogger
error: the package changelogger[https://gitlab.com/thecb4/changelogger] @ 0.1.2 contains incompatible dependencies:
    swift-package-manager[https://github.com/apple/swift-package-manager.git] @ swift-5.0-branch

Specify macos deployment target

Hello,

i have a problem on running swift-sh scripts with dependencies that uses @available(macOS 10.13, *)
If i run the script, the swift compiler always deploy it with deployment target "macos 10.10". How can force swift-sh to compile it with another deployment target.

Thank you,
Julian

Import local packages

First off, I think swift-sh rocks! I have been enjoying scripting in Swift for a little while now, but this project really takes it to the next level — thanks!

When developing a Swift package locally, I would like to be able to invoke it using swift-sh. i.e., allow swift-sh to take in a local swift packages.

e.g., import Foo // ~/Desktop/Foo

I am using tests to do most of my local development, but I would like to also test my swift package using swift-sh as a real production test. I plan on uploading my project to Github, which would allow swift-sh to grab it, but I would rather not have to rely on that workflow; Especially when making small incremental changes.

I looked through the source code a bit, and I don't think this is currently possible based on:

if let url = URL(string: dependencyName), url.scheme != nil {

I could be wrong though 😅.

I also came across this closed issue: #17, but that issue is related to local files.

[Idea] Command for building and exporting an executable

I'm in a situation where I have to create an executable out of a script to distribute it among my coworkers who don't have the development environment set up. I'm wondering whether this would be helpful in general and whether adding this functionality to swift-sh would be desired?

caching problem if script by same name is in two locations in the file system

Just had a similar issue to #64 just come up:

Simple case:

  1. create two folders: "test", "test2"
  2. in folder "test", create a file "test.swift" with the following contents:
#!/usr/bin/swift sh
print("hello from 'test' folder")
  1. copy "test.swift" into the "test2" folder.
  2. edit "test2/test.swift" to change the string "hello from 'test' folder" to "hello from 'test2' folder" and save the file.
  3. in terminal, cd to "test" folder and execute the test.swift file via ./test.swift
    -> builds and runs, prints:

    hello from 'test' folder

  4. still in the terminal, cd into "test2" folder and execute the test.swift file via .test.swift
    -> ISSUE: you get the same string printed and test.swift is NOT built:

    hello from 'test' folder
    Should build test.swift and print:
    hello from 'test2' folder

Seems like we need either a disambiguation strategy for cache folders that took into account location in the file system, or perhaps a step during swift-sh execution that validates that the Main.swift alias points to the actual file being executed and not some other copy of it somewhere in the file system.

Feature Request: Conversion from Script to Swift Package

swift-sh offers a convenient, lightweight alternative to packages for writing Swift code with third-party dependencies. And it does so cleverly by delegating that responsibility to Swift Package Manager behind the scenes.

It would be nice for swift-sh to provide a mechanism to "eject" from the single-file script to get at the underlying package.

For example, a developer might start with a proof-of-concept script and want to transition that functionality to a package suitable for distribution. They might run a command like swift sh eject foo.swift to move/copy the generated package (~/Library/Developer/swift-sh.cache/foo) to the current directory.

Alternatively, this functionality could also be exposed through documentation.

Proposal: make eject non-destructive

H! It is not correct to call this an "issue." This is really an enhancement to the way that the eject command works, which I wanted to propose for discussion. I'm guessing this is the right place to raise the issue? So here goes.

introduction

Right now, eject does two things: first, it erases your existing swift-sh script, and second, it creates a new Swift Package Manager (SPM) project based on the script.

I think one usually wants the second thing, creating a new project.

So I'd propose that eject should do this by default. Or else, it should take an optional flag --nondestructive which makes it work this way, creating the SPM project in its own directory but not erasing the original script.

motivation

Why? A few reasons.

First, eject feels like a compilation tool. It takes an input (a single file source script) and generates an output (a folder with a complete SPM project). It would be more intuitive and familiar if it behaved like other compilation tools, which produce an output but do not destroy their input along the way. This is how swiftc, clang, gcc, markdown.pl -- heck, even plain old zip-- all behave, and I think it is the right default. This is not only because of convention but also because it gives the user more options regarding what to do next.

Second, I have found I usually want to hold onto the original script because of the workflow I find I am using.

This is what you might call the "pop into Xcode" workflow. Working on a single file script, I want to jump into Xcode for a few hours, basically just to have the benefit of autocomplete and documentation quick lookup, over the standard libraries, my dependencies, and my own code. So I use eject to create an SPM project, and I run swift package generate-xcodeproj to generate the Xcode project, and then I work in Xcode. But then once I'm done coding, I don't want or need the complexity of an Xcode project or of an SPM project. I want to go back to the simplicity of a single file.

I also find swift-sh is easier than Package.swift for specifying dependencies. This is hard in Package.swift is hard, because of the subtle way Package.swift mixes nested enums, static functions, and argument overloads in order to define the specification, and because of the need to specify target relationships correctly. The swift-sh syntax for specifying dependency as an in-line comments is much easier, so in this area switching to SPM and to Xcode is a step backward. I'd rather iterate on my dependencies in swift-sh.

When I'm done in Xcode, I basically manually "reverse-eject" back into a swift-sh script. This is easier to do if I am just copying the new code into the original script file, rather than recreating a file that has been deleted.

Essentially, I think it would make sense for eject to be destructive only if the expected workflow were as follows: You start by hacking on a swift-sh script for a while, as a single file, using a plain old text editor. Then, eventually, when it gets complicated enough, you finally decide to convert it to a SPM project and maybe to an Xcode project. This is a one way transition and you will probably never want to go back to a single file again, because SPM or Xcode provides features you absolutely need for the project's level of complexity. What are these features, potentially? For instance: organizing code into separate files, setting up separate unit tests, managing associated files like required assets, multiple build targets, IDE autocomplete, multiple build schemes, more fine-grained control and UI for build settings, etc..

But what I find is that most of the time I want to "eject" to get to Xcode just for a couple of conveniences, temporarily for workflow benefits, and not as a sign of permanent transition in the project's complexity. This kind of "temporary eject" is likely to happen lots of times, but the "permanent eject" happens only once. So swift-sh should default to facilitating the more common case.

proposal

I see two options:

  1. Modify eject to be non-destructive by default

So if you had an Swift-sh file hello.swift, and you did swift sh eject hello.swift, it would still create a directory Hello as it does now. It just would not delete hello.swift

What if you had an executable Swift file named hello (which used the #!/usr/bin/swift sh shebang to be an executable)? Then you couldn't create a directory named Hello because of name conflict. In that case, I'd suggest that swift sh eject should just exit with an error complaining about the name conflict. Alternatively, it could take a -o or --output command line argument and let the user explicitly specify the name of the directory to be created.

  1. Modify eject to be a non-destructive when invoked with an optional flag --nondesctructive to make this behavior optional.

compatibility

Option 1 would be a breaking change. If anyone out there has already started building scripts that rely on the current destructive eject behavior, then this tools would need to be updated to be aware of the new behavior.

Option 2 would be a non-breaking change, since it introduces an optional flag.

Option 1 is better, in my opinion. Best to make the change now before too many folks get used to the wrong thing. 😉

last words

I realize this is a lot of words for a simple idea but I wanted to be thoughtful about how I suggested this. I expect the work to implement this change take less time than just articulating this proposal, but that's probably how it should be. I think swift-sh has an excellent power/weight ratio right now, and provides the same kind of benefit to its users. I'm trying to stay in that design territory.

It would be good if the script could detect it's running via swift-sh

A common hack is to use #filename to locate resources living alongside the source files. When swift-sh moves the script source to its cache, this doesn't work anymore.

Idea: Instead of copying the script itself, could you still point to the original file in the Package.swift, e.g.:

    .target(name: "main", dependencies: ["cows"], 
            path: "/Users/helge/dev/scripts/", 
            sources: ["blub.swift"])

I think that would be very useful regardless?

Can't import two dependencies from single repository

#!/usr/bin/swift sh

import Basic     // https://github.com/apple/swift-package-manager.git ~> 0.2.1
import Utility   // https://github.com/apple/swift-package-manager.git ~> 0.2.1

yields

Fatal error: Duplicate values for key: 'SwiftPM'

Import local files

This is a really great package!.. I was thinking (if possible) being able to include local files would be a big help

e.g.:

test1.swift

#!/usr/bin/swift sh
import Foobar // ./test2.swift

print(Foobar().foo())

test2.swift

final class Foobar {
	init() {

	}

	func foo() -> String {
		return "Foobar!"
	}
}

Not sure if it's even possible to do, but would allow to create a bit more advanced scripts with this :)

cann't run .swift file

i tried to run addTarget.swift file using terminal using this command:
swift sh addTarget.swift

but i get this error :

dyld: Symbol not found: _$s11SubSequenceSlTl
  Referenced from: /usr/local/bin/swift-sh
  Expected in: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/swift/macosx/libswiftCore.dylib
 in /usr/local/bin/swift-sh
Abort trap: 6

Swift 5.2 import issue

There seems to be an issue with import of modules that have a repo name that doesn't match module name.

This import is working with Swift 5.1
import SourceKittenFramework // jpsim/SourceKitten

but fails when using Swift 5.2 (Xcode 11.4 beta) with the following error:

/Users/me/Library/Developer/swift-sh.cache/report: error: manifest parse error: target 'report' depends on an unknown package 'SourceKittenFramework'

Cache bug

It's possible that Package.swift is re-written but swift-build doesn’t rebuild the resulting binary, this leads to us always running swift-build which delays script execution by ~120ms.

Also (unrelated) we should be faster that we currently are (when cache is valid and respected), worth trying to figure out why that is.

Autocompletion when editing script in Xcode doesn't work

Today, I researched a way for handling helper scripts for my Mac application. I tested all three major players (swift-sh, Marathon, Beak) and I like the approach of swift-sh the most. Especially the shebang and the idea of editing out into Xcode while working on the script without having to maintain the Xcode project afterwards.

Unfortunately, there have been also two issues which bother me a bit. The first one being the management of the Homebrew formula which I described in mxcl/homebrew-made#4. The second one is the inability to get the autocompleting in Xcode working.

Here are the steps I've performed. First, I installed the tool from source (version 1.12.0):

$ git clone https://github.com/mxcl/swift-sh.git
$ cd swift-sh
$ swift build -c release
$ cp .build/
$ cp .build/release/swift-sh "/Applications/Xcode 10.2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/"
$ cp .build/release/swift-sh-edit "/Applications/Xcode 10.2.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/bin/"

Then I created a sample script from the README:

$ cat <<EOF > script
#!/usr/bin/swift sh
import PromiseKit // @mxcl ~> 6.5
print(Promise.value("Hi!"))
EOF
$ chmod u+x script
$ ./script
# => Promise("Hi!")

So far everything worked smoothly, so I tried to edit the script in Xcode 10.2 using $ swift sh edit script. Xcode opened but no matter which scheme I selected the autocompletion didn't work. The script scheme allowed me to run the script and the script-completion scheme behaved very strangely showing errors complaining about script not being main.swift. Below are some screenshots showing the problem.

@mxcl I must be missing something. Could you please give me a hint regarding what I'm doing wrong?

xcode-scheme-script

xcode-scheme-script-completion

Getting CWD

I don't believe there's currently a way for a script to see which directory it's being run from. Using Path.cwd gives something like /Users/me/Library/Developer/swift-sh.cache/scriptname. Is there a way to get the terminal's current directory?

How to handle a script called `eject`

We should at least emit a warning if there is a script called eject in the and the command was called with swift sh eject.

The workaround would be swift sh ./eject or swift sh -- eject, neither is ideal, really we're being naughty by having an ambiguous command spec.

Whatever, we should detect if we're being called via the shebang and always do the script that way.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.