├── .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 |
--------------------------------------------------------------------------------