2

I'm struggling with improving script which I proposed as an answer to How to write a batch file showing path to executable and version of Python handling Python scripts on Windows? question. To prevent Open With dialog box I'd like to read output of ftype command, extract path of an executable from it and check if it exists.

After this

@echo off
setlocal EnableDelayedExpansion 
rem c:\ftype Python.File ->
rem Python.File="c:\path with spaces, (parentheses) and % signs\python.exe" "%1" %*
for /f "tokens=2 delims==" %%i in ('ftype Python.File') do (
    set "reg_entry=%%i"
)

reg_entry's contents is

"c:\path with spaces and (parentheses) and % signs\python.exe" "%1" %*

How do I split this to get "c:\path with spaces, (parentheses) and % signs\python.exe", "%1" and %*?

EDIT
I tried using call after reading Aacini's answer and it almost works. It doesn't handle % sign, however.

@echo off
setlocal EnableDelayedExpansion 
set input="c:\path with spaces and (parentheses) and %% signs\python.exe" "%%1" %%*
echo !input!
call :first_token output !input!
echo !output!
goto :eof

:first_token
set "%~1=%2"
goto :eof

Output

"c:\path with spaces and (parentheses) and % signs\python.exe" "%1" %*
"c:\path with spaces and (parentheses) and 1"
Community
  • 1
  • 1
Piotr Dobrogost
  • 41,292
  • 40
  • 236
  • 366
  • I looked at my computer's FTYPE output and found some entries where the file is NOT enclosed within quotes, even though the path contains spaces. I'm worried that your strategy using FTYPE may not be reliable. – dbenham Nov 19 '11 at 23:39
  • @dbenham I had noticed this, too. However given all these problems I had with writing what seemed to be simple batch file I glossed over this issue. – Piotr Dobrogost Nov 20 '11 at 11:48

4 Answers4

2

That is direct capability of Batch. In Batch the parameters of a Batch file are separated by spaces, and a parameter may be enclosed in quotes, so just pass the value of reg_entry as parameters of a Batch file an inside it take each parameter:

C:\>type test.bat
@echo off
:loop
echo %1
shift
if not "%1" == "" goto loop

.

C:\>echo %reg_entry%
"c:\path with spaces and (parentheses) and % signs\python.exe" "%1" %*

.

C:\>test %reg_entry%
"c:\path with spaces and (parentheses) and % signs\python.exe"
"%1"
%*
Aacini
  • 65,180
  • 12
  • 72
  • 108
  • All nice and well but how to use this approach in a context of existing batch file? Is parsing of parameters of `call` the same as parsing of parameters of a batch file? If so do I need any extra escaping of these parameters inside batch file as compared to passing them to a batch file on the command line? – Piotr Dobrogost Oct 29 '11 at 07:24
  • It's nearly the same, escaping isn't necessary if the content is inside of quotes. The only character which makes trouble is the caret, each caret (inside quotes) will be doubled by the call – jeb Oct 29 '11 at 15:51
2

As Aacini said, your problem can be solved with the internal parameter splitting by using the CALL statement.

To avoid losing % signs by the call you can double them just before the call expansion.
The keyline is set "input=!input:%%=%%%%!", the percent signs are halfed in one of the parser phases, so there are replaced single % by %%.

But even then this solution isn't perfect!

This solution has problems with special characters like &<>|, in your case only & as this is the only legal character in a filename/path.
That can be avoided by changing the line set "%~1=%2" to set ^"%~1=%2", this ensures that %2 uses the surrounding quotes.

But now you got another problem, all carets are doubled,
so I have to do another replacement for the output with set "output=!output:^^=^!".

The new code would look like this

@echo off
setlocal EnableDelayedExpansion 
set input="c:\path with spaces, exlcamation mark^!, ampersand &, carets ^ and (parentheses) and %% signs\python.exe" "%%1" %%*
echo !input!
set "input=!input:%%=%%%%!"
call :first_token output !input!
set "output=!output:^^=^!"
echo !output!
goto :eof

:first_token
set ^"%~1=%2"
goto :eof

EDIT: For handling also exclamation marks !
You need to change the :first_token function to

:first_token
setlocal DisableDelayedExpansion
set ^"temp=%2"
set ^"temp=%temp:!=^!%"
(
endlocal
set ^"%~1=%temp%"
)
goto :eof
jeb
  • 78,592
  • 17
  • 171
  • 225
  • There is still a bug - You forgot to handle `!`, another valid character in paths that can cause problems. – dbenham Nov 19 '11 at 20:34
  • :-) You got me, I didn't forgot it, but you also know that this can't be handled in a simple way with this `call` solution – jeb Nov 19 '11 at 22:27
  • I found a _simple_ solution for this special case, as the string is always quoted – jeb Nov 19 '11 at 22:36
2

An alternative parser that is very similar to the CALL parser is the simple FOR. There are two complicating factors:

1- The FOR must not be expanded while delayed expansion is enabled in case it contains !. This is easily handled.

2- The content must not contain wildcards * or ?. The ? can be temporarily substituted for and then returned. But there is no easy way to search and replace *.

Since this problem is trying to parse out a path, and paths cannot contain wildcards, this problem is easy to solve without using a CALL. I added ! to the test case for completeness.

@echo off
setlocal disableDelayedExpansion
set input="c:\path with spaces, ampersand &, carets ^ and (parentheses)! and %% signs\python.exe" "%%1" %%*
set input
set "output="
setlocal enableDelayedExpansion
for %%A in (!input!) do if not defined output endlocal & set output=%%A
set output

If we can rely on the fact that the first token will always be enclosed in quotes, then the solution is even easier. We can use FOR /F with both EOL and DELIMS set to ".

@echo off
setlocal disableDelayedExpansion
set input="c:\path with spaces, ampersand &, carets ^ and (parentheses)! and %% signs\python.exe" "%%1" %%*
set input
set "output="
setlocal enableDelayedExpansion
for /f eol^=^"^ delims^=^" %%A in ("!input!") do endlocal & set output="%%A"
set output

However, I just looked at my FTYPE output, and discovered some entries were not quoted, even if they contain spaces in the path! I don't think any of the answers on this page will handle this. In fact the entire premise behind the question may be flawed.

dbenham
  • 127,446
  • 28
  • 251
  • 390
  • Your comment *If we can rely on the fact that the first token will always be enclosed in quotes, then the solution is even easier.* seems to suggest that the first solution works even if the first token is **not** enclosed in quotes. – Piotr Dobrogost Nov 23 '11 at 20:40
  • @Piotr Dobrogost Correct. My 1st solution will work with unquoted paths as long as the path does not contain any spaces or special characters. If the path does contain space/special char then the path must be quoted. Of course the 2nd solution will never work without quotes. – dbenham Nov 23 '11 at 20:54
  • Thanks for info. However, I noticed I'm loosing `!` sign after `for /f "tokens=2 delims==" %%i in ('ftype Python.File') do set reg_entry=%%i` in my improved solution in [How to write a batch file showing path to executable and version of Python handling Python scripts on Windows?](http://stackoverflow.com/questions/7825780/) – Piotr Dobrogost Nov 23 '11 at 21:09
  • @Piotr Dobrogost - followed your link and provided solution in comment there. – dbenham Nov 23 '11 at 22:21
1

Essentially what you have to do is to convert the entire string into its elements, much as a parser would do it. In your case, lexical analysis would probably do the trick due to Windows rules about where spaces are allowed.

Fundamentally you need to build a finite state machine in your .cmd file with labels and conditional gotos. The FSA has states which process the various parts of the element you wish to collect. In a start state, you decide if you see a blank (skip and go back to start), a double quote (go to the part of the FSA that handles doubly-quoted strings), or something nonblank (go the the part of the FSA that collects nonblank characters).

The FSA part that collects double quoted strings picks off characters until it finds another double quote; that is what lets you capture blanks inside doubly quoted strings. I think you have to check for an "escaped" double quote (two of them in a row) and if found, replace them by a single double quote and continue collecting characters.

This is pretty ugly because the CMD script has truly awful string processing capabilities. Every (ugly) thing you need to know can be found by typing HELP SET to the DOS Command prompt. In particular, substringing is of the form %VAR:~n,m% which picks off m characters starting at index n in the environment variable %VAR%. I've found it useful to SET TEMP=%VAR% and then peel characters off of %TEMP% one by one by simple sequences such as

SET CHAR=%TEMP:~0,1%
SET TEMP=%TEMP:~1%

Enjoy.

Andriy M
  • 76,112
  • 17
  • 94
  • 154
Ira Baxter
  • 93,541
  • 22
  • 172
  • 341
  • I took the liberty of fixing, among other things, some minor mistakes in your code snippets, like unnecessary `%` in `SET %var%=...` or the wrong `1` where it should be `0` (meaning the first character). Please have a look just in case I've got things wrong. – Andriy M Oct 29 '11 at 13:27
  • Good explanation and yes, it can be solved with a finite state machine, but it's not necessary here as batch has this build in – jeb Nov 15 '11 at 06:53
  • Yes, if you can fork off another .CMD file. We're constrained in world to minimize the number of files lying around, so we're stuck with doing it the hard way. Unfortunately that makes us good at at it :-{ – Ira Baxter Nov 15 '11 at 08:00
  • You didn't need another CMD file, as Aacini, dbenham and I show in our answers (but perhaps I completly missundestand your last comment, as it's hard for me to translate it) – jeb Nov 19 '11 at 22:27
  • @jeb There are so many caveats ("double this, escape that, ...oops") and traps in the solutions that you folks have suggested that I think we'll stick to our scheme. At least we can adjust it any way we please. And it isn't *that* hard to write. – Ira Baxter Nov 19 '11 at 23:47