Monday, December 29, 2014

How to Stop Bashing and Take CMD

A colleague of mine is a Linux hacker who took a job in Seattle and has been thrust into Windows.  On his team, PowerShell is not always available, so he's left saying "look dude, I know bash; what the heck do I do with this??"  If you're in a similar situation, this is a primer for you.

Below are a few bash-isms and Unix-isms, and their cmd.exe analogues, as well as some things that are purely from CMD.  To try them, open up cmd.exe (Start > Run: cmd.exe, or Windows+R: cmd.exe) and go to town.


I'll start with some one-liners.  The $ prompt indicates commands that can be used in bash on GNU/Linux and similar operating systems, and the > prompt represents the cmd.exe equivalent.  Omit the prompts ($ and >) when trying these commands.

Run something else
$ bash -c something else
> cmd /c something else
> cmd /k something else

In the latter command, cmd will stay resident and permit further commands.

Display program return value:
$ echo $?

Some programs, when they are run, return a numeric status code.  Generally, 0 indicates success, and 1 or greater indicates an error.  Windows executables that display graphical windows (calc.exe and winword.exe are examples) return 0 immediately and unconditionally, and run "asynchronously" -- meaning, the command prompt receives the return value immediately and allows the user to type more commands.

Compare program return value:
$ if [ $? -eq 0 ]; then echo Success; fi;
> if %errorlevel% == 0 echo Success

Find program in path:
$ which which
> where where

Find file:
$ find / -type f -name whatever\*.txt
> dir /s \whatever*.txt
> dir /a/b/s \whatever*.txt

The latter command will show all files, even those having the hidden attribute (/a) and will provide bare output without any file sizes or other details (/b).

Find string in files:
$ grep -Ri needle *
> findstr /S /I needle *

Find string in program output:
$ ifconfig | grep 192
> ipconfig | findstr 192

Start service (e.g. mysql):
$ service mysql start
> sc start mysql
> net start mysql

The latter command (net.exe) can do many things including viewing and modifying local groups, authenticating to network shares and mapping them to drive letters, etc.  The other command (sc.exe) is strictly for starting and stopping services, viewing their configuration, and other tasks.

Restart web server:
$ apachectl -k restart > /dev/null 2>&1
> iisreset > nul 2>&1

Terminate by pid:
$ kill -s 9 916
> taskkill /f /PID 916

Terminate by name:
$ killall -s 9 kcalc
> taskkill /f /IM calc.exe

Shut down:
$ shutdown -h now
> shutdown /s /t 0

$ shutdown -r now
$ reboot
> shutdown /r /t 0

Add user to group: 
$ useradd -G root mike
> net localgroup administrators /add mike

Set environment variable:
$ variable=hello
> set variable=hello

Display environment variable: 
$ echo $variable
> echo %variable%

Prompt for environment variable:
$ read -p "Type something: " variable
$ echo $variable
> set /p variable=Type something:

> echo %variable%

Pre-set variables such as username:
$ echo $USER
> echo %username%

Dump environment: 
$ setenv
> set

The SET command also accepts partial variable names, and will list all the variables and values whose names match that string.

Compare files:
$ cmp file1 file2
> fc file1 file2

You can check the errorlevel (the numeric error or success code returned by the program) to determine whether the files are the same.  Identical files result in a 0 errorlevel, and differing files result in a return value of 1 or greater.

Display file contents:
$ cat file
> type file

> more file

The latter command, more, can be used to display the contents of those notorious alternate data streams, and also serves as a pager (see next).

Display file contents with pager:
$ less file
$ cat file | less
> more file
> type file | more

$ for ((n=2; n<=8; n+=2)); do echo $n; done
> for /l %n in (2, 2, 8) do echo %n

Operate on a set of arbitrary words: 
> for word in hello there; do echo $word; done
$ for %w in (hello there) do ( echo %w )

Echo the names of all text files in the current dir:
 > for file in *.txt; do echo $file; done
$ for %f in (*.txt) do echo %f

Echo the names of all text files recursively: 
$ for file in $(find . -name \*.txt); do echo $file; done
> for /f "usebackq" %f in (`dir /a/b/s *.txt`) do echo %f
> for /r %f in (*.txt) do echo %f

Parse IP addresses out of IP configuration:
 $ echo IP: $(ifconfig | grep 'inet addr' | awk -F: '{print $2}' | awk '{print $1}')
>for /f "usebackq delims=: tokens=1,2" %a in (`ipconfig ^| findstr /i IPv4`) do echo IP: %b


Read file, find pattern, copy to another location

rm -rf $dstdir;
mkdir $dstdir;

for file in $(find . -type f -name \*.c); do
    grep -i printf $file > /dev/null 2>&1;
    if [ $? -eq 0 ]; then
        cp $file $dstdir;

@echo off

set dstdir=%userprofile%\cfiles
if exist "%dstdir%" rmdir /s /q "%dstdir%"
mkdir "%dstdir%"

for /r %%f in (*.c) do (
    findstr /i printf %%f > nul 2>&1
    if not errorlevel 1 copy %%f "%dstdir%" > nul 2>&1

Because of the idiosynchrasies of the Windows command interpreter and native utilities, writing robust scripts for CMD is akin to any of the following activities:
  • Leveling and hanging a picture in an earthquake
  • Making a bed with a rabid dog in it
  • Asking a room full of four-year-olds to each draw a triangle
  • Wrestling with a snake, a crab, and an orangutan at the same time
  • Balancing a system of simultaneous equations by inspection while inebriated
Here are the idiosynchrasies that are relevant to the above script:

Double percent signs (e.g. %%f) are used to denote loop variables in .cmd and .bat files instead of single percent signs (e.g. %f) as on the command line.  Variable names must be a single character in length (e.g. %a on the command line, and %%a in a script file).

Also, evaluation of errorlevels can be done both by comparison with the %ERRORLEVEL% environment variable and using the IF [NOT] ERRORLEVEL construct.  The help for the if command (accessible by typing IF /?) states:

  ERRORLEVEL number Specifies a true condition if the last program run
                    returned an exit code equal to or greater than the number

  NOT               Specifies that Windows should carry out
                    the command only if the condition is false.

Just so you've got that straight: don't ask the command interpreter this:

IF ERRORLEVEL 0 ECHO Oh yes, everything is fine <-- NO, IT IS NOT

There are some more turds in the punch bowl...

If you mean to set variables in a for loop and reflect on those values later in the script, you must first invoke SETLOCAL ENABLEDELAYEDEXPANSION.  To obtain the most up-to-date value of each variable, you must then use exclamation points, not percent signs, to access the data.  For example: !frick!.

If you use a pipe within a backtick expression in a FOR /F "usebackq" statement, escape the pipe with the caret symbol.  For example, `dir /a/b/s *.txt ^| findstr x`.

Windows scripts access arguments as %1, %2, etc.  Tilde modifiers such as %~n0 (equivalent to basename $1) are used to parse filenames, and can be found in the help for the FOR command. 

Environment variables inherently support substring selection and pattern replacement:

C:\Users\mykill>echo %username:kill=ke%

C:\Users\mykill>echo %username:~0,3%

I will probably update this article to include more info.  Suggestions are welcome.

For more loopy help:
for /?

The help from a few other commands can also be informative:
if /?
setlocal /?

No comments:

Post a Comment