0

I'm trying to figure this out but I think I'm way off. This is what I found on the interwebs:

copy /A data.js data.js.new /B > nul
del data.js
ren data.js.new data.js

Somehow, it does nothing.

How can I create a bat file to remove the last character in a file (the character is a comma, if that matters)?

richard
  • 12,263
  • 23
  • 95
  • 151
  • All you're doing is making a copy of the file, deleting the original, and then renaming the copy to the original filename. Of course it does nothing! – miken32 Dec 14 '15 at 21:54
  • lol ok. Well that's what I thought but that was an answer to a similar question...http://www.computing.net/answers/programming/delete-last-character-from-text-file/22746.html – richard Dec 14 '15 at 21:55
  • Would love to see the website that says that code will strip a comma as the last character of a file. – Squashman Dec 14 '15 at 21:56
  • I put the link in the comment! – richard Dec 14 '15 at 21:56
  • http://stackoverflow.com/q/26215770/62576 – Ken White Dec 14 '15 at 21:58
  • 3
    @richard They are removing the EOF marker from the file with that copy statement. – Squashman Dec 14 '15 at 22:05
  • @Squashman. I see...how can I remove the last character instead? – richard Dec 14 '15 at 22:06
  • Actually, you mean to remove the last character of the last non-empty line, do you? I'm asking because there might be several empty lines at the end of the file -- or does this not apply for your files? – aschipfl Dec 15 '15 at 07:28

3 Answers3

4
@echo off
setlocal EnableDelayedExpansion

rem Count the number of lines, minus one
for /F %%a in ('find /C /V "" ^< data.js') do set /A lines=%%a-1

< data.js (

   rem Copy number-1 lines
   for /L %%i in (1,1,%lines%) do (
      set "line="
      set /P "line="
      echo(!line!
   )

   rem Process last line
   set /P "line="
   echo(!line:~0,-1!

) > data.js.new
move /Y data.js.new data.js
Aacini
  • 65,180
  • 12
  • 72
  • 108
  • I'm both angry and impressed that your solution seems to be bulletproof. – rojo Dec 15 '15 at 13:45
  • @rojo: Well, blame `for /F` command for all problems related to special characters management! Reading lines with `set /P` and accessing variables with !delayed expansion! will never cause problems. I also think that the toggling delayed expansion technique is not just weird, but also slow. Your Batch file solution will be every time slower as the file size increases because the repetitive execution of `setlocal` commands with an environment every time larger. – Aacini Dec 15 '15 at 18:10
  • I do agree that `set /P` is faster for large files than `for /F`, as well as more gracefully handling special characters than `set` with delayed expansion. I'd still use the JScript solution if I were me, though. Testing with 1000 iterations against a 395-line, 17.5kb file: My batch method = 110 seconds. Your batch method = 38 seconds. My JScript hybrid method = 18 seconds. – rojo Dec 15 '15 at 18:47
2

Perhaps more complicated than it needs to be, but I wanted to avoid delayed expansion clobbering exclamation marks in your file contents while preserving blank lines. Batch isn't the best language for this sort of thing. If I were you, I'd just use PowerShell or JScript and substring(0, data.length - 1) or similar.

Anyway, here is a pure batch solution:

@echo off
setlocal

set "file=test.txt"
set "idx=0"

for /f "delims=" %%I in ('findstr /n "^" "%file%"') do (
    set /a idx += 1
    setlocal enabledelayedexpansion
    for %%x in (!idx!) do endlocal & set "line[%%x]=%%I"
)

setlocal enabledelayedexpansion
for %%I in ("!line[%idx%]!") do endlocal & set "lastline=%%~I"
set "line[%idx%]=%lastline:~0,-1%"

setlocal enabledelayedexpansion
>"%file%" (
    for /L %%I in (1,1,%idx%) do echo(!line[%%I]:*:=!
)

Here's a batch snippet that abuses PowerShell to accomplish the same task.

>out.txt powershell "$c = (gc test.txt) -join \"`n\"; $c.substring(0, $c.length - 1)"
move /y out.txt test.txt >NUL

Or here's a hybrid batch + JScript solution. Save it with a .bat extension.

@if (@CodeSection == @Batch) @then

@echo off & setlocal

>out.txt (
    cscript /nologo /e:Jscript "%~f0" < test.txt
)

move /y out.txt test.txt >NUL

goto :EOF
@end // end batch / begin JScript hybrid chimera

var data = WSH.StdIn.ReadAll();
WSH.StdOut.Write(data.substring(0, data.length - 1));
rojo
  • 24,000
  • 5
  • 55
  • 101
  • It fails with my file content of `:::Label1`, it removes the colons, too. Why you didn't use `set "lastline=!line[%ubound%]!"` instead of your FOR loop? – jeb Dec 15 '15 at 12:03
  • @jeb OK it handles colons better now. I used the `for` loop because I'm avoiding using `set` while delayed expansion is enabled, to avoid bothering exclamation marks in the data. I just use delayed expansion while retrieving values, rather than while setting them. is that weird? I suppose I could've used `call set` to avoid using delayed expansion as well. – rojo Dec 15 '15 at 12:59
  • Toggling delayed expansion isn't weird, it seems to be the best technic here. Only your line `set "line[%idx%]=%lastline:~0,-1%"` can fail when there are quotes in the content – jeb Dec 15 '15 at 13:11
  • It actually tests successfully for me with both matched and mismatched quotation marks. – rojo Dec 15 '15 at 13:15
  • :-) The last line in all of my files is `&"& I got you` – jeb Dec 15 '15 at 13:36
  • Aw snap. Well, my preference would be `powershell "$c = (gc test.txt) -join \"\`n\"; $c.substring(0, $c.length - 1)"` when execution time isn't a priority, or the JScript solution when speed is needed. – rojo Dec 15 '15 at 13:45
1

The following batch file removes the last character from the file data.js. The advantage of this approach is, that there is only a single loop, for the sake of performance. The idea behind it is to walk though the lines of the file and output each one, but delayed by one iteration, so the last line can be handled separately after the loop:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
> "data.js.new" (
    for /F "delims=" %%L in ('
        findstr /N /R "^" "data.js"
    ') do (
        setlocal EnableDelayedExpansion
        if defined PREV (
            set "PREV=!PREV:*:=!"
            echo(!PREV!
        )
        endlocal
        set "PREV=%%L"
    )
    setlocal EnableDelayedExpansion
    set "PREV=!PREV:*:=!"
    if defined PREV (
        echo(!PREV:~,-1!
    )
    endlocal
)
endlocal
> nul move /Y "data.js.new" "data.js"

The for /F loop does not iterate through the lines directly, but through the output of findstr /N /R "^", which returns all lines prefixed by the line number and :, even empty lines get prefixed, so no line appears empty to for /F, which would ignore such otherwise. To retrieve the original line in the loop, everything up to the first : is stripped off.

The delayed environment variable expansion (by setlocal/endlocal; see also set /?) is toggled in order for the script not not lose any special characters and/or not to crash on them.

aschipfl
  • 33,626
  • 12
  • 54
  • 99
  • I fixed it, rearranging the set/endlocals and removing LINE and Flag variables – jeb Dec 15 '15 at 11:55
  • Even if you don't need it here, are you aware of the solutions for the set/endlocal scope problems at [SO:Make an environment variable survive ENDLOCAL](http://stackoverflow.com/questions/3262287/make-an-environment-variable-survive-endlocal/8257951#8257951)? – jeb Dec 15 '15 at 12:33
  • Yes, @jeb, I am (+1), but this would not have helped me in my original script, because it means to execute this quite complex routine for every loop iteration, which would reduce the overall performance, hence counteract with my intention of improving performance by walking through the file once only (compared to the other pure batch file solutions)... – aschipfl Dec 15 '15 at 12:43