2

I have folders setup like this:

/path/to/directory/SLUG_1/SLUG_1 - SLUG_2 - SLUG_3

SLUG_2 is a year, and it may have a letter after the year, like "1994" or "2003a".

I would like to rename those files to:

/path/to/directory/SLUG_1/SLUG_2 - SLUG_3

I'm getting pretty close with this command:

find $root -mindepth 2 -maxdeth 2 -type d | sed "s#\(\(/path/to/directory/[^/]*/\).* - \([0-9]\{4\}[a-bA-B]\? - .*\)\)#mv "\1" "\2\3"#

This prints:

mv "/path/to/directory/SLUG_1/SLUG_1 - SLUG_2 - SLUG_3" "/path/to/directory/SLUG_1/SLUG_2 - SLUG_3"

Which is exactly the command I want to execute. But I can't execute it.

I tried assigning the output to a variable and executing it by calling the variable. That didn't work. I tried a few variations on that idea, and I got errors.

It feels like I'm missing some tool here. Some iterating tool that makes this job easy. Any suggestions?

JoshuaD
  • 789
  • 1
  • 6
  • 13

3 Answers3

2

This prints:

mv ...

Which is exactly the command I want to execute. But I can't execute it.

I tried assigning the output to a variable and executing it by calling the variable. That didn't work. I tried a few variations on that idea, and I got errors.

It feels like I'm missing some tool here.

The easy way you are looking for is to append | bash to your command. That's how you can go from "a command which prints a command" to "actually running the command that was printed."

However, even though you've included double quotes in the command to be printed, this is a bad idea to include in a script.

You may have handled the whitespace aspect, but what if the filenames include double quote characters directly? Or newlines? Or variable names (which will be expanded within double quotes)?

The only characters that are illegal in filenames are a slash (/) and a null byte.

In writing scripts you should hold yourself to a much higher standard of robustness than you might at an interactive command line.

At a command line, you can see the command you are about to run, you can confirm it's correct, and you can then run it. In a script, there is no oversight and no confirmation. The script will do what you told it to do, no matter how destructive that may turn out to be.

So the correct approach is actually to use find, as detailed in my other answer. This will handle any filenames correctly and doesn't contain arbitrary code execution vulnerabilities.

Wildcard
  • 36,499
  • 1
    Appending |bash won't work if there are special characters in file names such as spaces. You can make it work, but it's a lot more complicated than simple find usage. – Gilles 'SO- stop being evil' Oct 27 '16 at 22:41
  • @Gilles, trust me, I know that. I guess my original answer didn't have enough dire warnings in it, though. How is it now? – Wildcard Oct 27 '16 at 23:31
  • (Actually, I would love to merge this with my other answer; is there a way to do that or should I just delete my other answer and edit the text from it into this one?) – Wildcard Oct 27 '16 at 23:32
  • 1
    Given that the sample names in the question contain spaces, I'd just delete this answer. I'd hardly ever condone piping file names into a shell, but if it's going to fail for the very example in the question, that's a clear no. – Gilles 'SO- stop being evil' Oct 27 '16 at 23:43
  • @Gilles, look again. The OP's command prints a mv command with the needed double quotes. – Wildcard Oct 27 '16 at 23:45
2

Assuming all subdirectories in /path/to/directory follow that naming convention:

cd /path/to/directory
prename -n 's~/\d{4}[a-z]? - ~/~i' */*

prename is the Perl rename (any of the variants will do).

Satō Katsura
  • 13,368
  • 2
  • 31
  • 50
1

Another answer, using GNU find to handle the renaming. This method is robust regardless of what characters may be in the filename.

If I understand your use case rightly, you want to rename directories that start with the full name of their parent directory. In other words, if your directory is named like so:

/some/path/abcdefghi/abcdefghi - something - else/

You want to rename it like so:

/some/path/abcdefghi/something - else/

Since you specify GNU as a tag on this question, you can use the GNU extensions to the find command and handle this like so:

find . -mindepth 2 -maxdepth 2 -type d -regextype posix-egrep -regex '.*/([^/]+)/\1[^/]+' -exec sh -c 'new="$(sed -r "s:/([^/]+)/\\1 ?-? ?([^/]+)\$:/\\1/\\2:" <<<$1)"; mv "$1" "$new"' find-sh {} \;

Test results:

[vagrant@localhost test]$ mkdir -p SLUG_1/SLUG_{1\ -\ SLUG_{2..4}\ -\ SLUG_5,7\ -\ something}
[vagrant@localhost test]$ find . -type d
.
./SLUG_1
./SLUG_1/SLUG_1 - SLUG_2 - SLUG_5
./SLUG_1/SLUG_1 - SLUG_4 - SLUG_5
./SLUG_1/SLUG_1 - SLUG_3 - SLUG_5
./SLUG_1/SLUG_7 - something
[vagrant@localhost test]$ find . -mindepth 2 -maxdepth 2 -type d -regextype posix-egrep -regex '.*/([^/]+)/\1[^/]+' -exec sh -c 'new="$(sed -r "s:/([^/]+)/\\1 ?-? ?([^/]+)\$:/\\1/\\2:" <<<$1)"; mv "$1" "$new"' find-sh {} \;
[vagrant@localhost test]$ find . -type d
.
./SLUG_1
./SLUG_1/SLUG_3 - SLUG_5
./SLUG_1/SLUG_4 - SLUG_5
./SLUG_1/SLUG_2 - SLUG_5
./SLUG_1/SLUG_7 - something
[vagrant@localhost test]$ 
Wildcard
  • 36,499
  • Can you explain that a little bit? In particular, I don't understand the find-sh {} at the end. – JoshuaD Oct 28 '16 at 06:12
  • @JoshuaD, you can read about it in detail here: http://unix.stackexchange.com/a/93331/135943 In short, that string is only used for error reporting by the sh shell spawned by find -exec sh .... I could set it to anything, but I chose find-sh to be relatively informative. – Wildcard Oct 28 '16 at 17:35