1"""Helper class to quickly write a loop over all standard input files.
2
3Typical use is:
4
5    import fileinput
6    for line in fileinput.input(encoding="utf-8"):
7        process(line)
8
9This iterates over the lines of all files listed in sys.argv[1:],
10defaulting to sys.stdin if the list is empty.  If a filename is '-' it
11is also replaced by sys.stdin and the optional arguments mode and
12openhook are ignored.  To specify an alternative list of filenames,
13pass it as the argument to input().  A single file name is also allowed.
14
15Functions filename(), lineno() return the filename and cumulative line
16number of the line that has just been read; filelineno() returns its
17line number in the current file; isfirstline() returns true iff the
18line just read is the first line of its file; isstdin() returns true
19iff the line was read from sys.stdin.  Function nextfile() closes the
20current file so that the next iteration will read the first line from
21the next file (if any); lines not read from the file will not count
22towards the cumulative line count; the filename is not changed until
23after the first line of the next file has been read.  Function close()
24closes the sequence.
25
26Before any lines have been read, filename() returns None and both line
27numbers are zero; nextfile() has no effect.  After all lines have been
28read, filename() and the line number functions return the values
29pertaining to the last line read; nextfile() has no effect.
30
31All files are opened in text mode by default, you can override this by
32setting the mode parameter to input() or FileInput.__init__().
33If an I/O error occurs during opening or reading a file, the OSError
34exception is raised.
35
36If sys.stdin is used more than once, the second and further use will
37return no lines, except perhaps for interactive use, or if it has been
38explicitly reset (e.g. using sys.stdin.seek(0)).
39
40Empty files are opened and immediately closed; the only time their
41presence in the list of filenames is noticeable at all is when the
42last file opened is empty.
43
44It is possible that the last line of a file doesn't end in a newline
45character; otherwise lines are returned including the trailing
46newline.
47
48Class FileInput is the implementation; its methods filename(),
49lineno(), fileline(), isfirstline(), isstdin(), nextfile() and close()
50correspond to the functions in the module.  In addition it has a
51readline() method which returns the next input line, and a
52__getitem__() method which implements the sequence behavior.  The
53sequence must be accessed in strictly sequential order; sequence
54access and readline() cannot be mixed.
55
56Optional in-place filtering: if the keyword argument inplace=1 is
57passed to input() or to the FileInput constructor, the file is moved
58to a backup file and standard output is directed to the input file.
59This makes it possible to write a filter that rewrites its input file
60in place.  If the keyword argument backup=".<some extension>" is also
61given, it specifies the extension for the backup file, and the backup
62file remains around; by default, the extension is ".bak" and it is
63deleted when the output file is closed.  In-place filtering is
64disabled when standard input is read.  XXX The current implementation
65does not work for MS-DOS 8+3 filesystems.
66"""
67
68import io
69import sys, os
70from 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
78def 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
93def close():
94    """Close the sequence."""
95    global _state
96    state = _state
97    _state = None
98    if state:
99        state.close()
100
101def 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
115def 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
124def 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
134def 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
144def 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
153def 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
162def 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
171class 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
401def 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
420def hook_encoded(encoding, errors=None):
421    def openhook(filename, mode):
422        return open(filename, mode, encoding=encoding, errors=errors)
423    return openhook
424
425
426def _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
441if __name__ == '__main__':
442    _test()
443