问题
I am obsessed with idea of mimicking functionality of 8-bit era programs by virtue of pure command line. So I want to make an array of menu options which can be selected with nominal (since there are no keymap for arrowkeys) left and right keys and confirmed with Enter. To keep things clear there is a working example I wrote. Please pay attention for ANSI escape codes I use A LOT, not sure if ESC control character would display in browser.
@ECHO OFF
ECHO Chose and option with [36mZ[0m for left, [36mX[0m for right, [36mC[0m for confirm
:PICKMENU
SETLOCAL
SET menumsg="[7mExit [0m Next"
SET menumsgDef="[7mExit [0m Next"
SET menumsgNxt="[0mExit [7m Next"
:subpick
CHOICE /N /C zxc /m:[3;1H%menumsg%
IF %errorlevel% EQU 0 ECHO Just in case of errors & GOTO PICKMENU
IF %errorlevel% EQU 1 SET menumsg=%menumsgDef% & GOTO subpick
IF %errorlevel% EQU 2 SET menumsg=%menumsgNxt% & GOTO subpick
IF %errorlevel% EQU 3 GOTO confirm
:confirm
IF %menumsg% EQU %menumsgNxt% (ECHO [0mNext option is chosen & GOTO PICKMENU) ELSE (ECHO ^Exit is chosen! & TIMEOUT 2 >NUL & EXIT)
ENDLOCAL
PAUSE
My problem begins when I try to wrap this menu in some kind of a function, where I could pass arguments instead of making hodgie code every time I need menu or dialog:
@ECHO OFF
REM ----So I start with setting variables with idea of argument string tokenisation on the mind----
SET $teststring=OptionOne OptionTwo OptionThree
SET /A $currentitem=1
REM ----I would use function that I found here on Stackoverflow to count amount of options in my string.----
REM ----This is magic for me, without this solution I would be using FOR loop and delims----
CALL :COUNTOPTIONS %$teststring%
:COUNTOPTIONS
SETLOCAL EnableDelayedExpansion
SET /A Count=0
:repeatme
IF NOT "%1"=="" (
SET /A Count+=1
SHIFT
GOTO :repeatme
)
ENDLOCAL & SET $optionsamount=%Count%
ECHO The number of words is %$optionsamount%
PAUSE
:PICKCURRENT
REM ----So I got maximum number of options, I guess it would be useful since by design user could stumble upon the
REM ----edge of menu by occasionaly pressing "left" or "right" controls too much. Thats how I limit the variable...
IF %$currentitem% LSS 1 SET /A $currentitem=1
IF %$currentitem% GTR %$optionsamount% SET /A $currentitem=%$optionsamount%
ECHO %$optionsamount%
REM ----This is where I completely lost it. I am trying to echo "options" string as separated items to the target location...-----
FOR /F "tokens=*" %%A IN ("%$teststring%") DO ECHO [1;1H%%A
REM ----...and echo "currently selected" item into the same place...
FOR /F "tokens=%$currentitem%" %%B IN ("%$itemstring%") DO SET $selector=[1;1H[7m%%B[0m
REM ----...and make it interactive with choice----
CHOICE /N /C zxc /m:%$selector%
IF %errorlevel% EQU 0 ECHO oops & GOTO PICKITEM
IF %errorlevel% EQU 1 SET /A "$currentitem=$currentitem-1" & GOTO PICKCURRENT
IF %errorlevel% EQU 2 SET /A "$currentitem=$currentitem+1" & GOTO PICKCURRENT
IF %errorlevel% EQU 3 GOTO confirm
PAUSE
(I shoveled script structure in favor of illustrating what am I doing).
Obviously, my problem is in lack of understanding loops and functions, but reading trough reference on ss64 and intense googling were not that enlightening so far. So I would appreciate pointing to education materials as well as comments on my code. I'd like to emphasize that the problem statement doesn't imply usage of additional libraries or resource kits.
回答1:
Asking for recommendations for libraries and tutorials is beyond the scope of S.O., and your code was tl;dr for so close to bed time. Maybe I'll give it another look tomorrow. The most obvious problem I see is that your functions are located within the main runtime, and you never goto past them. They should be after the final goto :EOF or exit /B so they aren't executed in the normal top-to-bottom sequence of the batch script. Example:
@echo off & setlocal
call :add 5 9 sum
echo The sum is %sum%
exit /b
rem // Functions go here, below exit /b
:add <num1> <num2> <return_var>
set /a %~3=%~1 + %~2
goto :EOF
:subtract <num1> <num2> <return_var>
set /a %~3=%~1 - %~2
goto :EOF
But I thought at least for now I might share a more complete menu script that doesn't require an ansi interpreter.
<# : Batch portion
@echo off & setlocal enabledelayedexpansion
set "menu[0]=Format C:"
set "menu[1]=Send spam to boss"
set "menu[2]=Truncate database *"
set "menu[3]=Randomize user password"
set "menu[4]=Download Dilbert"
set "menu[5]=Hack local AD"
for /L %%I in (6,1,15) do set "menu[%%I]=loop-generated demo item %%I"
set "default=0"
powershell -noprofile "iex (${%~f0} | out-string)"
echo You chose !menu[%ERRORLEVEL%]!.
goto :EOF
: end batch / begin PowerShell hybrid chimera #>
$menutitle = "CHOOSE YOUR WEAPON"
$menuprompt = "Use the arrow keys. Hit Enter to select."
$menufgc = "yellow"
$menubgc = "darkblue"
[int]$selection = $env:default
$h = $Host.UI.RawUI.WindowSize.Height
$w = $Host.UI.RawUI.WindowSize.Width
# assume the dialog must be at least as wide as the menu prompt
$len = [math]::max($menuprompt.length, $menutitle.length)
# get all environment vars matching menu[int]
$menu = gci env: | ?{ $_.Name -match "^menu\[(\d+)\]$" } | sort @{
# sort on array index as int
Expression={[int][RegEx]::Match($_.Name, '\d+').Value}
} | %{
$val = $_.Value.trim()
# truncate long values
if ($val.length -gt ($w - 8)) { $val = $val.Substring(0,($w - 11)) + "..." }
$val
# as long as we're looping through all vals anyway, check whether the
# dialog needs to be widened
$len = [math]::max($val.Length, $len)
}
# dialog must accomodate string length + box borders + idx label
$dialogwidth = $len + 8
# center horizontally
$xpos = [math]::floor(($w - $dialogwidth) / 2)
# center at top 1/3 of the console
$ypos = [math]::floor(($h - ($menu.Length + 4)) / 3)
# Is the console window scrolled?
$offY = [console]::WindowTop
# top left corner coords...
$x = [math]::max(($xpos - 1), 0); $y = [math]::max(($offY + $ypos - 1), 0)
$coords = New-Object Management.Automation.Host.Coordinates $x, $y
# ... to the bottom right corner coords
$rect = New-Object Management.Automation.Host.Rectangle `
$coords.X, $coords.Y, ($w - $xpos + 1), ($offY + $ypos + $menu.length + 4 + 1)
# The original console contents will be restored later.
$buffer = $Host.UI.RawUI.GetBufferContents($rect)
function destroy { $Host.UI.RawUI.SetBufferContents($coords,$buffer) }
$box = @{
"nw" = [char]0x2554 # northwest corner
"ns" = [char]0x2550 # horizontal line
"ne" = [char]0x2557 # northeast corner
"ew" = [char]0x2551 # vertical line
"sw" = [char]0x255A # southwest corner
"se" = [char]0x255D # southeast corner
"lsel" = [char]0x2192 # right arrow
"rsel" = [char]0x2190 # left arrow
}
function WriteTo-Pos ([string]$str, [int]$x = 0, [int]$y = 0,
[string]$bgc = $menubgc, [string]$fgc = $menufgc) {
$saveY = [console]::CursorTop
[console]::setcursorposition($x,$offY+$y)
Write-Host $str -b $bgc -f $fgc -nonewline
[console]::setcursorposition(0,$saveY)
}
# Wait for keypress of a recognized key, return virtual key code
function getKey {
# PgUp/PgDn + arrows + enter + 0-9
$valid = 33..34 + 37..40 + 13 + 48..(47 + [math]::min($menu.length, 10))
# 10=a, 11=b, etc.
if ($menu.length -gt 10) { $valid += 65..(54 + $menu.length) }
while (-not ($valid -contains $keycode)) {
$keycode = $Host.UI.RawUI.ReadKey('NoEcho,IncludeKeyDown').VirtualKeyCode
}
$keycode
}
# for centering the title and footer prompt
function center([string]$what, [string]$fill = " ") {
$lpad = $fill * [math]::max([math]::floor(($dialogwidth - 4 - $what.length) / 2), 0)
$rpad = $fill * [math]::max(($dialogwidth - 4 - $what.length - $lpad.length), 0)
"$lpad $what $rpad"
}
function menu {
$y = $ypos
WriteTo-Pos ($box.nw + (center $menutitle $box.ns) + $box.ne) $xpos ($y++)
WriteTo-Pos ($box.ew + (" " * ($dialogwidth - 2)) + $box.ew) $xpos ($y++)
# while $item can equal $menu[$i++] without error...
for ($i=0; $item = $menu[$i]; $i++) {
$rtpad = " " * [math]::max(($dialogwidth - 8 - $item.length), 0)
if ($i -eq $selection) {
WriteTo-Pos ($box.ew + " " + $box.lsel + " $item " + $box.rsel + $rtpad `
+ $box.ew) $xpos ($y++) $menufgc $menubgc
} else {
# if $i is 2 digits, switch to the alphabet for labeling
$idx = $i; if ($i -gt 9) { [char]$idx = $i + 55 }
WriteTo-Pos ($box.ew + " $idx`: $item $rtpad" + $box.ew) $xpos ($y++)
}
}
WriteTo-Pos ($box.sw + ([string]$box.ns * ($dialogwidth - 2) + $box.se)) $xpos ($y++)
WriteTo-Pos (" " + (center $menuprompt) + " ") $xpos ($y++)
1
}
while (menu) {
[int]$key = getKey
switch ($key) {
33 { $selection = 0; break } # PgUp/PgDn
34 { $selection = $menu.length - 1; break }
37 {} # left or up
38 { if ($selection) { $selection-- }; break }
39 {} # right or down
40 { if ($selection -lt ($menu.length - 1)) { $selection++ }; break }
# letter, number, or enter
default {
# if alpha key, align with VirtualKeyCodes of number keys
if ($key -gt 64) { $key -= 7 }
if ($key -gt 13) {$selection = $key - 48}
# restore the original console buffer contents
destroy
exit($selection)
}
}
}
回答2:
Well, I was able to find solution myself just in few sleepless days:
@ECHO OFF
:SELECTLOOP
SETLOCAL EnableDelayedExpansion
SET /A current=1
:subpick
ECHO ------
SET /A count=0
FOR /F "delims=" %%M IN ("ONE TWO THREE FOUR FIVE") DO FOR %%N IN (%%M) DO (
SET /A count+=1
SET str!count!=%%N
IF !count! NEQ !current! (ECHO %%N) ELSE (ECHO ^>%%N^<))
CHOICE /N /C zxc /m:"------"
IF %errorlevel% EQU 0 ECHO error message & GOTO SELECTLOOP
IF %errorlevel% EQU 1 set /A current-=1 & GOTO subcheck
IF %errorlevel% EQU 2 set /A current+=1 & GOTO subcheck
IF %errorlevel% EQU 3 (ECHO !current! IS CONFIRMED) & (TIMEOUT 3 >NUL)
:subcheck
CLS
IF !current! LSS 1 SET /A current=1
IF !current! GTR !count! SET /A current=!count!
GOTO subpick
PAUSE
来源:https://stackoverflow.com/questions/38175957/batch-script-navigation-menu-with-highlighted-selection