Friday, January 10, 2020

Alternative to negative lookbehinds in regular expressions

I recently faced the problem of requiring negative lookbehinds in a regex engine that does not support them (Golang's Regexp package, RE2, and Hyperscan).

For example, say you want to match the string "def" except if it is preceeded by "abc". In PCRE using negative lookbehinds you could achieve this with the regex:

(?<!abc)def

But this will not work in all regex engines, so I needed an alternative.

I found this Stackoverflow answer by user Sebastian Proske. The approach is clever, but tricky to comprehend and write for long and complex negative lookbehinds. I will try to explain the general approach here, and then present a script to help use this approach for longer and more complex cases.

Continuing the example above, the approach works by having alternatives for each possible prefix that is not "abc". These alternatives are:

  • The beginning of the string, or a character that is not "c", followed by "def"
  • The beginning of the string, or a character that is not "b", followed by "cdef"
  • The beginning of the string, or a character that is not "a", followed by "bcdef"

Written as a regex (line breaks and indentation for readability are not part of the regex):

(
    (^|[^c])def
    |
    (^|[^b])cdef
    |
    (^|[^a])bcdef
)

We can extract the common suffix out:

(
    (^|[^c])
    |
    (^|[^b])c
    |
    (^|[^a])bc
)
def

To support multiple alternative negative lookbehinds, such as both "abc" and "1234", which in PCRE syntax this can be written as "(?<!abc|1234)def", we can write:

(
    (^|[^c4])
    |
    (^|[^b])c
    |
    (^|[^a])bc
    |
    (^|[^3])4
    |
    (^|[^2])34
    |
    (^|[^1])234
)
def

To support character classes in the negative lookbehinds, such as "[bB]", which in PCRE syntax can be written as "(?<!a[bB]c)def", we can write:

(
    (^|[^c])
    |
    (^|[^bB])c
    |
    (^|[^a])[bB]c
)
def

For better equivalence to the negative lookbehinds, we can additionally use non-capturing groups:

(?:
    (?:^|[^c])
    |
    (?:^|[^b])c
    |
    (?:^|[^a])bc
)
def

Here's a quick and dirty Python script to help construct regexes following this approach. The example input here is equivalent to "(?<!a[bB]c|1234)".

The script is also available to conveniently run in your browser on repl.it: https://repl.it/@allanrbo/regexnegativelookbehindalternative1

negativePrefixes = [
"a[bB]c",
"1234",
]

def removeDuplicateChars(s):
  return "".join([c for i,c in enumerate(s) if c not in s[:i]])

def removeChars(s, charsToRemove):
  return "".join([c for i,c in enumerate(s) if c not in charsToRemove])

# Split into arrays of strings. Each string is either a single char, or a char class.
negativePrefixesSplit = []
for np in negativePrefixes:
  npSplit = []
  curCc = ""
  inCc = False
  for c in np:
    if c == "[":
      inCc = True
    elif c == "]":
      npSplit.append(removeDuplicateChars(curCc))
      curCc = ""
      inCc = False
    else:
      if inCc:  
        if c in "-\\":
          raise "Only really simply char classes are currently supported. No ranges or escapes, sorry."
        curCc += c
      else:
        npSplit.append(c)
  negativePrefixesSplit.append(npSplit)

allexprs = []

class Expr():
  pass

suffixLength = 0
while True:
  suffixes = []
  for np in negativePrefixesSplit:
    if suffixLength < len(np):
      suffixes.append(np[len(np)-suffixLength-1:])

  if len(suffixes) == 0:
    break

  exprs = []
  for suffix in suffixes:
    curChar = suffix[0]
    remainder = suffix[1:]
    expr = Expr()
    expr.curChar = curChar
    expr.remainder = remainder
    exprs.append(expr)

  # Is the remainder a subset of any other suffixes remainders?
  for i in range(len(exprs)):
    e1 = exprs[i]
    for j in range(len(exprs)):
      e2 = exprs[j]
      isSubset = True
      for k in range(len(e1.remainder)):
        if not set(e1.remainder[k]).issubset(set(e2.remainder[k])):
          isSubset = False
          break
      if isSubset:
        if e1.curChar == e2.curChar:
          e1.remainder = e2.remainder
          continue

        e1.curChar += e2.curChar
        e1.curChar = removeDuplicateChars(e1.curChar)
        for k in range(len(e1.remainder)):
          if len(set(e2.remainder[k]) - set(e1.remainder[k])) > 0:
            charsInCommon = "".join(set(e2.remainder[k]) & set(e1.remainder[k]))
            e2.remainder[k] = removeChars(e2.remainder[k], charsInCommon)

  # Remove duplicate expressions
  exprsFiltered = []
  for i in range(len(exprs)):
    e1 = exprs[i]
    alreadyExists = False
    for j in range(len(exprs)):
      if i == j:
        break

      e2 = exprs[j]

      sameC = set(e1.curChar) == set(e2.curChar)
      sameR = True
      for k in range(len(e1.remainder)):
        if set(e1.remainder[k]) != set(e2.remainder[k]):
          sameR = False
          break
      if sameC and sameR:
        alreadyExists = True
        break

    if not alreadyExists:
      exprsFiltered.append(e1)
  
  allexprs.extend(exprsFiltered)

  suffixLength += 1
  continue

out = "(?:\n"
for i in range(len(allexprs)):
  e = allexprs[i]
  out += ("(?:^|[^" + e.curChar + "])")
  for c in e.remainder:
    if len(c) > 1:
      out += "[" + c + "]"
    else:
      out += c
  if i != len(allexprs)-1:
    out += "|"
  out += "\n"
out += ")"

print("Human readable:")
print(out)
print()
print("Single line:")
print(out.replace("\n",""))
Example output:
Human readable:
(?:
(?:^|[^c4])|
(?:^|[^bB])c|
(?:^|[^3])4|
(?:^|[^a])[bB]c|
(?:^|[^2])34|
(?:^|[^1])234
)

Single line:
(?:(?:^|[^c4])|(?:^|[^bB])c|(?:^|[^3])4|(?:^|[^a])[bB]c|(?:^|[^2])34|(?:^|[^1])234)

Wednesday, June 20, 2018

Foreflight in Denmark

I enjoy using the Foreflight app when flying in the US, and recently discovered that it can work very well in Europe as well. They have a guide on how to import custom charts, and it turned out to be very easy. PDFs of aviation charts for Denmark can be downloaded from the AIM and converted via the tool described in Foreflights guide.

Here are a few charts current as of June 2018. Denmark, Copenhagen Area, Roskilde Visual Approach. Download these from Safari on your iPad, and it should prompt you to import them in Foreflight.

Tuesday, May 01, 2018

Remote Ubuntu dev setup with multi screen VNC

Goal: A VM in the cloud or similar running an Ubuntu desktop with your dev tools (Sublime Text, Visual Studio Code, etc.), that you can remotely connect to and comfortably work on, spanning all your local monitors. Like this:

We'll use TigerVNC. RealVNC 4 that comes with Ubuntu 16.04 can also be used, but it lacks the X extensions that are required for a well behaved multi monitor setup. Also, Visual Studio Code can't run on RealVNC 4 without a special patched version of libxcb.so.1.

Ubuntu 18.04

Desktop edition

sudo apt update
sudo apt install --no-install-recommends openssh-server tigervnc-standalone-server tigervnc-common

sudo su
cat /etc/X11/Xvnc-session
mv /etc/X11/Xvnc-session /etc/X11/Xvnc-session.orig
cat > /etc/X11/Xvnc-session << "EOF"
#!/bin/sh
export GNOME_SHELL_SESSION_MODE=ubuntu
export XDG_CURRENT_DESKTOP=ubuntu:GNOME
vncconfig -nowin &
exec /etc/X11/Xsession
vncserver -kill $DISPLAY
EOF
chmod +x /etc/X11/Xvnc-session
exit

# Start the VNC session (this must be run as your user from an SSH session after each reboot)
vncserver

# To stop the VNC session
vncserver -kill

Server edition

sudo apt-get update
sudo apt install --no-install-recommends tigervnc-standalone-server tigervnc-common ubuntu-desktop

And then followed by the same procedure with /etc/X11/Xvnc-session and vncserver as for desktop.



For additional desktop software, I recommend using the Ubuntu Software Center, which will install Snap packages for popular desktop software.
# Install the Ubuntu Software Center
sudo apt install gnome-software


Ubuntu 16.04

I only tried with server edition on 16.04, so adapt as needed.
sudo apt-get update

sudo apt-get install --no-install-recommends ubuntu-desktop gnome-terminal unity-lens-applications unity-lens-files gnome-settings-daemon
sudo apt-get install --no-install-recommends software-center

wget https://bintray.com/tigervnc/stable/download_file?file_path=ubuntu-16.04LTS%2Famd64%2Ftigervncserver_1.8.0-1ubuntu1_amd64.deb -O tigervncserver_1.8.0-1ubuntu1_amd64.deb
sudo dpkg -i tigervncserver_1.8.0-1ubuntu1_amd64.deb
sudo apt-get install -f

mkdir $HOME/.vnc/
cat > $HOME/.vnc/xstartup <<"EOF"
#!/bin/sh
unset SESSION_MANAGER
unset DBUS_SESSION_BUS_ADDRESS
/etc/X11/xinit/xinitrc &
vncconfig -nowin &
/usr/lib/x86_64-linux-gnu/unity/unity-panel-service &
/usr/lib/x86_64-linux-gnu/indicator-datetime/indicator-datetime-service &
/usr/lib/x86_64-linux-gnu/indicator-keyboard/indicator-keyboard-service &
unity &
EOF
chmod +x $HOME/.vnc/xstartup

# some kde or qt apps seem to look ugly without this package
sudo apt-get install kde-style-breeze-qt4

# Start the VNC session (this must be run as your user from an SSH session after each reboot)
vncserver

# To stop the VNC session
vncserver -kill :1
For Additional desktop software, I recommend using Snap:
sudo apt install snap

# For example install Visual Studio Code, Chrome, and Sublime, like this
sudo snap install vscode --classic
sudo snap install chromium
sudo snap install sublime-text --classic

Connecting

Use the TigerVNC client to take advantage of the dynamic resizing and multi-screen features. Binaries for Win/Mac/Linux can be found on their website.

Open the TigerVNC menu by pressing F8, and note the shortcut keys here for full screen, minimize, etc. Copy the remote .vnc/passwd file locally and pass it to the TigerVNC client with the -PasswordFile= option to avoid having to type your password repeatedly. For example, I connect from my Windows box using a cmd file with this content:
start "" "C:\utils\vnc\vncviewer64-1.8.0.exe" "yourRemoteServer:5901" -PasswordFile=C:\utils\vnc\passwd


Secure connection

If you are connecting over the internet, you'll want to use an SSH tunnel to secure the unencrypted VNC. In that case you can start the VNC server in a mode where it only listens on localhost, and then use SSH port forwarding.
vncserver  -localhost=yes -nolisten tcp
Connect using your preferred SSH client, and set up port forwarding to localhost:5901, for example from your local port 5901.

Wednesday, November 22, 2017

Delete the undeletable on Windows

I had trouble deleting C:\ProgramData\Docker on Docker for Windows using Windows containers. This is because it contains a full Windows directory layout, with all system files and special permissions and flags. Here's a Powershell snippet to get a file hierarchy on Windows into a state where it can be deleted.

$ErrorActionPreference = "stop"

function reallyDelete($d) {
    function renameToNumbersRecursive($dir) {
        $i = 0
        foreach($f in (dir $dir)) {
            while(Test-Path ($dir + "\" + $i)) {
                $i++
            }
            try {
                Rename-Item $f.FullName ($dir + "\" + $i)
            } catch {
                takeown /f $dir | Out-Null
                icacls $dir /reset | Out-Null
                takeown /f $f.FullName | Out-Null
                icacls $f.FullName /reset | Out-Null
                Rename-Item $f.FullName ($dir + "\" + $i)
            }
        }

        foreach($f in (dir $dir)) {
            if($f -is [System.IO.DirectoryInfo]) {
                renameToNumbersRecursive($f.FullName)
            }
        }
    }

    # paths might become too long for icacls, so rename paths to short numbers
    renameToNumbersRecursive $d

    takeown /f $d /r   | Out-Null
    icacls $d /reset /T   | Out-Null

    attrib -s -h -r "$d\*.*" /s /d   | Out-Null

    cmd /c del /s /q $d   | Out-Null
    cmd /c rmdir /s /q $d   | Out-Null
}

reallyDelete "C:\ProgramData\Docker"

Tuesday, May 30, 2017

Utility to remind you to keep a task diary

Here's a program that asks you every hour what you are doing, so you end up with a diary at the end of the day. It saves your input to a text file WhatAmIDoing.txt in the same folder you put the exe.

The "ask every hour" button creates a Windows Scheduled Task. Note that if you move the location of the exe file, you will need to click the "ask every hour" button again.

https://acoby.com/utils/WhatAmIDoing.exe


Thursday, September 29, 2016

Mailing list manager

I needed a simple mailing list manager for a sports club I'm in. We wanted it to be of the type where an administrator manages the lists (not the type where any random person can subscribe him- or herself). Couldn't find any decent looking free service offering this, so I decided to make one: mailgroup.io .

These are its features:
  • Free for noncommercial use.
  • Your own custom domain.
  • Or @mailgroup.io as domain if you prefer.
  • Your choice of members-only mailing lists, or everyone-may-write.
  • Detailed Postfix delivery reports for each mail for each recipient.
  • Max 50 recipients per account (negotiable).
  • Mail triggers. These are special email addresses that trigger HTTP POSTs with the email content to a defined URL when they receive email. Useful if you want some logic on your website to be triggered by email.


Wednesday, September 14, 2016

GLaDOS-like sound pack for Taranis

Here's a soundpack for the FrSky Taranis, based on the original, but run through Melodyne so it sounds like GLaDOS from Portal: https://acoby.com/fpv/gladosStyleTaranisSounds.zip