@@ -90,7 +90,7 @@ def get_parser():
9090 r'“raw” strings.'
9191 p = argparse .ArgumentParser (description = d )
9292 a = p .add_argument
93- a ('-V' , '--version' , action = 'version' , version = '%(prog)s 1.0 ' )
93+ a ('-V' , '--version' , action = 'version' , version = '%(prog)s 1.1 ' )
9494 a ('-B' , '--before' , type = int , default = 0 ,
9595 help = r'add lines of context before match' )
9696 a ('-A' , '--after' , type = int , default = 0 ,
@@ -100,7 +100,10 @@ def get_parser():
100100 a ('-P' , '--plain-replace' , action = 'store_true' ,
101101 help = r'treat replace expression as plain string' )
102102 a ('-e' , '--encoding' , default = 'utf8' ,
103- help = r'file encoding, e.g. cp850' )
103+ help = r'file encoding, e.g. utf8 (default), cp1252, or cp850' )
104+ a ('-E' , '--encoding-fallback' , default = 'cp1252' ,
105+ help = r'file encoding if the -e encoding fails, e.g. cp1252 (default) '
106+ r'or cp850. Use the empty string or the value of -e to avoid retrying.' )
104107 a ('-k' , '--keep' , action = 'store_true' ,
105108 help = r'try to keep timestamps when modifying files' )
106109 a ('-g' , '--grep' , action = 'store_true' ,
@@ -144,7 +147,6 @@ def write(*args, **kwargs):
144147 print (* args , end = '' , ** kwargs )
145148
146149
147-
148150def write_printable (text ):
149151 write (write_printable .nonprintable .sub (
150152 (lambda match : repr (match .group (0 )).replace ("'" , '' )), text ))
@@ -548,28 +550,82 @@ def ask_write(filename):
548550 return answer
549551
550552
551- class Action :
552- def _slurp (self , filename ):
553- file = codecs .open (filename , 'rb' , encoding = self ._encoding )
554- text = file .read ()
553+ def close_noexcept (file ):
554+ try :
555555 file .close ()
556+ except Exception :
557+ pass
558+
559+
560+ class Action :
561+ def _slurp (self , filename , encoding_override = None ):
562+ self ._encoding_override = encoding_override # Remember for writing
563+ if encoding_override :
564+ encoding = encoding_override
565+ else :
566+ encoding = self ._encoding
567+ try :
568+ file = codecs .open (filename , 'rb' , encoding = encoding )
569+ except Exception as e :
570+ warn ('cannot open {0} for reading: {1}' .format (filename , e ))
571+ return None
572+ try :
573+ text = file .read ()
574+ except UnicodeDecodeError as e :
575+ if encoding_override :
576+ warn ('fallback encoding fails, too: {0}' .format (e ))
577+ return None
578+ close_noexcept (file )
579+ if self ._encoding_fallback and self ._encoding_fallback != encoding :
580+ warn ('switching encoding {0}->{1} for {2}: {3}' .
581+ format (self ._encoding , self ._encoding_fallback ,
582+ filename , e ))
583+ return self ._slurp (filename , self ._encoding_fallback )
584+ warn ('{0} encoding error for {1}: {2}' .
585+ format (self ._encoding , filename , e ))
586+ return None
587+ except Exception as e :
588+ close_noexcept (file )
589+ warn ('error when reading {0}: {1}' .format (filename , e ))
590+ return None
591+ try :
592+ file .close ()
593+ except Exception as e :
594+ warn ('error when closing {0} after reading: {1}' .
595+ format (filename , e ))
596+ return None
556597 return text
557598
558599 def _write (self , filename , text ):
559600 stat = None
560601 if self ._keep :
561602 try :
562603 stat = os .stat (filename )
563- except :
604+ except Exception as e :
564605 stat = None
565- file = codecs .open (filename , 'wb' , encoding = self ._encoding )
566- file .write (text )
567- file .close ()
606+ warn ('cannot get timestamp of {0}: {1}' .format (filename , e ))
607+ if self ._write_encoding_override :
608+ encoding = self ._encoding_override
609+ else :
610+ encoding = self ._encoding
611+ try :
612+ file = codecs .open (filename , 'wb' , encoding = encoding )
613+ except Exception as e :
614+ warn ('cannot open {0} for writing: {1}' .format (filename , e ))
615+ return
616+ try :
617+ file .write (text )
618+ except Exception as e :
619+ warn ('write error to {0}: {1}' .format (filename , e ))
620+ try :
621+ file .close ()
622+ except Exception as e :
623+ warn ('cannot close {0} after writing: {1}' .format (filename , e ))
568624 if stat is not None :
569625 try :
570626 os .utime (filename , (stat .st_atime , stat .st_mtime ))
571- except :
572- pass
627+ except Exception as e :
628+ warn ( 'cannot set timestamp of {0}: {1}' . format ( filename , e ))
573629
574630 def _exit (self ):
575631 if self ._replace is None and not self ._have :
@@ -578,6 +634,8 @@ class Action:
578634
579635 def _process (self , filename ):
580636 text = self ._slurp (filename )
637+ if text is None :
638+ return
581639 matcher = Matcher (
582640 self ._search , self ._replace ,
583641 plain_search = self ._plain_search ,
@@ -651,8 +709,9 @@ class Action:
651709
652710 def _recurse (self , dir ):
653711 for (dirpath , dirnames , filenames ) in os .walk (dir ):
654- dirnames [:] = [name for name in sorted (dirnames )
655- if not self ._test_skip (os .path .join (dirpath , name ))]
712+ dirnames [:] = \
713+ [name for name in sorted (dirnames )
714+ if not self ._test_skip (os .path .join (dirpath , name ))]
656715 for name in sorted (filenames ):
657716 filename = os .path .join (dirpath , name )
658717 if not self ._test_skip (filename ) and os .path .isfile (filename ):
@@ -703,6 +762,7 @@ class Action:
703762 if options .verbose :
704763 self ._flags |= re .VERBOSE
705764 self ._encoding = options .encoding
765+ self ._encoding_fallback = options .encoding_fallback
706766 self ._keep = options .keep
707767 self ._quiet = options .quiet
708768 self ._skip = options .skip
0 commit comments