├── .gitignore ├── swiff.xcodeproj ├── project.xcworkspace │ └── contents.xcworkspacedata ├── xcshareddata │ └── xcschemes │ │ └── swiff.xcscheme └── project.pbxproj ├── Makefile ├── Package.swift ├── LICENSE ├── README.md └── Sources └── swiff └── main.swift /.gitignore: -------------------------------------------------------------------------------- 1 | xcuserdata/ 2 | .build/ 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /swiff.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PREFIX?=/usr/local 2 | INSTALL_NAME = swiff 3 | SOURCE_FILE="Sources/swiff/main.swift" 4 | 5 | install: build install_bin 6 | 7 | build: 8 | xcrun swiftc -o swiff ${SOURCE_FILE} 9 | 10 | install_bin: 11 | mkdir -p $(PREFIX)/bin 12 | install ./$(INSTALL_NAME) $(PREFIX)/bin 13 | rm ./$(INSTALL_NAME) 14 | 15 | uninstall: 16 | rm -f $(INSTALL_PATH) 17 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | // swift-tools-version:4.0 2 | 3 | import PackageDescription 4 | 5 | let package = Package( 6 | name: "swiff", 7 | products: [ 8 | .executable(name: "swiff", targets: ["swiff"]), 9 | ], 10 | dependencies: [ 11 | 12 | ], 13 | targets: [ 14 | .target( 15 | name: "swiff", 16 | dependencies: [] 17 | ), 18 | ] 19 | ) 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Agens AS 4 | Copyright (c) 2018 Håvard Fossli hfossli@gmail.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /swiff.xcodeproj/xcshareddata/xcschemes/swiff.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 58 | 64 | 65 | 66 | 67 | 68 | 69 | 75 | 77 | 83 | 84 | 85 | 86 | 88 | 89 | 92 | 93 | 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | macOS only 2 | 3 | # swiff 4 | 5 | 6 | 7 | Why not let the computer do all that diffing of timestamps you tend to do manually? 8 | 9 | ## 👋 Usage 10 | 11 | ### Live with any command 12 | ```sh 13 | command | swiff 14 | ``` 15 | 16 | Try it out 17 | ```sh 18 | while true; do echo "Foo"; sleep $[ ($RANDOM % 3) + 1 ]s; done | swiff 19 | ``` 20 | 21 | ### With [fastlane](https://github.com/fastlane/fastlane) 22 | ```sh 23 | fastlane build | swiff --fastlane 24 | ``` 25 | 26 | Or even shorter 27 | ```sh 28 | fastlane build | swiff -f 29 | ``` 30 | 31 | Or maybe you have an old build log from fastlane? 32 | ```sh 33 | cat build.log | swiff -f 34 | ``` 35 | (Swiff parses the timestamps produced by fastlane) 36 | 37 | ### With xcodebuild 38 | 39 | ```swift 40 | xcrun xcodebuild -project "MyApp.xcodeproj" -scheme "MyApp" | xcpretty | swiff 41 | ``` 42 | 43 | ## 🤲 Example output 44 | 45 | ### Summary 46 | Useful summary at the end with most important highlights 47 | 48 | 49 | 50 | 51 | ## ✌️ Install 52 | 53 | ### Globally by oneliner 54 | ```sh 55 | git clone git@github.com:agens-no/swiff.git && cd swiff && make && cd .. && rm -rf swiff/ 56 | ``` 57 | 58 | You may now type `swiff help` from any directory in terminal to verify that the install is complete 59 | 60 |
61 | What is the oneliner doing? 62 | 63 | 1. Uses git to clone `swiff` to a directory `swiff` in your current directory 64 | 2. moves in to the created `swiff` folder 65 | 3. builds `swiff` using the Makefile (basically compiling `Sources/swiff/main.swift` and installing `swiff` at `/usr/local/bin/swiff`) 66 | 4. moves back out of the folder 67 | 5. deletes the `swiff` folder 68 | 69 |
70 | 71 | ### Globally by cloning 72 | ```sh 73 | git clone git@github.com:agens-no/swiff.git 74 | cd swiff 75 | make 76 | ``` 77 | 78 | You may now type `swiff help` from any directory in terminal to verify that the install is complete 79 | 80 | ### Locally by oneliner 81 | 82 | ```sh 83 | curl --fail https://raw.githubusercontent.com/agens-no/swiff/master/Sources/swiff/main.swift > swiff.swift && swiftc -o swiff swiff.swift && rm swiff.swift 84 | ``` 85 | 86 | You may now type `./swiff help` from your current directory and use it like `fastlane build | ./swiff -f` 87 | 88 |
89 | What is the oneliner doing? 90 | 91 | 1. Uses curl to copy `Sources/swiff/main.swift` to a file called `swiff.swift` in your current directory 92 | 2. builds it using your current swift tooling 93 | 3. deletes swiff.swift 94 | 95 |
96 | 97 | ### Using [Mint](https://github.com/yonaskolb/mint) 98 | ``` 99 | $ mint install agens-no/swiff 100 | ``` 101 | 102 | ### Installation issues? 103 | 104 | Might be because of requirements: Swift 4, Xcode, macOS 105 | 106 | [Create a new issue](https://github.com/agens-no/swiff/issues/new) and let me know! 107 | 108 | ## ✊ Advanced usage 109 | 110 | ``` 111 | Usage: swiff [-l low] [-m medium] [-h high] [-r reset-mark] [-d diff-mode] [-s summary-limit] [-f --fastlane] 112 | -l, --low Threshold in seconds for low duration color formatting (default: 1) 113 | -m, --medium Threshold in seconds for medium duration color formatting (default: 5) 114 | -h, --high Threshold in seconds for high duration color formatting (default: 10) 115 | -r, --reset-mark String match to reset total counter (default: none) 116 | -d, --diff-mode Valid options is "live" or "fastlane" (default: live) 117 | -s, --summary-limit Maximum number of lines in summary (default: 20) 118 | 119 | -f, --fastlane Shortcut for --diff-mode fastlane --reset-mark "Step :" 120 | 121 | Example: cat build.log | swiff --low 1 --medium 5 --high 10 --reset-mark "Step: " --diff-mode live --summary-limit 20 122 | 123 | Example: fastlane build | swiff -f 124 | ``` 125 | 126 | ## 🤙 License 127 | 128 | MIT 129 | -------------------------------------------------------------------------------- /swiff.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 48; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | A32B1E2121334AE20022FF62 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = A32B1E2021334AE20022FF62 /* main.swift */; }; 11 | /* End PBXBuildFile section */ 12 | 13 | /* Begin PBXCopyFilesBuildPhase section */ 14 | A32B1E0D21334A6F0022FF62 /* CopyFiles */ = { 15 | isa = PBXCopyFilesBuildPhase; 16 | buildActionMask = 2147483647; 17 | dstPath = /usr/share/man/man1/; 18 | dstSubfolderSpec = 0; 19 | files = ( 20 | ); 21 | runOnlyForDeploymentPostprocessing = 1; 22 | }; 23 | /* End PBXCopyFilesBuildPhase section */ 24 | 25 | /* Begin PBXFileReference section */ 26 | A32B1E0F21334A6F0022FF62 /* swiff */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = swiff; sourceTree = BUILT_PRODUCTS_DIR; }; 27 | A32B1E2021334AE20022FF62 /* main.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; 28 | /* End PBXFileReference section */ 29 | 30 | /* Begin PBXFrameworksBuildPhase section */ 31 | A32B1E0C21334A6F0022FF62 /* Frameworks */ = { 32 | isa = PBXFrameworksBuildPhase; 33 | buildActionMask = 2147483647; 34 | files = ( 35 | ); 36 | runOnlyForDeploymentPostprocessing = 0; 37 | }; 38 | /* End PBXFrameworksBuildPhase section */ 39 | 40 | /* Begin PBXGroup section */ 41 | A32B1E0621334A6F0022FF62 = { 42 | isa = PBXGroup; 43 | children = ( 44 | A32B1E1E21334AE20022FF62 /* Sources */, 45 | A32B1E1021334A6F0022FF62 /* Products */, 46 | ); 47 | sourceTree = ""; 48 | }; 49 | A32B1E1021334A6F0022FF62 /* Products */ = { 50 | isa = PBXGroup; 51 | children = ( 52 | A32B1E0F21334A6F0022FF62 /* swiff */, 53 | ); 54 | name = Products; 55 | sourceTree = ""; 56 | }; 57 | A32B1E1E21334AE20022FF62 /* Sources */ = { 58 | isa = PBXGroup; 59 | children = ( 60 | A32B1E1F21334AE20022FF62 /* swiff */, 61 | ); 62 | path = Sources; 63 | sourceTree = ""; 64 | }; 65 | A32B1E1F21334AE20022FF62 /* swiff */ = { 66 | isa = PBXGroup; 67 | children = ( 68 | A32B1E2021334AE20022FF62 /* main.swift */, 69 | ); 70 | path = swiff; 71 | sourceTree = ""; 72 | }; 73 | /* End PBXGroup section */ 74 | 75 | /* Begin PBXNativeTarget section */ 76 | A32B1E0E21334A6F0022FF62 /* swiff */ = { 77 | isa = PBXNativeTarget; 78 | buildConfigurationList = A32B1E1621334A6F0022FF62 /* Build configuration list for PBXNativeTarget "swiff" */; 79 | buildPhases = ( 80 | A32B1E0B21334A6F0022FF62 /* Sources */, 81 | A32B1E0C21334A6F0022FF62 /* Frameworks */, 82 | A32B1E0D21334A6F0022FF62 /* CopyFiles */, 83 | ); 84 | buildRules = ( 85 | ); 86 | dependencies = ( 87 | ); 88 | name = swiff; 89 | productName = swiff; 90 | productReference = A32B1E0F21334A6F0022FF62 /* swiff */; 91 | productType = "com.apple.product-type.tool"; 92 | }; 93 | /* End PBXNativeTarget section */ 94 | 95 | /* Begin PBXProject section */ 96 | A32B1E0721334A6F0022FF62 /* Project object */ = { 97 | isa = PBXProject; 98 | attributes = { 99 | LastSwiftUpdateCheck = 0910; 100 | LastUpgradeCheck = 0910; 101 | ORGANIZATIONNAME = "Håvard Fossli"; 102 | TargetAttributes = { 103 | A32B1E0E21334A6F0022FF62 = { 104 | CreatedOnToolsVersion = 9.1; 105 | ProvisioningStyle = Automatic; 106 | }; 107 | }; 108 | }; 109 | buildConfigurationList = A32B1E0A21334A6F0022FF62 /* Build configuration list for PBXProject "swiff" */; 110 | compatibilityVersion = "Xcode 8.0"; 111 | developmentRegion = en; 112 | hasScannedForEncodings = 0; 113 | knownRegions = ( 114 | en, 115 | ); 116 | mainGroup = A32B1E0621334A6F0022FF62; 117 | productRefGroup = A32B1E1021334A6F0022FF62 /* Products */; 118 | projectDirPath = ""; 119 | projectRoot = ""; 120 | targets = ( 121 | A32B1E0E21334A6F0022FF62 /* swiff */, 122 | ); 123 | }; 124 | /* End PBXProject section */ 125 | 126 | /* Begin PBXSourcesBuildPhase section */ 127 | A32B1E0B21334A6F0022FF62 /* Sources */ = { 128 | isa = PBXSourcesBuildPhase; 129 | buildActionMask = 2147483647; 130 | files = ( 131 | A32B1E2121334AE20022FF62 /* main.swift in Sources */, 132 | ); 133 | runOnlyForDeploymentPostprocessing = 0; 134 | }; 135 | /* End PBXSourcesBuildPhase section */ 136 | 137 | /* Begin XCBuildConfiguration section */ 138 | A32B1E1421334A6F0022FF62 /* Debug */ = { 139 | isa = XCBuildConfiguration; 140 | buildSettings = { 141 | ALWAYS_SEARCH_USER_PATHS = NO; 142 | CLANG_ANALYZER_NONNULL = YES; 143 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 144 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 145 | CLANG_CXX_LIBRARY = "libc++"; 146 | CLANG_ENABLE_MODULES = YES; 147 | CLANG_ENABLE_OBJC_ARC = YES; 148 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 149 | CLANG_WARN_BOOL_CONVERSION = YES; 150 | CLANG_WARN_COMMA = YES; 151 | CLANG_WARN_CONSTANT_CONVERSION = YES; 152 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 153 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 154 | CLANG_WARN_EMPTY_BODY = YES; 155 | CLANG_WARN_ENUM_CONVERSION = YES; 156 | CLANG_WARN_INFINITE_RECURSION = YES; 157 | CLANG_WARN_INT_CONVERSION = YES; 158 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 159 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 160 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 161 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 162 | CLANG_WARN_STRICT_PROTOTYPES = YES; 163 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 164 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 165 | CLANG_WARN_UNREACHABLE_CODE = YES; 166 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 167 | CODE_SIGN_IDENTITY = "-"; 168 | COPY_PHASE_STRIP = NO; 169 | DEBUG_INFORMATION_FORMAT = dwarf; 170 | ENABLE_STRICT_OBJC_MSGSEND = YES; 171 | ENABLE_TESTABILITY = YES; 172 | GCC_C_LANGUAGE_STANDARD = gnu11; 173 | GCC_DYNAMIC_NO_PIC = NO; 174 | GCC_NO_COMMON_BLOCKS = YES; 175 | GCC_OPTIMIZATION_LEVEL = 0; 176 | GCC_PREPROCESSOR_DEFINITIONS = ( 177 | "DEBUG=1", 178 | "$(inherited)", 179 | ); 180 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 181 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 182 | GCC_WARN_UNDECLARED_SELECTOR = YES; 183 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 184 | GCC_WARN_UNUSED_FUNCTION = YES; 185 | GCC_WARN_UNUSED_VARIABLE = YES; 186 | MACOSX_DEPLOYMENT_TARGET = 10.13; 187 | MTL_ENABLE_DEBUG_INFO = YES; 188 | ONLY_ACTIVE_ARCH = YES; 189 | SDKROOT = macosx; 190 | SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; 191 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 192 | }; 193 | name = Debug; 194 | }; 195 | A32B1E1521334A6F0022FF62 /* Release */ = { 196 | isa = XCBuildConfiguration; 197 | buildSettings = { 198 | ALWAYS_SEARCH_USER_PATHS = NO; 199 | CLANG_ANALYZER_NONNULL = YES; 200 | CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; 201 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; 202 | CLANG_CXX_LIBRARY = "libc++"; 203 | CLANG_ENABLE_MODULES = YES; 204 | CLANG_ENABLE_OBJC_ARC = YES; 205 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 206 | CLANG_WARN_BOOL_CONVERSION = YES; 207 | CLANG_WARN_COMMA = YES; 208 | CLANG_WARN_CONSTANT_CONVERSION = YES; 209 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 210 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES; 211 | CLANG_WARN_EMPTY_BODY = YES; 212 | CLANG_WARN_ENUM_CONVERSION = YES; 213 | CLANG_WARN_INFINITE_RECURSION = YES; 214 | CLANG_WARN_INT_CONVERSION = YES; 215 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 216 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 217 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 218 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 219 | CLANG_WARN_STRICT_PROTOTYPES = YES; 220 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 221 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; 222 | CLANG_WARN_UNREACHABLE_CODE = YES; 223 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 224 | CODE_SIGN_IDENTITY = "-"; 225 | COPY_PHASE_STRIP = NO; 226 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 227 | ENABLE_NS_ASSERTIONS = NO; 228 | ENABLE_STRICT_OBJC_MSGSEND = YES; 229 | GCC_C_LANGUAGE_STANDARD = gnu11; 230 | GCC_NO_COMMON_BLOCKS = YES; 231 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 232 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 233 | GCC_WARN_UNDECLARED_SELECTOR = YES; 234 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 235 | GCC_WARN_UNUSED_FUNCTION = YES; 236 | GCC_WARN_UNUSED_VARIABLE = YES; 237 | MACOSX_DEPLOYMENT_TARGET = 10.13; 238 | MTL_ENABLE_DEBUG_INFO = NO; 239 | SDKROOT = macosx; 240 | SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; 241 | }; 242 | name = Release; 243 | }; 244 | A32B1E1721334A6F0022FF62 /* Debug */ = { 245 | isa = XCBuildConfiguration; 246 | buildSettings = { 247 | CODE_SIGN_STYLE = Automatic; 248 | PRODUCT_NAME = "$(TARGET_NAME)"; 249 | SWIFT_VERSION = 4.0; 250 | }; 251 | name = Debug; 252 | }; 253 | A32B1E1821334A6F0022FF62 /* Release */ = { 254 | isa = XCBuildConfiguration; 255 | buildSettings = { 256 | CODE_SIGN_STYLE = Automatic; 257 | PRODUCT_NAME = "$(TARGET_NAME)"; 258 | SWIFT_VERSION = 4.0; 259 | }; 260 | name = Release; 261 | }; 262 | /* End XCBuildConfiguration section */ 263 | 264 | /* Begin XCConfigurationList section */ 265 | A32B1E0A21334A6F0022FF62 /* Build configuration list for PBXProject "swiff" */ = { 266 | isa = XCConfigurationList; 267 | buildConfigurations = ( 268 | A32B1E1421334A6F0022FF62 /* Debug */, 269 | A32B1E1521334A6F0022FF62 /* Release */, 270 | ); 271 | defaultConfigurationIsVisible = 0; 272 | defaultConfigurationName = Release; 273 | }; 274 | A32B1E1621334A6F0022FF62 /* Build configuration list for PBXNativeTarget "swiff" */ = { 275 | isa = XCConfigurationList; 276 | buildConfigurations = ( 277 | A32B1E1721334A6F0022FF62 /* Debug */, 278 | A32B1E1821334A6F0022FF62 /* Release */, 279 | ); 280 | defaultConfigurationIsVisible = 0; 281 | defaultConfigurationName = Release; 282 | }; 283 | /* End XCConfigurationList section */ 284 | }; 285 | rootObject = A32B1E0721334A6F0022FF62 /* Project object */; 286 | } 287 | -------------------------------------------------------------------------------- /Sources/swiff/main.swift: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env xcrun swift 2 | 3 | import Foundation 4 | 5 | struct ANSIColors { 6 | static let clear = "\u{001B}[0m" 7 | static let red = "\u{001B}[38;5;160m" 8 | static let orange = "\u{001B}[38;5;202m" 9 | static let yellow = "\u{001B}[38;5;220m" 10 | static let green = "\u{001B}[0;32m" 11 | static let blue = "\u{001B}[0;36m" 12 | static let grey = "\u{001B}[38;5;237m" 13 | } 14 | 15 | struct Config { 16 | enum DiffMode: String { 17 | case fastlane 18 | case live 19 | } 20 | var scriptName = "time-diff" 21 | var diffMode = DiffMode.live 22 | var low = 1 23 | var medium = 5 24 | var high = 10 25 | var summaryLimit = 20 26 | var resetRegex: NSRegularExpression? 27 | 28 | func colorCode(duration: TimeInterval) -> String { 29 | switch Int(duration) { 30 | case high...: 31 | return ANSIColors.red 32 | case medium...: 33 | return ANSIColors.orange 34 | case low...: 35 | return ANSIColors.yellow 36 | default: 37 | return ANSIColors.grey 38 | } 39 | } 40 | 41 | func resetMatch(_ string: String) -> Bool { 42 | return config.resetRegex?.firstMatch(in: string, range: NSMakeRange(0, string.count)) != nil 43 | } 44 | } 45 | 46 | func usage(error: String) -> Never { 47 | let scriptLocation = CommandLine.arguments.first ?? "time-diff.swift" 48 | print(ANSIColors.red, "👉 ", error, ANSIColors.clear, separator: "") 49 | print(ANSIColors.red, "Script failed ", scriptLocation, ANSIColors.clear, separator: "") 50 | let defaultConfig = Config() 51 | print(""" 52 | 53 | Usage: \(scriptLocation) [-l low] [-m medium] [-h high] [-r reset-mark] [-d diff-mode] [-s summary-limit] [-f --fastlane] 54 | -l, --low Threshold in seconds for low duration color formatting (default: \(defaultConfig.low)) 55 | -m, --medium Threshold in seconds for medium duration color formatting (default: \(defaultConfig.medium)) 56 | -h, --high Threshold in seconds for high duration color formatting (default: \(defaultConfig.high)) 57 | -r, --reset-mark String match to reset total counter (default: none) 58 | -d, --diff-mode Valid options is "live" or "fastlane" (default: live) 59 | -s, --summary-limit Maximum number of lines in summary (default: \(defaultConfig.summaryLimit)) 60 | 61 | -f, --fastlane Shortcut for --diff-mode fastlane --reset-mark "Step :" 62 | 63 | Example: cat build.log | \(scriptLocation) --low \(defaultConfig.low) --medium \(defaultConfig.medium) --high \(defaultConfig.high) --reset-mark "Step: " --diff-mode \(defaultConfig.diffMode.rawValue) --summary-limit \(defaultConfig.summaryLimit) 64 | 65 | Example: fastlane build | \(scriptLocation) -f 66 | 67 | """) 68 | exit(1) 69 | } 70 | 71 | func parseCLIArguments() -> Config { 72 | var config = Config() 73 | var arguments = CommandLine.arguments 74 | arguments.removeFirst() 75 | while arguments.isEmpty == false { 76 | let argument = arguments.removeFirst() 77 | switch argument { 78 | case "-d", "--diff-mode": 79 | guard !arguments.isEmpty else { 80 | usage(error: "Missing value on option option") 81 | } 82 | guard let diffMode = Config.DiffMode(rawValue: arguments.removeFirst().lowercased()) else { 83 | usage(error: "Bad value sent to option option") 84 | } 85 | config.diffMode = diffMode 86 | case "-r", "--reset-mark": 87 | guard !arguments.isEmpty else { 88 | usage(error: "Missing value on --reset mark") 89 | } 90 | do { 91 | config.resetRegex = try NSRegularExpression(pattern: arguments.removeFirst()) 92 | } catch { 93 | usage(error: "Bad regex pattern passed to \(argument) option. Error: \(error.localizedDescription))") 94 | } 95 | case "-l", "--low": 96 | guard !arguments.isEmpty, let value = Int(arguments.removeFirst()) else { 97 | usage(error: "Bad value passed to \(argument) option") 98 | } 99 | config.low = value 100 | case "-m", "--medium": 101 | guard !arguments.isEmpty, let value = Int(arguments.removeFirst()) else { 102 | usage(error: "Bad value passed to \(argument) option") 103 | } 104 | config.medium = value 105 | case "-h", "--high": 106 | guard !arguments.isEmpty, let value = Int(arguments.removeFirst()) else { 107 | usage(error: "Bad value passed to \(argument) option") 108 | } 109 | config.high = value 110 | case "-s", "--summary-limit": 111 | guard !arguments.isEmpty, let value = Int(arguments.removeFirst()) else { 112 | usage(error: "Bad value passed to \(argument) option") 113 | } 114 | config.summaryLimit = value 115 | case "-f", "--fastlane": 116 | if config.resetRegex == nil { 117 | config.resetRegex = try! NSRegularExpression(pattern: "Step: ") 118 | } 119 | config.diffMode = .fastlane 120 | default: 121 | usage(error: "Unknown argument \"\(argument)\"") 122 | } 123 | } 124 | return config 125 | } 126 | 127 | extension String { 128 | func leftPadding(toLength: Int, withPad character: Character) -> String { 129 | return count < toLength ? String(repeating: character, count: toLength - count) + self : self 130 | } 131 | } 132 | 133 | func parseFastlaneDate(string: String) -> TimeInterval? { 134 | let scanner = Scanner(string: string) 135 | var hours: Int = 0 136 | var minutes: Int = 0 137 | var seconds: Int = 0 138 | if scanner.scanInt(&hours), 139 | scanner.scanString(":", into: nil), 140 | scanner.scanInt(&minutes), 141 | scanner.scanString(":", into: nil), 142 | scanner.scanInt(&seconds) { 143 | return TimeInterval(seconds) + (TimeInterval(minutes) * 60) + (TimeInterval(hours) * 60 * 60) 144 | } 145 | return nil 146 | } 147 | 148 | class Chapter { 149 | struct Offender { 150 | var duration: TimeInterval 151 | var timestamp: TimeInterval 152 | var line: String 153 | } 154 | var name: String 155 | var offenders: [Offender] = [] 156 | var endTime: TimeInterval? 157 | var startTime: TimeInterval? 158 | var duration: TimeInterval? { 159 | if let endTime = endTime, let startTime = startTime { 160 | return endTime - startTime 161 | } 162 | return nil 163 | } 164 | var limit: Int { 165 | didSet { 166 | trim() 167 | } 168 | } 169 | 170 | init(name: String, limit: Int) { 171 | self.name = name 172 | self.limit = limit 173 | } 174 | 175 | func addLineIfSlow(duration: TimeInterval, minimumLimit: TimeInterval, timestamp: TimeInterval, line: String) { 176 | guard duration > minimumLimit else { 177 | return 178 | } 179 | if duration > offenders.last?.duration ?? 0 || offenders.count < limit { 180 | offenders.append(Offender(duration: duration, timestamp: timestamp, line: line)) 181 | sortOffendersByDuration() 182 | trim() 183 | } 184 | } 185 | 186 | func sortOffendersByTimeStamp() { 187 | offenders.sort { $0.timestamp < $1.timestamp } 188 | } 189 | 190 | func sortOffendersByDuration() { 191 | offenders.sort { $0.duration > $1.duration } 192 | } 193 | 194 | func trim() { 195 | if offenders.count > limit { 196 | offenders.removeLast(offenders.count - limit) 197 | } 198 | } 199 | } 200 | 201 | let config = parseCLIArguments() 202 | var lastTime: Double? 203 | var time: Double? 204 | var chapter: Chapter = Chapter(name: "First chapter\n", limit: config.summaryLimit) 205 | var total: Chapter = Chapter(name: "Everything\n", limit: config.summaryLimit) 206 | var chapters: [Chapter] = [chapter] 207 | var lastLine: String? 208 | while let line = readLine(strippingNewline: false) { 209 | switch config.diffMode { 210 | case .fastlane: 211 | let dateString = String(line.prefix(9).suffix(8)) 212 | time = parseFastlaneDate(string: dateString) ?? time 213 | case .live: 214 | time = Date().timeIntervalSinceReferenceDate 215 | } 216 | if lastTime == nil { 217 | lastTime = time 218 | } 219 | if chapter.startTime == nil { 220 | chapter.startTime = time 221 | } 222 | if total.startTime == nil { 223 | total.startTime = time 224 | } 225 | if config.resetMatch(line) { 226 | print(ANSIColors.blue, 227 | "Reseting timer ---------------- ", 228 | ANSIColors.clear, 229 | line, separator: "", terminator: "") 230 | if let time = time { 231 | chapter.endTime = time 232 | } 233 | chapter = Chapter(name: line, limit: config.summaryLimit) 234 | chapters.append(chapter) 235 | chapter.startTime = time 236 | } 237 | else if let time = time, let chapterTime = chapter.startTime { 238 | let lastDiff = time - (lastTime ?? 0) 239 | let chapterDiff = time - chapterTime 240 | print(config.colorCode(duration: lastDiff), 241 | String(format: "+ %.0f", lastDiff).leftPadding(toLength: 7, withPad: " "), 242 | " seconds", 243 | ANSIColors.grey, 244 | " = ", 245 | String(format: "%.0f", chapterDiff).leftPadding(toLength: 5, withPad: " "), 246 | " seconds ", 247 | ANSIColors.clear, 248 | line, separator: "", terminator: "") 249 | if let lastLine = lastLine { 250 | chapter.addLineIfSlow(duration: lastDiff, minimumLimit: TimeInterval(config.low), timestamp: time, line: lastLine) 251 | total.addLineIfSlow(duration: lastDiff, minimumLimit: TimeInterval(config.low), timestamp: time, line: lastLine) 252 | } 253 | } 254 | else { 255 | print(" ", line, separator: "", terminator: "") 256 | } 257 | lastTime = time 258 | lastLine = line 259 | } 260 | 261 | chapter.endTime = time 262 | total.endTime = time 263 | 264 | let onlyOneChapterBesidesTotal = chapters.count == 2 265 | if onlyOneChapterBesidesTotal == true { 266 | chapters.removeLast() 267 | } 268 | 269 | func printSummary(chapter: Chapter) { 270 | print(ANSIColors.grey, 271 | String(format: "%.0f", chapter.duration ?? 0).leftPadding(toLength: 6, withPad: " "), 272 | " seconds in total ", 273 | ANSIColors.blue, 274 | "# ", 275 | ANSIColors.clear, 276 | chapter.name, separator: "", terminator: "") 277 | for offender in chapter.offenders { 278 | print(config.colorCode(duration: offender.duration), 279 | String(format: "%.0f", offender.duration).leftPadding(toLength: 15, withPad: " "), 280 | " seconds ", 281 | ANSIColors.blue, 282 | " ", 283 | ANSIColors.clear, 284 | offender.line, separator: "", terminator: "") 285 | } 286 | if chapter.offenders.count == 0 { 287 | print(ANSIColors.grey, 288 | "".leftPadding(toLength: 15, withPad: " "), 289 | " (No significant events)\n", 290 | ANSIColors.clear, separator: "", terminator: "") 291 | } 292 | print() 293 | } 294 | 295 | if config.summaryLimit > 0 { 296 | print("\n\n", ANSIColors.blue, "========================= Summary by timestamp =========================", ANSIColors.clear, "\n", separator: "") 297 | for chapter in chapters { 298 | chapter.sortOffendersByTimeStamp() 299 | printSummary(chapter: chapter) 300 | } 301 | 302 | print("\n", ANSIColors.blue, "========================= Summary by duration ==========================", ANSIColors.clear, "\n", separator: "") 303 | for chapter in chapters { 304 | chapter.sortOffendersByDuration() 305 | printSummary(chapter: chapter) 306 | } 307 | } 308 | --------------------------------------------------------------------------------