Skip to content

Instantly share code, notes, and snippets.

@nickanderson
Last active July 9, 2025 04:22
Show Gist options
  • Save nickanderson/1ffd7936b66b4a7c1e703fbbdc49e0d1 to your computer and use it in GitHub Desktop.
Save nickanderson/1ffd7936b66b4a7c1e703fbbdc49e0d1 to your computer and use it in GitHub Desktop.
Search and replace for Xresources with CFEngine 3

CFEngine manage Xscreensaver mode

This example implements search and replace in a file (specifically an Xresources formatted file).

bundle agent __main__
{
  methods:
      "Test data"
        usebundle => init_test_file( $(example.file) );
       "example";
}
bundle agent example
{
  vars:
      "file" string => "/tmp/t.conf";
      "a" data => '{
                       "*font": "DejaVu Sans Mono-11-REPLACED",
                       "*background": "#2e3436"
      }';

  files:
      "$(file)"
        edit_line => Xresources_replace_resource_value("*mode", "blank");

      "$(file)"
        edit_line => Xresources_replace_map( "@(a)" );
}
bundle agent init_test_file(file)
{
  files:
      "$(file)"
        content => "*mode:$(const.t)$(const.t)$(const.t)random
*font:$(const.t)$(const.t)$(const.t)DejaVu Sans Mono-11
*background:$(const.t)$(const.t)$(const.t)#2e3436";

}
bundle edit_line Xresources_replace_resource_value( resource, value )
{
  vars:
      "e_resource"
        string => escape( $(resource) ),
        comment => "Xresources resources may contain asterisks for wildcard matching it must be escaped.";

  replace_patterns:
      # I think this is a bug. $(match.1) doesn't seem to faithfully reproduce the text from the first capture group.
      # In the test file the 3 tabs in the first capture group matched by \s+ are replaced by a single tab.
      "(^$(e_resource):\s+)((?!$(value)$).*$)"
        replace_with => value( "$(match.1)$(value)" );

      # Works if there are 3 tabs
      #"(^$(e_resource):\t\t\t)((?!$(value)$).*$)"
      #  replace_with => value( "$(match.1)$(value)" );
}
bundle edit_line Xresources_replace_map( map )
{
  vars:
      "resources" slist => getindices( map );

  replace_patterns:
      # I think this is a bug. $(match.1) doesn't seem to faithfully reproduce the text from the first capture group.
      # In the test file the 3 tabs in the first capture group matched by \s+ are replaced by a single tab.
      "(^$(with):\s+)((?!$(map[$(resources)])$).*$)"
       with => escape( $(resources) ),
       replace_with => value( "$(match.1)$(map[$(resources)])" );

      #"(^$(with):\s+)((?!$(map[$(resources)])$).*$)"
      #  with => escape( $(resources) ),
      #  replace_with => value( "$(match.1)$(map[$(resources)])" );


      # In the test file the 3 tabs in the first capture group matched by \t\t\t are retained
      # While this faithfully reproduces the whitespace, it would not be robust to any deviation in the ammount of whitespace
      # But it works correctly if there are 3 tabs
       # "(^$(with):\t\t\t)((?!$(map[$(resources)])$).*$)"
       #   with => escape( $(resources) ),
       #   replace_with => value( "$(match.1)$(map[$(resources)])" );
}

Here is the state of the /tmp/t.conf file in the above policy before and after running the policy.

cat -T /tmp/t.conf 
*mode:^I^I^Irandom
*font:^I^I^IDejaVu Sans Mono-11
*background:^I^I^I#2e3436

After:

cat -T /tmp/t.conf 
*mode:^Iblank
*font:^IDejaVu Sans Mono-11-REPLACED
*background:^I#2e3436

Note the current implementation resulted in the original three tabs being replaced with a single, which I think is a bug. Hopefully I or somoene else remembers to log a ticket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment