I’m looping through a CSV file and using ForEach-Object
loop to grab info to attempt to update in_stock status on Woocommerce, what ends up happening is the woocommerce only see’s one entry. I’m not a programmer, I’m still learning PowerShell and for the life of me I just can’t understand the logic of for loops and it’s output properly. I know it reads the entries in the CSV, but I think it’s just overwriting the previous entry.
Another issue I’m having is properly setting in_stock values as true and false for each object respectively, if one is true then all false entries are also set as true. I can’t seem to figure out how to assign true | false correctly.
I’ve been looking up PowerShell using the MS docs on it and how to append hashtables but I’m still not finding the answers or examples that will point me in the right direction. I’ve gone so far as to purchase PowerShell tutorials offsite and still haven’t found a way to do this properly.
$website = "https://www.mywebsite.com"
$params += @{
type= @();
name = @();
SKU = @();
catalog_visibility = @();
regular_price = @();
in_stock = @();
categories = @();
}
$csv = Import-Csv C:testtest-upload.csv
$csv | Select-Object -Property Type, SKU, Name, 'Visibility in catalog',
'Tax status', 'In stock?', Stock, 'Backorders allowed?', 'Allow customer
reviews?', 'Regular price', Categories | ForEach-Object{
$params.type += $_.type
$params.SKU += $_.SKU
$params.name += $_.name
$params.catalog_visibility += $_.'Visibility in catalog'
$params.categories += $_.Categories
$params.regular_price += $_.'Regular price'
$params.in_stock += $_.'In stock?'
if ($params.in_stock = 0) {$params.in_stock -replace 0, $false}
elseif($params.in_stock = 1) {$params.in_stock -replace 1, $true}
}
foreach($key in $params.keys){
Write-Output $params[$key]
}
I’m looking to get something like this
{
"name": "part 1",
"type": "simple",
"SKU": "0001",
"regular_price": "21.99",
"in_stock: false",
"categories: category 1",
"catalog_visibility": "hidden",
},
{
"name": "part 2",
"type": "simple",
"SKU": "0002",
"regular_price": "11.99",
"in_stock: true",
"categories: category 2",
"catalog_visibility": "hidden",
}
and what I am actually getting is
{
"name": "part 1 part 2",
"type": "simple simple ",
"SKU": "0001 0002",
"regular_price": "21.99 11.99",
"in_stock: true true",
"categories: category 1 category 1",
"catalog_visibility": "hidden hidden",
}
I would really appreciate it if someone could point me in the right direction and give me a few tips on best practice
2
Answers
So what you are doing is a lot of
+=
to try and create an array, but you’re doing it at the wrong level. What you want to do is create a hashtable (or quite possibly a PSCustomObject) for each item in the CSV, and capture them as an array of objects (be they hashtable objects, or PSCustomObject objects). So, let’s try and restructure things a little to do that. I’m ditching the template, we don’t care, we’re defining it for each object anyway. I’m going to output a hashtable for each item in theForEach-Object
loop, and capture it in$params
. This should give you the results you want.Since you’re new to programming let’s talk a little bit about arrays and hashtables.
Arrays are like lists (sometimes they are called lists too), specifically, ordered lists by position.
Hashtables are a type of dictionary, whereby you have a Key that corresponds to a Value.
In PowerShell the syntax you’re using for creating an array is
@()
(that one’s empty, it could contain items) and the syntax you use for creating a hashtable is@{}
(also empty, could contain values).You don’t show your initial definition of
$params
, but based on the rest of the code I’m going to assume it’s like this:Then, you have this:
So what this would mean is that you took your array,
$params
, and added a new item to it. The new item is the hashtable literal you defined here. All the names you added, liketype
,name
,SKU
, etc. are Keys.According to your desired output, it does look like you want an array of hashtables, so I think that part is correct.
But note that the values you assigned to them are all empty arrays. This is curious because what you showed as your desired output has each hashtable with those keys being singular values, so I think that’s one issue, and in fact it’s clouding the area where the problem really is.
So let’s skip ahead to the body of the loop, where you use this pattern:
Remember that
$params
is an array, so you should have items in it starting at position 0, like$params[0]
,$params[1]
, etc. To change the SKU of the second hashtable in the array, you’d use$params[1].SKU
or$params[1]['SKU']
.But what you’re doing is just
$params.SKU
. In many languages, and indeed in PowerShell before v3, this would throw an error. The array itself doesn’t have a property namedSKU
. In PowerShell v3 though the dot.
operator was enhanced to allow it to introspect into an array and return each item’s property with the given name, that is:is the same as if we had done:
It’s very useful but might be confusing you here.
So back to your object,
$params
is an array with, so far, only a single hashtable in it. And in your loop you aren’t adding anything to$params
.Instead you ask for
$params.SKU
, which in this case will be theSKU
of every hashtable in the array, but there’s only one hashtable, so you only get oneSKU
.Then you add to the
SKU
itself:Here’s the part where setting
SKU
initially to an empty array is hiding your issue. IfSKU
were a string, this would fail, because strings don’t support+=
, but since it’s an array, you’re taking this new value, and adding it to the array ofSKU
s that exist as the value of the single hashtable you’re working against.Where to go from here
$params
arrayLet’s take a look:
This is the main problem you have, I left out the in stock part because I’m going to explain that logic separately.
It looks like your CSV has an
In stock?
column that can be0
or1
for false/true.First thing I’ll address is that
=
in PowerShell is always assignment. Testing for equality is-eq
, so:Next, let’s talk about true/false values; they’re called Boolean or bool for short, and you should usually use this data type to represent them. Any time you do a comparison for example like
$a -eq 5
you’re returning a bool.There’s strong support for converting other types to bool, for instance if you want to evaluate a number as bool,
0
is false, and all other values are true. For strings, a$null
value or an empty string is false, all other values are true. Note that if you have a string"0"
that is true because the string has a value.That also means that the number
0
is not the same as the string'0'
, but PowerShell does attempt to do conversions between types, usually trying to convert the right side’s type to the left side for comparison, so PowerShell will tell you0 -eq '0'
is true (same with'0' -eq 0
).And for your situation, reading from a CSV, those values will end up as strings, but because of the above, your equality tests will work anyway (it’s just worth knowing the details).
The issue with your use of
-replace
though, is that it’s a string operation, so even if it works, you’re going to end up with the string representation of a boolean, not the actual bool, even though you said to use$true
and$false
directly (and this is again because of type conversion;-replace
needs a string there, PowerShell converts your bool to string to satisfy it).So, after that long-winded explanation, what makes sense then is this:
in fact, the
elseif
isn’t necessary since you can only have 2 values:Even further though, we can use conversions to not need a conditional at all. Remember what I said about converting strings to numbers, and numbers to bool.
Now, we can do this:
cool! Let’s put it back into the other code:
Deeper dive!
Piping: you’re doing some calls like the
Import-Csv
call and assigning its output to a variable, then piping that variable into another command. That’s fine, it’s not wrong, but you could also just pipe the first command’s output directly into the second like so:I updated to formatting a little to show that you can use a line break after a pipe
|
, which can look a little cleaner.About
Select-Object
: its purpose is to take objects with a certain set of properties, and give you back a new object with a more limited (or sometimes with brand new) properties (it has other uses around changing the number of objects or filtering the array in other ways that aren’t relevant here at the moment).But I bring this up, because all the properties (columns) you’re selecting are by name, and therefore must exist on the input object. And since you refer to each one later directly as opposed to display the entire thing, there’s no reason to use
Select-Object
to filter down the properties, so that entire call can be removed:Nice! Looking slim.
About arrays and
+=
. This is ok in most cases to be honest, but you should know that each time you do this, in reality a new array is being created and all of the original items plus the new item are being copied into it. This doesn’t scale, but again it’s fine in most use cases.What you should also know is that the output from a pipeline (like any command, or your main script code, or the body of
ForEach-Object
is all sent to the next command in the pipeline (or back out the left side if there’s nothing else). This can be any number of items, and you can use assignment to get all of those values, like:$a
will be an array if there’s more than one item, and during processing it doesn’t continually create and destroy arrays.So how is this relevant to you? It means you don’t have to make
$params
an empty array and append to it, just return your new hashtables in each loop iteration, and then assign the output of your pipeline right to$params
!And now we’ve got your script down to a single pipeline (you could make it a single line but I prefer multi-line formatting).