There are a lot of command line argument parsers written in Swift available on Github, so when I needed one for a client project I was surprised I could not find any which fulfilled these requirements:
- treat unrecognised arguments as an error and list them.
Nor could I find any where it would be relatively easy to add these features, since most just loop through the arguments from first to last and have one big function trying to find out what to do with each of them. That becomes messy quickly.
I think it’s much simpler to create multiple small argument parsers, where each one in turn takes in the list of arguments, removes the ones it wants to handle and passes the rest on to the next one. Any arguments left over are then unrecognised and can be handled accordingly. This way the end user does not need to worry about the order of the arguments. The developer however needs to be careful about the order of the parsers.
Dealing with file paths in Swift is cumbersome. Using only the standard library leaves us with Strings, which we can join together or split by
/. It gets the job done but it’s not pretty, and we need a separate type so our methods can accept file paths exclusively and not just any old String. Foundation gives us this in NSURL/URL, which are also used for internet URLs so their method names are very general and long. E.g.
url.deletingPathExtension().lastPathComponent to return the file name without the extension.
The Swift Package Manager has separate types for absolute paths and relative paths, because in their opinion these are fundamentally different. I can see their point, but I think it should be up to the callers of an API and not the creators whether to use absolute or relative paths.
The best alternative I’ve been able to find is JohnSundell/Files, because it gets an important thing right: it differentiates between files and directories. These are fundamentally different things (even though the internal representation of their paths are identical) and should have different types with different functionalities. You can’t read from or write to a directory itself, nor can you add a directory to a file.
What I am looking for however has separate types not only for files and folders, but also for paths (which may or may not exist) and filesystem items (which do), and for files you just want to read from and not change and files you want to write to, rename, move and/or delete. Because filesystem access, maybe more than any other task solved by programming, has the potential to irrevocably mess things up. And one way to prevent this is extra type safety, leading to fewer programmer errors.
So I made the FileSmith library with FilePath/DirectoryPath, ReadableFile/WritableFile and Directory.
Sam Burnstone recently wrote about how to convert a simple shell script to Swift. Here’s the same shell script rewritten using SwiftShell and FileSmith.
Before building the Swift compiler it might be a good idea to check https://github.com/apple/swift to verify the build is currently passing. And to free up as much memory as possible first you can shut down the graphical interface with
sudo service lightdm stop.
(has been tested on Ubuntu 15.10):
I really don’t like using the ‘rm’ shell command – one misplaced character and you can do some serious damage. But when working in the Finder I don’t think twice about deleting files, because I know I can always get them back from the trash. So here is a Swift shell script which does exactly that – it moves files to the trash instead of deleting them permanently.
The syntax is very simple – all parameters refer to file system items which should be moved to the trash:
trash file.txt a_folder
trash *.m *.h
I’m rewriting Moderator (yet another command-line argument parser), and with Swift now being available for both OS X and Linux (Ubuntu) it should support a syntax which enables applications to fit in on both platforms.
POSIX* is I think the closest thing to a standard for this, so it will be the basis, with some modifications (The Python documentation also has some good insights).
* OS X is POSIX compliant and so is Linux (mostly).
Swift version 2.1.
In the previous post we implemented lazy splitting of collections, very useful for say splitting large texts into lines. But in SwiftShell I need the same functionality for text which is acquired piecemeal, like the output of a long-running shell command read sequentially, when needed. Because shell commands which are piped together in the terminal should get to work right away, and not just hang around waiting for the previous command to finish. Like this:
Both scripts start at the same time. The left one uses the functionality implemented below, while the right one reads the entire input into a string first, and therefore has to wait for the ‘linemaker’ command to finish before doing any actual work.
Swift version 2.1
There are already methods for splitting collections in the Swift Standard Library, but they do all the work immediately and return the results in an array. When dealing with large strings, or streams of text, I find it better to do the work lazily, when needed. The overall performance is not necessarily better, but it is smoother, as you get the first results immediately instead of having to wait a little while and then get everything at once. And memory usage is lower, no need to store everything in an array first.
I finally got around to updating the SwiftShell 2.0 readme with some actual usage instructions:
An OS X Framework for command line scripting in Swift.
Put this at the beginning of each script file:
try runAndPrint(bash: "cmd1 arg | cmd2 arg")
Runs a shell command just like you would in the terminal. If the command returns with a non-zero exit code it will throw a ShellError.
The name may seem a bit cumbersome, but it explains exactly what it does. SwiftShell never prints anything without explicitly being told to.
To be honest I’m not very good at shell scripting. It’s very useful for automation so I would like to be, but I just don’t like the syntax. For instance, this is how you check if a variable is greater than 100:
if [ $var -gt 100 ]
<do some stuff>
And here’s how to check if the file referred to in the first argument is readable and not empty:
if [ -r $1 ] && [ -s $1 ]
So I would much rather use Swift, as the syntax is nice, very nice indeed. But the things that bash shell scripts actually are good at, like running shell commands and accessing the shell environment, are not that straightforward in Swift. Here’s how you can perform the various tasks using only the Swift Standard Library and Foundation: