1

I exported all my pictures from Mac to my PC (Windows 11, but also running Ubuntu through PowerShell). The export created every directory (1000s of folders) in formats like the following and stripped out the original creation date:

April 1, 2004         
Amsterdam, September 18, 2018    
Anderson Arbor, March 24, 2011   

I want to be able to sort by date. Any thoughts on how to change these formats to something like:

2004, April 1   
2018, Amsterdam, September 18    
2011, Anderson Arbor, March 24

or even better would be:

2004-04-01    
2018-09-18 Amsterdam     
2011-03-24 Anderson Arbor
Kusalananda
  • 333,661
David
  • 11

3 Answers3

1

With zsh:

$ autoload -Uz zmv
$ zmodload zsh/datetime
$ zmv -n '(|(*), )([^ ]## <1-31>, <->)' \
         '$(strftime %F $(strftime -r "%B %d, %Y" $3))${2:+ $2}'
mv -- 'Amsterdam, September 18, 2018' '2018-09-18 Amsterdam'
mv -- 'Anderson Arbor, March 24, 2011' '2011-03-24 Anderson Arbor'
mv -- 'April 1, 2004' 2004-04-01

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

zmv is an autoloadable zsh function for batch renaming.

It takes two arguments:

  1. A glob pattern (which must be quoted as its expansion will be done by zmv itself).
  2. A string that will be evaluated to make up the replacement for each matching file.

In the replacement, what was matched by each (...) pair is available in $1, $2...

Here, the glob pattern is (|(*), )([^ ]## <1-31>, <->), where:

  • (x|y) matches either x or y, so (|(*), ) matches either nothing or anything (* which will end up in $2 in the replacement since it's in the second pair of (...)) ending in ", ".
  • [^ ] matches any single character other than space, x## matches one or more xs, so [^ ]## matches any sequence of one or more non-space characters.
  • <->, which is <x-y> without boundary matches any number, any sequence of decimal digits.

In the replacement, we use the strftime builtin from the zsh/datetime module to parse (with -r) and reformat the date that was captured in $3. That builtin provides a simple interface to the standard strftime() and strptime() functions to format and parse time to/from strings respectively.

%F is short for %Y-%m-%d to format dates as YYYY-MM-DD, %B for full month name, %d for day of the month, %Y for the year.


Instead of using strftime in a command substitution which means forking a process each time to be able to get its output, we could just get the list of month names from langinfo and do the translation to month number and formatting by hand:

zmodload zsh/langinfo
autoload -Uz zmv
months=( ${(v)langinfo[(I)MON_<1-12>]} )
#       1 2     3                 4         5
zmv -n "(|(*), )(${(j[|])months}) (<1-31>), (<->)" \
       '$5-${(l[2][0])months[(I)$3]}-${(l[2][0])4}${2:+ $2}'

Where:

  • ${(v)langinfo[(I)MON_<1-12>]} expands to the values of the $langinfo associative array whose keys match MON_<1-12>.
  • ${(j[|])months} joins the elements of the array with |.
  • $array[(I)$3] returns the largest index of array elements that match the pattern stored in $3.
  • ${(l[2][0])string} left-pads the string to a length of 2 with 0s.
0

Using Perl's rename:

Time::Piece is in Perl's core and is considered a cleaner way than the 'magic' and not 100% reliable Date::Parse module.

rename -n '
    BEGIN{use Time::Piece}
    my $d = localtime;
    my $months_re = join "|", map { "$_\\w+" } $d->mon_list;
    s/
        ($months_re)
        \s+(\d+),\s+(\d{4})
    /
        my $d = Time::Piece->strptime("$1 $2, $3", "%B %d, %Y");
        $d->strftime("%F")
    /xe;
    s/(.*),\s+(\d{4}-\d{2}-\d{2})/$2 $1/;
' ./*/

Remove -n (aka dry-run) when the output looks satisfactory.

Output

rename(Amsterdam, September 18, 2018/, 2018-09-18 Amsterdam/)
rename(Anderson Arbor, March 24, 2011/, 2011-03-24 Anderson Arbor/)
rename(April 1, 2004/, 2004-04-01/)
0

Using find and the perl rename utility (AKA prename, file-rename, perl-rename depending on what distro you are using), plus the Date::Parse and Date::Format library modules.

These modules are not included with perl, they need to be installed separately, either via the cpan command-line tool or a distro package. They are both in the TimeDate bundle - on Debian they can be installed with sudo apt-get install libtimedate-perl. I don't know what the package is named on other distros, but it's probably very similar.

BTW, the perl rename utility is not to be confused with the rename utility from util-linux which has completely different and incompatible capabilities and command-line options.

Anyway:

$ mkdir 'April 1, 2004' 'Amsterdam, September 18, 2018' 'Anderson Arbor, March 24, 2011'

$ find . -type d -print0 | rename -n -0 'BEGIN { use Date::Parse; use Date::Format; our $date_pattern = qr/[[:alpha:]]+ \d+, \d\d\d\d/; }; our $date_pattern;

            if (/^(.*\/)(.*)\b($date_pattern)$/) {
              my ($p1, $p2, $d) = ($1, $2, $3);
              my $t = str2time($d);

              s/\b$date_pattern//;
              $_ = sprintf(&quot;%s%s%s&quot;, $p1, time2str(&quot;%Y-%m-%d &quot;,$t), $p2);
              s/[, ]*$//;
            }'

rename(./April 1, 2004, ./2004-04-01) rename(./Amsterdam, September 18, 2018, ./2018-09-18 Amsterdam) rename(./Anderson Arbor, March 24, 2011, ./2011-03-24 Anderson Arbor)

NOTE: This is a dry-run due to the -n option. Once you're sure it does what you want, remove the -n or replace it with -v for verbose output.

After loading the Date::Parse and Date::Format modules and defining a variable ($date_pattern) in the BEGIN {} block, this rename script iterates over each directory name and attempts to match the "Month Day, Year" pattern.

If successful then all but the final element of the pathname is captured/extracted into variable $p1, any "prefix" portion of the final directory element before the date pattern into variable $p2 (this is optional and, if missing, will be an empty string), and the date pattern into variable $d.

The matched date $d is then converted to a time_t value (seconds since the epoch, Midnight Jan 1 1970) and assigned to variable $t using the str2time() function.

Then:

  1. The date pattern is stripped from the directory name

  2. The sprintf and time2str() function are used to modify the entire filename to conform to the new pattern by changing $_.

    This simple assignment to $_ highlights a very simple but significant fact about how perl rename works.

    In perl, the variable $_ is used as the default variable in loops and as the default argument/operand of many functions & operators if no other variable name is specified (See man perlvar and search for $_), including the s/// operator.

    With perl rename, $_ is used for the current path/filename and anything that changes $_ will result in it being renamed. If $_ is changed, it will be renamed. If it isn't changed, it won't be.

    Note that rename won't rename a file or directory over an existing filename unless you force it to with the -f option.

  3. All trailing spaces and commas (if any) are then stripped from the final directory name.


Other Notes:

  1. The $date_pattern variable is defined in the BEGIN{} block so that it is only executed once when the script starts (rather than once for every directory name on stdin). It's defined as a variable because it's used twice - if it ever needs to be changed, it's better to only need to change it in one place. It is defined as "one-or-more alphabetic characters, a space, one-or-more digits, another space, and four digits". This matches your example directory names, but may need to be adjusted if other variants are possible.

    It is declared with our so that it is valid for the entire scope of the rename script (otherwise it will only be available inside the BEGIN block). perl rename scripts run in perl's strict mode (see perldoc strict for details).

    qr is a perl quoting operator that quotes (and potentially pre-compiles, for performance) regular expressions - performance probably isn't a problem here, I've used it here mainly to work around the fact that it's a pain to use single-quotes inside a single-quoted one-liner script. See perldoc -f qr for details.

  2. by using find ... -print0 and rename's -0 option, NUL is used as the filename separator, so this will work with any directory name, even those containing newlines and other whitespace or shell meta-characters such as ;, &, > etc. NUL is the only character which can't be in a pathname.

cas
  • 78,579