5

Task

Consider the following: One needs to store a over time varying amount of pgfkeys entries with a nested data structure. All the entries have the same structure, but the content may vary. So one defines a macro to handle the pgfkeys structure initialization automatically when the user sets the data.

Artificial Example: Inventory (MWE)

\documentclass[parskip=full]{scrreprt}

\usepackage{pgffor}
\usepackage{pgfkeys}

% DESCRIPTION: Set up inventory entries manually
\pgfkeys{
    /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
        \noexpand\pgfqkeys{\pgfkeyscurrentpath}{##1}%
    },%
    /inventory/.cd,
        Manual 5 Speed/.is setter,
            Manual 5 Speed/inventory id/.initial = XXXXXXXXXX,
            Manual 5 Speed/product/.is setter,%
                Manual 5 Speed/product/manufacturer/.initial = EMPTY,
                Manual 5 Speed/product/product id/.initial = EMPTY,
        Manual 6 Speed/.is setter,
            Manual 6 Speed/inventory id/.initial = YYYYYYYYYY,
            Manual 6 Speed/product/.is setter,%
                Manual 6 Speed/product/manufacturer/.initial = EMPTY,
                Manual 6 Speed/product/product id/.initial = EMPTY
}

% DESCRIPTION: Logic for creating pgfkey "database" entry automatically and setting them to user specified data
% ARGUMENTS: #1 = entry product, #2 = entry data
\newcommand{\generateEntry}[2]{
    % DESCRIPTION: Creating the pgfkeys "family" and intialize with default values
    \pgfkeys{
        /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
            \noexpand\pgfqkeys{\pgfkeyscurrentpath}{##1}%
        },%
        /inventory/#1/.cd,
        inventory id/.initial = ZZZZZZZZZZ,
        product/.is setter,%
            product/manufacturer/.initial = EMPTY,
            product/product id/.initial = EMPTY
    }
    
    % DESCRIPTION: Setting the data to the one specified by the user
    \pgfqkeys{/inventory/#1}{#2}
}

% DESCRIPTION: Macro for testing whether it is a problem with wrapping \pgfqkeys in an additional macro
\newcommand{\setEntryData}[2]{
    \pgfqkeys{/inventory/#1}{#2}
}   

\begin{document}
    % DESCRIPTION: Setting the data of the manually added entry
    % with \pgfqkeys and "{}" notation
    % STATUS: WORKS AS EXPECTED
    \pgfqkeys{/inventory/Manual 5 Speed}{
        inventory id = 6,
        product = {
            manufacturer = Herbert Motors,
            product id = 433M5
        }
    }
    
    % DESCRIPTION: Setting the data of the manually added entry
    % with \pgfqkeys wrapped in macro and "{}" notation
    % STATUS: WORKS  AS EXPECTED
    \setEntryData{Manual 6 Speed}{
        inventory id = 11,
        product = {
            manufacturer = Herbert Motors,
            product id = 433M6
        }
    }
    
    % DESCRIPTION: Generate and set data for automatic entry
    % with "/" notation
    % STATUS: WORKS  AS EXPECTED
    \generateEntry{Automatic 4 Speed}{
        inventory id = 21,
        product/manufacturer = Jane's Speedshop,
        product/product id = JS4A
    }
    
    % DESCRIPTION: Generate and set data for automatic entry
    % with "{}" notation
    % STATUS: FAILS FOR NESTED KEYS
    \generateEntry{Automatic 5 Speed}{
        inventory id = 42,
        product = {
            manufacturer = Jane's Speedshop,
            product id = JS5A
        }
    }
    
    % DESCRIPTION: Visualize the data stored in the pgfkeys
    \section*{Inventory}
        \foreach \entry in {Manual 5 Speed, Manual 6 Speed, Automatic 4 Speed, Automatic 5 Speed}{
            \textbf{\entry}:\\
            inventory id: \pgfkeysvalueof{/inventory/\entry/inventory id}\\
            manufacturer: \pgfkeysvalueof{/inventory/\entry/product/manufacturer}\\
            product id:  \pgfkeysvalueof{/inventory/\entry/product/product id} \\[0.25cm]
        }
\end{document}

This produces the following output:
Output of MWE

Issue

When using the {...} notation to set the data of automatically generated entries, the data is not stored (see Automatic 5 Speed in MWE). The / notation seems to work.

What was tried so far

The problem seems to be related to the .is setter pgfkeys handler. It seems like the nested structure is not created, but a single key with e. g. product/manufacturer as id (with the / being interpreted as a letter, not a separator).
So far, the following has been tried:

  • Changing .code to .ecode
  • Modifying the expansion from \noexpand to other possibilities

Those experiments always resulted in errors and the compilation failing.

Question

Is there a simple way to achieve the desired behavior with pgfkeys (e.g. via .is family)?
Suggestions of other methods are also welcome. However, solutions with pgfkeys would be preferred, since the rest of the data handling in the real project is already based upon pgfkeys.

1 Answer 1

3

Your issue is hash doubling.

You're using the following code:

\newcommand{\generateEntry}[2]{
    % DESCRIPTION: Creating the pgfkeys "family" and intialize with default values
    \pgfkeys{
        /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
            \noexpand\pgfqkeysalso{\pgfkeyscurrentpath}{##1}%
        },%
     % ...
     }
     % ...
}

So you're using a \newcommand (first level definition), and inside that a .code handler (second level definition) and inside that a \pgfkeysedef (third level definition). But inside the replacement text specified for \pgfkeysedef you're using ##1, which is the second level parameter, so the argument of the .is setter handler, which is empty in all your usages (since you use product/.is setter,). You need to turn that one to the parameter meant to be picked up by \pgfkeysedef, hence you need to re-double the hashes:

\newcommand{\generateEntry}[2]{
    % DESCRIPTION: Creating the pgfkeys "family" and intialize with default values
    \pgfkeys{
        /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
            \noexpand\pgfqkeysalso{\pgfkeyscurrentpath}{####1}%
        },%
     % ...
     }
     % ...
}

This one will work.

The one with the manual setup worked because it wasn't inside \newcommand, hence you didn't need to double hashes twice and your code was correct.

Full corrected MWE (I also removed a few of your spurious spaces):

\documentclass[parskip=full]{scrreprt}

\usepackage{pgffor}
\usepackage{pgfkeys}

% DESCRIPTION: Set up inventory entries manually
\pgfkeys{
    /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
        \noexpand\pgfqkeys{\pgfkeyscurrentpath}{##1}%
    },%
    /inventory/.cd,
        Manual 5 Speed/.is setter,
            Manual 5 Speed/inventory id/.initial = XXXXXXXXXX,
            Manual 5 Speed/product/.is setter,%
                Manual 5 Speed/product/manufacturer/.initial = EMPTY,
                Manual 5 Speed/product/product id/.initial = EMPTY,
        Manual 6 Speed/.is setter,
            Manual 6 Speed/inventory id/.initial = YYYYYYYYYY,
            Manual 6 Speed/product/.is setter,%
                Manual 6 Speed/product/manufacturer/.initial = EMPTY,
                Manual 6 Speed/product/product id/.initial = EMPTY
}

% DESCRIPTION: Logic for creating pgfkey "database" entry automatically and setting them to user specified data
% ARGUMENTS: #1 = entry product, #2 = entry data
\newcommand{\generateEntry}[2]{%
    % DESCRIPTION: Creating the pgfkeys "family" and intialize with default values
    \pgfkeys{%
        /handlers/.is setter/.code=\pgfkeysedef{\pgfkeyscurrentpath}{%
            \noexpand\pgfqkeysalso{\pgfkeyscurrentpath}{####1}%
        },%
        /inventory/#1/.cd,
        inventory id/.initial = ZZZZZZZZZZ,
        product/.is setter,
        product/manufacturer/.initial = EMPTY,
        product/product id/.initial = EMPTY
    }%
    %
    % DESCRIPTION: Setting the data to the one specified by the user
    \pgfqkeys{/inventory/#1}{#2}%
}

% DESCRIPTION: Macro for testing whether it is a problem with wrapping \pgfqkeys in an additional macro
\newcommand{\setEntryData}[2]{%
    \pgfqkeys{/inventory/#1}{#2}%
}

\begin{document}
    % DESCRIPTION: Setting the data of the manually added entry
    % with \pgfqkeys and "{}" notation
    % STATUS: WORKS AS EXPECTED
    \pgfqkeys{/inventory/Manual 5 Speed}{
        inventory id = 6,
        product = {
            manufacturer = Herbert Motors,
            product id = 433M5
        }
    }

    % DESCRIPTION: Setting the data of the manually added entry
    % with \pgfqkeys wrapped in macro and "{}" notation
    % STATUS: WORKS  AS EXPECTED
    \setEntryData{Manual 6 Speed}{
        inventory id = 11,
        product = {
            manufacturer = Herbert Motors,
            product id = 433M6
        }
    }

    % DESCRIPTION: Generate and set data for automatic entry
    % with "/" notation
    % STATUS: WORKS  AS EXPECTED
    \generateEntry{Automatic 4 Speed}{
        inventory id = 21,
        product/manufacturer = Jane's Speedshop,
        product/product id = JS4A
    }

    % DESCRIPTION: Generate and set data for automatic entry
    % with "{}" notation
    % STATUS: FAILS FOR NESTED KEYS
    \generateEntry{Automatic 5 Speed}{
        inventory id = 42,
        product = {
            manufacturer = Jane's Speedshop,
            product id = JS5A
        }
    }

    % DESCRIPTION: Visualize the data stored in the pgfkeys
    \section*{Inventory}
        \foreach \entry in {Manual 5 Speed, Manual 6 Speed, Automatic 4 Speed, Automatic 5 Speed}{
            \textbf{\entry}:\\
            inventory id: \pgfkeysvalueof{/inventory/\entry/inventory id}\\
            manufacturer: \pgfkeysvalueof{/inventory/\entry/product/manufacturer}\\
            product id:  \pgfkeysvalueof{/inventory/\entry/product/product id} \\[0.25cm]
        }
\end{document}

enter image description here

2
  • Thank you very much for the answer and explanation! Never encountered this "double hash doubling" issue before. Usually, "single hash doubling" was sufficient for all my previous macro stacking :-) To be fair, I never tried mixing pgfkeys and .code handlers into the stacked macros before. After reading your explanation, I am quite surprised that my code worked (although being partially) in the first place... Commented 16 hours ago
  • @dsacre well TeX turns every ## in a definition into #, and that for every definition until it's one hastag followed by a number. So gor every nesting level you'll need to double the hashes. Commented 14 hours ago

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.