1

I have a txt file with values like this:

3.6.4.2
3.6.5.1
3.6.5.10
3.6.5.11
3.6.5.12
3.6.5.13
3.6.5.2
3.6.7.1
3.6.7.10
3.6.7.11
3.6.7.2
3.6.7.3

I need to write a batch script and return a sorted output. The problem is with last column, numbers .10 and .11 should go after .3 and so. I need the "latest version" to be on the bottom, which in this case is 3.6.7.11

In Linux I used "sort -t"." -k1n,1 -k2n,2 -k3n,3 -k4n,4" but I can't get it working with batch script.

Also I am not allowed to use Cygwin or PowerShell for some reasons.

In my batch code I am so far trying only various versions of this but nothing is working for me:

sort /+n versions.txt

The output used in this question is simply

sort versions.txt

It looks like that command sort is doing it correctly until I don't have 2 digits number used.

Mofi
  • 46,139
  • 17
  • 80
  • 143
Niki Szabo
  • 13
  • 5
  • Hi Mofi, I am new to the batch scripting, therefore I dont have any special batch code used so far, only using classic sort command – Niki Szabo Dec 22 '15 at 14:08
  • 2
    Check out my [JSORT.BAT sorting utility](http://www.dostips.com/forum/viewtopic.php?t=5595) over at DOSTips.com - it is a purely script based general purpose sorting solution. With JSORT.BAT, your solution is as simple as `call jsort versions.txt /n` – dbenham Dec 22 '15 at 15:30

5 Answers5

6

This is a common problem in Batch files. All sorting methods use a string comparison, where "10" comes before "2", so it is necessary to insert left zeros in the numbers less than 10. The Batch file below do that, but instead of generate a new file with the fixed numbers, it uses they to create an array that will be automatically sorted. After that, the array elements are shown in its natural (sorted) order.

EDIT: I modified the code in order to manage two digits numbers in the four parts.

@echo off
setlocal EnableDelayedExpansion

for /F "tokens=1-4 delims=." %%a in (input.txt) do (
    rem Patch the four numbers as a two digits ones
    set /A "a=100+%%a, b=100+%%b, c=100+%%c, d=100+%%d"
    rem Store line in the proper array element
    set "line[!a:~1!!b:~1!!c:~1!!d:~1!]=%%a.%%b.%%c.%%d"
)

rem Show array elements
for /F "tokens=2 delims==" %%a in ('set line[') do echo %%a

Output:

3.6.4.2
3.6.5.1
3.6.5.2
3.6.5.10
3.6.5.11
3.6.5.12
3.6.5.13
3.6.7.1
3.6.7.2
3.6.7.3
3.6.7.10
3.6.7.11
Aacini
  • 65,180
  • 12
  • 72
  • 108
  • 3
    Adding 100 to each number and using only last 2 digits is great. And great is also doing everything in memory using environment variables and the fact that command __set__ outputs environment variables always sorted by name. I like this solution much more than my solution. – Mofi Dec 22 '15 at 14:44
  • Thank you so much Aacini, this is indeed working for me. – Niki Szabo Dec 22 '15 at 14:48
2

Based on your example this will work. If you should somehow end up with examples like 3.6.5.02 and 3.6.5.2, then this code will not work.

@echo off
setlocal EnableDelayedExpansion
for /F "tokens=1-4 delims=. " %%G in (FILE.TXT) do (
   set N=0%%J
   set SORT[%%G%%H%%I!N:~-2!]=%%G.%%H.%%I.%%J
)
for /F "tokens=2 delims==" %%N in ('set SORT[') do echo %%N

pause
Squashman
  • 13,649
  • 5
  • 27
  • 36
  • Hi Squashman, thank you! That is indeed working, but if I add value for example 3.10.7.4 it puts it here: 3.1.0.6
    3.10.7.4
    3.1.2.1
    – Niki Szabo Dec 22 '15 at 14:28
  • @NikiSzabo, Yes. Sorting in batch gets very complicated because it does not do a numerical sort. I coded it based on your last node having numbers greater than 9. We have batch file called SORTN on dostips.com that will sort numerically for you. There is also a hybrid batch file on dostip.com called JSORT. Either of those should handle sorting just about anything you throw at them. – Squashman Dec 22 '15 at 14:42
  • 1
    It looks like Aacini and you had the same idea at same time on how to make the sort efficient in memory using environment variables. Your method for inserting always a leading 0 and using only last 2 digits is most likely even a little bit faster than adding 100 as no string to integer conversion needs to be done by command processor. This is also a great solution. – Mofi Dec 22 '15 at 14:53
  • @Mofi, it is a technique I have seen Aacini do plenty of times on Dostips.com. I suppose I could update it to take care of numbers greater than 9 in the other 3 nodes but Aacini has done that with his code already. Wish people would give accurate examples up front when they ask their questions. – Squashman Dec 22 '15 at 15:01
2

In pure batch scripting, you could use the following code snippet:

@echo off
setlocal EnableExtensions EnableDelayedExpansion
> "versions.tmp" (
    for /F "usebackq tokens=1,2,3,4 delims=." %%I in ("versions.txt") do (
        set "ITEM1=000%%I" & set "ITEM2=000%%J" & set "ITEM3=000%%K" & set "ITEM4=000%%L"
        echo !ITEM1:~-4!.!ITEM2:~-4!.!ITEM3:~-4!.!ITEM4:~-4!^|%%I.%%J.%%K.%%L
    )
)
< "versions.tmp" (
    for /F "tokens=2 delims=|" %%S in ('sort') do (
        echo %%S
    )
)
del /Q "versions.tmp"
endlocal
exit /B

This creates a temporary file, which contains the original line, prefixed with padded version numbers and a separtor |. Padded numbers means that each component is padded with leading zeros to four digits. Here is an example based on youe sample data:

0003.0006.0004.0002|3.6.4.2
0003.0006.0005.0001|3.6.5.1
0003.0006.0005.0010|3.6.5.10
0003.0006.0005.0011|3.6.5.11
0003.0006.0005.0012|3.6.5.12
0003.0006.0005.0013|3.6.5.13
0003.0006.0005.0002|3.6.5.2
0003.0006.0007.0001|3.6.7.1
0003.0006.0007.0010|3.6.7.10
0003.0006.0007.0011|3.6.7.11
0003.0006.0007.0002|3.6.7.2
0003.0006.0007.0003|3.6.7.3

This temporary file is then passed over to sort which does a purely alphabetic sorting. Since the numbers are padded, the sort order equals the true alphanumeric order. Here is the sorting result using the above example:

0003.0006.0004.0002|3.6.4.2
0003.0006.0005.0001|3.6.5.1
0003.0006.0005.0002|3.6.5.2
0003.0006.0005.0010|3.6.5.10
0003.0006.0005.0011|3.6.5.11
0003.0006.0005.0012|3.6.5.12
0003.0006.0005.0013|3.6.5.13
0003.0006.0007.0001|3.6.7.1
0003.0006.0007.0002|3.6.7.2
0003.0006.0007.0003|3.6.7.3
0003.0006.0007.0010|3.6.7.10
0003.0006.0007.0011|3.6.7.11

Finally, if you want to return the latest version number only, echo %%S by set "LVER=%%S" and place echo !LVER! after the closing ) of the second for /F loop.


Update:

Here is a solution that does not produce any temporary files, but uses a pipe | instead. Since a pipe creates new cmd instances for both left and right sides, and due to the fact that the (console) outputs are built in tiny bits and that there are multiple arithmetic operations done, it is rather slow:

@echo off
setlocal EnableExtensions DisableDelayedExpansion
(
    for /F "usebackq tokens=1,2,3,4 delims=." %%I in ("versions.txt") do @(
        set /A "10000+%%I" & echo( ^| set /P "=."
        set /A "10000+%%J" & echo( ^| set /P "=."
        set /A "10000+%%K" & echo( ^| set /P "=."
        set /A "10000+%%L" & echo(
    )
) | (
    for /F "tokens=1,2,3,4 delims=." %%S in ('sort') do @(
        set /A "%%S-10000" & echo( ^| set /P "=."
        set /A "%%T-10000" & echo( ^| set /P "=."
        set /A "%%U-10000" & echo( ^| set /P "=."
        set /A "%%V-10000" & echo(
    )
)
endlocal
exit /B
Left Side of the Pipe:

Instead of the substring expansion syntax like in the above approach using a temporary file, I add 10000 to every component of the version numbers (similar to Aacini's answer) in order to avoid delayed expansion, because this is not enabled in either new cmd instance. To output the resulting values, I make use of the fact that either of the for /F loops are running in cmd context rather than in batch context, where set /A outputs the result to STDOUT. set /A does not terminate its output with a line-break, so I use set /P to append a . after each but the last item, which in turn does not append a line-break. For the last item I append a line-break using a blank echo.

Right Side of the Pipe:

The sorting is again accomplished by the sort command, whose output is parsed by for /F. Here the previously added value 10000 is subtracted from each component to retrieve the original numbers. For outputting the result to the console, the same technique is used as for the other side of the pipe.

Piped Data:

The data passed over by the pipe looks like this (relying on the example of the question once again):

10003.10006.10004.10002
10003.10006.10005.10001
10003.10006.10005.10010
10003.10006.10005.10011
10003.10006.10005.10012
10003.10006.10005.10013
10003.10006.10005.10002
10003.10006.10007.10001
10003.10006.10007.10010
10003.10006.10007.10011
10003.10006.10007.10002
10003.10006.10007.10003
Community
  • 1
  • 1
aschipfl
  • 33,626
  • 12
  • 54
  • 99
2

Here is my solution working with 2 temporary files which works also if one of the other 3 version numbers becomes ever greater than 9.

@echo off
setlocal EnableExtensions EnableDelayedExpansion

set "VersionsFile=versions.txt"

rem Delete all temporary files perhaps existing from a previous
rem run if user of batch file has broken last batch processing.

if exist "%TEMP%\%~n0_?.tmp" del "%TEMP%\%~n0_?.tmp"

rem Insert a leading 0 before each number in version string if the
rem number is smaller than 10. And insert additionally a period at
rem start of each line. The new lines are written to a temporary file.

for /F "useback tokens=1-4 delims=." %%A in ("%VersionsFile%") do (
    if %%A LSS 10 ( set "Line=.0%%A." ) else ( set "Line=.%%A." )
    if %%B LSS 10 ( set "Line=!Line!0%%B." ) else ( set "Line=!Line!%%B." )
    if %%C LSS 10 ( set "Line=!Line!0%%C." ) else ( set "Line=!Line!%%C." )
    if %%D LSS 10 ( set "Line=!Line!0%%D" ) else ( set "Line=!Line!%%D" )
    echo !Line!>>"%TEMP%\%~n0_1.tmp"
)

rem Sort this temporary file with output written to one more temporary file.
rem The output could be also printed to __stdout__ and directly processed.

%SystemRoot%\System32\sort.exe "%TEMP%\%~n0_1.tmp" /O "%TEMP%\%~n0_2.tmp"

rem Delete the versions file before creating new with sorted lines.

del "%VersionsFile%"

rem Read sorted lines, remove all leading zeros after a period and also
rem the period inserted at start of each line making it easier to remove
rem all leading zeros. The lines are written back to the versions file.

for /F "useback delims=" %%L in ("%TEMP%\%~n0_2.tmp") do (
    set "Line=%%L"
    set "Line=!Line:.0=.!"
    set "Line=!Line:~1!"
    echo !Line!>>"%VersionsFile%"
)

rem Finally delete the two temporary files used by this batch file.

del "%TEMP%\%~n0_?.tmp" >nul

endlocal

The first temporary file with unsorted lines contains for input example:

.03.06.04.02
.03.06.05.01
.03.06.05.10
.03.06.05.11
.03.06.05.12
.03.06.05.13
.03.06.05.02
.03.06.07.01
.03.06.07.10
.03.06.07.11
.03.06.07.02
.03.06.07.03

The second temporary file with the sorted lines contains for input example:

.03.06.04.02
.03.06.05.01
.03.06.05.02
.03.06.05.10
.03.06.05.11
.03.06.05.12
.03.06.05.13
.03.06.07.01
.03.06.07.02
.03.06.07.03
.03.06.07.10
.03.06.07.11

For understanding the used commands and how they work, open a command prompt window, execute there the following commands, and read entirely all help pages displayed for each command very carefully.

  • call /? ... explains %~n0 (name of batch file without path and file extension)
  • del /?
  • echo /?
  • endlocal /?
  • for /?
  • if /?
  • rem /?
  • set /?
  • setlocal /?
  • sort /?
Mofi
  • 46,139
  • 17
  • 80
  • 143
  • Seems like you and me had a similar idea at the same time, using `sort` and temporary file; this is perhaps not as elegant as Aacini's solution, but it is great though, it is well explained... – aschipfl Dec 22 '15 at 15:08
2

Easiest solution would be to invoke PowerShell and treat the version numbers as actual System.Version objects. That way the Major, Minor, Build, and Revision segments will be treated as integers and sorted accordingly. You can call this from a batch script:

powershell "(gc textfile.txt | %%{[version]$_} | sort) -split ' '"

That's it. Easy one-liner. If doing it from the cmd prompt, replace the double %% with a single %. Here's a breakdown of the command:

  • Get the following as a string:
    • Get the contents of textfile.txt
    • For each line, cast the data as a System.Version object.
    • Sort as versions
  • The string will be a single line separated by spaces. Split on the spaces.

Output is as follows:

3.6.4.2
3.6.5.1
3.6.5.2
3.6.5.10
3.6.5.11
3.6.5.12
3.6.5.13
3.6.7.1
3.6.7.2
3.6.7.3
3.6.7.10
3.6.7.11

Partial credit should go to this question and accepted answer.

Community
  • 1
  • 1
rojo
  • 24,000
  • 5
  • 55
  • 101