date2name/date2name
2015-01-04 13:31:07 -06:00

330 lines
13 KiB
Python
Executable file

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Time-stamp: <2012-04-05 15:07:43 vk>
"""
date2name
~~~~~~~~~
This script adds (or removes) datestamps to (or from) file(s)
:copyright: (c) 2009-2012 by Karl Voit <tools@Karl-Voit.at>
:license: GPL v2 or any later version
:bugreports: <tools@Karl-Voit.at>
"""
## changes:
## * v0.0.1 -> 0.1: script is able to handle files and folders in (sub)directories
import re
import os
import time
import logging
import sys
from optparse import OptionParser
# global variables
PROG_VERSION = "0.1"
FORMATSTRING_COMPACT = "%Y%m%d"
FORMATSTRING_STANDARD = "%Y-%m-%d"
FORMATSTRING_MONTH = "%Y-%m"
FORMATSTRING_WITHTIME = "%Y-%m-%dT%H.%M.%S"
NODATESTAMP_PATTERN = re.compile('^\D')
COMPACT_PATTERN = re.compile('^\d{4,4}[01]\d[0123]\d[- _]')
STANDARD_PATTERN = re.compile('^\d{4,4}-[01]\d-[0123]\d[- _]')
MONTH_PATTERN = re.compile('^\d{4,4}-[01]\d[- _]')
WITHTIME_PATTERN = re.compile('^\d{4,4}-[01]\d-[0123]\d[T :_-][012]\d:[012345]\d:[012345]\d[- _]')
# cmdline parsing
USAGE = "\n\
%prog [options] file ...\n\
\n\
Per default, %prog gets the modification time of matching files\n\
and directories and adds a datestamp in standard ISO 8601+ format\n\
YYYY-MM-DD (http://datestamps.org/index.shtml) at the beginning of\n\
the file- or directoryname.\n\
If an existing timestamp is found, its style will be converted to the\n\
selected ISO datestamp format but the numbers stays the same.\n\
Executed with an examplefilename \"file\" this results e.g. in\n\
\"2008-12-31_file\".\n\
Note: Other that defined in ISO 8601+ the delimiter between hours,\n\
minutes, and seconds is not a colon but a dot. Colons are causing\n\
several problems on different file systems and are there fore replaced\n\
with the (older) DIN 5008 version with dots.\n\
\n\
Run %prog --help for usage hints"
# pylint: disable-msg=C0103
parser = OptionParser(usage=USAGE)
parser.add_option("-d", "--directories", dest="onlydirectories",
action="store_true",
help="modify only directory names")
parser.add_option("-f", "--files", dest="onlyfiles",
action="store_true",
help="modify only file names")
parser.add_option("-C", "--compact", dest="compact",
action="store_true",
help="use compact datestamp (YYYYMMDD)")
parser.add_option("-M", "--month", dest="month",
action="store_true",
help="use datestamp with year and month (YYYY-MM)")
parser.add_option("-w", "--withtime", dest="withtime",
action="store_true",
help="use datestamp including seconds (YYYY-MM-DDThh.mm.ss)")
# parser.add_option("-r", "--remove", dest="remove",
# action="store_true",
# help="remove all known datestamps")
parser.add_option("-m", "--mtime", dest="mtime",
action="store_true",
help="take modification time for datestamp [default]")
parser.add_option("-c", "--ctime", dest="ctime",
action="store_true",
help="take creation time for datestamp")
parser.add_option("-q", "--quiet", dest="quiet", action="store_true",
help="do not output anything but just errors on console")
parser.add_option("-v", "--verbose", dest="verbose", action="store_true",
help="enable verbose mode")
parser.add_option("--nocorrections", dest="nocorrections", action="store_true",
help="do not convert existing datestamps to new format")
parser.add_option("-s", "--dryrun", dest="dryrun", action="store_true",
help="enable dryrun mode: just simulate what would happen, do not modify files or directories")
parser.add_option("--version", dest="version", action="store_true",
help="display version and exit")
(options, args) = parser.parse_args()
def handle_logging():
"""Log handling and configuration"""
if options.verbose:
FORMAT = "%(levelname)-8s %(asctime)-15s %(message)s"
logging.basicConfig(level=logging.DEBUG, format=FORMAT)
elif options.quiet:
FORMAT = "%(levelname)-8s %(message)s"
logging.basicConfig(level=logging.CRITICAL, format=FORMAT)
else:
FORMAT = "%(message)s"
logging.basicConfig(level=logging.INFO, format=FORMAT)
def get_converted_basename(matched_pattern, item):
"""returns a new filename based on found timestamp information and currently selected datestamp format"""
if matched_pattern == "compact":
logging.debug("item \"%s\" matches compact pattern, doing conversion" % item)
item_year = item[0:4]
item_month = item[4:6]
item_day = item[6:8]
name_without_datestamp = item[8:]
elif matched_pattern == "standard":
logging.debug("item \"%s\" matches standard pattern, doing conversion" % item)
item_year = item[0:4]
item_month = item[5:7]
item_day = item[8:10]
name_without_datestamp = item[10:]
elif matched_pattern == "month":
logging.debug("item \"%s\" matches month pattern, doing conversion" % item)
item_year = item[0:4]
item_month = item[5:7]
logging.info("%s no datestamp information for day, so I will take \"00\" for conversion" % item)
item_day = "00"
name_without_datestamp = item[7:]
elif matched_pattern == "withtime":
logging.debug("item \"%s\" matches withtime pattern, doing conversion" % item)
item_year = item[0:4]
item_month = item[5:7]
item_day = item[8:10]
logging.warn("%s ... time will be lost due to conversion" % item)
name_without_datestamp = item[19:]
else:
logging.error("unknown matched pattern (internal error)")
logging.debug("item \"%s\" got year \"%s\" month \"%s\" day \"%s\"" % (item, item_year, item_month, item_day))
if options.compact:
return item_year + item_month + item_day + name_without_datestamp
elif options.month:
return item_year + "-" + item_month + name_without_datestamp
elif options.withtime:
## FIXXME: probably implement some kind of conversion to withtime-format
logging.warn("%s: Sorry! Conversion to withtime-format not implemented yet, taking standard format" % item)
return item_year + "-" + item_month + "-" + item_day + name_without_datestamp
else:
return item_year + "-" + item_month + "-" + item_day + name_without_datestamp
def get_timestamp_from_file(formatstring, item):
"""read out ctime or mtime of file and return new itemname"""
if options.ctime:
return time.strftime(formatstring, time.localtime(os.path.getctime(item))) + "_" + item
elif options.mtime:
return time.strftime(formatstring, time.localtime(os.path.getmtime(item))) + "_" + item
else:
logging.error("internal error: not ctime nor mtime chosen! You can correct this by giving a parameter")
def generate_new_basename(formatstring, basename):
"""generates the new itemname; considering options.nocorrections"""
if options.nocorrections or NODATESTAMP_PATTERN.match(basename):
logging.debug("basename \"" + basename + "\" matches nodatestamp-pattern or option nocorrections " + \
"is set: skipping further pattern matching")
new_basename = get_timestamp_from_file(formatstring, basename)
elif WITHTIME_PATTERN.match(basename):
logging.debug("basename \"%s\" matches withtime-pattern" % basename)
if options.withtime:
logging.debug("old pattern is the same as the recognised, basename stays the same")
return basename
else:
new_basename = get_converted_basename("withtime", basename)
elif STANDARD_PATTERN.match(basename):
logging.debug("basename \"%s\" matches standard-pattern" % basename)
if not options.withtime and not options.compact and not options.month:
logging.debug("old pattern is the same as the recognised, basename stays the same")
return basename
else:
new_basename = get_converted_basename("standard", basename)
elif COMPACT_PATTERN.match(basename):
logging.debug("basename \"%s\" matches compact-pattern" % basename)
if options.compact:
logging.debug("old pattern is the same as the recognised, basename stays the same")
return basename
else:
new_basename = get_converted_basename("compact", basename)
elif MONTH_PATTERN.match(basename):
logging.debug("basename \"%s\" matches month-pattern" % basename)
if options.month:
logging.debug("old pattern is the same as the recognised, basename stays the same")
return basename
else:
new_basename = get_converted_basename("month", basename)
else:
logging.debug("basename \"%s\" does not match any known datestamp-pattern" % basename)
new_basename = get_timestamp_from_file(formatstring, basename)
logging.debug("new basename is \"%s\"" % new_basename)
return new_basename
def get_full_name(path, filename):
return path + "/" + filename
def handle_item(path, basename, formatstring):
"""Handle timestamp adding or removing with directories or files"""
options.remove = False
if options.remove:
logging.debug("removing timestamp from base \"%s\"" % basename)
## FIXXME: implement datestamp removing
logging.error("Sorry! Removing of datestamps is not yet implemented")
else:
logging.debug("#######################+++~~- adding timestamp to base \"%s\"" % basename)
if options.onlyfiles and os.path.isdir(basename):
logging.debug("skipping directory \"%s\" because of command line option \"-f\"" % basename)
return
if options.onlydirectories and os.path.isfile(basename):
logging.debug("skipping file \"%s\" because of command line option \"-d\"" % basename)
return
new_basename = generate_new_basename(formatstring, basename)
logging.debug("new itemname for \"%s\" will be \"%s\"" % (basename, new_basename))
if options.dryrun:
if basename == new_basename:
logging.info("%s ... no modification" % basename)
else:
logging.info("%-40s > %s" % (get_full_name(path, basename), new_basename))
else:
if basename == new_basename:
logging.info("\"%s\" ... no modification" % get_full_name(path, basename))
else:
logging.debug("\"%s\" > \"%s\"" % (get_full_name(path, basename), new_basename))
logging.info("%-40s > %s" % (get_full_name(path, basename), new_basename))
os.rename(basename, new_basename)
def main():
"""Main function [make pylint happy :)]"""
if options.version:
print os.path.basename(sys.argv[0]) + " " + PROG_VERSION
sys.exit(0)
if len(args) < 1:
parser.error("invalid usage")
if (options.verbose and options.quiet):
parser.error("please use either verbose (--verbose) or quiet (-q) option")
if (options.onlyfiles and options.onlydirectories):
parser.error("please use either option files (-f) or option directories (-f) or none of them (for renaming directories and files)")
if (options.ctime and options.mtime):
parser.error("please use either ctime (-c) or mtime (-m) option")
if (not options.ctime and not options.mtime):
options.mtime = True
if (options.compact and options.withtime) \
or (options.compact and options.month) \
or (options.month and options.withtime):
parser.error("please use either the default, short, month, or withtime format")
# log handling
handle_logging()
filelist = args[0:]
logging.debug("filelist: [%s]" % filelist)
if options.compact:
formatstring = FORMATSTRING_COMPACT
elif options.month:
formatstring = FORMATSTRING_MONTH
elif options.withtime:
formatstring = FORMATSTRING_WITHTIME
else:
logging.debug("no option given for format string; taking standard format")
formatstring = FORMATSTRING_STANDARD
original_path = os.getcwd()
for item in filelist:
if os.path.isdir(item) or os.path.isfile(item):
logging.debug("handling item: " + item + " <-----------------")
path = os.path.dirname(item)
logging.debug("has directory: " + path)
basename = os.path.basename(item)
logging.debug("has basename: " + basename)
if path:
os.chdir(path)
logging.debug("changed cwd to: " + os.getcwd())
handle_item(os.path.dirname(item), os.path.basename(item), formatstring)
os.chdir(original_path)
else:
logging.critical("%s: is no file or directory (broken link?)" % item)
if __name__ == "__main__":
try:
main()
except KeyboardInterrupt:
logging.info("Received KeyboardInterrupt")
## END OF FILE #################################################################
# vim:foldmethod=indent expandtab ai ft=python tw=120 fileencoding=utf-8 shiftwidth=4