skip to Main Content

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


  1. 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 the ForEach-Object loop, and capture it in $params. This should give you the results you want.

    $website = "https://www.mywebsite.com"
    
    $csv = Import-Csv C:testtest-upload.csv
    $params = $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{ 
        @{
            type = $_.type
            SKU = $_.SKU
            name = $_.name 
            catalog_visibility = $_.'Visibility in catalog'
            categories = $_.Categories
            regular_price = $_.'Regular price'
            in_stock = [boolean][int]($_.'In stock?')
        }
    }
    
    Login or Signup to reply.
  2. 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:

    $params = @()
    

    Then, you have this:

    $params += @{ 
        type= @();
        name = @();
        SKU = @();
        catalog_visibility = @();
        regular_price = @();
        in_stock = @();
        categories = @();
    }
    

    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, like type, 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:

    $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?'
    

    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 named SKU. 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:

    $a = @('apple','orange','pear')
    $a.PadLeft(10,'~')
    

    is the same as if we had done:

    $a = @('apple','orange','pear')
    $a | ForEach-Object { $_.PadLeft(10,'~') }
    

    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 the SKU of every hashtable in the array, but there’s only one hashtable, so you only get one SKU.

    Then you add to the SKU itself:

    $params.SKU += $_.SKU
    

    Here’s the part where setting SKU initially to an empty array is hiding your issue. If SKU 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 of SKUs that exist as the value of the single hashtable you’re working against.


    Where to go from here

    1. don’t use arrays for your values in this case
    2. create a new hashtable in each iteration of your loop, then add that new hashtable to the $params array

    Let’s take a look:

    $params = @()
    
    $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 += @{  # new hashtable here
        type = $_.type
        SKU = $_.SKU
        name = $_.name 
        catalog_visibility = $_.'Visibility in catalog'
        categories = $_.Categories
        regular_price = $_.'Regular price'
      }
    }
    

    This is the main problem you have, I left out the in stock part because I’m going to explain that logic separately.


    $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}   
    }
    

    It looks like your CSV has an In stock? column that can be 0 or 1 for false/true.

    First thing I’ll address is that = in PowerShell is always assignment. Testing for equality is -eq, so:

    $params.in_stock = $_.'In stock?'
    if ($params.in_stock -eq 0) {$params.in_stock -replace 0, $false} 
     elseif($params.in_stock -eq 1) {$params.in_stock -replace 1, $true}   
    }
    

    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 you 0 -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:

    $params.in_stock = $_.'In stock?'
    if ($params.in_stock -eq 0) {
        $params.in_stock = $false
    } elseif($params.in_stock -eq 1) {
        $params.in_stock -eq $true
    }
    

    in fact, the elseif isn’t necessary since you can only have 2 values:

    $params.in_stock = $_.'In stock?'
    if ($params.in_stock -eq 0) {
        $params.in_stock = $false
    } else {
        $params.in_stock -eq $true
    }
    

    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.

    0 -as [bool]    # gives false
    "0" -as [bool]  # gives true (whoops)
    "0" -as [int]   # gives the number 0
    "0" -as [int] -as [bool]  # false!
    

    Now, we can do this:

    $params.in_stock = $_.'In stock?' -as [int] -as [bool]
    

    cool! Let’s put it back into the other code:

    $params = @()
    
    $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 += @{  # new hashtable here
        type = $_.type
        SKU = $_.SKU
        name = $_.name 
        catalog_visibility = $_.'Visibility in catalog'
        categories = $_.Categories
        regular_price = $_.'Regular price'
        in_stock = $_.'In stock?' -as [int] -as [bool]
      }
    }
    

    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:

    $params = @()
    
    Import-Csv C:testtest-upload.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 += @{  # new hashtable here
                type = $_.type
                SKU = $_.SKU
                name = $_.name 
                catalog_visibility = $_.'Visibility in catalog'
                categories = $_.Categories
                regular_price = $_.'Regular price'
                in_stock = $_.'In stock?' -as [int] -as [bool]
             }
         }
    

    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:

    $params = @()
    
    Import-Csv C:testtest-upload.csv |
        ForEach-Object { 
            $params += @{  # new hashtable here
                type = $_.type
                SKU = $_.SKU
                name = $_.name 
                catalog_visibility = $_.'Visibility in catalog'
                categories = $_.Categories
                regular_price = $_.'Regular price'
                in_stock = $_.'In stock?' -as [int] -as [bool]
            }
        }
    

    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 = Get-ChildItem $env:HOME  # get all of items in the directory
    

    $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!

    $params = Import-Csv C:testtest-upload.csv |
        ForEach-Object { 
            @{  # new hashtable here
                type = $_.type
                SKU = $_.SKU
                name = $_.name 
                catalog_visibility = $_.'Visibility in catalog'
                categories = $_.Categories
                regular_price = $_.'Regular price'
                in_stock = $_.'In stock?' -as [int] -as [bool]
            } # output is implicit
        }
    

    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).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search