Deferring expansion in a Windows command line with nested commands - batch-file

I have a command line that's being invoked by a third party executable (Intune Management Extension). Unfortunately, this executable prefers to expand environment variables in my command line before executing them. Further, it doesn't load system environment variables before it does this expansion.
So, if I put something like this in the command line, it resolves to nothing:
cmd /s /c "echo profile is: %userprofile%"
profile is:
I've found I can work around the problem by using delayed expansion as follows:
cmd /s /v /c "echo profile is: !userprofile!"
However, this breaks down in my real scenario, which is the following:
cmd /s /v /c "cmd /s /v /c "cmd /s /v /c "echo hello" > !userprofile!\tst.log""
The system cannot find the path specified.
however, oddly, this works:
cmd /s /v /c "cmd /s /v /c "echo hello" > !userprofile!\tst.log"
Can someone explain to me why the additional level of nesting breaks this?
An alternative acceptable answer to this question: how can I take an arbitrary sequence of valid command lines, combine them into a single line command chain safely (using &&) and redirect the output of ALL of them to an output log file? Keep in mind, my limitation here is that this is third party software and has the ability to execute a single command line.
A real world example - I have 3 commands:
Installs some software: powershell .\Install.ps1
Records if the software was installed successfully: reg add "HKEY_CURRENT_USER\SOFTWARE\Foo\bar" /v "Installed" /d "%date:~4% %time%" /f /reg:64 > nul
Outputs to a log file in the user profile directory: >> %userprofile%\tst.log
How can I combine this into a single command line?
Right now, I have
cmd /s /c "cmd /s /c "cmd /s /c "powershell .\Install.ps1" && reg add "HKEY_CURRENT_USER\SOFTWARE\Foo\bar" /v "Installed" /d "%date:~4% %time%" /f /reg:64 > nul" >> "%userprofile%\install.log" 2>&1"
This works, with the exception of the early expansion of %userprofile%...
Note - the executing third party software blows up if my outer command redirects the output (>>), hence the extra layer of wrapping...

The special characters in the command line is %, &, > and !. Some of these characters may need to be escaped to delay their use. If present in a string i.e. ">", then this can make them literal or if cmd removes the surrounding "" from the command line, may just delay their special action once they are no longer part of a string.
Can escape % with by doubling or more if needed i.e. %%. &, > and ! can be escaped with a caret ^ if needed. % and ! can remain special in strings. View ss64.com about Escape Characters, Delimiters and Quotes at the Windows command line.
In the OP, you have tried delayed expansion using cmd /v and replacing % with !.
You have also tried nesting cmd commands with up to 3 instances.
Consider example 1:
cmd /c echo 1 && echo 2 > file.txt 2>&1
Expectation is:
cmd /c echo 1 will execute.
echo 2 > file.txt 2>&1 will execute if already in a cmd process.
Consider example 2:
cmd /c (echo 1 ^&^& echo 2) > file.txt 2>&1
Expectation is:
cmd /c (echo 1 ^&^& echo 2) > file.txt 2>&1 will log line 1 (+ Space) and line 2 to file.txt.
The && is delayed by the preceding ^ before each character and the echo output is grouped by () so that file.txt gets all output.
The actual command to try:
cmd /v /c (powershell -file .\Install.ps1 ^&^& reg add "HKCU\SOFTWARE\Foo\bar" /v "Installed" /d "!date:~4! !time!" /f /reg:64 ^> nul) >> "!userprofile!\install.log" 2>&1
I do not have or have I previously used Intune Management Extension to be certain of success. The use of cmd /v and use of ! instead of % is based on your testing results etc.

Related

Stop searching for a string if found while using forfiles command

I wrote a simple batch file that search for a specific string.
forfiles /S /M TraceLog-* /d +%SearchDate% /c "cmd /c findstr /c:\"Not enough disk space to save focus debug data.\" #path" >> %FileName%
Is there a way to stop the run so the output file will contain only a single message of "Not enough disk space to save focus debug data".
That means - if the above string was found - stop the loop.
The forfiles command does not provide a feature to terminate the loop once a certain condition is fulfilled.
However, you could instead let the loop finish but write its output to a temporary file, whose first line you copy into the target log file afterwards:
rem // Do the loop but write output into a temporary file:
forfiles /S /M "TraceLog-*" /D +%SearchDate% /C "cmd /D /C findstr /C:\"Not enough disk space to save focus debug data.\" #path" > "%FileName%.tmp"
rem // Read the first line from the temporary file and store it in variable `LINE`:
< "%FileName%.tmp" (set "LINE=" & set /P LINE="")
rem // Append the read first line to the target log file:
if defined LINE echo(%LINE%>> "%FileName%"
rem // Eventually clean up the temporary file:
del "%FileName%.tmp"
This code (mis-)uses set /P intended to prompt a value from the user for reading the first line of a file in combination with input redirection <.

I want to execute few cmd commands from a text file by using a batch file

I have created a txt file with the following commands.
cd..
cd <Folder location>
del *_extract.txt /Q
Now, I want to create a batch file that will open cmd and execute the commands mentioned in the text file.
The batch file I have created has the following content
#echo off
start "clear" cmd.exe -m <textfilelocation>
The simplest way is as Stephan Said in the comment. Rename your file from .txt to .cmd or .bat. Then you can run it on its own, or simply call it from your other batch file as:
call "C:\Some Location\file.cmd"
If you really want to retain the txt file, then run a for loop:
#echo off
for /f "usebackq delims=" %%i in ("C:\Some path\file.txt") do %%i
Considering a text file with comments mayb, in this case using # (which is a non batch file standard) we can execute the lines that are not comments.
#echo off
for /f "delims=" %%i in ('type "C:\Some Path\file.txt" ^| findstr /v "#"') do %%i
The best option is to rename the file to .bat or .cmd (or make a (temporary) copy).
I for some reason it just has to be a .txt file, your options are very limited.
Besides Gerhards suggestion (the one with the for loop), a maybe better option is to "feed" the lines of the textfile to cmd (not recommended; provided just for academic reasons):
<script.txt %comspec%
or without command repetition and header:
<script.txt %comspec% /q /k
NOTES:
the lines in the script.txt have to be command-line syntax (for example for %a ..., not batch-file syntax for %%a ...)
GOTO and CALL :label won't work (they don't work on command-line)
a "multiline" for command works, but will clutter your output with a More? prompt per line (not suppressable).
if delayed expansion is needed, you need to add the /v:on switch to the cmd command (setlocal * has no effect on the command line)
Example-script.txt:
echo off
echo/
:start [ignored]
echo hello %username%
ping -n 1 www.google.com | find "TTL="
for %a in (alpha beta) do (
echo %a
)
goto :start [ignored]
echo done.
Output:
hello Stephan
Antwort von 172.217.21.206: Bytes=32 Zeit=12ms TTL=121
Mehr? Mehr? alpha
beta
done.

how to get if file modification date is older than N days [batch script]

I got one problem that i want to solve basically i want to delete file that exist for 2 day or more than 2 day and i want to know if it exist or not to be delete because i need to record in the Log.txt if the file been deleted or not
this is current code without the Log.txt
forfiles /p "E:\Backup\DailyWinflexBackup" /s /d -2 /c "cmd /c del #file : date >= 2 days >NUL"
i want to do the statement like this
IF (FILE WITH MORE THAN 2 days exist) (
::PROCESS DELETE
ECHO (MESSAGE)>>Log.txt
) ELSE (
ECHO (MESSAGE)>>Log.txt
)
Alright, I assume you want different log messages to be output in case a file has been deleted or not. For this you could use two nested forfiles loops, like in the following example:
> nul forfiles /S /P "E:\Backup\DailyWinflexBackup" /M "*" /C "cmd /C if #isdir==FALSE 2> nul forfiles /M #file /D -155 /C 0x22cmd /C del #path && > con echo 00x7840file deleted0x22 || > con echo #file NOT deleted"
The outer forfiles loop enumerates all matching files, not regarding their age (or last modification date). The query if#isdir==FALSE ensures that only files are handled, because forfiles also enumerates directories. The inner forfiles loop matches one item per iteration of the outer loop, and filters it by age (due to /D -2); if last modified 2 days ago or earlier, the file is deleted by del and a related log message is output by echo; if the file is younger, forfiles fails (the 2> nul part suppresses its error message), and therefore, the echo command behind || becomes executed. The > nul part at the very beginning suppresses empty lines returned by forfiles. Due to that, explicit redirection to the console, > con is needed for the echo outputs not to become suppressed too. To write the log messages to a log file, replace > con by something like > "\path\to\log\file.txt".

batch: ampersand causing issues with findstr

This part works well until there is an ampersand in the filename, in which case it crashes my script completely.
echo %filename% | findstr /i /b /c:"%name% (%year%)"
I can't just put the filename into quotation marks because I need to find the string at the beginning. So how can I do both?
For command line use:
echo %file^name:^&=^^^&% | ...
Inside a batch file
echo %%filename:^&=^^^&%% | ...
How it works?
As a pipe creates two new cmd.exe instances, the echo ... will be parsed twice.
The trick is to expand the filename only in the second expansion.
And then to expand and replace the & with ^& to avoid problems with the &.
The caret will be used to escape the ampersand and itself will be removed.
In the second expansion the parser only sees echo %filename:&=^&%.
To force the expansion into the second parse step, the percent signs have to be doubled for batch files.
From the command line, this doesn't work, but a simple caret anywhere in the variable name works.
Alternative solution:
echo "%filename%" | findstr /i /b /c:^"\"%filename% (%year%)\""
This adds simply quotes and uses also quotes in the search expression
Another option is to use delayed expansion, which requires an explicit cmd with the /v:on option.
cmd /v:on /c "(echo !filename!)" | findstr /i /b /c:"%name% (%year%)"
If your batch script already has enabled delayed expansion, then parentheses around the left side are needed to prevent the delayed expansion from occurring within the parent script (see Why does delayed expansion fail when inside a piped block of code?). The child process will still default to disabled delayed expansion, so the cmd /v:on /c ... is still needed.
#echo off
setlocal enableDelayedExpansion
...
(cmd /v:on /c "(echo !filename!)") | findstr /i /b /c:"%name% (%year%)"
Another way to delay the expansion until the sub-process is to escape the expansion
#echo off
setlocal enableDelayedExpansion
...
cmd /v:on /c "(echo ^!filename^!)" | findstr /i /b /c:"%name% (%year%)"

How to get the FIND command output count value into a variable in batch scripting?

I am using a command:
find /c "abc" "C:\Users\abc\Desktop\project\string.txt"
Output:
---------- C:\Users\abc\Desktop\project\string.txt: 4
I want to assign this value 4 to a variable so that I can use it for an if statement.
I would use:
For /F %%A In ('Find /C "abc"^<"C:\Users\abc\Desktop\project\string.txt"') Do (
Set "mlc=%%A")
Your %mlc% varaiable would hold the matched line count.
I'm not sure if this is the best method to do this, but it works:
find /c "abc" "C:\Users\abc\Desktop\project\string.txt" > tmpFile
set /p myvar= < tmpFile
del tmpFile
with your snytax the output is ---------- C:\Users\abc\Desktop\project\string.txt: 4
There is another syntax: type file.txt|find /c "abc", which gives you a beautiful output of just:
15
To get it into a variable, use a for /f loop:
for /f %%a in ('type file.txt^|find /c "abc"') do set count=%%a
echo %count%
(for use directly on commandline (not in a batchfile) use %a instead of %%a)
I am not a batch script / Windows command line pro, but I got this to work with the following, without a temporary file:
for /f "delims=" %%a in ('dir "%~dpSomeFolder\*.suffix" /b ^|find /c "suffix"') do set "fileCount=%%a"
Explanation:
I found it very confusing, why assigning a variable with a batch script, is so weird, considering it's "Windows", the most used operating system. Anyways this answer here is helpful. Even if it is a duplicate, I like the formatting more:
Assign command output to variable in batch file
%~dp0: basically translates to "path of this script". You can find info about this online.
SomeFolder\*.suffix: In my case this I was looking to count the number of files ending with a certain suffix. I had problems using the dir command with \s as this listed all the matches in subfolders I did not expect him too look. As if this was referenced to the path from which I executed this script from. Therefore, the path name with the asterisk "\*.suffix" solved that issue for me.
^|: When using the pipe sign "|" in "for command", specified inside the single quotation marks, you need to use a circumflex "^|", instead of just the "|", which you would normally use when just typing in the command in cmd (f.e. like dir "%~dp0Folder*.suffix" /b | find /c "suffix"
%%a: You have to use the "double percentage", as this is just a locale variable when writing this in a batch script.
FYI: you can have a look at the command help/ manual ("man" as I am used to Linux), with the command /? (f.e. dir /? or find /?)
I thought I would also mention how I then used this variable, as this might save some time for you ;) (batch code coloring somehow did not work here...).
IF %fileCount% NEQ 1 (ECHO Number of SUFFIX files does not equal 1! Found %fileCount% SUFFIX files inside the SomeFolder. Aborting script! & PAUSE & EXIT)

Resources