. | . | . | . | David McCracken |
Python Examplesupdated:2018.05.14 |
Using Python requires no further justification than that it is available on practically all computers and affords an extraordinary range of general and specialized libraries. But the language itself is well designed. It is at once both theoretically interesting and practical. It borrows judiciously from other languages and introduces entirely new ideas only where justified. Instead of encouraging meaningless idiosyncratic variations, it provides different idioms clearly intended to support different circumstances.
One of Python’s theoretical strengths is in its use of anonymity to reduce scaffolding code, which exists not to express intent but because the language needs it to know what to do. For example, all new languages invented after ALGOL recognize functions as expressions whose value is whatever the function returns. There is no need to assign the return to a variable just to use it. At its inception LISP carried this “first-classness” to the extreme and was the first language to support lambda expressions.
Python is the first language to extend anonymity beyond LISP by giving
first-classness to object initialization lists. This alone establishes it as seminal
in the development of synthetic languages. But it is also very practical,
reducing programming coordination cost in a variety of circumstances. For
example, to process a command line in which -C, -B, -F, -R, and -S are valid
command options, we might use
cmd = 'CBFRS'.find(sys.argv[1][1:2].upper())
.
In any other language, the String initializer 'CBFRS'
would have
to be assigned to a superfluous variable before it could be used and that
assignment would typically be located far away from its one-time use.
Another of Python’s original ideas clearly motivated by practical
programming concerns as well as an appreciation of theoretical principles is a
variety of generalized block termination branches. For example, Python
generalizes else
as a block predicate for any block that
completes, including a loop that reaches its parameterized conclusion without
being conditionally terminated. In practical terms this means that, for
example, a loop searching for something can branch one way if it finds what it
is looking for and another if it doesn’t, avoiding an ad hoc duplicate
test while imparting greater structure to the code.
# File: dSup.py - list version (vs. deque) # ............................................................................ import sys, os, msvcrt ESC = b'\x1b' def indexNoCase(slist, s): for idx in range(len(slist)): if slist[idx].upper() == s.upper(): return idx raise ValueError # .................. main process ............................................ if len(sys.argv) < 2: cmd = 1 # No argument defaults to -B, the most common operation elif sys.argv[1][0] == '-': if len(sys.argv[1]) == 1: cmd = 2 # '-' alone defaults to -F, second most common operation. else: cmd = 'CBFRS'.find(sys.argv[1][1:2].upper()) else: cmd = -1 # User typed a target directory # cmd is -1 = path, 0 = C, 1 = B, 2 = F, 3 = R, 4 = S # fo = open(os.path.dirname(sys.argv[0]) + '\\dSupList', mode = 'a+t') fo = open( os.getenv('HOME', 'TEMP') + '\\dSupList', mode = 'a+t') fo.seek(0) dlist = fo.readlines(-1) olist = dlist[:] # Copy for checking list change. # If the directory list is new or for some other reason doesn't contain CWD # then add it. If the list exists and contains CWD but not at the head then # move it to the head. try: target = os.getcwd() + '\n' idx = indexNoCase(dlist, target) if idx != 0: del dlist[idx] dlist.insert(0, target) except ValueError: dlist.insert(0, target) if cmd == 1: # B: move backward, i.e. to previous, by rotate left. dlist.append(dlist.pop(0)) elif cmd == 2: # F: move forward, i.e. to next, by rotate right. dlist.insert(0, dlist.pop()) elif cmd == 3: # R: remove a dir from the list. if len(sys.argv) < 3: # -R w/o arg means to remove current from list. del dlist[0] else: try: dir = " ".join(sys.argv[2:]) del dlist[indexNoCase(dlist, dir + '\n')] except ValueError: print('"', dir, '" is not in the list', sep='') exit(2) elif cmd == 4: # S: select from list # The current directory (dlist[0]) is included in the list. From the user's # perspective this is essentially "do nothing" so we don't need to provide an # Esc option. for i, d in enumerate(dlist): print('(' + str(i) + ')', d, end = "") while True: inp = msvcrt.getche() # Windows-specific Enter-less key input. if inp == b'\r' or inp == ESC: inp = b'0' # Enter and ESC default to 0 if inp.isdigit(): inp = int(inp) if inp < len(dlist): print('') # Print the newline we didn't get from getche. break print(' is out of range') # Select 0 means the current directory and the list is not changed. Otherwise # the selected directory is moved to the head of the list. if inp > 0: dlist.insert(0, dlist.pop(inp)) elif cmd == -1: # Specified path e.g. d \CmdTools\Bak dir = os.path.abspath(" ".join(sys.argv[1:])) + '\n' # Note " ".join defines space (not empty) as delimiter try: del dlist[indexNoCase(dlist, dir)] except ValueError: pass dlist.insert(0, dir) # The list is limited to 10 items to enable selection by index 0-9. If CWD is # not in the list at opening, it is added. If the user requests a specific # directory not in the list, it is added. Consequently, the list at this point # may contain as many as two extra items. Chopping them off the right # eliminates the oldest directories in most cases. However, rotation due to # revisits can change time order. while len(dlist) > 10: dlist.pop() # If cmd is 0 (command -C to clear the list) or the list has been changed then # the file is truncated to nothing. For clearing it is left this way but for # changed list, the new dlist is written into it before closing. if cmd == 0 or olist != dlist: # print('The file is being changed') fo.truncate(0) if cmd != 0: fo.writelines(dlist) fo.close() exit(0)
See companion BAT script d.bat and equivalent Bash script cdx
When I’m working at the command line I often find that I have to visit
and revisit several directories, often with long path names, which I
don’t like having to type in the first place, let alone repeatedly.
Windows change directory command cd
is easy to remember but
doesn’t provide any traversal history. The
pushd directory
and popd
commands provide
some history but it is hard to remember to use them and they only provide
simple backward history, that is to retrace our traversal path. We can’t
reverse course and go forward though our history and there is no means of
randomly selecting a recent directory except by typing its full path.
In Linux cd
is as unhelpful as it is in Windows.
pushd directory
and popd
are somewhat more
powerful than their Windows counterparts but are so unfriendly that few people
bother to use them. My bash script cdx
replaces the standard cd
with my version, whose central feature is
a circular path list, supporting forward as well as backward traversal and
selection from a menu of recently visited directories. Because the Bash
language interpreter is native to the Bash shell, my script can change the
directory of the process that invokes it. Python is not native to Linux or
Windows and cannot do this.
When a Python script is invoked, a new process is created for the Python interpreter. It inherits the parent environment but cannot change it. The native command language in Windows has long been BAT and only that could change the environment. But BAT is too crude to implement the capability required of my improved cd. I resolved this by creating a simple BAT script, called d.bat, which invokes a Python script, dSup.py, which does most of the work. BAT is so primitive that it can’t accept a return value from the Python script, so dSup.py writes the directory traversal list to a file, which the BAT script can read. The file coincidentally confers on the list persistence across sessions. Even after reboot/shutdown the list remains and opening a command window can automatically change to the last directory that had previously been open.
The command is d [directory | -C | -B | -F | -R | -S]
.
d directory
changes the directory just like
cd directory
but it also silently pushes the departing
directory onto the head of the traversal list. -B or no argument changes to the
previous directory, i.e. goes backward. -F or simply - goes forward in the
traversal list. d
and d -
enable rapid movement
backward and forward through a sequence of directories. -S displays the
traversal list as a menu, enabling selecting any recently visited directory by
number (single key press). -C clears the traversal list. -R directory
removes the given directory from the list. -R alone removes the current
directory from the list and returns to the previous.
d.bat exists only because a Python script cannot change the user’s
directory. d.bat does as little as possible. It doesn’t interpret the
command line but simply passes it through to dSup.py. dSup.py reads the
traversal list file, pushes the target directory to the head, and writes the
list back to the file. The target directory is the first line of the file. When
Python returns, d.bat reads the first line of the same file and tries to cd to
it. If this fails, it reinvokes dSup.py with the argument -R causing the bad
directory to be removed from the list. If the user tries to change to a
directory that either doesn’t exist or that they are not allowed to
access, the response appears to be the same as for the cd
command.
It might seem that the means of handling a bad directory is unnecessarily complicated when the Python script could simply reject bad directories but there are two problems with this. One is that Windows doesn’t always provide complete access rights information, especially for remote (network) directories. And dSup.py has no way to communicate with d.bat other than through the traversal list file, complicating error handling. These are not insurmountable problems but their solution is more complicated than the try-remove approach I’ve taken.
The list is stored in the dSupList file, which is located in
%HOME%
. Each directory is stored on one line. The first is the
target destination used by d.bat after invoking dSup.py. It will become the
current (if not already) if the cd command succeeds. Otherwise, d.bat reinvokes
this script with the -R argument to request that the bad directory be removed.
-R also provides a means for the user to remove the current directory and
revert to the previous (-R) or to remove any directory in the list (-R
directory ). The requested directory is moved or inserted at the head
(left end) of the list, making the list a most-recent-first history.
The file is opened for both reading and writing. It is initially read into dlist to get the history. If the list becomes changed in command processing (including unpredictable interaction in the -S select option) it will be written back into the file, entirely replacing the contents. Otherwise, we just want to close the file. Rather than tracing the status of dlist, a copy is made before any other processing, after which, if dlist and the copy are still equal, the file is not written.
This is especially valuable for a specific but quite frequent scenario. The
command prompt shortcut used to open a command window from the GUI opens onto
whatever directory is specified in the link. This is generic and rarely where
we have been recently working. If we just If we just invoke d
at
that point, the directory will be changed to the most recent one and the list
does not need to change, saving time and reducing disk wear. This is so handy
that it is worthy of being a permanent part of any general command prompt link,
i.e. %SystemRoot%\system32\cmd.exe /K d.bat
.
If each visited directory were unique, moving backward and forward through the list would be a simple rotation left (backward) or right (forward). But the user may type or select a directory that is already in the list. Just inserting it at the head, creating a duplicate, could quickly fill the list with one or two dominant directories (imagine toggling between two several times). To avoid this the directory is either picked up and moved to the head or the list is rotated, positioning the directory at the head.
Rotation sounds good but in actual use is inferior because it promotes ancient history over more recent. e.g. traversal K-L-M-N-A-B-C creates list C-B-A-N-M-L-K. Then select M would change the list by rotation to M-L-K-C-B-A-N. Then backward move would go to L rather than C as the user would expect. If pick/insert were used instead the list would become M-C-B-A-N-L-K and moving backward would retrace more recent history C-B-A. The N-L context of M would be lost but this might reflect the user’s intent in picking it out of context.
To avoid duplications in the list, before an explicit (user-entered) directory
is inserted, any matching element of the list is removed. To avoid being
confused by aliases, e.g. ..\Bak from C:\Work\Src = C:\Work\Bak and irrelevant
case differences, the requested directory is always normalized to full path for
storing in the list and compare is case-insensitive. Therefore, we have to use
our own indexNoCase function to search the list instead of
list.index()
.
The option to select a directory from the traversal list supports single-key
input (0-9) using getche, a function available through msvcrt. Although the
equivalent functionality is complicated in Linux, it is simple in Windows. For
a simple universal interface this could be replaced by input()
which requires two key presses, the number followed by Enter.
The -R option tells to remove a directory from the list. If a directory name is given, the list is searched for that. Otherwise, the head (left end) of the list is removed. This is always the current directory. If d.bat tries and fails to go to the directory at the head then it recalls this script, passing -R with the name of the directory that failed. The bad directory is by definition at the head of the file list, but it must still be provided in the command line because this (dSup.py) script always moves or adds the current directory to the head before processing commands.
-R without directory is useful when the user goes to a directory and finds it useless. -R clears the record while returning to the previous. The user will most often use -R with a name to remove from the list directories that have been deleted from the system or that interfere with -B and -F. Alternatively, with the -S option, the bad directory can be selected; it will then fail and be automatically removed. If the list gets really bad, -C clears it completely (except for CWD).
If the command line specifies a path (rather than one of the - options) and the
path is quoted or contains no spaces then it is the one argument
sys.argv[1]
. For user convenience, quoting is not necessary even
if the path contains spaces, in which case there will be multiple arguments. We
can sweep up all of these using sys.argv[1:]
but this creates a
list rather than a string. The string method " ".join
recombines
the list’s elements back into a single space-delimited string. Path
history requires absolute paths in order to function logically so
os.path.abspath
is applied to this in case it is relative.
Finally, a newline is appended to prepare the path string for insertion into
the path list.
The -R option takes an optional path argument. Join is also used in this case
to accept as one argument a path containing spaces. Here, everything after the
first argument is swept up into one string using
" ".join(sys.argv[2:])
.
The list may be implemented as a list or deque. deque.appendleft
is more efficient than list.insert(0)
but list more efficiently
moves a directory at a random position to the head. I have written two versions
of this, one using list and the other deque. The deque version includes a
general move to head function where the list version has one
insert(pop)
. The two versions behave identically and there is no
noticeable speed difference.
The -B command essentially traverses from the head (most recent) to the tail
(oldest) by rotating the list to the left. The deque version does this simply
by dlist.rotate(-1)
. The list version is not much more
complicated, using insert(pop)
. -F moves forward through the list
by rotating right.
# File: cpxUpdate.py # ............................................................................ import sys, os from msvcrt import getche from os import system # ---------------------------------------------------------------------------- # Class flushfile replaces the builtin stdout write with one that calls flush # after write. Newer Python versions of file.write take an optional flush # argument but making flush the default is easier and ensures that it is not # forgotten. This should always be used for interactive command line scripts. # ............................................................................ class flushfile: def __init__(self, f): self.f = f def write(self, x): self.f.write(x) self.f.flush() def flush(nothing): return # Prevents warning at end sys.stdout = flushfile(sys.stdout) # ---------------------------------------------------------------------------- Thumb = 'J:' Ref = 'X:' ESC = b'\x1b' doAll = False # ---------------------------------------------------------------------------- # Function: ask # Purpose: General single-keystroke user query. Checks keypress against given # acceptible responses and repeatedly queries the user until the key is legal. # Returns: index of matching key code. # Arguments: opt is a tuple whose first element is a query String and second a # Bytes list of acceptible key codes. This is a tuple instead of individual # arguments to facilitate shared patterns (every usage requires a matched pair) # .............................................................................. def ask(opt) : while True : print(opt[0], end = '? ') idx = opt[1].find(getche().upper()) if idx >= 0: print('') # Print the newline we didn't get from getche. return idx print(' is not an option. Try again.') # ------------------------------------------------------------------------------ # Function: askYn # Purpose: Simple shell over ask. This is a frequent use of ask. The user is # presented three options, Esc to abort, Y, and N. It could be used for other # things but primarily it is used to ask the user whether to update a # directory. Y means to do it. N means to skip it. Esc means to stop executing # the script. Returns: 1 if Y, 2 if N. On Esc this doesn't return but # immediately exits. # ............................................................................ def askYn() : idx = ask(('[Esc,Y,N]', ESC + b'YN')) if idx == 0 : exit(0) return idx # ---------------------------------------------------------------------------- # Function: askAbort # Purpose: Queries the user whether to abort or continue. Pressing Esc selects # abort. Any other key selects continue. This function does not exit on Esc but # returns the response. # Returns: True if the user presses Esc, False for any other key. # .............................................................................. def askAbort() : print('Press Esc to stop or any other key to continue: ', end = '') inp = getche().upper() print('') # newline we didn't get from getche if inp == ESC : return True else : return False # ------------------------------------------------------------------------------ def queryUpdate(clone) : print('Do you want to update ' + clone, end = '') return askYn() # ------------------------------------------------------------------------------ # Function: update # Purpose: Directory update process. First asks whether to update the given # clone directory. The user can enter Esc to abort the script, Y to update, or # N to skip this directory. If Y then compx is invoked twice, first with a # command to replace older and missing clone files with files from ref, and # then with a command to prune dead clone files. # # Returns: Doesn't return if user selects Esc; False if N. If user selects Y, # after updating the user is given a chance to Abort, in which case the script # exits reflecting the return code from compx, or continue, in which case True # is returned (to the caller). # # Arguments: # - clone and ref are the clone and reference full path names. clone may refer # to a thumb drive or computer, in either case as identified by drive letter. # ref could also be flexible but, in practice, is always a hard drive. # - args is a tuple of two Strings, the first and second compx invocation # option arguments. The full command line also includes the clone and reference # directories but the clone and ref arguments provide this information. args # is a two-tuple instead of two String arguments to make it easier for callers # for different directories to share common argument sets. e.g. argsA in the # Doc group. Be sure that there is a comma between the two strings. Two # strings without this would be concatenated, making args[0] a too-long # command line and args[1] empty without encountering a syntax error. # ............................. notes ........................................ # doAll # If doAll then every directory in the list is processed without asking and, # unless the compx program returns an error, there is no pause after # processing. # ............................................................................ def update(clone, ref, args) : if not doAll : if queryUpdate(clone) == 2 : # User says no to update return False prune = os.path.exists(clone) for cmd in ('compx ' + ref + clone + args[0], 'compx ' + clone + ref + args[1]) : print(cmd) ret = system(cmd) if (not doAll or ret > 0) and askAbort() : exit(ret) if not prune : break return True # ---------------------------------- BEGIN ------------------------------------- print('cpxUpdate 2016.05.07') print('Esc: Abort') print('1: Update DocDavid thumb drive (' + Thumb + ')') print('2: Update Doc thumb drive (' + Thumb + ')') print('3: Update this computer from reference computer (' + Ref + ')') idx = ask(('[Esc,1,2,3]', ESC + b'123')) if idx == 0 : exit(0) print('Do you want to process all directories without asking ', end = '') if askYn() == 1 : doAll = True if idx == 1 : #---- Update DocDavid thumb drive ------------------------------ update(Thumb + '\\DocDavid ', 'C:\\DocDavid ', ('-Q4 -OA1 -S-Bak -U -Y', '-OA1 -S-Bak -U1')) if idx == 2 : # ------- Update Doc Thumb Drive ------------------------------- argsA = ('-Q4 -OA1 -S-Bak -U -Y', '-OA1 -S-Bak -U1') update(Thumb + '\\CmdTools ', 'C:\\CmdTools ', ('-Q4 -OA1 -U', '-OA1 -U1')) update(Thumb + '\\Doc ', 'C:\\Doc ', argsA) update(Thumb + '\\SwDev ', 'C:\\SwDev ', ('-Q4 -OA1 -S3-Bak,Old,Debug,Release -U -Y', '-OA1 -S3-Bak,Old,Debug,Release -U1')) update(Thumb + '\\Media\\Pub ', 'C:\\Media\\Pub ', ('-Q4 -OA1 -U -Y', '-OA1 -U1')) update(Thumb + '\\Web\\Portfolio ', 'C:\\Web\\Portfolio ', argsA) update(Thumb + '\\Arc ', 'C:\\Arc ', argsA) update(Thumb + '\\DocPub ', 'C:\\DocPub ', argsA) if idx == 3 : # --------- Update this computer from reference ---------------- argsA = ('-Q3 -OA1 -S-Bak -U -Y', '-OA1 -S-Bak -U1') update('C:\\CmdTools ', Ref + '\\CmdTools ', ('-Q3 -OA1 -U', '-OA1 -U1')) update('C:\\DocDavid ', Ref + '\\DocDavid ', argsA) update('C:\\Doc ', Ref + '\\Doc ', argsA) update('C:\\SwDev ', Ref + '\\SwDev ', ('-Q3 -OA1 -S3-Bak,Old,Debug,Release -F-launch,ncb,opt,plg -U -Y', '-OA1 -S3-Bak,Old,Debug,Release,Lpcx*,Mcux* -U1')) # This compares any Lpcx or Mcux directories that exist in source but doesn't # ask whether to remove them on this computer. update('C:\\Media\\Pub ', Ref + '\\Media\\Pub ', ('-Q3 -U -Y', '-OA1 -U1')) # Note no -OA1 in first command update('C:\\Web\\Portfolio ', Ref + '\\Web\\Portfolio ', argsA) update('C:\\Arc ', Ref + '\\Arc ', argsA) update('C:\\DocPub ', Ref + '\\DocPub ', argsA) # The rest of these are only by request (of top level) regardless of doAll # but we don't need to ask if doAll is False because, in that case, update # will ask on the top level as well as all sub-directories. if doAll == False or queryUpdate('C:\\DocPubCommon ') == 1 : update('C:\\DocPubCommon ', Ref + '\\DocPubCommon ', argsA)
I do most of my work on one computer with regular backups to archival devices and a mirror computer. The mirror computer is a convenient second debugger when I’m developing software for multiple embedded targets but its main purpose is to allow immediate switch-over if something goes wrong with my main computer or a long job is consuming most of its bandwidth.
On my main computer I do a wide variety of work with different backup requirements. I want to be able to either include or exclude directories and files by name, including wild-cards; to control recursive depth; to optionally prune dead stuff from the clone (mirror computer or archive); to control the extent of automation (i.e. things that are done without asking for approval) on a case-by-case basis; and to execute very rapidly so that I don’t mind regularly invoking it.
Standard backup management programs don’t provide all of the capabilities that I need and they are much slower than I want. I decided on a Unix-like solution, breaking the problem into two parts, a fast and flexible file compare program with a more user-friendly Python front end.
The file compare program and its myriad command-line options do most of the work. Python could handle this very complex job but it would be much slower than the bare-metal C program. Consequently, this is a simple script, which could be done by less capable languages than Python. However, Python’s sophistication is not wasted.
This script is what I use for back up to two different USB thumb drives and a
mirror (“this”) computer. compx is invoked twice for many different
directory trees, first to add/update clone directories and files and then to
prune the clone of directories and files that have been removed from the
reference computer. Instead of repeating the complex invocations, burying
important variations in a mountain of repetition, I have written the function
update
to handle the task generically. The invocations of
update
are essentially declarative. I could have condensed the
code even further by iterating over a table of the argument sets but the
repeating statements invoking update
are as regular and nearly as
condensed as the table would be.
Purpose: Update clones from reference computer. Clones may be thumb drives, in which case the computer is the reference and C: is the reference drive. Alternatively, “this” computer is the clone and a computer on the network whose C: is mapped to another drive letter is the reference. This script works only in Windows. It contains a Windows-specific simplified means of providing single keystroke input but, more fundamentally, drive letters are essential to identifying reference and clone.
The script assigns X and J drives to Ref and Thumb. When updating the Doc or DocDavid thumb drive, ref is automatically C: and the clone (in both cases) is J. When updating “this” computer, C: is clone and X is ref. The Ref and Thumb assignments made by single statements at the beginning of the script and can easily be changed. They are not options because it would be dangerous to have the user identify these by command or query, as a simple mistake could cause source files to be deleted. It would be safe to call this script from another, which could be trusted to correctly identify the drives; but there is little difference between editing the assignments in that script vs. this one.
Nearly identical directories and commands are used to update “this”
computer as for the two thumb drives. However, drive letters are different and
the compx command arguments differ in -Q4 for thumb drives vs. -Q3 for
“this” computer. This controls whose time stamps get updated when file
contents match but their time stamps don’t. Because of these differences,
it is easier to use fully independent arguments to update
for
“this” computer vs. thumb drives.
The update
function is primarily designed to support updating the
Doc thumb drive, which has multiple directories. Since the user is given the
option to selectively skip each one, this question is built into
update
. This question isn’t needed when updating the
DocDavid thumb drive, which has only this one directory. Otherwise
update
is the same for either case so it is also used for DocDavid.
Because the clone drive/ directory is used both to ask the user and in the
compx commands, another function argument would be required to skip the
question. I decided not to add this because, while the redundant question
appears silly, it is not much of a bother.
The update
command lines (whether for thumb or clone computer) for Doc,
Arc, DocPub, Web\Portfolio are all identical except for the directory. They
could be processed in a loop to reduce code but then they would have to be done
in a group. It is most convenient for the user if directories are processed in
order of most to least commonly updated so that routine updating is easily
shortened (by ESC). But this group contains a mix of common and rarely updated
directories. Instead of a loop with embedded update process, I wrote the
update
function, reducing each directory’s unique code to
little more than the loop approach while enabling complete freedom of order.
The function is made sufficiently general to support all cases, including
directories not in this group.
In most directories there are many relatively small matching files. Displaying each file’s name would fill up the display with information of little value to the user but we do want to indicate progress. For this, the compx option -OA1 is selected. It just shows a progress dot for each match. Media/Pub is different. It has relatively few, very large files, which can take a long time to compare byte-by-byte, should they have mismatched time stamps. For it, the first (clone) command does not include a -OA argument, thereby selecting the default, which is to show the names of matching files. The gives the user more confidence in the progress than an occasional dot and it doesn’t consume an inordinate amount of display space because there are few files.
By default compx uses a quick file compare, which says that two files are equal if they have the same name (of course), size, and time (last modified). Often, identical files have different time stamps, forcing a potentially long byte-by-byte comparison. If this reveals that they are identical then we can change the time of one of them to match the other, reducing the next script execution time. The -Q argument to compx controls all quick compare options, including whose time stamp gets changed. When updating “this” computer, we typically can’t change anything in the reference computer and must change the clone file time. But when updating a thumb drive we have to change the reference file time stamp because thumb drives use FAT16, which doesn’t support modification time. Consequently, for time stamp change to effectively reduce execution times, thumb drives should be updated first, potentially changing time stamps in the reference computer. Then clone computers are updated, potentially changing their files’ time stamps. This may occasionally (but very rarely) cause a make hiccup.
IMPORTANT TIME NOTE: Windows has a time stamping bug. If a computer’s time zone setting is changed, even just by turning on/off automatic DST, all file time stamps appear changed, forcing a long byte-by-byte comparison on every file. As a general rule, turn off automatic DST adjust on all computers. The reason for this instead of turning it on for all is that the dates of time change are subject to government decision and can change and a computer not on automatic OS updates can change at the wrong time.
If there are no file/directory/copy/create problems when updating a clone but a
clone directory can’t be deleted in the reverse (pruning) operation, the
problem is most likely a hidden file. To test for this, at a command prompt in
the offending directory type dir /ah /s
. Any hidden files revealed
by this can be unhidden (attrib -h filename
) but it may
still be necessary to reboot to update the file system enough for compx to be
able to delete the files. It is much easier to simply delete the directory
through Windows Explorer. This could be done under any circumstances but it is
best to first determine the problem in order to try to avoid it in the future.
If we simply remove by hand any directories that compx can’t remove we
will not know the cause.
Print is to stdout, which is buffered by default. This mixes up the display.
Python has added flush argument to print but it defaults to False, forcing
every print invocation to include flush = True and it won’t work on an
older version of Python. We could call stdout.flush()
after every
print but I have chosen instead to use a derived file write class.