0

I have a folder with a number of .png files, for example:

Jeff Smith 1.png
Jeff Donald 1.png
Jeff Donald 2.png
Jeff Smith 2.png
Jeff Roberts.png
Kyle Reds.png
Kyle Reds 1.png
Kyle Blues 1.png
Kyle Blues 2.png
Kyle Person.png
etc 
etc

How would I write a bash script that could create a folder for every unique name.

For the example above, we would get folders:

Jeff Smith
Jeff Donald
Kyle Reds
Kyle Blues
Kyle Person 
etc

I am brand new to bash (and coding in general) - hoping to get some help on this

Thanks

  • 1
    You'd be better off without any spaces in file or directory names - with spaces they just become harder to deal with. Consider replacing every space with an underscore or similar. – Ed Morton Aug 23 '20 at 01:20
  • 1
    How should `Carl von Linne.png` be treated? – Kusalananda Aug 23 '20 at 08:18
  • you might loop around your list `ls *.png > listefile` & substitude spaces by underscores `_` & ignore `.png` with a while loop to build your directories `while read ; do mkdir $(echo $REPLY | sed 's/ /_/g;s/.png//') ; done < listefile` for example – francois P Aug 23 '20 at 21:48

1 Answers1

1

With zsh:

#! /bin/zsh -
() { mkdir -p -- ${(u)argv%%( <->|).png}; } *.png

Where:

  • () { code; } arguments is an anonymous function that takes the list of non-hidden .png files in the current directory as arguments (accessible as $argv or $@ within the code).
  • <-> is the zsh glob operator that matches any sequence of decimal digits (<first-last> with neither first nor last specified).
  • ${(u)array-expansion} expands to the unique elements (removes duplicates) of the array expansion.
  • ${array%%pattern} expands to the elements of the array stripped of the longest string at the end that matches the pattern.

So, here, we're making a directory for each unique string made of the png files stripped of an optional " <digits>" followed by .png.

With bash and GNU tools, you could do something similar with:

#! /bin/bash -
export LC_ALL=C # needed for sed to deal with arbitrary byte values, for
                # [0-9] to match on 0123456789 only and for sort -u to report
                # unique records as opposed to one of several that sort the
                # same.

shopt -s failglob # to properly handle the cases where there's no png
set -o pipefail   # ditto, to report a failure exit status in that case.

printf '%s\0' *.png |
  sed -Ez 's/( [0-9]+)?\.png$//' |
  sort -zu |
  xargs -r0 mkdir -p --

Or avoiding GNUisms and with bash 4+:

#! /bin/bash -
shopt -s failglob extglob
typeset -A unique
files=(*.png) || exit
for file in "${files[@]}"; do
  file=${file%.png}
  file=${file% +([0123456789])}
  unique[$file]=
done
mkdir -p -- "${!unique[@]}"

Note that if there a file called " 12.png" for instance in the current directory, that would result in an empty directory name. In that last bash solution above, that would cause a syntax errors as bash associative arrays don't support elements with an empty key, and in all others, you'd get an error by mkdir which would not be able to create a directory with an empty name.

Note that macos doesn't come with GNU tools by default, and it comes with a very older version of bash. It has always been coming with zsh though. If you have to use bash there and can't install newer versions nor GNU tools, you could resort to perl instead:

#! /bin/bash -
perl -e '
  for (<*.png>) {
    s/\.png\z//;
    s/ \d+\z//;
    $unique{$_} = undef;
  }
  $ret = 0;
  for (keys %unique) {
    unless (mkdir($_) || ($!{EEXIST} && -d $_)) {
      warn "Cannot create $_: $!\n";
      $ret = 1;
    }
  }
  exit $ret;'

(though there's nothing bash-specific in that code which should work with the sh of any system, provided perl is installed).

Stéphane Chazelas
  • 522,931
  • 91
  • 1,010
  • 1,501