forked from Github/date2name
330 lines
13 KiB
Python
Executable file
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
|