]> arthur.barton.de Git - bup.git/commitdiff
Migrate ftp, etc. to our _helpers bytes-oriented readline
authorRob Browning <rlb@defaultvalue.org>
Wed, 3 Jun 2020 06:34:26 +0000 (01:34 -0500)
committerRob Browning <rlb@defaultvalue.org>
Sun, 21 Jun 2020 16:20:58 +0000 (11:20 -0500)
This allows us to preserve the status quo for now, even without the
LC_CTYPE override, i.e. binary stdin/stdout, etc.

Signed-off-by: Rob Browning <rlb@defaultvalue.org>
lib/bup/shquote.py
lib/bup/t/tshquote.py
lib/cmd/ftp-cmd.py
t/test-ftp

index d7e451aa45fe007859cab765ebd0af752cc58470..829f3f3477e26225e1d460249965307eeaf7a1dd 100644 (file)
@@ -2,8 +2,10 @@
 from __future__ import absolute_import
 import re
 
-q = "'"
-qq = '"'
+from bup.compat import bytes_from_byte
+
+q = b"'"
+qq = b'"'
 
 
 class QuoteError(Exception):
@@ -14,31 +16,31 @@ def _quotesplit(line):
     inquote = None
     inescape = None
     wordstart = 0
-    word = ''
+    word = b''
     for i in range(len(line)):
-        c = line[i]
+        c = bytes_from_byte(line[i])
         if inescape:
             if inquote == q and c != q:
-                word += '\\'  # single-q backslashes can only quote single-q
+                word += b'\\'  # single-q backslashes can only quote single-q
             word += c
             inescape = False
-        elif c == '\\':
+        elif c == b'\\':
             inescape = True
         elif c == inquote:
             inquote = None
             # this is un-sh-like, but do it for sanity when autocompleting
             yield (wordstart, word)
-            word = ''
+            word = b''
             wordstart = i+1
         elif not inquote and not word and (c == q or c == qq):
             # the 'not word' constraint on this is un-sh-like, but do it
             # for sanity when autocompleting
             inquote = c
             wordstart = i
-        elif not inquote and c in [' ', '\n', '\r', '\t']:
+        elif not inquote and c in [b' ', b'\n', b'\r', b'\t']:
             if word:
                 yield (wordstart, word)
-            word = ''
+            word = b''
             wordstart = i+1
         else:
             word += c
@@ -55,9 +57,9 @@ def quotesplit(line):
     backslash escapes.
 
     Note that this implementation isn't entirely sh-compatible.  It only
-    dequotes words that *start* with a quote character, that is, a string like
+    dequotes words that *start* with a quote character, that is, bytes like
        hello"world"
-    will not have its quotes removed, while a string like
+    will not have its quotes removed, while bytes like
        hello "world"
     will be turned into [(0, 'hello'), (6, 'world')] (ie. quotes removed).
     """
@@ -78,7 +80,7 @@ def unfinished_word(line):
     to read more bytes first.
 
     Args:
-      line: an input string
+      line: bytes
     Returns:
       quotechar,word: the initial quote char (or None), and the partial word.
     """
@@ -86,39 +88,38 @@ def unfinished_word(line):
         for (wordstart,word) in _quotesplit(line):
             pass
     except QuoteError:
-        firstchar = line[wordstart]
+        firstchar = bytes_from_byte(line[wordstart])
         if firstchar in [q, qq]:
             return (firstchar, word)
         else:
             return (None, word)
     else:
-        return (None, '')
-
+        return (None, b'')
 
 def quotify(qtype, word, terminate):
-    """Return a string corresponding to given word, quoted using qtype.
+    """Return a bytes corresponding to given word, quoted using qtype.
 
-    The resulting string is dequotable using quotesplit() and can be
-    joined with other quoted strings by adding arbitrary whitespace
+    The resulting bytes are dequotable using quotesplit() and can be
+    joined with other quoted bytes by adding arbitrary whitespace
     separators.
 
     Args:
       qtype: one of '', shquote.qq, or shquote.q
-      word: the string to quote.  May contain arbitrary characters.
+      word: the bytes to quote.  May contain arbitrary characters.
       terminate: include the trailing quote character, if any.
     Returns:
-      The quoted string.
+      The quoted bytes.
     """
     if qtype == qq:
-        return qq + word.replace(qq, '\\"') + (terminate and qq or '')
+        return qq + word.replace(qq, b'\\"') + (terminate and qq or b'')
     elif qtype == q:
-        return q + word.replace(q, "\\'") + (terminate and q or '')
+        return q + word.replace(q, b"\\'") + (terminate and q or b'')
     else:
-        return re.sub(r'([\"\' \t\n\r])', r'\\\1', word)
+        return re.sub(br'([\"\' \t\n\r])', br'\\\1', word)
 
 
 def quotify_list(words):
-  """Return a minimally-quoted string produced by quoting each word.
+  """Return minimally-quoted bytes produced by quoting each word.
 
   This calculates the qtype for each word depending on whether the word
   already includes singlequote characters, doublequote characters, both,
@@ -127,17 +128,17 @@ def quotify_list(words):
   Args:
     words: the list of words to quote.
   Returns:
-    The resulting string, with quoted words separated by ' '.
+    The resulting bytes, with quoted words separated by ' '.
   """
   wordout = []
   for word in words:
     qtype = q
-    if word and not re.search(r'[\s\"\']', word):
-      qtype = ''
+    if word and not re.search(br'[\s\"\']', word):
+      qtype = b''
     elif q in word and qq not in word:
       qtype = qq
     wordout.append(quotify(qtype, word, True))
-  return ' '.join(wordout)
+  return b' '.join(wordout)
 
 
 def what_to_add(qtype, origword, newword, terminate):
@@ -148,7 +149,7 @@ def what_to_add(qtype, origword, newword, terminate):
        terminate=False: 'ston'
        terminate=True:  'ston\"'
 
-    This is useful when calculating tab completion strings for readline.
+    This is useful when calculating tab completions for readline.
 
     Args:
       qtype: the type of quoting to use (ie. the first character of origword)
@@ -157,10 +158,10 @@ def what_to_add(qtype, origword, newword, terminate):
         origword.
       terminate: true if we should add the actual quote character at the end.
     Returns:
-      The string to append to origword to produce (quoted) newword.
+      The bytes to append to origword to produce (quoted) newword.
     """
     if not newword.startswith(origword):
-        return ''
+        return b''
     else:
         qold = quotify(qtype, origword, terminate=False)
         return quotify(qtype, newword, terminate=terminate)[len(qold):]
index f17346df40d7b0286560a406aebdae09aba3e373..8c85d4bc10f2bd69fb01122ef7dfed86a23caa7d 100644 (file)
@@ -13,42 +13,43 @@ def qst(line):
 @wvtest
 def test_shquote():
     with no_lingering_errors():
-        WVPASSEQ(qst("""  this is    basic \t\n\r text  """),
-                 ['this', 'is', 'basic', 'text'])
-        WVPASSEQ(qst(r""" \"x\" "help" 'yelp' """), ['"x"', 'help', 'yelp'])
-        WVPASSEQ(qst(r""" "'\"\"'" '\"\'' """), ["'\"\"'", '\\"\''])
-
-        WVPASSEQ(shquote.quotesplit('  this is "unfinished'),
-                 [(2,'this'), (7,'is'), (10,'unfinished')])
-
-        WVPASSEQ(shquote.quotesplit('"silly"\'will'),
-                 [(0,'silly'), (7,'will')])
-
-        WVPASSEQ(shquote.unfinished_word('this is a "billy" "goat'),
-                 ('"', 'goat'))
-        WVPASSEQ(shquote.unfinished_word("'x"),
-                 ("'", 'x'))
-        WVPASSEQ(shquote.unfinished_word("abra cadabra "),
-                 (None, ''))
-        WVPASSEQ(shquote.unfinished_word("abra cadabra"),
-                 (None, 'cadabra'))
-
-        (qtype, word) = shquote.unfinished_word("this is /usr/loc")
-        WVPASSEQ(shquote.what_to_add(qtype, word, "/usr/local", True),
-                 "al")
-        (qtype, word) = shquote.unfinished_word("this is '/usr/loc")
-        WVPASSEQ(shquote.what_to_add(qtype, word, "/usr/local", True),
-                 "al'")
-        (qtype, word) = shquote.unfinished_word("this is \"/usr/loc")
-        WVPASSEQ(shquote.what_to_add(qtype, word, "/usr/local", True),
-                 "al\"")
-        (qtype, word) = shquote.unfinished_word("this is \"/usr/loc")
-        WVPASSEQ(shquote.what_to_add(qtype, word, "/usr/local", False),
-                 "al")
-        (qtype, word) = shquote.unfinished_word("this is \\ hammer\\ \"")
-        WVPASSEQ(word, ' hammer "')
-        WVPASSEQ(shquote.what_to_add(qtype, word, " hammer \"time\"", True),
-                 "time\\\"")
-
-        WVPASSEQ(shquote.quotify_list(['a', '', '"word"', "'third'", "'", "x y"]),
-                 "a '' '\"word\"' \"'third'\" \"'\" 'x y'")
+        WVPASSEQ(qst(b"""  this is    basic \t\n\r text  """),
+                 [b'this', b'is', b'basic', b'text'])
+        WVPASSEQ(qst(br""" \"x\" "help" 'yelp' """), [b'"x"', b'help', b'yelp'])
+        WVPASSEQ(qst(br""" "'\"\"'" '\"\'' """), [b"'\"\"'", b'\\"\''])
+
+        WVPASSEQ(shquote.quotesplit(b'  this is "unfinished'),
+                 [(2, b'this'), (7, b'is'), (10, b'unfinished')])
+
+        WVPASSEQ(shquote.quotesplit(b'"silly"\'will'),
+                 [(0, b'silly'), (7, b'will')])
+
+        WVPASSEQ(shquote.unfinished_word(b'this is a "billy" "goat'),
+                 (b'"', b'goat'))
+        WVPASSEQ(shquote.unfinished_word(b"'x"),
+                 (b"'", b'x'))
+        WVPASSEQ(shquote.unfinished_word(b"abra cadabra "),
+                 (None, b''))
+        WVPASSEQ(shquote.unfinished_word(b"abra cadabra"),
+                 (None, b'cadabra'))
+
+        qtype, word = shquote.unfinished_word(b"this is /usr/loc")
+        WVPASSEQ(shquote.what_to_add(qtype, word, b"/usr/local", True),
+                 b"al")
+        qtype, word = shquote.unfinished_word(b"this is '/usr/loc")
+        WVPASSEQ(shquote.what_to_add(qtype, word, b"/usr/local", True),
+                 b"al'")
+        qtype, word = shquote.unfinished_word(b"this is \"/usr/loc")
+        WVPASSEQ(shquote.what_to_add(qtype, word, b"/usr/local", True),
+                 b"al\"")
+        qtype, word = shquote.unfinished_word(b"this is \"/usr/loc")
+        WVPASSEQ(shquote.what_to_add(qtype, word, b"/usr/local", False),
+                 b"al")
+        qtype, word = shquote.unfinished_word(b"this is \\ hammer\\ \"")
+        WVPASSEQ(word, b' hammer "')
+        WVPASSEQ(shquote.what_to_add(qtype, word, b" hammer \"time\"", True),
+                 b"time\\\"")
+
+        WVPASSEQ(shquote.quotify_list([b'a', b'', b'"word"', b"'third'", b"'",
+                                       b"x y"]),
+                 b"a '' '\"word\"' \"'third'\" \"'\" 'x y'")
index 53b8c222dac1829ecbdc9c5f865fa501e2161cc9..30e523de940f5041e4282e9434e44ebfb2f32b32 100755 (executable)
@@ -13,8 +13,8 @@ exec "$bup_python" "$0" ${1+"$@"}
 from __future__ import absolute_import, print_function
 import sys, os, stat, fnmatch
 
-from bup import options, git, shquote, ls, vfs
-from bup.compat import argv_bytes, input
+from bup import _helpers, options, git, shquote, ls, vfs
+from bup.compat import argv_bytes
 from bup.helpers import chunkyreader, handle_ctrl_c, log
 from bup.io import byte_stream, path_msg
 from bup.repo import LocalRepo
@@ -26,10 +26,6 @@ class OptionError(Exception):
     pass
 
 
-def input_bytes(s):
-    return s.encode('iso-8859-1')
-
-
 def do_ls(repo, args, out):
     try:
         opt = ls.opts_from_cmdline(args, onabort=OptionError)
@@ -45,21 +41,21 @@ def write_to_file(inf, outf):
 
 
 def inputiter():
-    if os.isatty(sys.stdin.fileno()):
+    if os.isatty(stdin.fileno()):
         while 1:
             try:
-                yield input('bup> ')
+                yield _helpers.readline(b'bup> ')
             except EOFError:
                 print()  # Clear the line for the terminal's next prompt
                 break
     else:
-        for line in sys.stdin:
+        for line in stdin:
             yield line
 
 
 def _completer_get_subs(repo, line):
     (qtype, lastword) = shquote.unfinished_word(line)
-    dir, name = os.path.split(lastword.encode('iso-8859-1'))
+    dir, name = os.path.split(lastword)
     dir_path = vfs.resolve(repo, dir or b'/')
     _, dir_item = dir_path[-1]
     if not dir_item:
@@ -71,14 +67,23 @@ def _completer_get_subs(repo, line):
     return qtype, lastword, subs
 
 
+_attempt_start = None
+_attempt_end = None
+def attempt_completion(text, start, end):
+    global _attempt_start, _attempt_end
+    _attempt_start = start
+    _attempt_end = end
+    return None
+
 _last_line = None
 _last_res = None
-def completer(text, iteration):
+def enter_completion(text, iteration):
     global repo
+    global _attempt_end
     global _last_line
     global _last_res
     try:
-        line = readline.get_line_buffer()[:readline.get_endidx()]
+        line = _helpers.get_line_buffer()[:_attempt_end]
         if _last_line != line:
             _last_res = _completer_get_subs(repo, line)
             _last_line = line
@@ -90,12 +95,10 @@ def completer(text, iteration):
             leaf_name, leaf_item = res[-1]
             fullname = os.path.join(*(name for name, item in res))
             if stat.S_ISDIR(vfs.item_mode(leaf_item)):
-                ret = shquote.what_to_add(qtype, lastword,
-                                          fullname.decode('iso-8859-1') + '/',
+                ret = shquote.what_to_add(qtype, lastword, fullname + b'/',
                                           terminate=False)
             else:
-                ret = shquote.what_to_add(qtype, lastword,
-                                          fullname.decode('iso-8859-1'),
+                ret = shquote.what_to_add(qtype, lastword, fullname,
                                           terminate=True) + b' '
             return text + ret
     except Exception as e:
@@ -118,26 +121,24 @@ git.check_repo_or_die()
 
 sys.stdout.flush()
 out = byte_stream(sys.stdout)
+stdin = byte_stream(sys.stdin)
 repo = LocalRepo()
 pwd = vfs.resolve(repo, b'/')
 rv = 0
 
+
+
 if extra:
-    lines = extra
+    lines = (argv_bytes(arg) for arg in extra)
 else:
-    try:
-        import readline
-    except ImportError:
-        log('* readline module not available: line editing disabled.\n')
-        readline = None
-
-    if readline:
-        readline.set_completer_delims(' \t\n\r/')
-        readline.set_completer(completer)
+    if hasattr(_helpers, 'readline'):
+        _helpers.set_completer_word_break_characters(b' \t\n\r/')
+        _helpers.set_attempted_completion_function(attempt_completion)
+        _helpers.set_completion_entry_function(enter_completion)
         if sys.platform.startswith('darwin'):
             # MacOS uses a slightly incompatible clone of libreadline
-            readline.parse_and_bind('bind ^I rl_complete')
-        readline.parse_and_bind('tab: complete')
+            _helpers.parse_and_bind(b'bind ^I rl_complete')
+        _helpers.parse_and_bind(b'tab: complete')
     lines = inputiter()
 
 for line in lines:
@@ -147,13 +148,14 @@ for line in lines:
     cmd = words[0].lower()
     #log('execute: %r %r\n' % (cmd, parm))
     try:
-        if cmd == 'ls':
+        if cmd == b'ls':
             # FIXME: respect pwd (perhaps via ls accepting resolve path/parent)
             do_ls(repo, words[1:], out)
-        elif cmd == 'cd':
+            out.flush()
+        elif cmd == b'cd':
             np = pwd
             for parm in words[1:]:
-                res = vfs.resolve(repo, input_bytes(parm), parent=np)
+                res = vfs.resolve(repo, parm, parent=np)
                 _, leaf_item = res[-1]
                 if not leaf_item:
                     raise Exception('%s does not exist'
@@ -163,13 +165,14 @@ for line in lines:
                     raise Exception('%s is not a directory' % path_msg(parm))
                 np = res
             pwd = np
-        elif cmd == 'pwd':
+        elif cmd == b'pwd':
             if len(pwd) == 1:
                 out.write(b'/')
             out.write(b'/'.join(name for name, item in pwd) + b'\n')
-        elif cmd == 'cat':
+            out.flush()
+        elif cmd == b'cat':
             for parm in words[1:]:
-                res = vfs.resolve(repo, input_bytes(parm), parent=pwd)
+                res = vfs.resolve(repo, parm, parent=pwd)
                 _, leaf_item = res[-1]
                 if not leaf_item:
                     raise Exception('%s does not exist' %
@@ -177,13 +180,14 @@ for line in lines:
                                                        in res)))
                 with vfs.fopen(repo, leaf_item) as srcfile:
                     write_to_file(srcfile, out)
-        elif cmd == 'get':
+            out.flush()
+        elif cmd == b'get':
             if len(words) not in [2,3]:
                 rv = 1
                 raise Exception('Usage: get <filename> [localname]')
-            rname = input_bytes(words[1])
+            rname = words[1]
             (dir,base) = os.path.split(rname)
-            lname = input_bytes(len(words) > 2 and words[2] or base)
+            lname = len(words) > 2 and words[2] or base
             res = vfs.resolve(repo, rname, parent=pwd)
             _, leaf_item = res[-1]
             if not leaf_item:
@@ -193,9 +197,9 @@ for line in lines:
                 with open(lname, 'wb') as destfile:
                     log('Saving %s\n' % path_msg(lname))
                     write_to_file(srcfile, destfile)
-        elif cmd == 'mget':
+        elif cmd == b'mget':
             for parm in words[1:]:
-                dir, base = os.path.split(input_bytes(parm))
+                dir, base = os.path.split(parm)
 
                 res = vfs.resolve(repo, dir, parent=pwd)
                 _, dir_item = res[-1]
@@ -217,10 +221,10 @@ for line in lines:
                             with open(name, 'wb') as destfile:
                                 log('Saving %s\n' % path_msg(name))
                                 write_to_file(srcfile, destfile)
-        elif cmd == 'help' or cmd == '?':
-            # FIXME: move to stdout
-            log('Commands: ls cd pwd cat get mget help quit\n')
-        elif cmd in ('quit', 'exit', 'bye'):
+        elif cmd == b'help' or cmd == b'?':
+            out.write(b'Commands: ls cd pwd cat get mget help quit\n')
+            out.flush()
+        elif cmd in (b'quit', b'exit', b'bye'):
             break
         else:
             rv = 1
index e9f4166467b18e757d83f4c081a96ecd6c5bad22..aa4b0a5acdff80133ff9eca5c98d3f2df5759108 100755 (executable)
@@ -63,7 +63,7 @@ with test_tempdir(b'ftp-') as tmpdir:
     
     wvstart('help')
     wvpasseq(b'Commands: ls cd pwd cat get mget help quit\n',
-             exo((bup_cmd, b'ftp'), input=b'help\n', stderr=PIPE).err)
+             exo((bup_cmd, b'ftp'), input=b'help\n', stderr=PIPE).out)
 
     wvstart('pwd/cd')
     wvpasseq(b'/\n', bup(b'ftp', input=b'pwd\n').out)