1

I found Rojo's really cool batch script located here: Read ini from windows batch file and it works great but there is 1 quirk that I can't figure out. Any time it encounters an & in the batch file, it tries to execute what immediately follows.

Here's an example ini file:

Password=3]#{$[1[Hx&_r

URL=http://www.osha.gov/pls/oshaweb/owasrch.search_form?p_doc_type=STANDARDS&p_toc_level=0 

upon execution

C:\Scripts\Batch>INI /i Database /v MP2 "C:\ProgramData\Infor\MP2 6.1 SQL Server Edition\Mp2_2.ini"
MP2
'_r' is not recognized as an internal or external command,operable program or batch file.
'p_toc_level' is not recognized as an internal or external command,operable program or batch file.
Community
  • 1
  • 1
Matt Williamson
  • 6,947
  • 1
  • 23
  • 36
  • I found the issues, but think, Rojo will change it by himself. – Endoro Apr 15 '13 at 13:14
  • I'm working on switching the script to a batch + JScript hybrid format, as JScript is more tolerant of special characters than pure batch. More later.... – rojo Apr 15 '13 at 14:45
  • Rojo - very cool. Thanks Endoro - What are the issues to the existing script? – Matt Williamson Apr 15 '13 at 16:40
  • @MattWilliamson - Test it and let me know how it works for you please. Also, since you're new around here, you might ought to [see this info](http://meta.stackexchange.com/a/5235/187716) regarding marking answers as accepted. – rojo Apr 15 '13 at 16:52
  • @Endoro - I'm just wondering what issues you found with the original code. Just for my own edification because I couldn't figure it out. – Matt Williamson Apr 16 '13 at 10:57
  • This is rojo's script. He can change the code [here](http://stackoverflow.com/a/15413717/2098699), if he wants :). – Endoro Apr 16 '13 at 11:31
  • @Endoro - I did. Or, rather, I left the pure batch script alone and just added the hybrid script as an alternative. :> If you have any suggestions for changes to the pure batch script, feel free to submit edits or pastebin fixes or whatever. – rojo Apr 16 '13 at 16:03

1 Answers1

2

Here you go Matt. I haven't tested this as thoroughly as it ought to be, so please let me know if you have any problems with it. After I spend a little time polishing it and feel that it's been thoroughly tested, I might update my answer on that Read ini from Windows batch file page as well.

@if (@a==@b) @end /* -- batch / JScript hybrid line to begin JScript comment

:: --------------------
:: ini.bat
:: ini.bat /? for usage
:: --------------------

@echo off
setlocal enabledelayedexpansion

goto begin

:: color code by jeb -- https://stackoverflow.com/a/5344911/1683264
:c
set "param=^%~2" !
set "param=!param:"=\"!"
findstr /p /A:%1 "." "!param!\..\X" nul
<nul set /p ".=%DEL%%DEL%%DEL%%DEL%%DEL%%DEL%%DEL%"
exit /b
:: but it doesn't handle slashes.  :(
:s
<NUL set /p "=/"&exit /b

:usage
for /F "tokens=1,2 delims=#" %%a in ('"prompt #$H#$E# & echo on & for %%b in (1) do rem"') do set "DEL=%%a"
<nul > X set /p ".=."

echo Usage:
call :c 07 "   query:"
call :c 0F " %~nx0 "&call :s&call :c 0F "i item ["&call :s&call :c 0F "s section] inifile"&echo;
call :c 07 "   create or modify:"
call :c 0F " %~nx0 "&call :s&call :c 0F "i item "&call :s&call :c 0F "v value ["&call :s&call :c 0F "s section] inifile"&echo;
call :c 07 "   delete:"
call :c 0F " %~nx0 "&call :s&call :c 0F "d item ["&call :s&call :c 0F "s section] inifile"&echo;
echo;
echo Take the following ini file for example:
echo;
echo    [Config]
echo    password=1234
echo    usertries=0
echo    allowterminate=0
echo;
echo To read the "password" value:
call :c 0F "   %~nx0 "&call :s&call :c 0F "s Config "&call :s&call :c 0F "i password inifile"&echo;
echo;
echo To modify the "usertries" value to 5:
call :c 0F "   %~nx0 "&call :s&call :c 0F "s Config "&call :s&call :c 0F "i usertries "&call :s&call :c 0F "v 5 inifile"&echo;
echo;
echo To add a "timestamp" key with a value of the current date and time:
call :c 0F "   %~nx0 "&call :s&call :c 0F "s Config "&call :s&call :c 0F "i timestamp "&call :s&call :c 0F "v ""%DEL%%%%%date%%%% %%%%time%%%%""%DEL% inifile"&echo;
echo;
echo To delete the "allowterminate" key:
call :c 0F "   %~nx0 "&call :s&call :c 0F "s Config "&call :s&call :c 0F "d allowterminate inifile"&echo;
echo;
call :c 07 "In the above examples, "&call :s
call :c 0F "s Config "
echo is optional, but will allow the selection of
echo a specific item where the ini file contains similar items in multiple sections.
del X
goto :EOF

:begin
if "%~1"=="" goto usage
for %%I in (item value section found) do set %%I=
for %%I in (%*) do (
    if defined next (
        if !next!==/i set "item=%%~I"
        if !next!==/v (
            set modify=true
            set "value=%%~I"
        )
        if !next!==/d (
            set "item=%%~I"
            set modify=true
            set delete=true
        )
        if !next!==/s set "section=%%~I"
        set next=
    ) else (
        for %%x in (/i /v /s /d) do if "%%~I"=="%%x" set "next=%%~I"
        if not defined next (
            set "arg=%%~I"
            if "!arg:~0,1!"=="/" (
                1>&2 echo Error: Unrecognized option "%%~I"
                1>&2 echo;
                1>&2 call :usage
                exit /b 1
            ) else set "inifile=%%~I"
        )
    )
)
for %%I in (item inifile) do if not defined %%I goto usage
if not exist "%inifile%" (
    1>&2 echo Error: %inifile% not found.
    exit /b 1
)

cscript /nologo /e:jscript "%~f0" "%inifile%" "!section!" "!item!" "!value!" "%modify%" "%delete%"

exit /b %ERRORLEVEL%

:: Begin JScript portion */
var inifile = WSH.Arguments(0),
section = WSH.Arguments(1),
item = WSH.Arguments(2),
value = WSH.Arguments(3),
modify = WSH.Arguments(4),
del = WSH.Arguments(5),
fso = new ActiveXObject("Scripting.FileSystemObject"),
stream = fso.OpenTextFile(inifile, 1),

// (stream.ReadAll() will not preserve blank lines.)
data = [];
while (!stream.atEndOfStream) { data.push(stream.ReadLine()); }
stream.Close();

// trims whitespace from beginning and end, returns lower case
String.prototype.unify = function() { return this.replace(/^\s+|\s+$/,'').toLowerCase(); };

// splices a new element into an array just after the last non-empty element.  If first arg is a number, start at that position and look backwards.
Array.prototype.cram = function() {
    for (var args=[], i=0; i<arguments.length; i++) { args.push(arguments[i]); }
    var i = (typeof args[0] == "number" && Math.floor(args[0]) == args[0]) ? args.shift() : this.length;
    while (i>0 && !this[--i].length) {};
    for (var j=0; j<args.length; j++) this.splice(++i, 0, args[j]);
}

function saveAndQuit() {
    while (data && !data[data.length - 1].length) data.pop();
    var stream = fso.OpenTextFile(inifile, 2, true);
    stream.Write(data.join('\r\n') + '\r\n');
    stream.Close();
    WSH.Quit(0);
}

function fatal(err) {
    WSH.StdErr.WriteLine(err);
    WSH.Quit(1);
}

if (section && !/^\[.+\]$/.test(section)) section = '[' + section + ']';

if (modify) {
    if (section) {
        for (var i=0; i<data.length; i++) {
            if (data[i].unify() == section.unify()) {
                for (var j=i + 1; j<data.length; j++) {
                    if (/^\s*\[.+\]\s*$/.test(data[j])) break;
                    var keyval = data[j].split('=') || [];
                    if (keyval.length < 2) continue;
                    var key = keyval.shift(), val = keyval.join('=');
                    if (key.unify() == item.unify()) {
                        if (del) data.splice(j, 1);
                        else {
                            data[j] = item + '=' + value;
                            WSH.Echo(value);
                        }
                        saveAndQuit();
                    }
                }
                if (del) fatal(item + ' not found in ' + section + ' in ' + inifile);
                data.cram(j ,item + '=' + value);
                WSH.Echo(value);
                saveAndQuit();
            }
        }
        if (del) fatal(section + ' not found in ' + inifile);
        data.cram('\r\n' + section, item + '=' + value);
        WSH.Echo(value);
        saveAndQuit();
    }
    else { // if (!section)
        for (var i=0; i<data.length; i++) {
            var keyval = data[i].split('=') || [];
            if (keyval.length < 2) continue;
            var key = keyval.shift(), val = keyval.join('=');
            if (key.unify() == item.unify()) {
                if (del) data.splice(i, 1);
                else {
                    data[i] = item + '=' + value;
                    WSH.Echo(value);
                }
                saveAndQuit();
            }
        }
        if (del) fatal(item + ' not found in ' + inifile);
        data.cram(item + '=' + value);
        WSH.Echo(value);
        saveAndQuit();
    }
}
else if (section) { // and if (!modify)
    for (var i=0; i<data.length; i++) {
        if (data[i].unify() == section.unify()) {
            for (var j=i + 1; j<data.length; j++) {
                if (/^\s*\[.+\]\s*$/.test(data[j])) fatal(item + ' not found in ' + section + ' in ' + inifile);
                var keyval = data[j].split('=') || [];
                if (keyval.length < 2) continue;
                var key = keyval.shift(), val = keyval.join('=');
                if (key.unify() == item.unify()) {
                    WSH.Echo(val);
                    WSH.Quit(0);
                }
            }
        }
    }
    fatal(section + ' not found in ' + inifile);
}
else { // if (item) and nothing else
    for (var i=0; i<data.length; i++) {
        var keyval = data[i].split('=') || [];
        if (keyval.length < 2) continue;
        var key = keyval.shift(), val = keyval.join('=');
        if (key.unify() == item.unify()) {
            WSH.Echo(val);
            WSH.Quit(0);
        }
    }
    fatal(item + ' not found in ' + inifile);
}

Update

The script should preserve and return the appropriate %ERRORLEVEL%. Success returns a value of 0; error, 1.

Also, I made the usage documentation prettier using some of jeb's code.

Community
  • 1
  • 1
rojo
  • 24,000
  • 5
  • 55
  • 101
  • I think `Array.cram()` should become a permanent addition to ECMAscript, don't you? :) – rojo Apr 15 '13 at 16:49
  • I seems to be working but it doesn't appear to be returning back to the calling batch file. The line immediately following is never processed. Maybe it's just me but test it by adding a pause after the call to INI.bat and see if you get the same result. – Matt Williamson Apr 15 '13 at 17:51
  • @MattWilliamson - in the script you're using to call `ini.bat`, use the actual word `call` to do so. `call ini.bat arguments` will prevent the `exit /b`s and `goto :EOF`s of `ini.bat` or any other `.bat` you call from exiting the caller, if that makes sense. – rojo Apr 15 '13 at 19:05
  • That makes perfect sense. Thanks I made your external INI.bat into an inline function in my batch file and it works perfectly but there is no output to tell if the function completed successfully or not. Not a big deal but it would be nice to have. Also, I don't get the `@if (@a==@b) @end` bit. What does it do? – Matt Williamson Apr 16 '13 at 10:53
  • When either querying or setting the value of a key, the script outputs the value. That's your notification of successful completion. Do something like `for /f "delims=" %%I in ('call ini.bat /s section /i item /v %value%') do if "%%I"="%value%" set success=true`. Also, I'm making a small change that will hopefully allow the batch script preserve ERRORLEVEL=0 for success, ERRORLEVEL=1 for failure. ... The `@if (@a==@b) @end` line is valid syntax evaluating as *false* in both batch and JScript. Its function is to do exactly nothing, but the `/*` at the end begins a multiline comment in JScript. – rojo Apr 16 '13 at 12:19
  • An Errorlevel would be perfect. If its function is nothing why is it there? I get that you have to put the /* in so the script doesn't read the batch portion but why can't you just do :: /* or REM /* ? – Matt Williamson Apr 16 '13 at 12:59
  • Because `::` and `REM` are invalid in JScript and would throw an error. See, it's a hybrid script, one script containing two languages. The batch half runs until `cscript thisfile.bat` re-runs the same script, but with a JScript interpreter. The batch code will be commented out from JScript, so JScript evaluates the first line as false, all the batch code as a multiline comment, and starts working where it encounters the `*/` in the middle of the script. The first line is the only line that is mandatorily evaluated by both batch and JScript, so it must be error-free in both. – rojo Apr 16 '13 at 13:39
  • How do I get the return value read from the INI into a variable in the batch script? – Matt Williamson Apr 19 '13 at 12:55
  • `for /f "delims=" %%I in ('call ini.bat args') do set "result=%%I"` ... or if your arguments contain an apostrophe, use the `usebackq` option. `for /f "usebackq delims=" %%I in (\`call ini.bat args\`) do set "result=%%I"` – rojo Apr 19 '13 at 13:19
  • How about if I changed the ini.bat into an inline function in the script? for /f "delims=" %%I in ('call :INI /i Database "C:\ProgramData\my.ini"') do set value=%%I gives me "Invalid attempt to call batch label outside of batch script." – Matt Williamson Apr 19 '13 at 13:33
  • Instead of putting the `for` loop around `call :INI args` put the `for` loop around the `cscript` line within the `:INI` subroutine. – rojo Apr 19 '13 at 13:40
  • This didn't work. It seems like as soon as the :INI sub terminates the value in the variable goes away. When I try to echo the variable, it's just giving me Echo is off. Wouldn't we have to create a return variable from the :INI sub? – Matt Williamson Apr 19 '13 at 14:38
  • Move the `setlocal enabledelayedexpansion` line out of the `:INI` sub up to the top of your script. That should allow any variables set within the `:INI` sub to remain defined in the scope of the main body of your script. – rojo Apr 19 '13 at 16:56
  • That did it. Thanks man. I really appreciate all the help you've given me. – Matt Williamson Apr 19 '13 at 17:19