Unable to move forward using cd - powershell

I'm having a problem moving forward through a path with PowerShell. I am able to move up the directory but not down. Here's the situation:
I open PowerShell and type in the "pwd" command and it shows that I am currently in PS C:\Users\Robert Inspiron14>
I type the command "cd.." and now I move to PS C:\Users>
I then attempt to change directories by typing: "cd C:\Users\Robert Inspiron14" and I am unable to. Unfortunately, I can't post a picture yet due to lack of reputation.
I'm able to perform the change in CMD but not PowerShell. Also, I don't know how to change the User from "Robert Inspiron14" to just "Robert". Any help is appreciated!

Before PowerShell can execute your cd command, it needs to parse it, and PowerShell's parser interprets your command like this:
cd C:\Users\Robert Inspiron14
\/ \_____________/ \________/
Command Name | |
argument 1 |
argument 2
In other words, C:\Users\Robert and Inspiron14 are interpreted as separate arguments.
Neither argument is a path to a valid directory, so cd (or rather Set-Location for which cd is an alias) throws an error.
You can force PowerShell to recognize C:\Users\Robert Inspiron14 as a single string argument by qualifying its boundaries using quotation marks (both " and ' will work):
cd 'C:\Users\Robert Inspiron14'
You can read more about how PowerShell parses command expressions in the about_Parsing help topic

To complement Mathias R. Jessen's helpful answer with more background information:
Quoting an argument that contains spaces is a general syntactic necessity, in all shells, because unquoted spaces are used to separate multiple arguments.
It isn't only spaces that require quoting, but any of PowerShell's so-called metacharacters (characters that, when used unquoted, have syntactic function); for instance, passing the path to a directory literally named a;b requires quoting as well, given that ; would otherwise be interpreted as a statement separator.
There are multiple quoting styles:
Since your path value is a literal - it contains no variable references or expressions - a verbatim (single-quoted) string ('...') is the best choice.
cd 'C:\Users\Robert Inspiron14'
If your path contains variables or subexpressions, you must use an expandable (double-quoted) string ("...")[1]
cd "$HOME\Documents"
Another, less common solution is to individually escape the space characters with `, the so-called backtick, PowerShell's escape character:
cd C:\Users\Robert` Inspiron14
Also note:
PowerShell's tab-completion automatically applies quoting as necessary.
cd.. is the name of a built-in function in PowerShell, whose sole purpose is to emulate cmd.exe's (questionably permissive) behavior (see below); the function performs a syntactically correct Set-Location .. call (verify by executing ${function:cd..}), with a space separating the command name from its argument.
Contrast with cmd.exe:
Unfortunately, cmd.exe's built-in cd command decided not to enforce its usual syntax rules, and enabled calls such as cd C:\Program Files.
It should never have done that: While convenient at first glance, it constitutes a problematic exception from the usual rules that invites confusion.
Note that cmd.exe's tab completion properly quotes arguments that contain spaces.
Similarly, cd.. was unfortunately allowed as as syntactically exceptional alternative to the correct cd .. - see the comments on this answer for details.
[1] Note "..."-quoting isn't strictly necessary if you use variable references in a path, as long as any literal components do not require quoting; e.g., $HOME\foo is fine without quoting, whereas the " around "$HOME\foo bar" are required. With subexpressions ($(...)), the rules get more complicated, so the simplest approach is to always use "..."-quoting with them.

Related

How to preserve multiple spaces as a string argument for powershell in cmd in Windows 10

I am trying to run a powershell command with several string arguments via cmd.
The context is that I want to use the Compress-Archive cmdlet from a Matlab function/script. And Matlab only has access to cmd.
But I can't find a way to preserve consecutive spaces in the string arguments when calling powershell.exe in cmd.
C:\Users\Artus>powershell.exe echo 'a c'
a c
C:\Users\Artus>powershell.exe echo \"a c\"
a c
C:\Users\Artus>powershell.exe echo `"a c`"
c`
C:\Users\Artus>powershell.exe echo \"`'a c`'\"
'a c'
I tried to adapt the answers for this, this and this questions and none worked.
How does one avoid the removal of consecutive spaces when passing arguments to powershell.exe? Is there a way to ask powershell.exe to accept an argument as string literal?
To add an explanation to Theo's effective solution:
# Use of "..." around the entire argument is the key.
# (The extra space before and after the command is just for visual clarity.)
powershell.exe " echo 'a c' "
# Alternative with embedded double-quoting
powershell.exe " echo \"a c\" "
That is, enclosing the entire command in "..." is necessary to avoid the whitespace normalization you saw.
When you pass a command (piece of PowerShell code) to the PowerShell CLI, via the -Command (-c) parameter (which is positionally implied in your case), PowerShell performs the following command-line parsing first, by splitting the command line into:
white-space separated tokens
with double-quoted tokens ("...") getting recognized as single tokens even if they contain spaces, with the interior spaces getting preserved as-is; note that these (unescaped) " are removed in the process).
Note: By contrast, '...'-enclosed tokens are not recognized as single tokens on the command line (even though inside a PowerShell session they are), so that 'a b' is split into verbatim 'a and b'.
The resulting tokens are then joined with a single space to form the single string that is then interpreted and executed as PowerShell code.
It is during the splitting by whitespace - which can be any number of spaces between tokens - that the information about how many spaces there were between tokens is lost.
Only inside "..."-enclosed tokens is the whitespace preserved as-is, hence the use of "..." around the entire command above.
If you need to use " quoting as part of the PowerShell command (to use string interpolation), " characters must be escaped as \", as shown in the second command at the top.
However, if you're calling from cmd.exe / a batch file, this may break due to how cmd.exe parses command lines. In such edge cases, use the workarounds discussed in this answer.

`doskey /macros:all` produces quoted string I can't get rid of

First, I'm in PowerShell and I've entered the doskey /exename=powershell.exe option.
Second, I did something that I now realize doesn't quite work:
doskey envpath=$env:Path -split ';'
The goal was to have it print the path environment variable (whatever it is at the time I later enter envpath). However, it seems to have evaluated $env:Path while defining the macro, so the macro now appears to be all the paths in my path environment variable followed by '-split ;'. So that's a problem, but only listed here for context. I'll figure that out separately. The purpose of this question (one question per post) is the following:
I was following this and getting something weird...
If I now enter doskey /macros:all I get:
"envpath=C:\WINDOWS\system32;C:\WINDOWS;<etc>;" -split ;
Please note the quotes.
Now, if, per the above-linked other answer, I enter doskey envpath=something (literally) then doskey /macros:all returns:
"envpath=C:\WINDOWS\system32;C:\WINDOWS;<etc>;" -split ;
envpath=something
(which is expected except for the quoted part).
And when I do doskey envpath= it clears/deletes that macro, and doskey /macros:all, returns the first result again.
So my question: What is this entry in the quotes and how do I get rid of that please?
Hopefully I've explained that clearly enough. If confused please feel free to ask for clarification. Thanks in advance for help!
As noted in the answer to your related question,
it's best to avoid use of doskey.exe in PowerShell, because getting it to work requires forgoing PowerShell's own, rich command-line editing experience, by unloading the PSReadLine module first (Remove-Module PSReadLine) - see this answer for background information.
the better alternative is to define a PowerShell function and add it to your $PROFILE file, as shown in the linked answer.
If you want to use doskey.exe nonetheless, define your macro as follows (for PowerShell (Core) 7+, replace powershell.exe with pwsh.exe):
doskey /exename=powershell.exe envpath = `$env:Path -split "';'"
The tokens that make up the PowerShell command must be passed as individual arguments to doskey.exe, and be sure to follow the = with a space.
If you accidentally pass the PowerShell command as a single, quoted argument, doskey.exe stores enclosing "..." as part of the macro and includes these quotes when it expands the macro.
If you additionally also include the macro name in that single, quoted argument, you not only get a virtually unusable macro,[1] you also cannot remove it in-session (neither individually, with doskey /exename=powershell.exe envpath=, nor as part of clearing all macros with Alt-F10) - you must start a new PowerShell session to get rid of it.
Note that the macro name is also included if you attempt partial quoting in PowerShell, e.g., doskey envpath="`$env:Path -split ','" is effectively the same as doskey "envpath=`$env:Path -split ','", due to how PowerShell rebuilds the command line behind the scenes (see below).
To avoid instant expansion of $env:Path, the $ character is preceded by PowerShell's escape character, the so-called backtick (`).
To preserve the '...'-quoting around ;, outer "..." quoting is used.
This is necessary, because PowerShell rebuilds the command line behind the scenes when it invokes external programs, which involves translating '...' quoting into "..." quoting if necessary; that is, irrespective of what quoting was originally used on the PowerShell side, an argument is enclosed in "..." if it contains spaces and used unquoted otherwise; thus, ';' by itself would turn into just ; on the behind-the-scenes command line; an originally partially quoted argument that contains spaces ends up being double-quoted as a whole.
[1] In a macro definition that doskey /macros or doskey /macros:all lists as "envpath=C:\WINDOWS\system32;C:\WINDOWS;<etc>;" -split ;, the macro name is "envpath verbatim, i.e. including the opening ". The - then unbalanced - closing " is retained in the text to expanded to.

String containing double quotes as argument for PowerShell Script

This question was asked many times on SO and yet...
All I've seen were solutions where the input string has to be modified. Either by replacing all double quotes with single quotes or by using backticks.
But I have no control over the input string since I have no access to the source. I cannot change Hello "W"orld to Hello 'W'orld or Hello """W"""orld
What I can do is to wrap the whole string with any escaping characters. For example with single quotes around 'Hello "W"orld'. But none of thoses escaping mechanisms I tried worked. And I can change my PowerShell script
Q: How can I pass a string with double quotes to PowerShell as argument and retain the quotes?
How to reproduce
Save this
cls
write-host $args[0]
as PowerShell script echoArgs1.ps1 on your desktop.
Open a CMD window, navigate to your desktop folder and enter
powershell -file echoArgs1.ps1 "Hello "W"orld"
Current Output
Desired Output
You're using the $(CurText) macro to pass the currently selected text in Visual Studio to a PowerShell script file via an external tools definition.
Unfortunately, Visual Studio doesn't offer a way to escape double quotes in the selected text to guarantee that it is ultimately seen as-is by whatever external executable you pass it to.
(For most executables, including PowerShell, embedding literal " chars. in a "..."-enclosed argument requires escaping them as \" - see this answer for the full story.)
Due to this lack of proper escaping, PowerShell won't parse text passed as an argument to a script file (*.ps1) via the -File CLI parameter as expected if it contains literal " chars.
This is Visual Studio's shortcoming, but there is a workaround:
With just one argument being passed, inspect the raw command line via [Environment]::CommandLine, and consider everything after the *.ps1 file the argument, verbatim.
To simplify that process, pass $(CurText) without enclosing it in "..." in the external-tool definition (and make sure that it is separated from the previous token by just one space char.).
Inside of echoArgs1.ps1, use the following command to retrieve the argument verbatim:
$rawText = ([Environment]::CommandLine -split '\.ps1 ', 2)[-1]
The problem is that the command line interpreter has already removed the quotes. In other words, the quotes are already gone before the command reaches the PowerShell interpreter.
What you might try to do is: pulling the original bare command line ($MyInvocation.Line) and resolve the arguments by removing the command itself:
$FileName = [System.IO.Path]::GetFileName($MyInvocation.MyCommand.Path)
$Arguments = $MyInvocation.Line -Replace ("^.*\\" + $FileName.Replace(".", "\.") + "['"" ]\s*")
Write-Host $Arguments
Note that there are a few pitfalls with regards to the command filename in the command line:
it might contain a relative or absolute path
it might be quoted or not

Command line escaping single quote for PowerShell

I have a Windows application and on events, it calls a command like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x' -data '%y'"
The name parameter sometimes has ' in it. Is it possible to escape that somehow?
This is actually a lot trickier than you'd think. Escaping nested quotes in strings passed from cmd to PowerShell is a major headache. What makes this one especially tricky is that you need to make the replacement in a variable expanded by cmd in the quoted argument passed to powershell.exe within a single-quoted argument passed to a PowerShell script parameter. AFAIK cmd doesn't have any native functionality for even basic string replacements, so you need PowerShell to do the replacement for you.
If the argument to the -data paramater (the one contained in the cmd variable x) doesn't necessarily need to be single-quoted, the simplest thing to do is to double-quote it, so that single quotes within the value of x don't need to be escaped at all. I say "simplest", but even that is a little tricky. As Vasili Syrakis indicated, ^ is normally the escape character in cmd, but to escape double quotes within a (double-)quoted string, you need to use a \. So, you can write your batch command like this:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name \"%x%\" -data '%y%'"
That passes the following command to PowerShell:
G:\test.ps1 -name "value of x, which may contain 's" -data 'value of y'
If, however, x can also contain characters that are special characters in PowerShell interpolated strings (", $, or `), then it becomes a LOT more complicated. The problem is that %x is a cmd variable that gets expanded by cmd before PowerShell has a chance to touch it. If you single-quote it in the command you're passing to powershell.exe and it contains a single quote, then you're giving the PowerShell session a string that gets terminated early, so PowerShell doesn't have the opportunity to perform any operations on it. The following obviously doesn't work, because the -replace operator needs to be supplied a valid string before you can replace anything:
'foo'bar' -replace "'", "''"
On the other hand, if you double-quote it, then PowerShell interpolates the string before it performs any replacements on it, so if it contains any special characters, they're interpreted before they can be escaped by a replacement. I searched high and low for other ways to quote literal strings inline (something equivalent to perl's q//, in which nothing needs to be escaped but the delimiter of your choice), but there doesn't seem to be anything.
So, the only solution left is to use a here string, which requires a multi-line argument. That's tricky in a batch file, but it can be done:
setlocal EnableDelayedExpansion
set LF=^
set pscommand=G:\test.ps1 -name #'!LF!!x!!LF!'# -data '!y!'
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "!pscommand!"
This assumes that x and y were set earlier in the batch file. If your app can only send a single-line command to cmd, then you'll need to put the above into a batch file, adding the following two lines to the beginning:
set x=%~1
set y=%~2
Then invoke the batch file like this:
path\test.bat "%x%" "%y%"
The ~ strips out the quotes surrounding the command line arguments. You need the quotes in order to include spaces in the variables, but the quotes are also added to the variable value. Batch is stupid that way.
The two blank lines following set LF=^ are required.
That takes care of single quotes which also interpreting all other characters in the value of x literally, with one exception: double quotes. Unfortunately, if double quotes may be part of the value as you indicated in a comment, I don't believe that problem is surmountable without the use of a third party utility. The reason is that, as mentioned above, batch doesn't have a native way of performing string replacements, and the value of x is expanded by cmd before PowerShell ever sees it.
BTW...GOOD QUESTION!!
UPDATE:
In fact, it turns out that it is possible to perform static string replacements in cmd. Duncan added an answer that shows how to do that. It's a little confusing, so I'll elaborate on what's going on in Duncan's solution.
The idea is that %var:hot=cold% expands to the value of the variable var, with all occurrences of hot replaced with cold:
D:\Scratch\soscratch>set var=You're such a hot shot!
D:\Scratch\soscratch>echo %var%
You're such a hot shot!
D:\Scratch\soscratch>echo %var:hot=cold%
You're such a cold scold!
So, in the command (modified from Duncan's answer to align with the OP's example, for the sake of clarity):
powershell G:\test.ps1 -name '%x:'=''%' -data '%y:'=''%'
all occurrences of ' in the variables x and y are replaced with '', and the command expands to
powershell G:\test.ps1 -name 'a''b' -data 'c''d'
Let's break down the key element of that, '%x:'=''%':
The two 's at the beginning and the end are the explicit outer quotes being passed to PowerShell to quote the argument, i.e. the same single quotes that the OP had around %x
:'='' is the string replacement, indicating that ' should be replaced with ''
%x:'=''% expands to the value of the variable x with ' replaced by '', which is a''b
Therefore, the whole thing expands to 'a''b'
This solution escapes the single quotes in the variable value much more simply than my workaround above. However, the OP indicated in an update that the variable may also contain double quotes, and so far this solution still doesn't pass double quotes within x to PowerShell--those still get stripped out by cmd before PowerShell receives the command.
The good news is that with the cmd string replacement method, this becomes surmountable. Execute the following cmd commands after the initial value of x has already been set:
Replace ' with '', to escape the single quotes for PowerShell:
set x=%x:'=''%
Replace " with \", to escape the double quotes for cmd:
set x=%x:"=\"%
The order of these two assignments doesn't matter.
Now, the PowerShell script can be called using the syntax the OP was using in the first place (path to powershell.exe removed to fit it all on one line):
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x' -data '%y'"
Again, if the app can only send a one-line command to cmd, these three commands can be placed in a batch file, and the app can call the batch file and pass the variables as shown above (first bullet in my original answer).
One interesting point to note is that if the replacement of " with \" is done inline rather than with a separate set command, you don't escape the "s in the string replacement, even though they're inside a double-quoted string, i.e. like this:
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x:"=\"' -data '%y'"
...not like this:
powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name '%x:\"=\\"' -data '%y'"
I'm slightly unclear in the question whether %x and %y are CMD variables (in which case you should be using %x% to substitute it in, or a substitution happening in your other application.
You need to escape the single quote you are passing to PowerShell by doubling it in the CMD.EXE command line. You can do this by replacing any quotes in the variable with two single quotes.
For example:
C:\scripts>set X=a'b
C:\scripts>set Y=c'd
C:\scripts>powershell .\test.ps1 -name '%x:'=''%' '%y:'=''%'
Name is 'a'b'
Data is 'c'd'
where test.ps1 contains:
C:\scripts>type test.ps1
param($name,$data)
write-output "Name is '$name'"
write-output "Data is '$data'"
If the command line you gave is being generated in an external application, you should still be able to do this by assigning the string to a variable first and using & to separate the commands (be careful to avoid trailing spaces on the set command).
set X=a'b& powershell .\test.ps1 -name '%x:'=''%'
The CMD shell supports both a simple form of substitution, and a way to extract substrings when substituting variables. These only work when substituting in a variable, so if you want to do multiple substitutions at the same time, or substitute and substring extraction then you need to do one at a time setting variables with each step.
Environment variable substitution has been enhanced as follows:
%PATH:str1=str2%
would expand the PATH environment variable, substituting each occurrence
of "str1" in the expanded result with "str2". "str2" can be the empty
string to effectively delete all occurrences of "str1" from the expanded
output. "str1" can begin with an asterisk, in which case it will match
everything from the beginning of the expanded output to the first
occurrence of the remaining portion of str1.
May also specify substrings for an expansion.
%PATH:~10,5%
would expand the PATH environment variable, and then use only the 5
characters that begin at the 11th (offset 10) character of the expanded
result. If the length is not specified, then it defaults to the
remainder of the variable value. If either number (offset or length) is
negative, then the number used is the length of the environment variable
value added to the offset or length specified.
%PATH:~-10%
would extract the last 10 characters of the PATH variable.
%PATH:~0,-2%
would extract all but the last 2 characters of the PATH variable.
I beleive you can escape it with ^:
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name ^'%x^' -data ^'%y^'"
Try encapsulating your random single quote variable inside a pair of double quotes to avoid the issue.
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -ExecutionPolicy Bypass "G:\test.ps1 -name `"%x`" -data `"%y`""
The problem arises because you used single quotes and the random extra single quote appearing inside the single quotes fools PowerShell. This shouldn't occur if you double quote with backticks, as the single quote will not throw anything off inside double quotes and the backticks will allow you to double/double quote.
Just FYI, I ran into some trouble with a robocopy command in powershell and wanting to exclude a folder with a single quote in the name (and backquote didn't help and ps and robocopy don't work with double quote); so, I solved it by just making a variable for the folder with a quote in it:
$folder="John Doe's stuff"
robocopy c:\users\jd\desktop \\server\folder /mir /xd 'normal folder' $folder
One wacky way around this problem is to use echo in cmd to pipe your command to 'powershell -c "-" ' instead of using powershell "arguments"
for instance:
ECHO Write-Host "stuff'" | powershell -c "-"
output:
stuff'
Couple of notes:
Do not quote the command text you are echoing or it won't work
If you want to use any pipes in the PowerShell command they must be triple-escaped with carats to work properly
^^^|

How can I avoid escaping by accident in Perl using system()?

I want to run some commands using the system() command, I do this way:
execute_command_error("trash-put '/home/$filename'");
Where execute_command_error will report if there was an error with whatever system command it ran. I know I could just unlink the file using Perl commands, but I want to delete stuff using trash-put as it's a type of recycling program.
My problem is that $filename will sometimes have apostrophes, quotes, and other weird characters in it that mess up the system command or Perl itself.
Generate the command name and arguments as an array, and pass that to system:
my(#command) = ("trash-put", 'home/$filename');
system #command;
This means that Perl does not invoke the shell to do any metacharacter expansion (or I/O redirection, or command piping, or ...). It does mean it does exactly what you told it to do.
sub execute_command_error
{
system #_;
}
Borrowing information from the copious collection of comments:
Which is clearly documented in perldoc -f system or at perldoc.perl.org/functions/system.html (#Ether).
(See also the discussion of 'exec' below which is closely related.)
Did you mean to put $filename in single quotes? (#mobrule).
I did intend to use single quotes - I'm demonstrating that the $filename does not get expanded by Perl or Shell...In my test script, I used 'my.$file', and that gave me a file with a $ in the name - as I intended.
I think the desired quoting if you do want to invoke the shell (for example if you want some piping) is $command_line = "\"$command\" \"$arg1\" \"$arg2\"...". (#Jefromi).
Adding double quotes around the arguments won't help with embedded $, backtick1, '$(...)' and related notations. You more nearly need single quotes around things, but then you need to rewrite embedded single quotes as "'\''" which generates a single quote to terminate the current single-quoted argument, a backslash-quote combination to represent a single quote, and another single quote to resume the single-quoted argument.
This would be a good solution if I used system command directly; however I am using webmin's execute_command function, which is a bit over my head so I wouldn't know how to edit it to allow for arrays. Could you expand on the rewrite of embedded single quotes as "'\''"...This is what I will use, for now. (#Brian)
Roughly speaking, the way the (Unix) shells treat single quotes is "everything from the first single quote up to the next is literal text, no metacharacters". So, to get the shell to treat something as literal text, enclose it in single characters. That deals with everything except single quotes themselves. As my comment says, you have to use the 4-character replacement string to get a single quote embedded into the middle of a single quoted argument.
There is probably a neater way to do it than this (using one or two map operations, perhaps), but this should work:
for (my $i = 0; $i < scalar(#command); $i++)
{
$command[$i] =~ s/'/'\\''/g; # Replace single quotes by the magic sequence
$command[$i] = "'$command[$i]'"; # Wrap value in single quotes
}
You can then join the array to make a single string for transmission to execute_command.
It's better to write that as system { $command[0] } #command to handle the case where #command has one element. This is one of the things I talk about in the "Secure Programming Techniques" chapter of Mastering Perl. (#briandfoy).
As a general rule, I'll accept this correction. I'm not sure it is crucial in this instance, though, where the command name is provided by the program and it is only the arguments that are possibly provided the user. The command name 'trash-put' is safe from shell expansions (IFS is reset to default by the shell when it starts, so that avenue of attack is not available).
This issue is discussed in the 'perldoc -f exec' man page:
If you don't really want to execute the first argument, but want to lie to the program you are executing about its own name, you can specify the program you actually want to run as an "indirect object" (without a comma) in front of the LIST. (This always forces interpretation of the LIST as a multivalued list, even if there is only a single scalar in the list.)
Example:
$shell = '/bin/csh';
exec $shell '-sh'; # pretend it's a login shell
or, more directly,
exec {'/bin/csh'} '-sh'; # pretend it's a login shell
When the arguments get executed via the system shell, results are subject to its quirks and capabilities. See "STRING" in perlop for details.
Using an indirect object with exec or system is also more secure. This usage (which also works fine with system()) forces interpretation of the arguments as a multivalued list, even if the list had just one argument. That way you're safe from the shell expanding wildcards or splitting up words with whitespace in them.
#args = ( "echo surprise" );
exec #args; # subject to shell escapes
# if #args == 1
exec { $args[0] } #args; # safe even with one-arg list
The first version, the one without the indirect object, ran the echo program, passing it "surprise" an argument. The second version didn't; it tried to run a program named "echo surprise", didn't find it, and set $? to a non-zero value indicating failure.
1 How do you get a back-tick to display in Markdown?
As stated in perldoc -f system:
If there is more than one argument in LIST, or if LIST is an array
with more than one value, starts the program given by the first element of the list with arguments given by the rest of the list. If there is
only one scalar argument, the argument is checked for shell metacharacters, and if there are any, the entire argument is passed to the system's
command shell for parsing (this is "/bin/sh -c" on Unix platforms, but varies on other platforms). If there are no shell metacharacters in the
argument, it is split into words and passed directly to "execvp", which is more efficient.
I like to use IPC::System::Simple's version of system(), which gives more control, such as being able to capture various exceptions and handle certain error codes as "bad" and others as "good":
use IPC::System::Simple qw(system);
system("cat *.txt"); # will die on failure