Substring in PowerShell to truncate string length - string

Is it possible in PowerShell, to truncate a string, (using SubString()?), to a given maximum number of characters, even if the original string is already shorter?
For example:
foreach ($str in "hello", "good morning", "hi") { $str.subString(0, 4) }
The truncation is working for hello and good morning, but I get an error for hi.
I would like the following result:
hell
good
hi

You need to evaluate the current item and get the length of it. If the length is less than 4 then use that in the substring function.
foreach ($str in "hello", "good morning", "hi") {
$str.subString(0, [System.Math]::Min(4, $str.Length))
}

Or you could just keep it simple, using PowerShell's alternative to a ternary operator:
foreach ($str in "hello", "good morning", "hi") {
$(if ($str.length -gt 4) { $str.substring(0, 4) } else { $str })
}
While all the other answers are "correct", their efficiencies go from sub-optimal to potentially horrendous. The following is not a critique of the other answers, but it is intended as an instructive comparison of their underlying operation. After all, scripting is more about getting it running soon than getting it running fast.
In order:
foreach ($str in "hello", "good morning", "hi") {
$str.subString(0, [System.Math]::Min(4, $str.Length))
}
This is basically the same as my offering except that instead of just returning $str when it is too short, we call substring and tell it to return the whole string. Hence, sub-optimal. It is still doing the if..then..else but just inside Min, vis.
if (4 -lt $str.length) {4} else {$str.length}
foreach ($str in "hello", "good morning", "hi") { $str -replace '(.{4}).+','$1' }
Using regular expression matching to grab the first four characters and then replace the whole string with them means that the entire (possibly very long) string must be scanned by the matching engine of unknown complexity/efficiency.
While a person can see that the '.+' is simply to match the entire remainder of the string, the matching engine could be building up a large list of backtracking alternatives since the pattern is not anchored (no ^ at the begining). The (not described) clever bit here is that if the string is less than five characters (four times . followed by one or more .) then the whole match fails and replace returns $str unaltered.
foreach ($str in "hello", "good morning", "hi") {
try {
$str.subString(0, 4)
}
catch [ArgumentOutOfRangeException] {
$str
}
}
Deliberately throwing exceptions instead of programmatic boundary checking is an interesting solution, but who knows what is going on as the exception bubbles up from the try block to the catch. Probably not much in this simple case, but it would not be a recommended general practice except in situations where there are many possible sources of errors (making it cumbersome to check for all of them), but only a few responses.
Interestingly, an answer to a similar question elsewhere using -join and array slices (which don't cause errors on index out of range, just ignore the missing elements):
$str[0..3] -join "" # Infix
(or more simply)
-join $str[0..3] # Prefix
could be the most efficient (with appropriate optimisation) given the strong similarity between the storage of string and char[]. Optimisation would be required since, by default, $str[0..3] is an object[], each element being a single char, and so bears little resemblance to a string (in memory). Giving PowerShell a little hint could be useful,
-join [char[]]$str[0..3]
However, maybe just telling it what you actually want,
new-object string (,$str[0..3]) # Need $str[0..3] to be a member of an array of constructor arguments
thereby directly invoking
new String(char[])
is best.

You could trap the exception:
foreach ($str in "hello", "good morning", "hi") {
try {
$str.subString(0, 4)
}
catch [ArgumentOutOfRangeException] {
$str
}
}

More regex love, using lookbehind:
PS > 'hello','good morning','hi' -replace '(?<=(.{4})).+'
hell
good
hi

I'm late to the party as always! I have used the PadRight string function to address such an issue. I cannot comment on its relative efficiency compared to the other suggestions:
foreach ($str in "hello", "good morning", "hi") { $str.PadRight(4, " ").SubString(0, 4) }

You can also use -replace
foreach ($str in "hello", "good morning", "hi") { $str -replace '(.{4}).+','$1' }
hell
good
hi

Old thread, but I came across the same problem and ended up with the below:-
$str.padright(4,"✓").substring(0,4).replace("✓","")
Replace the ✓ character with whatever rogue character you want. I used the character obtained from pressing the ALT GR and backtick key on the keyboard.

UGH, I feel so dirty, but here it is:
-join ("123123123".ToCharArray() | select -first 42)
outputs full string: 123123123
-join ("123123123".ToCharArray() | select -first 3)
outputs first 3 characters: 123

Related

parsing conditional operators as strings and using as conditional operators

my $line ="Corner:Default,Output:fall_delay_slew_1,Mean=34.97p,Std- dev=1.767p,Min=30.02p,Max=39.71p"; #added semicolon
my $my_value="COND = Mean > 3"; #this has come from the parsed file.
$my_value =~ m/(\w+)\s*(.)\s*(\d+)/;
my $cond=$1;
my $sign=$2;
my $value=$3;
print "DEBUG:cond is $cond and sign $sign and value $value \n";
if ( $line =~ m/$cond=(.*?),/) {
if ( "$value $sign $1" ) {
print "$value is $sign than $1\n";
} else {
print "actual value is less\n";
}
}
If you see in the above if statement always evaluates to true.
How can I solve this kind of problem i.e $sign = "<" (could be any operator)
but when I want to compare it with $value I want it to function as an
operator and not as a string.
What you're willing to do (executing a string as code) can be done with
eval. That doesn't mean it is the most appropriate way of doing
it though. Do it only if you guarantee your input safety and check for
it.
A better way would be checking the operator your self and determining
how to proceed. If you use a recent Perl version, the given-when
feature can be handy to do this:
use feature 'switch'; # not needed if you already use 5.010 or greater
given ($sign) {
when ('<') { say "$cond less than $value" }
when ('>') { say "$cond greater than $value" }
default {
warn "unrecognized operator `$sign'\n";
# decide what to do
}
}

How to compare strings that have an ampersand in them in PowerShell

I am using PowerShell to compare two strings that have an ampersand (&) in them (i.e. the string "Policies & Procedures").
No matter what I try, I cannot get these strings to match. I have tried trimmed the strings to get rid of an extra white spaces. I have tried wrapping the the string in both single and double quotes (and a combination of both):
"Policies & Procedures"
'Policies & Procedures'
"'Policies & Procedures'"
The code I am using to compare the strings is:
if ($term1 -eq $term2) {
do something
}
Inspecting the strings visually - they are identical, however the if statement never evaluates to true. Is there a way to compare these two strings so that it does evaluate to true?
EDIT
The context in which I am doing this string compare is looking for a term name in a taxonomy for a SharePoint site. Here is the code I am using:
function getTerm($termName) {
foreach($term in $global:termset.Terms) {
$termTrimmed = $term.Name.trim()
Write-Host "term name = $termTrimmed" -foregroundcolor cyan
if ($termTrimmed -eq $termName) {
return $term
}
}
return null
}
I have printed both term.Name and termName to the screen and they are identical. If there is no ampersand in the string, this function works. If there is an ampersand this function fails. This is how I know the ampersand is the problem.
This is a known quirk:
There are two types of ampersands that you need to be aware of when
playing with SharePoint Taxonomy
Our favorite and most loved
& ASCII Number: 38
And the impostor
& ASCII Number: 65286
After reading this article by Nick Hobbs, it became apparent
that when you create a term it replaces the 38 ampersand with a
65286 ampersand.
This then becomes a problem if you want to do a comparison with your
original source (spreadsheet, database, etc) as they are no longer the
same.
As detailed in Nick’s article, you can use the
TaxonomyItem.NormalizeName method to create a "Taxonomy" version of
your string for comparison:
Try this (not tested on real SharePoint):
function getTerm($termName)
{
foreach($term in $global:termset.Terms) {
$termNormalized = [Microsoft.SharePoint.Taxonomy.TaxonomyItem]::NormalizeName($term.Name)
if ($termNormalized -eq $termName) {
return $term
}
}
return null
}
After converting both strings to char arrays and comparing the unicode value of the ampersands the problem is revealed. The ampersand used in the search string has a value of 38 while the ampersand returned from the SharePoint term store has a value of 65286 (called a full ampersand although looks identical to a regular ampersand on screen).
The solution was to write my own string comparison function and take into account the differences in the ampersand values. Here is the code:
function getTerm($termName) {
$searchChars = $termName.toCharArray()
$size = $searchChars.Count;
foreach($term in $global:termset.Terms) {
$match = $True
$chars = $term.Name.trim().toCharArray()
if ($size -eq $chars.Count) {
for ($i = 0; $i -lt $size; $i++) {
if ($searchChars[$i] -ne $chars[$i]) {
# handle the difference between a normal ampersand and a full width ampersand
$charCode1 = [int] $searchChars[$i]
$charCode2 = [int] $chars[$i]
if ((($charCode1 -eq 38) -or ($charCode1 -eq 65286 )) -and (($charCode2 -eq 38) -or ($charCode2 -eq 65286 ))) {
continue
} else {
$match = $False
break
}
}
}
} else {
$match = $False
}
if ($match -eq $True) {
return $term
}
}
return $null
}

Add to integer within string?

I would like to make a string by incrementing a variable within the string.
eg.
$result = "Result: $amount++";
How can this be achieved?
It can be done using trickery.
$result = "Result: ${\( $amount++ )}";
But why would you want to???
$result = "Result: ".$amount++;
If you want to modify a number in a string, you have to use the e modifier for the s operation. This makes Perl evaluating the replacement as an expression.
#! /usr/bin/perl
$_ = "Result: 1\n";
s/\d+/$&+1/e;
print;
It is documented in the Perl manual.
I take it that you have a string that already contains a 'number' (string of digits), and you want to increment that number within.
You'd have to extract the "number" first, in one way or another, since it is merely a string of chars when inside a string; then increment it and join it all back. I'll take it that it is a string of digits bounded by non-digits
my ($pre, $num, $post) = $str =~ m/(\D*)(\d+)(\D*)/;
$str = $pre . ($num+1) . $post;
This makes a critical assumption that the word contains a string of digits in only one place and no digits elsewhere, since if that were not the case the problem would be ill posed.
Just for the curiousity of it I'd like to add a bit to this. A part of a string can be accessed by substr, and that function can be manipulated as an lvalue (can be assigned to). So, if you were to know the starting position and the length of your "number" (what can be found in various ways) you could cram the above process in one statement, if you must
substr($str, $num_beg, $num_len) = substr($str, $num_beg, $num_len) + 1;
or, equally bad
substr($str, $num_beg, $num_len) = ($str =~ m/(\d+)/)[0] + 1;
Now your starting $str string contains the "number" within it incremented. However, this is plain nasty and I cannot recommend any of it. Finally, you can of course find $num_beg and $num_len on the fly, inside of substr, but that is just too much as the poor string would be processed three times in a single statement. (Also, this changes your $str in place, which your question hints is not what you want.)
Added Regex provide the capability to run code in the replacement part, by using /e modifier.
my $str = "ah20bah";
$str =~ s/(\d+)/$1+1/e;
say $str; # it's 'ah21bah'
See this in perlrequick and in perlop.

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`""

perl code behaving strange

I have written a code to get the url of a website and then search for a string and then compare that string(actually a number) with a hardcoded number
#!/usr/bin/perl
use LWP::Simple;
my $oldversion =36;
$pageURL="http://www.google.com/isos/preFCS5.3/LATESTGOODCVP/";
my $simplePage=get($pageURL);
my $newPage = "$simplePage";
my $str = (split("href=\"CVP-LATEST-5.3.0.",$newPage ))[1];
my $version = substr("$str",0,2);
print $version; // HERE IT PRINT 37 WHICH IS CORRECT
if($version =! $oldVersion )
{
print $version; // BUT HERE IT PRINTS 1 WHICH IS WRONG. HOW IS IT CHANGING ?
##-- fetch the zip and save it as perlhowto.zip
my $status = getstore("http://www.google.com/isos/preFCS5.3/LATESTGOODCVP/CVP-LATEST-5.3.0.$version.iso", "CVP-LATEST-5.3.0.$version.iso");
}
else
{
print("Currently new version\n");
}
Why is it changing the value ? its not able to download the file becuase of that.
You mean !=, not =!, which is an assignment of a negation.
Also, split always uses a regex (except for the very special case of a string that has a single space), so those .s in 5.3.0. will match any non-newline. You probably want to \-escape them.
You may be interested in the uscan script in the debian devtools package.
You have got your "not equals" operator backwards. It should be != rather than =!.
By using =! you are in effect saying "set $version to the negated value of $oldversion".
Here is the offending line
if($version =! $oldVersion ) # Should be if($version != $oldVersion )
Also notice that by using the != operator you are telling perl that $version and $oldversion contain numbers. For string comparisons you should use the ne operator, which assumes that these variables contain strings.
if($version ne $oldVersion ) # String inequality
Here is the documentation for equality operators -
http://perldoc.perl.org/perlop.html#Equality-Operators
It's because you are assigning to $version the value !$oldVersion in this "test":
if($version =! $oldVersion )
And $oldVersion is nothing--but $oldversion is 37. You are assigning $version the boolean negation of an undefined variable. Undefined is boolean false, and so the negation is boolean true or 1.
If you read very much on perl, you're bound to come across the advice to use strict and warnings. Had you done that, it would have told you, among other things:
Global symbol "$oldVersion" requires explicit package name at - line 21.
This means that you didn't declare $oldVersion as lexical (my) or package-level (our) in this package, so if you want to use it, please include the package where you're getting it. In a vast majority of cases, a seasoned Perl programmer will recognize this as "Ugh, I didn't declare $oldVersion!" and the reason is that you declared $oldversion.
Your use of split doesn't make a lot of sense here. What you really want are the two digits following the CVP-LATEST-5.3.0. string. You're also not really doing anything by assigning one variable to another with the addition of quotes ($newPage = "$simplePage").
And, of course, as others have pointed out, the comparison is != not =!.
I'd rewrite this as:
use strict;
use warnings;
use LWP::Simple;
my $oldVersion = 36;
my $url = 'http://www.google.com/isos/preFCS5.3/LATESTGOODCVP/';
my $newPage = get($url)
or die "Cannot retrieve contents from $url\n";
if ( $newPage =~ /href=\"CVP-LATEST-5\.3\.0\.(\d\d)/ ) {
my $version = $1;
if ( $version != $oldVersion ) {
my $status = getstore($url . "CVP-LATEST-5.3.0.$version.iso",
"CVP-LATEST-5.3.0.$version.iso");
} else {
print "Already at most recent version\n";
}
} else {
die "Cannot find version tag in contents from $url\n";
}

Resources