Building string in batch script on Windows - windows

I'm trying to build a string in my batch script but I couldn't get it to working.
Here's what I'm trying:
#echo off
set str="wt -p "
for /r "C:\Documents\Files\" %%f in (*) do (
set str=%str% "Command Prompt" cmd /k "echo %%f";new-tab -p
)
echo %str%
pause
wt -p command is supposed to open it in the new Windows Terminal instead of regular Command Prompt.
I want to specify that %%f which is the full path of the file does include blank spaces inside it. So the command should be accepting it.
I've always been struggling with creating batch scripts, it is overwhelming after some point too. What am I doing wrong here?

When you want to use a variable that you defined/changed within a code block, you need delayed expansion.
#echo off
setlocal enabledelayedexpansion
set "str=wt -p "
or /r "C:\Documents\Files\" %%f in (*) do
set "str=!str!"Command Prompt" cmd /k "echo %%f";new-tab -p "
)
echo %str%
goto :eof

Related

How to source a Windows batch script

For example, I have a simple batch script get_path.bat
#echo off
echo C:\Software\dt
Also, I have another simple batch script switch_dir.bat
#echo off
get_path.bat > target.tmp
set /p TARGET=<target.tmp
cd %TARGET%
Now, what I want to accomplish is that when I invoke in cmd.exe the batch file switch_dir.bat, my current working directory changes to C:\Software\dt.
As of now, the scripts work but they run in a process spawned from cmd.exe so my current working directory stays the same. What is missing? Basically, we need Unix-like source or . here.
Well, there are many possible solutions:
Start your script with cmd /c:
All you have to write in cmd is:
cmd /c switch_dir.bat
Using popd/pushd in your batch file:
In your switch_dir.bat add:
#echo off
pushd dir\you\want\to\remain\
get_path.bat > target.tmp
set /p TARGET=<target.tmp
cd %TARGET%
rem [code...]
popd
An additional note: A better way to find directory specified in get_path.bat is to use a for /f loop like this:
#echo off
pushd dir\you\want\to\remain\
for /f "delims=" %%A IN ('get_path.bat') do set TARGET=%%A
cd %TARGET%
rem [code...]
popd

In a CMD batch file, can I determine if it was run from powershell?

I have a Windows batch file whose purpose is to set some environment variables, e.g.
=== MyFile.cmd ===
SET MyEnvVariable=MyValue
Users can run this prior to doing work that needs the environment variable, e.g.:
C:\> MyFile.cmd
C:\> echo "%MyEnvVariable%" <-- outputs "MyValue"
C:\> ... do work that needs the environment variable
This is roughly equivalent to the "Developer command prompt" shortcuts installed by Visual Studio, which set environment variables needed to run VS utilities.
However if a user happens to have a Powershell prompt open, the environment variable is of course not propagated back to Powershell:
PS C:\> MyFile.cmd
PS C:\> Write-Output "${env:MyEnvVariable}" # Outputs an empty string
This can be confusing for users who switch between CMD and PowerShell.
Is there a way I can detect in my batch file MyFile.cmd that it was called from PowerShell, so that I can, for example, display a warning to the user? This needs to be done without any 3rd party utility.
Your own answer is robust and while it is generally slow due to needing to run a PowerShell process, it can be made significantly faster by optimizing the PowerShell command used to determine the calling shell:
#echo off
setlocal
CALL :GETPARENT PARENT
IF /I "%PARENT%" == "powershell" GOTO :ISPOWERSHELL
IF /I "%PARENT%" == "pwsh" GOTO :ISPOWERSHELL
endlocal
echo Not running from Powershell
SET MyEnvVariable=MyValue
GOTO :EOF
:GETPARENT
SET "PSCMD=$ppid=$pid;while($i++ -lt 3 -and ($ppid=(Get-CimInstance Win32_Process -Filter ('ProcessID='+$ppid)).ParentProcessId)) {}; (Get-Process -EA Ignore -ID $ppid).Name"
for /f "tokens=*" %%i in ('powershell -noprofile -command "%PSCMD%"') do SET %1=%%i
GOTO :EOF
:ISPOWERSHELL
echo. >&2
echo ERROR: This batch file may not be run from a PowerShell prompt >&2
echo. >&2
exit /b 1
On my machine, this runs about 3 - 4 times faster (YMMV) - but still takes almost 1 second.
Note that I've added a check for process name pwsh as well, so as to make the solution work with PowerShell Core too.
Much faster alternative - though less robust:
The solution below relies on the following assumption, which is true in a default installation:
Only a system environment variable named PSModulePath is persistently defined in the registry (not also a user-specific one).
The solution relies on detecting the presence of a user-specific path in PSModulePath, which PowerShell automatically adds when it starts.
#echo off
echo %PSModulePath% | findstr %USERPROFILE% >NUL
IF %ERRORLEVEL% EQU 0 goto :ISPOWERSHELL
echo Not running from Powershell
SET MyEnvVariable=MyValue
GOTO :EOF
:ISPOWERSHELL
echo. >&2
echo ERROR: This batch file may not be run from a PowerShell prompt >&2
echo. >&2
exit /b 1
Alternative approach for launching a new cmd.exe console window on demand:
Building on the previous approach, the following variant simply re-invokes the batch file in a new cmd.exe window on detecting that it is being run from PowerShell.
This is not only more convenient for the user, it also mitigates the problem of the solutions above yielding false positives: When run from an interactive cmd.exe session that was launched from PowerShell, the above solutions will refuse to run, even though they should, as PetSerAl points out.
While the solution below also doesn't detect this case per se, it still opens a useable - albeit new - window with the environment variables set.
#echo off
REM # Unless already being reinvoked via cmd.exe, see if the batch
REM # file is being run from PowerShell.
IF NOT %1.==_isNew. echo %PSModulePath% | findstr %USERPROFILE% >NUL
REM # If so, RE-INVOKE this batch file in a NEW cmd.exe console WINDOW.
IF NOT %1.==_isNew. IF %ERRORLEVEL% EQU 0 start "With Environment" "%~f0" _isNew & goto :EOF
echo Running from cmd.exe, setting environment variables...
REM # Set environment variables.
SET MyEnvVariable=MyValue
REM # If the batch file had to be reinvoked because it was run from PowerShell,
REM # but you want the user to retain the PowerShell experience,
REM # restart PowerShell now, after definining the env. variables.
IF %1.==_isNew. powershell.exe
GOTO :EOF
After setting all environment variables, note how the last IF statement, also re-invokes PowerShell, but in the same new window, based on the assumption that the calling user prefers working in PowerShell.
The new PowerShell session will then see newly defined environment variables, though note that you'll need two successive exit calls to close the window.
As Joe Cocker used to say "I get by with a little help from my friends".
In this case from Lieven Keersmaekers, whose comments led me to the following solution:
#echo off
setlocal
CALL :GETPARENT PARENT
IF /I "%PARENT%" == "powershell.exe" GOTO :ISPOWERSHELL
endlocal
echo Not running from Powershell
SET MyEnvVariable=MyValue
GOTO :EOF
:GETPARENT
SET CMD=$processes = gwmi win32_process; $me = $processes ^| where {$_.ProcessId -eq $pid}; $parent = $processes ^| where {$_.ProcessId -eq $me.ParentProcessId} ; $grandParent = $processes ^| where {$_.ProcessId -eq $parent.ParentProcessId}; $greatGrandParent = $processes ^| where {$_.ProcessId -eq $grandParent.ParentProcessId}; Write-Output $greatGrandParent.Name
for /f "tokens=*" %%i in ('powershell -command "%CMD%"') do SET %1=%%i
GOTO :EOF
:ISPOWERSHELL
echo.
echo ERROR: This batch file may not be run from a PowerShell prompt
echo.
cmd /c "exit 1"
GOTO :EOF
I did something like this for Chocolatey's RefreshEnv.cmd script: Make refreshenv.bat error if powershell.exe is being used.
My solution didn't end being used, for unrelated reasons, but it's available in this repo: beatcracker/detect-batch-subshell. Here is copy of it, just in case.
Script that will only run if called directly from interactive command processor session
Script will detect if it's run from non-interactive session (cmd.exe /c detect-batch-subshell.cmd) and show approriate error message.
Non-interactive shell includes PowerShell/PowerShell ISE, Explorer, etc... Basically anything that will try to execute script by running it in the separate cmd.exe instance.
Hovewer, dropping into the cmd.exe session from PowerShell/PowerShell ISE and executing script there will work.
Dependencies
wmic.exe - comes with Windows XP Professional and up.
Example:
Open cmd.exe
Type detect-batch-subshell.cmd
Output:
> detect-batch-subshell.cmd
Running interactively in cmd.exe session.
Example:
Open powershell.exe
Type detect-batch-subshell.cmd
Output:
PS > detect-batch-subshell.cmd
detect-batch-subshell.cmd only works if run directly from cmd.exe!
Code
detect-batch-subshell.cmd
#echo off
setlocal EnableDelayedExpansion
:: Dequote path to command processor and this script path
set ScriptPath=%~0
set CmdPath=%COMSPEC:"=%
:: Get command processor filename and filename with extension
for %%c in (!CmdPath!) do (
set CmdExeName=%%~nxc
set CmdName=%%~nc
)
:: Get this process' PID
:: Adapted from: http://www.dostips.com/forum/viewtopic.php?p=22675#p22675
set "uid="
for /l %%i in (1 1 128) do (
set /a "bit=!random!&1"
set "uid=!uid!!bit!"
)
for /f "tokens=2 delims==" %%i in (
'wmic Process WHERE "Name='!CmdExeName!' AND CommandLine LIKE '%%!uid!%%'" GET ParentProcessID /value'
) do (
rem Get commandline of parent
for /f "tokens=1,2,*" %%j in (
'wmic Process WHERE "Handle='%%i'" GET CommandLine /value'
) do (
rem Strip extra CR's from wmic output
rem http://www.dostips.com/forum/viewtopic.php?t=4266
for /f "delims=" %%x in ("%%l") do (
rem Dequote path to batch file, if any (3rd argument)
set ParentScriptPath=%%x
set ParentScriptPath=!ParentScriptPath:"=!
)
rem Get parent process path
for /f "tokens=2 delims==" %%y in ("%%j") do (
rem Dequote parent path
set ParentPath=%%y
set ParentPath=!ParentPath:"=!
rem Handle different invocations: C:\Windows\system32\cmd.exe , cmd.exe , cmd
for %%p in (!CmdPath! !CmdExeName! !CmdName!) do (
if !ParentPath!==%%p set IsCmdParent=1
)
rem Check if we're running in cmd.exe with /c switch and this script path as argument
if !IsCmdParent!==1 if %%k==/c if "!ParentScriptPath!"=="%ScriptPath%" set IsExternal=1
)
)
)
if !IsExternal!==1 (
echo %~nx0 only works if run directly from !CmdExeName!^^!
exit 1
) else (
echo Running interactively in !CmdExeName! session.
)
endlocal
Like the answer from beatcracker I think it would be better to not take assumptions about the external shell that can be used to launch the batch script, for instance, the issue can also arise when running the batch file through the bash shell.
Because it exclusively uses the native facilities of CMD and has no dependency on any external tool or the WMI, the execution time is very fast.
#echo off
call :IsInvokedInternally && (
echo Script is launched from an interactive CMD shell or from another batch script.
) || (
echo Script is invoked by an external App. [PowerShell, BASH, Explorer, CMD /C, ...]
)
exit /b
:IsInvokedInternally
setlocal EnableDelayedExpansion
:: Getting substrings from the special variable CMDCMDLINE,
:: will modify the actual Command Line value of the CMD Process!
:: So it should be saved in to another variable before applying substring operations.
:: Removing consecutive double quotes eg. %systemRoot%\system32\cmd.exe /c ""script.bat""
set "SavedCmdLine=!cmdcmdline!"
set "SavedCmdLine=!SavedCmdLine:""="!"
set /a "DoLoop=1, IsExternal=0"
set "IsCommand="
for %%A in (!SavedCmdLine!) do if defined DoLoop (
if not defined IsCommand (
REM Searching for /C switch, everything after that, is CMD commands
if /i "%%A"=="/C" (
set "IsCommand=1"
) else if /i "%%A"=="/K" (
REM Invoking the script with /K switch creates an interactive CMD session
REM So it will be considered an internal invocatoin
set "DoLoop="
)
) else (
REM Only check the first command token to see if it references this script
set "DoLoop="
REM Turning delayed expansion off to prevent corruption of file paths
REM which may contain the Exclamation Point (!)
REM It is safe to do a SETLOCAL here because the we have disabled the Loop,
REM and the routine will be terminated afterwards.
setlocal DisableDelayedExpansion
if /i "%%~fA"=="%~f0" (
set "IsExternal=1"
) else if /i "%%~fA"=="%~dpn0" (
set "IsExternal=1"
)
)
)
:: A non-zero ErrorLevel means the script is not launched from within CMD.
exit /b %IsExternal%
It checks the command line that used to launch the CMD shell to tell if script have been launched from within CMD or by an external app using the command line signature /C script.bat which is typically used by non CMD shells to launch batch scripts.
If for any reason the external launch detection needs to bypasses, for instance when manually launching the script with additional commands to take advantage the defined variables, it can done by prepending # to the path of the script in CMD command line:
cmd /c #MyScript.bat & AdditionalCommands

How to process 2 FOR loops after each other in batch?

My problem is that two FOR loops are working separately, but don't want to work one after another.
The goal is:
The first loop creates XML files and only when the creation has already been done the second loop starts and counts the size of created XML files and writes it into .txt file.
#echo off
Setlocal EnableDelayedExpansion
for /f %%a in ('dir /b /s C:\Users\NekhayenkoO\test\') do (
echo Verarbeite %%~na
jhove -m PDF-hul -h xml -o C:\Users\NekhayenkoO\outputxml\%%~na.xml %%a
)
for /f %%i in ('dir /b /s C:\Users\NekhayenkoO\outputxml\') do (
echo %%~ni %%~zi >> C:\Users\NekhayenkoO\outputxml\size.txt
)
pause
This question can be answered easily when knowing what jhove is.
So I searched in world wide web for jhove, found very quickly the homepage JHOVE | JSTOR/Harvard Object Validation Environment and downloaded also jhove-1_11.zip from SourceForge project page of JHOVE.
All this was done by me to find out that jhove is a Java application which is executed on Linux and perhaps also on Mac using the shell script jhove and on Windows the batch file jhove.bat for making it easier to use by users.
So Windows command interpreter searches in current directory and next in all directories specified in environment variable PATH for a file matching the file name pattern jhove.* having a file extension listed in environment variable PATHEXT because jhove.bat is specified without file extension and without path in the batch file.
But the execution of a batch file from within a batch file without usage of command CALL results in script execution of current batch file being continued in the other executed batch file without ever returning back to the current batch file.
For that reason Windows command interpreter runs into jhove.bat on first file found in directory C:\Users\NekhayenkoO\test and never comes back.
This behavior can be easily watched by using two simple batch files stored for example in C:\Temp.
Test1.bat:
#echo off
cd /D "%~dp0"
for %%I in (*.bat) do Test2.bat "%%I"
echo %~n0: Leaving %~f0
Test2.bat:
#echo %~n0: Arguments are: %*
#echo %~n0: Leaving %~f0
On running from within a command prompt window C:\Temp\Test1.bat the output is:
Test2: Arguments are: "Test1.bat"
Test2: Leaving C:\Temp\Test2.bat
The processing of Test1.bat was continued on Test2.bat without coming back to Test1.bat.
Now Test1.bat is modified to by inserting command CALL after do.
Test1.bat:
#echo off
cd /D "%~dp0"
for %%I in (*.bat) do call Test2.bat "%%I"
echo Leaving %~f0
The output on running Test1.bat from within command prompt window is now:
Test2: Arguments are: "Test1.bat"
Test2: Leaving C:\Temp\Test2.bat
Test2: Arguments are: "Test2.bat"
Test2: Leaving C:\Temp\Test2.bat
Test1: Leaving C:\Temp\Test1.bat
Batch file Test1.bat calls now batch file Test2.bat and therefore the FOR loop is really executed on all *.bat files found in directory of the two batch files.
Therefore the solution is using command CALL as suggested already by Squashman:
#echo off
setlocal EnableDelayedExpansion
for /f %%a in ('dir /b /s "%USERPROFILE%\test\" 2^>nul') do (
echo Verarbeite %%~na
call jhove.bat -m PDF-hul -h xml -o "%USERPROFILE%\outputxml\%%~na.xml" "%%a"
)
for /f %%i in ('dir /b /s "%USERPROFILE%\outputxml\" 2^>nul') do (
echo %%~ni %%~zi>>"%USERPROFILE%\outputxml\size.txt"
)
pause
endlocal
A reference to environment variable USERPROFILE is used instead of C:\Users\NekhayenkoO.
All file names are enclosed in double quotes in case of any file found in the directory contains a space character or any other special character which requires enclosing in double quotes.
And last 2>nul is added which redirects the error message output to handle STDERR by command DIR on not finding any file to device NUL to suppress it. The redirection operator > must be escaped here with ^ to be interpreted on execution of command DIR and not as wrong placed redirection operator on parsing already the command FOR.
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 /?
cd /?
dir /?
echo /?
for /?
And read also the Microsoft article Using command redirection operators.
You need to use the START command with the /WAIT flag when you launch an external application.
I believe it would look something like this:
START /WAIT jhove -m PDF-hul -h xml -o C:\Users\NekhayenkoO\outputxml\%%~na.xml %%a
That should cause the batch file to pause and wait for the external application to finish before proceeding.

windows cmd enabledelayedexpansion not working

I'm having trouble running !var! examples ad described here http://ss64.com/nt/delayedexpansion.html
Instead of the expected variable content output as the example describes, I get the literal "bang V A R bang" output, any idea?
C:\>Setlocal EnableDelayedExpansion
C:\>Set _var=first
C:\>Set _var=second& Echo %_var% !_var!
first !_var!
thanks.
You are getting an unexpected result because you are issuing the commands at the command prompt. Create a batch file by putting the following commands in a file with a .bat extension then run the batch file.
#echo off
Setlocal EnableDelayedExpansion
Set _var=first
Set _var=second& Echo %_var% !_var!
E.g., if I created a batch file named delayedexp.bat with the above contents, I would see the following when I run it:
C:\Users\JDoe\Documents\>delayedexp
first second
setlocal only works within the confines of a command script:
help setlocal
If you have access to the parameters of the cmd call, you can set parameter /v. Must be first. And use ! instead % for variables.
%windir%\system32\cmd.exe /v /c set a=10&echo a=!a!&echo My Path is %CD%&pause
This is how, for example, you can get the date in the Russia-France format directly in the Windows shortcut, where a simple percentage is impossible due to its doubling. In std queries with the /v parameter, both a percentage and an exclamation mark will work fine, but single % for cicles.
%windir%\system32\cmd.exe /v /c echo off&for /F "tokens=1-6 delims=:., " %A In ("!date! !time!") Do (Echo %A.%B.%C %D:%E:%F)&pause

Redirect command as well as output to text file in Windows CLI?

I need to redirect both the command as well as its output to a text file in Windows CLI. For instance, I am running the nslookup command on a subnet using a FOR loop,
for /L %i IN (1,1,254) DO nslookup 192.168.1.%i >> nslookup.txt
However, this only redirects the output of the command.
Is there a way to redirect both the command as well as the output to a text file? Please do not tell me about clip and select all/copy commands.
You can proceed the command with "cmd /c" to start a new command prompt, and redirect the output of the command prompt:
cmd /c for /L %i IN (1,1,254) DO nslookup 192.168.1.%i > nslookup.txt
Note that you only need to use a single greater than (>) since the output of cmd is going to nslookup.txt. Sadly, this misses the error output, so you are not seeing the ***Request to UnKnown timed-out for each failed address.
Your FOR loop is right on and it sounds like you are already getting the output you want, so all you need to do is ECHO the command before running it:
for /L %i IN (1,1,254) DO ECHO nslookup 192.168.1.%i&nslookup 192.168.1.%i >> nslookup.txt
The & chains the commands together so the ECHO is run before the nslookup.
If you want to use a batch file, it becomes a bit more clear:
#ECHO OFF
SETLOCAL EnableDelayedExpansion
SET Outfile=nslookup.txt
REM Log the date/time.
ECHO %DATE% - %TIME%>%Outfile%
FOR /L %%i IN (1,1,254) DO (
SET Command=nslookup 192.168.1.%%i
REM Print the command being run.
ECHO !Command!>>%Outfile%
REM Run the command.
!Command!>>%Outfile%
)
ENDLOCAL
for /L %i IN (1,1,254) DO (#echo nslookup 192.168.1.%i & nslookup 192.168.1.%i) >> nslookup.txt
This works. Still I'm sure there are smarter ways to do this.

Resources