Use list.sort's keyword parameters
[joel/kofoto.git] / src / packages / kofoto / commandline / main.py
1 # Disable check for exception type except clause (see cmdInspectPath):
2 # pylint: disable-msg=W0702
3
4 """Main Kofoto commandline client code."""
5
6 __all__ = ["main"]
7
8 import codecs
9 import getopt
10 import os
11 import sys
12 import time
13
14 from kofoto.clientenvironment import ClientEnvironment, ClientEnvironmentError
15 from kofoto.clientutils import \
16     DIRECTORIES_TO_IGNORE, \
17     expanduser, \
18     get_file_encoding, \
19     walk_files
20 from kofoto.albumtype import AlbumType
21 from kofoto.config import DEFAULT_CONFIGFILE_LOCATION
22 from kofoto.imageversiontype import ImageVersionType
23 from kofoto.search import \
24     BadTokenError, ParseError, Parser, UnterminatedStringError
25 from kofoto.shelf import \
26     computeImageHash, \
27     makeValidTag
28 from kofoto.shelfexceptions import \
29     AlbumDoesNotExistError, \
30     AlbumExistsError, \
31     BadAlbumTagError, \
32     BadCategoryTagError, \
33     CategoriesAlreadyConnectedError, \
34     CategoryDoesNotExistError, \
35     CategoryExistsError, \
36     CategoryLoopError, \
37     CategoryPresentError, \
38     ExifImportError, \
39     ImageDoesNotExistError, \
40     ImageVersionDoesNotExistError, \
41     ImageVersionExistsError, \
42     MultipleImageVersionsAtOneLocationError, \
43     NotAnImageFileError, \
44     ObjectDoesNotExistError, \
45     ShelfLockedError, \
46     ShelfNotFoundError, \
47     UndeletableAlbumError, \
48     UnknownAlbumTypeError, \
49     UnknownImageVersionTypeError, \
50     UnsettableChildrenError, \
51     UnsupportedShelfError
52
53 ######################################################################
54 ### Constants.
55
56 PRINT_ALBUMS_INDENT = 4
57
58 ######################################################################
59 ### Exceptions.
60
61 class ArgumentError(Exception):
62     """Bad arguments to command."""
63     pass
64
65 ######################################################################
66 ### Help text data.
67
68 optionDefinitionList = [
69     ("    --configfile FILE",
70      "Use configuration file FILE instead of the default (%s)." % (
71          DEFAULT_CONFIGFILE_LOCATION)),
72     ("    --database FILE",
73      "Use the metadata database FILE instead of the default (specified in the"
74      " configuration file)."),
75     ("    --gencharenc ENCODING",
76      "Generate HTML pages with character encoding ENCODING instead of the"
77      " default (utf-8)."),
78     ("-h, --help",
79      "Display this help."),
80     ("    --identify-by-hash",
81      "Identify image versions by hash. This is the default."),
82     ("    --identify-by-path",
83      "Identify image versions by path."),
84     ("    --ids",
85      "Print ID numbers instead of locations."),
86     ("    --include-all",
87      "Include all image versions for images matching a search expression."),
88     ("    --include-important",
89      "Include all image versions with type \"important\" for images matching a"
90      " search expression."),
91     ("    --include-original",
92      "Include all image versions with type \"original\" for images matching a"
93      " search expression."),
94     ("    --include-other",
95      "Include all image versions with type \"other\" for images matching a"
96      " search expression."),
97     ("    --include-primary",
98      "Include all primary image versions for images matching a search"
99      " expression."),
100     ("    --no-act",
101      "Do everything which is supposed to be done, but don't commit any changes"
102      " to the database."),
103     ("-0, --null",
104      "Use null characters instead of newlines when printing image version"
105      " locations. This is mainly useful in combination with \"xargs"
106      " --null\"."),
107     ("    --position POSITION",
108      "Add/register to position POSITION. Default: last."),
109     ("-t, --type TYPE",
110      "Use album type TYPE when creating an album or output type TYPE when"
111      " generating output."),
112     ("-v, --verbose",
113       "Be verbose (and slower)."),
114     ("    --version",
115      "Print version to standard output."),
116     ]
117
118 albumAndImageCommandsDefinitionList = [
119     ("add-category CATEGORY OBJECT [OBJECT ...]",
120      "Add a category to the given objects."),
121     ("delete-attribute ATTRIBUTE OBJECT [OBJECT ...]",
122      "Delete an attribute from the given objects."),
123     ("get-attribute ATTRIBUTE OBJECT",
124      "Get an attribute's value for an object."),
125     ("get-attributes OBJECT",
126      "Get attributes for an object."),
127     ("get-categories OBJECT",
128      "Get categories for an object."),
129     ("remove-category CATEGORY OBJECT [OBJECT ...]",
130      "Remove a category from the given objects."),
131     ("search SEARCHEXPRESSION",
132      "Search for images matching an expression and print image version"
133      " locations on standard output (or image IDs if the --ids option is"
134      " given) separated by newlines (or by null characters if --null is"
135      " given). By default, only primary image versions are printed, but other"
136      " versions can be printed by supplying one or several of the options"
137      " --include-all, --include-important, --include-original and"
138      " --include-other. (If no --include-* option is supplied,"
139      " --include-primary is assumed.)"),
140     ("set-attribute ATTRIBUTE VALUE OBJECT [OBJECT ...]",
141      "Set ATTRIBUTE to VALUE for the given objects."),
142     ]
143
144 albumCommandsDefinitionList = [
145     ("add ALBUM OBJECT [OBJECT ...]",
146      "Add the given objects (albums and images) to the album ALBUM. (The"
147      " objects are placed last if a position is not specified with"
148      " --position.)"),
149     ("create-album TAG",
150      "Create an empty, unlinked album with tag TAG. If a type argument is not"
151      " given with -t/--type, an album of type \"plain\" will be created."),
152     ("destroy-album ALBUM [ALBUM ...]",
153      "Destroy the given albums permanently. All metadata is also destroyed,"
154      " but not the album's children."),
155     ("generate ROOTALBUM DIRECTORY [SUBALBUM ...]",
156      "Generate output for ROOTALBUM in the directory DIRECTORY. If subalbums"
157      " are given, only generate those albums, their descendants and their"
158      " immediate parents. Use -t/--type to use another output type than the"
159      " default."),
160     ("print-albums [ALBUM]",
161      "Print the album graph for ALBUM (default: root). If -v/--verbose is"
162      " given, also print images and attributes."),
163     ("register ALBUM PATH [PATH ...]",
164      "Register objects (i.e. directories and images) and add them to the album"
165      " ALBUM. (The objects are placed last.) Directories are recursively"
166      " scanned for other directories and images, which also will be"
167      " registered."),
168     ("remove ALBUM POSITION [POSITION ...]",
169      "Remove the objects at the given positions from ALBUM."),
170     ("rename-album OLDTAG NEWTAG",
171      "Rename album tag."),
172     ("sort-album ALBUM [ATTRIBUTE]",
173      "Sort the contents of ALBUM by an attribute (default: captured)."),
174     ]
175
176 imageCommandsDefinitionList = [
177     ("destroy-image IMAGE [IMAGE ...]",
178      "Destroy the given images permanently. All metadata and image versions"
179      " are also destroyed (but not the image files on disk)."),
180     ("get-imageversions IMAGE",
181      "Print image versions for an image. If -v/--verbose is given, print more"
182      " information."),
183     ]
184
185 imageversionCommandsDefinitionList = [
186     ("destroy-imageversion IMAGEVERSION [IMAGEVERSION ...]",
187      "Destroy the given image versions permanently. All metadata is also"
188      " destroyed (but not the image files on disk)."),
189     ("find-missing-imageversions",
190      "Find missing image versions and print them to standard output."),
191     ("find-modified-imageversions",
192      "Find modified image versions and print them to standard output."),
193     ("inspect-path PATH [PATH ...]",
194      "Traverse the given paths and print whether each found file is a"
195      " registered, modified, moved or unregistered image version or a"
196      " non-image."),
197     ("make-primary IMAGEVERSION [IMAGEVERSION ...]",
198      "Make an image version the primary version."),
199     ("reread-exif IMAGEVERSION [IMAGEVERSION ...]",
200      "Reread EXIF information for the given image versions."),
201     ("set-imageversion-comment VALUE IMAGEVERSION [IMAGEVERSION ...]",
202      "Set comment of the given image versions."),
203     ("set-imageversion-image IMAGE IMAGEVERSION [IMAGEVERSION ...]",
204      "Set the image to which the given image versions belong."),
205     ("set-imageversion-type IMAGEVERSIONTYPE IMAGEVERSION [IMAGEVERSION ...]",
206      "Set type of the given image versions."),
207     ("update-contents PATH [PATH ...]",
208      "Traverse the given paths recursively and remember the new contents"
209      " (checksum, width and height) of found image versions."),
210     ("update-locations PATH [PATH ...]",
211      "Traverse the given paths recursively and remember the new locations of"
212      " found image versions."),
213     ]
214
215 categoryCommandsDefinitionList = [
216     ("connect-category PARENTCATEGORY CHILDCATEGORY",
217      "Make a category a child of another category."),
218     ("disconnect-category PARENTCATEGORY CHILDCATEGORY",
219      "Remove parent-child realationship bewteen two categories."),
220     ("create-category TAG DESCRIPTION",
221      "Create category."),
222     ("destroy-category CATEGORY [CATEGORY ...]",
223      "Destroy category permanently."),
224     ("print-categories",
225      "Print category tree."),
226     ("rename-category OLDTAG NEWTAG",
227      "Rename category tag."),
228     ("set-category-description TAG DESCRIPTION",
229      "Set category description."),
230     ]
231
232 miscellaneousCommandsDefinitionList = [
233     ("clean-cache",
234      "Clean up the image cache (remove left-over generated images)."),
235     ("print-statistics",
236      "Print some statistics about the database."),
237     ]
238
239 parameterSemanticsDefinitionList = [
240     ("ALBUM",
241      "An integer ID or an album tag (see TAG)."),
242     ("ATTRIBUTE",
243      "An arbitrary attribute name."),
244     ("CATEGORY",
245      "A category tag (see TAG)."),
246     ("DESCRIPTION",
247      "An arbitrary string."),
248     ("ENCODING",
249      "An encoding parsable by Python, e.g. utf-8, latin1 or iso-8859-1."),
250     ("FILE",
251      "A path to a file."),
252     ("IMAGE",
253      "An integer ID or a path to an image version file. If it's a path to an"
254      " image version, its corresponding image is selected. If"
255      " --identify-by-path is given, the path is used for identifying the image"
256      " version; otherwise the file's content is used for identification."),
257     ("IMAGEVERSION",
258      "An integer ID or a path to an image version file. If --identify-by-path"
259      " is given, the path is used for identifying the image version; otherwise"
260      " the file's contents used for identification."),
261     ("IMAGEVERSIONTYPE",
262      "\"important\", \"original\" or \"other\"."),
263     ("OBJECT",
264      "An integer ID, an album tag (see TAG) or a path to an image version"
265      " file. If it's a path to an image version, its corresponding image is"
266      " selected. If --identify-by-path is given, the path is used for"
267      " identifying the image version; otherwise the file's contents used for"
268      " identification."),
269     ("PATH",
270      "A path to a directory or a file."),
271     ("POSITION",
272      "An integer specifying an index into an album's children. 0 is the first"
273      " position, 1 is the second, and so on."),
274     ("SEARCHEXPRESSION",
275      "A search expression."
276      " See http://kofoto.rosdahl.net/trac/wiki/SearchExpressions for more"
277      " information."),
278     ("TAG",
279      "A text string not containing space or @ characters and not consisting"
280      " solely of integers."),
281     ("TYPE",
282      "An album type (as listed below) or an HTML output type (for the moment,"
283      " only \"woolly\" is allowed)."),
284     ("VALUE",
285      "An arbitrary string."),
286     ]
287
288 albumTypesDefinitionList = [
289     ("orphans",
290      "All albums and images that don't exist in any plain album."),
291     ("plain",
292      "An ordinary container that holds albums and images."),
293     ("search",
294      "An album containing the albums and images that match a search string"
295      " (sorted by capture timestamp). The search string is read from the"
296      " album's \"query\" attribute."),
297     ]
298
299 ######################################################################
300 ### Helper functions.
301
302 def printDefinitionList(deflist, outfile, tindent, dindent, width):
303     """Print a properly indented and wrapped definition list.
304
305     A definition list is a list of two-tuples where the first element
306     of the tuple is a string containing some term and the second
307     element of the tuple is the textual description of the term.
308
309     Arguments:
310
311     deflist -- The definition list.
312     outfile -- The file to which the formatted definition list should
313                be written.
314     tindent -- An integer specifying the left margin of the terms.
315     dindent -- An integer specifying the left margin of the descriptions.
316     width   -- An integer specifying the right margin of the descriptions.
317     """
318
319     from textwrap import TextWrapper
320     wrapper = TextWrapper(width=width)
321     for term, definition in deflist:
322         wrapper.subsequent_indent = (tindent + dindent) * " "
323         if len(term) < dindent - 1:
324             wrapper.initial_indent = tindent * " "
325             textToWrap = "%s%s%s" % (
326                 term,
327                 (dindent - len(term)) * " ",
328                 definition)
329         else:
330             wrapper.initial_indent = wrapper.subsequent_indent
331             outfile.write("%s%s\n" % (tindent * " ", term))
332             textToWrap = definition
333         outfile.write("%s\n" % wrapper.fill(textToWrap))
334
335 def displayHelp():
336     """Print commandline usage help text to standard output."""
337     sys.stdout.write(
338         "Usage: kofoto [options] command [parameters]\n"
339         "\n"
340         "Options:\n"
341         "\n")
342     printDefinitionList(optionDefinitionList, sys.stdout, 4, 27, 79)
343     sys.stdout.write(
344         "\n"
345         "Commands:\n"
346         "\n"
347         "    For albums and images\n"
348         "    =====================\n")
349     printDefinitionList(
350         albumAndImageCommandsDefinitionList, sys.stdout, 4, 27, 79)
351     sys.stdout.write(
352         "\n"
353         "    For albums\n"
354         "    ==========\n")
355     printDefinitionList(albumCommandsDefinitionList, sys.stdout, 4, 27, 79)
356     sys.stdout.write(
357         "\n"
358         "    For images\n"
359         "    ==========\n")
360     printDefinitionList(imageCommandsDefinitionList, sys.stdout, 4, 27, 79)
361     sys.stdout.write(
362         "\n"
363         "    For image versions\n"
364         "    ==================\n")
365     printDefinitionList(
366         imageversionCommandsDefinitionList, sys.stdout, 4, 27, 79)
367     sys.stdout.write(
368         "\n"
369         "    For categories\n"
370         "    ==============\n")
371     printDefinitionList(categoryCommandsDefinitionList, sys.stdout, 4, 27, 79)
372     sys.stdout.write(
373         "\n"
374         "    Miscellaneous\n"
375         "    =============\n")
376     printDefinitionList(
377         miscellaneousCommandsDefinitionList, sys.stdout, 4, 27, 79)
378     sys.stdout.write(
379         "\n"
380         "Parameter semantics:\n"
381         "\n"
382         "    Parameter         Interpretation\n"
383         "    --------------------------------\n")
384     printDefinitionList(
385         parameterSemanticsDefinitionList, sys.stdout, 4, 18, 79)
386     sys.stdout.write(
387         "\n"
388         "Album types:\n"
389         "\n"
390         "    Type              Description\n"
391         "    -----------------------------\n")
392     printDefinitionList(
393         albumTypesDefinitionList, sys.stdout, 4, 18, 79)
394
395 def printOutput(infoString):
396     """Print an informational string to standard output."""
397     sys.stdout.write(infoString)
398     sys.stdout.flush()
399
400 def printNotice(noticeString):
401     """Print a notice to standard error."""
402     sys.stderr.write("Notice: " + noticeString)
403
404 def printError(errorString):
405     """Print an error to standard error."""
406     sys.stderr.write("Error: " + errorString)
407
408 def printErrorAndExit(errorString):
409     """Print an error and exit the program with and error code."""
410     printError(errorString)
411     sys.exit(1)
412
413 def sloppyGetAlbum(env, idOrTag):
414     """Get an album by ID number or tag string."""
415     try:
416         return env.shelf.getAlbum(int(idOrTag))
417     except ValueError:
418         return env.shelf.getAlbumByTag(idOrTag)
419
420 def sloppyGetImage(env, idOrLocation):
421     """Get an image by ID number or location."""
422     try:
423         return env.shelf.getImage(int(idOrLocation))
424     except ValueError:
425         try:
426             if env.identifyByPath:
427                 imageversion = env.shelf.getImageVersionByLocation(
428                     idOrLocation)
429             else:
430                 imageversion = env.shelf.getImageVersionByHash(
431                     computeImageHash(idOrLocation))
432             return imageversion.getImage()
433         except ImageVersionDoesNotExistError:
434             raise ImageDoesNotExistError(idOrLocation)
435
436 def sloppyGetImageVersion(env, idOrLocation):
437     """Get an image version by ID number or location."""
438     try:
439         return env.shelf.getImageVersion(int(idOrLocation))
440     except ValueError:
441         if env.identifyByPath:
442             return env.shelf.getImageVersionByLocation(idOrLocation)
443         else:
444             return env.shelf.getImageVersionByHash(
445                 computeImageHash(idOrLocation))
446
447 def sloppyGetObject(env, idOrTagOrLocation):
448     """Get an object version by ID number or location."""
449     try:
450         return sloppyGetAlbum(env, idOrTagOrLocation)
451     except AlbumDoesNotExistError:
452         try:
453             return sloppyGetImage(env, idOrTagOrLocation)
454         except (ImageDoesNotExistError, ImageVersionDoesNotExistError), x:
455             raise ObjectDoesNotExistError(x)
456
457 def parseAlbumType(atype):
458     """Parse an album type.
459
460     This function takes a string representing an album type and
461     returns an alternative from kofoto.shelf.AlbumType. If the album
462     type does not exist, UnknownAlbumTypeError is raised.
463     """
464     try:
465         return {
466             u"plain": AlbumType.Plain,
467             u"orphans": AlbumType.Orphans,
468             u"search": AlbumType.Search,
469             }[atype]
470     except KeyError:
471         raise UnknownAlbumTypeError(atype)
472
473 def parseImageVersionType(ivtype):
474     """Parse an image version type.
475
476     This function takes a string representing an image version type
477     and returns an alternative from kofoto.shelf.ImageVersionType. If
478     the image version type does not exist,
479     UnknownImageVersionTypeError is raised. """
480     try:
481         return {
482             u"important": ImageVersionType.Important,
483             u"original": ImageVersionType.Original,
484             u"other": ImageVersionType.Other,
485             }[ivtype]
486     except KeyError:
487         raise UnknownImageVersionTypeError(ivtype)
488
489 ######################################################################
490 ### Helper classes.
491
492 class CommandlineClientEnvironment(ClientEnvironment):
493     """A class encapsulating the environment useful to a command."""
494
495     def __init__(self):
496         ClientEnvironment.__init__(self)
497         # Defaults:
498         self.identifyByPath = False
499         self.includeAll = False
500         self.includeImportant = False
501         self.includeOriginal = False
502         self.includeOther = False
503         self.includePrimary = False
504         self.noAct = False
505         self.useNullCharacters = False
506         self.position = -1
507         self.printIDs = False
508         self.type = None
509         self.verbose = False
510
511
512     def _writeInfo(self, infoString):
513         sys.stdout.write(infoString)
514         sys.stdout.flush()
515
516 ######################################################################
517 ### Commands.
518
519 def cmdAdd(env, args):
520     """Handler for the add command."""
521     if len(args) < 2:
522         raise ArgumentError
523     destalbum = sloppyGetAlbum(env, args[0])
524     objects = [sloppyGetObject(env, x) for x in args[1:]]
525     addHelper(env, destalbum, objects)
526
527
528 def addHelper(env, destalbum, objects):
529     """Helper function for cmdAdd."""
530     oldchildren = list(destalbum.getChildren())
531     if env.position == -1:
532         pos = len(oldchildren)
533     else:
534         pos = env.position
535     destalbum.setChildren(oldchildren[:pos] + objects + oldchildren[pos:])
536
537
538 def cmdAddCategory(env, args):
539     """Handler for the add-category command."""
540     if len(args) < 2:
541         raise ArgumentError
542     category = env.shelf.getCategoryByTag(args[0])
543     for arg in args[1:]:
544         sloppyGetObject(env, arg).addCategory(category)
545
546
547 def cmdCleanCache(env, args):
548     """Handler for the clean-cache command."""
549     if env.noAct:
550         raise ArgumentError
551     if len(args) != 0:
552         raise ArgumentError
553     env.imageCache.cleanup()
554
555
556 def cmdConnectCategory(env, args):
557     """Handler for the connect-category command."""
558     if len(args) != 2:
559         raise ArgumentError
560     parent = env.shelf.getCategoryByTag(args[0])
561     child = env.shelf.getCategoryByTag(args[1])
562     parent.connectChild(child)
563
564
565 def cmdCreateAlbum(env, args):
566     """Handler for the create-album command."""
567     if len(args) != 1:
568         raise ArgumentError
569     if env.type:
570         atype = env.type
571     else:
572         atype = u"plain"
573     env.shelf.createAlbum(args[0], parseAlbumType(atype))
574
575
576 def cmdCreateCategory(env, args):
577     """Handler for the create-category command."""
578     if len(args) != 2:
579         raise ArgumentError
580     env.shelf.createCategory(args[0], args[1])
581
582
583 def cmdDeleteAttribute(env, args):
584     """Handler for the delete-attribute command."""
585     if len(args) < 2:
586         raise ArgumentError
587     attr = args[0]
588     for arg in args[1:]:
589         sloppyGetObject(env, arg).deleteAttribute(attr)
590
591
592 def cmdDestroyAlbum(env, args):
593     """Handler for the destroy-album command."""
594     if len(args) == 0:
595         raise ArgumentError
596     for arg in args:
597         env.shelf.deleteAlbum(sloppyGetAlbum(env, arg).getId())
598
599
600 def cmdDestroyCategory(env, args):
601     """Handler for the destroy-category command."""
602     if len(args) == 0:
603         raise ArgumentError
604     for arg in args:
605         env.shelf.deleteCategory(env.shelf.getCategoryByTag(arg).getId())
606
607
608 def cmdDestroyImage(env, args):
609     """Handler for the destroy-image command."""
610     if len(args) == 0:
611         raise ArgumentError
612     for arg in args:
613         env.shelf.deleteImage(sloppyGetImage(env, arg).getId())
614
615
616 def cmdDestroyImageVersion(env, args):
617     """Handler for the destroy-imageversion command."""
618     if len(args) == 0:
619         raise ArgumentError
620     for arg in args:
621         env.shelf.deleteImageVersion(sloppyGetImageVersion(env, arg).getId())
622
623
624 def cmdDisconnectCategory(env, args):
625     """Handler for the disconnect-category command."""
626     if len(args) != 2:
627         raise ArgumentError
628     parent = env.shelf.getCategoryByTag(args[0])
629     child = env.shelf.getCategoryByTag(args[1])
630     parent.disconnectChild(child)
631
632
633 def cmdFindMissingImageVersions(env, args):
634     """Handler for the find-missing-imageversions command."""
635     if len(args) != 0:
636         raise ArgumentError
637     paths = []
638     for iv in env.shelf.getAllImageVersions():
639         location = iv.getLocation()
640         if env.verbose:
641             env.out("Checking %s ...\n" % location)
642         if not os.path.exists(location):
643             paths.append(location)
644     for path in paths:
645         env.out("%s\n" % path)
646
647
648 def cmdFindModifiedImageVersions(env, args):
649     """Handler for the find-modified-imageversions command."""
650     if len(args) != 0:
651         raise ArgumentError
652     paths = []
653     for iv in env.shelf.getAllImageVersions():
654         location = iv.getLocation()
655         if env.verbose:
656             env.out("Checking %s ...\n" % location)
657         try:
658             realId = computeImageHash(location)
659             storedId = iv.getHash()
660             if realId != storedId:
661                 paths.append(location)
662         except IOError:
663             pass
664     for path in paths:
665         env.out("%s\n" % path)
666
667
668 def cmdGenerate(env, args):
669     """Handler for the generate command."""
670     if len(args) < 2:
671         raise ArgumentError
672     root = sloppyGetAlbum(env, args[0])
673     dest = args[1]
674     subalbums = [sloppyGetAlbum(env, x) for x in args[2:]]
675     if env.type:
676         otype = env.type
677     else:
678         otype = u"woolly"
679     import kofoto.generate
680     try:
681         generator = kofoto.generate.Generator(otype, env)
682         generator.generate(root, subalbums, dest, env.gencharenc)
683     except kofoto.generate.OutputTypeError, x:
684         env.errexit("No such output module: %s\n" % x)
685
686
687 def cmdGetAttribute(env, args):
688     """Handler for the get-attribute command."""
689     if len(args) != 2:
690         raise ArgumentError
691     obj = sloppyGetObject(env, args[1])
692     value = obj.getAttribute(args[0])
693     if value:
694         env.out(value + "\n")
695
696
697 def cmdGetAttributes(env, args):
698     """Handler for the get-attributes command."""
699     if len(args) != 1:
700         raise ArgumentError
701     obj = sloppyGetObject(env, args[0])
702     for name in obj.getAttributeNames():
703         env.out("%s: %s\n" % (name, obj.getAttribute(name)))
704
705
706 def cmdGetCategories(env, args):
707     """Handler for the get-categories command."""
708     if len(args) != 1:
709         raise ArgumentError
710     obj = sloppyGetObject(env, args[0])
711     for category in obj.getCategories():
712         env.out("%s (%s) <%s>\n" % (
713             category.getDescription(),
714             category.getTag(),
715             category.getId()))
716
717
718 def cmdGetImageVersions(env, args):
719     """Handler for the get-imageversions command."""
720     if len(args) != 1:
721         raise ArgumentError
722     image = sloppyGetImage(env, args[0])
723     for iv in image.getImageVersions():
724         if env.printIDs:
725             env.out("%s\n" % iv.getId())
726         elif env.verbose:
727             env.out("%s\n  %s%s\n" % (
728                 iv.getLocation(),
729                 iv.isPrimary() and "Primary, " or "",
730                 iv.getType()))
731             if iv.getComment():
732                 env.out("  %s\n" % iv.getComment())
733         else:
734             env.out("%s\n" % iv.getLocation())
735
736
737 def cmdInspectPath(env, args):
738     """Handler for the inspect-path command."""
739     if len(args) < 1:
740         raise ArgumentError
741     import Image as PILImage
742     for filepath in walk_files(args):
743         try:
744             imageversion = env.shelf.getImageVersionByHash(
745                 computeImageHash(filepath))
746             if imageversion.getLocation() == os.path.realpath(filepath):
747                 env.out("[Registered]   %s\n" % filepath)
748             else:
749                 env.out("[Moved]        %s\n" % filepath)
750         except ImageVersionDoesNotExistError:
751             try:
752                 imageversion = env.shelf.getImageVersionByLocation(filepath)
753                 env.out("[Modified]     %s\n" % filepath)
754             except MultipleImageVersionsAtOneLocationError:
755                 env.out("[Multiple]     %s\n" % filepath)
756             except ImageVersionDoesNotExistError:
757                 try:
758                     PILImage.open(filepath)
759                     env.out("[Unregistered] %s\n" % filepath)
760 #                except IOError:
761                 except: # Work-around for buggy PIL.
762                     env.out("[Non-image]    %s\n" % filepath)
763
764
765 def cmdMakePrimary(env, args):
766     """Handler for the make-primary command."""
767     if len(args) == 0:
768         raise ArgumentError
769     for iv in args:
770         sloppyGetImageVersion(env, iv).makePrimary()
771
772
773 def cmdPrintAlbums(env, args):
774     """Handler for the print-albums command."""
775     if len(args) > 0:
776         root = sloppyGetAlbum(env, args[0])
777     else:
778         root = env.shelf.getRootAlbum()
779     printAlbumsHelper(env, root, 0, 0, [])
780
781
782 def printAlbumsHelper(env, obj, position, level, visited):
783     """Helper function for cmdPrintAlbums."""
784     imgtmpl = "%(indent)s[I] {%(position)s} <%(id)s>\n"
785     imgvertmpl = "%(indent)s[V] %(location)s {%(primary)s%(type)s} <%(id)s>\n"
786     if env.verbose:
787         albtmpl = "%(indent)s[A] %(name)s {%(position)s} <%(id)s> (%(type)s)\n"
788     else:
789         albtmpl = "%(indent)s[A] %(name)s <%(id)s> (%(type)s)\n"
790     indentspaces = PRINT_ALBUMS_INDENT * " "
791     if obj.isAlbum():
792         tag = obj.getTag()
793         env.out(albtmpl % {
794             "indent": level * indentspaces,
795             "name": tag,
796             "position": position,
797             "id": obj.getId(),
798             "type": obj.getType(),
799             })
800         if tag in visited:
801             env.out("%s[...]\n" % ((level + 1) * indentspaces))
802         else:
803             pos = 0
804             if env.verbose:
805                 children = obj.getChildren()
806             else:
807                 children = obj.getAlbumChildren()
808             for child in children:
809                 printAlbumsHelper(
810                     env,
811                     child,
812                     pos,
813                     level + 1,
814                     visited + [tag])
815                 pos += 1
816     else:
817         env.out(imgtmpl % {
818             "indent": level * indentspaces,
819             "position": position,
820             "id": obj.getId(),
821             })
822         for iv in obj.getImageVersions():
823             env.out(imgvertmpl % {
824                 "indent": (level + 1) * indentspaces,
825                 "id": iv.getId(),
826                 "location": iv.getLocation(),
827                 "primary": iv.isPrimary() and "Primary " or "",
828                 "type": iv.getType(),
829                 })
830     if env.verbose:
831         attrtmpl = "%(indent)s%(key)s: %(value)s\n"
832         names = obj.getAttributeNames()
833         for name in names:
834             env.out(attrtmpl % {
835                 "indent": (level + 1) * indentspaces,
836                 "key": name,
837                 "value": obj.getAttribute(name),
838                 })
839
840
841 def cmdPrintCategories(env, args):
842     """Handler for the print-categories command."""
843     if len(args) != 0:
844         raise ArgumentError
845     for category in env.shelf.getRootCategories():
846         printCategoriesHelper(env, category, 0)
847
848
849 def printCategoriesHelper(env, category, level):
850     """Helper function for cmdPrintCategories."""
851     indentspaces = PRINT_ALBUMS_INDENT * " "
852     env.out("%s%s (%s) <%s>\n" % (
853         level * indentspaces,
854         category.getDescription(),
855         category.getTag(),
856         category.getId()))
857     for child in category.getChildren():
858         printCategoriesHelper(env, child, level + 1)
859
860
861 def cmdPrintStatistics(env, dummy):
862     """Handler for the print-statistics command."""
863     stats = env.shelf.getStatistics()
864     env.out("Number of albums: %d\n" % stats["nalbums"])
865     env.out("Number of categories: %d\n" % stats["ncategories"])
866     env.out("Number of images: %d\n" % stats["nimages"])
867     env.out("Number of image versions: %d\n" % stats["nimageversions"])
868
869
870 def cmdRegister(env, args):
871     """Handler for the register command."""
872     if len(args) < 2:
873         raise ArgumentError
874     destalbum = sloppyGetAlbum(env, args[0])
875     registrationTimeString = unicode(time.strftime("%Y-%m-%d %H:%M:%S"))
876     registerHelper(
877         env,
878         destalbum,
879         registrationTimeString,
880         args[1:])
881
882
883 def registerHelper(env, destalbum, registrationTimeString, paths):
884     """Helper function for cmdRegister."""
885     paths.sort()
886     newchildren = []
887     for path in paths:
888         if env.verbose:
889             env.out("Processing %s ...\n" % path)
890         if os.path.isdir(path):
891             tag = os.path.basename(path)
892             if tag in DIRECTORIES_TO_IGNORE:
893                 if env.verbose:
894                     env.out("Ignoring.\n")
895                 continue
896             tag = makeValidTag(tag)
897             while True:
898                 try:
899                     album = env.shelf.createAlbum(tag)
900                     break
901                 except AlbumExistsError:
902                     tag += "_"
903             newchildren.append(album)
904             env.out("Registered directory %s as an album with tag %s\n" % (
905                 path,
906                 tag))
907             registerHelper(
908                 env,
909                 album,
910                 registrationTimeString,
911                 [os.path.join(path, x) for x in os.listdir(path)])
912         elif os.path.isfile(path):
913             try:
914                 image = env.shelf.createImage()
915                 env.shelf.createImageVersion(
916                     image, path, ImageVersionType.Original)
917                 image.setAttribute(u"registered", registrationTimeString)
918                 newchildren.append(image)
919                 if env.verbose:
920                     env.out("Registered image: %s\n" % path)
921             except NotAnImageFileError, x:
922                 env.out("Ignoring non-image file: %s\n" % path)
923             except ImageVersionExistsError, x:
924                 env.err("Ignoring already registered image version: %s\n" % path)
925         else:
926             env.err("No such file or directory (ignored): %s\n" % path)
927     addHelper(env, destalbum, newchildren)
928
929
930 def cmdRemove(env, args):
931     """Handler for the remove command."""
932     if len(args) < 2:
933         raise ArgumentError
934     album = sloppyGetAlbum(env, args[0])
935     positions = []
936     for pos in args[1:]:
937         try:
938             positions.append(int(pos))
939         except ValueError:
940             env.errexit("Bad position: %s.\n" % pos)
941     positions.sort(reverse=True)
942     children = list(album.getChildren())
943     if not (0 <= positions[0] < len(children)):
944         env.errexit("Bad position: %d.\n" % positions[0])
945     for pos in positions:
946         del children[pos]
947     album.setChildren(children)
948
949
950 def cmdRemoveCategory(env, args):
951     """Handler for the remove-category command."""
952     if len(args) < 2:
953         raise ArgumentError
954     category = env.shelf.getCategoryByTag(args[0])
955     for arg in args[1:]:
956         sloppyGetObject(env, arg).removeCategory(category)
957
958
959 def cmdRenameAlbum(env, args):
960     """Handler for the rename-album command."""
961     if len(args) != 2:
962         raise ArgumentError
963     sloppyGetAlbum(env, args[0]).setTag(args[1])
964
965
966 def cmdRenameCategory(env, args):
967     """Handler for the rename-category command."""
968     if len(args) != 2:
969         raise ArgumentError
970     env.shelf.getCategoryByTag(args[0]).setTag(args[1])
971
972
973 def cmdRereadExif(env, args):
974     """Handler for the reread-exif command."""
975     if len(args) == 0:
976         raise ArgumentError
977     for iv in args:
978         sloppyGetImageVersion(env, iv).importExifTags(True)
979
980
981 def cmdSearch(env, args):
982     """Handler for the search command."""
983     if len(args) != 1:
984         raise ArgumentError
985     parser = Parser(env.shelf)
986     objects = env.shelf.search(parser.parse(args[0]))
987     objects = [x for x in objects if not x.isAlbum()]
988     ivs = []
989     for o in objects:
990         for iv in o.getImageVersions():
991             ivs.append(iv)
992     if env.printIDs:
993         ids = set()
994         for iv in ivs:
995             ids.add(iv.getImage().getId())
996         output = [str(x) for x in ids]
997     else:
998         output = []
999         for iv in ivs:
1000             t = iv.getType()
1001             if (env.includeAll or
1002                 (env.includeImportant and t == ImageVersionType.Important) or
1003                 (env.includeOriginal and t == ImageVersionType.Original) or
1004                 (env.includeOther and t == ImageVersionType.Other) or
1005                 (env.includePrimary and iv.isPrimary())):
1006                 output.append(iv.getLocation())
1007         output.sort()
1008     if output:
1009         if env.useNullCharacters:
1010             terminator = u"\0"
1011         else:
1012             terminator = u"\n"
1013         env.out(u"%s%s" % (terminator.join(output), terminator))
1014
1015
1016 def cmdSetAttribute(env, args):
1017     """Handler for the set-attribute command."""
1018     if len(args) < 3:
1019         raise ArgumentError
1020     attr = args[0]
1021     value = args[1]
1022     for arg in args[2:]:
1023         sloppyGetObject(env, arg).setAttribute(attr, value)
1024
1025
1026 def cmdSetCategoryDescription(env, args):
1027     """Handler for the set-category-description command."""
1028     if len(args) != 2:
1029         raise ArgumentError
1030     env.shelf.getCategoryByTag(args[0]).setDescription(args[1])
1031
1032
1033 def cmdSetImageVersionComment(env, args):
1034     """Handler for the set-imageversion-comment command."""
1035     if len(args) < 2:
1036         raise ArgumentError
1037     comment = args[0]
1038     for iv in args[1:]:
1039         sloppyGetImageVersion(env, iv).setComment(comment)
1040
1041
1042 def cmdSetImageVersionImage(env, args):
1043     """Handler for the set-imageversion-image command."""
1044     if len(args) < 2:
1045         raise ArgumentError
1046     image = sloppyGetImage(env, args[0])
1047     for iv in args[1:]:
1048         sloppyGetImageVersion(env, iv).setImage(image)
1049
1050
1051 def cmdSetImageVersionType(env, args):
1052     """Handler for the set-imageversion-type command."""
1053     if len(args) < 2:
1054         raise ArgumentError
1055     ivtype = parseImageVersionType(args[0])
1056     for iv in args[1:]:
1057         sloppyGetImageVersion(env, iv).setType(ivtype)
1058
1059
1060 def cmdSortAlbum(env, args):
1061     """Handler for the sort-album command."""
1062     if not 1 <= len(args) <= 2:
1063         raise ArgumentError
1064     if len(args) == 2:
1065         attr = args[1]
1066     else:
1067         attr = u"captured"
1068     album = sloppyGetAlbum(env, args[0])
1069     children = sorted(album.getChildren(), key=lambda x: x.getAttribute(attr))
1070     album.setChildren(children)
1071
1072
1073 def cmdUpdateContents(env, args):
1074     """Handler for the update-contents command."""
1075     if len(args) < 1:
1076         raise ArgumentError
1077     for filepath in walk_files(args):
1078         try:
1079             imageversion = env.shelf.getImageVersionByLocation(filepath)
1080             oldhash = imageversion.getHash()
1081             imageversion.contentChanged()
1082             if imageversion.getHash() != oldhash:
1083                 env.out("New checksum: %s\n" % filepath)
1084             else:
1085                 if env.verbose:
1086                     env.out(
1087                         "Same checksum as before: %s\n" % filepath)
1088         except ImageVersionDoesNotExistError:
1089             if env.verbose:
1090                 env.out("Unregistered image/file: %s\n" % filepath)
1091         except MultipleImageVersionsAtOneLocationError:
1092             env.errexit(
1093                 "Multiple known image versions at this location: %s\n" % (
1094                 filepath))
1095
1096
1097 def cmdUpdateLocations(env, args):
1098     """Handler for the update-locations command."""
1099     if len(args) < 1:
1100         raise ArgumentError
1101     for filepath in walk_files(args):
1102         try:
1103             imageversion = env.shelf.getImageVersionByHash(
1104                 computeImageHash(filepath))
1105             oldlocation = imageversion.getLocation()
1106             if oldlocation != os.path.realpath(filepath):
1107                 imageversion.locationChanged(filepath)
1108                 env.out("New location: %s --> %s\n" % (
1109                     oldlocation,
1110                     imageversion.getLocation()))
1111             else:
1112                 if env.verbose:
1113                     env.out(
1114                         "Same location as before: %s\n" % filepath)
1115         except IOError, x:
1116             if env.verbose:
1117                 env.out("Failed to read: %s (%s)\n" % (
1118                     filepath,
1119                     x))
1120         except ImageVersionDoesNotExistError:
1121             if env.verbose:
1122                 env.out("Unregistered image/file: %s\n" % filepath)
1123
1124
1125 commandTable = {
1126     "add": cmdAdd,
1127     "add-category": cmdAddCategory,
1128     "clean-cache": cmdCleanCache,
1129     "connect-category": cmdConnectCategory,
1130     "create-album": cmdCreateAlbum,
1131     "create-category": cmdCreateCategory,
1132     "delete-attribute": cmdDeleteAttribute,
1133     "destroy-album": cmdDestroyAlbum,
1134     "destroy-category": cmdDestroyCategory,
1135     "destroy-image": cmdDestroyImage,
1136     "destroy-imageversion": cmdDestroyImageVersion,
1137     "disconnect-category": cmdDisconnectCategory,
1138     "find-missing-imageversions": cmdFindMissingImageVersions,
1139     "find-modified-imageversions": cmdFindModifiedImageVersions,
1140     "generate": cmdGenerate,
1141     "get-attribute": cmdGetAttribute,
1142     "get-attributes": cmdGetAttributes,
1143     "get-categories": cmdGetCategories,
1144     "get-imageversions": cmdGetImageVersions,
1145     "inspect-path": cmdInspectPath,
1146     "make-primary": cmdMakePrimary,
1147     "print-albums": cmdPrintAlbums,
1148     "print-categories": cmdPrintCategories,
1149     "print-statistics": cmdPrintStatistics,
1150     "register": cmdRegister,
1151     "remove": cmdRemove,
1152     "remove-category": cmdRemoveCategory,
1153     "rename-album": cmdRenameAlbum,
1154     "rename-category": cmdRenameCategory,
1155     "reread-exif": cmdRereadExif,
1156     "search": cmdSearch,
1157     "set-attribute": cmdSetAttribute,
1158     "set-category-description": cmdSetCategoryDescription,
1159     "set-imageversion-comment": cmdSetImageVersionComment,
1160     "set-imageversion-image": cmdSetImageVersionImage,
1161     "set-imageversion-type": cmdSetImageVersionType,
1162     "sort-album": cmdSortAlbum,
1163     "update-contents": cmdUpdateContents,
1164     "update-locations": cmdUpdateLocations,
1165 }
1166
1167 ######################################################################
1168 ### Main
1169
1170 def main(argv):
1171     """Run the program.
1172
1173     Arguments:
1174
1175     argv -- A list of arguments.
1176     """
1177     env = CommandlineClientEnvironment()
1178
1179     argv = [x.decode(env.filesystemEncoding) for x in argv]
1180
1181     sys.stdin = codecs.getreader(get_file_encoding(sys.stdin))(sys.stdin)
1182     sys.stdout = codecs.getwriter(get_file_encoding(sys.stdout))(sys.stdout)
1183     sys.stderr = codecs.getwriter(get_file_encoding(sys.stderr))(sys.stderr)
1184
1185     try:
1186         optlist, args = getopt.gnu_getopt(
1187             argv[1:],
1188             "0ht:v",
1189             ["configfile=",
1190              "database=",
1191              "gencharenc=",
1192              "help",
1193              "identify-by-hash",
1194              "identify-by-path",
1195              "ids",
1196              "include-all",
1197              "include-important",
1198              "include-original",
1199              "include-other",
1200              "include-primary",
1201              "no-act",
1202              "null",
1203              "position=",
1204              "type=",
1205              "verbose",
1206              "version"])
1207     except getopt.GetoptError:
1208         printErrorAndExit("Unknown option. See \"kofoto --help\" for help.\n")
1209
1210     # Other defaults:
1211     shelfLocation = None
1212     configFileLocation = None
1213     genCharEnc = "utf-8"
1214
1215     for opt, optarg in optlist:
1216         if opt == "--configfile":
1217             configFileLocation = expanduser(optarg)
1218         elif opt == "--database":
1219             shelfLocation = optarg
1220         elif opt == "--gencharenc":
1221             genCharEnc = str(optarg)
1222         elif opt in ("-h", "--help"):
1223             displayHelp()
1224             sys.exit(0)
1225         elif opt == "--identify-by-hash":
1226             env.identifyByPath = False
1227         elif opt == "--identify-by-path":
1228             env.identifyByPath = True
1229         elif opt == "--ids":
1230             env.printIDs = True
1231         elif opt == "--include-all":
1232             env.includeAll = True
1233         elif opt == "--include-important":
1234             env.includeImportant = True
1235         elif opt == "--include-original":
1236             env.includeOriginal = True
1237         elif opt == "--include-other":
1238             env.includeOther = True
1239         elif opt == "--include-primary":
1240             env.includePrimary = True
1241         elif opt == "--no-act":
1242             printNotice(
1243                 "no-act: No changes will be commited to the database!\n")
1244             env.noAct = True
1245         elif opt in ("-0", "--null"):
1246             env.useNullCharacters = True
1247         elif opt == "--position":
1248             if optarg == "last":
1249                 env.position = -1
1250             else:
1251                 try:
1252                     env.position = int(optarg)
1253                 except ValueError:
1254                     printErrorAndExit("Invalid position: \"%s\"\n" % optarg)
1255         elif opt in ("-t", "--type"):
1256             env.type = optarg
1257         elif opt in ("-v", "--verbose"):
1258             env.verbose = True
1259         elif opt == "--version":
1260             sys.stdout.write("%s\n" % env.version)
1261             sys.exit(0)
1262
1263     if not (env.includeAll or env.includeImportant or env.includeOriginal
1264             or env.includeOther or env.includePrimary):
1265         env.includePrimary = True
1266
1267     if len(args) == 0:
1268         printErrorAndExit(
1269             "No command given. See \"kofoto --help\" for help.\n")
1270
1271     try:
1272         env.setup(configFileLocation, shelfLocation)
1273     except ClientEnvironmentError, e:
1274         printErrorAndExit(e[0])
1275
1276     if not commandTable.has_key(args[0]):
1277         printErrorAndExit(
1278             "Unknown command \"%s\". See \"kofoto --help\" for help.\n" % (
1279             args[0]))
1280
1281     try:
1282         if env.shelf.isUpgradable():
1283             printNotice(
1284                 "Upgrading %s to new database format...\n" % env.shelfLocation)
1285             if not env.shelf.tryUpgrade():
1286                 printErrorAndExit(
1287                     "Failed to upgrade metadata database format.\n")
1288         env.shelf.begin()
1289     except ShelfNotFoundError, x:
1290         printErrorAndExit("Could not open metadata database \"%s\".\n" % (
1291             env.shelfLocation))
1292     except ShelfLockedError, x:
1293         printErrorAndExit(
1294             "Could not open metadata database \"%s\".\n" % env.shelfLocation +
1295             "Another process is locking it.\n")
1296     except UnsupportedShelfError, filename:
1297         printErrorAndExit(
1298             "Could not read metadata database file %s (too new database"
1299             " format?).\n" % filename)
1300     try:
1301         env.gencharenc = genCharEnc
1302         env.out = printOutput
1303         env.err = printError
1304         env.errexit = printErrorAndExit
1305         env.thumbnailsizelimit = env.config.getcoordlist(
1306             "album generation", "thumbnail_size_limit")[0]
1307         env.defaultsizelimit = env.config.getcoordlist(
1308             "album generation", "default_image_size_limit")[0]
1309
1310         imgsizesval = env.config.getcoordlist(
1311             "album generation", "other_image_size_limits")
1312         imgsizesset = set(imgsizesval) # Get rid of duplicates.
1313         defaultlimit = env.config.getcoordlist(
1314             "album generation", "default_image_size_limit")[0]
1315         imgsizesset.add(defaultlimit)
1316         imgsizes = sorted(
1317             imgsizesset, key=lambda x, y: cmp(x[0] * x[1], y[0] * y[1]))
1318         env.imagesizelimits = imgsizes
1319
1320         commandTable[args[0]](env, args[1:])
1321         if env.noAct:
1322             env.shelf.rollback()
1323             printOutput(
1324                 "no-act: All changes to the database have been revoked!\n")
1325         else:
1326             env.shelf.commit()
1327         sys.exit(0)
1328     except ArgumentError:
1329         printErrorAndExit(
1330             "Bad arguments to command. See \"kofoto --help\" for help.\n")
1331     except UndeletableAlbumError, x:
1332         printError("Undeletable album: \"%s\".\n" % x.args[0])
1333     except BadAlbumTagError, x:
1334         printError("Bad album tag: \"%s\".\n" % x.args[0])
1335     except AlbumExistsError, x:
1336         printError("Album already exists: \"%s\".\n" % x.args[0])
1337     except ImageDoesNotExistError, x:
1338         printError("Image does not exist: \"%s\".\n" % x.args[0])
1339     except AlbumDoesNotExistError, x:
1340         printError("Album does not exist: \"%s\".\n" % x.args[0])
1341     except ObjectDoesNotExistError, x:
1342         printError("Object does not exist: \"%s\".\n" % x.args[0])
1343     except UnknownAlbumTypeError, x:
1344         printError("Unknown album type: \"%s\".\n" % x.args[0])
1345     except UnsettableChildrenError, x:
1346         printError(
1347             "Cannot modify children of \"%s\" (children are created"
1348             " virtually).\n" % x.args[0])
1349     except CategoryExistsError, x:
1350         printError("Category already exists: \"%s\".\n" % x.args[0])
1351     except CategoryDoesNotExistError, x:
1352         printError("Category does not exist: \"%s\".\n" % x.args[0])
1353     except BadCategoryTagError, x:
1354         printError("Bad category tag: %s.\n" % x.args[0])
1355     except CategoryPresentError, x:
1356         printError("Object %s is already associated with category %s.\n" % (
1357             x.args[0], x.args[1]))
1358     except CategoriesAlreadyConnectedError, x:
1359         printError("Categories %s and %s are already connected.\n" % (
1360             x.args[0], x.args[1]))
1361     except CategoryLoopError, x:
1362         printError(
1363             "Connecting %s to %s would make a loop in the categories.\n" % (
1364             x.args[0], x.args[1]))
1365     except UnterminatedStringError, x:
1366         printError(
1367             "While scanning search expression: unterminated string starting at"
1368             " character %d.\n" % x.args[0])
1369     except BadTokenError, x:
1370         printError(
1371             "While scanning search expression: bad token starting at character"
1372             " %d.\n" % x.args[0])
1373     except ParseError, x:
1374         printError(
1375             "While parsing search expression: %s.\n" % x.args[0])
1376     except UnknownImageVersionTypeError, x:
1377         printError("Unknown image version type: \"%s\".\n" % x.args[0])
1378     except ExifImportError, x:
1379         printError("Failed to import EXIF information from \"%s\".\n" % (
1380             x.args[0]))
1381     except KeyboardInterrupt:
1382         printOutput("Interrupted.\n")
1383     except IOError, e:
1384         if e.filename:
1385             errstr = "%s: \"%s\"" % (e.strerror, e.filename)
1386         else:
1387             errstr = e.strerror
1388         printError("%s.\n" % errstr)
1389     env.shelf.rollback()
1390     sys.exit(1)