8

When reading tutorials on batch-renaming files in bash and using the sort command to sort file contents, I have not been able to figure out how to combine the two.

I have a directory whose contents are sorted using tags within the filename, similar to how the program TagSpaces handles things. I add whatever tags I can think of to the end of the filename when I create it or download it. Here's an example:

Sunrise (2) #wallpaper #4k #googleimages.jpg

Now I want to go through all these files and rename them so the tags are sorted alphabetically, without affecting anything before or after the tags (e.g. a picture's title or file extension). So the above would become:

Sunrise (2) #4k #googleimages #wallpaper.jpg

How do I accomplish this? I can't even figure out how to pass a file's name, and not its contents, to a command like sort, whose output I could then perhaps pipe to mv.

5
#!/bin/bash

for i in dir_for_files/*; do
    filename=${i%%#*}
    sorted_tags=$(grep -o "#[^ .]*" <<< "$i" | sort -n | tr '\n' ' ')
    ext=${i#*.}
    echo mv "$i" "${filename}${sorted_tags% }.$ext"
done

Testing:

##### Before sorting #####    
$ ls -1 dir_for_files
Note (3) #textfile #notes #important.txt
Sunrise (2) #wallpaper #4k #googleimages.jpg
Sunset (2) #wallpaper #2k #images.jpg

$ ./sort_tags.sh

##### After sorting #####
$ ls -1 dir_for_files
Note (3) #important #notes #textfile.txt
Sunrise (2) #4k #googleimages #wallpaper.jpg
Sunset (2) #2k #images #wallpaper.jpg
3

If you have the perl-based rename (prename on some systems) then you could do a split + sort the tags using perl. For example, given

$ ls *.jpg
Sunrise (2) #wallpaper #4k #googleimages.jpg

then (with some ugly wrangling to remove and replace the .jpg suffix)

$ rename -v 'my @F = split / #/, substr($_,0,-4); $_ = (join " #", shift @F, sort @F) . ".jpg"' *.jpg
Sunrise (2) #wallpaper #4k #googleimages.jpg renamed as Sunrise (2) #4k #googleimages #wallpaper.jpg

Checking

$ ls *.jpg
Sunrise (2) #4k #googleimages #wallpaper.jpg

Probably lots of room for improvement - but I hope it gives you some idea.

3

With zsh:

autoload zmv # best in ~/.zshrc
zmv -n '([^#]#)(\#*)(.*)' '$1${(j: :)${(os: :)2}}$3'

(remove the -n (dry-run) if happy).

  • [^#]#: 0 or more non-# characters (# is like * in regexps)
  • s: : split on space
  • o: order (sort)
  • j: :: join with space.

So, we're splitting the part in between the first # (included) and last . (excluded) on space, sort the resulting list which we join back with space.

Recursively:

zmv -n '(**/)([^#]#)(\#*)(.*)' '$1$2${(j: :)${(os: :)3}}$4'

To allow spaces in tag names, we could split on # and trim trailing spaces, sort and join on # with:

zmv -n '([^#]#\#)(*)(.*)' '$1${(j: #:)${(os:#:)2}%% #}$3'

Add a (#qD) glob qualifier if you also want to process hidden files (Dot files) or want to process files in hidden directories.

2

Nice question!

Here is my simple bash script for this:

for file in *.jpg; do 
    afile=( ${file#*)} ); 
    echo mv "$file" "${file%%#*}$(echo $(sort<(printf "%s\n" "${afile[@]%%.*}"))).jpg";
done

Explanation:

  • In afile=( ${file#*)} );: we are converting the string to an array. In this state shell is performing word splitting with spaces unless you quote the string.

    In ${file#*)} (cut-up-to-first-prefix): we are stripping everything from the start of the string till first ) seen using shell parameter expansion as it will result #wallpaper #4k #googleimages.jpgconsidering the file="Sunrise (2) #wallpaper #4k #googleimages.jpg"

  • In ${file%%#*} (cut-up-to-last-suffix); stripping start from end to the begging of string till last # seen. this will result Sunrise (2)

  • In ${afile[@]%%.*} (cut-up-to-last-suffix): same as above, stripping start from end to the begging of string (here in each indexed array) till last . seen. this will result #wallpaper #4k #googleimages, we could also use ${afile[@]%.*} better!

  • In printf "%s\n" "${afile[@]%%.*}": we are printing the array elements with newlines ([@] used for indexed array), (why with newlines? Because we will sort them and we should split the elements in newlines)

  • In $(sort<(printf "%s\n" "${afile[@]%%.*}")): we are sorting the elements (or tags).

  • In $(echo $(sort<(printf "%s\n" "${afile[@]%%.*}"))): same as above but we used extra echo command to gathering the sorted elements into the one linear.

    possible same also with using double xargs like ... |xargs -n1| sort | xargs.
    Please see below example to better understand this step:

    echo -e "C\n4\nB" |sort
    4
    B
    C
    echo $(echo -e "C\n4\nB" |sort)
    4 B C
    

Finally at the end, mv command is renaming the $file to the modified name should be.

Ps: remove echo mv ... in front of the mv to exit dry-run and perform actual renaming.

1

This seems a bit too complicated for Bash to be honest. It's not impossible, but in my opinion, you're far better off using a language made for "real" programming here. A Bash solution will likely be unmaintanable. (Not trying to insult the Bash-only solution, I think the rename-magic is pretty amazing, actually)

That said, here's a solution with Ruby. You can write it to a file and then just execute the file from within your shell, given that you have Ruby installed.

#!/usr/bin/env ruby
Dir.glob('*.jpg').each do |filename|
    # `name` contains the name of the file, without the tags. `tags` is an array
    # of all tags of the file.
    name, *tags = filename.split(' #')
    # if there aren't any tags, just skip the file.
    if tags.empty?
        next
    end
    # remove the trailing '.jpg' and sort all the tags
    tags.last.gsub!(/\.jpg$/,'')
    tags.sort!
    tags = [name] + tags
    # finally, move the file to the correct location with sorted tags.
    File.rename filename, "#{tags.join(' #')}.jpg"
end

To execute the script, simply place it in the directory where your images are in. This solution should be fairly resilient to silly image names and tags like C# won't cause problems. That said, be sure to make a backup before running the script. Movement operations can be just as destructive as a rm.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.