Why do Windows programs parse command-line switches out of their executable's path? (The latter being what is commonly known as argv[0].)
For example, xcopy:
C:\Temp\foo>c:/windows/system32/xcopy.exe /f /r /i /d /y * ..\bar\
Invalid number of parameters
C:\Temp\foo>c:\windows\system32\xcopy.exe /f /r /i /d /y * ..\bar\
C:\Temp\foo\blah -> C:\Temp\bar\blah
1 File(s) copied
What behavior should I follow in my own programs?
Are there many users that expect to type command-line switches without a space (e.g. program/? instead of program /?), and should I try to support this, or should I just report an error and exit immediately?
What other caveats do I need to be aware of? (In addition to Anon.'s comment below that "debug/program" runs debug.exe from PATH even if "debug\program.exe" exists.)
I think it's actually the DOS shell doing this:
My understanding is that DOS chose to use the forward slash (/) for command-line options (i.e., "DIR /s"), even before DOS supported sub-directories. Later, at the point that they introduced sub-directories, they realized they couldn't use forward slashes as the path separator (as was the standard on UNIX), so they had to use the backslash instead.
Also a factor is that DOS doesn't require a space between the command name and the first parameter. (I.e., "CD\" is the same as "CD \".)
Because of the above, my guess is that it isn't the program that is parsing the command line "incorrectly"-- instead it is the DOS shell that is using "C:" as the executable / command name, and the rest as the command line argument(s). (Of course, a quite test app could verify this, but I'm away from my compiler at the moment.)
I expect that any program doing this would be using GetCommandLine() instead of argv[] to access the command line arguments, and failing to account for the fungibility of / and \ in user-mode paths on Windows.
I have a couple of suggestions that might help. These are the result of a class that I made (and use) just to handle parameters and switches.
I check the argument array to see which delimiter is being used for the path and which is being used for the switches / parameters.
I personally differentiate between switches and parameters using a slash for one and a hyphen for the other.
If a switch is passed that doesn't match any of the expected parameters or switches, I check to see if multiple switches were passed with only one slash. This one has caused and will cause more issues for users if they mistype something. For instance, if I were looking for /d /e /l -or- /del SomeThing and the user inputs /del with the intent of passing the d e and l switches.
In the object, I stuff the switches in a std:: container and the parameter and parameter values in another std:: container which are then made available to the consumer application to process as it sees fit.
Related
Based on a related question and some Googling/testing, it is pretty clear that the maximum length of a command line in cmd.exe is 8,191 characters. However...my script seems to successfully exceed that length, and I can't figure out why it works.
So I have:
A script written/saved as a .ps1
I'm converting it to a .bat using the method from Converting PowerShell to Batch
If I run the .bat - there don't seem to be any issues
If I try to echo the $encoded command or just remove #echo off before running the .bat - it is easy to see that the length/content of $encoded has been truncated...yet it works as expected
I realize this is a bit of a strange question since I'm trying to ascertain some kind of hidden truth about something that works vs. something that is broken, but I'd like to understand why it is working!
Note:
The primary reason I'm creating a .bat for my script is because I have a number of small 'programs' that I need to run on my work computer and share with others. Our company's execution policies don't allow running scripts, and it is much easier for others to use something I've written if it is a .bat since they won't need to understand/use PowerShell. They can just place the .bat in the right place, give it two clicks, and then sit back and relax until something happens.
EDIT: Here's the truncated command in the cmd.exe window. If I convert from Base64 back to readable text, it is clearly missing about half of the script. Yet - I'm not noticing any problems
The 8191-character limit applies to the interactive command line and calls to cmd.exe's CLI (via cmd /c)
It does not apply to commands invoked from batch files - there the limit is close to[1] 32KiB (32,768) characters.
However, it still seems to apply selectively to cmd.exe's internal commands, such as echo; by contrast, a call to an external program such as powershell.exe is not affected (though it is possible for specific external programs to have their own, lower limits).
As an aside:
As clever as the linked method of converting PowerShell scripts to batch files is, the resulting batch files lack support for arguments to be passed through to the PowerShell code, and adding support for that would be impractical (it would require Base64-encoding the arguments too, at invocation time, which isn't possible from a batch file except with the help of non-built-in utilities).
If your script / batch file is written to either require no arguments on invocation or to prompt for them interactively (as in your case), this limitation is not a concern.
However, you may want argument support for any of the following reasons:
To allow calling the batch file from cmd.exe (Command Prompt) with arguments.
To allow calling with arguments from other environments that support passing arguments, notably Task Scheduler and the Windows Run dialog (WinKey-R).
To make the batch file support drag-and-drop (which implicitly passes the dropped files' paths as arguments).
As for the overall reason to wrap PowerShell code in batch files:
Batch files (.cmd, .bat) can be launched directly, as if they were executables, system-wide.
By contrast, this is not supported for PowerShell scripts (.ps1), which from outside PowerShell must be invoked via an explicit call to the PowerShell CLI; note that systems may be configured to categorically prevent execution of .ps1 scripts - see next section.
If you do need argument support in your batch file, the best - but inconvenient - solution is to distribute two files:
the original *.ps1 file...
and a companion batch file, with the same base file name, to be placed in the same directory - which can be invoked from outside PowerShell - whose sole purpose is to invoke the *.ps1 file via powershell.exe with pass-through arguments.
E.g. foo.cmd would accompany foo.ps1, with the following - invariant - content:
#powershell.exe -NoProfile -File "%~dpn0.ps1" %*
Note:
Important: The above assumes that the effective PowerShell execution policy allows execution of script files (.ps1).
If you cannot make this assumption, place -ExecutionPolicy RemoteSigned before -NoProfile above (or, to deactivate all checks, -ExecutionPolicy Bypass), but note that an execution policy based on GPO (Group Policy Objects) may still prevent script execution.
To call PowerShell (Core) 7+ instead, use pwsh.exe instead of powershell.exe.
%~dpn0 expands to the full path of the enclosing batch file itself without its filename extension; appending .ps1 therefore targets the companion PowerShell script in the same directory; run cmd /c call /? for an explanation of the syntax.
%* passes any and all arguments received by the batch file on.
[1] In practice, the limits appear to be: 32,767 characters in batch files, and - lower by 3 chars. - 32,764 for Powershell (both on the command line / via the CLI and in *.ps1 scripts). In PowerShell, the limit may in practice be even lower than that, because PowerShell expands the names of executables it locates via $env:PATH to their full paths in the command lines constructed behind the scenes, which cmd.exe doesn't do.
I have created a custom command entry in the registry to add an item to the Windows Explorer context menu when the user right-clicks on a folder. Here is exactly what the value looks like in the registry:
"C:\Program Files\Directory Switcher\DirectorySwitcher.exe" "%V" "2021.0"
%V returns the current directory. If the directory path has any folders with spaces in the name it causes the path to split into additional command line arguments. To get around this, Microsoft tells you put quotes around it "%V".
The specific issue is that when %V is at the drive root, the backslash in C:\ escapes the end quote and causes the rest of the command line parameters to be parsed incorrectly. For example, at C:\ I get a single argument C:" 2021.0 rather than the expected two of C:\ and 2021.0.
How do I properly encapsulate %V so that it works for normal folder paths and drive roots that end with a backslash? The alternative is to change my program to look for this edge case but I would rather understand how to correct my shell verbs.
(Information about %V can be found at this SuperUser question)
Official Microsoft documentation about shell command strings can be found here
If your program can deal with relative paths, I would enter
"%V\a\.."
resulting in
X:\\a\..
for a root drive, but C# can handle the double backslash.
The idea is: go into a (nonexisting) directory and back up again.
"%V\."
might even be better and shorter.
I am writing an batch file in Windows to run post-installation scripts, and one of the things that needs to be done is to add a directory to the system path.
The script is working, and it does something like this:
setx Path "%PATH%;c:\path\to\add" -m
This is setting the path correctly, but this script could potentially be run multiple times if the user reinstalls the program.
I would like to search the string for c:\path\to\add so I don't keep adding the same path over and over to the system path. This is pretty trivial in Linux with sed, but I don't know what the command is in Windows. I've found findstr, but this seems to only work on files.
Is this possible in Windows without installing additional software?
EDIT:
I'm using Inno Setup to create the install executable.
At the risk of some downvotes till an expert provides a sound way of doing this,
the below removes the specific path from the environment variable if it exists, so that it can be added again:
set str=%path%
:: str is the same with path
set str=%str:;C:\Path\To\Add=%
:: ";c:\path\to\add" is now removed from str
setx Path "%str%;c:\path\to\add" -m
:: proceed with setting the path
This carries the risk of removing the string if it is in fact actually a part of a path, for instance c:\path\to\add\somefolder. Also if the path actually ends with a \, or it is the first entry and it in fact does not start with ;, etc..
Various forms can be called consecutively to circumvent some of these,
set str=%str:;C:\Path\To\Add\;=;%
set str=%str:;C:\Path\To\Add;=;%
set str=%str:;C:\Path\To\Add\=%
set str=%str:C:\Path\To\Add\;=%
set str=%str:;C:\Path\To\Add=%
But, AAMOF I'n not sure this is a sane way of doing this..
This is my code snippet to find the "cvsnt" path in the PATH variable.
if not "x%PATH:cvsnt=%" == "x%PATH%" goto proceed
set PATH=w:\build-repository\cvsnt\2.5.03-2382;%PATH%
echo Path added
:proceed
The part to look at is
not "x%PATH:cvsnt=%" == "x%PATH%"
First I replace all occurrences of "cvsnt" with an empty string. The result is compared to PATH without replacement of "cvsnt". If they are not equal because of "cvsnt" was replaced then it exists in PATH and the code can proceed.
The starting x before %PATH% is only a placeholder to protect against certain "improper" starting characters. see Batch file: Find if substring is in string (not in a file)
The setx utility has a drawback, it can not handle variables longer than 1024 characters.
I have been wrote a script to handle cases longer than the limit and without a need to install anything. Answered the question here: Set environment variables with NSIS in Window 7
Instead of adding the path each time - you could check if the executable you are looking for can be found within the path using a command like this:
for %f in (cmd.exe) do if [%~$PATH:f]==[] setx Path "%PATH%;c:\path\to\add" -m
Make sure to check for /? to read more about the magic of %~$PATH:f.
This is a bit of a hackish workaround, but follows the logic you expect:
Search the PATH
Conditionally add to the PATH
It never removes anything from the PATH, so there's no fear of screwing up Windows by removing something accidently. Also, it checks the PATH variable directly, so you don't have to worry about another file with the same name that lives somewhere on the PATH.
echo %PATH% > myTmpPath.tmp
find /C /I "c:\path\to\add" myTmpPath.tmp
if %ERRORLEVEL% neq 0 setx PATH "%PATH%;c:\path\to\add"
del myTmpPath.tmp
I hate using temporary files, but it's quick and dirty, and probably safer than removing something from the PATH.
The third line is the only tricky one. Basically, this line tests the outcome of the FIND command above. Rob van der Woude states:
FIND returns an errorlevel of 1 or higher if the search string wasn't found.
He also explains:
Some executables return negative numbers for errorlevels! However, this can be
fixed by using the following code to check for non-zero return codes:
IF %ERRORLEVEL% NEQ 0 ...
I've been working on spooling a bat file from a oracle query that would copy contents from one location to another,
Now that command that are getting generated are of lengths that are greater than 255 characters, e.g
C:> copy x y ( where len (x+y) > 255)
As this is throwing an error, is there a work around to manage this kind of situation to increase that command length?
P.S. Some paths+filenames are of length that are larger that 259 characters, to which I found that say there is less to argue
You could use subst to name the two subdirectories your working from with drive letters. Obviously the are not real, but logical drives then, but you could substantially shorten the paths.
LASTDRIVE=Z
SUBST S: c:\this is a very long path name\source
SUBST T: d:\this is a very long path name\Target
#do whatever you need to, like
copy s:\filename T:\filename
SUBST S: /D
SUBST T: /D
The /D parameter frees the association.
You may want to consider using DBMS_FILE_TRANSFER.COPY_FILE instead of creating a bat file. You can avoid using bat files (which are platform dependent) altogether.
SUBST (has already been proposed)
use 8.3 notation (e.g. C:\Progra~1\ - also has been proposed)
Use this syntax (if you run Command prompt in Windows):
copy \?\c:\verylongpath\verylongname \?\d:\anotherverylongpath\
Try using a .cmd file, not a .bat file unless you are using Win95/98/ME. This could solve the entire problem right there.
If that doesn't do it, you can break a command by preceeding a line break with the cmd-escape char ^ or by wrapping the command in parentheses.
I don't think so; I'd write that to a file maybe.
Probably no, sounds like you're hitting the MAX_PATH limitation. See File Names, Paths, and Namespaces on MSDN.
Possible workaround might be to use the short path equivalents, e.g. C:\Progra~1.
According to the the following article Command prompt (Cmd. exe) command-line string limitation,
On computers running Microsoft Windows XP or later, the maximum length of the string that you can use at the command prompt is 8191 characters. On computers running Microsoft Windows 2000 or Windows NT 4.0, the maximum length of the string that you can use at the command prompt is 2047 characters.
What is that one not usually known command in unix and windows that you know?
It is heard that windows contains several hidden applications which sometimes
may be very useful.
linux:
history (history of command line)
mogrify (for all image needs/operations)
screen (for running programs after logging off via ssh)
In widows XP if you have ever tried to do somthing like this
cd \\pc\c$
You will have recieved the error
CMD does not support UNC paths as current directories.
Well you can use UNC paths as long as you map them to a temp drive letter like so.
pushd \\pc\c$
Then when you want to return simply...
popd
Windows:
fdisk /mbr
Saved my life (and system) after a Linux partition went berserk.
Linux:
strace
Came handy getting passwords with classmates running a telnet from a shell I was logged in ;-)
I'm not sure if this counts as unknown, but rsync is invaluable.
In older versions of Windows (XP, in particular), I found the shutdown command invaluable. For example:
shutdown /s /t 3600
will shut down the computer in an hour. Linux, of course, has a similar command (I'd say the majority of Linux users are intimately familiar with "shutdown -h now"), but the Windows equivalent is less well known.
The reason I mentioned older versions of Windows is that in newer ones (Vista I know for sure, don't know about Windows Server 200x) the functionality of shutdown has been hobbled a bit. For example, you can only set a maximum wait time of ten minutes, which makes it useless if you want your computer to shut down in an hour or two, when a download is done.
The hosts file can be used to filter online advertising.
In bash's ~/.bashrc file:
set -o vi
and in ~/.inputrc
set editing-mode vi
set keymap vi
Also, Using !$ to avoid retyping:
ls long/dir/name/i/dont/want/to/repeat/file.txt
rm !$
In Unix: apropos (rough idea of what you want) | less
On Windows XP+:
fsutil, the file system utility. I use this when I have to create test files of a specific size (fsutil file createnew <filename> <length>).
netstat, Displays protocol statistics and current TCP/IP network connections.
netsh, the network services shell; command line hook into all sorts of network info.
reg, the registry shell, for working with the registry from the command line.
on windows i used to like gpedit.msc but i think its only on certain versions of xp
and regedit of course
mmc.exe
you can do amazing things with the bare-bone version of the management console, given admin access to some machines in a network.
In PowerShell, you can:
cd \\server\c$\
In Windows, I use SET alot to get the basic information of the computer easily. There's also: IPCONFIG /FLUSHDNS, IPCONFIG /REGISTERDNS (to clear and reload dns entries), TRACERT (used to trace a path between your location and another on the network/internet), NETSTAT -s -p tcp (for network statistics), and PATHPING (like ping but better!)
I find that findstr is relatively unknown, at least I didn't know about it. It's a rough equivalent to grep, nice when you're not necessarily wanting or needing to install something like mingw or cygwin or even a natively built grep.
c:\Users\logan>findstr /?
Searches for strings in files.
FINDSTR [/B] [/E] [/L] [/R] [/S] [/I] [/X] [/V] [/N] [/M] [/O] [/P] [/F:file]
[/C:string] [/G:file] [/D:dir list] [/A:color attributes] [/OFF[LINE]]
strings [[drive:][path]filename[ ...]]
/B Matches pattern if at the beginning of a line.
/E Matches pattern if at the end of a line.
/L Uses search strings literally.
/R Uses search strings as regular expressions.
/S Searches for matching files in the current directory and all
subdirectories.
/I Specifies that the search is not to be case-sensitive.
/X Prints lines that match exactly.
/V Prints only lines that do not contain a match.
/N Prints the line number before each line that matches.
/M Prints only the filename if a file contains a match.
/O Prints character offset before each matching line.
/P Skip files with non-printable characters.
/OFF[LINE] Do not skip files with offline attribute set.
/A:attr Specifies color attribute with two hex digits. See "color /?"
/F:file Reads file list from the specified file(/ stands for console).
/C:string Uses specified string as a literal search string.
/G:file Gets search strings from the specified file(/ stands for console).
/D:dir Search a semicolon delimited list of directories
strings Text to be searched for.
[drive:][path]filename
Specifies a file or files to search.
Use spaces to separate multiple search strings unless the argument is prefixed
with /C. For example, 'FINDSTR "hello there" x.y' searches for "hello" or
"there" in file x.y. 'FINDSTR /C:"hello there" x.y' searches for
"hello there" in file x.y.
Regular expression quick reference:
. Wildcard: any character
* Repeat: zero or more occurrences of previous character or class
^ Line position: beginning of line
$ Line position: end of line
[class] Character class: any one character in set
[^class] Inverse class: any one character not in set
[x-y] Range: any characters within the specified range
\x Escape: literal use of metacharacter x
\<xyz Word position: beginning of word
xyz\> Word position: end of word
For full information on FINDSTR regular expressions refer to the online Command
Reference.
I just thought to put this in as I used it today about on 5 windows XP machines.
systeminfo
Gives you a list of your system details including os, hotfix/updates, hardware and network information. Sure you can get all this information in a lot of other places, either with commands or in the GUI but this is a great command to find out a lot about a machine very quickly.