1 """Helper class to quickly write a loop over all standard input files.
2 
3 Typical use is:
4 
5     import fileinput
6     for line in fileinput.input(encoding="utf-8"):
7         process(line)
8 
9 This iterates over the lines of all files listed in sys.argv[1:],
10 defaulting to sys.stdin if the list is empty.  If a filename is '-' it
11 is also replaced by sys.stdin and the optional arguments mode and
12 openhook are ignored.  To specify an alternative list of filenames,
13 pass it as the argument to input().  A single file name is also allowed.
14 
15 Functions filename(), lineno() return the filename and cumulative line
16 number of the line that has just been read; filelineno() returns its
17 line number in the current file; isfirstline() returns true iff the
18 line just read is the first line of its file; isstdin() returns true
19 iff the line was read from sys.stdin.  Function nextfile() closes the
20 current file so that the next iteration will read the first line from
21 the next file (if any); lines not read from the file will not count
22 towards the cumulative line count; the filename is not changed until
23 after the first line of the next file has been read.  Function close()
24 closes the sequence.
25 
26 Before any lines have been read, filename() returns None and both line
27 numbers are zero; nextfile() has no effect.  After all lines have been
28 read, filename() and the line number functions return the values
29 pertaining to the last line read; nextfile() has no effect.
30 
31 All files are opened in text mode by default, you can override this by
32 setting the mode parameter to input() or FileInput.__init__().
33 If an I/O error occurs during opening or reading a file, the OSError
34 exception is raised.
35 
36 If sys.stdin is used more than once, the second and further use will
37 return no lines, except perhaps for interactive use, or if it has been
38 explicitly reset (e.g. using sys.stdin.seek(0)).
39 
40 Empty files are opened and immediately closed; the only time their
41 presence in the list of filenames is noticeable at all is when the
42 last file opened is empty.
43 
44 It is possible that the last line of a file doesn't end in a newline
45 character; otherwise lines are returned including the trailing
46 newline.
47 
48 Class FileInput is the implementation; its methods filename(),
49 lineno(), fileline(), isfirstline(), isstdin(), nextfile() and close()
50 correspond to the functions in the module.  In addition it has a
51 readline() method which returns the next input line, and a
52 __getitem__() method which implements the sequence behavior.  The
53 sequence must be accessed in strictly sequential order; sequence
54 access and readline() cannot be mixed.
55 
56 Optional in-place filtering: if the keyword argument inplace=1 is
57 passed to input() or to the FileInput constructor, the file is moved
58 to a backup file and standard output is directed to the input file.
59 This makes it possible to write a filter that rewrites its input file
60 in place.  If the keyword argument backup=".<some extension>" is also
61 given, it specifies the extension for the backup file, and the backup
62 file remains around; by default, the extension is ".bak" and it is
63 deleted when the output file is closed.  In-place filtering is
64 disabled when standard input is read.  XXX The current implementation
65 does not work for MS-DOS 8+3 filesystems.
66 """
67 
68 import io
69 import sys, os
70 from types import GenericAlias
71 
72 __all__ = ["input", "close", "nextfile", "filename", "lineno", "filelineno",
73            "fileno", "isfirstline", "isstdin", "FileInput", "hook_compressed",
74            "hook_encoded"]
75 
76 _state = None
77 
78 def input(files=None, inplace=False, backup="", *, mode="r", openhook=None,
79           encoding=None, errors=None):
80     """Return an instance of the FileInput class, which can be iterated.
81 
82     The parameters are passed to the constructor of the FileInput class.
83     The returned instance, in addition to being an iterator,
84     keeps global state for the functions of this module,.
85     """
86     global _state
87     if _state and _state._file:
88         raise RuntimeError("input() already active")
89     _state = FileInput(files, inplace, backup, mode=mode, openhook=openhook,
90                        encoding=encoding, errors=errors)
91     return _state
92 
93 def close():
94     """Close the sequence."""
95     global _state
96     state = _state
97     _state = None
98     if state:
99         state.close()
100 
101 def nextfile():
102     """
103     Close the current file so that the next iteration will read the first
104     line from the next file (if any); lines not read from the file will
105     not count towards the cumulative line count. The filename is not
106     changed until after the first line of the next file has been read.
107     Before the first line has been read, this function has no effect;
108     it cannot be used to skip the first file. After the last line of the
109     last file has been read, this function has no effect.
110     """
111     if not _state:
112         raise RuntimeError("no active input()")
113     return _state.nextfile()
114 
115 def filename():
116     """
117     Return the name of the file currently being read.
118     Before the first line has been read, returns None.
119     """
120     if not _state:
121         raise RuntimeError("no active input()")
122     return _state.filename()
123 
124 def lineno():
125     """
126     Return the cumulative line number of the line that has just been read.
127     Before the first line has been read, returns 0. After the last line
128     of the last file has been read, returns the line number of that line.
129     """
130     if not _state:
131         raise RuntimeError("no active input()")
132     return _state.lineno()
133 
134 def filelineno():
135     """
136     Return the line number in the current file. Before the first line
137     has been read, returns 0. After the last line of the last file has
138     been read, returns the line number of that line within the file.
139     """
140     if not _state:
141         raise RuntimeError("no active input()")
142     return _state.filelineno()
143 
144 def fileno():
145     """
146     Return the file number of the current file. When no file is currently
147     opened, returns -1.
148     """
149     if not _state:
150         raise RuntimeError("no active input()")
151     return _state.fileno()
152 
153 def isfirstline():
154     """
155     Returns true the line just read is the first line of its file,
156     otherwise returns false.
157     """
158     if not _state:
159         raise RuntimeError("no active input()")
160     return _state.isfirstline()
161 
162 def isstdin():
163     """
164     Returns true if the last line was read from sys.stdin,
165     otherwise returns false.
166     """
167     if not _state:
168         raise RuntimeError("no active input()")
169     return _state.isstdin()
170 
171 class FileInput:
172     """FileInput([files[, inplace[, backup]]], *, mode=None, openhook=None)
173 
174     Class FileInput is the implementation of the module; its methods
175     filename(), lineno(), fileline(), isfirstline(), isstdin(), fileno(),
176     nextfile() and close() correspond to the functions of the same name
177     in the module.
178     In addition it has a readline() method which returns the next
179     input line, and a __getitem__() method which implements the
180     sequence behavior. The sequence must be accessed in strictly
181     sequential order; random access and readline() cannot be mixed.
182     """
183 
184     def __init__(self, files=None, inplace=False, backup="", *,
185                  mode="r", openhook=None, encoding=None, errors=None):
186         if isinstance(files, str):
187             files = (files,)
188         elif isinstance(files, os.PathLike):
189             files = (os.fspath(files), )
190         else:
191             if files is None:
192                 files = sys.argv[1:]
193             if not files:
194                 files = ('-',)
195             else:
196                 files = tuple(files)
197         self._files = files
198         self._inplace = inplace
199         self._backup = backup
200         self._savestdout = None
201         self._output = None
202         self._filename = None
203         self._startlineno = 0
204         self._filelineno = 0
205         self._file = None
206         self._isstdin = False
207         self._backupfilename = None
208         self._encoding = encoding
209         self._errors = errors
210 
211         # We can not use io.text_encoding() here because old openhook doesn't
212         # take encoding parameter.
213         if (sys.flags.warn_default_encoding and
214                 "b" not in mode and encoding is None and openhook is None):
215             import warnings
216             warnings.warn("'encoding' argument not specified.",
217                           EncodingWarning, 2)
218 
219         # restrict mode argument to reading modes
220         if mode not in ('r', 'rb'):
221             raise ValueError("FileInput opening mode must be 'r' or 'rb'")
222         self._mode = mode
223         self._write_mode = mode.replace('r', 'w')
224         if openhook:
225             if inplace:
226                 raise ValueError("FileInput cannot use an opening hook in inplace mode")
227             if not callable(openhook):
228                 raise ValueError("FileInput openhook must be callable")
229         self._openhook = openhook
230 
231     def __del__(self):
232         self.close()
233 
234     def close(self):
235         try:
236             self.nextfile()
237         finally:
238             self._files = ()
239 
240     def __enter__(self):
241         return self
242 
243     def __exit__(self, type, value, traceback):
244         self.close()
245 
246     def __iter__(self):
247         return self
248 
249     def __next__(self):
250         while True:
251             line = self._readline()
252             if line:
253                 self._filelineno += 1
254                 return line
255             if not self._file:
256                 raise StopIteration
257             self.nextfile()
258             # repeat with next file
259 
260     def nextfile(self):
261         savestdout = self._savestdout
262         self._savestdout = None
263         if savestdout:
264             sys.stdout = savestdout
265 
266         output = self._output
267         self._output = None
268         try:
269             if output:
270                 output.close()
271         finally:
272             file = self._file
273             self._file = None
274             try:
275                 del self._readline  # restore FileInput._readline
276             except AttributeError:
277                 pass
278             try:
279                 if file and not self._isstdin:
280                     file.close()
281             finally:
282                 backupfilename = self._backupfilename
283                 self._backupfilename = None
284                 if backupfilename and not self._backup:
285                     try: os.unlink(backupfilename)
286                     except OSError: pass
287 
288                 self._isstdin = False
289 
290     def readline(self):
291         while True:
292             line = self._readline()
293             if line:
294                 self._filelineno += 1
295                 return line
296             if not self._file:
297                 return line
298             self.nextfile()
299             # repeat with next file
300 
301     def _readline(self):
302         if not self._files:
303             if 'b' in self._mode:
304                 return b''
305             else:
306                 return ''
307         self._filename = self._files[0]
308         self._files = self._files[1:]
309         self._startlineno = self.lineno()
310         self._filelineno = 0
311         self._file = None
312         self._isstdin = False
313         self._backupfilename = 0
314 
315         # EncodingWarning is emitted in __init__() already
316         if "b" not in self._mode:
317             encoding = self._encoding or "locale"
318         else:
319             encoding = None
320 
321         if self._filename == '-':
322             self._filename = '<stdin>'
323             if 'b' in self._mode:
324                 self._file = getattr(sys.stdin, 'buffer', sys.stdin)
325             else:
326                 self._file = sys.stdin
327             self._isstdin = True
328         else:
329             if self._inplace:
330                 self._backupfilename = (
331                     os.fspath(self._filename) + (self._backup or ".bak"))
332                 try:
333                     os.unlink(self._backupfilename)
334                 except OSError:
335                     pass
336                 # The next few lines may raise OSError
337                 os.rename(self._filename, self._backupfilename)
338                 self._file = open(self._backupfilename, self._mode,
339                                   encoding=encoding, errors=self._errors)
340                 try:
341                     perm = os.fstat(self._file.fileno()).st_mode
342                 except OSError:
343                     self._output = open(self._filename, self._write_mode,
344                                         encoding=encoding, errors=self._errors)
345                 else:
346                     mode = os.O_CREAT | os.O_WRONLY | os.O_TRUNC
347                     if hasattr(os, 'O_BINARY'):
348                         mode |= os.O_BINARY
349 
350                     fd = os.open(self._filename, mode, perm)
351                     self._output = os.fdopen(fd, self._write_mode,
352                                              encoding=encoding, errors=self._errors)
353                     try:
354                         os.chmod(self._filename, perm)
355                     except OSError:
356                         pass
357                 self._savestdout = sys.stdout
358                 sys.stdout = self._output
359             else:
360                 # This may raise OSError
361                 if self._openhook:
362                     # Custom hooks made previous to Python 3.10 didn't have
363                     # encoding argument
364                     if self._encoding is None:
365                         self._file = self._openhook(self._filename, self._mode)
366                     else:
367                         self._file = self._openhook(
368                             self._filename, self._mode, encoding=self._encoding, errors=self._errors)
369                 else:
370                     self._file = open(self._filename, self._mode, encoding=encoding, errors=self._errors)
371         self._readline = self._file.readline  # hide FileInput._readline
372         return self._readline()
373 
374     def filename(self):
375         return self._filename
376 
377     def lineno(self):
378         return self._startlineno + self._filelineno
379 
380     def filelineno(self):
381         return self._filelineno
382 
383     def fileno(self):
384         if self._file:
385             try:
386                 return self._file.fileno()
387             except ValueError:
388                 return -1
389         else:
390             return -1
391 
392     def isfirstline(self):
393         return self._filelineno == 1
394 
395     def isstdin(self):
396         return self._isstdin
397 
398     __class_getitem__ = classmethod(GenericAlias)
399 
400 
401 def hook_compressed(filename, mode, *, encoding=None, errors=None):
402     if encoding is None and "b" not in mode:  # EncodingWarning is emitted in FileInput() already.
403         encoding = "locale"
404     ext = os.path.splitext(filename)[1]
405     if ext == '.gz':
406         import gzip
407         stream = gzip.open(filename, mode)
408     elif ext == '.bz2':
409         import bz2
410         stream = bz2.BZ2File(filename, mode)
411     else:
412         return open(filename, mode, encoding=encoding, errors=errors)
413 
414     # gzip and bz2 are binary mode by default.
415     if "b" not in mode:
416         stream = io.TextIOWrapper(stream, encoding=encoding, errors=errors)
417     return stream
418 
419 
420 def hook_encoded(encoding, errors=None):
421     def openhook(filename, mode):
422         return open(filename, mode, encoding=encoding, errors=errors)
423     return openhook
424 
425 
426 def _test():
427     import getopt
428     inplace = False
429     backup = False
430     opts, args = getopt.getopt(sys.argv[1:], "ib:")
431     for o, a in opts:
432         if o == '-i': inplace = True
433         if o == '-b': backup = a
434     for line in input(args, inplace=inplace, backup=backup):
435         if line[-1:] == '\n': line = line[:-1]
436         if line[-1:] == '\r': line = line[:-1]
437         print("%d: %s[%d]%s %s" % (lineno(), filename(), filelineno(),
438                                    isfirstline() and "*" or "", line))
439     print("%d: %s[%d]" % (lineno(), filename(), filelineno()))
440 
441 if __name__ == '__main__':
442     _test()
443