Created
April 5, 2016 17:58
-
-
Save efirestone/ce01ae109e08772647eb061b3bb387c3 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Created by Eric Firestone on 3/22/16. | |
// Copyright © 2016 Square, Inc. All rights reserved. | |
// Released under the Apache v2 License. | |
// | |
// Adapted from https://gist.github.com/blakemerryman/76312e1cbf8aec248167 | |
import Foundation | |
let GlobBehaviorBashV3 = Glob.Behavior( | |
supportsGlobstar: false, | |
includesFilesFromRootOfGlobstar: false, | |
includesDirectoriesInResults: true, | |
includesFilesInResultsIfTrailingSlash: false | |
) | |
let GlobBehaviorBashV4 = Glob.Behavior( | |
supportsGlobstar: true, // Matches Bash v4 with "shopt -s globstar" option | |
includesFilesFromRootOfGlobstar: true, | |
includesDirectoriesInResults: true, | |
includesFilesInResultsIfTrailingSlash: false | |
) | |
let GlobBehaviorGradle = Glob.Behavior( | |
supportsGlobstar: true, | |
includesFilesFromRootOfGlobstar: true, | |
includesDirectoriesInResults: false, | |
includesFilesInResultsIfTrailingSlash: true | |
) | |
/** | |
Finds files on the file system using pattern matching. | |
*/ | |
class Glob: CollectionType { | |
/** | |
* Different glob implementations have different behaviors, so the behavior of this | |
* implementation is customizable. | |
*/ | |
struct Behavior { | |
// If true then a globstar ("**") causes matching to be done recursively in subdirectories. | |
// If false then "**" is treated the same as "*" | |
let supportsGlobstar: Bool | |
// If true the results from the directory where the globstar is declared will be included as well. | |
// For example, with the pattern "dir/**/*.ext" the fie "dir/file.ext" would be included if this | |
// property is true, and would be omitted if it's false. | |
let includesFilesFromRootOfGlobstar: Bool | |
// If false then the results will not include directory entries. This does not affect recursion depth. | |
let includesDirectoriesInResults: Bool | |
// If false and the last characters of the pattern are "**/" then only directories are returned in the results. | |
let includesFilesInResultsIfTrailingSlash: Bool | |
} | |
static var defaultBehavior = GlobBehaviorBashV4 | |
private var isDirectoryCache = [String: Bool]() | |
let behavior: Behavior | |
var paths = [String]() | |
var startIndex: Int { return paths.startIndex } | |
var endIndex: Int { return paths.endIndex } | |
init(pattern: String, behavior: Behavior = Glob.defaultBehavior) { | |
self.behavior = behavior | |
var adjustedPattern = pattern | |
let hasTrailingGlobstarSlash = pattern.hasSuffix("**/") | |
var includeFiles = !hasTrailingGlobstarSlash | |
if behavior.includesFilesInResultsIfTrailingSlash { | |
includeFiles = true | |
if hasTrailingGlobstarSlash { | |
// Grab the files too. | |
adjustedPattern += "*" | |
} | |
} | |
let patterns = behavior.supportsGlobstar ? expandGlobstar(adjustedPattern) : [adjustedPattern] | |
for pattern in patterns { | |
var gt = glob_t() | |
if executeGlob(pattern, gt: >) { | |
populateFiles(gt, includeFiles: includeFiles) | |
} | |
globfree(>) | |
} | |
paths = Array(Set(paths)).sort { lhs, rhs in | |
lhs.compare(rhs) != NSComparisonResult.OrderedDescending | |
} | |
clearCaches() | |
} | |
// MARK: Private | |
private var globalFlags = GLOB_TILDE | GLOB_BRACE | GLOB_MARK | |
private func executeGlob(pattern: UnsafePointer<CChar>, gt: UnsafeMutablePointer<glob_t>) -> Bool { | |
return 0 == glob(pattern, globalFlags, nil, gt) | |
} | |
private func expandGlobstar(pattern: String) -> [String] { | |
guard pattern.containsString("**") else { | |
return [pattern] | |
} | |
var results = [String]() | |
var parts = pattern.componentsSeparatedByString("**") | |
let firstPart = parts.removeFirst() | |
var lastPart = parts.joinWithSeparator("**") | |
let fileManager = NSFileManager.defaultManager() | |
var directories: [String] | |
do { | |
directories = try fileManager.subpathsOfDirectoryAtPath(firstPart).flatMap { subpath in | |
let fullPath = NSString(string: firstPart).stringByAppendingPathComponent(subpath) | |
var isDirectory = ObjCBool(false) | |
if fileManager.fileExistsAtPath(fullPath, isDirectory: &isDirectory) && isDirectory { | |
return fullPath | |
} else { | |
return nil | |
} | |
} | |
} catch { | |
directories = [] | |
print("Error parsing file system item: \(error)") | |
} | |
if behavior.includesFilesFromRootOfGlobstar { | |
// Check the base directory for the glob star as well. | |
directories.insert(firstPart, atIndex: 0) | |
// Include the globstar root directory ("dir/") in a pattern like "dir/**" or "dir/**/" | |
if lastPart.isEmpty { | |
results.append(firstPart) | |
} | |
} | |
if lastPart.isEmpty { | |
lastPart = "*" | |
} | |
for directory in directories { | |
let partiallyResolvedPattern = NSString(string: directory).stringByAppendingPathComponent(lastPart) | |
results.appendContentsOf(expandGlobstar(partiallyResolvedPattern)) | |
} | |
return results | |
} | |
private func isDirectory(path: String) -> Bool { | |
var isDirectory = isDirectoryCache[path] | |
if let isDirectory = isDirectory { | |
return isDirectory | |
} | |
var isDirectoryBool = ObjCBool(false) | |
isDirectory = NSFileManager.defaultManager().fileExistsAtPath(path, isDirectory: &isDirectoryBool) && isDirectoryBool | |
isDirectoryCache[path] = isDirectory! | |
return isDirectory! | |
} | |
private func clearCaches() { | |
isDirectoryCache.removeAll() | |
} | |
private func populateFiles(gt: glob_t, includeFiles: Bool) { | |
let includeDirectories = behavior.includesDirectoriesInResults | |
for i in 0..<Int(gt.gl_matchc) { | |
if let path = String.fromCString(gt.gl_pathv[i]) { | |
if !includeFiles || !includeDirectories { | |
let isDirectory = self.isDirectory(path) | |
if (!includeFiles && !isDirectory) || (!includeDirectories && isDirectory) { | |
continue | |
} | |
} | |
paths.append(path) | |
} | |
} | |
} | |
// MARK: Subscript Support | |
subscript(i: Int) -> String { | |
return paths[i] | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// | |
// Created by Eric Firestone on 3/22/16. | |
// Copyright © 2016 Square, Inc. All rights reserved. | |
// Released under the Apache v2 License. | |
// | |
// Adapted from https://gist.github.com/blakemerryman/76312e1cbf8aec248167 | |
import XCTest | |
class GlobTests : XCTestCase { | |
let tmpFiles = ["foo", "bar", "baz", "dir1/file1.ext", "dir1/dir2/dir3/file2.ext"] | |
var tmpDir = "" | |
override func setUp() { | |
super.setUp() | |
var tmpDirTmpl = "/tmp/glob-test.XXXXX".cStringUsingEncoding(NSUTF8StringEncoding)! | |
self.tmpDir = String.fromCString(mkdtemp(&tmpDirTmpl))! | |
let flags = S_IRWXU | S_IRWXG | S_IROTH | S_IXOTH | |
mkdir("\(tmpDir)/dir1/", flags) | |
mkdir("\(tmpDir)/dir1/dir2", flags) | |
mkdir("\(tmpDir)/dir1/dir2/dir3", flags) | |
for file in tmpFiles { | |
close(open("\(tmpDir)/\(file)", O_CREAT)) | |
} | |
} | |
override func tearDown() { | |
for file in tmpFiles { | |
unlink("\(tmpDir)/\(file)") | |
} | |
rmdir("\(tmpDir)/dir1/dir2/dir3") | |
rmdir("\(tmpDir)/dir1/dir2") | |
rmdir("\(tmpDir)/dir1") | |
rmdir(self.tmpDir) | |
super.tearDown() | |
} | |
func testBraces() { | |
let pattern = "\(tmpDir)/ba{r,y,z}" | |
let glob = Glob(pattern: pattern) | |
var contents = [String]() | |
for file in glob { | |
contents.append(file) | |
} | |
XCTAssertEqual(contents, ["\(tmpDir)/bar", "\(tmpDir)/baz"], "matching with braces failed") | |
} | |
func testNothingMatches() { | |
let pattern = "\(tmpDir)/nothing" | |
let glob = Glob(pattern: pattern) | |
var contents = [String]() | |
for file in glob { | |
contents.append(file) | |
} | |
XCTAssertEqual(contents, [], "expected empty list of files") | |
} | |
func testDirectAccess() { | |
let pattern = "\(tmpDir)/ba{r,y,z}" | |
let glob = Glob(pattern: pattern) | |
XCTAssertEqual(glob.paths, ["\(tmpDir)/bar", "\(tmpDir)/baz"], "matching with braces failed") | |
} | |
func testIterateTwice() { | |
let pattern = "\(tmpDir)/ba{r,y,z}" | |
let glob = Glob(pattern: pattern) | |
var contents1 = [String]() | |
var contents2 = [String]() | |
for file in glob { | |
contents1.append(file) | |
} | |
let filesAfterOnce = glob.paths | |
for file in glob { | |
contents2.append(file) | |
} | |
XCTAssertEqual(contents1, contents2, "results for calling for-in twice are the same") | |
XCTAssertEqual(glob.paths, filesAfterOnce, "calling for-in twice doesn't only memoizes once") | |
} | |
func testIndexing() { | |
let pattern = "\(tmpDir)/ba{r,y,z}" | |
let glob = Glob(pattern: pattern) | |
XCTAssertEqual(glob[0], "\(tmpDir)/bar", "indexing") | |
} | |
// MARK: - Globstar - Bash v3 | |
func testGlobstarBashV3NoSlash() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**" | |
let pattern = "\(tmpDir)/**" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV3) | |
XCTAssertEqual(glob.paths, ["\(tmpDir)/bar", "\(tmpDir)/baz", "\(tmpDir)/dir1/", "\(tmpDir)/foo"]) | |
} | |
func testGlobstarBashV3WithSlash() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**/" | |
let pattern = "\(tmpDir)/**/" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV3) | |
XCTAssertEqual(glob.paths, ["\(tmpDir)/dir1/"]) | |
} | |
func testGlobstarBashV3WithSlashAndWildcard() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**/*" | |
let pattern = "\(tmpDir)/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV3) | |
XCTAssertEqual(glob.paths, ["\(tmpDir)/dir1/dir2/", "\(tmpDir)/dir1/file1.ext"]) | |
} | |
func testDoubleGlobstarBashV3() { | |
let pattern = "\(tmpDir)/**/dir2/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV3) | |
XCTAssertEqual(glob.paths, ["\(tmpDir)/dir1/dir2/dir3/file2.ext"]) | |
} | |
// MARK: - Globstar - Bash v4 | |
func testGlobstarBashV4NoSlash() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**" | |
let pattern = "\(tmpDir)/**" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV4) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/", | |
"\(tmpDir)/bar", | |
"\(tmpDir)/baz", | |
"\(tmpDir)/dir1/", | |
"\(tmpDir)/dir1/dir2/", | |
"\(tmpDir)/dir1/dir2/dir3/", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
"\(tmpDir)/dir1/file1.ext", | |
"\(tmpDir)/foo" | |
]) | |
} | |
func testGlobstarBashV4WithSlash() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**/" | |
let pattern = "\(tmpDir)/**/" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV4) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/", | |
"\(tmpDir)/dir1/", | |
"\(tmpDir)/dir1/dir2/", | |
"\(tmpDir)/dir1/dir2/dir3/", | |
]) | |
} | |
func testGlobstarBashV4WithSlashAndWildcard() { | |
// Should be the equivalent of "ls -d -1 /(tmpdir)/**/*" | |
let pattern = "\(tmpDir)/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV4) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/bar", | |
"\(tmpDir)/baz", | |
"\(tmpDir)/dir1/", | |
"\(tmpDir)/dir1/dir2/", | |
"\(tmpDir)/dir1/dir2/dir3/", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
"\(tmpDir)/dir1/file1.ext", | |
"\(tmpDir)/foo", | |
]) | |
} | |
func testDoubleGlobstarBashV4() { | |
let pattern = "\(tmpDir)/**/dir2/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorBashV4) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/dir1/dir2/dir3/", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
]) | |
} | |
// MARK: - Globstar - Gradle | |
func testGlobstarGradleNoSlash() { | |
// Should be the equivalent of | |
// FileTree tree = project.fileTree((Object)'/tmp') { | |
// include 'glob-test.7m0Lp/**' | |
// } | |
// | |
// Note that the sort order currently matches Bash and not Gradle | |
let pattern = "\(tmpDir)/**" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorGradle) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/bar", | |
"\(tmpDir)/baz", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
"\(tmpDir)/dir1/file1.ext", | |
"\(tmpDir)/foo", | |
]) | |
} | |
func testGlobstarGradleWithSlash() { | |
// Should be the equivalent of | |
// FileTree tree = project.fileTree((Object)'/tmp') { | |
// include 'glob-test.7m0Lp/**/' | |
// } | |
// | |
// Note that the sort order currently matches Bash and not Gradle | |
let pattern = "\(tmpDir)/**/" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorGradle) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/bar", | |
"\(tmpDir)/baz", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
"\(tmpDir)/dir1/file1.ext", | |
"\(tmpDir)/foo", | |
]) | |
} | |
func testGlobstarGradleWithSlashAndWildcard() { | |
// Should be the equivalent of | |
// FileTree tree = project.fileTree((Object)'/tmp') { | |
// include 'glob-test.7m0Lp/**/*' | |
// } | |
// | |
// Note that the sort order currently matches Bash and not Gradle | |
let pattern = "\(tmpDir)/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorGradle) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/bar", | |
"\(tmpDir)/baz", | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
"\(tmpDir)/dir1/file1.ext", | |
"\(tmpDir)/foo", | |
]) | |
} | |
func testDoubleGlobstarGradle() { | |
// Should be the equivalent of | |
// FileTree tree = project.fileTree((Object)'/tmp') { | |
// include 'glob-test.7m0Lp/**/dir2/**/*' | |
// } | |
// | |
// Note that the sort order currently matches Bash and not Gradle | |
let pattern = "\(tmpDir)/**/dir2/**/*" | |
let glob = Glob(pattern: pattern, behavior: GlobBehaviorGradle) | |
XCTAssertEqual(glob.paths, [ | |
"\(tmpDir)/dir1/dir2/dir3/file2.ext", | |
]) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I updated the code for Swift 3 and packaged it for SwiftPM at https://github.com/Bouke/Glob.