Merge branch 'master' into patch-2

This commit is contained in:
Ben Chatelain 2018-01-27 12:39:24 -07:00 committed by GitHub
commit ae913cb747
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 709 additions and 257 deletions

6
.bundle/config Normal file
View file

@ -0,0 +1,6 @@
---
BUNDLE_CLEAN: "true"
BUNDLE_BIN: "bin"
BUNDLE_JOBS: "8"
BUNDLE_DISABLE_SHARED_GEMS: "true"
BUNDLE_PATH: ".rubygems"

29
.gitignore vendored
View file

@ -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

View file

@ -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

71
CHANGELOG.md Normal file
View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"

View file

@ -30,66 +30,70 @@ public struct Argument<T> {
}
}
/// 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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, argument: Argument<T>) -> Result<T, CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, argument: Argument<T>) -> Result<T, CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, argument: Argument<[T]>) -> Result<[T], CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, argument: Argument<[T]>) -> Result<[T], CommandantError<ClientError>> {
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))
}
}

View file

@ -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<Character>)
case flag(OrderedSet<Character>)
}
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)

View file

@ -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:))
}
}

View file

@ -85,12 +85,19 @@ public final class CommandRegistry<ClientError: Error> {
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<C: CommandProtocol>(_ 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<C: CommandProtocol>(_ 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<ClientError>>? {
return run(command: verb, arguments: arguments)
}
}

View file

@ -136,12 +136,18 @@ internal func informativeUsageError<T: ArgumentProtocol, ClientError>(_ option:
return informativeUsageError("--\(option.key) (\(T.name))", option: option)
}
/// Constructs an error that describes how to use the option.
internal func informativeUsageError<T: ArgumentProtocol, ClientError>(_ option: Option<[T]>) -> CommandantError<ClientError> {
return informativeUsageError("--\(option.key) (\(option.defaultValue))", option: option)
}
/// Constructs an error that describes how to use the option.
internal func informativeUsageError<T: ArgumentProtocol, ClientError>(_ option: Option<[T]?>) -> CommandantError<ClientError> {
return informativeUsageError("--\(option.key) (\(T.name))", option: option)
}
/// Constructs an error that describes how to use the given boolean option.
internal func informativeUsageError<ClientError>(_ option: Option<Bool>) -> CommandantError<ClientError> {
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

View file

@ -46,10 +46,10 @@ public struct HelpCommand<ClientError: Error>: 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)")
}

View file

@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>0.11.1</string>
<string>0.12.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>

View file

@ -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

View file

@ -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<LogOptions, CommandantError<YourErrorType>> {
/// static func evaluate(_ m: CommandMode) -> Result<LogOptions, CommandantError<YourErrorType>> {
/// 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 <*> <T, U, ClientError>(f: Result<((T) -> U), CommandantError<Client
}
}
/// 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 func <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError<ClientError>> {
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! }
}
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<T>) -> Result<T, CommandantError<ClientError>> {
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 func <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<T?>) -> Result<T?, CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<T?>) -> Result<T?, CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<[T]>) -> Result<[T], CommandantError<ClientError>> {
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 <| <T: ArgumentProtocol, ClientError>(mode: CommandMode, option: Option<[T]?>) -> Result<[T]?, CommandantError<ClientError>> {
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 <| <ClientError>(mode: CommandMode, option: Option<Bool>) -> Result<Bool, CommandantError<ClientError>> {
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 <| <ClientError>(mode: CommandMode, option: Option<Bool>) -> Result<Bool, CommandantError<ClientError>> {
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

View file

@ -0,0 +1,51 @@
/// A poor man's ordered set.
internal struct OrderedSet<T: Hashable> {
fileprivate var values: [T] = []
init<S: Sequence>(_ 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)
}
}

View file

@ -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 <| <ClientError> (mode: CommandMode, option: Switch) -> Result<Bool, CommandantError<ClientError>> {
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 <| <ClientError> (mode: CommandMode, option: Switch) -> Result<Bool, CommandantError<ClientError>> {
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))
}
}
}

View file

@ -17,7 +17,7 @@ public enum Result<T, Error: Swift.Error>: 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<T, Error: Swift.Error>: 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<T, Error: Swift.Error>: 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<Result>(ifSuccess: (T) -> Result, ifFailure: (Error) -> Result) -> Result {
switch self {
case let .success(value):
@ -108,15 +111,38 @@ public enum Result<T, Error: Swift.Error>: ResultProtocol, CustomStringConvertib
// MARK: - Derive result from failable closure
public func materialize<T>(_ f: () throws -> T) -> Result<T, AnyError> {
return materialize(try f())
}
public func materialize<T>(_ f: @autoclosure () throws -> T) -> Result<T, AnyError> {
do {
return .success(try f())
} catch {
return .failure(AnyError(error))
}
}
@available(*, deprecated, message: "Use the overload which returns `Result<T, AnyError>` instead")
public func materialize<T>(_ f: () throws -> T) -> Result<T, NSError> {
return materialize(try f())
}
@available(*, deprecated, message: "Use the overload which returns `Result<T, AnyError>` instead")
public func materialize<T>(_ f: @autoclosure () throws -> T) -> Result<T, NSError> {
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<T>(_ f: @autoclosure () throws -> T) -> Result<T, NSErro
#if !os(Linux)
/// 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 an object or `nil` + an error, by reference. e.g.:
///
/// Result.try { NSData(contentsOfURL: URL, options: .DataReadingMapped, error: $0) }
/// Result.try { NSData(contentsOfURL: URL, options: .dataReadingMapped, 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`<T>(_ function: String = #function, file: String = #file, line: Int = #line, `try`: (NSErrorPointer) -> T?) -> Result<T, NSError> {
var error: NSError?
return `try`(&error).map(Result.success) ?? .failure(error ?? Result<T, NSError>.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<T: NSError>(_ 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<Int, NoError>` 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 {

View file

@ -51,6 +51,14 @@ public extension ResultProtocol {
ifFailure: Result<U, Error>.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<R: ResultProtocol>(_ 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<Error2>(_ transform: (Error) -> Error2) -> Result<Value, Error2> {
return flatMapError { .failure(transform($0)) }
@ -62,6 +70,14 @@ public extension ResultProtocol {
ifSuccess: Result<Value, Error2>.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<U, Error2>(success: (Value) -> U, failure: (Error) -> Error2) -> Result<U, Error2> {
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<U>(_ transform: (Value) throws -> U) -> Result<U, Error> {
@ -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 &&& <L: ResultProtocol, R: ResultProtocol> (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 >>- <T: ResultProtocol, U> (result: T, transform: (T.Value) -> Result<U, T.Error>) -> Result<U, T.Error> {
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()

4
Seeds/Seedfile.lock Normal file
View file

@ -0,0 +1,4 @@
---
SEEDS:
- Commandant (master)
- Result (3.2.4)

View file

@ -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 = "<group>"; };
326E4D331CCD66ADFE19CE39 /* Command.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Command.swift; path = Seeds/Commandant/Sources/Commandant/Command.swift; sourceTree = "<group>"; };
5150F7FB7CF2A77F675D8E92 /* ResultProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ResultProtocol.swift; path = Seeds/Result/Result/ResultProtocol.swift; sourceTree = "<group>"; };
55E3BFBE58DFCE19A53A23D7 /* LinuxSupport.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LinuxSupport.swift; path = Seeds/Commandant/Sources/Commandant/LinuxSupport.swift; sourceTree = "<group>"; };
693A98981CBFFA760004D3B4 /* Search.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Search.swift; sourceTree = "<group>"; };
693A989A1CBFFAAA0004D3B4 /* NSURLSession+Synchronous.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSURLSession+Synchronous.swift"; sourceTree = "<group>"; };
8FDC2B8063EC231E28353D23 /* HelpCommand.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = HelpCommand.swift; path = Seeds/Commandant/Sources/Commandant/HelpCommand.swift; sourceTree = "<group>"; };
900A1E801DBAC8CB0069B1A8 /* Info.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Info.swift; sourceTree = "<group>"; };
9257C5FABA335E5F060CB7F7 /* Result.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Result.swift; path = Seeds/Result/Result/Result.swift; sourceTree = "<group>"; };
AF1B6BEDF32AF3F8A575FB1F /* Switch.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = Switch.swift; path = Seeds/Commandant/Sources/Commandant/Switch.swift; sourceTree = "<group>"; };
B4237E5AA1A289D03D2A2FB8 /* ArgumentProtocol.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ArgumentProtocol.swift; path = Seeds/Commandant/Sources/Commandant/ArgumentProtocol.swift; sourceTree = "<group>"; };
D9871C2273F4D762A1F19B07 /* OrderedSet.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = OrderedSet.swift; path = Seeds/Commandant/Sources/Commandant/OrderedSet.swift; sourceTree = "<group>"; };
EA9D96DDBBCCCC5944160ABE /* ArgumentParser.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ArgumentParser.swift; path = Seeds/Commandant/Sources/Commandant/ArgumentParser.swift; sourceTree = "<group>"; };
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 = "<group>"; };
@ -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;
};

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0920"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@ -26,6 +26,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
@ -45,6 +46,7 @@
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
language = ""
launchStyle = "1"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"

105
mas-cli/Commands/Info.swift Normal file
View file

@ -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<InfoOptions, CommandantError<MASError>> {
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 ?? ""
}
}

View file

@ -24,7 +24,7 @@ struct InstallCommand: CommandProtocol {
switch downloadResults.count {
case 0:
return .success()
return .success(())
case 1:
return .failure(downloadResults[0])
default:

View file

@ -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)")
}
}
}

View file

@ -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

View file

@ -42,7 +42,7 @@ struct UpgradeCommand: CommandProtocol {
switch updateResults.count {
case 0:
return .success()
return .success(())
case 1:
return .failure(updateResults[0])
default:

View file

@ -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)
}
}

View file

@ -17,6 +17,7 @@ public struct StderrOutputStream: TextOutputStream {
let registry = CommandRegistry<MASError>()
let helpCommand = HelpCommand(registry: registry)
registry.register(AccountCommand())
registry.register(InfoCommand())
registry.register(InstallCommand())
registry.register(ListCommand())
registry.register(OutdatedCommand())

View file

@ -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

View file

@ -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"