4

I need to rename a couple of files using shell scripting, by a certain "key". This key includes both strings as well as extracted portions of file path that I get with find.

I am on a Mac, OSX El Capitan and am using ZSH. Here is the directory tree:

├── 300x250
│   ├── 300x250-img-fallback.jpg
│   └── index/
├── 300x600
│   ├── 300x600-img-fallback.jpg
│   └── index/
├── 336x280
│   ├── 336x280-img-fallback.jpg
│   └── index/
└── 970x250
    ├── 970x250-img-fallback.jpg
    └── index/

I need to rename ../index/ folders into ../c2_[parentFolderName]/. This is what I am trying:

find . -type d -mindepth 2 -maxdepth 2 -exec sh -c 'echo -- mv "$0" "$(dirname "$0")"/"C2_"$(basename "$0/..")""' {} \;

This doesn't seem to be the proper way to get the basename of the parent unfortunately.

find . -name "*index" -exec sh -c 'echo -- mv "$0" "$(dirname "$0")"/"C2_"$(basename "$0/..")""' {} \;

This one is just a variation which also does not work (there is no reason why it should :) ).

I am quite new to shell scripting and am trying to learn as much as possible in a shell agnostic kind of way, so please disregard that I am using ZSH currently.

3 Answers3

4

Best would be to use zsh's zmv:

autoload zmv # best in ~/.zshrc
zmv -n '(*)/index' '$1/C2_$1'

(remove -n when happy).

For a portable (POSIX sh) solution:

for dir in */index;  do
  mv -i -- "$dir" "${dir%/*}/C2_${dir%/*}"
done

(using -i as a poor man's ersatz to the sanity checks zmv does).

If you wanted to use find portably (POSIXly), you'd need to forget about -mindepth/-maxdepth, which you can replace with combinations of -path and -prune:

LC_ALL=C find . -path './*/*' -prune -name index -exec sh -c '
  for dir do
    top=${dir#./}
    top=${top%/*}
    mv -i -- "$dir" "$top/C2_$top"
  done' sh {} +

One difference with the other two approaches is that it will not follow symlinks and that it will also look for index in hidden directories.

Those make use of the standard ${var#pattern}, ${var%pattern} parameter expansion operators described in countless Q&As here or at the POSIX shell specification.

  • Stéphane, thank you for your reply! Since I'm new to this, care to explain a bit more? I "believe" that this finds everything (*) "here" with name "index" and replaces variable1 with a string_variable2. How come ot doesn't start with 0 like in bash? – Alexander Starbuck Aug 30 '16 at 11:30
  • 1
    @AlexStarbuck, $0 is the programs/function name while $1..., $n are the positional parameters. I've added links to documentation. See also Is there a reason why the first element of a Zsh array is indexed by 1 instead of 0? – Stéphane Chazelas Aug 30 '16 at 11:42
  • I was taught that when you use {} with say find, the results get automatically "ordered" into variables $0, $1, $2, ... Seems that there is much more to learn. – Alexander Starbuck Aug 30 '16 at 13:56
  • 1
    @AlexStartbuck, in sh -c '...' a b c, indeed a goes to $0 and b to $1, but $0 is interpreted as the name of that inline script, not an argument to that inline script, just like when you run sh myscript a b c, myscript (this time not inline) gets a, b, c as arguments ("$@") and myscript in $0. So you should give the name that you want to give to that inline script as that first argument (I usually use sh). Where it matters is that that $0 gets used in error messages. Try for instance: sh -c 'echo > /' foo bar – Stéphane Chazelas Aug 30 '16 at 14:12
  • After running: sh -c 'echo > /' foo bar, as you suggested, I got this: foo: /: Is a directory :) – Alexander Starbuck Aug 30 '16 at 14:16
  • 1
    @AlexStartbuck, yes, exactly, see how the error message is reported as coming from a command called foo, as that's what ended up in $0 (the inline script name). – Stéphane Chazelas Aug 30 '16 at 14:18
2

For what it's worth, your attempt

find . -type d  -exec sh -c 'echo -- mv "$0" "$(dirname "$0")"/"C2_"$(basename "$0/..")""' {} \;

seems to almost work, except that basename doesn't interpret the dot-dot. But taking dirname should give you the parent's name, and you can take the basename of that. Something like this:

$ mkdir -p 300x250/index 970x250/index
$ find . -name index -exec sh -c 'echo mv "$1" "$(dirname "$1")/C2_$(basename "$(dirname "$1")")"' sh {} \;
mv ./300x250/index ./300x250/C2_300x250
mv ./970x250/index ./970x250/C2_970x250

As Stéphane mentioned, the first argument after sh -c "..." is taken as the script's name, which goes in $0 and is distinct from the usual positional parameters from $1 upward. The latter have tools specifically to work with them, like shift and $@, and they don't work with $0.

For just one argument, using $0 could work, but as the shell's name might be used for other purposes, it's preferable to set something sensible (like sh) there, and put the actual arguments after that.

ilkkachu
  • 138,973
  • Ilkkachu - how would you in that case get the grandparent's basename? Or further up the folder hierarchy? – Alexander Starbuck Aug 30 '16 at 13:55
  • 1
    $0 is also used for error messages by the shell, so it's generally better to put something sensible in it like sh or inline-sh. Using -exec sh ... inline-sh {} + and a loop in sh also avoids running one sh instance per file. – Stéphane Chazelas Aug 30 '16 at 13:58
  • The ${file%/*} operator has that extra benefit over $(dirname -- "$file") (beside the obvious performance one) that it also works if $file ends in newline characters. – Stéphane Chazelas Aug 30 '16 at 14:01
  • @StéphaneChazelas One shell per file?! :O I see. Unfortunately, this is as much as I can grasp now, without someone guiding me through your script step by step I am unlikely to lear correctly. Is there a way I can contact dou directly for a couple of messages? – Alexander Starbuck Aug 30 '16 at 14:12
  • @Alex, yes, find -exec {} ; runs the given command for every file separately. -exec {} + stacks multiple files to the command line of one invocation. And yes, the for x in */index approach is usually better than using find (for various reasons), and cutting the string with the shell's operations is faster than calling basename and dirname. I meant this only as a minimal fix to your original attempt, and frankly I'd prefer if you checked another answer as accepted. – ilkkachu Aug 30 '16 at 17:48
0

Never worked on ZSH, but below code works very well on bash

SHW@SHW:/tmp/test1 # tree
.
├── 300x250
│   ├── 300x250-img-fallback.jpg
│   └── index
├── 300x600
│   ├── 300x600-img-fallback.jpg
│   └── index
├── 336x280
│   ├── 336x280-img-fallback.jpg
│   └── index
└── 970x250
    ├── 970x250-img-fallback.jpg
    └── index

8 directories, 4 files

SHW@SHW:/tmp/test1 # for i in `find . -type d -name index`; do  mv $i `dirname $i | cut -c3-`/c2_`dirname $i | cut -c3-`; done

SHW@SHW:/tmp/test1 # tree
.
├── 300x250
│   ├── 300x250-img-fallback.jpg
│   └── c2_300x250
├── 300x600
│   ├── 300x600-img-fallback.jpg
│   └── c2_300x600
├── 336x280
│   ├── 336x280-img-fallback.jpg
│   └── c2_336x280
└── 970x250
    ├── 970x250-img-fallback.jpg
    └── c2_970x250

8 directories, 4 files
SHW
  • 14,786
  • 14
  • 66
  • 101