-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathwininteg.py
More file actions
969 lines (837 loc) · 36.4 KB
/
wininteg.py
File metadata and controls
969 lines (837 loc) · 36.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
#!/usr/bin/env python
# ***** BEGIN LICENSE BLOCK *****
# Version: MPL 1.1/GPL 2.0/LGPL 2.1
#
# The contents of this file are subject to the Mozilla Public License
# Version 1.1 (the "License"); you may not use this file except in
# compliance with the License. You may obtain a copy of the License at
# http://www.mozilla.org/MPL/
#
# Software distributed under the License is distributed on an "AS IS"
# basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See the
# License for the specific language governing rights and limitations
# under the License.
#
# The Original Code is Komodo code.
#
# The Initial Developer of the Original Code is ActiveState Software Inc.
# Portions created by ActiveState Software Inc are Copyright (C) 2000-2007
# ActiveState Software Inc. All Rights Reserved.
#
# Contributor(s):
# ActiveState Software Inc
#
# Alternatively, the contents of this file may be used under the terms of
# either the GNU General Public License Version 2 or later (the "GPL"), or
# the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
# in which case the provisions of the GPL or the LGPL are applicable instead
# of those above. If you wish to allow use of your version of this file only
# under the terms of either the GPL or the LGPL, and not to allow others to
# use your version of this file under the terms of the MPL, indicate your
# decision by deleting the provisions above and replace them with the notice
# and other provisions required by the GPL or the LGPL. If you do not delete
# the provisions above, a recipient may use your version of this file under
# the terms of any one of the MPL, the GPL or the LGPL.
#
# ***** END LICENSE BLOCK *****
"""
Python interface to integrating an app with Microsoft Windows.
Current it provides a command line and module integrate to add an
remove file associations.
"""
# Dev Notes:
# - On Win9x QueryValueEx returns the empty string for a non-existant
# default key value. On non-Win9x an EnvironmentError is raised. Care has
# been made in the code to handle this API semantic difference.
#
#TODO:
# - Use the ASSOC and FTYPE command line utils instead of all this
# registry entry mucking, if possible! Do these commands exist even
# on Win9x machine? I wonder.
# - Perhaps reduce "add_assoc" to "register_type" (which includes a
# default icon) and "add_assoc". Then "add_assoc" fails if there is
# no type registered.
# - Add interface for adding a shortcut on the desktop.
# - Add interface for adding a shortcut on the quick launch bar.
# - Test suite! There are subtle _winreg API differences on Win9x
# which should be tested.
from __future__ import absolute_import
from __future__ import print_function
import os
import sys
import cmd
import pprint
import getopt
import logging
import itertools
from six.moves import range
if sys.platform.startswith("win"):
import six.moves.winreg
#---- exceptions
class WinIntegError(Exception):
pass
#---- globals
_version_ = (0, 2, 1)
log = logging.getLogger("wininteg")
#---- internal support routines
def _splitall(path):
"""Split the given path into all its directory parts and return the list
of those parts (see Python Cookbook recipe for test suite.)
"""
allparts = []
while 1:
parts = os.path.split(path)
if parts[0] == path: # sentinel for absolute paths
allparts.insert(0, parts[0])
break
elif parts[1] == path: # sentinel for relative paths
allparts.insert(0, parts[1])
break
else:
path = parts[0]
allparts.insert(0, parts[1])
return allparts
class _ListCmd(cmd.Cmd):
"""Pass arglists instead of command strings to commands.
Modify the std Cmd class to pass arg lists instead of command lines.
This seems more appropriate for integration with sys.argv which handles
the proper parsing of the command line arguments (particularly handling
of quoting of args with spaces).
"""
name = "_ListCmd"
def cmdloop(self, intro=None):
raise NotImplementedError
def onecmd(self, argv):
# Differences from Cmd
# - use an argv, rather than a command string
# - don't specially handle the '?' redirect to 'help'
# - don't allow the '!' shell out
if not argv:
return self.emptyline()
self.lastcmd = argv
cmdName = argv[0]
try:
func = getattr(self, 'do_' + cmdName)
except AttributeError:
return self.default(argv)
try:
return func(argv)
except TypeError as ex:
log.error("%s: %s", cmdName, ex)
log.error("try '%s help %s'", self.name, cmdName)
if 1: # for debugging
print()
import traceback
traceback.print_exception(*sys.exc_info())
def default(self, args):
log.error("unknown syntax: '%s'", " ".join(args))
return 1
def _do_one_help(self, arg):
try:
# If help_<arg1>() exists, then call it.
func = getattr(self, 'help_' + arg)
except AttributeError:
try:
doc = getattr(self, 'do_' + arg).__doc__
except AttributeError:
doc = None
if doc: # *do* have help, print that
sys.stdout.write(doc + '\n')
sys.stdout.flush()
else:
log.error("no help for '%s'", arg)
else:
return func()
# Technically this improved do_help() does not fit into _ListCmd, and
# something like this would be more appropriate:
# def do_help(self, argv):
# cmd.Cmd.do_help(self, ' '.join(argv[1:]))
# but I don't want to make another class for it.
def do_help(self, argv):
if argv[1:]:
for arg in argv[1:]:
retval = self._do_one_help(arg)
if retval:
return retval
else:
doc = self.__class__.__doc__ # try class docstring
if doc:
sys.stdout.write(doc + '\n')
sys.stdout.flush()
elif __doc__: # else try module docstring
sys.stdout.write(__doc__)
sys.stdout.flush()
def emptyline(self):
# Differences from Cmd
# - Don't repeat the last command for an emptyline.
pass
def _parseFirstArg(cmd):
cmd = cmd.strip()
if cmd.startswith('"'):
# The .replace() is to ensure it does not mistakenly find the
# second '"' in, say (escaped quote):
# "C:\foo\"bar" arg1 arg2
idx = cmd.replace('\\"', 'XX').find('"', 1)
if idx == -1:
raise WinIntegError("Malformed command: %r" % cmd)
first, rest = cmd[1:idx], cmd[idx+1:]
rest = rest.lstrip()
else:
if ' ' in cmd:
first, rest = cmd.split(' ', 1)
else:
first, rest = cmd, ""
return first
def _getTypeName(ext):
"""Calculate a reasonable Windows "type name" for the given extension."""
assert ext[0] == '.', "Extension is invalid: '%s'" % ext
# First try some common/generally accepted type name mappings.
commonTypeMappings = {
'.pl': 'Perl',
'.py': 'Python.File',
'.js': 'JSFile',
'.xml': 'XMLFile',
'.xsl': 'XSLFile',
'.xslt': 'XSLTFile',
'.pm': 'Perl.Module',
'.t': 'Perl.TestScript',
#XXX This is the name that ActiveTcl/TclPro uses for the .tcl file
# association. We choose to use its name as well.
'.tcl': 'ActiveTclScript',
'.php': 'PHPFile',
'.plx': 'PlxFile',
'.wsdl': 'WSDLFile',
}
typeName = commonTypeMappings.get(ext, None)
# Fallback: the name will be "FOOFile" for an extension ".foo".
if typeName is None:
typeName = ext[1:].upper() + "File"
return typeName
def _getTypeNameFromRegistry(ext, root=None):
"""Read the type name from the registry
@param ext {unicode} The extension; must start with a leading period
@param root {HKEY} The root tree to use; if unspecified, HKCR is used.
(i.e. the merged tree)
@returns {unicode} The type name, or None if not found
"""
log.debug("_getTypeNameFromRegistry: getting '%s'", ext)
assert ext[0] == '.', "Extension is invalid: '%s'" % ext
import six.moves.winreg
if root is None:
root = six.moves.winreg.HKEY_CLASSES_ROOT
elif root in (six.moves.winreg.HKEY_LOCAL_MACHINE, six.moves.winreg.HKEY_CURRENT_USER):
# use the Software\Classes subkey
root = six.moves.winreg.OpenKey(root, r"Software\Classes")
try:
extKey = six.moves.winreg.OpenKey(root, ext)
except WindowsError:
log.debug("Failed to open '%s'", ext)
return None
log.debug("Opened key '%s'", ext)
# Get the type name from this key.
try:
typeName, typeNameType = _safeQueryValueEx(extKey, "")
except WindowsError:
return None
return typeName
def _safeQueryValueEx(key, name):
"""Try to work around some issues with string length and NULL terminators
in string registry entries.
For example, sometimes (don't know how to reproduce those circumstances
yet -- see Komodo bug 33333) a QueryValueEx will return a string with a
number of '\x00' null characters. This method strips those.
XXX See the not about the different behaviour of QueryValueEx on Win9x
versus WinNT for null values. Perhaps this method could abstract
that.
"""
value, valueType = six.moves.winreg.QueryValueEx(key, name)
if valueType in (six.moves.winreg.REG_SZ, six.moves.winreg.REG_MULTI_SZ, six.moves.winreg.REG_EXPAND_SZ):
value = value.strip('\x00')
return (value, valueType)
def _deleteKeyIfEmpty(root, keyName, rootDesc="..."):
"""Delete the given registry key, and any ancestor keys up to the root, if
they are empty (have no subkeys or values).
@param root {HKEY} The ancestor which should not be deleted
@param keyName {unicode} The name of the subkey to delete if empty
@param rootDesc {unicode} A string describing the root (for logging)
@returns {int} The number of keys deleted
"""
import six.moves.winreg
count = 0
log.debug(r"_deleteKeyIfEmpty: deleting %s\%s", rootDesc, keyName)
while keyName:
with six.moves.winreg.OpenKey(root, keyName) as key:
try:
six.moves.winreg.EnumValue(key, 0)
except WindowsError:
pass
else:
log.debug("_deleteKeyIfEmpty: %s has values", keyName)
return count # not empty
try:
six.moves.winreg.EnumKey(key, 0)
except WindowsError:
pass
else:
log.debug("_deleteKeyIfEmpty: %s has subkeys", keyName)
return count # not empty
six.moves.winreg.DeleteKey(root, keyName)
count += 1
log.info(r"deleted '%s\%s' key", rootDesc, keyName)
keyName = "\\".join(keyName.split("\\")[:-1])
# try again with the parent
return count
#---- public module interface
def getHKLMRegistryValue(keyName, valueName):
"""Return a (<value>, <valueType>) tuple for the given registry value.
An EnvironmentError is raised if the value does not exist.
(Note: On Win9x the empty string may be returned for non-existant values
instead of raising an environment error.)
"""
log.debug("getHKLMRegistryValue(keyName=%r, valueName=%r)", keyName,
valueName)
import six.moves.winreg
key = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_LOCAL_MACHINE, keyName)
return _safeQueryValueEx(key, valueName)
def setHKLMRegistryValue(keyName, valueName, valueType, value):
"""Set the given value in the registry.
An EnvironmentError is raised if unsuccessful.
"""
log.debug("setHKLMRegistryValue(keyName=%r, valueName=%r, valueType=%r, "\
"value=%r)", keyName, valueName, valueType, value)
import six.moves.winreg
# Open the key for writing.
try:
key = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_LOCAL_MACHINE, keyName,
0, six.moves.winreg.KEY_SET_VALUE)
except EnvironmentError as ex:
# Either do not have permissions or we must create the keys
# leading up to this key. Presume that latter, if the former
# then it will fall out in the subsequent calls.
parts = _splitall(keyName)
for i in range(len(parts)):
partKeyName = os.path.join(*parts[:i+1])
partKey = six.moves.winreg.CreateKey(six.moves.winreg.HKEY_LOCAL_MACHINE,
partKeyName)
key = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_LOCAL_MACHINE, keyName,
0, six.moves.winreg.KEY_SET_VALUE)
# Write the given value.
six.moves.winreg.SetValueEx(key, valueName, 0, valueType, value)
def getFileAssociation(ext):
"""Return the register filetype and an order list of associated actions.
"ext" is the extension to lookup. It must include the leading '.'.
Returns the following:
(<filetype>, <filetype display name>, <ordered list of actions>)
where the list of actions is intended to be ordered as they would be
in the Windows Explorer context menu for a file with that extension.
Each action is a tuple,
(<action name>, <action display string>, <command line>)
If the file type is not found, raises WinIntegError
"""
log.debug("getFileAssociation(ext=%r)", ext)
import six.moves.winreg
#---- 1. Find the type name from the extension.
typeName = _getTypeNameFromRegistry(ext)
if typeName is None:
raise WinIntegError("unrecognize extension: '%s'" % ext)
# Get the type display name from the type key (it is the default value).
displayName = None
try:
with six.moves.winreg.OpenKey(six.moves.winreg.HKEY_CLASSES_ROOT, typeName) as typeKey:
displayName = _safeQueryValueEx(typeKey, "")[0]
except WindowsError as ex:
pass
#---- 2. Get the current actions associated with this file type.
# Get a list of all the current actions. E.g. for this layout:
# HKEY_CLASSES_ROOT
# Python.File
# shell
# Edit -> (value not set)
# Edit2 -> "&Edit with Komodo"
# open -> (value not set)
# the actions are:
# [("Edit", "&Edit"), ("Edit2", "&Edit with Komodo"),
# ("open", "&Open")]
# Implicit naming rules:
# - "open" and "print" get capitalized, others do not seems to (including
# "edit").
# - the first letter is made the accesskey with a '&'-prefix
actionNames = []
try:
shellKey = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_CLASSES_ROOT,
"%s\\shell" % typeName)
for index in itertools.count():
try:
actionName = six.moves.winreg.EnumKey(shellKey, index)
try:
with six.moves.winreg.OpenKey(shellKey, actionName) as actionKey:
actionDisplayName = _safeQueryValueEx(actionKey, None)[0]
except WindowsError as ex:
if ex.winerror != 2: # ERROR_FILE_NOT_FOUND
raise
actionDisplayName = None
if not actionDisplayName:
if actionName.lower() == "open":
actionDisplayName = "&Open"
else:
actionDisplayName = "&"+actionName
actionNames.append( (actionName, actionDisplayName) )
except WindowsError:
break
except WindowsError:
pass
log.debug("action names for '%s': %s", typeName, actionNames)
actions = []
for actionName, actionDisplayName in actionNames:
command = None
try:
commandKey = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_CLASSES_ROOT,
"%s\\shell\\%s\\command"
% (typeName, actionName))
except WindowsError as ex:
pass
else:
try:
command, commandType = _safeQueryValueEx(commandKey, "")
except WindowsError:
pass
actions.append( (actionName, actionDisplayName, command) )
#---- 3. Sort the actions as does Windows Explorer
# This seems to use the following rules:
# - If there is an "opennew", then that is first and all others are
# after in alphabetical order.
# - Else if there is an "open", then that is first and all others are
# after in alphabetical order.
name2action = {}
for action in actions:
name2action[action[0].lower()] = action
if "opennew" in name2action:
default = name2action["opennew"]
del name2action["opennew"]
elif "open" in name2action:
default = name2action["open"]
del name2action["open"]
else:
default = None
actions = [name2action[k] for k in sorted(name2action.keys())]
if default: actions.insert(0, default)
return (typeName, displayName, actions)
def checkFileAssociation(ext, action, exe):
"""Check that the given association is setup as expected.
"ext" is the extention (it must include the leading dot).
"action" is the association action to check.
"exe" is the expected associated executable.
This can raise an WindowsError if unsuccessful.
"""
log.debug("checkFileAssociation(ext=%r, action=%r, exe=%r)",
ext, action, exe)
import six.moves.winreg
#---- Find the type name from the extension.
try:
[typeName, typeDisplayName, actions] = getFileAssociation(ext)
except WinIntegError:
return "'%s' extension is not registered with system" % ext
#---- Abort check if there is no matching action.
for actionName, actionDisplayName, command in actions:
log.debug("actionDisplayName: %r actionName: %r action: %r",
actionDisplayName, actionName, action)
if (actionDisplayName.lower() == action.lower()
or actionName.lower() == action.lower()):
break
else:
actionsSummary = ', '.join(a[1] or a[0] for a in actions)
return "no '%s' action is associated with %s/%s "\
"(existing actions are: %s)"\
% (action, ext, typeName, actionsSummary)
#---- Check that actual command matches expectation.
if ' ' in exe:
expectedCommands = ['"%s" "%%1" %%*' % exe]
else:
expectedCommands = ['%s "%%1" %%*' % exe,
'"%s" "%%1" %%*' % exe] # allow redundant quotes
for expectedCommand in expectedCommands:
if expectedCommand == command:
return None
else:
return ("current '%s' command for %s/%s doesn't match "
"expectation:\n\tcurrent: %s\n\texpected: %s"
% (actionDisplayName, ext, typeName, command,
expectedCommands[0]))
def addFileAssociation(ext, action, exe, fallbackTypeName=None):
"""Add a file association from the given extension to the given
executable.
"ext" is the extention (it must include the leading dot).
"action" is the association action to make.
"exe" is the executable to which to associate.
"fallbackTypeName" is a file type name to use ONLY IF a type name
does not already exist for the given extension.
This can raise an EnvironmentError if unsuccessful. (XXX Can this be
limited to a WindowsError?)
"""
log.debug("addFileAssociation(ext=%r, action=%r, exe=%r, "\
"fallbackTypeName=%r)", ext, action, exe, fallbackTypeName)
import six.moves.winreg
userClasses = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_CURRENT_USER, r"Software\Classes")
#---- 1. Find the type name from the extension.
typeName = _getTypeNameFromRegistry(ext)
if typeName is None:
typeName = fallbackTypeName or _getTypeName(ext)
with six.moves.winreg.CreateKey(userClasses, ext) as extKey:
# re-open the key with write access
with six.moves.winreg.OpenKey(extKey, "", 0, six.moves.winreg.KEY_SET_VALUE) as extKey:
six.moves.winreg.SetValueEx(extKey, "", 0, six.moves.winreg.REG_SZ, typeName)
log.info("type name for '%s' is '%s'", ext, typeName)
#---- 2. Get the current actions associated with this file type.
# Get a list of all the current actions.
try:
currActions = getFileAssociation(ext)[2]
except WinIntegError:
currActions = []
log.info("current actions for '%s': %s", typeName, currActions)
#---- 3. Determine which subkey of HKCR\\$typeName\\shell to use for
# action.
if ' ' in action: # e.g. "Edit with Komodo"
# We might want to replace one of the existing actions if the
# action names are the same.
for currAction in currActions:
if action .replace('&', '').lower() ==\
currAction[1].replace('&', '').lower():
actionKeyName = currAction[0]
break
else:
# Pick an action key name that does not conflict.
currActionKeyNames = set(a[0].lower() for a in currActions)
for i in [''] + list(range(2, 100)):
actionKeyName = action.split()[0] + str(i) # Edit1, Edit2, ...
if actionKeyName.lower() not in currActionKeyNames:
break
else:
raise WinIntegError("Could not determine a non-conflicting "\
"action key name for file type '%s' and "\
"action '%s'." % (typeName, action))
actionName = action
else: # e.g. "Edit"
actionKeyName = action
actionName = None
actionKeyPath = "%s\\shell\\%s" % (typeName, actionKeyName)
log.info("creating '%s' action at key 'HKCR\\%s'",
actionName or actionKeyName, actionKeyPath)
#---- 4. Register the action.
# First, set the action name if necessary (and ensure the action key
# is created).
actionKey = six.moves.winreg.CreateKey(userClasses,
r"%s\shell\%s" % (typeName, actionKeyName))
if actionName is not None:
log.info("setting name for action key '%s' of file type '%s': '%s'",
actionKeyName, typeName, actionName)
six.moves.winreg.SetValueEx(actionKey, "", 0, six.moves.winreg.REG_SZ, actionName)
# Next, determine the command and create/update the "command" subkey.
if ' ' in exe:
command = '"%s" "%%1" %%*' % exe
else:
command = '%s "%%1" %%*' % exe
with six.moves.winreg.CreateKey(actionKey, "command") as commandKey:
log.info("setting command for '%s' action of '%s' file type: %r",
actionName or actionKeyName, typeName, command)
six.moves.winreg.SetValueEx(commandKey, "", 0, six.moves.winreg.REG_EXPAND_SZ, command)
def removeFileAssociation(ext, action, exe, fromHKLM=False):
"""Remove the given file association PROVIDED the current state of
the association points to the given executable.
"ext" is the extention (it must include the leading dot).
"action" is the association action to make.
"exe" is the executable to which to associate.
"fromHKLM", if set, causes the association to be removed from HKLM instead
of HKCU; this may raise a WindowsError if permissions are denied
This can raise an WindowsError if unsuccessful.
Returns True if the association was removed; False if it there was no need
to (not set, or set to a different executable).
"""
log.debug("removeFileAssociation(ext=%r, action=%r, exe=%r, HKLM=%r)", ext,
action, exe, fromHKLM)
import six.moves.winreg
if fromHKLM:
HKCR = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_LOCAL_MACHINE, r"Software\Classes")
else:
HKCR = six.moves.winreg.OpenKey(six.moves.winreg.HKEY_CURRENT_USER, r"Software\Classes")
#---- 1. Find the type name and associations from the extension.
try:
[typeName, typeDisplay, currActions] = getFileAssociation(ext)
except WinIntegError:
log.warn("extension '%s' is not registered, giving up", ext)
return False
log.info("type name for '%s' is '%s' actions: %r", ext, typeName, currActions)
#---- 2. Determine which subkey of HKCR\\$typeName\\shell is relevant.
actionKeyName = None
actionData = None
for currAction in currActions:
if currAction[1]:
if action .replace('&', '').lower() ==\
currAction[1].replace('&', '').lower():
actionKeyName, actionDisplayName, command = currAction
break
else:
if action .replace('&', '').lower() ==\
currAction[0].replace('&', '').lower():
actionKeyName, actionDisplayName, command = currAction
break
else:
log.info("could not find relevant current action to remove: '%s'",
action)
return False
log.info("relevant current action: '%s' command: '%s'",
actionKeyName, command)
#---- 3. Abort if the current action is NOT to the given exe.
commandExe = _parseFirstArg(command)
if os.path.split(exe)[-1].lower() != os.path.split(commandExe)[-1].lower():
log.warn("current association, %r, is not to the given exe, "\
"%r, aborting", commandExe, exe)
return False
#---- 4. Remove the action key.
with six.moves.winreg.OpenKey(HKCR, r"%s\shell\%s\command" % (typeName, actionKeyName), 0, six.moves.winreg.KEY_SET_VALUE) as commandKey:
six.moves.winreg.DeleteValue(commandKey, "")
log.info("deleted default value for 'HKCR\\%s\\shell\\%s\\command'",
typeName, actionKeyName)
# Clean up an empty registry branch.
try:
subkey = r"%s\shell\%s\command" % (typeName, actionKeyName)
# re-open HKCR with write access
with six.moves.winreg.OpenKey(HKCR, "", 0, six.moves.winreg.KEY_SET_VALUE) as root:
log.debug("re-opened root")
numDeleted = _deleteKeyIfEmpty(root, subkey, rootDesc="HKCR")
if numDeleted == 1:
# perhaps there's a description in <type>\shell\<action>\(Default)
subkey = r"%s\shell\%s" % (typeName, actionKeyName)
def tryDeleteDefault():
with six.moves.winreg.OpenKey(HKCR, subkey, 0, six.moves.winreg.KEY_QUERY_VALUE | six.moves.winreg.KEY_SET_VALUE) as actionKey:
try:
six.moves.winreg.EnumKey(actionKey, 0)
return None # other subkeys exist, don't delete
except WindowsError:
pass
hasDefault = True
try:
# check if the default value exists (the user-visible description of the file type)
six.moves.winreg.QueryValueEx(actionKey, "")
except WindowsError:
# there's no default value
hasDefault = False
try:
six.moves.winreg.EnumValue(actionKey, 1 if hasDefault else 0)
return None # other values exist
except WindowsError:
pass
if hasDefault:
six.moves.winreg.DeleteValue(actionKey, "")
return _deleteKeyIfEmpty(root, subkey, rootDesc="HKCR")
moreDeleted = tryDeleteDefault()
if moreDeleted is not None:
numDeleted += moreDeleted
log.debug("deleted %r keys", numDeleted)
if numDeleted >= len(subkey.split("\\")):
# the whole thing was deleted; clean up the extension tree as well
with six.moves.winreg.OpenKey(root, ext, 0, six.moves.winreg.KEY_SET_VALUE) as extKey:
six.moves.winreg.DeleteValue(extKey, None)
try:
_deleteKeyIfEmpty(root, ext, rootDesc="HKCR")
except WindowsError as ex:
if ex.winerror != 2: # ERROR_FILE_NOT_FOUND
raise
log.debug("removeFileAssociation: Can't find %s", ext)
# ignore not found errors, the file extension part may have
# come from the HKLM version of HKCR
except WindowsError as ex:
if ex.winerror != 5: # ERROR_ACCESS_DENIED
raise
log.debug("removeFileAssociation: Access denied (%r)", ex)
return True
#---- command line interface
class WinIntegShell(_ListCmd):
"""
wininteg - a tool for integrating an app into Microsoft Window
Usage:
wininteg [<options>...] <command> [<args>...]
Options:
-h, --help Print this help and exit.
-V, --version Print the version info and exit.
-v, --verbose More verbose output.
Wininteg's usage is intended to feel like p4's command line
interface.
Getting Started:
wininteg help print this help
wininteg help <command> help on a specific command
Commands:
get_assoc EXT list assocations for EXT
add_assoc EXT ACTION APPPATH add assocation for EXT
check_assoc EXT ACTION APPPATH check expected EXT assocation
remove_assoc EXT ACTION APPPATH remove specific assoc for EXT
"""
name = "wininteg"
def emptyline(self):
self.do_help(["help"])
def help_usage(self):
sys.stdout.write(__doc__)
sys.stdout.flush()
def do_get_assoc(self, argv):
"""
get_assoc -- Get the current file association.
wininteg get_assoc [<options>...] <ext>
<ext> is the extention (it must include the leading dot).
This looks up and prints all associated actions and shell commands
for the current extension.
"""
# Process options.
try:
optlist, args = getopt.getopt(argv[1:], "")
except getopt.GetoptError as ex:
log.error("get_assoc: %s", ex)
log.error("get_assoc: try 'wininteg help get_assoc'")
return 1
# Process arguments.
if len(args) != 1:
log.error("get_assoc: incorrect number of arguments: %s", args)
log.error("get_assoc: try 'wininteg help get_assoc'")
return 1
ext = args[0]
try:
type, name, actions = getFileAssociation(ext)
print("File Type: %s (%s)" % (name, type))
if actions:
print("Actions:")
for aName, aDisplayName, aCommand in actions:
print(" %s (%s)" % (aDisplayName, aName))
print(" %s" % aCommand)
else:
print("Actions: <none>")
except Exception as ex:
log.error(str(ex))
if log.isEnabledFor(logging.DEBUG):
import traceback
traceback.print_exception(*sys.exc_info())
return 1
def do_check_assoc(self, argv):
"""
check_assoc -- Check that a file association is as expected
wininteg check_assoc [<options>...] <ext> <action> <exe>
<ext> is the extention (it must include the leading dot).
<action> is the association action to check.
<exe> is the expected associated executable.
"""
# Process options.
try:
optlist, args = getopt.getopt(argv[1:], "", [])
except getopt.GetoptError as ex:
log.error("add_assoc: %s", ex)
log.error("add_assoc: try 'wininteg help check_assoc'")
return 1
# Process arguments.
if len(args) != 3:
log.error("check_assoc: incorrect number of arguments: %s", args)
log.error("check_assoc: try 'wininteg help check_assoc'")
return 1
ext, action, exe = args
try:
msg = checkFileAssociation(ext, action, exe)
if msg is not None:
print(msg)
except Exception as ex:
log.error(str(ex))
if log.isEnabledFor(logging.DEBUG):
import traceback
traceback.print_exception(*sys.exc_info())
return 1
def do_add_assoc(self, argv):
"""
add_assoc -- Add a file association.
wininteg add_assoc [<options>...] <ext> <action> <exe>
<ext> is the extention (it must include the leading dot).
<action> is the association action to make.
<exe> is the executable to which to associate.
Options:
--type-name=<name>, -t <name>
Specify a _fallback_ type name for the given extension.
An association is made for the given extension to the given executable.
If the extension already has a register type name, then that
name is used. You may provide a fallback type name to use, if it
is needed, otherwise one will be created based on the extension.
"""
# Process options.
try:
optlist, args = getopt.getopt(argv[1:], "t:", ["type-name="])
except getopt.GetoptError as ex:
log.error("add_assoc: %s", ex)
log.error("add_assoc: try 'wininteg help add_assoc'")
return 1
fallbackTypeName = None
for opt, optarg in optlist:
if opt in ("-t", "--type-name"):
fallbackTypeName = optarg
# Process arguments.
if len(args) != 3:
log.error("add_assoc: incorrect number of arguments: %s", args)
log.error("add_assoc: try 'wininteg help add_assoc'")
return 1
ext, action, exe = args
try:
addFileAssociation(ext, action, exe, fallbackTypeName)
except Exception as ex:
log.error(str(ex))
if log.isEnabledFor(logging.DEBUG):
import traceback
traceback.print_exception(*sys.exc_info())
return 1
def do_remove_assoc(self, argv):
"""
remove_assoc -- Remove a file association.
wininteg remove_assoc <ext> <action> <exe>
<ext> is the extention (it must include the leading dot).
<action> is the association action to remove.
<exe> is the executable to which to associate.
The given file association is removed, PROVIDED the currently
registered command is for the given executable. If it is not
then the association is left alone: we don't want to disrupt a
file association to another app.
"""
# Process options.
try:
optlist, args = getopt.getopt(argv[1:], "")
except getopt.GetoptError as ex:
log.error("remove_assoc: %s", ex)
log.error("remove_assoc: try 'wininteg help remove_assoc'")
return 1
# Process arguments.
if len(args) != 3:
log.error("remove_assoc: incorrect number of arguments: %s", args)
log.error("remove_assoc: try 'wininteg help remove_assoc'")
return 1
ext, action, exe = args
try:
removeFileAssociation(ext, action, exe)
except Exception as ex:
log.error(str(ex))
if log.isEnabledFor(logging.DEBUG):
import traceback
traceback.print_exception(*sys.exc_info())
return 1
def _main(argv):
logging.basicConfig()
try:
optlist, args = getopt.getopt(argv[1:], "hVv",
["help", "version", "verbose"])
except getopt.GetoptError as msg:
log.error("%s. Your invocation was: %s", msg, argv)
log.error("Try 'wininteg --help'.")
return 1
for opt, optarg in optlist:
if opt in ("-h", "--help"):
sys.stdout.write(WinIntegShell.__doc__)
return 0
elif opt in ("-V", "--version"):
print("wininteg %s" % '.'.join([str(i) for i in _version_]))
return 0
elif opt in ("-v", "--verbose"):
log.setLevel(Logger.DEBUG)
shell = WinIntegShell()
return shell.onecmd(args)
if __name__ == "__main__":
__file__ = os.path.abspath(sys.argv[0])
sys.exit( _main(sys.argv) )