Skip to content

Commit

Permalink
Minor fixes, now pass ext to callback
Browse files Browse the repository at this point in the history
Improved try to allow multiple files making it more like dry-run (maybe will combine),
minor fixes to file path and extension parsing that was probably wrong before,
updated readme including clearer description of swift-sh use and the Regex additions,
breaking change that adds file extension param to rename func (not inout ATM so can't change it)
  • Loading branch information
Pierre Houston committed Dec 21, 2020
1 parent 934c298 commit 8c3fcee
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 56 deletions.
2 changes: 1 addition & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ let package = Package(
],
dependencies: [
// Dependencies declare other packages that this package depends on.
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.0.2"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "0.3.0"),
.package(url: "https://github.com/JohnSundell/Files", from: "4.0.0"),
.package(url: "https://github.com/sharplet/Regex", from: "2.0.0"),
],
Expand Down
23 changes: 13 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,17 @@ A library making it easy to make a swift command-line program for renaming files

#### Details

It exports a struct `RenameOptions` conforming to the `ParsableArguments` protocol from Apple's `ArgumentParser`. It can be used with `@OptionGroup()` and your own `ParsableCommand` and also provides a `runRename()` function you can call within your own `run()`, doing most of the work needed.
This package exports a struct `RenameOptions` conforming to the `ParsableArguments` protocol from Apple's `ArgumentParser`. It's intended to be used with `@OptionGroup()` and your own `ParsableCommand` and provides a `runRename()` function you can call within your own `run()`, implementing all the boilerplate file system and string processing involved in a command that renames files. Your code is little more than your custom regular expressions or any such manipulation of the base filename.

`runRename()` takes a function argument with a inout `name` parameter, you provide this function which changes `name` as desired. This is called for every file passed on the command line, with the file extension omitted if any, and the file gets renamed accordingly.
`runRename()` takes a function argument with a inout `name` `String` (and file extension `String`), you provide this function which changes `name` as desired. This is called for every file passed on the command line, with the directory omitted and file extension separated, and the file gets renamed accordingly. Leave `name` unchanged (or change to empty string) to do nothing to the file.

`RenameOptions` defines arguments `verbose`, `quiet`, `dry-run`.
`RenameOptions` defines arguments `--verbose`/`-v`, `--quiet`/`-q`, `--dry-run`, `--try` (not to mention the defaults provided by ArgumentParser, `--help`/`-h` and `--generate-completion-script`). The difference between `--dry-run` and `--try` are that the former fails as usual if the file arguments aren't found, the latter will allow any file argument as if they were files that existed; both show the would-be results of the rename without carrying it out.

It works well with `swift-sh`, also the `Regex` package at http://github.com/sharplet/Regex which `RenameCommand` extends with an overload of its `replace` functions added to `String` allowing you to more conveniently specify case insensitive.
It works well with `swift-sh`, also the `sharplet/Regex` package which `RenameCommand` extends with an overload of its `String` extension functions allowing you to more conveniently specify case insensitive. See below.

#### Example

This simple Swift "script" source file "myrename" (no ".swift" extension needed):
With `swift-sh` installed, this simple Swift "script" source file "myrename" (no ".swift" extension needed) is all you need to give you a fully functional custom file renaming command:

```swift
#!/usr/bin/swift sh
Expand All @@ -27,7 +27,7 @@ struct RenameMoviesCommand: ParsableCommand {
@OptionGroup() var options: RenameCommand.RenameOptions

func run() throws {
try options.runRename() { name in
try options.runRename() { name, _ in
name.replaceAll(matching: #"\."#, with: " ")
name.replaceFirst(matching: " 720p", .ignoreCase, with: "")
name.replaceFirst(matching: " 1080p", .ignoreCase, with: "")
Expand All @@ -39,21 +39,24 @@ struct RenameMoviesCommand: ParsableCommand {
RenameMoviesCommand.main()
```

after `chmod a+x myrename` and moving it to somewhere in the shell command path like `/usr/local/bin`, can then do:
The functions `replaceFirst` and `replaceAll` are from `sharplet/Regex`. If your script uses this package too, you're also able to pass options such as `.ignoreCase` to those shortcut functions as shown rather than having to construct a `Regex` yourself to provide those options.

Thanks to the magic of `swift-sh`, after a `chmod a+x myrename` and moving it to somewhere in the shell command path like `/usr/local/bin`, you can then do:

```bash
$ myrename --help
OVERVIEW: Renames my ripped movies from their old name format to how I prefer them now.

USAGE: myrename [<files> ...] [--quiet] [--verbose] [--dry-run]
USAGE: myrename [<files> ...] [--quiet] [--verbose] [--dry-run] [--try]

ARGUMENTS:
<files> Files to rename.

OPTIONS:
-q, --quiet Suppress non-error output.
-v, --verbose Verbose output (overrides "--quiet").
--dry-run Show what would be renamed (no files are changed).
--dry-run Show what would be renamed (overrides "--quiet", no files are changed).
--try Try hypothetical file names (overrides "--quiet", no files are changed).
-h, --help Show help information.

$ myrename ~/Movies/Die.Hard.1988.720p.mp4
Expand All @@ -68,7 +71,7 @@ If you use the [fish shell](https://fishshell.com/), add this [selection functio
$ myrename (selection)
```

(exercise for the reader: make something similar work in other shells)
(exercise for the reader: make something similar that works in other shells)

## See Also

Expand Down
98 changes: 53 additions & 45 deletions Sources/RenameCommand/RenameCommand.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,99 +32,107 @@ import ArgumentParser
import Files
import Regex

public typealias RenameFunc = (_ name: inout String, _ extn: String) throws -> Void

public struct RenameOptions: ParsableArguments {
@Argument(help: "Files to rename.")
@Argument(help: "Files to rename.", completion: .file())
public var files: [String]

@Flag(name: .shortAndLong, help: "Suppress non-error output.")
public var quiet: Bool
public var quiet: Bool = false

@Flag(name: .shortAndLong, help: "Verbose output (overrides \"--quiet\").")
public var verbose: Bool
public var verbose: Bool = false

@Flag(name: .customLong("dry-run"), help: "Show what would be renamed (no files are changed).")
public var dryRun: Bool
@Flag(name: .customLong("dry-run"), help: "Show what would be renamed (overrides \"--quiet\", no files are changed).")
public var dryRun: Bool = false

@Option(name: .customLong("try"), help: "Try hypothetical file name (file parameters ignored, no files are changed).")
public var tryPattern: String?
@Flag(name: .customLong("try"), help: "Try hypothetical file names (overrides \"--quiet\", no files are changed).")
public var `tryOut`: Bool = false

public init() { } // swift complains if this not present

@discardableResult
public func runRename(_ renameFunc: (_ name: inout String) -> Void) throws -> Int {
if let tryPattern = tryPattern {
guard !tryPattern.isEmpty else {
return 1
}

var (baseName, fileExtn) = separateExtension(tryPattern)
reportBefore(index: 1, path: "\(tryPattern)")

renameFunc(&baseName)

let replacementName = fileExtn != nil ? "\(baseName).\(fileExtn!)" : baseName
reportAfter(index: 1, original: tryPattern, replacement: replacementName)

return 0
}

public func runRename(_ renameFunc: RenameFunc) throws -> Int {
var i = 0, nrenamed = 0
for path in files {
let (_, fileName) = separateFile(path)
if fileName.isEmpty { // disregard empty argument strings
continue
}
i += 1

let file = try File(path: path)
guard let parent = file.parent else {
throw Files.LocationError(path: path, reason: .cannotRenameRoot)
let file: File?
if tryOut {
file = nil
} else {
file = try File(path: path)
if file!.parent == nil {
throw Files.LocationError(path: path, reason: .cannotRenameRoot)
}
}
let fileName = file.name
var (baseName, fileExtn) = separateExtension(fileName)
reportBefore(index: i, path: "\(parent.path)\(fileName)")

renameFunc(&baseName)

reportBefore(index: i, path: path)

let (fileBase, fileExtn) = separateExtension(fileName)
var newBase = fileBase
try renameFunc(&newBase, fileExtn)

let replacementName = fileExtn != nil ? "\(baseName).\(fileExtn!)" : baseName
let replacementName = newBase.isEmpty ? fileName : "\(newBase).\(fileExtn)"
if replacementName != fileName {
if !dryRun {
try file.rename(to: replacementName)
if !dryRun && !tryOut {
try file!.rename(to: replacementName)
}
nrenamed += 1
}

reportAfter(index: i, original: fileName, replacement: replacementName)
}
return nrenamed
}

func reportBefore(index i: Int, path: String) {
if verbose {
print("\(i). \(path)")
print("\(i). '\(path)'")
}
}

func reportAfter(index i: Int, original: String, replacement: String) {
if replacement != original {
if verbose {
print("\(String(repeating: " ", count: "\(i)".count)) renamed to \(replacement)")
} else if !quiet {
print("\(String(repeating: " ", count: "\(i)".count)) renamed to '\(replacement)'")
} else if !quiet || tryOut || dryRun {
print("'\(original)' renamed to '\(replacement)'")
}
} else {
if verbose {
print("\(String(repeating: " ", count: "\(i)".count)) not renamed")
} else if !quiet && dryRun {
} else if !quiet || tryOut || dryRun {
print("'\(original)' not renamed")
}
}
}

func separateExtension(_ name: String) -> (base: String, extn: String?) {
func separateFile(_ full: String) -> (path: String, file: String) {
var path = ""
var file = full
let components = full.split(separator: "/", omittingEmptySubsequences: false)
if components.count > 1 {
path = components.dropLast().joined(separator: "/")
file = String(components.last!)
}
return (path, file) // to re-concatenate, path.isEmpty ? "\(path)/\(file)" : file
}

func separateExtension(_ name: String) -> (base: String, extn: String) {
var base = name
var extn: String? = nil
let components = name.split(separator: ".")
if let ext = components.last {
extn = String(ext)
var extn = ""
let components = name.split(separator: ".", omittingEmptySubsequences: false)
if components.count > 1, let e = components.last, e.count > 0 {
base = components.dropLast().joined(separator: ".")
extn = String(e)
}
return (base, extn)
return (base, extn) // to re-concatenate, extn.isEmpty ? "\(base).\(extn)" : base
}
}

Expand Down

0 comments on commit 8c3fcee

Please sign in to comment.