diff --git a/.bundle/config b/.bundle/config new file mode 100644 index 0000000..8d2dd95 --- /dev/null +++ b/.bundle/config @@ -0,0 +1,6 @@ +--- +BUNDLE_CLEAN: "true" +BUNDLE_BIN: "bin" +BUNDLE_JOBS: "8" +BUNDLE_DISABLE_SHARED_GEMS: "true" +BUNDLE_PATH: ".rubygems" diff --git a/.gitignore b/.gitignore index 64661b1..00f11e5 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,10 @@ xcuserdata *.hmap *.ipa +# Bundler +/.rubygems/ +/bin/ + # CocoaPods # # We recommend against adding the Pods directory to your .gitignore. However @@ -43,6 +47,25 @@ xcuserdata Carthage/Build +# CocoaSeeds +!Seeds/Seedfile.lock +Seeds/**/.git* +Seeds/**/.swift-version +Seeds/**/.travis.yml +Seeds/**/*.md +Seeds/**/Cartfile* +Seeds/**/Carthage/ +Seeds/**/LICENSE* +Seeds/**/Package.* +Seeds/**/*Info.plist +Seeds/**/*.podspec +Seeds/**/*.xc* +Seeds/**/Tests/ +# Commandant +!Seeds/Commandant/Source/*.swift +# Result +!Seeds/Result/Result/*.swift + ## https://github.com/github/gitignore/blob/master/Global/OSX.gitignore .DS_Store .AppleDouble @@ -70,7 +93,5 @@ Network Trash Folder Temporary Items .apdisk -mas-cli.zip -mas-cli.dSYM.zip -mas.xcarchive.zip - +# Build artifacts +*.zip diff --git a/.travis.yml b/.travis.yml index 7c0fa9b..7c572ee 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: objective-c -xcode_sdk: macosx10.11 -osx_image: xcode8.2 +xcode_sdk: macosx10.13 +osx_image: xcode9.2 env: global: @@ -8,6 +8,9 @@ env: - LC_ALL=en_US.UTF-8 - LANGUAGE=en_US.UTF-8 +install: + - bundle install + script: - script/build diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..5e7b9cc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,71 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) +and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +## [v1.3.1] Better Errors - 2016-09-25 +- Descriptive error messages instead of exit codes +- Fixed nullability issue with `list` command +- Simpler upgrade checking + +## [v1.3.0] Multiple app install - 2016-09-14 +- Fix install of Free apps (#19) +- Install / Upgrade multiple apps at once +- Skip Install if the app is already installed + +## [v1.2.2] Secure Password entry - 2016-09-14 +- Support reading password from STDIN +- Fix building with Swift 2.3/Xcode 8 + +## [v1.2.1] - 2016-09-13 +- Support reading password from STDIN +- Fix building with Swift 2.3/Xcode 8 + +## [v1.2.0] Search - 2016-04-16 +- `search` command +- Fix `mas list` illegal instruction (#16) + +## [v1.1.3] - 2016-02-21 +- Fix Illegal Instruction: 4 error (#10) + +## [v1.1.2] Upload dSYM correctly - 2016-02-21 +- Move the dSYM to the xcarchive + +## [v1.1.1] Upload dSYM - 2016-02-21 +- Upload dSYM from Travis release + +## [v1.1.0] Sign In - 2016-02-13 +- Added `signin` command (#3) +- Added `signout` command + +## [v1.0.2] Upgrade all - 2015-12-30 +### Features +- Added `upgrade` command (#1) + +### Fixes +- Updated to latest version of Commandant +- Broken `install` command after updating Commandant + +## [v1.0.1] - 2015-12-30 +- Bump version to 1.0.1 + +## [v1.0.0] - 2015-09-20 +- Initial Release + +[Unreleased]: https://github.com/mas-cli/mas/compare/v1.3.1...HEAD +[v1.3.1]: https://github.com/mas-cli/mas/compare/v1.3.0...v1.3.1 +[v1.3.0]: https://github.com/mas-cli/mas/compare/v1.2.2...v1.3.0 +[v1.2.2]: https://github.com/mas-cli/mas/compare/v1.2.1...v1.2.2 +[v1.2.1]: https://github.com/mas-cli/mas/compare/v1.2.0...v1.2.1 +[v1.2.0]: https://github.com/mas-cli/mas/compare/v1.1.2...v1.2.0 +[v1.1.3]: https://github.com/mas-cli/mas/compare/v1.1.2...v1.1.3 +[v1.1.2]: https://github.com/mas-cli/mas/compare/v1.1.1...v1.1.2 +[v1.1.1]: https://github.com/mas-cli/mas/compare/v1.1.0...v1.1.1 +[v1.1.0]: https://github.com/mas-cli/mas/compare/v1.0.2...v1.1.0 +[v1.0.2]: https://github.com/mas-cli/mas/compare/v1.0.1...v1.0.2 +[v1.0.1]: https://github.com/mas-cli/mas/compare/v1.0.0...v1.0.1 +[v1.0.0]: https://github.com/mas-cli/mas/compare/7e0e18d8335cf5eee6a162ea7981ad02ca4294b2...v1.0.0 diff --git a/Gemfile.lock b/Gemfile.lock index 1ffef0c..688ae01 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,30 +1,21 @@ GEM remote: https://rubygems.org/ specs: - activesupport (5.0.0.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) - minitest (~> 5.1) - tzinfo (~> 1.1) - claide (1.0.0) - cocoaseeds (0.8.0) - colorize (~> 0.7.0) + CFPropertyList (2.3.6) + claide (1.0.2) + cocoaseeds (0.8.3) + colored2 (~> 3.1) xcodeproj (>= 0.28) - colored (1.2) - colorize (0.7.7) - concurrent-ruby (1.0.2) - i18n (0.7.0) - minitest (5.9.0) - rouge (1.11.1) - thread_safe (0.3.5) - tzinfo (1.2.2) - thread_safe (~> 0.1) - xcodeproj (1.3.1) - activesupport (>= 3) - claide (>= 1.0.0, < 2.0) - colored (~> 1.2) - xcpretty (0.2.2) - rouge (~> 1.8) + colored2 (3.1.2) + nanaimo (0.2.3) + rouge (2.0.7) + xcodeproj (1.5.4) + CFPropertyList (~> 2.3.3) + claide (>= 1.0.2, < 2.0) + colored2 (~> 3.1) + nanaimo (~> 0.2.3) + xcpretty (0.2.8) + rouge (~> 2.0.7) PLATFORMS ruby @@ -34,4 +25,4 @@ DEPENDENCIES xcpretty BUNDLED WITH - 1.12.5 + 1.16.1 diff --git a/README.md b/README.md index 0c7e975..d041f10 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,9 @@ # mas-cli -A simple command line interface for the Mac App Store. Designed for scripting -and automation. +A simple command line interface for the Mac App Store. Designed for scripting and automation. + +[![Build Status](https://travis-ci.org/mas-cli/mas.svg?branch=master)](https://travis-ci.org/mas-cli/mas) ## Install diff --git a/Seedfile b/Seedfile index 493066f..a2dfb86 100644 --- a/Seedfile +++ b/Seedfile @@ -1,2 +1,3 @@ -github "carthage/Commandant", "0.11.1", :files => "Sources/Commandant/*.swift" -github "antitypical/Result", "3.0.0", :files => "Result/*.swift" +# Commandant 0.12.0 doesn't have Swift 4 fixes yet +github "carthage/Commandant", "master", files: "Sources/Commandant/*.swift" +github "antitypical/Result", "3.2.4", files: "Result/*.swift" diff --git a/Seeds/Commandant/Sources/Commandant/Argument.swift b/Seeds/Commandant/Sources/Commandant/Argument.swift index 9c7d293..ceba4cf 100644 --- a/Seeds/Commandant/Sources/Commandant/Argument.swift +++ b/Seeds/Commandant/Sources/Commandant/Argument.swift @@ -30,66 +30,70 @@ public struct Argument { } } -/// Evaluates the given argument in the given mode. -/// -/// If parsing command line arguments, and no value was specified on the command -/// line, the argument's `defaultValue` is used. -public func <| (mode: CommandMode, argument: Argument) -> Result> { - switch mode { - case let .arguments(arguments): - guard let stringValue = arguments.consumePositionalArgument() else { - if let defaultValue = argument.defaultValue { - return .success(defaultValue) - } else { - return .failure(missingArgumentError(argument.usage)) +// MARK: - Operators + +extension CommandMode { + /// Evaluates the given argument in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the argument's `defaultValue` is used. + public static func <| (mode: CommandMode, argument: Argument) -> Result> { + switch mode { + case let .arguments(arguments): + guard let stringValue = arguments.consumePositionalArgument() else { + if let defaultValue = argument.defaultValue { + return .success(defaultValue) + } else { + return .failure(missingArgumentError(argument.usage)) + } } - } - if let value = T.from(string: stringValue) { - return .success(value) - } else { - return .failure(argument.invalidUsageError(stringValue)) - } + if let value = T.from(string: stringValue) { + return .success(value) + } else { + return .failure(argument.invalidUsageError(stringValue)) + } - case .usage: - return .failure(informativeUsageError(argument)) + case .usage: + return .failure(informativeUsageError(argument)) + } } -} -/// Evaluates the given argument list in the given mode. -/// -/// If parsing command line arguments, and no value was specified on the command -/// line, the argument's `defaultValue` is used. -public func <| (mode: CommandMode, argument: Argument<[T]>) -> Result<[T], CommandantError> { - switch mode { - case let .arguments(arguments): - guard let firstValue = arguments.consumePositionalArgument() else { - if let defaultValue = argument.defaultValue { - return .success(defaultValue) - } else { - return .failure(missingArgumentError(argument.usage)) + /// Evaluates the given argument list in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the argument's `defaultValue` is used. + public static func <| (mode: CommandMode, argument: Argument<[T]>) -> Result<[T], CommandantError> { + switch mode { + case let .arguments(arguments): + guard let firstValue = arguments.consumePositionalArgument() else { + if let defaultValue = argument.defaultValue { + return .success(defaultValue) + } else { + return .failure(missingArgumentError(argument.usage)) + } } - } - var values = [T]() + var values = [T]() - guard let value = T.from(string: firstValue) else { - return .failure(argument.invalidUsageError(firstValue)) - } - - values.append(value) - - while let nextValue = arguments.consumePositionalArgument() { - guard let value = T.from(string: nextValue) else { - return .failure(argument.invalidUsageError(nextValue)) + guard let value = T.from(string: firstValue) else { + return .failure(argument.invalidUsageError(firstValue)) } values.append(value) + + while let nextValue = arguments.consumePositionalArgument() { + guard let value = T.from(string: nextValue) else { + return .failure(argument.invalidUsageError(nextValue)) + } + + values.append(value) + } + + return .success(values) + + case .usage: + return .failure(informativeUsageError(argument)) } - - return .success(values) - - case .usage: - return .failure(informativeUsageError(argument)) } } diff --git a/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift b/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift index 5eaec41..0ed0890 100644 --- a/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift +++ b/Seeds/Commandant/Sources/Commandant/ArgumentParser.swift @@ -18,7 +18,7 @@ private enum RawArgument: Equatable { case value(String) /// One or more flag arguments (e.g 'r' and 'f' for `-rf`) - case flag(Set) + case flag(OrderedSet) } private func ==(lhs: RawArgument, rhs: RawArgument) -> Bool { @@ -67,11 +67,11 @@ public final class ArgumentParser { rawArguments.append(contentsOf: options.map { arg in if arg.hasPrefix("-") { // Do we have `--{key}` or `-{flags}`. - let opt = arg.characters.dropFirst() + let opt = arg.dropFirst() if opt.first == "-" { return .key(String(opt.dropFirst())) } else { - return .flag(Set(opt)) + return .flag(OrderedSet(opt)) } } else { return .value(arg) diff --git a/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift b/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift index 2fab660..cf73b25 100644 --- a/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift +++ b/Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift @@ -31,11 +31,14 @@ extension String: ArgumentProtocol { } } -// MARK: - migration support -@available(*, unavailable, renamed: "ArgumentProtocol") -public typealias ArgumentType = ArgumentProtocol - -extension ArgumentProtocol { - @available(*, unavailable, renamed: "from(string:)") - static func fromString(_ string: String) -> Self? { return nil } +extension RawRepresentable where RawValue: StringProtocol, Self: ArgumentProtocol { + public static func from(string: String) -> Self? { + return RawValue(string).flatMap(Self.init(rawValue:)) + } +} + +extension RawRepresentable where RawValue: FixedWidthInteger, Self: ArgumentProtocol { + public static func from(string: String) -> Self? { + return RawValue(string).flatMap(Self.init(rawValue:)) + } } diff --git a/Seeds/Commandant/Sources/Commandant/Command.swift b/Seeds/Commandant/Sources/Commandant/Command.swift index 538c72b..5fb6109 100644 --- a/Seeds/Commandant/Sources/Commandant/Command.swift +++ b/Seeds/Commandant/Sources/Commandant/Command.swift @@ -85,12 +85,19 @@ public final class CommandRegistry { public init() {} - /// Registers the given command, making it available to run. + /// Registers the given commands, making those available to run. /// - /// If another command was already registered with the same `verb`, it will - /// be overwritten. - public func register(_ command: C) where C.ClientError == ClientError, C.Options.ClientError == ClientError { - commandsByVerb[command.verb] = CommandWrapper(command) + /// If another commands were already registered with the same `verb`s, those + /// will be overwritten. + @discardableResult + public func register(_ commands: C...) + -> CommandRegistry + where C.ClientError == ClientError, C.Options.ClientError == ClientError + { + for command in commands { + commandsByVerb[command.verb] = CommandWrapper(command) + } + return self } /// Runs the command corresponding to the given verb, passing it the given @@ -155,12 +162,14 @@ extension CommandRegistry { // Extract the executable name. let executableName = arguments.remove(at: 0) - let verb = arguments.first ?? defaultVerb - if arguments.count > 0 { + // use the default verb even if we have other arguments + var verb = defaultVerb + if let argument = arguments.first, !argument.hasPrefix("-") { + verb = argument // Remove the command name. arguments.remove(at: 0) } - + switch run(command: verb, arguments: arguments) { case .success?: exit(EXIT_SUCCESS) @@ -211,14 +220,3 @@ extension CommandRegistry { return launchTask("/usr/bin/env", arguments: [ subcommand ] + arguments) } } - -// MARK: - migration support -@available(*, unavailable, renamed: "CommandProtocol") -public typealias CommandType = CommandProtocol - -extension CommandRegistry { - @available(*, unavailable, renamed: "run(command:arguments:)") - public func runCommand(_ verb: String, arguments: [String]) -> Result<(), CommandantError>? { - return run(command: verb, arguments: arguments) - } -} diff --git a/Seeds/Commandant/Sources/Commandant/Errors.swift b/Seeds/Commandant/Sources/Commandant/Errors.swift index 0e3eddf..1e894ab 100644 --- a/Seeds/Commandant/Sources/Commandant/Errors.swift +++ b/Seeds/Commandant/Sources/Commandant/Errors.swift @@ -136,12 +136,18 @@ internal func informativeUsageError(_ option: return informativeUsageError("--\(option.key) (\(T.name))", option: option) } +/// Constructs an error that describes how to use the option. +internal func informativeUsageError(_ option: Option<[T]>) -> CommandantError { + return informativeUsageError("--\(option.key) (\(option.defaultValue))", option: option) +} + +/// Constructs an error that describes how to use the option. +internal func informativeUsageError(_ option: Option<[T]?>) -> CommandantError { + return informativeUsageError("--\(option.key) (\(T.name))", option: option) +} + /// Constructs an error that describes how to use the given boolean option. internal func informativeUsageError(_ option: Option) -> CommandantError { let key = option.key return informativeUsageError((option.defaultValue ? "--no-\(key)" : "--\(key)"), option: option) } - -// MARK: - migration support -@available(*, unavailable, message: "Use ErrorProtocol instead of ClientErrorType") -public typealias ClientErrorType = Error diff --git a/Seeds/Commandant/Sources/Commandant/HelpCommand.swift b/Seeds/Commandant/Sources/Commandant/HelpCommand.swift index 90ef6a7..be5b305 100644 --- a/Seeds/Commandant/Sources/Commandant/HelpCommand.swift +++ b/Seeds/Commandant/Sources/Commandant/HelpCommand.swift @@ -46,10 +46,10 @@ public struct HelpCommand: CommandProtocol { print("Available commands:\n") - let maxVerbLength = self.registry.commands.map { $0.verb.characters.count }.max() ?? 0 + let maxVerbLength = self.registry.commands.map { $0.verb.count }.max() ?? 0 for command in self.registry.commands { - let padding = repeatElement(Character(" "), count: maxVerbLength - command.verb.characters.count) + let padding = repeatElement(Character(" "), count: maxVerbLength - command.verb.count) print(" \(command.verb)\(String(padding)) \(command.function)") } diff --git a/Seeds/Commandant/Sources/Commandant/Info.plist b/Seeds/Commandant/Sources/Commandant/Info.plist index 43f91b3..90ab2a6 100644 --- a/Seeds/Commandant/Sources/Commandant/Info.plist +++ b/Seeds/Commandant/Sources/Commandant/Info.plist @@ -15,7 +15,7 @@ CFBundlePackageType FMWK CFBundleShortVersionString - 0.11.1 + 0.12.0 CFBundleSignature ???? CFBundleVersion diff --git a/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift b/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift deleted file mode 100644 index 847653c..0000000 --- a/Seeds/Commandant/Sources/Commandant/LinuxSupport.swift +++ /dev/null @@ -1,14 +0,0 @@ -// -// LinuxSupport.swift -// Commandant -// -// Created by Norio Nomura on 3/26/16. -// Copyright © 2016 Carthage. All rights reserved. -// - -import Foundation - -// swift-corelibs-foundation is still written in Swift 2 API. -#if os(Linux) - typealias Process = Task -#endif diff --git a/Seeds/Commandant/Sources/Commandant/Option.swift b/Seeds/Commandant/Sources/Commandant/Option.swift index ab7b618..c11207c 100644 --- a/Seeds/Commandant/Sources/Commandant/Option.swift +++ b/Seeds/Commandant/Sources/Commandant/Option.swift @@ -19,18 +19,19 @@ import Foundation /// struct LogOptions: OptionsProtocol { /// let verbosity: Int /// let outputFilename: String +/// let shouldDelete: Bool /// let logName: String /// -/// static func create(verbosity: Int)(outputFilename: String)(logName: String) -> LogOptions { -/// return LogOptions(verbosity: verbosity, outputFilename: outputFilename, logName: logName) +/// static func create(_ verbosity: Int) -> (String) -> (Bool) -> (String) -> LogOptions { +/// return { outputFilename in { shouldDelete in { logName in LogOptions(verbosity: verbosity, outputFilename: outputFilename, shouldDelete: shouldDelete, logName: logName) } } } /// } /// -/// static func evaluate(m: CommandMode) -> Result> { +/// static func evaluate(_ m: CommandMode) -> Result> { /// return create /// <*> m <| Option(key: "verbose", defaultValue: 0, usage: "the verbosity level with which to read the logs") /// <*> m <| Option(key: "outputFilename", defaultValue: "", usage: "a file to print output to, instead of stdout") -/// <*> m <| Switch(flag: "d", key: "delete", defaultValue: false, usage: "delete the logs when finished") -/// <*> m <| Option(usage: "the log to read") +/// <*> m <| Switch(flag: "d", key: "delete", usage: "delete the logs when finished") +/// <*> m <| Argument(usage: "the log to read") /// } /// } public protocol OptionsProtocol { @@ -82,6 +83,8 @@ extension Option: CustomStringConvertible { } } +// MARK: - Operators + // Inspired by the Argo library: // https://github.com/thoughtbot/Argo /* @@ -141,74 +144,130 @@ public func <*> (f: Result<((T) -> U), CommandantError(mode: CommandMode, option: Option) -> Result> { - let wrapped = Option(key: option.key, defaultValue: option.defaultValue, usage: option.usage) - // Since we are passing a non-nil default value, we can safely unwrap the - // result. - return (mode <| wrapped).map { $0! } -} +extension CommandMode { + /// Evaluates the given option in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the option's `defaultValue` is used. + public static func <| (mode: CommandMode, option: Option) -> Result> { + let wrapped = Option(key: option.key, defaultValue: option.defaultValue, usage: option.usage) + // Since we are passing a non-nil default value, we can safely unwrap the + // result. + return (mode <| wrapped).map { $0! } + } -/// Evaluates the given option in the given mode. -/// -/// If parsing command line arguments, and no value was specified on the command -/// line, `nil` is used. -public func <| (mode: CommandMode, option: Option) -> Result> { - let key = option.key - switch mode { - case let .arguments(arguments): - var stringValue: String? - switch arguments.consumeValue(forKey: key) { - case let .success(value): - stringValue = value + /// Evaluates the given option in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, `nil` is used. + public static func <| (mode: CommandMode, option: Option) -> Result> { + let key = option.key + switch mode { + case let .arguments(arguments): + var stringValue: String? + switch arguments.consumeValue(forKey: key) { + case let .success(value): + stringValue = value - case let .failure(error): - switch error { - case let .usageError(description): + case let .failure(error): + switch error { + case let .usageError(description): + return .failure(.usageError(description: description)) + + case .commandError: + fatalError("CommandError should be impossible when parameterized over NoError") + } + } + + if let stringValue = stringValue { + if let value = T.from(string: stringValue) { + return .success(value) + } + + let description = "Invalid value for '--\(key)': \(stringValue)" return .failure(.usageError(description: description)) - - case .commandError: - fatalError("CommandError should be impossible when parameterized over NoError") + } else { + return .success(option.defaultValue) } - } - if let stringValue = stringValue { - if let value = T.from(string: stringValue) { + case .usage: + return .failure(informativeUsageError(option)) + } + } + + /// Evaluates the given option in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the option's `defaultValue` is used. + public static func <| (mode: CommandMode, option: Option<[T]>) -> Result<[T], CommandantError> { + let wrapped = Option<[T]?>(key: option.key, defaultValue: option.defaultValue, usage: option.usage) + // Since we are passing a non-nil default value, we can safely unwrap the + // result. + return (mode <| wrapped).map { $0! } + } + + /// Evaluates the given option in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, `nil` is used. + public static func <| (mode: CommandMode, option: Option<[T]?>) -> Result<[T]?, CommandantError> { + let key = option.key + + switch mode { + case let .arguments(arguments): + let stringValue: String? + switch arguments.consumeValue(forKey: key) { + case let .success(value): + stringValue = value + + case let .failure(error): + switch error { + case let .usageError(description): + return .failure(.usageError(description: description)) + + case .commandError: + fatalError("CommandError should be impossible when parameterized over NoError") + } + } + + guard let unwrappedStringValue = stringValue else { + return .success(option.defaultValue) + } + + let components = unwrappedStringValue.split( + omittingEmptySubsequences: true, + whereSeparator: [",", " "].contains + ) + var resultValues: [T] = [] + for component in components { + guard let value = T.from(string: String(component)) else { + let description = "Invalid value for '--\(key)': \(unwrappedStringValue)" + return .failure(.usageError(description: description)) + } + resultValues.append(value) + } + return .success(resultValues) + + case .usage: + return .failure(informativeUsageError(option)) + } + } + + /// Evaluates the given boolean option in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the option's `defaultValue` is used. + public static func <| (mode: CommandMode, option: Option) -> Result> { + switch mode { + case let .arguments(arguments): + if let value = arguments.consumeBoolean(forKey: option.key) { return .success(value) + } else { + return .success(option.defaultValue) } - - let description = "Invalid value for '--\(key)': \(stringValue)" - return .failure(.usageError(description: description)) - } else { - return .success(option.defaultValue) - } - case .usage: - return .failure(informativeUsageError(option)) + case .usage: + return .failure(informativeUsageError(option)) + } } } - -/// Evaluates the given boolean option in the given mode. -/// -/// If parsing command line arguments, and no value was specified on the command -/// line, the option's `defaultValue` is used. -public func <| (mode: CommandMode, option: Option) -> Result> { - switch mode { - case let .arguments(arguments): - if let value = arguments.consumeBoolean(forKey: option.key) { - return .success(value) - } else { - return .success(option.defaultValue) - } - - case .usage: - return .failure(informativeUsageError(option)) - } -} - -// MARK: - migration support -@available(*, unavailable, renamed: "OptionsProtocol") -public typealias OptionsType = OptionsProtocol diff --git a/Seeds/Commandant/Sources/Commandant/OrderedSet.swift b/Seeds/Commandant/Sources/Commandant/OrderedSet.swift new file mode 100644 index 0000000..e2a9351 --- /dev/null +++ b/Seeds/Commandant/Sources/Commandant/OrderedSet.swift @@ -0,0 +1,51 @@ +/// A poor man's ordered set. +internal struct OrderedSet { + fileprivate var values: [T] = [] + + init(_ sequence: S) where S.Element == T { + for e in sequence where !values.contains(e) { + values.append(e) + } + } + + @discardableResult + mutating func remove(_ member: T) -> T? { + if let index = values.index(of: member) { + return values.remove(at: index) + } else { + return nil + } + } +} + +extension OrderedSet: Equatable { + static func == (_ lhs: OrderedSet, rhs: OrderedSet) -> Bool { + return lhs.values == rhs.values + } +} + +extension OrderedSet: Collection { + subscript(position: Int) -> T { + return values[position] + } + + var count: Int { + return values.count + } + + var isEmpty: Bool { + return values.isEmpty + } + + var startIndex: Int { + return values.startIndex + } + + var endIndex: Int { + return values.endIndex + } + + func index(after i: Int) -> Int { + return values.index(after: i) + } +} diff --git a/Seeds/Commandant/Sources/Commandant/Switch.swift b/Seeds/Commandant/Sources/Commandant/Switch.swift index b41e4e0..c6bc791 100644 --- a/Seeds/Commandant/Sources/Commandant/Switch.swift +++ b/Seeds/Commandant/Sources/Commandant/Switch.swift @@ -44,20 +44,25 @@ extension Switch: CustomStringConvertible { } } -/// Evaluates the given boolean switch in the given mode. -/// -/// If parsing command line arguments, and no value was specified on the command -/// line, the option's `defaultValue` is used. -public func <| (mode: CommandMode, option: Switch) -> Result> { - switch mode { - case let .arguments(arguments): - var enabled = arguments.consume(key: option.key) - if let flag = option.flag { - enabled = arguments.consumeBoolean(flag: flag) - } - return .success(enabled) +// MARK: - Operators - case .usage: - return .failure(informativeUsageError(option.description, usage: option.usage)) +extension CommandMode { + /// Evaluates the given boolean switch in the given mode. + /// + /// If parsing command line arguments, and no value was specified on the command + /// line, the option's `defaultValue` is used. + public static func <| (mode: CommandMode, option: Switch) -> Result> { + switch mode { + case let .arguments(arguments): + var enabled = arguments.consume(key: option.key) + + if let flag = option.flag, !enabled { + enabled = arguments.consumeBoolean(flag: flag) + } + return .success(enabled) + + case .usage: + return .failure(informativeUsageError(option.description, usage: option.usage)) + } } } diff --git a/Seeds/Result/Result/Result.swift b/Seeds/Result/Result/Result.swift index e8c7adb..c92524d 100644 --- a/Seeds/Result/Result/Result.swift +++ b/Seeds/Result/Result/Result.swift @@ -17,7 +17,7 @@ public enum Result: ResultProtocol, CustomStringConvertib self = .failure(error) } - /// Constructs a result from an Optional, failing with `Error` if `nil`. + /// Constructs a result from an `Optional`, failing with `Error` if `nil`. public init(_ value: T?, failWith: @autoclosure () -> Error) { self = value.map(Result.success) ?? .failure(failWith()) } @@ -31,14 +31,17 @@ public enum Result: ResultProtocol, CustomStringConvertib public init(attempt f: () throws -> T) { do { self = .success(try f()) - } catch { + } catch var error { + if Error.self == AnyError.self { + error = AnyError(error) + } self = .failure(error as! Error) } } // MARK: Deconstruction - /// Returns the value from `Success` Results or `throw`s the error. + /// Returns the value from `success` Results or `throw`s the error. public func dematerialize() throws -> T { switch self { case let .success(value): @@ -50,7 +53,7 @@ public enum Result: ResultProtocol, CustomStringConvertib /// Case analysis for Result. /// - /// Returns the value produced by applying `ifFailure` to `Failure` Results, or `ifSuccess` to `Success` Results. + /// Returns the value produced by applying `ifFailure` to `failure` Results, or `ifSuccess` to `success` Results. public func analysis(ifSuccess: (T) -> Result, ifFailure: (Error) -> Result) -> Result { switch self { case let .success(value): @@ -108,15 +111,38 @@ public enum Result: ResultProtocol, CustomStringConvertib // MARK: - Derive result from failable closure +public func materialize(_ f: () throws -> T) -> Result { + return materialize(try f()) +} + +public func materialize(_ f: @autoclosure () throws -> T) -> Result { + do { + return .success(try f()) + } catch { + return .failure(AnyError(error)) + } +} + +@available(*, deprecated, message: "Use the overload which returns `Result` instead") public func materialize(_ f: () throws -> T) -> Result { return materialize(try f()) } +@available(*, deprecated, message: "Use the overload which returns `Result` instead") public func materialize(_ f: @autoclosure () throws -> T) -> Result { do { return .success(try f()) - } catch let error as NSError { - return .failure(error) + } catch { +// This isn't great, but it lets us maintain compatibility until this deprecated +// method can be removed. +#if _runtime(_ObjC) + return .failure(error as NSError) +#else + // https://github.com/apple/swift-corelibs-foundation/blob/swift-3.0.2-RELEASE/Foundation/NSError.swift#L314 + let userInfo = _swift_Foundation_getErrorDefaultUserInfo(error) as? [String: Any] + let nsError = NSError(domain: error._domain, code: error._code, userInfo: userInfo) + return .failure(nsError) +#endif } } @@ -124,21 +150,23 @@ public func materialize(_ f: @autoclosure () throws -> T) -> Result(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> T?) -> Result { var error: NSError? return `try`(&error).map(Result.success) ?? .failure(error ?? Result.error(function: function, file: file, line: line)) } -/// Constructs a Result with the result of calling `try` with an error pointer. +/// Constructs a `Result` with the result of calling `try` with an error pointer. /// /// This is convenient for wrapping Cocoa API which returns a `Bool` + an error, by reference. e.g.: /// /// Result.try { NSFileManager.defaultManager().removeItemAtURL(URL, error: $0) } +@available(*, deprecated, message: "This will be removed in Result 4.0. Use `Result.init(attempt:)` instead. See https://github.com/antitypical/Result/issues/85 for the details.") public func `try`(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> Bool) -> Result<(), NSError> { var error: NSError? return `try`(&error) ? @@ -148,9 +176,9 @@ public func `try`(_ function: String = #function, file: String = #file, line: In #endif -// MARK: - ErrorProtocolConvertible conformance +// MARK: - ErrorConvertible conformance -extension NSError: ErrorProtocolConvertible { +extension NSError: ErrorConvertible { public static func error(from error: Swift.Error) -> Self { func cast(_ error: Swift.Error) -> T { return error as! T @@ -160,14 +188,70 @@ extension NSError: ErrorProtocolConvertible { } } -// MARK: - +// MARK: - Errors /// An “error” that is impossible to construct. /// /// This can be used to describe `Result`s where failures will never /// be generated. For example, `Result` describes a result that -/// contains an `Int`eger and is guaranteed never to be a `Failure`. -public enum NoError: Swift.Error { } +/// contains an `Int`eger and is guaranteed never to be a `failure`. +public enum NoError: Swift.Error, Equatable { + public static func ==(lhs: NoError, rhs: NoError) -> Bool { + return true + } +} + +/// A type-erased error which wraps an arbitrary error instance. This should be +/// useful for generic contexts. +public struct AnyError: Swift.Error { + /// The underlying error. + public let error: Swift.Error + + public init(_ error: Swift.Error) { + if let anyError = error as? AnyError { + self = anyError + } else { + self.error = error + } + } +} + +extension AnyError: ErrorConvertible { + public static func error(from error: Error) -> AnyError { + return AnyError(error) + } +} + +extension AnyError: CustomStringConvertible { + public var description: String { + return String(describing: error) + } +} + +// There appears to be a bug in Foundation on Linux which prevents this from working: +// https://bugs.swift.org/browse/SR-3565 +// Don't forget to comment the tests back in when removing this check when it's fixed! +#if !os(Linux) + +extension AnyError: LocalizedError { + public var errorDescription: String? { + return error.localizedDescription + } + + public var failureReason: String? { + return (error as? LocalizedError)?.failureReason + } + + public var helpAnchor: String? { + return (error as? LocalizedError)?.helpAnchor + } + + public var recoverySuggestion: String? { + return (error as? LocalizedError)?.recoverySuggestion + } +} + +#endif // MARK: - migration support extension Result { diff --git a/Seeds/Result/Result/ResultProtocol.swift b/Seeds/Result/Result/ResultProtocol.swift index 2bfdffb..678f294 100644 --- a/Seeds/Result/Result/ResultProtocol.swift +++ b/Seeds/Result/Result/ResultProtocol.swift @@ -51,6 +51,14 @@ public extension ResultProtocol { ifFailure: Result.failure) } + /// Returns a Result with a tuple of the receiver and `other` values if both + /// are `Success`es, or re-wrapping the error of the earlier `Failure`. + public func fanout(_ other: @autoclosure () -> R) -> Result<(Value, R.Value), Error> + where Error == R.Error + { + return self.flatMap { left in other().map { right in (left, right) } } + } + /// Returns a new Result by mapping `Failure`'s values using `transform`, or re-wrapping `Success`es’ values. public func mapError(_ transform: (Error) -> Error2) -> Result { return flatMapError { .failure(transform($0)) } @@ -62,6 +70,14 @@ public extension ResultProtocol { ifSuccess: Result.success, ifFailure: transform) } + + /// Returns a new Result by mapping `Success`es’ values using `success`, and by mapping `Failure`'s values using `failure`. + public func bimap(success: (Value) -> U, failure: (Error) -> Error2) -> Result { + return analysis( + ifSuccess: { .success(success($0)) }, + ifFailure: { .failure(failure($0)) } + ) + } } public extension ResultProtocol { @@ -82,11 +98,11 @@ public extension ResultProtocol { } /// Protocol used to constrain `tryMap` to `Result`s with compatible `Error`s. -public protocol ErrorProtocolConvertible: Swift.Error { +public protocol ErrorConvertible: Swift.Error { static func error(from error: Swift.Error) -> Self } -public extension ResultProtocol where Error: ErrorProtocolConvertible { +public extension ResultProtocol where Error: ErrorConvertible { /// Returns the result of applying `transform` to `Success`es’ values, or wrapping thrown errors. public func tryMap(_ transform: (Value) throws -> U) -> Result { @@ -108,10 +124,11 @@ public extension ResultProtocol where Error: ErrorProtocolConvertible { infix operator &&& : LogicalConjunctionPrecedence /// Returns a Result with a tuple of `left` and `right` values if both are `Success`es, or re-wrapping the error of the earlier `Failure`. +@available(*, deprecated, renamed: "ResultProtocol.fanout(self:_:)") public func &&& (left: L, right: @autoclosure () -> R) -> Result<(L.Value, R.Value), L.Error> where L.Error == R.Error { - return left.flatMap { left in right().map { right in (left, right) } } + return left.fanout(right) } precedencegroup ChainingPrecedence { @@ -124,6 +141,7 @@ infix operator >>- : ChainingPrecedence /// Returns the result of applying `transform` to `Success`es’ values, or re-wrapping `Failure`’s errors. /// /// This is a synonym for `flatMap`. +@available(*, deprecated, renamed: "ResultProtocol.flatMap(self:_:)") public func >>- (result: T, transform: (T.Value) -> Result) -> Result { return result.flatMap(transform) } @@ -164,8 +182,11 @@ public typealias ResultType = ResultProtocol @available(*, unavailable, renamed: "Error") public typealias ResultErrorType = Swift.Error -@available(*, unavailable, renamed: "ErrorProtocolConvertible") -public typealias ErrorTypeConvertible = ErrorProtocolConvertible +@available(*, unavailable, renamed: "ErrorConvertible") +public typealias ErrorTypeConvertible = ErrorConvertible + +@available(*, deprecated, renamed: "ErrorConvertible") +public protocol ErrorProtocolConvertible: ErrorConvertible {} extension ResultProtocol { @available(*, unavailable, renamed: "recover(with:)") @@ -174,7 +195,7 @@ extension ResultProtocol { } } -extension ErrorProtocolConvertible { +extension ErrorConvertible { @available(*, unavailable, renamed: "error(from:)") public static func errorFromErrorType(_ error: Swift.Error) -> Self { fatalError() diff --git a/Seeds/Seedfile.lock b/Seeds/Seedfile.lock new file mode 100644 index 0000000..f8d3b45 --- /dev/null +++ b/Seeds/Seedfile.lock @@ -0,0 +1,4 @@ +--- +SEEDS: +- Commandant (master) +- Result (3.2.4) diff --git a/mas-cli.xcodeproj/project.pbxproj b/mas-cli.xcodeproj/project.pbxproj index 70c3077..c639660 100644 --- a/mas-cli.xcodeproj/project.pbxproj +++ b/mas-cli.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 47; + objectVersion = 48; objects = { /* Begin PBXBuildFile section */ @@ -11,11 +11,12 @@ 0C47E694564FCB59996690DD /* Command.swift in Sources */ = {isa = PBXBuildFile; fileRef = 326E4D331CCD66ADFE19CE39 /* Command.swift */; }; 0EBF5CDD379D7462C3389536 /* Result.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9257C5FABA335E5F060CB7F7 /* Result.swift */; }; 15E27926A580EABEB1B218EF /* Switch.swift in Sources */ = {isa = PBXBuildFile; fileRef = AF1B6BEDF32AF3F8A575FB1F /* Switch.swift */; }; - 25209791ED0F49CF5BAF7348 /* LinuxSupport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E3BFBE58DFCE19A53A23D7 /* LinuxSupport.swift */; }; 3053D11E74A22A4C5A6BE833 /* Errors.swift in Sources */ = {isa = PBXBuildFile; fileRef = F547B3DC473CFB1BE0AEB70A /* Errors.swift */; }; 30EA893640B02CCF679F9C57 /* Option.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2AD7FE171F643805F7BC38A7 /* Option.swift */; }; 693A98991CBFFA760004D3B4 /* Search.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A98981CBFFA760004D3B4 /* Search.swift */; }; 693A989B1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift in Sources */ = {isa = PBXBuildFile; fileRef = 693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */; }; + 900A1E811DBAC8CB0069B1A8 /* Info.swift in Sources */ = {isa = PBXBuildFile; fileRef = 900A1E801DBAC8CB0069B1A8 /* Info.swift */; }; + 92AE0FD7BE06D64692E6C1E6 /* OrderedSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = D9871C2273F4D762A1F19B07 /* OrderedSet.swift */; }; AD0785BC0EC6BBF4ED560DCC /* ArgumentParser.swift in Sources */ = {isa = PBXBuildFile; fileRef = EA9D96DDBBCCCC5944160ABE /* ArgumentParser.swift */; }; ADE553C828AF4EAFF39ED3E1 /* ArgumentProtocol.swift in Sources */ = {isa = PBXBuildFile; fileRef = B4237E5AA1A289D03D2A2FB8 /* ArgumentProtocol.swift */; }; EBD6B44FDF65E0253153629F /* HelpCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8FDC2B8063EC231E28353D23 /* HelpCommand.swift */; }; @@ -56,13 +57,14 @@ 2AD7FE171F643805F7BC38A7 /* Option.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Option.swift; path = Seeds/Commandant/Sources/Commandant/Option.swift; sourceTree = ""; }; 326E4D331CCD66ADFE19CE39 /* Command.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Command.swift; path = Seeds/Commandant/Sources/Commandant/Command.swift; sourceTree = ""; }; 5150F7FB7CF2A77F675D8E92 /* ResultProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResultProtocol.swift; path = Seeds/Result/Result/ResultProtocol.swift; sourceTree = ""; }; - 55E3BFBE58DFCE19A53A23D7 /* LinuxSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LinuxSupport.swift; path = Seeds/Commandant/Sources/Commandant/LinuxSupport.swift; sourceTree = ""; }; 693A98981CBFFA760004D3B4 /* Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = ""; }; 693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSURLSession+Synchronous.swift"; sourceTree = ""; }; 8FDC2B8063EC231E28353D23 /* HelpCommand.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HelpCommand.swift; path = Seeds/Commandant/Sources/Commandant/HelpCommand.swift; sourceTree = ""; }; + 900A1E801DBAC8CB0069B1A8 /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = ""; }; 9257C5FABA335E5F060CB7F7 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Seeds/Result/Result/Result.swift; sourceTree = ""; }; AF1B6BEDF32AF3F8A575FB1F /* Switch.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Switch.swift; path = Seeds/Commandant/Sources/Commandant/Switch.swift; sourceTree = ""; }; B4237E5AA1A289D03D2A2FB8 /* ArgumentProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ArgumentProtocol.swift; path = Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift; sourceTree = ""; }; + D9871C2273F4D762A1F19B07 /* OrderedSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OrderedSet.swift; path = Seeds/Commandant/Sources/Commandant/OrderedSet.swift; sourceTree = ""; }; EA9D96DDBBCCCC5944160ABE /* ArgumentParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ArgumentParser.swift; path = Seeds/Commandant/Sources/Commandant/ArgumentParser.swift; sourceTree = ""; }; ED031A781B5127C00097692E /* mas */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = mas; sourceTree = BUILT_PRODUCTS_DIR; }; ED031A7B1B5127C00097692E /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; @@ -152,8 +154,8 @@ 326E4D331CCD66ADFE19CE39 /* Command.swift */, F547B3DC473CFB1BE0AEB70A /* Errors.swift */, 8FDC2B8063EC231E28353D23 /* HelpCommand.swift */, - 55E3BFBE58DFCE19A53A23D7 /* LinuxSupport.swift */, 2AD7FE171F643805F7BC38A7 /* Option.swift */, + D9871C2273F4D762A1F19B07 /* OrderedSet.swift */, AF1B6BEDF32AF3F8A575FB1F /* Switch.swift */, ); name = Commandant; @@ -206,6 +208,7 @@ EDE296521C700F4300554778 /* SignOut.swift */, EDD3B3621C34709400B56B88 /* Upgrade.swift */, EDB6CE8B1BAEC3D400648B4D /* Version.swift */, + 900A1E801DBAC8CB0069B1A8 /* Info.swift */, ); name = Commands; path = "mas-cli/Commands"; @@ -307,17 +310,17 @@ attributes = { LastSwiftMigration = 0730; LastSwiftUpdateCheck = 0700; - LastUpgradeCheck = 0800; + LastUpgradeCheck = 0920; ORGANIZATIONNAME = "Andrew Naylor"; TargetAttributes = { ED031A771B5127C00097692E = { CreatedOnToolsVersion = 7.0; - LastSwiftMigration = 0800; + LastSwiftMigration = 0920; }; }; }; buildConfigurationList = ED031A731B5127C00097692E /* Build configuration list for PBXProject "mas-cli" */; - compatibilityVersion = "Xcode 6.3"; + compatibilityVersion = "Xcode 8.0"; developmentRegion = English; hasScannedForEncodings = 0; knownRegions = ( @@ -351,6 +354,7 @@ ED0F23871B87537200AE40CD /* Account.swift in Sources */, F184B6B7CD9C013CACDED0FB /* Argument.swift in Sources */, AD0785BC0EC6BBF4ED560DCC /* ArgumentParser.swift in Sources */, + 900A1E811DBAC8CB0069B1A8 /* Info.swift in Sources */, ADE553C828AF4EAFF39ED3E1 /* ArgumentProtocol.swift in Sources */, 0C47E694564FCB59996690DD /* Command.swift in Sources */, ED0F238B1B87569C00AE40CD /* Downloader.swift in Sources */, @@ -359,11 +363,11 @@ EBD6B44FDF65E0253153629F /* HelpCommand.swift in Sources */, ED0F237F1B87522400AE40CD /* Install.swift in Sources */, ED0F23901B87A56F00AE40CD /* ISStoreAccount.swift in Sources */, - 25209791ED0F49CF5BAF7348 /* LinuxSupport.swift in Sources */, ED0F23831B87533A00AE40CD /* List.swift in Sources */, ED031A7C1B5127C00097692E /* main.swift in Sources */, 693A989B1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift in Sources */, 30EA893640B02CCF679F9C57 /* Option.swift in Sources */, + 92AE0FD7BE06D64692E6C1E6 /* OrderedSet.swift in Sources */, ED0F23851B87536A00AE40CD /* Outdated.swift in Sources */, ED0F23891B87543D00AE40CD /* PurchaseDownloadObserver.swift in Sources */, EDCBF9531D89AC6F000039C6 /* Reset.swift in Sources */, @@ -391,14 +395,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -438,14 +448,20 @@ CLANG_CXX_LIBRARY = "libc++"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; @@ -483,6 +499,7 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mphys.mas-cli"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "mas-cli/mas-cli-Bridging-Header.h"; + SWIFT_VERSION = 4.0; }; name = Debug; }; @@ -501,7 +518,8 @@ PRODUCT_BUNDLE_IDENTIFIER = "com.mphys.mas-cli"; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "mas-cli/mas-cli-Bridging-Header.h"; - SWIFT_OPTIMIZATION_LEVEL = "-O"; + SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_VERSION = 4.0; }; name = Release; }; diff --git a/mas-cli.xcodeproj/xcshareddata/xcschemes/mas-cli.xcscheme b/mas-cli.xcodeproj/xcshareddata/xcschemes/mas-cli.xcscheme index 2915b23..4c8f875 100644 --- a/mas-cli.xcodeproj/xcshareddata/xcschemes/mas-cli.xcscheme +++ b/mas-cli.xcodeproj/xcshareddata/xcschemes/mas-cli.xcscheme @@ -1,6 +1,6 @@ @@ -45,6 +46,7 @@ buildConfiguration = "Debug" selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" + language = "" launchStyle = "1" useCustomWorkingDirectory = "NO" ignoresPersistentStateOnLaunch = "NO" diff --git a/mas-cli/Commands/Info.swift b/mas-cli/Commands/Info.swift new file mode 100644 index 0000000..3f7c901 --- /dev/null +++ b/mas-cli/Commands/Info.swift @@ -0,0 +1,105 @@ +// +// Info.swift +// mas-cli +// +// Created by Denis Lebedev on 21/10/2016. +// Copyright © 2016 Andrew Naylor. All rights reserved. +// + +import Foundation + +struct InfoCommand: CommandProtocol { + let verb = "info" + let function = "Display app information from the Mac App Store" + + func run(_ options: InfoOptions) -> Result<(), MASError> { + guard let infoURLString = infoURLString(options.appId), + let searchJson = URLSession.requestSynchronousJSONWithURLString(infoURLString) as? [String: Any] else { + return .failure(.searchFailed) + } + + guard let resultCount = searchJson[ResultKeys.ResultCount] as? Int, resultCount > 0, + let results = searchJson[ResultKeys.Results] as? [[String: Any]], + let result = results.first else { + print("No results found") + return .failure(.noSearchResultsFound) + } + + print(AppInfoFormatter.format(appInfo: result)) + + return .success(()) + } + + private func infoURLString(_ appId: String) -> String? { + if let urlEncodedAppId = appId.URLEncodedString { + return "https://itunes.apple.com/lookup?id=\(urlEncodedAppId)" + } + return nil + } +} + +struct InfoOptions: OptionsProtocol { + let appId: String + + static func create(_ appId: String) -> InfoOptions { + return InfoOptions(appId: appId) + } + + static func evaluate(_ m: CommandMode) -> Result> { + return create + <*> m <| Argument(usage: "the app id to show info") + } +} + +private struct AppInfoFormatter { + + private enum Keys { + static let Name = "trackCensoredName" + static let Version = "version" + static let Price = "formattedPrice" + static let Seller = "sellerName" + static let VersionReleaseDate = "currentVersionReleaseDate" + static let MinimumOS = "minimumOsVersion" + static let FileSize = "fileSizeBytes" + static let AppStoreUrl = "trackViewUrl" + } + + static func format(appInfo: [String: Any]) -> String { + let headline = [ + "\(appInfo.stringOrEmpty(key: Keys.Name))", + "\(appInfo.stringOrEmpty(key: Keys.Version))", + "[\(appInfo.stringOrEmpty(key: Keys.Price))]", + ].joined(separator: " ") + + return [ + headline, + "By: \(appInfo.stringOrEmpty(key: Keys.Seller))", + "Released: \(humaReadableDate(appInfo.stringOrEmpty(key: Keys.VersionReleaseDate)))", + "Minimum OS: \(appInfo.stringOrEmpty(key: Keys.MinimumOS))", + "Size: \(humanReadableSize(appInfo.stringOrEmpty(key: Keys.FileSize)))", + "From: \(appInfo.stringOrEmpty(key: Keys.AppStoreUrl))", + ].joined(separator: "\n") + } + + private static func humanReadableSize(_ size: String) -> String { + let bytesSize = Int64(size) ?? 0 + return ByteCountFormatter.string(fromByteCount: bytesSize, countStyle: .file) + } + + private static func humaReadableDate(_ serverDate: String) -> String { + let serverDateFormatter = DateFormatter() + serverDateFormatter.locale = Locale(identifier: "en_US_POSIX") + serverDateFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + + let humanDateFormatter = DateFormatter() + humanDateFormatter.timeStyle = .none + humanDateFormatter.dateStyle = .medium + return serverDateFormatter.date(from: serverDate).flatMap(humanDateFormatter.string(from:)) ?? "" + } +} + +extension Dictionary { + fileprivate func stringOrEmpty(key: Key) -> String { + return self[key] as? String ?? "" + } +} diff --git a/mas-cli/Commands/Install.swift b/mas-cli/Commands/Install.swift index f6f7e3e..5d3591e 100644 --- a/mas-cli/Commands/Install.swift +++ b/mas-cli/Commands/Install.swift @@ -24,7 +24,7 @@ struct InstallCommand: CommandProtocol { switch downloadResults.count { case 0: - return .success() + return .success(()) case 1: return .failure(downloadResults[0]) default: diff --git a/mas-cli/Commands/Reset.swift b/mas-cli/Commands/Reset.swift index 219148b..4b6c5c2 100644 --- a/mas-cli/Commands/Reset.swift +++ b/mas-cli/Commands/Reset.swift @@ -58,12 +58,13 @@ struct ResetCommand: CommandProtocol { } // Wipe Download Directory - let directory = CKDownloadDirectory(nil) - do { - try FileManager.default.removeItem(atPath: directory!) - } catch { - if options.debug { - printError("removeItemAtPath:\"\(directory)\" failed, \(error)") + if let directory = CKDownloadDirectory(nil) { + do { + try FileManager.default.removeItem(atPath: directory) + } catch { + if options.debug { + printError("removeItemAtPath:\"\(directory)\" failed, \(error)") + } } } diff --git a/mas-cli/Commands/Search.swift b/mas-cli/Commands/Search.swift index 8616bc9..5931f4e 100644 --- a/mas-cli/Commands/Search.swift +++ b/mas-cli/Commands/Search.swift @@ -42,7 +42,7 @@ struct SearchCommand: CommandProtocol { } func searchURLString(_ appName: String) -> String? { - if let urlEncodedAppName = appName.URLEncodedString() { + if let urlEncodedAppName = appName.URLEncodedString { return "https://itunes.apple.com/search?entity=macSoftware&term=\(urlEncodedAppName)&attribute=allTrackTerm" } return nil diff --git a/mas-cli/Commands/Upgrade.swift b/mas-cli/Commands/Upgrade.swift index 3399837..945e1cd 100644 --- a/mas-cli/Commands/Upgrade.swift +++ b/mas-cli/Commands/Upgrade.swift @@ -42,7 +42,7 @@ struct UpgradeCommand: CommandProtocol { switch updateResults.count { case 0: - return .success() + return .success(()) case 1: return .failure(updateResults[0]) default: diff --git a/mas-cli/NSURLSession+Synchronous.swift b/mas-cli/NSURLSession+Synchronous.swift index 9a5e4e2..bd54a8c 100644 --- a/mas-cli/NSURLSession+Synchronous.swift +++ b/mas-cli/NSURLSession+Synchronous.swift @@ -55,9 +55,8 @@ public extension URLSession { public extension String { /// Return an URL encoded string - func URLEncodedString() -> String? { - let customAllowedSet = CharacterSet.urlQueryAllowed - return addingPercentEncoding(withAllowedCharacters: customAllowedSet) + var URLEncodedString: String? { + return addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) } } diff --git a/mas-cli/main.swift b/mas-cli/main.swift index f79c5bf..c69be2a 100644 --- a/mas-cli/main.swift +++ b/mas-cli/main.swift @@ -17,6 +17,7 @@ public struct StderrOutputStream: TextOutputStream { let registry = CommandRegistry() let helpCommand = HelpCommand(registry: registry) registry.register(AccountCommand()) +registry.register(InfoCommand()) registry.register(InstallCommand()) registry.register(ListCommand()) registry.register(OutdatedCommand()) diff --git a/script/build b/script/build index 875e681..37a8a6d 100755 --- a/script/build +++ b/script/build @@ -11,11 +11,21 @@ main() { } build() { - set -o pipefail && xcodebuild -project "mas-cli.xcodeproj" -scheme mas-cli -configuration Release clean build | xcpretty -c + set -o pipefail && \ + xcodebuild -project "mas-cli.xcodeproj" \ + -scheme mas-cli \ + -configuration Release \ + clean build \ + | bundle exec xcpretty --color } archive() { - set -o pipefail && xcodebuild -project "mas-cli.xcodeproj" -scheme mas-cli -archivePath mas.xcarchive archive | xcpretty -c + set -o pipefail && \ + xcodebuild -project "mas-cli.xcodeproj" \ + -scheme mas-cli \ + -archivePath mas.xcarchive \ + archive \ + | bundle exec xcpretty --color } main diff --git a/script/package b/script/package index 6137fa6..7c63693 100755 --- a/script/package +++ b/script/package @@ -1,6 +1,7 @@ #!/bin/bash -e echo "==> Moving dSYM to xcarchive" +mkdir -p mas.xcarchive/dSYMs/ mv build/mas.dSYM mas.xcarchive/dSYMs/ echo "==> Compressing mas.xcarchive"