2

I'm trying to convert a long, complex, Windows batch file into Python.

Things work except for subtle problems, which I suspect have something to do with quoting, but can't quite figure out.

In a batch file, this works fine:

Reg.exe add "HKCR\7-Zip.zip\shell\open\command" /ve /t REG_SZ /d "\"C:\Program Files\7-Zip\7zFM.exe\" \"%%1\"" /f

This Python code is meant to, but doesn't, do the same thing:

import os, subprocess

cmd = r'Reg.exe add "HKCR\7-Zip.zip\shell\open\command" /ve /t REG_SZ /d "\"C:\Program Files\7-Zip\7zFM.exe\" \"%%1\"" /f'
#os.system(cmd)
subprocess.call(cmd, shell=True)

Note that the (raw) string cmd and the batch file are exactly identical.

Both the os.system() and subprocess.call() result in the same thing - no error (Reg.exe says everything is fine), but a different effect on the system.

In my testing, the batch file configures the archiver 7z to open the .ZIP file itself (correct result).

The Python code causes 7z to open the folder where the .ZIP is located (wrong).

How to get Python to do what the batch file does?

nerdfever.com
  • 1,652
  • 1
  • 20
  • 41
  • can you try `cmd = ['Reg.exe','add',r'HKCR\7-Zip.zip\shell\open\command','/ve','/t','REG_SZ','/d',r'\"C:\Program Files\7-Zip\7zFM.exe\" \"%%1\"','/f']` – Jean-François Fabre Apr 01 '18 at 19:35
  • Is there a reason you need to use the shell here? You're not using any shell features, just executing a command with some arguments, so you're just trying to figure out how to fight with two layers of quoting instead of none this way… – abarnert Apr 01 '18 at 19:48
  • (Actually, make that three layers of quoting instead of one, because you've also got the unavoidable registry string quoting…) – abarnert Apr 01 '18 at 19:50
  • Doubling percent only escapes it in batch scripts, not in an interactive or `/c` command. However, escaping percent is not an issue here since it's not paired. Simply use a single percent. (CMD has no completely reliable way to escape multiple percents on the command line or `/c` commands. Its "^" escape character often works to disrupt variable-name matching, but not if the variable name starts with "^".) – Eryk Sun Apr 02 '18 at 08:13
  • Also, don't set the value using HKCR. That's a dynamic view that's intended for reading. The result of writing to HKCR depends on what's already defined in "[HKLM|HKCU]\Software\Classes". When adding a setting, it should be defined explicitly in either the HKLM or HKCU registry hive. – Eryk Sun Apr 02 '18 at 08:14

2 Answers2

2

OK, shot in the dark:

I would drop shell=True and use a argument list to pass to subprocess. The quoting will be handled automatically:

cmd = ['Reg.exe','add',r'HKCR\7-Zip.zip\shell\open\command','/ve','/t','REG_SZ','/d',r'"C:\Program Files\7-Zip\7zFM.exe" "%1"','/f']
rc = subprocess.call(cmd)

also check the return code of subprocess.call

If you want to process several commands like this "automatically", I'd suggest to use shlex.split, I'm not saying that it will solve everything, but it does a fine job with the quotes (protecting the quoted arguments, with quote nesting):

import shlex
text = r"""Reg.exe add "HKCR\7-Zip.zip\shell\open\command" /ve /t REG_SZ /d "\"C:\Program Files\7-Zip\7zFM.exe\" \"%%1\"" /f"""

print([x.replace("%%","%") for x in shlex.split(text)])  # %% => % in listcomp, add more filters if needed

result:

['Reg.exe', 'add', 'HKCR\\7-Zip.zip\\shell\\open\\command', '/ve', '/t', 'REG_SZ', '/d', '"C:\\Program Files\\7-Zip\\7zFM.exe" "%1"', '/f']

same thing with raw prefix would be:

['Reg.exe', 'add', r'HKCR\7-Zip.zip\shell\open\command', '/ve', '/t', 'REG_SZ', '/d', r'"C:\Program Files\7-Zip\7zFM.exe" "%1"', '/f']

pretty close right? :)

Jean-François Fabre
  • 137,073
  • 23
  • 153
  • 219
  • @eryksun thanks. BTW (unrelated) I've put a bounty on my other Popen/stdin question. You may be interested :) – Jean-François Fabre Apr 02 '18 at 08:01
  • Yes, that works (upvoted the answer). However exactly what rule you used to transform the original command line into the the new one isn't clear to me. I've got thousands of lines of batch file (not all involving reg.exe) to process; I need an algorithm - hand tweaking each one isn't going to work here. – nerdfever.com Apr 02 '18 at 15:42
  • For example, some of your strings are raw, some aren't. Why? Exactly why you dropped "\" before the path to 7zFM.exe isn't clear, nor is the quoting or change to single % around %1. – nerdfever.com Apr 02 '18 at 15:45
  • 1
    raw is useful to avoid doubling the backslashes in string literals. If there are no backslashes, no need to. See my edit, should be a good start. – Jean-François Fabre Apr 02 '18 at 15:51
  • That is indeed a good start. Unfortunately for me I don't have the patience to analyze the batch file to figure out whatever other filters might be needed. I found a solution - posting my own answer. – nerdfever.com Apr 02 '18 at 18:23
1

Jean-François Fabre's answer is good, and probably the most Pythonic answer.

Unfortunately for me, I don't have the patience to analyze the batch file to figure out whatever other filters might be needed, so I came up with this solution, which works regardless of the syntax, quoting, and escape sequences in the batch file lines:

def do(command):
    '''Executes command at Windows command line.'''

    import os, subprocess, uuid

    batchFile = open("temp_" + str(uuid.uuid4()) + ".bat", 'w')
    batchFile.write(command)
    batchFile.close()
    subprocess.call(batchFile.name, shell=True)
    os.remove(batchFile.name)

All it does is create a one-line batch file and then run it. Brute-force.

It's a bit slow because it has the overhead of creating, calling, and deleting the one-line batch file each time.

Here's a faster version that creates one big batch file with all the command lines. Whenever you call it with defer=False, it executes all the commands to date:

# thanks to https://stackoverflow.com/questions/279561/what-is-the-python-equivalent-of-static-variables-inside-a-function
def static_vars(**kwargs):
    def decorate(func):
        for k in kwargs:
            setattr(func, k, kwargs[k])
        return func
    return decorate

@static_vars(batchFile=None)
def do(command, defer=True):
    '''Executes command at Windows command line.
       Runs immediately, including all previously deferred commands, if defer is not True
    '''
    import os, subprocess, uuid

    if do.batchFile == None:
        do.batchFile = open("temp_" + str(uuid.uuid4()) + ".bat", 'w')

    do.batchFile.write(command + "\n") # append to file

    if not defer:
        do.batchFile.close()
        subprocess.call(do.batchFile.name, shell=True)
        os.remove(do.batchFile.name)
        do.batchFile = None
nerdfever.com
  • 1,652
  • 1
  • 20
  • 41