docs: def complete_message(buf):
    "returns msg,buf_remaining or None,buf"
    # todo: read dollar-length for strings; I dont think I can blindly trust newlines. learn about escaping
    # note: all the length checks are +1 over what I need because I'm asking for *complete* lines.
    lines=buf.split('\r\n')
    if len(lines)<=1: return None,buf
    nargs_raw=lines.pop(0)
    assert nargs_raw[0]=='*'
    nargs=int(nargs_raw[1:])
    args=[]
    while len(lines)>=2: # 2 because if there isn't at least a blank at the end, we're missing a terminator
      if lines[0][0]=='+': args.append(lines.pop(0))
      elif lines[0][0]==':': args.append(int(lines.pop(0)[1:]))
      elif lines[0][0]=='$':
        if len(lines)<3: return None,buf
        slen,s=int(lines[0][1:]),lines[1]
        if slen!=len(s): raise ValueError('length mismatch %s %r'%(slen,s)) # probably an escaping issue
        lines=lines[2:]
        args.append(s)
      else: raise ValueError('expected initial code in %r'%lines)
    if len(args)==nargs: return args,'\r\n'.join(lines)
    else: return None,buf