[stdlib-candidate] Clean up file bulk-rename a little (#798)

[Generated diff](https://www.diffnow.com/report/xmq4f) for command
because move broke it.

- No `file` prefix similar to other [filesystem
commands](https://www.nushell.sh/commands/categories/filesystem.html)
- Input paths instead of directory param for filters or globbing
- Record closure param with original path (`$in` is still stem)
- `--verbose` table output
- `--no-exectute` for dry run with `--verbose`
- Shorthand flags
- Parallel renaming for large directories
- More tests

@amtoine Request feedback 🙏🏼 Happy to revise ot revert anything!

```console
❯ bulk-rename -h
Rename bulk input files in parallel using a closure.

The reason behind this command is quite simple:
- Sometimes one receives a bunch of files with integer ids: 1, 2, 3, ...
- These ids come rarely with padding... i.e. 1 instead of 001 when there are 3-digit ids
- This means that file with id 9 will be sorted way after file with id 1000

This command allows to do such a task!

Examples:
  Rename `.mise.toml` files to `.mise.local.toml` recursively
  > glob **/.mise.toml | bulk-rename { str append .local }

  Rename files in `/foo` with a name that has an id to have 3 digits with 0-padding
  > ls /foo | bulk-rename { |path|
     if $path.input.type == file {
       $path.stem | parse "some_format_{id}"
         | get 0
         | update id { fill --alignment r --character 0 --width 3 }
         | $"some_format_($in.id)"
     }
     # else skip dirs
   }

Usage:
  > main {flags} <update_stem>

Flags:
  -v, --verbose - Show which files were renamed, if any
  -n, --no-execute - Do not make any changes; add --verbose to see what would be made
whitespace bug: nushell/nushell#12264
  -h, --help - Display the help message for this command

Parameters:
  update_stem <closure()>: The code to rename the file stem: receives the old stem as input and a record param with both `stem` and `input` keys

Input/output types:
  ╭───┬───────────┬─────────────────────────────────╮
  │ # │   input   │             output              │
  ├───┼───────────┼─────────────────────────────────┤
  │ 0 │ list<any> │ nothing                         │
  │ 1 │ list<any> │ table<old: string, new: string> │
  ╰───┴───────────┴─────────────────────────────────╯
```
This commit is contained in:
Texas Toland 2024-03-30 16:19:06 -05:00 committed by GitHub
parent f39976902a
commit 268201e4ac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 146 additions and 87 deletions

View File

@ -0,0 +1,49 @@
# Rename bulk input files in parallel using a closure.
#
# The reason behind this command is quite simple:
# - Sometimes one receives a bunch of files with integer ids: 1, 2, 3, ...
# - These ids come rarely with padding... i.e. 1 instead of 001 when there are 3-digit ids
# - This means that file with id 9 will be sorted way after file with id 1000
#
# This command allows to do such a task!
#
# Examples:
# Rename `.mise.toml` files to `.mise.local.toml` recursively
# > glob **/.mise.toml | bulk-rename { str append .local }
#
# Rename files in `/foo` with a name that has an id to have 3 digits with 0-padding
# > ls /foo | bulk-rename { |path|
# if $path.input.type == file {
# $path.stem | parse "some_format_{id}"
# | get 0
# | update id { fill --alignment r --character 0 --width 3 }
# | $"some_format_($in.id)"
# }
# # else skip dirs
# }
export def main [
update_stem: closure, # The code to rename the file stem: receives the old stem as input and a record param with both `stem` and `input` keys
--verbose (-v), # Show which files were renamed, if any
--no-execute (-n) # Do not make any changes; add --verbose to see what would be made
]: [list<any> -> nothing, list<any> -> table<old: path new: path>] {
let renamed = par-each --keep-order { |input|
let update_or_keep_stem = { |parts|
do $update_stem { stem: $in input: $input } | default $parts.stem
}
let old = if ($input | describe) == string {
$input
} else {
$input.name # convenience for ls
}
let new = $old | path parse | update stem $update_or_keep_stem | path join
if $new != $old {
if not $no_execute {
mv --force --verbose=$verbose $old $new
}
{ old: $old new: $new }
}
}
if $verbose {
$renamed
}
}

View File

@ -1,35 +0,0 @@
# rename a bulk of files in a directory using a closure
#
# the reason behind this command is quite simple
# - sometimes one receives a bunch of files with integer ids: 1, 2, 3, ...
# - these ids come rarely with padding... i.e. 1 instead of 001 when there are 3-digit ids
# - this means that file with id 9 will be sorted way after file with id 1000
#
# this command allows to do such a task!
#
# # Examples
# rename files in `/foo` with a name that has an id to have 3 digits with 0-padding
# > file bulk-rename /foo {
# parse "some_format_{id}"
# | get 0
# | update id { fill --alignment r --character 0 --width 3 }
# | $"some_format_($in.id)"
# }
export def "file bulk-rename" [
directory: path, # the path where files need to be renamed in bulk
stem_update: closure, # the code to run on the stem of the files: should start with parsing the format and end with reconstructing the same format
--verbose, # be verbose when moving the files around
]: nothing -> nothing {
ls --full-paths $directory | insert new {|row|
$row.name | path parse | update stem $stem_update | path join
}
| each {
if $verbose {
mv --force --verbose $in.name $in.new
} else {
mv --force $in.name $in.new
}
}
null
}

View File

@ -2,5 +2,5 @@
export module record/
export module str/
# commands
export use fs.nu *
export use bulk-rename.nu *
export use set-env.nu *

View File

@ -0,0 +1,95 @@
use std assert
use ../std-rfc 'bulk-rename'
const fixture = [
.gitignore
Cargo.toml
LICENSE
README.md
src
test.nu
]
export def 'test ls' [] {
let expects = [
.gitignore # hidden by ls
_Cargo.toml
_LICENSE
_README.md
_src
_test.nu
]
test $expects {
ls $in | bulk-rename { '_' + $in }
}
}
export def 'test --no-execute' [] {
test $fixture {
ls $in | bulk-rename --no-execute { '_' + $in }
}
}
export def 'test --verbose' [] {
let expects = [
# .gitignore unchanged
_Cargo.toml
_LICENSE
_README.md
_src
_test.nu
]
let renamed = test $fixture {
ls $in | bulk-rename --verbose --no-execute { '_' + $in }
}
assert equal ($renamed.new | each { path basename }) $expects
}
export def 'test skip-extensions' [] {
let expects = [
.gitignore
Cargo.toml
LICENSE.txt # changed
README.md
src.txt # changed
test.nu
]
test $expects {
ls $in | bulk-rename { |path|
if $path.input.name ends-with $path.stem {
$path.stem + .txt
}
}
}
}
export def 'test glob' [] {
let expects = [
LICENSE # skipped
_.gitignore
_Cargo.toml
_README.md
_test.nu
src # skipped
]
test $expects {
glob ($in | path join *.*) | bulk-rename { '_' + $in }
}
}
def test [expects: list<string> command: closure] {
let test_dir = $nu.temp-path | path join (random uuid)
def actual-files [] {
ls --all --short-names $test_dir | get name | sort
}
# before
mkdir $test_dir
$fixture | each { |name| touch ($test_dir | path join $name) }
assert equal (actual-files) $fixture
# test
let renamed = $test_dir | do $command
assert equal (actual-files) $expects
# after
rm --recursive --force $test_dir
$renamed
}

View File

@ -1,50 +0,0 @@
use std assert
use ../std-rfc "file bulk-rename"
alias rename = file bulk-rename
export def "test file bulk-rename" [] {
let test_dir = $nu.temp-path | path join (random uuid)
mkdir $test_dir
seq 1 10 | each {|i| touch ($test_dir | path join $"some_($i)_format.txt") }
let expected = [
"some_10_format.txt",
"some_1_format.txt",
"some_2_format.txt",
"some_3_format.txt",
"some_4_format.txt",
"some_5_format.txt",
"some_6_format.txt",
"some_7_format.txt",
"some_8_format.txt",
"some_9_format.txt",
]
let actual = glob $"($test_dir)/*" | str replace $test_dir "" | str trim --left --char "/"
assert equal ($actual | sort) $expected
rename $test_dir {
parse "some_{i}_format"
| get 0
| update i { fill --alignment r --character 0 --width 3 }
| $"some_($in.i)_format"
}
let expected = [
"some_001_format.txt",
"some_002_format.txt",
"some_003_format.txt",
"some_004_format.txt",
"some_005_format.txt",
"some_006_format.txt",
"some_007_format.txt",
"some_008_format.txt",
"some_009_format.txt",
"some_010_format.txt",
]
let actual = glob $"($test_dir)/*" | str replace $test_dir "" | str trim --left --char "/"
assert equal ($actual | sort) $expected
rm -rf $test_dir
}

View File

@ -1,3 +1,3 @@
export module fs.nu
export module bulk-rename.nu
export module record.nu
export module str_xpend.nu