Windows batch version of Unix select command? - bash

I'm trying to create a Windows version of a simple bash script I have, but I cannot seem to find a Windows version of the Unix 'select' command. Is there one?
Here's the script:
#!/bin/bash
echo "Enter the number of the file you want to select:"
select THE_FILE in someDir/*;
do
echo "You picked $THE_FILE ($REPLY)"
# Will do some stuff here.
break;
done
It was very easy to find this Linux example, so I am a bit perplexed that I cannot seem to find a Windows equivalent.
Edit: The select command prompts the user to select a file from a given directory (that's an oversimplification). To clarify, the script from above will produce the following output, assuming there is a subdirectory 'someDir' with only those three text files in it.
Enter the number of the file you want to select:
1) someDir/somefile1.txt
2) someDir/somefile2.txt
3) someDir/somefile3.txt
#? 2
You picked someDir/somefile2.txt (2)

Windows batch commands does not include any equivalent to the select command. So you will have to build your own version.
You will need:
call command. Create a subroutine and reuse it
for command to iterate over the files, or for /f to iterate over the output of another command returning the list of files
For short lists, choice command is more friendly to the user as it is not needed to press enter, but for longer lists or if you don't know the number of files to select, set /p is a better option
Here, just a sample
#echo off
setlocal enableextensions disabledelayedexpansion
call :select "someDir\*" THE_FILE
echo You picked %THE_FILE%
goto :eof
:select mask returnVar
setlocal enableextensions disabledelayedexpansion
rem Configure internal variables
set "fileNumber="
set "maxFiles=-1"
for /f "delims==" %%a in ('2^>nul set f[') do set "%%a="
echo Enter the number of the file you want to select:
rem Search files, show list and create array with file list
rem Using xcopy to get the list of files because it will show
rem relative paths when a relative mask is used as input
for /f "tokens=1,* delims=:" %%a in ('
xcopy "%~1" "%temp%" /l ^| findstr /n /r /c:"[\\\.:]"
') do (
echo %%a^) %%b
set "f[%%a]=%%~fb"
set "maxFiles=%%a"
)
rem Prompt
:select.ask
set /p "fileNumber=#? "
rem Validate input
set /a "fileNumber=fileNumber+0" 2>nul
if %fileNumber% gtr %maxFiles% set "fileNumber=-1"
if %fileNumber% lss 1 set "fileNumber="
if not defined fileNumber (
echo Wrong selection
goto :select.ask
)
rem Retrieve file from array
setlocal enabledelayedexpansion
for %%a in ("!f[%fileNumber%]!") do (
endlocal
set "selectedFile=%%~a"
)
rem Return selection to caller
endlocal & set "%~2=%selectedFile%"
goto :eof

See Choice /? or set /?.
choice /c:yn
If errorelevel 1 if not errorlevel 2 echo Y was chosen

Although a bash select equivalent is not included in Windows Batch, it is very easy to write your own. It may be a subroutine with the exact same parameters of Linux select so you don't need to learn something new in order to use it; this way, the "in" word in second parameter will not be used in the Batch code.
#echo off
call :select THE_FILE in someDir/*
echo You picked %THE_FILE% (%errorlevel%)
goto :EOF
rem Subroutine that emulates Linux's select
:select returnVar in directory
setlocal EnableDelayedExpansion
echo Enter the number of the file you want to select:
rem Show the files and create an array with them
set n=0
for %%a in (%3) do (
set /A n+=1
set file[!n!]=%%a
echo !n!^) %%a
)
rem Get the number of the desired file
:getNumber
set /P "number=#? "
if not defined file[%number%] goto getNumber
rem Return selected file to caller
for /F "delims=" %%a in ("!file[%number%]!") do endlocal & set "%1=%%a" & exit /B %number%
Previous code is straigthforward, but post a comment here if you have any doubt about it. Perhaps the most complex part is the for /F command at last line, that is required to save the value of the !filename[%number%]! in the %%a FOR parameter before execute the endlocal and the assignment to the first parameter, that is the way to return that value to the calling program. If endlocal would be executed first, the !delayed expansion! of the variable will no longer work...
Although this code does not return the path of the selected file, it is very easy to add such feature, but the code will complicate a little.

While a batch-file solution is desired by the OP, it is worth presenting a PowerShell solution that is much more concise:
($files = get-childitem -file someDir\) | % { $i=0 } { ++$i; "$i) $_" }
$reply = read-host -p "Enter the number of the file you want to select"
"You picked $($files[$reply-1]) ($reply)"
$files = get-childitem -file someDir\ collects all files in directory someDir in variable $files
Note that the -file option for restricting child items to files requires PowerShell 3.0 or higher; on earlier versions, pipe to ? { -not $_.PSIsContainer } instead.
% { $i=0 } { ++$i; "$i) $_" } outputs each filename prefixed with its 1-based index.
% is shorthand for the ForEach-Object object cmdlet, which processes a block of code for each input object
{ $i=0 } initializes the index variable (executed once, before iteration)
{ ++$i; "$i) $_" } is executed for each input object, $_; ++$i increments the index, "$i) $_" prints the index followed by ) and a space, followed by the input object's default property, which is the filename in this case. (If you wanted to print the full path instead, for instance, you'd use "$i) $($_.fullname)").
Note how no explicit output (print) command is needed - the results are output to the terminal by default.
$reply = read-host -p "Enter the number of the file you want to select" reads a single line from the terminal with the specified prompt and stores the user's input in variable $reply.
"You picked $($files[$reply-1]) ($reply)" outputs the result; again, no explicit output command is required.

Related

Manipulate path string BATCH

I need to process a full path to a file, i.e. C:\fold1\fold2...\foldN\filename.dat in order to get the parent file folder and file name separately, that is root=C:\fold1\fold2...\foldN and file=filaname.dat.
This because I want to generalise a call to a program no matter from where you call the main .bat (this one), such to be able to perform a pushd to %root% before calling this specific program.
I am working at this, but I get stack when posing line=%%c:
: before remove quotes from %var%
set var=%var:"=%
if exist %temp% (
if exist %temp%\filepath.txt del %temp%\filepath.txt
echo %var% > %temp%\filepath.txt
)
for /f "tokens=1,* delims= " %%z in (%temp%\filepath.txt) do (
set line=%%z
echo Line writes %line%.
set root=empty
goto :processtoken
)
:processtoken
for /f "tokens=1,* delims=\" %%b in ("%line%") do (
echo %%b
echo %%c
set line=%%c rem this does not work, why??
echo Now line writes %line%
if "%root%"=="empty" (
echo [INFO] Root is empty, initialising..
echo %%c
set root=%%c
) else (
set root=%root%\%%c
)
echo Root is %root%
goto :end
)
The problem I am facing is that when I print to see how %line% has changed, it shows that %line% (after set line=%%c corresponds to the FULL PATH (while my intention is to recursively get to the file name, I still need to add the condition in finding the "\" string in %%c, when not present anymore it will mean we got to the final step, i.e. %root% will now correspond to the final root folder).
Thanks to who will try to help me resolving this issue.
EDIT:
this main program is called as follow:
prog arg1 arg2 arg3 arg4 arg5
arg5 is the path to the file which will internally be the argument for this other program. To be as general as possible I want to made functional this program no matter from where you call it. Still you have two options:
you are already in the folder containing the file to be processed by this internally called program, in such case arg5 will be passed as filename.dat (without quotes, except if it contains spaces)
you are not in the root folder so you pass directly the FULL PATH (in double quotes if it contains spaces).
The problem is in case 2, since this internal program works properly when you call it from root directory and you pass to it only FILENAME.DAT.
This is why I posed this question. %var% is simply arg5, in the ways I explained hereby.
I hope I have been a little more clear than before.
PS. I'm not an experienced programmer in all ways, so I do apologise if I miss in clearance and professionalism. The write/read from/to %temp% folder was just to exploit a newer way of programming in batch, nothing else. I knew it was superfluous.
since you mentioned set var=%5 in a comment:
set "root=%~dp5"
set "file=%~nx5"
should be all you need. See call /? for more details.
You should take a look at the for-variable modifiers (FOR /?)
root=%%~dpz is used to expand to: d=drive and p=path of z
file=%%~nxz is used to expand to: n=name and x=extension of z
Then you can change your block to
for /f "tokens=1,* delims= " %%z in (%temp%\filepath.txt) do (
set "line=%%z"
set "root=%%~dpz"
set "file=%%~nxz"
REM ** Show the variables
set root
set file
)
Another way to do this in a batch-file would be to use the PowerShell Split-Path command designed to do this.
SET "THEFILE=%TEMP%\file path.txt"
FOR /F "delims=" %%A IN ('powershell -NoLogo -NoProfile -Command
"""$([Convert]::ToChar(34))$(Split-Path -Path """%THEFILE%""")$([Convert]::ToChar(34)) $([Convert]::ToChar(34))$(Split-Path -Path """%THEFILE%""" -Leaf)$([Convert]::ToChar(34))"""') DO (
SET "FILESTRING=%%A"
)
SET "PATHPART="
FOR %%A IN (%FILESTRING%) DO (
IF NOT DEFINED PATHPART (SET "PATHPART=%%~A") ELSE (SET "FILEPART=%%~A")
)
ECHO PATHPART is "%PATHPART%"
ECHO FILEPART is "%FILEPART%"
Of course, it is easier if the script is written for PowerShell.
$TheFile = "$Env:TEMP\filepath.txt"
$PathPart = Split-Path -Path $TheFile
$FilePart = Split-Path -Path $TheFile -Leaf
UPDATE:
Actually, it appears that splitting the path is not needed to use PUSHD. Just add \.. to the end.
PUSHD C:\Users\joe\afile.txt\..

Loop through files in a folder and check if they have different extensions

I have a folder that contains files; each document should have .pdf and .xml format. I need to write a BAT file to run from a scheduled task to verify that both documents exist for each.
My logic is:
loop through files in the folder
strip each file to its name without extension
check that same name files exist for both .xml and pdf.
if not mark a flag variable as problem
when done, if the flag variable is marked, send an Email notification
I know how to use blat to sending email, but I'm having trouble to execute the loop. I found a way to get path and file name without extension but can't merge them.
I've used batch files a few time, before but I'm far from an expert. What am I missing?
Here's the code I have so far:
set "FolderPath=E:\TestBat\Test\"
echo %FolderPath%
for %%f in (%FolderPath%*) do (
set /p val=<%%f
For %%A in ("%%f") do (
Set Folder=%%~dpA
Set Name=%%~nxA
)
echo Folder is: %Folder%
echo Name is: %Name%
if NOT EXIST %FolderPath%%name%.xml
set flag=MISSING
if NOT EXIST %FolderPath%%name%.pdf
set flag=MISSING
)
echo %Flag%
pause
There is no need for fancy code for a task such as this:
#Echo Off
Set "FolderPath=E:\TestBat\Test"
If /I Not "%CD%"=="%FolderPath%" PushD "%FolderPath%" 2>Nul||Exit/B
Set "flag="
For %%A In (*.pdf *.xml) Do (
If /I "%%~xA"==".pdf" (If Not Exist "%%~nA.xml" Set "flag=MISSING")
If /I "%%~xA"==".xml" (If Not Exist "%%~nA.pdf" Set "flag=MISSING")
)
If Defined flag Echo=%flag%
Timeout -1
Something like this :
set "FolderPath=E:\TestBat\Test\"
pushd "%FolderPath%"
for %%a in (*.xml) do (
if exist "%%~na.pdf"(
echo ok
) else (
rem do what you want here
echo Missing
)
)
popd
Is this what you want?
#echo off
setlocal enabledelayedexpansion
set "FolderPath=E:\TestBat\Test\"
echo !FolderPath!
for /f "usebackq delims=" %%f in (`dir !FolderPath! /B`) do (
set /p val=<%%f
For %%A in ("%%f") do (
Set Folder=%%~dpA
Set name=%%~nxA
)
echo Folder is: !Folder!
echo Name is: !name!
if NOT EXIST !FolderPath!!name!.xml set flag=MISSING
if NOT EXIST !FolderPath!!name!.pdf set flag=MISSING
)
echo Flag: !flag!
pause
endlocal
You should reformat your code and keep in mind that the grama for batch file is critical. BTW, if you are trying to update the existing batch variable and read it later, you should enable localdelayedexpansion and use ! instead of %.
Keep it simple:
#echo off
pushd "E:\TestBat\Test" || exit /B 1
for %%F in ("*.pdf") do if not exist "%%~nF.xml" echo %%~nxF
for %%F in ("*.xml") do if not exist "%%~nF.pdf" echo %%~nxF
popd
This returns all files that appear orphaned, that is, where the file with the same name but the other extension (.pdf, .xml) is missing. To implement a variable FLAG to indicate there are missing files, simply append & set "FLAG=missing" to each for line and ensure FLAG is empty initially. Then you can check it later by simply using if defined FLAG.
Note: This does not cover the e-mail notification issue. Since I do not know the BLAT tool you mentioned, I have no clue how you want to transfer the listed files to it (command line arguments, temporary file, or STDIN stream?).
In case there is a huge number of files in the target directory, another approach might be better in terms of performance, provided that the number of file system accesses is reduced drastically (note that the above script accesses the file system within the for loop body by if exist, hence for every iterated file individually). So here is an attempt relying on a temporary file and the findstr command:
#echo off
pushd "E:\TestBat\Test" || exit /B 1
rem // Return all orphaned `.pdf` files:
call :SUB "*.pdf" "*.xml"
rem // Return all orphaned `.xml` files:
call :SUB "*.xml" "*.pdf"
popd
exit /B
:SUB val_pattern_orphaned val_pattern_missing
set "LIST=%TEMP%\%~n0_%RANDOM%.tmp"
> "%LIST%" (
rem // Retrieve list of files with one extension:
for %%F in ("%~2") do (
rem /* Replace the extension by the other one,
rem then write the list to a temporary file;
rem this constitutes a list of expected files: */
echo(%%~nF%~x1
)
)
rem /* Search actual list of files with the other extension
rem for occurrences of the list of expected files and
rem return each item that does not match: */
dir /B /A:-D "%~1" | findstr /L /I /X /V /G:"%LIST%"
rem // Clean up the temporary file:
del "%LIST%"
exit /B
To understand how it works, let us concentrate on the first sub-routine call call :SUB "*.pdf" "*.xml" using an example; let us assume the target directory contains the following files:
AlOnE.xml
ExtrA.pdf
sAmplE.pdf
sAmplE.xml
So in the for loop a list of .xml files is gathered:
AlOnE.xml
sAmplE.xml
This is written to a temporary file but with the extensions .xml replaced by .pdf:
AlOnE.pdf
sAmplE.pdf
The next step is to generate a list of actually existing .pdf files:
ExtrA.pdf
sAmplE.pdf
This is piped into a findstr command line, that searches this list for search strings that are gathered from the temporary file, returning non-matching lines only. In other words, findstr returns only those lines of the input list that do not occur in the temporary file:
ExtrA.pdf
To finally get also orphaned .xml files, the second sub-routine call is needed.
Since this script uses a temporary file containing a file list which is processed once by findstr to find any orphaned files per extension, the overall number of file system access operations is lower. The weakest part however is the for loop (containing string concatenation operations).

How to get a specific line from command output in batch script into a variable?

I'd like to get a changelist description from perforce, which involves calling a p4 describe -s , so the ouput would be as below. Is there a way to get (trimmed characters from the third line) from the output just using windows batch syntax?
Change 6582 by username on 2016/12/06 00:35:41
MyChangeDescription
Affected files ...
... //depot/foo.txt#7 edit
... //depot/foo2.txt#6 edit
Give this a shot:
p4 -Ztag -F %Description% change -o 6582
#ECHO OFF
SETLOCAL
SET "sourcedir=U:\sourcedir"
SET "filename1=%sourcedir%\q40986156.txt"
FOR /f "usebackqskip=2tokens=*" %%a IN ("%filename1%") DO (
SET "desc=%%a"
GOTO show
)
:show
ECHO "%desc%"
GOTO :EOF
You would need to change the setting of sourcedir to suit your circumstances.
I used a file named q40986156.txt containing your data for my testing.
This uses a file as input. Since I don't have access to perforce, I can't test it but
#ECHO OFF
SETLOCAL
FOR /f "skip=2tokens=*" %%a IN ('p4 describe -s') DO (
SET "desc=%%a"
GOTO show
)
:show
ECHO "%desc%"
GOTO :EOF
should be equivalent.
Simply, read the output of the command, skip the first 2 lines, tokenise the entire line, skipping leading spaces. Assign the string found to a variable and immediately exit the loop.

Batch file to process csv document to add space in postcode field

I have a csv file populated with name, address, and postcode. A large number of the postcodes do not have the required space in between e.g LU79GH should be LU7 9GH and W13TP should be W1 3TP. I need to add a space in each postcode field if it is not there already, the space should always be before the last 3 characters.
What is the best way to solve this via windows command line?
Many Thanks
You can do this with for /f as follows:
#echo off
setlocal enabledelayedexpansion
if "%~1" equ "" (echo.%~0: usage: missing file name.& exit /b 1)
if "%~2" neq "" (echo.%~0: usage: too many arguments.& exit /b 1)
for /f %%i in (%~1) do (echo.%%i& goto :afterheader)
:afterheader
for /f "skip=1 tokens=1-3 delims=," %%i in (%~1) do (
set name=%%i
set address=%%j
set postcode=%%k
set postcode=!postcode: =!
echo.!name!,!address!,!postcode:~0,-3! !postcode:~-3!
)
exit /b 0
Demo:
> type data.csv
name,address,postcode
n1,a1,LU79GH
n2,a2,W13TP
n1,a1,LU7 9GH
n2,a2,W1 3TP
> .\add-space.bat data.csv
name,address,postcode
n1,a1,LU7 9GH
n2,a2,W1 3TP
n1,a1,LU7 9GH
n2,a2,W1 3TP
You can redirect the output to a file to capture it. (But you can't redirect to the same file as the input, because then the redirection will overwrite the input file before it can be read by the script. If you want to overwrite the original file, you can redirect the output to a new file, and then move the new file over the original after the script has finished.)
Using windows you could do something with Powershell.
$document = (Get-Content '\doc.csv')
foreach($line in $document) {
Write-Host $line
// Add logic to cut out exactly what column your looking at with
$list = $line -split","
// Then use an if statement and regular expression to match ones with no space
if($list[0] -match ^[A-Z0-9]$){
// item has no space add logic to add space and write to file
}else{
// item has space or doesnt match the above regular expression could skip this
}
}
Pretty good documentation online check out http://ss64.com/ps/ for help with powershell.
Parsing CSV can be tricky because a comma may be a column delimiter, or it may be a literal character within a quoted field.
Since your postcode is always the last field, I would simply look at the 4th character from the end of the entire line, and if it is not already a space, than insert a space before the last 3 characters in the line. I will also assume that the first line of the file lists the field names, so you don't want to modify that one.
Using pure batch (assuming no values contain !):
#echo off
setlocal enableDelayedExpansion
set "skip=true"
>"test.csv.new" (
for /f "usebackq delims=" %%A in ("test.csv") do (
set "line=%%A"
if "!line:~-4,1!" equ " " set "skip=true"
if defined skip (echo !line!) else (echo !line:~0,-3! !line:~-3!)
set "skip="
)
)
move /y "test.csv.new" "test.csv" >nul
The solution is simpler if you use my JREPL.BAT regular expression text processor. It is a pure script (hybrid JScript/batch) that runs natively on any Windows machine from XP onward. The following one liner will do the trick:
jrepl "[^ ](?=...$)" "$& " /jbegln "skip=(ln==1)" /f test.csv /o -
Use CALL JREPL ... if you use the command within another script.

using for loop to input multiple data

I am new to programming. Here is my dilemma. I have to replace multiple files in multiple locations across multiple computers.
I have written a bat script where I am defining all the variables and calling a txt file with appropriate information. For example -testing.txt has the values
Apple, Potato,Beef
Apple, Potato,Pork
The logic I am applying is as follows: I am using this txt file for reading and then going to each location to change the file
set Path=%Path%;c:\Tools\UnxUtils\usr\local\wbin
SET SORC=C:\tools\logosource\NEWImages\ApiSite\Content
for /F "usebackq delims=, tokens=1-3" %%a in (C:\tools\xxxx\testing.txt) do (
SET HOSTNAME=%%a
SET CUSTNAME=%%c
SET STYPE=%%b
SET DEST=\\%HOSTNAME%\c$\Documents and Settings\blahblah\My Documents\%CUSTNAME%\%STYPE%\goodman\
echo HOSTNAME is %HOSTNAME%
echo CUSTNAME is %CUSTNAME%
echo STYPE is %STYPE%
echo DEST is %DEST%
echo SORC is %SORC%
)
copy "%DEST%\ApiSite\Content\images\michael.gif" "%DEST%"
copy /b /y "%SORC%\images\george.gif" "%DEST%\ApiSite\Content\images\michael.gif"
goto End
:Error
ECHO Error! You must pass in a servername
goto End
:End
The problem is that my loop is only reading the last line my txt file. ie. it reads "Apple, Potato,Pork" and sets the DEST TO THAT VALUE.
What i really want is to read line 1 (Apple, Potato,Beef) set the DEST using these parameters and change the files, then go back and read the second line (Apple, Potato,Pork) and set the DEST using these parameters and change the files.
Actually your code is reading every line in the file, but your logic is wrong.
Your main problem is you want to do the COPY statements for each line, but you have the COPY statements outside the loop. Of course they will only get executed once, and the values used will be the values that were set by the last line in the file. The solution is to move the COPY statements inside the loop.
Your other problem is you are attempting to set a variable within a parenthesized block, and then access the value using %var% - that cannot work because the expansion occurs when the statement is parsed and the entire block is parsed once before any lines are read. You could solve that problem by using delayed expansion, (type HELP SET from the command line prompt for more information about delayed expansion). But there really isn't any need to save the values in variables. Simply use the FOR variables directly. Because the DEST is used multiple times, I used an additional FOR loop to define a %%d variable that contains the DEST value. The ~ removes the quotes that the FOR loop added.
Also, you are ending the script by using GOTO END and defining an :END label at the end of file. That works, but there is an implicit :EOF label at the end of every script. You can simply use GOTO :EOF without defining a label. The other option is to use EXIT /B
You have an :ERROR routine that is not being called - I presume you have additional code that you are not showing.
set Path=%Path%;c:\Tools\UnxUtils\usr\local\wbin
SET "SORC=C:\tools\logosource\NEWImages\ApiSite\Content"
for /F "usebackq delims=, tokens=1-3" %%a in (C:\tools\xxxx\testing.txt) do (
for %%d in (
"\\%%a\c$\Documents and Settings\blahblah\My Documents\%%b\%%b\goodman\"
) do (
echo HOSTNAME=%%a
echo CUSTNAME=%%c
echo STYPE=%%b
echo DEST=%%~d
echo SORC is %SORC%
copy "%%~d\ApiSite\Content\images\michael.gif" "%%~d"
copy /b /y "%SORC%\images\george.gif" "%%~d\ApiSite\Content\images\michael.gif"
)
)
exit /b
:Error
ECHO Error! You must pass in a servername
exit /b

Resources