From 1b6883c9df11719bd414ee5a7b5710b82325071c Mon Sep 17 00:00:00 2001 From: a dinosaur Date: Fri, 15 Nov 2024 23:59:56 +1100 Subject: [PATCH] add different match types to search --- Sources/dpk-cli/ExactMatcher.swift | 36 ++++++++++++++++ Sources/dpk-cli/GlobMatcher.swift | 38 +++++++++++++++++ Sources/dpk-cli/PatternMatcher.swift | 12 ++++++ Sources/dpk-cli/RegexMatcher.swift | 29 +++++++++++++ .../Subcommands/DpkSearchCommand.swift | 41 ++++++++++--------- 5 files changed, 137 insertions(+), 19 deletions(-) create mode 100644 Sources/dpk-cli/ExactMatcher.swift create mode 100644 Sources/dpk-cli/GlobMatcher.swift create mode 100644 Sources/dpk-cli/PatternMatcher.swift create mode 100644 Sources/dpk-cli/RegexMatcher.swift diff --git a/Sources/dpk-cli/ExactMatcher.swift b/Sources/dpk-cli/ExactMatcher.swift new file mode 100644 index 0000000..d7c3edb --- /dev/null +++ b/Sources/dpk-cli/ExactMatcher.swift @@ -0,0 +1,36 @@ +/* + * darwin-apk © 2024 Gay Pizza Specifications + * SPDX-License-Identifier: Apache-2.0 + */ + +import Foundation +import ArgumentParser + +struct ExactMatcher: PatternMatcher { + private let _matches: [String] + private let _ignoreCase: Bool + + init(patterns: [String], ignoreCase: Bool) throws(ArgumentParser.ExitCode) { + self._matches = patterns + self._ignoreCase = ignoreCase + } + + func match(_ field: String) -> Bool { + if self._ignoreCase { + for match in self._matches { + // May want to use localizedCaseInsensitiveCompare + // if localised descriptions ever become involved + if field.caseInsensitiveCompare(match) == .orderedSame { + return true + } + } + } else { + for match in self._matches { + if field == match { + return true + } + } + } + return false + } +} diff --git a/Sources/dpk-cli/GlobMatcher.swift b/Sources/dpk-cli/GlobMatcher.swift new file mode 100644 index 0000000..d60132a --- /dev/null +++ b/Sources/dpk-cli/GlobMatcher.swift @@ -0,0 +1,38 @@ +/* + * darwin-apk © 2024 Gay Pizza Specifications + * SPDX-License-Identifier: Apache-2.0 + */ + +import Foundation +import ArgumentParser + +struct GlobMatcher: PatternMatcher { + private let _patterns: [String] + private let _flags: Int32 + + init(patterns: [String], ignoreCase: Bool) throws(ArgumentParser.ExitCode) { + self._patterns = patterns + self._flags = ignoreCase ? FNM_CASEFOLD : 0 + } + + func match(_ field: String) -> Bool { + for pattern in self._patterns { + // Quick hack to make matching without explicit globs easier + if pattern.rangeOfCharacter(from: .init(charactersIn: "*?[]")) == nil { + if self._flags & FNM_CASEFOLD != 0 { + return field.localizedCaseInsensitiveContains(pattern) + } else { + return field.contains(pattern) + } + } + let res = fnmatch(pattern, field, self._flags) + if res == FNM_NOMATCH { + continue + } else if res == 0 { + return true + } + fatalError("fnmatch error \(res)") + } + return false + } +} diff --git a/Sources/dpk-cli/PatternMatcher.swift b/Sources/dpk-cli/PatternMatcher.swift new file mode 100644 index 0000000..66ae642 --- /dev/null +++ b/Sources/dpk-cli/PatternMatcher.swift @@ -0,0 +1,12 @@ +/* + * darwin-apk © 2024 Gay Pizza Specifications + * SPDX-License-Identifier: Apache-2.0 + */ + +import Foundation +import ArgumentParser + +protocol PatternMatcher { + init(patterns: [String], ignoreCase: Bool) throws(ExitCode) + func match(_ field: String) -> Bool +} diff --git a/Sources/dpk-cli/RegexMatcher.swift b/Sources/dpk-cli/RegexMatcher.swift new file mode 100644 index 0000000..42fb6a2 --- /dev/null +++ b/Sources/dpk-cli/RegexMatcher.swift @@ -0,0 +1,29 @@ +/* + * darwin-apk © 2024 Gay Pizza Specifications + * SPDX-License-Identifier: Apache-2.0 + */ + +import Foundation +import ArgumentParser + +struct RegexMatcher: PatternMatcher { + private let _patterns: [Regex<_StringProcessing.AnyRegexOutput>] + + init(patterns: [String], ignoreCase: Bool) throws(ExitCode) { + do { + self._patterns = try patterns.map(Regex.init) + } catch { + print("Bad pattern \(error.localizedDescription)") + throw .validationFailure + } + } + + func match(_ field: String) -> Bool { + for pattern in self._patterns { + if (try? pattern.firstMatch(in: field)) != nil { + return true + } + } + return false + } +} diff --git a/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift b/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift index cb37b31..bf45466 100644 --- a/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift +++ b/Sources/dpk-cli/Subcommands/DpkSearchCommand.swift @@ -13,21 +13,34 @@ struct DpkSearchCommand: AsyncParsableCommand { abstract: "Search for packages with a pattern matching name and description", aliases: [ "s" ]) - @Flag + @Flag(name: .shortAndLong, help: "Use regular expressions instead of globbing") + var regex: Bool = false + @Flag(name: [ .customShort("x"), .long ], help: "Match given strings exactly") + var exact: Bool = false + @Flag(name: [ .customShort("I"), .long ], help: "Use case-sensitive matching") + var caseSensitive: Bool = false + @Flag(name: .shortAndLong, help: "Only match names instead of names & descriptions") var nameOnly: Bool = false @Argument var patterns: [String] func run() async throws(ExitCode) { - let re: [Regex<_StringProcessing.AnyRegexOutput>] - do { - re = try patterns.map(Regex.init) - } catch { - print("Bad pattern \(error.localizedDescription)") + if self.regex && self.exact { + print("Only one of \(self._regex.description) and \(self._exact.description) is allowed") throw .validationFailure } + let matcher: PatternMatcher.Type = if self.regex { + RegexMatcher.self + } else if self.exact { + ExactMatcher.self + } else { + GlobMatcher.self + } + let match: any PatternMatcher + match = try matcher.init(patterns: patterns, ignoreCase: !self.caseSensitive) + let repositories: [String], architectures: [String] do { repositories = try await PropertyFile.read(name: "repositories") @@ -55,20 +68,10 @@ struct DpkSearchCommand: AsyncParsableCommand { throw .failure } - do { - for package in index.packages { - for pattern in re { - if try - pattern.firstMatch(in: package.name) != nil || - (!self.nameOnly && pattern.firstMatch(in: package.packageDescription) != nil) { - print(package.shortDescription) - break - } - } + for package in index.packages { + if match.match(package.name) || (!self.nameOnly && match.match(package.packageDescription)) { + print(package.shortDescription) } - } catch { - print("Something went wrong: \(error.localizedDescription)") - throw .failure } } }