Powershell replace function strange behavior - string

a simple example and i don't know how to get it to work...
function replace($rep, $by){
Process { $_ -replace $rep, $by }
}
when I do
"test" | replace("test", "foo")
the result is
test
When I do
function replace(){
Process { $_ -replace "test", "foo" }
}
"test" | replace()
the result is
foo
any idea ?

Functions in PowerShell follow the same argument rules as cmdlets and native commands, that is, arguments are separated by spaces (and yes, this also means you don't need to quote your arguments, as they are automatically interpreted as strings in that parsing mode):
'test' | replace test foo
So if you call a PowerShell function or cmdlet with arguments in parentheses you'll get a single argument that is an array within the function. Invocations of methods on objects follow other rules (that look roughly like in C#).
To elaborate a little: PowerShell has two different modes in which it parses a line: expression mode and command mode. In expression mode PowerShell behaves like a REPL. You can type 1+1 and get 2 back, or type 'foo' -replace 'o' and get f back. Command mode is for mimicking a shell's behaviour. That's when you want to run command, e.g. Get-ChildItem or & 'C:\Program Files\Foo\foo.exe' bar blah. Within parentheses mode determination starts anew which is why Write-Host (Get-ChildItem) is different from Write-Host Get-ChildItem.

Remove the () in your function call and remove comma.
"test" | replace "test" "foo"

Related

PowerShell, getting string syntax correct in 7-zip script

I can't seem to solve this, been struggling with it for a while (maybe it's simple; I just can't see it as I've been looking at it for so long).
I can get the 7z.exe syntax to work, but when I try and put it together into a simple script, it fails.
e.g., if I run .\zip.ps1 "C:\test\test.zip" "C:\test\test1.txt" "C:\test\test2.txt*
Instead of zipping up the 2 files required, it zips up everything in the C:\test folder, completely ignoring my arguments.
How can I adjust the below string syntax so that 7z.exe will correctly respect the input arguments and compress them from within a PowerShell script?
"Received $($args.Count) files:"
$parent = Split-Path (Split-Path $args[0]) -Leaf
$sevenzip = "C:\Program Files\7-Zip\7z.exe"
$zipname = "$($parent) $(Get-Date -Format 'yyyy_MM_dd HH_mm_ss').zip"
$args_line = $args | foreach-object { "`"$_`"" }
# $args_line = '"' + $($args -join """ """) + '"' # Want to use """ here so that it can capture not only files with spaces, but files with ' in the filename
''
"Zip Name : $zipname"
''
"Arguments : $args_line"
''
if (Test-Path $sevenzip) {
if (Test-Path "C:\0\$zipname") { rm "C:\0\$zipname" -Force }
''
'String output of the line to run:'
"& ""$sevenzip"" a -r -tzip ""C:\0\$zipname"" $args_line" # Taking this output and pasting onto console works.
''
& "$sevenzip" a -r -tzip "C:\0\$zipname" "$args_line" # This does not work
} else {
"7z.exe was not found at '$sevenzip', please check and try again"
}
The error that I get is:
Files read from disk: 0
Archive size: 22 bytes (1 KiB)
Scan WARNINGS for files and folders:
1 : The system cannot find the file specified.
----------------
Pass $args directly to your & "$sevenzip" call:
& "$sevenzip" a -r -tzip "C:\0\$zipname" $args
This makes PowerShell pass the array elements as individual arguments, automatically enclosing them in "..." if needed (based on whether they contain spaces).
Using arrays as arguments for external programs is in effect an implicit form of array-based splatting; thus, you could alternatively pass #args.
Generally, note that in direct invocation[1] you cannot pass multiple arguments to an external program via a single string; that is, something like "$args_line" cannot be expected to work, because it is passed as a single argument to the target program.
If you want to emulate the resulting part of the command line, for display purposes:
($argListForDisplay = $args.ForEach({ ($_, "`"$_`"")[$_ -match ' '] })) -join ' '
Note:
Each argument is conditionally enclosed in "..." - namely based on whether it contains at least one space - and the resulting tokens are joined to form a single, space-separated list (string).
The assumption is that no argument has embedded " chars.
A simplified example:
& { # an ad-hoc script block that functions like a script or function
# Construct the argument list *for display*
$argListForDisplay = $args.ForEach({ ($_, "`"$_`"")[$_ -match ' '] }) -join ' '
#"
The following is the equivalent of:
Write-Output $argListForDisplay
"#
# Pass $args *directly*
# Simply prints the argument received one by one, each on its own line.
# Note: With PowerShell-native commands, generally use #args instead (see below).
Write-Output $args
} firstArg 'another Arg' lastArg # sample pass-through arguments
Output:
The following is the equivalent of:
Write-Output firstArg "another Arg" lastArg
firstArg
another Arg
lastArg
Note: Write-Output is used in lieu of an external program for convenience. Technically, you'd have to use #args instead of $args in order to pass the array elements as individual, positional arguments, but, as stated, this is the default behavior with externals programs. Write-Output, as a PowerShell-native command, receives the array as a whole, as a single argument when $args is used; it just so happens to process that array the same way as if its elements had been passed as individual arguments.
[1] You can use a single string as an -ArgumentList value for Start-Process. However, Start-Process is usually the wrong tool for invoking console applications such as 7z.exe - see this answer.

Why is this condition being met when verifying parameters using the -notMatch condition?

I am validating parameters passed into a release pipeline with Powershell. The parameter I am passing as a pipeline variable is val1. Here is my code below:
if ("$(Value)" -notMatch "val1" -or "$(Value)" -notMatch "val2"){
Write-Host "$(Value) must be val1 || val2"
Write-Host "Value of param: "$(Value)""
exit 1
}
When I print my value is states val1. Why is this condition being met? I thought perhaps it was because its case sensitive however even when I modify the condition to catch the exact case, it's still being met.
If your variable should have only val1 or val2, you have to use -and instead -or.
if ("$(Value)" -notMatch "val1" -and "$(Value)" -notMatch "val2"){
Write-Host "$(Value) must be val1 || val2"
Write-Host "Value of param: "$(Value)""
exit 1
}
Change it to
if ("$(Value)" -notMatch "val1" -and "$(Value)" -notMatch "val2"){
Write-Host "$(Value) must be val1 || val2"
Write-Host "Value of param: "$(Value)""
exit 1
}
Note: $(Value) in the question as well as in the code below is an Azure pipeline macro, not a PowerShell variable reference such as $Value. Since such a macro is expanded to its value before PowerShell sees it, enclosing it in "..." - again, as in the question and in the code below - ensures that no syntax error occurs if the value happens to contain spaces or other PowerShell metacharacters.
The Shamrai Aleksander's helpful answer explains the logic error, but there may be an additional problem:
-match (and its -notmatch variant), the regular-expression matching operator, matches substrings by default, so that "$(Value)" expanding to aval1, for instance, would mistakenly pass the test as well.
for literal, whole-value comparisons against a collection, you can use the -in operator (note that -contains serves the same purpose only with the operands reversed) and its variants, notably -notin in this case; use -cnotin for case-sensitive comparisons.
# Note: `$(Value)` is an *Azure pipeline macro (variable)*, which is expanded
# *before* PowerShell sees the code.
# In pure PowerShell code, the equivalent would be just `$Value`
if ("$(Value)" -notin 'val1', 'val2') {
Write-Error '$(Value) must be val1 || val2'
exit 1
}
I thought perhaps it was because its case-sensitive however
Note that all PowerShell operators are case-insensitive by default (when they operate on text); PowerShell is case-insensitive by default in most respects.
Case-sensitive operation requires use of the c prefixed variants, such as -cmatch.
Optionally, to signal the intent more clearly, you can signal case-insensitive operation by using the i-prefixed variants, such as -imatch (all i-prefixed variants behave like their non-prefixed base forms).

Is it possible to execute a function in shell by calling it with parenthesis

How can I execute a function in shell by calling it with parenthesis i.e.:
my_func()
{
echo $0
echo $1
}
my_func("aa","bb")
and not like that
my_func "aa" "bb"
You can't. Why do you want to apply some other language's syntax to the shell?
You cannot, in most shells, bash included, ( and ) are, with the exception of the empty () form a reserved control operator. You could escape or quote, but that's hardly useful.
You can slightly abuse { } brace expansion:
my_func {1,2,3}
since {1,2,3} expands to "1 2 3", though it has other features too. You must ensure there are no (unquoted) spaces within, {1, 2, 3} is treated exactly as-is. Literal commas must be quoted or \ escaped. You need a space after the function name, or it will do something rather different.
Or, if you really want to play games, the [ ] can be misused too, not entirely unlike the way [ (aka test) works, at the expense of non-trivial parsing. This is a passable though imperfect example which shows why:
function my_func ()
{
local IFS=" ,"
set -- $*;
for ((ff=1; ff<=$#; ff++)); do
printf "\$%i=<%s> " $ff "${!ff}"
done
printf "\n"
}
my_func [ "a",b,c ]
my_func [ 1, 2 ,3 ]
bash is not tcl... In case you're trying to write a program which is valid as two or more different languages, you should search instead for polyglot programs.
I'm still working on a way to replace { and } with BEGIN and END.
How can I execute a function in shell by calling it with parenthesis?
Try this:
my_func "(" ")"

How do I delay expansion of variables in PowerShell strings?

Whatever you want to call it, I'm trying to figure out a way to take the contents of an existing string and evaluate them as a double-quoted string. For example, if I create the following strings:
$string = 'The $animal says "meow"'
$animal = 'cat'
Then, Write-Host $string would produce The $animal says "meow". How can I have $string re-evaluated, to output (or assign to a new variable) The cat says "meow"?
How annoying...the limitations on comments makes it very difficult (if it's even possible) to include code with backticks. Here's an unmangled version of the last two comments I made in response to zdan below:
----------
Actually, after thinking about it, I realized that it's not reasonable to expect The $animal says "meow" to be interpolated without escaping the double quotes, because if it were a double-quoted string to begin with, the evaluation would break if the double quotes weren't escaped. So I suppose the answer would be that it's a two step process:
$newstring = $string -replace '"', '`"'
iex "`"$string`""
One final comment for posterity: I experimented with ways of getting that all on one line, and almost anything that you'd think works breaks once you feed it to iex, but this one works:
iex ('"' + ($string -replace '"', '`"') + '"')
Probably the simplest way is
$ExecutionContext.InvokeCommand.ExpandString($var)
You could use Invoke-Expression to have your string reparsed - something like this:
$string = 'The $animal says `"meow`"'
$animal = 'cat'
Invoke-Expression "Write-Host `"$string`""
Note how you have to escape the double quotes (using a backtick) inside your string to avoid confusing the parser. This includes any double quotes in the original string.
Also note that the first command should be a command, if you need to use the resulting string, just pipe the output using write-output and assign that to a variable you can use later:
$result = Invoke-Expression "write-output `"$string`""
As noted in your comments, if you can't modify the creation of the string to escape the double quotes, you will have to do this yourself. You can also wrap this in a function to make it look a little clearer:
function Invoke-String($str) {
$escapedString = $str -replace '"', '`"'
Invoke-Expression "Write-Output `"$escapedString`""
}
So now it would look like this:
# ~> $string = 'The $animal says "meow"'
# ~> $animal = 'cat'
# ~> Invoke-String $string
The cat says "meow"
You can use the -f operator. This is the same as calling [String]::Format as far as I can determine.
PS C:\> $string = 'The {0} says "meow"'
PS C:\> $animal = 'cat'
PS C:\> Write-Host ($string -f $animal)
The cat says "meow"
This avoids the pitfalls associated with quote stripping (faced by ExpandString and Invoke-Expression) and arbitrary code execution (faced by Invoke-Expression).
I've tested that it is supported in version 2 and up; I am not completely certain it's present in PowerShell 1.
Edit: It turns out that string interpolation behavior is different depending on the version of PowerShell. I wrote a better version of the xs (Expand-String) cmdlet with unit tests to deal with that behavior over here on GitHub.
This solution is inspired by this answer about shortening calls to object methods while retaining context. You can put the following function in a utility module somewhere, and it still works when you call it from another module:
function xs
{
[CmdletBinding()]
param
(
# The string containing variables that will be expanded.
[parameter(ValueFromPipeline=$true,
Position=0,
Mandatory=$true)]
[string]
$String
)
process
{
$escapedString = $String -replace '"','`"'
$code = "`$ExecutionContext.InvokeCommand.ExpandString(`"$escapedString`")"
[scriptblock]::create($code)
}
}
Then when you need to do delayed variable expansion, you use it like this:
$MyString = 'The $animal says $sound.'
...
$animal = 'fox'
...
$sound = 'simper'
&($MyString | xs)
&(xs $MyString)
PS> The fox says simper.
PS> The fox says simper.
$animal and $sound aren't expanded until the last two lines. This allows you to set up a $MyString up front and delay expansion until the variables have the values you want.
Invoke-Expression "`"$string`""

Bash, referring to array by value?

Is there some way to access a variable by referring to it by a value?
BAR=("hello", "world")
function foo() {
DO SOME MAGIC WITH $1
// Output the value of the array $BAR
}
foo "BAR"
Perhaps what you're looking for is indirect expansion. From man bash:
If the first character of parameter is an exclamation point (!), a level of variable indirection is introduced. Bash uses the
value of the variable formed from the rest of parameter as the name of the variable; this variable is then expanded and that value
is used in the rest of the substitution, rather than the value of parameter itself. This is known as indirect expansion. The
exceptions to this are the expansions of ${!prefix*} and ${!name[#]} described below. The exclamation point must immediately fol‐
low the left brace in order to introduce indirection.
Related docs: Shell parameter expansion (Bash Manual) and Evaluating indirect/reference variables (BashFAQ).
Here's an example.
$ MYVAR="hello world"
$ VARNAME="MYVAR"
$ echo ${!VARNAME}
hello world
Note that indirect expansion for arrays is slightly cumbersome (because ${!name[#]} means something else. See linked docs above):
$ BAR=("hello" "world")
$ v="BAR[#]"
$ echo ${!v}
hello world
$ v="BAR[0]"
$ echo ${!v}
hello
$ v="BAR[1]"
$ echo ${!v}
world
To put this in context of your question:
BAR=("hello" "world")
function foo() {
ARR="${1}[#]"
echo ${!ARR}
}
foo "BAR" # prints out "hello world"
Caveats:
Indirect expansion of the array syntax will not work in older versions of bash (pre v3). See BashFAQ article.
It appears you cannot use it to retrieve the array size. ARR="#${1}[#]" will not work. You can however work around this issue by making a copy of the array if it is not prohibitively large. For example:
function foo() {
ORI_ARRNAME="${1}[#]"
local -a ARR=(${!ORI_ARRNAME}) # make a local copy of the array
# you can now use $ARR as the array
echo ${#ARR[#]} # get size
echo ${ARR[1]} # print 2nd element
}
BAR=("hello", "world")
function foo() {
eval echo "\${$1[#]}"
}
foo "BAR"
You can put your arrays into a dictionary matched with their names. Then you can look up this dictionary to find your array and display its contents.

Resources