15

I am looking for a way (preferably terminal) to organize over 1000 fonts by their first letter.

Basically create directories A-Z, # then move the font files to those directories based on their filename first character. Fonts that begin with numbers [0-9] or other special characters to be moved to the # directory.

3

6 Answers 6

13

A late python option:

#!/usr/bin/env python3
import os
import sys
import shutil

def path(dr, f): return os.path.join(dr, f)

dr = sys.argv[1]
for f in os.listdir(dr):
    fsrc = path(dr, f)
    if os.path.isfile(fsrc):
        s = f[0]; target = path(dr, s.upper()) if s.isalpha() else path(dr, "#")
        if not os.path.exists(target):
            os.mkdir(target)
        shutil.move(fsrc, path(target, f))

How to use

  1. Copy the script into an empty file, save it as move_files.py
  2. Run it with the directory as argument:

    python3 /path/to/move_files.py /path/to/files
    

The script will only create the (sub) directory(-ies) (uppercase) if it is actually needed

Explanation

The script:

  • lists the files, gets the first character (defines the sourcepath):

    for f in os.listdir(dr):
        s = f[0]; fsrc = path(dr, f)
    
  • checks if the item is a file:

    if os.path.isfile(fsrc):
    
  • defines the targeted folder for either if the first char is alpha or not:

    target = path(dr, s.upper()) if s.isalpha() else path(dr, "#")
    
  • checks if the folder already exists or not, creates it if not:

    if not os.path.exists(target):
        os.mkdir(target)
    
  • moves the item into its corresponding folder:

    shutil.move(fsrc, path(target, f))
    
4
  • Hey Jacob. Is there a check for uppercase first letters?
    – Parto
    Jan 10, 2017 at 13:49
  • @Parto Absolutely! In what way do you need it? (I can check in 3,5 hrs teaching :) Jan 10, 2017 at 13:51
  • It actually did. Perfect implementation.
    – Parto
    Jan 10, 2017 at 13:56
  • After considering, I have decided to go with this answer for several reasons: 1). The explanation on what is going on. 2). The answer run correctly on first try
    – Parto
    Jan 12, 2017 at 5:39
11

Code-golfed yet readable with just two commands and two regular expressions:

mkdir -p '#' {a..z}
prename -n 's|^[[:alpha:]]|\l$&/$&|; s|^[0-9]|#/$&|' [[:alnum:]]?*

If you have a huge amount of files to move, too many to fit into the process argument list (yes, there's a limit and it may be just a few kilobytes), you can generate the file list with a different command and pipe that to prename, e. g.:

find -mindepth 1 -maxdepth 1 -name '[[:alnum:]]?*' -printf '%f\n' |
prename -n 's|^[[:alpha:]]|\l$&/$&|; s|^[0-9]|#/$&|'

This has the added benefit of not trying to move the literal file name [[:alnum:]]?* if no files match the glob pattern. find also allows many more match criteria than shell globbing. An alternative is to set the nullglob shell option and close the standard input stream of prename.1

In both cases remove the -n switch to actually move the files and not just show how they would be moved.

Addendum: You can remove the empty directories again with:

rmdir --ignore-fail-on-non-empty '#' {a..z}

1 shopt -s nullglob; prename ... <&-

0
8

If you don't mind zsh, a function and a couple of zmv commands:

mmv() {echo mkdir -p "${2%/*}/"; echo mv -- "$1" "$2";}
autoload -U zmv
zmv -P mmv '([a-zA-Z])(*.ttf)' '${(UC)1}/$1$2'
zmv -P mmv '([!a-zA-Z])(*.ttf)' '#/$1$2'

The mmv function makes the directory and moves the file. zmv then provides pattern-matching and substitution. First, moving filenames starting with an alphabet, then everything else:

$ zmv -P mmv '([a-zA-Z])(*.ttf)' '${(UC)1}/$1$2'
mkdir -p A/
mv -- abcd.ttf A/abcd.ttf
mkdir -p A/
mv -- ABCD.ttf A/ABCD.ttf
$ zmv -P mmv '([!a-zA-Z])(*.ttf)' '#/$1$2'
mkdir -p #/
mv -- 123.ttf #/123.ttf
mkdir -p #/
mv -- 七.ttf #/七.ttf

Run again without the echo in mmv's definition to actually perform the move.

8

I didn't work out a nice way to make the directory names uppercase (or move the files with uppercase letters), although you could do it afterwards with rename...

mkdir {a..z} \#; for i in {a..z}; do for f in "$i"*; do if [[ -f "$f" ]]; then echo mv -v -- "$f" "$i"; fi; done; done; for g in [![:alpha:]]*; do if [[ -f "$g" ]]; then echo mv -v -- "$g" \#; fi; done

or more readably:

mkdir {a..z} \#; 
for i in {a..z}; do 
  for f in "$i"*; do
    if [[ -f "$f" ]]; then 
      echo mv -v -- "$f" "$i"; 
    fi 
  done
done
for g in [![:alpha:]]*; do 
  if [[ -f "$g" ]]; then 
    echo mv -v -- "$g" \#
  fi
done

Remove echo after testing to actually move the files

And then

rename -n 'y/[a-z]/[A-Z]/' *

remove -n if it looks good after testing and run again.

6
  • 2
    You can use if [[ -d "${i^}" ]] to make the variable i capital, and mkdir {A..Z} at the start.
    – Arronical
    Jan 10, 2017 at 11:04
  • Let me try this and see
    – Parto
    Jan 10, 2017 at 11:43
  • @Arronical thanks! I will leave it however, since you posted it your own way
    – Zanna
    Jan 10, 2017 at 11:53
  • @Zanna I like that we came at it from different directions, iterating through the letters and using them as search criteria had never occurred to me. I'm sure there's a clever and fast solution with find, but can't get my head around it!
    – Arronical
    Jan 10, 2017 at 13:38
  • Hey Zanna, this didn't move the fonts that start with an uppercase letter. Otherwise worked fine.
    – Parto
    Jan 10, 2017 at 13:48
7

The following commands within the directory containing the fonts should work, if you want to use from outside the font storage directory, change for f in ./* to for f in /directory/containing/fonts/*. This is a very shell based method, so quite slow, and is also non-recursive. This will only create directories, if there are files which start with the matching character.

target=/directory/to/store/alphabet/dirs
mkdir "$target"
for f in ./* ; do 
  if [[ -f "$f" ]]; then 
    i=${f##*/}
    i=${i:0:1}
    dir=${i^}
    if [[ $dir != [A-Z] ]]; then 
      mkdir -p "${target}/#" && mv "$f" "${target}/#"
    else
      mkdir -p "${target}/$dir" && mv "$f" "${target}/$dir"
    fi
  fi
done

As a one liner, again from within the font storage directory:

target=/directory/to/store/alphabet/dirs; mkdir "$target" && for f in ./* ; do if [[ -f "$f" ]]; then i=${f##*/}; i=${i:0:1} ; dir=${i^} ; if [[ $dir != [A-Z] ]]; then mkdir -p "${target}/#" && mv "$f" "${target}/#"; else mkdir -p "${target}/$dir" && mv "$f" "${target}/$dir" ; fi ; fi ; done

A method using find, with similar string manipulation, using bash parameter expansion, which will be recursive, and should be somewhat quicker than the pure shell version:

find . -type f -exec bash -c 'target=/directory/to/store/alphabet/dirs ; mkdir -p "$target"; f="{}" ; i="${f##*/}"; i="${i:0:1}"; i=${i^}; if [[ $i = [[:alpha:]] ]]; then mkdir -p "${target}/$i" && mv "$f" "${target}/$i"; else mkdir -p "${target}/#" && mv "$f" "${target}/#"; fi' \;

Or more readably:

find . -type f -exec bash -c 'target=/directory/to/store/alphabet/dirs 
   mkdir -p "$target"
   f="{}"
   i="${f##*/}"
   i="${i:0:1}"
   i=${i^}
   if [[ $i = [[:alpha:]] ]]; then 
      mkdir -p "${target}/$i" && mv "$f" "${target}/$i"
   else
      mkdir -p "${target}/#" && mv "$f" "${target}/#"
   fi' \;
1
  • This one worked too. Even for uppercase and lowercase letters.
    – Parto
    Jan 10, 2017 at 14:01
5

Map each filename to a directory name using tr, then mkdir and mv:

find /src/dir -type f -print0 |
xargs -0 -I{} bash -c \
  'dir=/dest/$(basename "{}" | cut -c1 | tr -C "a-zA-Z\n" "#" | tr "a-z "A-Z"); mkdir -p $dir; mv "{}" $dir'
5
  • I really like this, this is along the lines of what I was grasping for with my wish to use find. Is there a way to only create upper case directory names, and move lower case filenames into them?
    – Arronical
    Jan 10, 2017 at 17:52
  • 1
    I added another tr to convert to uppercase.
    – xn.
    Jan 10, 2017 at 17:57
  • Why the detour through xargs just to call bash again? Wouldn't it be simpler and much more readable to pipe the output of find into a while-loop and read it there record-by-record? Jan 10, 2017 at 20:08
  • Good question, @DavidFoerster. I guess bias against loops in one-liners and preference for a more functional approach. But a bash script in a string isn't very elegant either, so I'd say the while loop version (bit.ly/2j2mhyb) is perhaps better.
    – xn.
    Jan 10, 2017 at 20:40
  • By the way, you can avoid the nasty {} substitution if you let xargs append the argument and then refer to $1 inside the shell script, e. g.: xargs -0 -n1 -- bash -c 'dir=/dest/$(basename "$1" | ...); ...; mv "$1" "$dir"' _. (Mind the final _!) Jan 10, 2017 at 20:45

You must log in to answer this question.

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