Skip to main content
A Keyboard Shortcut to Clean Up Phone Numbers, IBANs, etc. Before Pasting

A Keyboard Shortcut to Clean Up Phone Numbers, IBANs, etc. Before Pasting

·2 mins

Do you know those situations where you copy something to the clipboard—such as a phone number, a credit card number, or an IBAN—to paste it into a web form, but the form is annoyingly strict and won’t allow spaces, dashes, or parentheses?

To solve this, I came up with a little Hammerspoon module that binds to ⌃⌘V and strips everything except ASCII letters, digits, and plus signs.

Examples:

  • +49 30 23125-123 → +493023125123
  • (030) 23125-123 → 03023125123
  • GB33 BUKB 2020 1555 5555 55 → GB33BUKB20201555555555

Bonus features:

  • It uses hs.eventtap.keyStrokes(), so it doesn’t perform an actual paste but instead simulates typing the string. This helps with form fields that, for whatever reason, prevent you from pasting into them.
  • If you accidentally apply it to a canonical UUID or a string containing an @ sign—typically an email address—it preserves the punctuation and only trims surrounding whitespace.
local module = {}

-- Compact identifiers commonly consist only of ASCII letters, digits, and +.
local function sanitizeIdentifier(text)
  return text:gsub("[^A-Za-z0-9+]", "")
end

local function trim(text)
  return text:match("^%s*(.-)%s*$")
end

local function shouldPreserveFormatting(text)
  -- Punctuation is meaningful in email addresses and canonical UUIDs.
  if text:find("@", 1, true) then
    return true
  end

  return text:match("^%x%x%x%x%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%-%x%x%x%x%x%x%x%x%x%x%x%x$") ~= nil
end

local function pasteIdentifier()
  local text = hs.pasteboard.getContents()
  if not text then
    hs.alert.show("Clipboard does not contain text")
    return
  end

  local trimmedText = trim(text)
  local value = shouldPreserveFormatting(trimmedText) and trimmedText or sanitizeIdentifier(text)

  module.pasteTimer = hs.timer.doAfter(0.05, function()
    module.pasteTimer = nil

    -- Let the shortcut's physical Ctrl and Cmd keys come up first. Typing the
    -- cleaned value directly avoids swapping and synchronizing the clipboard.
    hs.eventtap.keyStrokes(value)
  end)
end

function module.start()
  module.hotkeys = {
    hs.hotkey.bind({ "ctrl", "cmd" }, "v", pasteIdentifier),
  }
end

return module

Save it as identifier_paste.lua and include in your init.lua like so:

require("modules.identifier_paste").start()