Getting PagerDuty's Latest vCard with curl and awk

This is a really low tech / low effort solution to solve a specific problem, you can apply this for whatever 1 off use cases you have.
Prefer video? Here it is on YouTube.
This post is focused on a specific use case, but the same strategy and ideas can be applied to scraping other sites.
At a company I do work for, we use PagerDuty to get notified of critical downtime events. It’s important to get notified on my phone if something happens, especially outside of business hours.
I didn’t want to install PagerDuty’s native mobile app, instead I prefer getting called through my phone number. It’s 1 less app draining my battery. Also it avoids needing to sign into work’s SSO on my personal mobile device to access PagerDuty.
With that said, that means needing to get updated when PagerDuty updates their set of phone numbers that they could call you from. Android and iOS have a contact app which supports vCard. It’s a format that stores a bunch of information about a contact. In this case with PagerDuty, it contains a full list of their phone numbers.
The win there is it’s only 1 single contact, there’s not separate entries for each number. Now you can star or favorite this contact so it goes through DnD mode which is important if something bad happens during the middle of the night (I put my phone in DnD then),
PagerDuty lets you download their vCard from https://support.pagerduty.com/main/docs/notification-phone-numbers and even tells you the date it was updated.
I put this into an alias called pdvcard (PagerDuty vCard) which I run every Friday:
alias pdvcard="curl -sL https://support.pagerduty.com/main/docs/notification-phone-numbers \
| awk 'match(\$0, /<li[^>]*>[[:space:]]*([0-9]{4}-[0-9]{2}-[0-9]{2})/, a) { print a[1]; exit }'"
It returns the date when it was last updated:
$ pdvcard
2026-07-28
It works on Linux and macOS as long as you’re using a version of macOS from
2017+ due to the availability of certain awk features.
The idea is, if I see a new date than last time I download the vCard on my phone and import it. It only takes a few seconds when it changes and thankfully it doesn’t change often, that’s why I’m ok with this low tech solution.
I’m also ok with running the above only once a week because downtime events are super rare to begin with. The odds of downtime happening + PagerDuty updating their list of numbers + PagerDuty calling out from one of these new numbers is extremely low.
In case it’s not clear, not having the new number is a problem because it won’t be a part of your PagerDuty vCard contact that’s white listed to go through DnD so if it rings in the middle of the night you might not see it.
# How the Alias Works
First, curl is used to get the HTML response from PagerDuty’s site, this is
the string we’re going to search through. Specifically, we want to match
<li>YYYY-MM-DD since this contains the date of when the vCard was last
updated.
match is a function that searches a string for a regex match(string, regex, array), it returns the position where the match starts (or 0 if none is
found).
In this case, we’re doing match($0, /.../, a) which searches the current line
($0) for the regex. Any captured groups from a match are stored in the array
named a. Awk runs this internally for each line of input and as a reminder
$0 contains the current line’s string.
As for the regex (everything within /.../):
<li[^>]*>ultimately boils down to matching an<li>tag that could have optional attributes like<li class="hello">, on PagerDuty’s site there’s no attributes but I added this to future proof the script a little bit in case that changes[[:space:]]*handles zero or more whitespace characters which comes down to handling optional whitespace, again this is future proofing in case PagerDuty ever introduces a leading space before the date([0-9]{4}-[0-9]{2}-[0-9]{2})is the capture group (the(...)part) which looks for a YYYY-MM-DD format of digits which is the date we want
After that, we have an array (a) which has the captured groups. We only have
1 capture group so a[0] is the entire match and a[1] is the first capture.
If we printed a[0] it would return <li>2026-07-28 where as a[1] gives us
just the first captured group’s match which is 2026-07-28. If we had a 2nd
capture group (another set of (...)), it would be in a[2], and so on.
With that said, { print a[1]; exit } will run when match() returns a
non-zero value and we print the first capture as described above. We exit to
quit awk to avoid scanning anymore of the output since we only want the first
match since there’s only one <li> with the date in the output.
If the page you’re checking has multiple entries and you care about let’s say
the last one, you wouldn’t want to exit here. Instead you could let it run to
the end and adjust the awk script to output the last match, I’ll leave that
one up to you!
# Ideas to Make It Robust
The above is a quick win. I run it once a week, it takes less than 5 seconds of time. PagerDuty also doesn’t add numbers that often so most weeks require doing nothing.
It doesn’t depend on long running services, scheduled tasks or databases.
But it does require doing manual tasks, one downside is it’s quite dependent on scraping some company’s site. If they change their HTML markup then you need to adjust the regex to match. It would be easy to see that happen when I run it and it fails, but still, that’s something to think about.
Also, if you had multiple people depending on keeping this up to date then they each need to do the manual task which isn’t great.
How I would make this better in a script that runs on a daily cron job on a server:
- Download the vCard file from PagerDuty’s site to a temp directory
- Such as
/tmp/pagerduty-vcard-file.vcf
- Such as
- Get the checksum of the file
- Such as
md5sum /tmp/pagerduty-vcard-file.vcf | cut -d " " -f "1" - The intent is saving it to
~/.local/state/pagerduty/latest-vcard-checksum
- Such as
- If the above state file doesn’t exist, write the file out
- If the above state file does exist:
- Is it the same as step 2’s checksum?
- Exit and do nothing because nothing changed
- Is it different than step 2’s checksum?
- Write out a new state file so it gets updated
- Send a Slack notification / email / etc. that it’s been updated
- Is it the same as step 2’s checksum?
The above is nice because it doesn’t depend on scraping anything, it also allows multiple people to get notified if something changes and doesn’t require anyone to run manual tasks to check it every week.
The video below covers the low tech alias based solution.
# Demo Video
Timestamps
- 1:30 – Running the alias
- 2:09 – Breaking down the alias
- 3:14 – Awk’s match function
- 4:10 – Playing around with the awk command
- 4:38 – Breaking down the regex
- 5:54 – Exiting early and multiple capture groups
- 6:36 – Downsides to this approach
- 7:09 – A more robust approach
Have you done low effort scraping before? Let me know below.