[Python-ideas] Iterating non-newline-separated files should be easier

Akira Li 4kir4.1i at gmail.com
Wed Jul 23 14:13:06 CEST 2014


Andrew Barnert <abarnert at yahoo.com> writes:

> On Jul 22, 2014, at 9:05, Akira Li <4kir4.1i at gmail.com> wrote:
>
>> Paul Moore <p.f.moore at gmail.com> writes:
>>
>>> On 21 July 2014 01:41, Andrew Barnert
>>> <abarnert at yahoo.com.dmarc.invalid> wrote:
>>>> OK, I wrote up a draft PEP, and attached it to the bug (if that's
>>>> not a good thing to do, apologies); you can find it at
>>>> http://bugs.python.org/file36008/pep-newline.txt
>>>
>>> As a suggestion, how about adding an example of a simple nul-separated
>>> filename filter - the sort of thing that could go in a find -print0 |
>>> xxx | xargs -0 pipeline? If I understand it, that's one of the key
>>> motivating examples for this change, so seeing how it's done would be
>>> a great help.
>>>
>>> Here's the sort of thing I mean, written for newline-separated files:
>>>
>>> import sys
>>>
>>> def process(filename):
>>>    """Trivial example"""
>>>    return filename.lower()
>>>
>>> if __name__ == '__main__':
>>>
>>>    for filename in sys.stdin:
>>>        filename = process(filename)
>>>        print(filename)
>>>
>>> This is also an example of why I'm struggling to understand how an
>>> open() parameter "solves all the cases". There's no explicit open()
>>> call here, so how do you specify the record separator? Seeing how you
>>> propose this would work would be really helpful to me.
>>
>> `find -print0 | ./tr-filename -0 | xargs -0` example implies that you
>> can replace `sys.std*` streams without worrying about preserving
>> `sys.__std*__` streams:
>>
>>  #!/usr/bin/env python
>>  import io
>>  import re
>>  import sys
>>  from pathlib import Path
>>
>>  def transform_filename(filename: str) -> str: # example
>>      """Normalize whitespace in basename."""
>>      path = Path(filename)
>>      new_path = path.with_name(re.sub(r'\s+', ' ', path.name))
>>      path.replace(new_path) # rename on disk if necessary
>>      return str(new_path)
>>
>>  def SystemTextStream(bytes_stream, **kwargs):
>>      encoding = sys.getfilesystemencoding()
>>      return io.TextIOWrapper(bytes_stream,
>>          encoding=encoding,
>>          errors='surrogateescape' if encoding != 'mbcs' else 'strict',
>>          **kwargs)
>>
>>  nl = '\0' if '-0' in sys.argv else None
>>  sys.stdout = SystemTextStream(sys.stdout.detach(), newline=nl)
>>  for line in SystemTextStream(sys.stdin.detach(), newline=nl):
>>      print(transform_filename(line.rstrip(nl)), end=nl)
>
> Nice, much more complete example than mine. I just tried to handle as
> many edge cases as the original he asked about, but you handle
> everything.
>
>> io.TextIOWrapper() plays the role of open() in this case. The code
>> assumes that `newline` parameter accepts '\0'.
>>
>> The example function handles Unicode whitespace to demonstrate why
>> opaque bytes-based cookies can't be used to represent filenames in this
>> case even on POSIX, though which characters are recognized depends on
>> sys.getfilesystemencoding().
>>
>> Note:
>>
>> - `end=nl` is necessary because `print()` prints '\n' by default -- it
>>  does not use `file.newline`
>
> Actually, yes it does. Or, rather, print pastes on a '\n', but
> sys.stdout.write translates any '\n' characters to sys.stdout.writenl
> (a private variable that's initialized from the newline argument at
> construction time if it's anything other than None or '').

You are right. I've stopped reading the source for print() function at
`PyFile_WriteString("\n", file);` line assuming that "\n" is not
translated if newline="\0". But the current behaviour if "\0" were in
"the other legal values" category (like "\r") would be to translate "\n"
[1]:

  When writing output to the stream, if newline is None, any '\n'
  characters written are translated to the system default line
  separator, os.linesep. If newline is '' or '\n', no translation takes
  place. If newline is any of the other legal values, any '\n'
  characters written are translated to the given string.

[1] https://docs.python.org/3/library/io.html#io.TextIOWrapper

Example:

  $ ./python -c 'import sys, io;
  sys.stdout=io.TextIOWrapper(sys.stdout.detach(), newline="\r\n");
  sys.stdout.write("\n\r\r\n")'| xxd
  0000000: 0d0a 0d0d 0d0a                           ......

"\n" is translated to b"\r\n" here and "\r" is left untouched (b"\r").

In order to newline="\0" case to work, it should behave similar to
newline='' or newline='\n' case instead i.e., no translation should take
place, to avoid corrupting embed "\n\r" characters. My original code
works as is in this case i.e., *end=nl is still necessary*.

> But of course that's the newline argument to sys.stdout, and you only
> changed sys.stdin, so you do need end=nl anyway. (And you wouldn't
> want output translation here anyway, because that could also translate
> \n' characters in the middle of a line, re-creating the same problem
> we're trying to avoid...)
>
> But it uses sys.stdout.newline, not sys.stdin.newline.

The code affects *both* sys.stdout/sys.stdin. Look [2]:

>>  sys.stdout = SystemTextStream(sys.stdout.detach(), newline=nl)
>>  for line in SystemTextStream(sys.stdin.detach(), newline=nl):
>>      print(transform_filename(line.rstrip(nl)), end=nl)

[2] https://mail.python.org/pipermail/python-ideas/2014-July/028372.html

>> - SystemTextStream() handles undecodable in the current locale filenames
>>  i.e., non-ascii names are allowed even in C locale (LC_CTYPE=C)
>> - undecodable filenames are not supported on Windows. It is not clear
>>  how to pass an undecodable filename via a pipe on Windows -- perhaps
>>  `GetShortPathNameW -> fsencode -> pipe` might work in some cases. It
>>  assumes that the short path exists and it is always encodable using
>>  mbcs. If we can control all parts of the pipeline *and* Windows API
>>  uses proper utf-16 (not ucs-2) then utf-8 can be used to pass
>>  filenames via a pipe otherwise ReadConsoleW/WriteConsoleW could be
>>  tried e.g., https://github.com/Drekin/win-unicode-console
>
> First, don't both the Win32 APIs and the POSIX-ish layer in msvcrt on
> top of it guarantee that you can never get such unencodable filenames
> (sometimes by just pretending the file doesn't exist, but if possible
> by having the filesystem map it to something valid, unique, and
> persistent for this session, usually the short name)?
> Second, trying to solve this implies that you have some other native
> (as opposed to Cygwin) tool that passes or accepts such filenames over
> simple pipes (as opposed to PowerShell typed ones). Are there any?
> What does, say, mingw's find do with invalid filenames if it finds
> them?

In short: I don't know :)

To be clear, I'm talking about native Windows applications (not
find/xargs on Cygwin). The goal is to process robustly *arbitrary*
filenames on Windows via a pipe (SystemTextStream()) or network (bytes
interface).

I know that (A)nsi API (and therefore "POSIX-ish layer" that uses narrow
strings such main(), fopen(), fstream is broken e.g., Thai filenames on
Greek computer [3]. Unicode (W) API should enforce utf-16 in principle
since Windows 2000 [4]. But I expect ucs-2 shows its ugly head in many
places due to bad programming practices (based on the common wrong
assumption that Unicode == UTF-16 == UCS-2) and/or bugs that are not
fixed due to MS' backwards compatibility policies in the past [5].

[3]
http://blog.gatunka.com/2014/04/25/character-encodings-for-modern-programmers/
[4] http://en.wikipedia.org/wiki/UTF-16#Use_in_major_operating_systems_and_environments
[5] http://blogs.msdn.com/b/oldnewthing/archive/2003/10/15/55296.aspx


--
Akira


More information about the Python-ideas mailing list