├── .gitignore ├── Sources ├── sugar │ └── main.swift └── sugar_core │ ├── Performable.swift │ ├── Group+Add.swift │ ├── PrintableError.swift │ ├── ScriptRunner.swift │ ├── Command.swift │ ├── CommandManager.swift │ ├── ZAlgorithm.swift │ ├── String+KMP.swift │ └── Adder.swift ├── .swiftlint.yml ├── Package.swift ├── README.md └── Package.resolved /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | /.build 3 | /Packages 4 | /*.xcodeproj 5 | Podfile 6 | -------------------------------------------------------------------------------- /Sources/sugar/main.swift: -------------------------------------------------------------------------------- 1 | import sugar_core 2 | 3 | let manager = CommandManager() 4 | manager.run() 5 | -------------------------------------------------------------------------------- /.swiftlint.yml: -------------------------------------------------------------------------------- 1 | disabled_rules: 2 | - line_length 3 | - force_try 4 | - force_cast 5 | - operator_whitespace 6 | - trailing_whitespace 7 | 8 | included: 9 | - Sources 10 | - Tests 11 | 12 | type_body_length: 13 | - 400 #warning 14 | - 600 #error 15 | 16 | file_length: 17 | - 800 #warning 18 | - 1000 #error 19 | -------------------------------------------------------------------------------- /Sources/sugar_core/Performable.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Performable.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Commander 9 | 10 | internal protocol Performable { 11 | init(with runner: ScriptRunner) 12 | func perform(_ arguments: ArgumentConvertible...) 13 | } 14 | -------------------------------------------------------------------------------- /Sources/sugar_core/Group+Add.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Group+Add.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Commander 9 | 10 | extension Group { 11 | func add(command: Command) { 12 | addCommand(command.name, command.description, command.wrapper) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Sources/sugar_core/PrintableError.swift: -------------------------------------------------------------------------------- 1 | // 2 | // PrintableError.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public protocol PrintableError: Error, Equatable, CustomStringConvertible { 11 | var message: String { get } 12 | } 13 | 14 | public extension PrintableError { 15 | static func ==(lhs: Self, rhs: Self) -> Bool { 16 | return lhs.message == rhs.message 17 | } 18 | 19 | var description: String { 20 | let description = "💥 \(message)" 21 | return description 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Sources/sugar_core/ScriptRunner.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ScriptRunner.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | 10 | public final class ScriptRunner { 11 | 12 | private var count = 0 13 | private let runLoop = RunLoop.current 14 | 15 | public func lock() { 16 | count += 1 17 | } 18 | public func unlock() { 19 | count -= 1 20 | } 21 | public func wait() { 22 | while count > 0 && 23 | runLoop.run(mode: RunLoopMode.defaultRunLoopMode, before: Date(timeIntervalSinceNow: 0.1)) { 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | // The swift-tools-version declares the minimum version of Swift required to build this package. 3 | 4 | import PackageDescription 5 | 6 | let package = Package( 7 | name: "sugar", 8 | dependencies: [ 9 | .package(url: "https://github.com/JohnSundell/Releases.git", from: "2.0.0"), 10 | .package(url: "https://github.com/JohnSundell/Files.git", from: "2.0.0"), 11 | .package(url: "https://github.com/kylef/Commander.git", from: "0.8.0"), 12 | .package(url: "https://github.com/JohnSundell/ShellOut.git", from: "2.0.0") 13 | ], 14 | targets: [ 15 | .target( 16 | name: "sugar", 17 | dependencies: ["sugar_core"] 18 | ), 19 | .target( 20 | name: "sugar_core", 21 | dependencies: ["Commander", "Releases", "ShellOut", "Files"] 22 | ) 23 | ] 24 | ) 25 | -------------------------------------------------------------------------------- /Sources/sugar_core/Command.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | import Commander 10 | 11 | internal enum Command: String { 12 | case add 13 | } 14 | 15 | extension Command { 16 | 17 | var name: String { 18 | switch self { 19 | case .add: 20 | return "add" 21 | } 22 | } 23 | 24 | var description: String { 25 | switch self { 26 | case .add: 27 | return "Add pod entry to Podfile" 28 | } 29 | } 30 | 31 | var wrapper: CommandType { 32 | switch self { 33 | case .add: 34 | return command( 35 | Argument("pod", description: "Name of the pod to add."), 36 | Option("version", default: -1.0, flag: "v", description: "Specified version of pod to install."), 37 | Option("path", default: ".", flag: "p", description: "Specified path to Podfile.")) { pod, version, path in 38 | Adder().perform(pod: pod, version: version, path: path) 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sugar 🍬 2 | 3 | [![Swift 4.0](https://img.shields.io/badge/swift-4.0-orange.svg)](#) 4 | [![@nugmanoff](https://img.shields.io/badge/contact-%40nugmanoff-brightgreen.svg)](https://t.me/nugmanoff) 5 | 6 | Trivial command-line tool build in educational purposes & for personal use. 7 | Allows to easily add pod entry to CocoaPods Podfile without any need to open it with text editor. 8 | 9 | ## Installation 10 | 11 | The easiest way to install Sugar is using Swift Package Manager: 12 | 13 | ``` 14 | $ git clone git@github.com:nugmanoff/sugar.git 15 | $ cd sugar 16 | $ swift build -c release -Xswiftc -static-stdlib 17 | $ cp -f .build/release/sugar /usr/local/bin/sugar 18 | ``` 19 | ## Usage 20 | 21 | Without any specifications (have to be executed in the directory where Podfile is located). 22 | ``` 23 | sugar add Alamofire 24 | ``` 25 | Specifies particular version of pod to install. 26 | ``` 27 | sugar add Alamofire -v 4.0 28 | ``` 29 | Specifies path of Podfile to update. 30 | ``` 31 | sugar add Alamofire -p /Users/Aidar/Workspace/CLI/sugar 32 | ``` 33 | 34 | ## Dependencies 35 | 36 | - [Commander](https://github.com/kylef/Commander) 37 | - [SwiftLint](https://github.com/realm/SwiftLint) 38 | - [Files](https://github.com/JohnSundell/Files) 39 | - [Releases](https://github.com/JohnSundell/Releases) 40 | 41 | ## Help, feedback or suggestions? 42 | 43 | Feel free to contact me on [Telegram](https://t.me/nugmanoff) for discussions, news & announcements about Sugar & other projects. 44 | -------------------------------------------------------------------------------- /Sources/sugar_core/CommandManager.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Command.swift 3 | // indexifyPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/4/18. 6 | // 7 | 8 | import Foundation 9 | import Commander 10 | import Releases 11 | 12 | // MARK: - Error 13 | 14 | public enum CommandManagerError { 15 | case failedToResolveVersion 16 | } 17 | 18 | extension CommandManagerError: PrintableError { 19 | public var message: String { 20 | switch self { 21 | case .failedToResolveVersion: 22 | return "Failed to resolve release version" 23 | } 24 | } 25 | } 26 | 27 | // MARK: - CommandManager 28 | 29 | public final class CommandManager { 30 | 31 | private typealias Error = CommandManagerError 32 | 33 | private var version = String() 34 | private var group = Group() 35 | private var repo = "https://github.com/nugmanoff/sugar.git" 36 | 37 | // MARK: - Init 38 | 39 | public init() { 40 | resolveCommands() 41 | } 42 | 43 | public convenience init(with version: String) { 44 | self.init() 45 | self.version = version 46 | } 47 | 48 | public func run() { 49 | group.run(version) 50 | } 51 | 52 | // MARK: - Private 53 | 54 | private func resolveCommands() { 55 | group.add(command: .add) 56 | } 57 | 58 | private func resolveVersion() { 59 | do { 60 | let url = URL(string: repo)! 61 | let releases = try! Releases.versions(for: url) 62 | guard let latestRelease = releases.last?.string else { 63 | throw Error.failedToResolveVersion 64 | } 65 | version = latestRelease 66 | } catch { 67 | print(Error.failedToResolveVersion) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /Package.resolved: -------------------------------------------------------------------------------- 1 | { 2 | "object": { 3 | "pins": [ 4 | { 5 | "package": "Commander", 6 | "repositoryURL": "https://github.com/kylef/Commander.git", 7 | "state": { 8 | "branch": null, 9 | "revision": "e5b50ad7b2e91eeb828393e89b03577b16be7db9", 10 | "version": "0.8.0" 11 | } 12 | }, 13 | { 14 | "package": "Files", 15 | "repositoryURL": "https://github.com/JohnSundell/Files.git", 16 | "state": { 17 | "branch": null, 18 | "revision": "6171f9e9d8619678da22355560f3dbed6368058d", 19 | "version": "2.0.1" 20 | } 21 | }, 22 | { 23 | "package": "Releases", 24 | "repositoryURL": "https://github.com/JohnSundell/Releases.git", 25 | "state": { 26 | "branch": null, 27 | "revision": "0a3c1ba8ce8bb27c02c225979bfd02888b38f0cc", 28 | "version": "2.0.1" 29 | } 30 | }, 31 | { 32 | "package": "Require", 33 | "repositoryURL": "https://github.com/JohnSundell/Require.git", 34 | "state": { 35 | "branch": null, 36 | "revision": "7cfbd0d8a2dede0e01f6f0d8ab2c7acef1df112e", 37 | "version": "2.0.1" 38 | } 39 | }, 40 | { 41 | "package": "ShellOut", 42 | "repositoryURL": "https://github.com/JohnSundell/ShellOut.git", 43 | "state": { 44 | "branch": null, 45 | "revision": "f1c253a34a40df4bfd268b09fdb101b059f6d52d", 46 | "version": "2.1.0" 47 | } 48 | }, 49 | { 50 | "package": "Spectre", 51 | "repositoryURL": "https://github.com/kylef/Spectre.git", 52 | "state": { 53 | "branch": null, 54 | "revision": "e34d5687e1e9d865e3527dd58bc2f7464ef6d936", 55 | "version": "0.8.0" 56 | } 57 | } 58 | ] 59 | }, 60 | "version": 1 61 | } 62 | -------------------------------------------------------------------------------- /Sources/sugar_core/ZAlgorithm.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ZAlgorithm.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | 10 | func zetaAlgorithm(ptrn: String) -> [Int]? { 11 | 12 | let pattern = Array(ptrn.characters) 13 | let patternLength = pattern.count 14 | 15 | guard patternLength > 0 else { 16 | return nil 17 | } 18 | 19 | var zeta = [Int](repeating: 0, count: patternLength) 20 | 21 | var left = 0 22 | var right = 0 23 | var k_1 = 0 24 | var betaLength = 0 25 | var textIndex = 0 26 | var patternIndex = 0 27 | 28 | for k in 1 ..< patternLength { 29 | if k > right { 30 | patternIndex = 0 31 | 32 | while k + patternIndex < patternLength && 33 | pattern[k + patternIndex] == pattern[patternIndex] { 34 | patternIndex += 1 35 | } 36 | 37 | zeta[k] = patternIndex 38 | 39 | if zeta[k] > 0 { 40 | left = k 41 | right = k + zeta[k] - 1 42 | } 43 | } else { 44 | k_1 = k - left + 1 45 | betaLength = right - k + 1 46 | 47 | if zeta[k_1 - 1] < betaLength { 48 | zeta[k] = zeta[k_1 - 1] 49 | } else if zeta[k_1 - 1] >= betaLength { 50 | textIndex = betaLength 51 | patternIndex = right + 1 52 | 53 | while patternIndex < patternLength && pattern[textIndex] == pattern[patternIndex] { 54 | textIndex += 1 55 | patternIndex += 1 56 | } 57 | 58 | zeta[k] = patternIndex - k 59 | left = k 60 | right = patternIndex - 1 61 | } 62 | } 63 | } 64 | return zeta 65 | } 66 | -------------------------------------------------------------------------------- /Sources/sugar_core/String+KMP.swift: -------------------------------------------------------------------------------- 1 | // 2 | // String+KMP.swift 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | 10 | extension String { 11 | 12 | func lastIndex(of ptrn: String) -> Int? { 13 | guard let indexes = indexesOf(ptrn: ptrn) else { 14 | return nil 15 | } 16 | return indexes.last 17 | } 18 | 19 | fileprivate func indexesOf(ptrn: String) -> [Int]? { 20 | 21 | let text = Array(self.characters) 22 | let pattern = Array(ptrn.characters) 23 | 24 | let textLength: Int = text.count 25 | let patternLength: Int = pattern.count 26 | 27 | guard patternLength > 0 else { 28 | return nil 29 | } 30 | 31 | var suffixPrefix: [Int] = [Int](repeating: 0, count: patternLength) 32 | var textIndex: Int = 0 33 | var patternIndex: Int = 0 34 | var indexes: [Int] = [Int]() 35 | 36 | let zeta = zetaAlgorithm(ptrn: ptrn) 37 | 38 | for patternIndex in (1 ..< patternLength).reversed() { 39 | textIndex = patternIndex + zeta![patternIndex] - 1 40 | suffixPrefix[textIndex] = zeta![patternIndex] 41 | } 42 | 43 | textIndex = 0 44 | patternIndex = 0 45 | 46 | while textIndex + (patternLength - patternIndex - 1) < textLength { 47 | 48 | while patternIndex < patternLength && text[textIndex] == pattern[patternIndex] { 49 | textIndex += 1 50 | patternIndex += 1 51 | } 52 | 53 | if patternIndex == patternLength { 54 | indexes.append(textIndex - patternIndex) 55 | } 56 | 57 | if patternIndex == 0 { 58 | textIndex += 1 59 | } else { 60 | patternIndex = suffixPrefix[patternIndex - 1] 61 | } 62 | } 63 | 64 | guard !indexes.isEmpty else { 65 | return nil 66 | } 67 | return indexes 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Sources/sugar_core/Adder.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Adder 3 | // sugarPackageDescription 4 | // 5 | // Created by Aidar Nugmanov on 1/22/18. 6 | // 7 | 8 | import Foundation 9 | import Commander 10 | import Files 11 | 12 | // MARK: - AdderError 13 | 14 | public enum AdderError { 15 | case failedToLocatePodfile 16 | case failedToReadContents 17 | case corruptedStructureError 18 | case failedToUpdatePodfile 19 | } 20 | 21 | extension AdderError: PrintableError { 22 | public var message: String { 23 | switch self { 24 | case .failedToLocatePodfile: 25 | return "Failed to located Podfile in the current directory." 26 | case .failedToReadContents: 27 | return "Failed to read contents of Podfile." 28 | case .corruptedStructureError: 29 | return "Corrupted structure of Podfile detected." 30 | case .failedToUpdatePodfile: 31 | return "Failed to update Podfile." 32 | } 33 | } 34 | } 35 | 36 | // MARK: - Adder 37 | 38 | public final class Adder { 39 | 40 | private var runner = ScriptRunner() 41 | private typealias Error = AdderError 42 | 43 | // MARK: - Init 44 | 45 | public init() { 46 | } 47 | 48 | public func perform(pod: String, version: Double, path: String) { 49 | addPod(pod, with: version, and: path) 50 | } 51 | 52 | // MARK: - Private 53 | 54 | private func addPod(_ pod: String, with version: Double, and path: String) { 55 | do { 56 | var podfile = try getContents(from: path) 57 | let index = try getIndex(for: podfile) 58 | var entry = "\n pod '\(pod)'" 59 | entry += (version != -1.0 ? ", '~> \(version)'" : "") 60 | podfile.insert(contentsOf: entry.characters, at: String.Index.init(encodedOffset: index)) 61 | try updatePodfile(with: podfile, at: path) 62 | } catch { 63 | print(error.localizedDescription) 64 | } 65 | } 66 | 67 | private func updatePodfile(with contents: String, at path: String) throws { 68 | guard let file = try? Folder(path: path).file(named: "Podfile") else { 69 | throw Error.failedToLocatePodfile 70 | } 71 | do { 72 | try file.write(data: contents.data(using: .utf8)!) 73 | } catch { 74 | throw Error.failedToUpdatePodfile 75 | } 76 | } 77 | 78 | private func getIndex(for podfile: String) throws -> Int { 79 | guard let lastIndex = podfile.lastIndex(of: "end") else { 80 | throw Error.corruptedStructureError 81 | } 82 | return lastIndex - 1 83 | } 84 | 85 | private func getContents(from path: String) throws -> String { 86 | guard let file = try? Folder(path: path).file(named: "Podfile") else { 87 | throw Error.failedToLocatePodfile 88 | } 89 | guard let contents = try? file.readAsString() else { 90 | throw Error.failedToReadContents 91 | } 92 | return contents 93 | } 94 | } 95 | --------------------------------------------------------------------------------