0

I have a simple string that will rename all jpeg files in a folder sequentially:

ls -v | cat -n | while read n f; do mv -n "$f" "pic-$n.jpg"; done

But I have a folder with many different file types. Is there a simple string or variable I can replace the .jpg with to retain the file extensions? Or does it have to be a full script now? I'd like to keep it to a one liner if possible.

Examples

Say I have a folder like this:

a long hash name.mp4
another name.m4v
video.mov
another name.mp4

and so on. I want to rename the files like the following so that they look more organized and don't have too many characters, like so:

vid-1.mp4
vid-2.m4v
vid-3.mov
vid-4.mp4

If I were to use ls -v | cat -n | while read n f; do mv -n "$f" "vid-$n"; done, then I wouldn't have file extensions. I want to retain the file extensions because it makes it easier sometimes to open them.

I am mainly wondering if there's a global variable in bash to define file extensions

5
  • 1
    Please edit your question to give one or two examples - I suspect you're looking for parameter expansions like "${f%.*}" and "${f##*.}" but it's hard to tell from your description Commented Jan 13 at 14:07
  • 1
    And also please show us an example of your file names before renaming and what you want after renaming. Parsing ls this way is rarely a good idea.
    – terdon
    Commented Jan 13 at 14:17
  • example added to my original post Commented Jan 13 at 16:06
  • @steeldriver I was messing with "${f%.*}" and "${f##*.}" but I couldn't figure out how to get it to work in the above one-liner. I am mainly wondering if there's a global variable in bash to define file extensions Commented Jan 13 at 16:08
  • @DealTime444 well the %.* in ${f%.*} matches the shortest "dot extension" - so in that sense, it's "global" Commented Jan 13 at 17:04

2 Answers 2

2

Don't use a loop here, just use rename:

$ ls
'a bad name.png'   asdlha.gif   askd.png  'a worse'$'\n''name.jpg'   kasdjh.jpg

$ rename -n 's/.*(\..+)/"pic-" . ++$a . "$1"/se' *
a bad name.png -> pic0.png
asdlha.gif -> pic1.gif
askd.png -> pic2.png
a worse
name.jpg -> pic3.jpg
kasdjh.jpg -> pic4.jpg

If that looks right, re-run without the -n to actually rename.

Explanation:

The rename command is a perl script that can run perl code to rename files. Here, we are using the substitution operator (s/old/new/) with a couple of special options:

  • The /s makes the substitution operator treat its input as a single line, allowing . to match newline characters. We need this to handle file names with newlines, as I have in the example above.
  • The /e lets us evaluate the right hand side (s/left/right/) as a perl expression. The expression "pic-" . ++$a . "$1" means "the string pic-, then the value of the variable $a incremented by one, and then whatever was matched inside the parentheses on the left hand side". In our case, the extension.

Next, the regular expression used (.*(\..+)) means "zero or more characters (.*) until the last . (\. matches a literal .), then one or more characters (.+). Because the last \..+ is in parentheses, that makes what was matched (the extension) available as $1 on the right hand side.

After running the command above without the -n, I was left with:

$ rename  's/.*(\..+)/"pic-" . ++$a . "$1"/se' *
terdon@oregano foo $ ls
pic-1.png  pic-2.gif  pic-3.png  pic-4.jpg  pic-5.jpg

Alternatively, you can do this in the shell with something like:

i=1; for file in *; do 
  ext="${file##*.}"; 
  mv -- "$file" pic-$((i++))."$ext"; 
done

Here, we loop over all files (and directories, if you don't want to rename directories be careful), saving each as $file. Then, we get the extension with ${file##*.}. The ${variable##pattern} format will remove the longest match for pattern from the variable $variable. By using ${file##*.} we remove everything until the last ., leaving only the extension which we save as $ext.

Next, the mv command will rename $file to pic-, plus the value of the variable $i, which we initialize with i=1, incremented by one, followed by a . and the extension.

Like the rename solution above, this too can handle arbitrary file names:

$ ls
'a bad name.png'   asdlha.gif   askd.png  'a worse'$'\n''name.jpg'   kasdjh.jpg
$ i=1; for file in *; do 
  ext="${file##*.}"; 
  mv -- "$file" pic-$((i++))."$ext"; 
done
$ ls
pic-1.png  pic-2.gif  pic-3.png  pic-4.jpg  pic-5.jpg
5
  • When I run that rename command I get this: Global symbol "$i" requires explicit package name (did you forget to declare "my $i"?) at line 1 Commented Jan 13 at 15:56
  • @DealTime444 argh yes, sorry. Ubuntu has a different version fo rename than I'm used to. Try the updated command (just change $i to $a a variable that is already globally defined in perl).
    – terdon
    Commented Jan 13 at 16:08
  • rename 's/.*(\..+)/"pic-" . ++$a . "$1"/se' * works perfectly, but I am left wondering if there's a globally defined variable in bash for file extensions that I could have added to my original command Commented Jan 13 at 16:15
  • No, there isn't. That has nothing to do with bash anyway, it is a perl variable. And in any case, no, bash has no special thing for extensions. Remember that extensions in Linux are irrelevant. With very, very few exceptions, such as the gzip command, nobody cares about extensions. An image named foo.boing can be a png just fine. So there aren't many tools designed to do things with extensions since extensions are optional.
    – terdon
    Commented Jan 13 at 16:16
  • Understood, it's just that sometimes certain software gets confused if you have the wrong file extension. It also helps when I have default applications set for certain file types. Anyway, I think the rename command answers my question. I asked for a one-liner and that's what you gave me. Thanks Commented Jan 13 at 16:21
2

If you specifically need the files to be named arithmetically in version sort order (as with ls -v) then I'd suggest something like

printf '%s\0' *.* | sort -zV | {
  local n=1
  while IFS= read -r -d '' f; do 
    echo mv -nv -- "$f" "pic-$((++n)).${f##*.}"
  done
}

or, if you want to zero-pad the new names (which makes subsequent sorting easier), perhaps

printf '%s\0' *.* | sort -zV | { 
  local n=1
  while IFS= read -r -d '' f; do 
    printf -v p 'pic-%03d.%s' $((++n)) "${f##*.}"
    echo mv -nv -- "$f" "$p"
  done
}

Remove the echo once you have confirmed that the right files will be processed in the desired order. If you don't care about readability you can of course write these as "one liners" if you wish, ex.

printf '%s\0' *.* | sort -zV | { local n=1; while IFS= read -r -d '' f; do printf -v p 'pic-%03d.%s' $((++n)) "${f##*.}"; echo mv -nv -- "$f" "$p"; done; }

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .