简体   繁体   中英

Filter duplicates in a batch arguments array

I'm writing a batch, where a user must input a list of devices to activate as batch arguments. The list may contain device numbers and/or names in random order with possible duplicates and invalids (ie non-existing or not Ready devices). The batch :default section stores an array dev_#=name of all currently Ready devices in random order. The initial task is to filter the user input to exclude duplicates and invalids, and present a list of sorted ready device numbers to activate. Then the devices from that list are activated. What efficient short code would do the filtering task?

@echo off
setlocal EnableDelayedExpansion
:default
set "dev_1=AB" & set "dev_2=BA" & set "dev_3=AC" & set "dev_16=CA" & set "dev_19=BC" & set "dev_6=CB" & set "dev_7=ZA"
set "dev_8=AZ" & set "dev_29=UA" & set "dev_10=YZ" & set "dev_18=YW" & set "dev_12=AK" & set "dev_13=AJ" & set "dev_14=HT"

rem batch arguments entered by user
rem 1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA

rem Desired sorted filtered output
rem 1 2 3 6 7 10 12

One liner with native batch commands - no way!

But it doesn't take a lot of code, especially if you are creative with designing some data structures. Note that the only code that counts is the :test routine. The rest is intitialization and test cases.

@echo off
setlocal

:: Clear devices
for /f "delims==" %%A in ('set dev. 2^>nul') do set "%%A="

:: Define devices
for %%A in (
  " 1=AB" " 2=BA" " 3=AC" "16=CA" "19=BC" " 6=CB" " 7=ZA"
  " 8=AZ" "29=UA" "10=YZ" "18=YW" "12=AK" "13=AJ" "14=HI"
) do for /f "tokens=1,2 delims==" %%a in (%%A) do for %%N in (%%a) do (
  set "dev.%%N= %%a"
  set "dev.%%b= %%a"
)

call :test 1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA
call :test sadf asdfa
call :test aj 6 13 CB
exit /b

:test
setlocal EnableDelayedExpansion

:: Clear selected
for /f "delims==" %%A in ('set selected. 2^>nul') do set "%%A="

:loadArgs  Define a variable for each unique device specified in args
if "%~1" neq "" (
  if defined dev.%~1 set "selected.!dev.%~1!=1"
  shift /1
  goto :loadArgs
)

:: Write out the unique values in sorted order
set "dev= "
for /f "tokens=2 delims== " %%A in ('set selected. 2^>nul') do set "dev=!dev!%%A "
echo selected devices = %dev:~1%

exit /b

--OUTPUT--

selected devices = 1 2 3 6 7 10 12
selected devices =
selected devices = 6 13

If you are confident that users will never include * , ? , or ) within the arguments, then a FOR loop can replace the GOTO loop. The :test routine simplifies to the following:

:test
setlocal EnableDelayedExpansion

:: Clear selected
for /f "delims==" %%A in ('set selected. 2^>nul') do set "%%A="

:: Define a variable for each unique device specified in args
for %%A in (%*) do if defined dev.%%~A set "selected.!dev.%%~A!=1"

:: Write out the unique values in sorted order
set "dev= "
for /f "tokens=2 delims== " %%A in ('set selected. 2^>nul') do set "dev=!dev!%%A "
echo selected devices = %dev:~1%

exit /b

Now if you are willing to step outside the bounds of pure native batch, then a one liner is certainly doable. I would use my JSORT.BAT and JREPL.BAT utilities. JSORT is able to sort numbers, so leading spaces are no longer needed in the initialization values.

Now the :test routine is truly a one liner:

@echo off
setlocal

:: Clear devices
for /f "delims==" %%A in ('set dev. 2^>nul') do set "%%A="

:: Define devices
for %%A in (
  "1=AB" "2=BA" "3=AC" "16=CA" "19=BC" "6=CB" "7=ZA"
  "8=AZ" "29=UA" "10=YZ" "18=YW" "12=AK" "13=AJ" "14=HI"
) do for /f "tokens=1,2 delims== " %%a in (%%A) do (
  set "dev.%%a=%%a"
  set "dev.%%b=%%a"
)

call :test 1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA
call :test sadf asdfa
call :test aj 6 13 CB
exit /b

:test
(for %%A in (. %*) do @call echo(%%dev.%%A%%) | jsort /n /u | jrepl "(\s|%%.*?%%)+" " " /m | jrepl "^ " ""
exit /b

or

:test
echo(%*|jrepl "(^|\s*)(\S+)" "env('dev.'+$2)" /jmatch | jsort /n /u | jrepl "\s+" " " /m | jrepl "^ " ""
exit /b

If you are open to alternatives, here is an example in VBScript (save in a file with .vbs extension instead of .bat ):

input = InputBox("", , "1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA")
values = Split(input)

' Adds the values to a sorted list that matches the case    
Set list = CreateObject( "System.Collections.Sortedlist" )    
For Each value in values
   Select Case value
      Case "1" , "AB" : list(1 ) = 0
      Case "2" , "BA" : list(2 ) = 0
      Case "3" , "AC" : list(3 ) = 0
      Case "6" , "CB" : list(6 ) = 0
      Case "7" , "ZA" : list(7 ) = 0
      Case "8" , "AZ" : list(8 ) = 0
      Case "10", "YZ" : list(10) = 0
      Case "12", "AK" : list(12) = 0
      Case "13", "AJ" : list(13) = 0
      Case "14", "HT" : list(14) = 0
      Case "16", "CA" : list(16) = 0
      Case "18", "YW" : list(18) = 0
      Case "19", "BC" : list(19) = 0
      Case "29", "UA" : list(29) = 0
   End Select
Next

' To show the items in the list   
For i = 0 To list.Count - 1
    output = output & list.GetKey(i) & " "
Next
MsgBox output

Another alternative is PowerShell (extension .ps1 ):

$s = "1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA"
$a = -split $s 

$a = $a -replace "AB", 1
$a = $a -replace "BA", 2
$a = $a -replace "AC", 3
$a = $a -replace "CB", 6
$a = $a -replace "ZA", 7
$a = $a -replace "AZ", 8
$a = $a -replace "YZ", 10
$a = $a -replace "AK", 12
$a = $a -replace "AJ", 13
$a = $a -replace "HT", 14
$a = $a -replace "CA", 16
$a = $a -replace "YW", 18
$a = $a -replace "BC", 19
$a = $a -replace "UA", 29

$a = $a | ? { -split "1 2 3 6 7 8 10 12 13 14 16 18 19 29" -contains $_ }
$l = $a | % { iex $_ }
$l = $l | sort-object -Unique 
$l -join " "

But running a PowerShell script is a bit challenging, so instead you can run it from a .bat file like this:

powershell -noprofile -command "&{"^
 "$s = '1 3 6 AB BA YZ 12 17 21 YK AU AK BA BU ZA' ;"^
 "$a = -split $s ;"^
 "$a = $a -replace 'AB', 1  ;"^
 "$a = $a -replace 'BA', 2  ;"^
 "$a = $a -replace 'AC', 3  ;"^
 "$a = $a -replace 'CB', 6  ;"^
 "$a = $a -replace 'ZA', 7  ;"^
 "$a = $a -replace 'AZ', 8  ;"^
 "$a = $a -replace 'YZ', 10 ;"^
 "$a = $a -replace 'AK', 12 ;"^
 "$a = $a -replace 'AJ', 13 ;"^
 "$a = $a -replace 'HT', 14 ;"^
 "$a = $a -replace 'CA', 16 ;"^
 "$a = $a -replace 'YW', 18 ;"^
 "$a = $a -replace 'BC', 19 ;"^
 "$a = $a -replace 'UA', 29 ;"^
 "$a = $a | ? { -split '1 2 3 6 7 8 10 12 13 14 16 18 19 29' -contains $_ } ;"^
 "$l = $a | %% { iex $_ } ;"^
 "$l = $l | sort-object -Unique ;"^
 "$l -join ' ' ;"^
 "}"

pause

It's an extended comment to show an application example for readers of native Cmd sorting used in Dave's above answer. It requires padding numbered vars with zeros.

@echo off
setlocal EnableDelayedExpansion
set "devs= 1 10 12 2 3 4 6 7 8 9"
for %%k in (!devs!) do (set /a "devl=100+%%k" & set "devl=!devl:~-2!"
    set "dev.!devl!=!devl!")
for /f "tokens=2 delims==" %%l in ('set dev.') do set "devn=!devn! %%l"
echo !devn!

exit /b

:: output
:: 01 02 03 04 06 07 08 09 10 12

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM