File: mergeall-products/unzipped/launch-mergeall-Console.py

#!/usr/bin/python
"""
================================================================================
launch-mergeall-Console.py:
  shell console launcher (part of the mergeall system)

See UserGuide.html for version, license, platforms, and attribution.

Launch mergeall.py after inputting settings in console window (3.X and 2.X).
See also launch-mergeall-GUI.py for a GUI alternative to this console script.
The code here was experimental, and evolved with time; please pardon our dust.

Drag me out to a no-arguments shortcut on Windows to use hard-coded path
defaults in this script.  Or, pass 1 argument = name of path defaults file
in "launch-config" subdir of this directory, having from/to/log paths on
individual lines (though always asks about defaults anyhow).  Examples:

  launch-mergeall.py                                    (hardcoded defaults)
  launch-mergeall.py launch-configs\tablet-upload.txt   (tablet upload dflts)
  launch-mergeall.py launch-configs\tablet-download.txt (tablet download dflts)

TBD: see notes ahead on stdout stream input prompts and interactive mode;
until resolved, this precludes log files in interactive mergeall runs.

TBD: this is largely 2.X compatible, but may still have issues in stream
decoding for non-ASCII filenames.  This remains suggested exercise for now.

This script was patched in 1.6 for a Python 2.X Unicode issue described in the 
GUI launcher's file.  Still, because this script routes text to a console, you 
must generally set PYTHNIOENCODING in your shell for non-ASCII filenames, 
especially in 3.X; see UserGuide.html, docs/Whitepaper.html, and mergeall.py.

[2.0] This script was updated for the new '-backup' option added in release
2.0, though the GUI launcher is much more commonly used.  Unlike the GUI,
which does -report and -auto but no interactively-selected changes, arg
-backup here can apply to either -auto, or manual changes for [not -report].
Aso added note about new findings for spawnee interactive prompts issues.

[3.0] This script was updated for all the new modes, options, and porting
changes of this release, along with the GUI launcher.  See this version's
changes list in docs/Revisions.html for details.

[3.2] This script still works, but was largely subsumed by the later GUI 
launcher, and its selective-updates mode hasn't been used live in years. 
It should probably be dropped someday, but your mileage may vary.
================================================================================
"""

from __future__ import print_function         # 2.X
import sys, os, time, webbrowser, subprocess

# [3.0] for frozen app/exes, fix module+resource visibility (sys.path)
import fixfrozenpaths

RunningOnMac     = sys.platform.startswith('darwin')
RunningOnWindows = sys.platform.startswith('win')
RunningOnLinux   = sys.platform.startswith('linux')

if sys.version[0] == '2':                     # 2.X
    input = raw_input
    #import codecs
    #open = codecs.open    # [1.4] log binary mode from stream, not text files

# [1.4] how spawned mergeall subproc's text is written and decoded here
STREAM_ENCODE = 'utf8'

# [1.6] display version number initially
from __version__ import VERSION               # [3.2] don't hardcode
print('mergeall %.1f' % VERSION)

# [3.0] user configs: logfile editor popup only (no GUI here)
try:
    from mergeall_configs import LOGEDITORPOPUP
except Exception as why:
    LOGEDITORPOPUP = True   # default: show a saved logfile in text editor too


################################################################################
# initial defaults: change here, or via user input (use C:\... for main drive);
# logfile names include '-yymmdd-hhmmss' to make unique and retain old versions
################################################################################


test = True
if test:
    # default to shipped test dirs
    frompath = 'test' + os.sep + 'test1'            # [3.0] portable to Win+Unix
    topath   = 'test' + os.sep + 'test2'
    try:
        # try Windows user's desktop
        logpath = r'C:\Users\%s\Desktop' % os.environ['username']
        assert os.path.exists(logpath)
    except:
        try:
            # [3.0] try same on Linux and Mac OS X
            logpath = os.path.join(os.environ['HOME'], 'Desktop')
            assert os.path.exists(logpath)
        except:
            logpath = '<none>'
else:
    # old testing code: now unused 
    frompath = r'D:\YOUR-STUFF'                     # e.g., sd card in win81 tablet
    topath   = r'E:\YOUR-STUFF'                     # e.g., terabyte backup drive
    logpath  = frompath + r'\admin\mergeall-logs'   # creates a diff if in from/to!

# cmd arg? => filename, having from/to/log defaults on separate lines
if len(sys.argv) > 1:
    defaults = open(sys.argv[1])
    frompath = defaults.readline().strip()     # assume okay, drop /n at end
    topath   = defaults.readline().strip() 
    logpath  = defaults.readline().strip()

datestamp   = time.strftime('date%y%m%d-time%H%M%S')
logfilepath = logpath + os.sep + 'mergeall-%s.txt' % (datestamp)


################################################################################
# interact to get settings
################################################################################


def yes(prompt, hint=' (y=yes): '):
    return input(prompt + hint).lower() in ['y', 'yes']

print('\nFROM path = "%s"' % frompath)
if not yes('use this?'):
    frompath = input('enter new FROM path: ')

print('\nTO path = "%s"' % topath)
if not yes('use this?'):
    topath = input('enter new TO path: ')

# script in same cwd here so no abs path needed, don't support -peek or -verify 
if yes('\nReport differences only?'):
    argmode = '-report'                         # report differences and stop
elif yes('Automatically resolve differences in TO (else asks)?'):
    argmode = '-auto'                           # run in-place updates to sync auto
else:
    argmode = ''                                # ask in console before each update

dolog = argmode and yes('\nSave output to log file too?')    # iff non-interactive
if dolog:
    print('Log file path = "%s"' % logfilepath)
    if not yes('use this?'):
       logfilepath = input('enter new Log file path: ')

# [2.0] new auto-backup option, applies to both -auto and manual changes here
dobkp = (argmode != '-report') and yes('\nBackup changes made in TO?')

# [2.4] quiet mode, omit per-file backup messages (show just one indicator)
if dobkp:
    doquiet = yes('Suppress per-file backup messages?')

# [3.0] skip system cruft files mode (all run modes)
# GUI's 3.0 "skip compare messages" moot here: console scroll is fast
docruft = yes('Skip system cruft files in both FROM and TO?')


################################################################################
# spawn mergeall, interact or capture output+errors
################################################################################

#-----------------------------------------------------------------------------
# TBD: interactive mode prompts issue (and cause for two spawn schemes below):
# subproc's stdin will inherit stdin here (the console) by default, but
# stream linebuffering in Python 3.X text mode may hide subproc's input()
# prompts till the next \n.  Binary mode streams can be unbuffered and may
# suffice for ascii prompts, but may fail for showing multibyte characters
# in Unicode filenames (if any are present in subject trees).  This seems
# to preclude catching stdout of interactive scripts in 3.X in general,
# and may be another Unicode "Catch22": input() prompts with no \n won't
# appear in intercepted subprocess streams until _after_ text is entered.
#
# Update:
# See also the "2.0 UPDATE" ahead.  Reading by bytes with unbufferred
# binary mode streams and stdout flushes works, but seems to preclude
# decoding Unicode to text for display -- individual bytes can't be decoded,
# there's no way to know when a non \n-terminated print string is complete,
# and eolns may be problematic, as their bytes may be part of other chars.
# stdin=sys.stdin is not needed: no-op, as descriptor inherited normally.
# Tools like PyExpect may not apply here: Python 3.X doesn't use C's IO lib. 
#-----------------------------------------------------------------------------


# verify: show settings, confirm run
argbkp = ' -backup' if dobkp else ''
if dobkp: argbkp += ' -quiet' if doquiet else ''   # [2.4]
argcruft = ' -skipcruft' if docruft else ''        # [3.0]

print('\nReady to run:')
print('\tcmd = "mergeall.py %s %s %s%s%s"' %
                 (frompath, topath, argmode, argbkp, argcruft))
print('\tlog = %s' %
                 ('"%s"' % logfilepath if dolog else '(none)'))

# not quite as descriptive as GUI, but suffices
if argmode in ['-auto', '']:
    print('\n*WARNING*: this may change your TO directory permanently and ')
    print('irrevocably, by adding, replacing, and deleting files and folders.\n')

if not yes('Proceed?'):                              
    input('Not run (press Enter to close)')   # keep console open on Windows
    sys.exit(0)


# [2.0] these would work in interactive mode, and could pass to mergeall in
# -auto mode because it doesn't start interactive help() for non-tty, but
# catch path here to avoid displaying command-line doc from mergeall;

if not os.path.exists(frompath):
    input('**Bad FROM path: run cancelled (press Enter to close)')
    sys.exit(1)
if not os.path.exists(topath):
    input('**Bad TO path: run cancelled (press Enter to close)')
    sys.exit(1)


#-----------------------------------------------------------------------------
# [1.2] make subproc's streams encoding match Popen expectation (Win=cp1252);
# could instead use utf8 here, if binary streams + manual utf8 line decode;
# required only if PYTHONIOENCODING set in shell (mine is) -- Popen ignores
# this and uses locale, and has no direct way to name stream encoding (why??);
#
# import locale
# os.environ['PYTHONIOENCODING'] = locale.getpreferredencoding(False)  # inherited
#
# [1.4] Unicode stream encoding, take 2: need to use UTF8 for prints in mergeall
# subproc, and binary mode Popen reads + manual decoding here; Popen's locale
# encoding works for reading the stream, but not for prints within mergeall;
#
# [3.2] PYTHONIOENCODING is not moot in py3.6+.  3.6 made the Windows console
# itself UTF8, by stdout redirected to a file is still the Windows code page.
#-----------------------------------------------------------------------------

os.environ['PYTHONIOENCODING'] = STREAM_ENCODE      # inherited (moot in py3.6?)


#-----------------------------------------------------------------------------
# INTERACTIVE MODE SPAWN (not [-auto or -report]):
#
# no log, run in same console so tty streams are shared (else prompts can be
# problematic);  file path error prompts from mergeall work correctly in this
# mode, but pre-check to avoid displaying mergeall's command-line help [2.0];
#-----------------------------------------------------------------------------

# [3.0] data+scripts not in os.getcwd() if run from a cmdline elsewhere,
# and __file__ may not work if running as a frozen PyInstaller executable;
# use __file__ of this file for Mac apps, not module: it's in a zipfile;

launcherpath = fixfrozenpaths.fetchMyInstallDir(__file__)   # absolute

# [3.0] allow for frozen executables
if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):
    # pyinstaller exe [3.0]
    freezename = 'mergeall.exe' if RunningOnWindows else 'mergeall'
    mergeallpath = os.path.join(launcherpath, freezename)
    mergeallspwn = [mergeallpath]
    cmdpath = mergeallpath
else:
    # py2app Mac app or source (original code)
    mergeallpath = os.path.join(launcherpath, 'mergeall.py')
    mergeallspwn = ['python', mergeallpath]
    cmdpath = sys.executable

if argmode == '':
    # former caveat: script not in os.getcwd() if run elsewhere (see gui)
    # assume args do not need to be manually quoted in the cmd sequence
    cmdseq = mergeallspwn + [frompath, topath]
    if dobkp:
        cmdseq += ['-backup']                 # [2.0] for interactive changes too
        if doquiet:
            cmdseq += ['-quiet']              # [2.4] ditto, but only if -backup
    if docruft:
        cmdseq += ['-skipcruft']              # [3.0] for '', -report, or -auto
    os.spawnv(os.P_WAIT, cmdpath, cmdseq)     # interact in this console
    input('Done (press Enter to close)')      # keep console open on Windows
    sys.exit(0) 


#-----------------------------------------------------------------------------
# NON-INTERACTIVE MODE SPAWN (-auto or -report):
#
# run command-line in separate process, read/show/save output;  requires more
# control over streams here than 3.X's os.popen/spawnv provide;  uses python
# -u so sub's stdout stream is unbufferred, and hence not delayed;  other
# Popen ideas explored...
#     universal_newlines=True,     # [1.2] text mode streams, auto decode
#     stdin=sys.stdin)             # provide sub's stdin here?: inherited
#
# [1.4] change: drop -u here, as it makes eolns \n in 2.X but \r\n in 3.X;
# UPDATE: '-u' unbuffered flag reinstated, else 10+ second output delay
# for some devices; this also requires linebreak mapping to handle the
# 2.X/3.X difference; see GUI launcher's 1.4 change log for more details.
#
# [2.0] UPDATE note: this spawning mode _does_ support interactive prompts,
# if stdout is unbuffered and binary, output is read by bytes instead of
# lines, and this process's own stdout is flushed after each character:
#
# while True:
#     byte = subproc.stdout.read(1)       
#     if not byte: break
#     sys.stdout.write(byte.decode())     # but to which scheme?
#     sys.stdout.flush()
#
# However, it's not clear that this can support multi-byte Unicode filename
# chracters in the output (what would the console do with their individual
# bytes?), and this may make it difficult to normalize 2.X/3.X endline
# characters, as their raw undecoded bytes may be part of another multibyte
# Unicode character.  Reading by bytes is also slower, though perhaps not
# significantly so to an interactive user.  Keep two spawn schemes for now.
#-----------------------------------------------------------------------------

# allow non-ascii filenames in my log file text: write chars as utf8:
# logfile = open(logfilepath, 'w', encoding=STREAM_ENCODE)
# [1.4] use binary mode from stream: 2.X codecs.open doesn't expand \n;

if dolog:
    try:
        logfile = open(logfilepath, 'wb')
    except:
        # [2.0] not exception text
        input('**Bad log-file path: run cancelled (press Enter to close)')
        sys.exit(1)

# former caveat: script not in os.getcwd() if run elsewhere (see gui)
extras = {}
if hasattr(sys, 'frozen') and (RunningOnWindows or RunningOnLinux):
    # pyinstaller exe [3.0]
    cmdseq = [mergeallpath, frompath, topath]
    if RunningOnWindows:
        # else spawn hangs unless launcher uses --console (with popup!)
        extras = dict(stdin=subprocess.DEVNULL)
else:
    # py2app Mac app or source (original code)
    cmdseq = [sys.executable, '-u', mergeallpath, frompath, topath]    # [1.4] need '-u'

if argmode:
    cmdseq.append(argmode)       # else fails if empty and not special-cased above (but is)
if dobkp:
    cmdseq.append('-backup')     # [2.0] not added for -report (but harmless if is)
if dobkp and doquiet:
    cmdseq.append('-quiet')      # [2.4] not added (or asked) unless -backup
if docruft:
    cmdseq.append('-skipcruft')  # [3.0] added (and asked) for both -report and -auto


# [1.5] shell should be True on Windows so that it uses filename associations,
# but False on Linux so that it doesn't just start a python" interactive shell;
# uses a command sequence (not string): args are auto-quoted by subprocess;
doshell = sys.platform.startswith('win') 

subproc = subprocess.Popen(
              cmdseq,                     # a string cmd may fail on Unix
              shell=doshell,              # [1.5] see note above, platform specific
              universal_newlines=False,   # [1.4] binary mode, manual decode/eoln
              stdout=subprocess.PIPE,     # capture sub's stdout here
              stderr=subprocess.STDOUT,   # route sub's stderr to its stdout
              **extras)

# read sub's output
# no need to thread this here (this is not a GUI: okay to block)

for binline in subproc.stdout:            # read stdout+stderr lines
    try:
        line = binline.decode(STREAM_ENCODE)       # [1.4] manual decode here, match subproc
    except UnicodeDecodeError:
        line = b'(UNDECODABLE LINE): ' + binline   # [1.6] 2.X fix--see details in GUI launcher
    print(line.rstrip())                           # scroll to console (assume can handle)
    
    if dolog:                                      # also save to log file; [1.4]: binary
        eoln = os.linesep.encode()                 # must be bytes in 3.X (no-op in 2.X)
        binline = binline.replace(b'\r\n', b'\n')  # [1.4]: got just \n from '-u' in 2.X only
        binline = binline.replace(b'\n', eoln)     # replaces are no-op in 3.X and unix 
        logfile.write(binline)

# mergeall exited: close/show log file if logging
if dolog:
    logfile.close()
    if LOGEDITORPOPUP:
        # [3.0] Mac OS X is pickier about file URLs
        if sys.platform.startswith('darwin'):
            logfilepath = 'file:' + os.path.abspath(logfilepath)
        webbrowser.open(logfilepath)   # popup, uses start=Notepad on Windows
    else:
        print('See log file in logs folder.')

input('Done (press Enter to close)')   # keep console open on Windows till Enter



[Home page] Books Code Blog Python Author Train Find ©M.Lutz