I was cleaning up and organizing bookmarks in Google Chrome today and decided to find out where they were stored on my computer. I found the Bookmarks file in a local app data folder. Opening it up in Notepad I was pleasantly surprised to discover it is in JSON. Excellent! This gives me an opportunity to try out some of the new web cmdlets in PowerShell v3 and build a little tool to help find broken links.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
The first step is to convert the file from JSON into PowerShell objects.
$File = "$env:localappdata\Google\Chrome\User Data\Default\Bookmarks" $data = Get-content $file | out-string | ConvertFrom-Json
To convert from JSON, the input needs to be one long string. Get-Content writes an array of strings so by piping to Out-String first, ConvertFrom-JSON is happy. Here's what I end up with.
$data
checksum roots version
-------- ----- -------
03a5a00f42bb4860f6f8dd4d543e34af @{bookmark_bar=; other=; synced=} 1
The roots property is where things are stored.
$data.roots | format-list
bookmark_bar : @{children=System.Object[]; date_added=12989558548133917; date_modified=12998163702118866; id=1;
name=Bookmarks bar; type=folder}
other : @{children=System.Object[]; date_added=12989558548133917; date_modified=12998151911083334; id=2;
name=Other bookmarks; type=folder}
synced : @{children=System.Object[]; date_added=12989558548133917; date_modified=0; id=3; name=Mobile bookmarks;
type=folder}
As far as I know these are hard-coded properties. Each property can have a nested property called children which will be a collection of bookmarks and subfolders.
$data.roots.bookmark_bar
children : {@{date_added=12993566428951635; id=67; name=PowerShell.org • View unanswered posts; type=url;
url=http://powershell.org/discuss/search.php?search_id=unanswered}, @{date_added=12989558569500214;
id=5; name=HostGator.com Control Panel; type=url;
url=http://gator1172.hostgator.com:2082/frontend/x3/index.php?post_login=91229198020615},
@{date_added=12989558569506214; id=7; name=Vet Followers; type=url;
url=https://www.socialoomph.com/vetfollowers}, @{date_added=12989558569508214; id=8; name=Google+;
type=url;
url=https://plus.google.com/up/start/?continue=https://plus.google.com/&type=st&gpcaz=3cba226a}...}
date_added : 12989558548133917
date_modified : 12998163702118866
id : 1
name : Bookmarks bar
type : folder
Here's what an child item looks like:
date_added : 12998151910630334
id : 76
name : Facebook
type : url
url : http://www.facebook.com/
Awesome. All I need to do is get the url.
$data.roots.bookmark_bar.children | select Name,url
name url
---- ---
PowerShell.org • View unanswered posts http://powershell.org/discuss/search.php?search_id=unans...
HostGator.com Control Panel http://gator1172.hostgator.com:2082/frontend/x3/index.ph...
Vet Followers https://www.socialoomph.com/vetfollowers
Google+ https://plus.google.com/up/start/?continue=https://plus....
Bottlenose http://bottlenose.com/home#streams/everything
Power Tweet javascript:(function(){url="http://www.twylah.com/bookma...
Facebook http://www.facebook.com/
Blog Dashboard https://jdhitsolutions.com/blog/wp-admin/index.php
To validate if the URL is good, I can use Invoke-Webrequest.
invoke-webrequest -Uri http://www.facebook.com -UseBasicParsing | Select StatusCode
StatusCode
----------
200
All I want is the status code so I'm using basic parsing to speed things up. Now that I have the basics, I can turn this into a script.
#requires -version 3.0
#comment based help is here
[cmdletbinding()]
Param (
[Parameter(Position=0)]
[ValidateScript({Test-Path $_})]
[string]$File = "$env:localappdata\Google\Chrome\User Data\Default\Bookmarks",
[switch]$Validate
)
Write-Verbose -Message "Starting $($MyInvocation.Mycommand)"
#A nested function to enumerate bookmark folders
Function Get-BookmarkFolder {
[cmdletbinding()]
Param(
[Parameter(Position=0,ValueFromPipeline=$True)]
$Node
)
Process {
foreach ($child in $node.children) {
#get parent folder name
$parent = $node.Name
if ($child.type -eq 'Folder') {
Write-Verbose "Processing $($child.Name)"
Get-BookmarkFolder $child
}
else {
$hash= [ordered]@{
Folder = $parent
Name = $child.name
URL = $child.url
Added = [datetime]::FromFileTime(([double]$child.Date_Added)*10)
Valid = $Null
Status = $Null
}
If ($Validate) {
Write-Verbose "Validating $($child.url)"
if ($child.url -match "^http") {
#only test if url starts with http or https
Try {
$r = Invoke-WebRequest -Uri $child.url -DisableKeepAlive -UseBasicParsing
if ($r.statuscode -eq 200) {
$hash.Valid = $True
} #if statuscode
else {
$hash.valid = $False
}
$hash.status = $r.statuscode
Remove-Variable -Name r -Force
}
Catch {
Write-Warning "Could not validate $($child.url)"
$hash.valid = $False
$hash.status = $Null
}
} #if url
} #if validate
#write custom object
New-Object -TypeName PSobject -Property $hash
} #else url
} #foreach
} #process
} #end function
#convert Google Chrome Bookmark filefrom JSON
$data = Get-Content $file | Out-String | ConvertFrom-Json
#these should be the top level "folders"
$data.roots.bookmark_bar | Get-BookmarkFolder
$data.roots.other | Get-BookmarkFolder
$data.roots.synced | Get-BookmarkFolder
Write-Verbose -Message "Ending $($MyInvocation.Mycommand)"
This script converts the bookmarks file from JSON and creates custom objects for each bookmark using the nested Get-BookMarkFolder function. This processes each child if it is a url. If it is a folder then it passes the folder name recursively to Get-BookmarkFolder. Because validation might be time consuming, I made it optional with -Validate. I also converted the date_added property into a user-friendly datetime format. The value in the file is a file time, i.e. number of ticks since 1/1/1601. Although the actual value needs to be multiplied by 10 to get the correct date time.
When I run the script, without validation I get an object like this for each bookmark.
Folder : Misc
Name : [Release][Alpha0.6] CyanogenMod 9 Touchpad - RootzWiki
URL : http://rootzwiki.com/topic/15509-releasealpha06-cyanogenmod-9-touchpad/
Added : 8/15/2012 10:42:49 PM
Valid :
Status :
If I validate it will look like this:
Folder : Misc
Name : [Release][Alpha0.6] CyanogenMod 9 Touchpad - RootzWiki
URL : http://rootzwiki.com/topic/15509-releasealpha06-cyanogenmod-9-touchpad/
Added : 8/15/2012 10:42:49 PM
Valid : True
status : 200
Bookmarks that fail will show as False. I haven't gotten around to figuring out how to rewrite the bookmarks file, but I would probably end up using ConvertTo-JSON.
The web cmdlets in PowerShell v3 can be a lot of fun to work with. But one word of caution: do NOT try to turn the script in the ISE using -validate. It is possible that Invoke-WebRequest will begin consuming all memory on your computer, unless you make sure the ISE process is completely killed.
I hope you'll download Get-ChromeBookmark and let me know what you think.

Neat Stuff
Simply Awesome
The date_added is actually in nanoseconds. A tick is 10 nanoseconds, thats why you had to multiply by ten.
Thanks for the clarification.