Initial Version (work in progress)

This commit is contained in:
Salar Rahmanian 2024-10-05 13:10:39 -07:00
parent 93069902ad
commit 9dd3399d5c
11 changed files with 544 additions and 57 deletions

41
Sources/Algebra.swift Normal file
View file

@ -0,0 +1,41 @@
//
// Copyright © 2024 Salar Rahmanian.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
struct FishHistoryEntry: Equatable {
let cmd: String
let when: Int
let paths: [String]
func getDate() -> Date {
Date(timeIntervalSince1970: TimeInterval(when))
}
func writeEntry() -> [String] {
var output: [String] = []
output.append("- cmd: \(cmd)")
output.append(" when: \(when)")
if !paths.isEmpty {
output.append(" paths:")
paths.forEach { output.append(" - \(String($0))") }
}
return output
}
}

38
Sources/FileHelpers.swift Normal file
View file

@ -0,0 +1,38 @@
//
// Copyright © 2024 Salar Rahmanian.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
func getPath(_ pathStr: String) -> URL? {
let userHomeDirectory = FileManager.default.homeDirectoryForCurrentUser.path
var filePath: String = pathStr
if pathStr.hasPrefix("~") {
filePath = (pathStr as NSString).expandingTildeInPath
}
if pathStr.hasPrefix("$HOME") {
filePath = filePath.replacingOccurrences(of: "$HOME", with: userHomeDirectory)
}
if !FileManager.default.fileExists(atPath: filePath) {
return nil
}
return URL(fileURLWithPath: filePath)
}

103
Sources/Fishee.swift Normal file
View file

@ -0,0 +1,103 @@
//
// Copyright © 2024 Salar Rahmanian.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import ArgumentParser
import Foundation
let DEFAULT_FISH_HISTORY_LOCATION: String = "~/.local/share/fish/fish_history"
@main
struct Fishee: ParsableCommand {
@Option(name: [.short, .customLong("history-file")], help: "Location of your fish history file. Will default to ~/.local/share/fish/fish_history")
var fishHistoryLocationStr: String?
@Option(name: .shortAndLong, help: "File path to file to merge with history file.")
var mergeFile: String?
@Option(
name: [.short, .customLong("output-file")],
help: "File to write to. Default: same as current history file."
)
var writeFileStr: String?
@Flag(
name: .shortAndLong,
help: "Dry run. Will only print to the console without actually modifying the history file."
)
var dryRun: Bool = false
@Flag(
name: .shortAndLong,
help: "Remove duplicates from combined history. Default: false"
)
var removeDuplicates: Bool = false
@Flag(
name: .shortAndLong,
inversion: .prefixedNo,
help: "Backup fish history file given before writing."
)
var backup: Bool = true
var fishHistoryLocation: URL? {
let pathStr = fishHistoryLocationStr ?? DEFAULT_FISH_HISTORY_LOCATION
return getPath(pathStr)
}
var writeFileLocation: URL? {
let pathStr = writeFileStr ?? DEFAULT_FISH_HISTORY_LOCATION
return getPath(pathStr)
}
public func run() throws {
let mergeFileLocation = mergeFile.flatMap { getPath($0) }
let finalHistory: [FishHistoryEntry] = switch (fishHistoryLocation, mergeFileLocation) {
case let (fishHistoryLocation?, mergeFileLocation?):
{
let currentHistory = parseFishHistory(from: fishHistoryLocation.path) ?? []
let toMergeHistory = parseFishHistory(from: mergeFileLocation.path) ?? []
return mergeFishHistory(currentHistory, toMergeHistory, removeDuplicates: removeDuplicates)
}()
case let (fishHistoryLocation?, nil):
parseFishHistory(from: fishHistoryLocation.path) ?? []
default:
[]
}
if dryRun {
finalHistory.forEach { print("\($0.writeEntry().joined(separator: "\n"))") }
}
else {
if let writePath = writeFileLocation?.path {
let result = writeFishHistory(
to: writePath,
history: finalHistory,
historyFileLocation: fishHistoryLocation?.path,
backup: backup
)
if result {
print("Succussfully updated \(writePath)")
}
else {
print("Failed to update \(writePath)")
}
}
}
return
}
}

131
Sources/Parser.swift Normal file
View file

@ -0,0 +1,131 @@
//
// Copyright © 2024 Salar Rahmanian.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
import Foundation
func backupHistory(_ path: String) -> Bool {
let fileManager = FileManager.default
guard fileManager.fileExists(atPath: path) else {
print("File does not exist at path: \(path)")
return false
}
let fileURL = URL(fileURLWithPath: path)
let fileExtension = fileURL.pathExtension
let fileNameWithoutExtension = fileURL.deletingPathExtension().lastPathComponent
let directory = fileURL.deletingLastPathComponent()
let newFileName = "\(fileNameWithoutExtension)_copy"
let newFileURL = directory.appendingPathComponent(newFileName).appendingPathExtension(fileExtension)
do {
try fileManager.copyItem(at: fileURL, to: newFileURL)
print("File duplicated successfully to: \(newFileURL.path)")
return true
} catch {
return false
}
}
func writeFishHistory(to path: String, history: [FishHistoryEntry], historyFileLocation: String?, backup: Bool = true) -> Bool {
var output = ""
if backup {
if let backupFile = historyFileLocation {
let result = backupHistory(backupFile)
if !result {
print("Failed to backup \(backupFile) so aborting!")
return false
}
}
}
history.forEach { output += $0.writeEntry().joined(separator: "\n") + "\n" }
if !output.isEmpty {
do {
try output.write(toFile: path, atomically: true, encoding: .utf8)
print("Successfully wrote merged history to \(path)")
return true
} catch {
print("Error writing merged history: \(error)")
return false
}
}
else {
print("Nothing to write to \(path)")
return false
}
}
func parseFishHistory(from filePath: String) -> [FishHistoryEntry]? {
guard let fileContents = try? String(contentsOfFile: filePath) else {
print("Failed to open file.")
return nil
}
let lines = fileContents.split(separator: "\n").map { String($0).trimmingCharacters(in: .whitespaces) }
let initialState: (entries: [FishHistoryEntry], currentCmd: String?, currentWhen: Int?, currentPaths: [String]) = ([], nil, nil, [])
let result = lines.reduce(into: initialState) { state, line in
if line.starts(with: "- cmd:") {
if let cmd = state.currentCmd, let when = state.currentWhen {
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: state.currentPaths)
state.entries.append(entry)
state.currentPaths = []
}
state.currentCmd = String(line.dropFirst("- cmd:".count).trimmingCharacters(in: .whitespaces))
} else if line.starts(with: "when:") {
if let whenValue = Int(line.dropFirst("when:".count).trimmingCharacters(in: .whitespaces)) {
state.currentWhen = whenValue
}
} else if line.starts(with: "paths:") {
return
} else if line.starts(with: "- ") {
let path = String(line.dropFirst("- ".count).trimmingCharacters(in: .whitespaces))
state.currentPaths.append(path)
}
}
if let cmd = result.currentCmd, let when = result.currentWhen {
let entry = FishHistoryEntry(cmd: cmd, when: when, paths: result.currentPaths)
return result.entries + [entry]
}
return result.entries
}
func mergeFishHistory(_ left: [FishHistoryEntry], _ right: [FishHistoryEntry], removeDuplicates: Bool = false) -> [FishHistoryEntry] {
let merged = left + right
if removeDuplicates {
let finalList = merged.reduce(into: [String:FishHistoryEntry]()) { result, entry in
if result[entry.cmd] == nil {
result[entry.cmd] = entry
}
}
return Array(finalList.values)
} else {
return merged
}
}